import { dtoFields, eventDefaultOf, eventFields, knownValues, querySchema, requestFieldsOf, requestReplyReplySchema, requestReplySchema, requestReplySourceSchema, resolveEventTarget, typedFields, type EventTarget, type ResolvedField, } from '../db/schema.ts'; import type { Store } from '../db/store.ts'; import type { Connection } from '../protocol/connection.ts'; import { isNestedType, metaDefinitions, metaType, nestedClasses, nestedClassOf, } from '../protocol/dtoTypes.ts'; import { isLegacyFidelity } from '../protocol/fidelity.ts'; import { enumClassOf, eventMetaTypeOf, jsonTypeOf, jvmClassOf, rangeOf, titleCase, } from '../protocol/fieldTypes.ts'; import { eventReplyMeta } from '../protocol/gsfSchemas.ts'; import { ERROR_CODE, MESSAGE_TYPE } from '../protocol/messageTypes.ts'; import { resourceNotMappedNack } from '../protocol/msgNack.ts'; import type { FieldMetadata, GenesisMessage, MockServerConfig, QueryDef, RequestReplyDef, Row, TypedRequestReply, } from '../types.ts'; import { hiddenFields, requestingUser } from './resourceAuth.ts'; // createMetaRequestMessage (foundation-comms/src/connect/message.ts) puts the // resource name in DETAILS.FEATURE — not DATASOURCE_NAME/RESOURCE_NAME despite // what every other message type in this protocol would suggest. function resolveResourceName(message: GenesisMessage): string { return ( message.DETAILS?.FEATURE ?? message.DETAILS?.DATASOURCE_NAME ?? message.DETAILS?.RESOURCE_NAME ); } // A FIELD / REQUEST_FIELD / REPLY_FIELD entry: VALID_VALUES on ENUM fields // only (DataServerMetaData.kt, ReqRepMetaData.kt). function fieldMetadata(field: ResolvedField): FieldMetadata { return { NAME: field.name, TYPE: field.type, ...(field.type === 'ENUM' && knownValues(field) ? { VALID_VALUES: knownValues(field) } : {}), }; } // Index-less queries report the unnamed RECORD_ID index the real dataserver // adds (QueryDefinition.DEFAULT_RECORD_ID_INDEX). const DEFAULT_INDEXES = [{ NAME: null, FIELDS: 'RECORD_ID' }]; function dataserverMeta( name: string, query: QueryDef, config: MockServerConfig, store: Store, hidden: Set, ): Row { const fields = querySchema(query, config, store).fields.filter( (field) => !hidden.has(field.name), ); return { TYPE: 'DATASERVER', NAME: name, FIELD: fields.map(fieldMetadata), ...(query.legacyEngine ? {} : { INDEXES: query.indexes?.length ? query.indexes.map((index) => ({ NAME: index.name, FIELDS: index.fields.join(' ') })) : DEFAULT_INDEXES, }), }; } // The two request-server shapes of ReqRepMetaData.kt: criteria-only // (REQUEST_FIELD: [], SORTABLE_FIELDS, CRITERIA_FIELDS, no INDICES) and plain // (REQUEST_FIELD from the request block, INDICES: [], // CRITERIA_ONLY_REQUEST: false). REPLY_FIELD is the reply block, else the // source's fields then RECORD_ID and TIMESTAMP, for either // (requestReplyReplySchema). SORTABLE_FIELDS and CRITERIA_FIELDS are the // entity's fields whatever the reply block says (the dictionary entry's // nativeQueryFields, RequestReplyServer.kt). function requestServerMeta( name: string, requestReply: RequestReplyDef, config: MockServerConfig, store: Store, hidden: Set, ): Row { const visible = (field: ResolvedField) => !hidden.has(field.name); const replyFields = requestReplyReplySchema(requestReply, config, store).fields.filter(visible); const entityFields = requestReplySourceSchema(requestReply, config, store).fields; if (requestReply.criteriaOnly) { const names = entityFields.filter(visible).map((field) => field.name); return { TYPE: 'REQUEST_SERVER', REQUEST_FIELD: [], CRITERIA_ONLY_REQUEST: true, SORTABLE_FIELDS: names, CRITERIA_FIELDS: names, REPLY_FIELD: replyFields.map(fieldMetadata), NAME: name, }; } const requestFields = requestFieldsOf(requestReply, store).map( (fieldName) => entityFields.find((field) => field.name === fieldName) ?? { name: fieldName, type: 'STRING', nullable: false, generated: false, }, ); return { TYPE: 'REQUEST_SERVER', REQUEST_FIELD: requestFields.map(fieldMetadata), INDICES: [], CRITERIA_ONLY_REQUEST: false, REPLY_FIELD: replyFields.map(fieldMetadata), NAME: name, }; } // One FIELD entry of an event's META (EventMeta's MetaField, flattened as the // real server serializes it). Only a table event's class (a generated DAO) // carries the dictionary's annotations: TITLE, MIN_LENGTH / MAX_LENGTH, // MIN / MAX and GENESIS_TYPE. A delete's .ById and a custom DTO have // none of them (GENESIS_TYPE null). An ENUM always names its values, its // class and a DEFAULT_VALUE (the class's first value when the field has no // default); DEFAULT appears only when the field has a default. export function eventMetaField( field: ResolvedField, { annotated: table, appName }: { annotated: boolean; appName?: string }, ): Row { const type = field.type; // A table's DAO, or a DAO the DTO unwraps: its annotations. const annotated = table || field.dao === true; // A list, set, map, nested class or Any: no default, no annotations. if (isNestedType(type)) { return { NAME: field.name, DESCRIPTION: nestedClassOf(field), ...metaType(field), OPTIONAL: field.optional ?? false, READ_ONLY: false, GENESIS_TYPE: null, }; } const enumClass = () => field.enumClass ?? enumClassOf(field.table, field.name, appName); const values = knownValues(field); const fieldDefault = eventDefaultOf(field); const meta: Row = { NAME: field.name }; if (annotated) meta.TITLE = field.title ?? titleCase(field.name); meta.DESCRIPTION = field.description ?? jvmClassOf(type, enumClass); meta.NULLABLE = field.nullable; if (annotated && type === 'STRING') { if (field.minLength !== undefined) meta.MIN_LENGTH = field.minLength; if (field.maxSize !== undefined) meta.MAX_LENGTH = field.maxSize; } const range = annotated ? rangeOf(field) : undefined; if (range) [meta.MIN, meta.MAX] = range; if (type === 'ENUM') { // The enum class's own default: the declared one, else its first value. if (values) meta.VALID_VALUES = values; const typeDefault = field.default ?? values?.[0]; if (typeDefault !== undefined) meta.DEFAULT_VALUE = typeDefault; meta.ENUM_CLASS = enumClass(); } meta.TYPE = eventMetaTypeOf(type); meta.JSON_TYPE = jsonTypeOf(type); if (type !== 'ENUM' && field.default !== undefined) meta.DEFAULT_VALUE = field.default; // OPTIONAL is about the payload, resolved by eventFields (schema.ts). meta.OPTIONAL = field.optional ?? false; meta.READ_ONLY = false; if (fieldDefault !== undefined) meta.DEFAULT = fieldDefault; meta.GENESIS_TYPE = annotated ? type : null; return meta; } // A typed request server's META (CustomReqRep's buildMetaData, GSF // v8.15.29): the request class's fields in DETAILS.FIELD and the reply // class's in DETAILS.REPLY, each described as an event DTO's field is, and // the criteria keys ReqRepMeta has had since GSF 8.14 (ReqRepMeta.kt:41-43), // sent null. function typedRequestServerMeta( name: string, typed: TypedRequestReply, appName: string | undefined, ): Row { const describe = ({ className, fields }: TypedRequestReply['request']) => { const resolved = typedFields(fields); return { DESCRIPTION: className, FIELD: resolved.map((field) => eventMetaField(field, { annotated: false, appName })), DEFINITIONS: metaDefinitions(nestedClasses(resolved), (nested) => dtoFields(nested.fields).map((field) => eventMetaField(field, { annotated: false, appName }), ), ), }; }; return { NAME: name, ...describe(typed.request), REPLY: { NAME: `REP_${name}`, ...describe(typed.reply) }, CRITERIA_ONLY_REQUEST: false, SORTABLE_FIELDS: null, CRITERIA_FIELDS: null, TYPE: 'REQUEST_SERVER', }; } function eventMeta( name: string, target: EventTarget, fields: ResolvedField[], appName: string | undefined, ): Row { const annotated = target.kind === 'table'; return { NAME: name, ...(target.title ? { TITLE: target.title } : {}), DESCRIPTION: target.className, FIELD: fields.map((field) => eventMetaField(field, { annotated, appName })), DEFINITIONS: metaDefinitions(nestedClasses(fields), (nested) => dtoFields(nested.fields, target.table).map((field) => eventMetaField(field, { annotated: false, appName }), ), ), REPLY: eventReplyMeta(), REQUIRES_APPROVAL: false, SCHEDULABLE: false, TYPE: 'EVENT_HANDLER', }; } // Connect.getMetadata reads metaResponse.DETAILS (not the top-level message) // and extractFieldDefinitions (metadata.utils.ts) branches on DETAILS.TYPE: // DATASERVER resources need DETAILS.FIELD, REQUEST_SERVER ones need // DETAILS.REPLY_FIELD. Get either wrong and every grid silently reports // "Metadata Fetch failed" / "Unsupported resource type: undefined". Event // META is what the AI assistant builds its write tools from // (ai-assistant/src/genesis/event-fields.ts): no event META, no write tools. // Request servers answer by resource name (TRADE); REQ_TRADE is not a // resource name and gets the router's 404, as on the real server. export function handleMetaRequest( message: GenesisMessage, connection: Connection, config: MockServerConfig, store: Store, ): void { const resourceName = resolveResourceName(message); const query = config.queries?.[resourceName]; const requestReply = config.requestReplies?.[resourceName]; const legacy = isLegacyFidelity(config); const event = query || requestReply || legacy ? undefined : resolveEventTarget(resourceName, config); if (!query && !requestReply && !event) { connection.reply( message, legacy ? { MESSAGE_TYPE: message.MESSAGE_TYPE, ERROR: [ { CODE: ERROR_CODE.UNKNOWN_RESOURCE, TEXT: `Unknown resource: ${resourceName}` }, ], } : resourceNotMappedNack(resourceName), ); return; } if (legacy) { connection.reply(message, { MESSAGE_TYPE: message.MESSAGE_TYPE, DETAILS: legacyMeta(resourceName, query, requestReply, connection, config, store), }); return; } let details: Row; if (event) { details = eventMeta(resourceName, event, eventFields(event, config, store), config.appName); } else { // Hidden fields disappear from the metadata too — a column the user can't // see must not be offered to the grid at all. const hidden = hiddenFields((query ?? requestReply)!.auth, requestingUser(connection)); if (query) details = dataserverMeta(resourceName, query, config, store, hidden); else if (requestReply!.typed) { const typed = requestReply!.typed; details = typed.meta ? structuredClone(typed.meta) : typedRequestServerMeta(resourceName, typed, config.appName); } else details = requestServerMeta(resourceName, requestReply!, config, store, hidden); } connection.reply(message, { MESSAGE_TYPE: MESSAGE_TYPE.FEATURE_ACK, DETAILS: details }); } // What this engine answered up to 15.47: {NAME, TYPE} fields only, KEY: [] // on dataservers, REPLY_FIELD alone on request servers, and no event META. // Declared types (and query fields blocks) still apply. function legacyMeta( resourceName: string, query: QueryDef | undefined, requestReply: RequestReplyDef | undefined, connection: Connection, config: MockServerConfig, store: Store, ): Row { const hidden = hiddenFields((query ?? requestReply)!.auth, requestingUser(connection)); const schema = query ? querySchema(query, config, store) : requestReplySchema(requestReply!, config, store); const fields = schema.fields .filter((field) => !hidden.has(field.name)) .map((field) => ({ NAME: field.name, TYPE: field.type })); return query ? { NAME: resourceName, TYPE: 'DATASERVER', FIELD: fields, KEY: [] } : { NAME: resourceName, TYPE: 'REQUEST_SERVER', REPLY_FIELD: fields }; }