import { eventFields, isDeclared, resolveEventTarget, type ResolvedField } from '../db/schema.ts'; import type { Store } from '../db/store.ts'; import { eventValue } from '../db/values.ts'; import type { Connection } from '../protocol/connection.ts'; import { decodedValue } from '../protocol/dtoTypes.ts'; import type { NackErrorInput } from '../protocol/errors.ts'; import { schemaFieldErrors, validateSchema } from '../protocol/schemaValidation.ts'; import type { GenesisMessage, MockServerConfig, Row } from '../types.ts'; import { eventInboundSchema } from './jsonSchema.ts'; // The schema check every event goes through on the real server before its // handler runs (GenesisMessageDecoder, GSF v8.15.29): the whole message // against the event's INBOUND JSON schema — the one JSON_SCHEMA_REQUEST // serves — each failure a FieldError VALIDATION_ERROR (see // protocol/schemaValidation.ts). An empty list means it passed. // // Only an event whose DETAILS class is declared is checked: a table event on // a table with TableDef.fields, or an eventSchemas entry with `fields`. An // inferred schema is a guess from the seed rows (a column every row leaves // empty isn't in it at all), and checking against it would NACK payloads the // real server accepts. export function validateEventMessage( message: GenesisMessage, connection: Connection, config: MockServerConfig, store: Store, ): NackErrorInput[] { const name = message.MESSAGE_TYPE; const declared = declaredEvent(name, config, store); if (!declared) return []; const inbound = eventInboundSchema(name, declared.target, declared.fields, config.appName); // The router stamps the session's user on everything it forwards, so the // schema's required USER_NAME is always there by the time it is checked. const forwarded = { ...message, USER_NAME: connection.userName ?? '' }; return schemaFieldErrors(validateSchema(inbound, forwarded)); } // The event's target and DETAILS fields, when its DETAILS class is declared. function declaredEvent( name: string, config: MockServerConfig, store: Store, ): | { target: NonNullable>; fields: ResolvedField[] } | undefined { const target = resolveEventTarget(name, config); if (!target) return undefined; const declared = target.kind === 'dto' || (target.table !== undefined && isDeclared(target.table, config, store)); return declared ? { target, fields: eventFields(target, config, store) } : undefined; } // DETAILS as the handler gets them, after the schema check: a declared // BigDecimal field's string is a number, as GSF hands its handler a // deserialized BigDecimal (db/values.ts eventValue), and a nested class // loses the keys it doesn't declare, which its schema lets through // (protocol/dtoTypes.ts decodedValue). Anything else, and any undeclared // event's DETAILS, as sent. export function eventDetails(message: GenesisMessage, config: MockServerConfig, store: Store): Row { const details: Row = message.DETAILS ?? {}; const declared = declaredEvent(message.MESSAGE_TYPE, config, store); if (!declared) return details; let converted: Row | undefined; for (const field of declared.fields) { if (!Object.hasOwn(details, field.name)) continue; const value = eventValue(field, decodedValue(field, details[field.name])); if (value === details[field.name]) continue; converted ??= { ...details }; converted[field.name] = value; } return converted ?? details; }