// The field schema of a table, view, query, request server or event: what // META_REQUEST and JSON_SCHEMA_REQUEST describe, and which columns a query's // rows carry. Declared fields (TableDef.fields) win; without them the fields // are inferred from the rows (metadata.ts), as before declared schemas // existed, so a config that declares nothing keeps working. import { isLegacyFidelity } from '../protocol/fidelity.ts'; import { daoClassOf, titleCase } from '../protocol/fieldTypes.ts'; import type { CrudEventVerb, FieldDef, FieldTypeDef, GenesisFieldType, IndexDef, MockServerConfig, QueryDef, RequestReplyDef, Row, TableDef, ViewDef, } from '../types.ts'; import { RECORD_FIELDS, RECORD_ID, TIMESTAMP } from './identity.ts'; import { deriveFields } from './metadata.ts'; import type { Store } from './store.ts'; import { columnScale } from './values.ts'; import { planView } from './views.ts'; export interface ResolvedField { name: string; // A GenesisFieldType, or whatever a fieldTypes override names. type: string; nullable: boolean; // Filled in by the database on insert (sequence / auto-increment). generated: boolean; values?: string[]; // undefined: no default. default?: unknown; maxSize?: number; minLength?: number; min?: number; max?: number; // A BIGDECIMAL's scale: a table column's always (default 5), a derived or // DTO field's only when declared. See db/values.ts. scale?: number; description?: string; title?: string; // FieldDef.optional; for an event field, resolved by eventFields. optional?: boolean; // The table the column comes from (for a view, the joined table for a // join's column), for generated enum class names. table?: string; // A DTO field's nested parts (FieldDef.items, unique, className, fields, // enumClass). items?: FieldTypeDef; unique?: boolean; className?: string; fields?: FieldDef[]; enumClass?: string; // From a DAO a DTO unwraps (FieldDef.dao). dao?: boolean; } export interface ResolvedSchema { fields: ResolvedField[]; // True when the fields come from TableDef.fields rather than from rows. declared: boolean; } export function pkFieldsOf(def: TableDef): string[] { return Array.isArray(def.pkField) ? def.pkField : [def.pkField]; } // The declared generated fields, plus the key of a table that mints its own // keys through sequencePrefix. export function generatedFieldsOf(def: TableDef): Set { return new Set(generatedFieldList(def)); } // The same fields in the order GSF lists a table's generated fields // (EventHandlerUtils.kt generatedFields: the id sequences, then the // auto-increment fields): the sequencePrefix key first, then the declared // SEQUENCE fields, then the AUTO_INCREMENT ones. export function generatedFieldList(def: TableDef): string[] { const pkFields = pkFieldsOf(def); const declared = def.generated ?? []; const fields = [ ...(def.sequencePrefix && pkFields.length === 1 ? [pkFields[0]] : []), ...declared.filter((entry) => entry.kind === 'SEQUENCE').map((entry) => entry.field), ...declared.filter((entry) => entry.kind === 'AUTO_INCREMENT').map((entry) => entry.field), ]; return [...new Set(fields)]; } function fromFieldDef( field: FieldDef | (Omit & { name: string }), table: string | undefined, notNullByDefault: boolean, generated: boolean, ): ResolvedField { return { name: field.name, type: field.type, nullable: field.nullable ?? !notNullByDefault, generated, ...(field.values ? { values: field.values } : {}), ...(field.default !== undefined ? { default: field.default } : {}), ...(field.maxSize !== undefined ? { maxSize: field.maxSize } : {}), ...(field.minLength !== undefined ? { minLength: field.minLength } : {}), ...(field.min !== undefined ? { min: field.min } : {}), ...(field.max !== undefined ? { max: field.max } : {}), ...(field.scale !== undefined ? { scale: field.scale } : {}), ...(field.description !== undefined ? { description: field.description } : {}), ...(field.title !== undefined ? { title: field.title } : {}), ...(field.optional !== undefined ? { optional: field.optional } : {}), ...(table ? { table } : {}), ...(field.items !== undefined ? { items: field.items } : {}), ...(field.unique !== undefined ? { unique: field.unique } : {}), ...(field.className !== undefined ? { className: field.className } : {}), ...(field.fields !== undefined ? { fields: field.fields } : {}), ...(field.enumClass !== undefined ? { enumClass: field.enumClass } : {}), ...(field.dao ? { dao: true } : {}), }; } function declaredTableFields(tableName: string, def: TableDef): ResolvedField[] { const pkFields = pkFieldsOf(def); const generated = generatedFieldsOf(def); return (def.fields ?? []).map((field) => { const resolved = fromFieldDef( field, tableName, pkFields.includes(field.name) || generated.has(field.name), generated.has(field.name), ); const scale = columnScale(field); return scale === undefined ? resolved : { ...resolved, scale }; }); } // Inference from rows: a type per column (metadata.ts), nothing nullable but // the key, no defaults or enum values (rows can't say). function inferredFields( rows: Row[], pkFields: string[], generated: Set, table: string | undefined, ): ResolvedField[] { return deriveFields(rows).map(({ NAME, TYPE }) => ({ name: NAME, type: TYPE, nullable: !(pkFields.includes(NAME) || generated.has(NAME)), generated: generated.has(NAME), ...(table ? { table } : {}), })); } function derivedTypeDef( name: string, entry: NonNullable[string], ): Omit & { name: string } { return typeof entry === 'string' ? { name, type: entry } : { ...entry, name }; } function viewOf(name: string, config: MockServerConfig, store: Store): ViewDef | undefined { return store.views.get(name) ?? config.views?.[name]; } // A view over a declared base table: the base table's columns and its // `select` aliases, each join's selected columns typed from the joined table // (at any depth), and the derived fields typed from derivedTypes (or // inferred from the view's rows when not given) — narrowed to the view's // fields block when it has one. A joined column keeps its own nullability // only when its join is reached from the base through INNER joins alone; // any other may find no match, so it is nullable (GSF's // transformOutputFields). function declaredViewFields( viewName: string, view: ViewDef, config: MockServerConfig, store: Store, ): ResolvedField[] { const plan = planView(viewName, view); const fields = new Map(); const baseFields = resolveSourceSchema(view.base, config, store).fields; for (const field of baseFields) fields.set(field.name, field); const aliased = ( name: string, source: ResolvedField | undefined, nullable: boolean | undefined, ): ResolvedField => { const { title: _title, ...rest } = source ?? { name, type: 'STRING', nullable: true, generated: false, }; return { ...rest, name, nullable: nullable ?? rest.nullable, generated: false }; }; for (const [name, sourceField] of Object.entries(view.select ?? {})) { const source = baseFields.find((field) => field.name === sourceField); fields.set(name, aliased(name, source, undefined)); } for (const join of plan.joins) { const joined = resolveSourceSchema(join.table, config, store).fields; const nullable = join.alwaysMatched ? undefined : true; for (const [name, sourceField] of Object.entries(join.def.select ?? {})) { const source = joined.find((field) => field.name === sourceField); fields.set(name, aliased(name, source, nullable)); } } const derivedNames = Object.keys(view.derived ?? {}); const untyped = derivedNames.filter((name) => !view.derivedTypes?.[name]); const inferred = untyped.length > 0 ? deriveFields( store .getMetadataRows(viewName) .map((row) => Object.fromEntries(untyped.map((name) => [name, row[name]]))), ) : []; for (const name of derivedNames) { const typed = view.derivedTypes?.[name]; fields.set( name, typed ? fromFieldDef(derivedTypeDef(name, typed), undefined, false, false) : { name, type: inferred.find((field) => field.NAME === name)?.TYPE ?? 'STRING', nullable: true, generated: false, }, ); } return view.fields ? pick(fields, view.fields) : [...fields.values()]; } function pick(fields: Map, names: string[]): ResolvedField[] { return names.flatMap((name) => { const field = fields.get(name); return field ? [field] : []; }); } // Whether a table (or a view, through its base table) declares its fields. // Checked without resolving the schema, so it costs nothing for a big // undeclared table. export function isDeclared(source: string, config: MockServerConfig, store: Store): boolean { const table = config.tables?.[source]; if (table) return !!table.fields; const view = viewOf(source, config, store); return view ? isDeclared(view.base, config, store) : false; } // The fields of a table or view. Unknown names resolve to no fields. Under // fidelity: 'legacy' everything is inferred, as up to 15.47: declarations, // fields blocks and derivedTypes are ignored. export function resolveSourceSchema( source: string, config: MockServerConfig, store: Store, ): ResolvedSchema { const legacy = isLegacyFidelity(config); const table = config.tables?.[source]; if (!legacy && table?.fields) { return { fields: declaredTableFields(source, table), declared: true }; } const view = viewOf(source, config, store); if (!legacy && view && isDeclared(view.base, config, store)) { return { fields: declaredViewFields(source, view, config, store), declared: true }; } const generated = table ? generatedFieldsOf(table) : new Set(); let fields = inferredFields( store.getMetadataRows(source), store.getPkFields(source), generated, table ? source : view?.base, ); if (legacy) return { fields, declared: false }; // derivedTypes and the fields block describe a view even when the rest of // it is inferred. if (view?.derivedTypes) { fields = fields.map((field) => { const typed = view.derivedTypes?.[field.name]; return typed ? fromFieldDef(derivedTypeDef(field.name, typed), undefined, false, false) : field; }); } if (view?.fields) fields = pick(new Map(fields.map((field) => [field.name, field])), view.fields); return { fields, declared: false }; } // fieldTypes overrides the TYPE of the named fields, declared or inferred. A // table column made a BIGDECIMAL takes the default scale, as every column of // that type has one (see db/values.ts). export function withTypeOverrides( schema: ResolvedSchema, fieldTypes: Record | undefined, ): ResolvedSchema { if (!fieldTypes) return schema; return { ...schema, fields: schema.fields.map((field) => { const type = fieldTypes[field.name]; if (!type) return field; const scale = field.table ? columnScale({ type, scale: field.scale }) : field.scale; return { ...field, type, ...(scale !== undefined ? { scale } : {}) }; }), }; } // A query's fields block: those fields only, in its order. Names the source // doesn't have are dropped, as the real dataserver drops them. export function projectSchema(schema: ResolvedSchema, names: string[] | undefined): ResolvedSchema { if (!names) return schema; const byName = new Map(schema.fields.map((field) => [field.name, field])); return { ...schema, fields: names.flatMap((name) => { const field = byName.get(name); return field ? [field] : []; }), }; } export function querySchema( query: QueryDef, config: MockServerConfig, store: Store, ): ResolvedSchema { return projectSchema( withTypeOverrides(resolveSourceSchema(query.source, config, store), query.fieldTypes), isLegacyFidelity(config) ? undefined : query.fields, ); } // The Genesis type of each field a query's criteria may read, where it is // known — what GSF compiles a criteria against (criteria.ts): every column of // a declared source, else only the fields its fieldTypes, or its view's // derivedTypes, name. A type inferred from the rows is only a guess. export function queryCriteriaTypes( query: QueryDef, config: MockServerConfig, store: Store, ): Map { if (isDeclared(query.source, config, store)) { return new Map( querySchema(query, config, store).fields.map((field) => [field.name, field.type]), ); } const types = new Map(); const derived = viewOf(query.source, config, store)?.derivedTypes ?? {}; for (const [name, entry] of Object.entries(derived)) { types.set(name, typeof entry === 'string' ? entry : entry.type); } for (const [name, type] of Object.entries(query.fieldTypes ?? {})) types.set(name, type); return types; } // A resolver-only request server has no table: its fields come from the // def's metadataRows sample. export function requestReplySchema( requestReply: RequestReplyDef, config: MockServerConfig, store: Store, ): ResolvedSchema { const schema = requestReply.source ? resolveSourceSchema(requestReply.source, config, store) : { fields: resolverFields(requestReply, isLegacyFidelity(config)), declared: false }; return withTypeOverrides(schema, requestReply.fieldTypes); } // A resolver's fields: its metadataRows', then any fieldTypes name they // don't have (typed by withTypeOverrides). fidelity: 'legacy' lists the // metadataRows' alone, as 15.47 did. function resolverFields(requestReply: RequestReplyDef, legacy: boolean): ResolvedField[] { const fields = inferredFields(requestReply.metadataRows ?? [], [], new Set(), undefined); if (legacy) return fields; const named = new Set(fields.map((field) => field.name)); const declared = Object.keys(requestReply.fieldTypes ?? {}).filter((name) => !named.has(name)); return [ ...fields, ...declared.map((name) => ({ name, type: 'STRING', nullable: true, generated: false })), ]; } // The record identity every table entity carries after its own columns: what // a request server with no reply block sends in each row and lists in // REPLY_FIELD, and what a criteria-only one can sort and filter by. export const RECORD_IDENTITY_FIELDS: ResolvedField[] = [ { name: RECORD_ID, type: 'LONG', nullable: false, generated: true }, { name: TIMESTAMP, type: 'NANO_TIMESTAMP', nullable: false, generated: true }, ]; const withIdentity = (fields: ResolvedField[]): ResolvedField[] => [ ...fields.filter((field) => !RECORD_FIELDS.includes(field.name)), ...RECORD_IDENTITY_FIELDS, ]; // What a request server's database entity has: its source's fields, then // RECORD_ID and TIMESTAMP (a resolver's: its metadataRows). A criteria-only // server sorts and filters by these (SORTABLE_FIELDS, CRITERIA_FIELDS), and // REQUEST may name any of them, whatever the reply block says. export function requestReplySourceSchema( requestReply: RequestReplyDef, config: MockServerConfig, store: Store, ): ResolvedSchema { const schema = requestReplySchema(requestReply, config, store); return requestReply.source && !requestReply.resolver ? { ...schema, fields: withIdentity(schema.fields) } : schema; } // What a request server's REP_ rows carry (getReplyFields in // RequestReplyServer.kt, GSF v8.15.29): the reply block (`fields`), else the // source's fields followed by RECORD_ID and TIMESTAMP — a table entity's own // field list, which the real server sends whether or not the server is // criteria-only (auth's RIGHT sends no RECORD_ID only because its reply block // names CODE and DESCRIPTION). REPLY_FIELD and the JSON schema's REPLY come // from here. A resolver's fields are its metadataRows'; under fidelity: // 'legacy' everything is the source schema, as up to 15.47. export function requestReplyReplySchema( requestReply: RequestReplyDef, config: MockServerConfig, store: Store, ): ResolvedSchema { if (isLegacyFidelity(config)) return requestReplySchema(requestReply, config, store); const schema = requestReplySourceSchema(requestReply, config, store); if (!requestReply.fields || !requestReply.source || requestReply.resolver) return schema; return projectSchema(schema, requestReply.fields); } // The columns a source-backed request server's REP_ rows carry, in order (see // requestReplyReplySchema). undefined — rows as stored, which already end // with their RECORD_ID and TIMESTAMP — for an undeclared source with no reply // block, for a resolver, and under fidelity: 'legacy'. export function requestReplyFieldNames( requestReply: RequestReplyDef, config: MockServerConfig, store: Store, ): string[] | undefined { if (isLegacyFidelity(config) || !requestReply.source || requestReply.resolver) return undefined; const declared = sourceFieldNames(requestReply.source, config, store); const names = declared && [ ...declared.filter((name) => !RECORD_FIELDS.includes(name)), ...RECORD_FIELDS, ]; if (!requestReply.fields) return names; return names ? requestReply.fields.filter((name) => names.includes(name)) : requestReply.fields; } // The request block's fields: requestFields, else the source's key. export function requestFieldsOf(requestReply: RequestReplyDef, store: Store): string[] { if (requestReply.requestFields) return requestReply.requestFields; return requestReply.source ? store.getPkFields(requestReply.source) : []; } // An ENUM's values, or undefined when none are known: a client must then be // told nothing rather than an empty list (grid-pro builds an empty select from // VALID_VALUES: [], and a JSON schema `enum: []` accepts no value at all). export function knownValues(field: ResolvedField): string[] | undefined { return field.values?.length ? field.values : undefined; } // The DEFAULT an event field reports (META DEFAULT, JSON schema `default`). // Only an optional property has one, and it is its type's default // (PALMetadataUtils.kt:159, getPropertyDefaultValue, GSF v8.15.29): the // dictionary default, or for an ENUM with none the enum's first value // (PALMetadataUtils.kt:205, EnumType defaultValue = ... ?: jsonValues.firstOrNull()). export function eventDefaultOf(field: ResolvedField): unknown { if (!field.optional) return undefined; if (field.default !== undefined) return field.default; return field.type === 'ENUM' ? knownValues(field)?.[0] : undefined; } // --- query rows -------------------------------------------------------------- function sourceFieldNames( source: string, config: MockServerConfig, store: Store, ): string[] | undefined { const table = config.tables?.[source]; if (table) return table.fields?.map((field) => field.name); const view = viewOf(source, config, store); if (!view) return undefined; const base = sourceFieldNames(view.base, config, store); // An undeclared base: the view's own fields block is all that is known. if (!base) return view.fields; const names = new Set(base); for (const alias of Object.keys(view.select ?? {})) names.add(alias); for (const join of view.joins ?? []) { for (const alias of Object.keys(join.select ?? {})) names.add(alias); } for (const name of Object.keys(view.derived ?? {})) names.add(name); return view.fields ? view.fields.filter((name) => names.has(name)) : [...names]; } // The columns a query's rows carry, in order: its fields block, else every // declared column of its source (a view's own fields block included). // undefined when neither is known, and always under fidelity: 'legacy': rows // go out as stored. export function queryFieldNames( query: QueryDef | undefined, config: MockServerConfig, store: Store, ): string[] | undefined { if (!query || isLegacyFidelity(config)) return undefined; const names = sourceFieldNames(query.source, config, store); if (!query.fields) return names; return names ? query.fields.filter((name) => names.includes(name)) : query.fields; } // A row with exactly these columns. The real dataserver sends every column it // exposes, null when unset, so a missing value goes out as null. export function projectRow(row: Row, names: string[] | undefined): Row { if (!names) return row; return Object.fromEntries(names.map((name) => [name, row[name] ?? null])); } // --- indexes ----------------------------------------------------------------- // The name the dictionary gives an index declared without one // (DslFieldContext.kt generateIndexName, GSF v8.15.29): '_BY_' and the // fields, each without a leading '
_', joined with '_' — so // primaryKey("COUNTERPARTY_ID") is COUNTERPARTY_BY_ID and // unique("INSTRUMENT_ID") on POSITION is POSITION_BY_INSTRUMENT_ID. export function defaultIndexName(table: string, fields: string[]): string { const prefix = `${table}_`; const names = fields.map( (field) => (field.startsWith(prefix) && field.slice(prefix.length)) || field, ); return `${prefix}BY_${names.join('_')}`; } const sameFields = (a: string[], b: string[]) => a.length === b.length && a.every((field, index) => field === b[index]); // The primary key's index name: a declared unique index over exactly the key // fields names it, else the dictionary default. NACKs name it in FIELD. export function primaryKeyIndexName(table: string, def: TableDef): string { const pkFields = pkFieldsOf(def); const declared = def.indexes?.find((index) => index.unique && sameFields(index.fields, pkFields)); return declared?.name ?? defaultIndexName(table, pkFields); } // The table's other unique indexes, each a constraint an insert or modify // must not break. export function secondaryUniqueIndexes(def: TableDef): IndexDef[] { const pkFields = pkFieldsOf(def); return (def.indexes ?? []).filter((index) => index.unique && !sameFields(index.fields, pkFields)); } // --- events ------------------------------------------------------------------ export const CRUD_EVENT_VERBS: readonly CrudEventVerb[] = ['INSERT', 'MODIFY', 'DELETE']; // A generic event's name: Create names the handler '
_' // (as GSF's GenericInsertEventHandler.messageType() does too) and the event // handler serves it as EVENT_
_. export function crudEventName(table: string, verb: CrudEventVerb): string { return `EVENT_${table}_${verb}`; } // The table and verb of a generic CRUD event a table registers through // TableDef.events, or undefined. export function crudEventOf( name: string | undefined, config: MockServerConfig, ): { table: string; verb: CrudEventVerb } | undefined { if (!name?.startsWith('EVENT_')) return undefined; for (const [table, def] of Object.entries(config.tables ?? {})) { for (const verb of def.events ?? []) { if (crudEventName(table, verb) === name) return { table, verb }; } } return undefined; } // Every generic CRUD event the tables register, in table then verb order. export function crudEventNames(config: MockServerConfig): string[] { return Object.entries(config.tables ?? {}).flatMap(([table, def]) => CRUD_EVENT_VERBS.filter((verb) => def.events?.includes(verb)).map((verb) => crudEventName(table, verb), ), ); } // Any event named .../_DELETE(_...)? takes only the key fields, which is what // entity-management's delete confirmation copies from the selected row. export function isDeleteEventName(name: string): boolean { return /_DELETE(_|$)/.test(name); } // Zero-config default: EVENT_
_ maps to
if it's a // registered table name (longest match wins, so e.g. a hypothetical // COUNTERPARTY_AUTH table can't shadow COUNTERPARTY). Covers every event // naming pattern in this codebase (INSERT, MODIFY, DELETE, plus suffixed // variants like INSERT_WITH_APPROVAL, DEMO_INSERT, MODIFY_PRICE, INSERT_FAIL) // without hand-maintaining a table per event. Override via // config.eventSchemas for names that don't follow this convention. export function inferEventTable( eventName: string | undefined, tableNames: string[], ): string | undefined { if (!eventName) return undefined; // Not every caller asks by event name: entity-management resolves a grid's // schema from the bare resource/table name (FEATURE: 'TRADE'), so accept an // exact table match before requiring the EVENT_ prefix. Without this the // request errors with UNKNOWN_RESOURCE and the form renders no fields. if (tableNames.includes(eventName)) return eventName; if (!eventName.startsWith('EVENT_')) return undefined; const rest = eventName.slice('EVENT_'.length); const candidates = tableNames.filter((name) => rest === name || rest.startsWith(`${name}_`)); candidates.sort((a, b) => b.length - a.length); return candidates[0]; } export interface EventTarget { // How the real server describes the DETAILS class: 'table', a generated // DAO (titles, lengths, ranges, genesis types; fields sorted by name); // 'byId', a delete's
.ById (the key fields only, all mandatory, no // annotations); 'dto', a custom class (EventSchemaOverride.fields, no // annotations). kind: 'table' | 'byId' | 'dto'; // The table (or view) the DETAILS take. For a 'dto', where its ENUM // columns come from, if anywhere. table?: string; // DESCRIPTION: the DETAILS class name. className: string; // TITLE, on 'table' events only. title?: string; fieldTypes?: Record; fields?: FieldDef[]; } // The table an event writes: config.eventSchemas first, then the table that // registers it as a generic CRUD event, then the EVENT_
_ // convention. Only a registered event (an eventHandlers or eventSchemas // entry, or a TableDef.events verb) has metadata, as only a deployed event // handler has on the real server; anything else is the router's 404. // `bareTable` also accepts a plain table name, which only JSON_SCHEMA_REQUEST // does (see the README). export function resolveEventTarget( name: string | undefined, config: MockServerConfig, { bareTable = false }: { bareTable?: boolean } = {}, ): EventTarget | undefined { if (!name) return undefined; const override = config.eventSchemas?.[name]; const crud = crudEventOf(name, config); if (name.startsWith('EVENT_') && !override && !config.eventHandlers?.[name] && !crud) { return undefined; } const tableNames = Object.keys(config.tables ?? {}); const table = override?.table ?? crud?.table ?? (bareTable || name.startsWith('EVENT_') ? inferEventTable(name, tableNames) : undefined); const fieldTypes = override?.fieldTypes ? { fieldTypes: override.fieldTypes } : {}; if (override?.fields) { return { kind: 'dto', ...(table ? { table } : {}), className: override.className ?? daoClassOf(table ?? name.replace(/^EVENT_/, '')), fields: override.fields, ...fieldTypes, }; } if (!table) return undefined; const byId = override?.pkOnly ?? isDeleteEventName(name); return byId ? { kind: 'byId', table, className: override?.className ?? `${daoClassOf(table)}.ById`, ...fieldTypes, } : { kind: 'table', table, className: override?.className ?? daoClassOf(table), title: titleCase(table), ...fieldTypes, }; } // Kotlin reflection lists a class's properties sorted by their (camelCase) // names, and the real server only then turns them into UPPER_UNDERSCORE — // so DATE_FIELD (dateField) sorts before DATETIME_FIELD (datetimeField), // which a plain sort of the field names would reverse. function propertyName(field: ResolvedField): string { return field.name .toLowerCase() .replace(/_+([a-z0-9])/g, (_match, char: string) => char.toUpperCase()); } const byName = (a: ResolvedField, b: ResolvedField) => { const [left, right] = [propertyName(a), propertyName(b)]; return left < right ? -1 : left > right ? 1 : 0; }; // A custom class's fields, as a DTO event's DETAILS or a class nested in one: // sorted by name, each optional only when it declares a default or // `optional: true` (a DTO's Kotlin defaults are invisible). // A typed request server's class fields (TypedRequestReply), in the order // they're listed rather than by name. export function typedFields(fields: FieldDef[]): ResolvedField[] { const resolved = new Map(dtoFields(fields).map((field) => [field.name, field])); return fields.map(({ name }) => resolved.get(name)!); } export function dtoFields( fields: FieldDef[], table?: string, fieldTypes?: Record, ): ResolvedField[] { const resolved = fields.map((field) => fromFieldDef(field, table, false, false)); return withTypeOverrides({ fields: resolved, declared: true }, fieldTypes) .fields.map((field) => ({ ...field, optional: field.optional ?? field.default !== undefined, })) .sort(byName); } // The fields an event's DETAILS take, sorted by name as the real server // lists a class's properties — except a delete's, which are the key fields // in key order, all mandatory. A key field the schema doesn't know (an // inferred table with no rows) is still listed, as a STRING. `optional` is // always resolved: FieldDef.optional when given, else whether the class // property has a default (PALMetadataUtils.kt mapMemberProperty: optional = // propertyType != Mandatory). A generated DAO gives every nullable and every // generated column a default; a custom DTO's defaults are invisible, so a // DTO field is optional only when it declares a default or `optional: true`. export function eventFields( target: EventTarget, config: MockServerConfig, store: Store, ): ResolvedField[] { if (target.kind === 'dto') { return dtoFields(target.fields ?? [], target.table, target.fieldTypes); } const table = target.table!; const { fields } = withTypeOverrides( resolveSourceSchema(table, config, store), target.fieldTypes, ); if (target.kind === 'byId') { return store.getPkFields(table).map((name) => { const { default: _default, ...field } = fields.find( (candidate) => candidate.name === name, ) ?? { name, type: 'STRING' satisfies GenesisFieldType, nullable: false, generated: false, }; return { ...field, nullable: false, generated: false, optional: false }; }); } return fields .map((field) => ({ ...field, optional: field.optional ?? (field.generated || field.nullable || field.default !== undefined), })) .sort(byName); }