@proofkit/fmodata 0.1.0-alpha.19 → 0.1.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -0
- package/dist/esm/client/builders/default-select.d.ts +4 -1
- package/dist/esm/client/builders/default-select.js +3 -2
- package/dist/esm/client/builders/default-select.js.map +1 -1
- package/dist/esm/client/builders/expand-builder.js.map +1 -1
- package/dist/esm/client/builders/query-string-builder.d.ts +1 -0
- package/dist/esm/client/builders/query-string-builder.js.map +1 -1
- package/dist/esm/client/builders/response-processor.d.ts +2 -0
- package/dist/esm/client/builders/response-processor.js +8 -3
- package/dist/esm/client/builders/response-processor.js.map +1 -1
- package/dist/esm/client/database.d.ts +27 -4
- package/dist/esm/client/database.js +17 -4
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/delete-builder.d.ts +2 -0
- package/dist/esm/client/delete-builder.js +2 -0
- package/dist/esm/client/delete-builder.js.map +1 -1
- package/dist/esm/client/entity-set.d.ts +8 -7
- package/dist/esm/client/entity-set.js +21 -26
- package/dist/esm/client/entity-set.js.map +1 -1
- package/dist/esm/client/error-parser.js.map +1 -1
- package/dist/esm/client/filemaker-odata.d.ts +15 -2
- package/dist/esm/client/filemaker-odata.js +25 -2
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/dist/esm/client/insert-builder.d.ts +2 -0
- package/dist/esm/client/insert-builder.js +2 -0
- package/dist/esm/client/insert-builder.js.map +1 -1
- package/dist/esm/client/query/query-builder.d.ts +26 -15
- package/dist/esm/client/query/query-builder.js +43 -9
- package/dist/esm/client/query/query-builder.js.map +1 -1
- package/dist/esm/client/query/response-processor.d.ts +1 -0
- package/dist/esm/client/query/types.d.ts +24 -3
- package/dist/esm/client/record-builder.d.ts +24 -13
- package/dist/esm/client/record-builder.js +50 -17
- package/dist/esm/client/record-builder.js.map +1 -1
- package/dist/esm/client/update-builder.d.ts +2 -0
- package/dist/esm/client/update-builder.js +2 -0
- package/dist/esm/client/update-builder.js.map +1 -1
- package/dist/esm/client/webhook-builder.d.ts +126 -0
- package/dist/esm/client/webhook-builder.js +197 -0
- package/dist/esm/client/webhook-builder.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/orm/field-builders.d.ts +12 -2
- package/dist/esm/orm/field-builders.js +18 -2
- package/dist/esm/orm/field-builders.js.map +1 -1
- package/dist/esm/orm/table.d.ts +23 -10
- package/dist/esm/orm/table.js +17 -30
- package/dist/esm/orm/table.js.map +1 -1
- package/dist/esm/types.d.ts +32 -2
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/validation.d.ts +5 -5
- package/dist/esm/validation.js +44 -13
- package/dist/esm/validation.js.map +1 -1
- package/package.json +2 -2
- package/src/client/builders/default-select.ts +12 -1
- package/src/client/builders/expand-builder.ts +1 -1
- package/src/client/builders/query-string-builder.ts +6 -0
- package/src/client/builders/response-processor.ts +10 -0
- package/src/client/database.ts +54 -12
- package/src/client/delete-builder.ts +5 -1
- package/src/client/entity-set.ts +72 -44
- package/src/client/error-parser.ts +3 -0
- package/src/client/filemaker-odata.ts +39 -6
- package/src/client/insert-builder.ts +4 -0
- package/src/client/query/query-builder.ts +198 -35
- package/src/client/query/response-processor.ts +15 -25
- package/src/client/query/types.ts +35 -6
- package/src/client/record-builder.ts +159 -32
- package/src/client/update-builder.ts +4 -1
- package/src/client/webhook-builder.ts +285 -0
- package/src/index.ts +6 -0
- package/src/orm/field-builders.ts +24 -2
- package/src/orm/table.ts +40 -48
- package/src/types.ts +62 -6
- package/src/validation.ts +58 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.js","sources":["../../src/validation.ts"],"sourcesContent":["import type { ODataRecordMetadata } from \"./types\";\nimport { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { FMTable } from \"./orm/table\";\nimport {\n ValidationError,\n ResponseStructureError,\n RecordCountMismatchError,\n} from \"./errors\";\n\n/**\n * Validates and transforms input data for insert/update operations.\n * Applies input validators (writeValidators) to transform user input to database format.\n * Fields without input validators are passed through unchanged.\n *\n * @param data - The input data to validate and transform\n * @param inputSchema - Optional schema containing input validators for each field\n * @returns Transformed data ready to send to the server\n * @throws ValidationError if any field fails validation\n */\nexport async function validateAndTransformInput<T extends Record<string, any>>(\n data: Partial<T>,\n inputSchema?: Record<string, StandardSchemaV1>,\n): Promise<Partial<T>> {\n // If no input schema, return data as-is\n if (!inputSchema) {\n return data;\n }\n\n const transformedData: Record<string, any> = { ...data };\n\n // Process each field that has an input validator\n for (const [fieldName, fieldSchema] of Object.entries(inputSchema)) {\n // Only process fields that are present in the input data\n if (fieldName in data) {\n const inputValue = data[fieldName];\n\n try {\n // Run the input validator to transform the value\n let result = fieldSchema[\"~standard\"].validate(inputValue);\n if (result instanceof Promise) {\n result = await result;\n }\n\n // Check for validation errors\n if (result.issues) {\n throw new ValidationError(\n `Input validation failed for field '${fieldName}'`,\n result.issues,\n {\n field: fieldName,\n value: inputValue,\n cause: result.issues,\n },\n );\n }\n\n // Store the transformed value\n transformedData[fieldName] = result.value;\n } catch (error) {\n // If it's already a ValidationError, re-throw it\n if (error instanceof ValidationError) {\n throw error;\n }\n\n // Otherwise, wrap the error\n throw new ValidationError(\n `Input validation failed for field '${fieldName}'`,\n [],\n {\n field: fieldName,\n value: inputValue,\n cause: error,\n },\n );\n }\n }\n }\n\n // Fields without input validators are already in transformedData (passed through)\n return transformedData as Partial<T>;\n}\n\n// Type for expand validation configuration\nexport type ExpandValidationConfig = {\n relation: string;\n targetSchema?: Record<string, StandardSchemaV1>;\n targetTable?: FMTable<any, any>;\n table?: FMTable<any, any>; // For transformation\n selectedFields?: string[];\n nestedExpands?: ExpandValidationConfig[];\n};\n\n/**\n * Validates a single record against a schema, only validating selected fields.\n * Also validates expanded relations if expandConfigs are provided.\n */\nexport async function validateRecord<T extends Record<string, any>>(\n record: any,\n schema: Record<string, StandardSchemaV1> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n): Promise<\n | { valid: true; data: T & ODataRecordMetadata }\n | { valid: false; error: ValidationError }\n> {\n // Extract OData metadata fields (don't validate them - include if present)\n const { \"@id\": id, \"@editLink\": editLink, ...rest } = record;\n\n // Only include metadata fields if they actually exist and have values\n const metadata: Partial<ODataRecordMetadata> = {};\n if (id) metadata[\"@id\"] = id;\n if (editLink) metadata[\"@editLink\"] = editLink;\n\n // If no schema, just return the data with metadata\n if (!schema) {\n return {\n valid: true,\n data: { ...rest, ...metadata } as T & ODataRecordMetadata,\n };\n }\n\n // Filter out FileMaker system fields that shouldn't be in responses by default\n const { ROWID, ROWMODID, ...restWithoutSystemFields } = rest;\n\n // If selected fields are specified, validate only those fields\n if (selectedFields && selectedFields.length > 0) {\n const validatedRecord: Record<string, any> = {};\n\n for (const field of selectedFields) {\n const fieldName = String(field);\n const fieldSchema = schema[fieldName];\n\n if (fieldSchema) {\n const input = rest[fieldName];\n try {\n let result = fieldSchema[\"~standard\"].validate(input);\n if (result instanceof Promise) result = await result;\n\n // if the `issues` field exists, the validation failed\n if (result.issues) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n result.issues,\n {\n field: fieldName,\n value: input,\n cause: result.issues,\n },\n ),\n };\n }\n\n validatedRecord[fieldName] = result.value;\n } catch (originalError) {\n // If the validator throws directly, wrap it\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n [],\n {\n field: fieldName,\n value: input,\n cause: originalError,\n },\n ),\n };\n }\n } else {\n // For fields not in schema (like when explicitly selecting ROWID/ROWMODID)\n // include them from the original response\n validatedRecord[fieldName] = rest[fieldName];\n }\n }\n\n // Validate expanded relations\n if (expandConfigs && expandConfigs.length > 0) {\n for (const expandConfig of expandConfigs) {\n const expandValue = rest[expandConfig.relation];\n\n // Check if expand field is missing\n if (expandValue === undefined) {\n // Check for inline error array (FileMaker returns errors inline when expand fails)\n if (Array.isArray(rest.error) && rest.error.length > 0) {\n // Extract error message from inline error\n const errorDetail = rest.error[0]?.error;\n if (errorDetail?.message) {\n const errorMessage = errorDetail.message;\n // Check if the error is related to this expand by checking if:\n // 1. The error mentions the relation name, OR\n // 2. The error mentions any of the selected fields\n const isRelatedToExpand =\n errorMessage\n .toLowerCase()\n .includes(expandConfig.relation.toLowerCase()) ||\n (expandConfig.selectedFields &&\n expandConfig.selectedFields.some((field) =>\n errorMessage.toLowerCase().includes(field.toLowerCase()),\n ));\n\n if (isRelatedToExpand) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,\n [],\n {\n field: expandConfig.relation,\n },\n ),\n };\n }\n }\n }\n // If no inline error but expand was expected, that's also an issue\n // However, this might be a legitimate case (e.g., no related records)\n // So we'll only fail if there's an explicit error array\n } else {\n // Original validation logic for when expand exists\n if (Array.isArray(expandValue)) {\n // Validate each item in the expanded array\n const validatedExpandedItems: any[] = [];\n for (let i = 0; i < expandValue.length; i++) {\n const item = expandValue[i];\n const itemValidation = await validateRecord(\n item,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedExpandedItems.push(itemValidation.data);\n }\n validatedRecord[expandConfig.relation] = validatedExpandedItems;\n } else {\n // Single expanded item (shouldn't happen in OData, but handle it)\n const itemValidation = await validateRecord(\n expandValue,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedRecord[expandConfig.relation] = itemValidation.data;\n }\n }\n }\n }\n\n // Merge validated data with metadata\n return {\n valid: true,\n data: { ...validatedRecord, ...metadata } as T & ODataRecordMetadata,\n };\n }\n\n // Validate all fields in schema, but exclude ROWID/ROWMODID by default\n const validatedRecord: Record<string, any> = { ...restWithoutSystemFields };\n\n for (const [fieldName, fieldSchema] of Object.entries(schema)) {\n const input = rest[fieldName];\n try {\n let result = fieldSchema[\"~standard\"].validate(input);\n if (result instanceof Promise) result = await result;\n\n // if the `issues` field exists, the validation failed\n if (result.issues) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n result.issues,\n {\n field: fieldName,\n value: input,\n cause: result.issues,\n },\n ),\n };\n }\n\n validatedRecord[fieldName] = result.value;\n } catch (originalError) {\n // If the validator throws an error directly, catch and wrap it\n // This preserves the original error instance for instanceof checks\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n [],\n {\n field: fieldName,\n value: input,\n cause: originalError,\n },\n ),\n };\n }\n }\n\n // Validate expanded relations even when not using selected fields\n if (expandConfigs && expandConfigs.length > 0) {\n for (const expandConfig of expandConfigs) {\n const expandValue = rest[expandConfig.relation];\n\n // Check if expand field is missing\n if (expandValue === undefined) {\n // Check for inline error array (FileMaker returns errors inline when expand fails)\n if (Array.isArray(rest.error) && rest.error.length > 0) {\n // Extract error message from inline error\n const errorDetail = rest.error[0]?.error;\n if (errorDetail?.message) {\n const errorMessage = errorDetail.message;\n // Check if the error is related to this expand by checking if:\n // 1. The error mentions the relation name, OR\n // 2. The error mentions any of the selected fields\n const isRelatedToExpand =\n errorMessage\n .toLowerCase()\n .includes(expandConfig.relation.toLowerCase()) ||\n (expandConfig.selectedFields &&\n expandConfig.selectedFields.some((field) =>\n errorMessage.toLowerCase().includes(field.toLowerCase()),\n ));\n\n if (isRelatedToExpand) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,\n [],\n {\n field: expandConfig.relation,\n },\n ),\n };\n }\n }\n }\n // If no inline error but expand was expected, that's also an issue\n // However, this might be a legitimate case (e.g., no related records)\n // So we'll only fail if there's an explicit error array\n } else {\n // Original validation logic for when expand exists\n if (Array.isArray(expandValue)) {\n // Validate each item in the expanded array\n const validatedExpandedItems: any[] = [];\n for (let i = 0; i < expandValue.length; i++) {\n const item = expandValue[i];\n const itemValidation = await validateRecord(\n item,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedExpandedItems.push(itemValidation.data);\n }\n validatedRecord[expandConfig.relation] = validatedExpandedItems;\n } else {\n // Single expanded item (shouldn't happen in OData, but handle it)\n const itemValidation = await validateRecord(\n expandValue,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedRecord[expandConfig.relation] = itemValidation.data;\n }\n }\n }\n }\n\n return {\n valid: true,\n data: { ...validatedRecord, ...metadata } as T & ODataRecordMetadata,\n };\n}\n\n/**\n * Validates a list response against a schema.\n */\nexport async function validateListResponse<T extends Record<string, any>>(\n response: any,\n schema: Record<string, StandardSchemaV1> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n): Promise<\n | { valid: true; data: (T & ODataRecordMetadata)[] }\n | { valid: false; error: ResponseStructureError | ValidationError }\n> {\n // Check if response has the expected structure\n if (!response || typeof response !== \"object\") {\n return {\n valid: false,\n error: new ResponseStructureError(\"an object\", response),\n };\n }\n\n // Extract @context (for internal validation, but we won't return it)\n const { \"@context\": context, value, ...rest } = response;\n\n if (!Array.isArray(value)) {\n return {\n valid: false,\n error: new ResponseStructureError(\n \"'value' property to be an array\",\n value,\n ),\n };\n }\n\n // Validate each record in the array\n const validatedRecords: (T & ODataRecordMetadata)[] = [];\n\n for (let i = 0; i < value.length; i++) {\n const record = value[i];\n const validation = await validateRecord<T>(\n record,\n schema,\n selectedFields,\n expandConfigs,\n );\n\n if (!validation.valid) {\n return {\n valid: false,\n error: validation.error,\n };\n }\n\n validatedRecords.push(validation.data);\n }\n\n return {\n valid: true,\n data: validatedRecords,\n };\n}\n\n/**\n * Validates a single record response against a schema.\n */\nexport async function validateSingleResponse<T extends Record<string, any>>(\n response: any,\n schema: Record<string, StandardSchemaV1> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n mode: \"exact\" | \"maybe\" = \"maybe\",\n): Promise<\n | { valid: true; data: (T & ODataRecordMetadata) | null }\n | { valid: false; error: RecordCountMismatchError | ValidationError }\n> {\n // Check for multiple records (error in both modes)\n if (\n response.value &&\n Array.isArray(response.value) &&\n response.value.length > 1\n ) {\n return {\n valid: false,\n error: new RecordCountMismatchError(\n mode === \"exact\" ? \"one\" : \"at-most-one\",\n response.value.length,\n ),\n };\n }\n\n // Handle empty responses\n if (!response || (response.value && response.value.length === 0)) {\n if (mode === \"exact\") {\n return {\n valid: false,\n error: new RecordCountMismatchError(\"one\", 0),\n };\n }\n // mode === \"maybe\" - return null for empty\n return {\n valid: true,\n data: null,\n };\n }\n\n // Single record validation\n const record = response.value?.[0] ?? response;\n const validation = await validateRecord<T>(\n record,\n schema,\n selectedFields,\n expandConfigs,\n );\n\n if (!validation.valid) {\n return validation as { valid: false; error: ValidationError };\n }\n\n return {\n valid: true,\n data: validation.data,\n };\n}\n"],"names":["validatedRecord"],"mappings":";AAmBsB,eAAA,0BACpB,MACA,aACqB;AAErB,MAAI,CAAC,aAAa;AACT,WAAA;AAAA,EAAA;AAGH,QAAA,kBAAuC,EAAE,GAAG,KAAK;AAGvD,aAAW,CAAC,WAAW,WAAW,KAAK,OAAO,QAAQ,WAAW,GAAG;AAElE,QAAI,aAAa,MAAM;AACf,YAAA,aAAa,KAAK,SAAS;AAE7B,UAAA;AAEF,YAAI,SAAS,YAAY,WAAW,EAAE,SAAS,UAAU;AACzD,YAAI,kBAAkB,SAAS;AAC7B,mBAAS,MAAM;AAAA,QAAA;AAIjB,YAAI,OAAO,QAAQ;AACjB,gBAAM,IAAI;AAAA,YACR,sCAAsC,SAAS;AAAA,YAC/C,OAAO;AAAA,YACP;AAAA,cACE,OAAO;AAAA,cACP,OAAO;AAAA,cACP,OAAO,OAAO;AAAA,YAAA;AAAA,UAElB;AAAA,QAAA;AAIc,wBAAA,SAAS,IAAI,OAAO;AAAA,eAC7B,OAAO;AAEd,YAAI,iBAAiB,iBAAiB;AAC9B,gBAAA;AAAA,QAAA;AAIR,cAAM,IAAI;AAAA,UACR,sCAAsC,SAAS;AAAA,UAC/C,CAAC;AAAA,UACD;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,OAAO;AAAA,UAAA;AAAA,QAEX;AAAA,MAAA;AAAA,IACF;AAAA,EACF;AAIK,SAAA;AACT;AAgBA,eAAsB,eACpB,QACA,QACA,gBACA,eAIA;;AAEA,QAAM,EAAE,OAAO,IAAI,aAAa,UAAU,GAAG,SAAS;AAGtD,QAAM,WAAyC,CAAC;AAC5C,MAAA,GAAa,UAAA,KAAK,IAAI;AACtB,MAAA,SAAmB,UAAA,WAAW,IAAI;AAGtC,MAAI,CAAC,QAAQ;AACJ,WAAA;AAAA,MACL,OAAO;AAAA,MACP,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS;AAAA,IAC/B;AAAA,EAAA;AAIF,QAAM,EAAE,OAAO,UAAU,GAAG,wBAA4B,IAAA;AAGpD,MAAA,kBAAkB,eAAe,SAAS,GAAG;AAC/C,UAAMA,mBAAuC,CAAC;AAE9C,eAAW,SAAS,gBAAgB;AAC5B,YAAA,YAAY,OAAO,KAAK;AACxB,YAAA,cAAc,OAAO,SAAS;AAEpC,UAAI,aAAa;AACT,cAAA,QAAQ,KAAK,SAAS;AACxB,YAAA;AACF,cAAI,SAAS,YAAY,WAAW,EAAE,SAAS,KAAK;AAChD,cAAA,kBAAkB,QAAS,UAAS,MAAM;AAG9C,cAAI,OAAO,QAAQ;AACV,mBAAA;AAAA,cACL,OAAO;AAAA,cACP,OAAO,IAAI;AAAA,gBACT,gCAAgC,SAAS;AAAA,gBACzC,OAAO;AAAA,gBACP;AAAA,kBACE,OAAO;AAAA,kBACP,OAAO;AAAA,kBACP,OAAO,OAAO;AAAA,gBAAA;AAAA,cAChB;AAAA,YAEJ;AAAA,UAAA;AAGFA,2BAAgB,SAAS,IAAI,OAAO;AAAA,iBAC7B,eAAe;AAEf,iBAAA;AAAA,YACL,OAAO;AAAA,YACP,OAAO,IAAI;AAAA,cACT,gCAAgC,SAAS;AAAA,cACzC,CAAC;AAAA,cACD;AAAA,gBACE,OAAO;AAAA,gBACP,OAAO;AAAA,gBACP,OAAO;AAAA,cAAA;AAAA,YACT;AAAA,UAEJ;AAAA,QAAA;AAAA,MACF,OACK;AAGLA,yBAAgB,SAAS,IAAI,KAAK,SAAS;AAAA,MAAA;AAAA,IAC7C;AAIE,QAAA,iBAAiB,cAAc,SAAS,GAAG;AAC7C,iBAAW,gBAAgB,eAAe;AAClC,cAAA,cAAc,KAAK,aAAa,QAAQ;AAG9C,YAAI,gBAAgB,QAAW;AAEzB,cAAA,MAAM,QAAQ,KAAK,KAAK,KAAK,KAAK,MAAM,SAAS,GAAG;AAEtD,kBAAM,eAAc,UAAK,MAAM,CAAC,MAAZ,mBAAe;AACnC,gBAAI,2CAAa,SAAS;AACxB,oBAAM,eAAe,YAAY;AAIjC,oBAAM,oBACJ,aACG,YAAY,EACZ,SAAS,aAAa,SAAS,YAAA,CAAa,KAC9C,aAAa,kBACZ,aAAa,eAAe;AAAA,gBAAK,CAAC,UAChC,aAAa,cAAc,SAAS,MAAM,YAAa,CAAA;AAAA,cACzD;AAEJ,kBAAI,mBAAmB;AACd,uBAAA;AAAA,kBACL,OAAO;AAAA,kBACP,OAAO,IAAI;AAAA,oBACT,4CAA4C,aAAa,QAAQ,MAAM,YAAY;AAAA,oBACnF,CAAC;AAAA,oBACD;AAAA,sBACE,OAAO,aAAa;AAAA,oBAAA;AAAA,kBACtB;AAAA,gBAEJ;AAAA,cAAA;AAAA,YACF;AAAA,UACF;AAAA,QACF,OAIK;AAED,cAAA,MAAM,QAAQ,WAAW,GAAG;AAE9B,kBAAM,yBAAgC,CAAC;AACvC,qBAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AACrC,oBAAA,OAAO,YAAY,CAAC;AAC1B,oBAAM,iBAAiB,MAAM;AAAA,gBAC3B;AAAA,gBACA,aAAa;AAAA,gBACb,aAAa;AAAA,gBACb,aAAa;AAAA,cACf;AACI,kBAAA,CAAC,eAAe,OAAO;AAClB,uBAAA;AAAA,kBACL,OAAO;AAAA,kBACP,OAAO,IAAI;AAAA,oBACT,4CAA4C,aAAa,QAAQ,cAAc,CAAC,KAAK,eAAe,MAAM,OAAO;AAAA,oBACjH,eAAe,MAAM;AAAA,oBACrB;AAAA,sBACE,OAAO,aAAa;AAAA,sBACpB,OAAO,eAAe,MAAM;AAAA,oBAAA;AAAA,kBAC9B;AAAA,gBAEJ;AAAA,cAAA;AAEqB,qCAAA,KAAK,eAAe,IAAI;AAAA,YAAA;AAEjDA,6BAAgB,aAAa,QAAQ,IAAI;AAAA,UAAA,OACpC;AAEL,kBAAM,iBAAiB,MAAM;AAAA,cAC3B;AAAA,cACA,aAAa;AAAA,cACb,aAAa;AAAA,cACb,aAAa;AAAA,YACf;AACI,gBAAA,CAAC,eAAe,OAAO;AAClB,qBAAA;AAAA,gBACL,OAAO;AAAA,gBACP,OAAO,IAAI;AAAA,kBACT,4CAA4C,aAAa,QAAQ,MAAM,eAAe,MAAM,OAAO;AAAA,kBACnG,eAAe,MAAM;AAAA,kBACrB;AAAA,oBACE,OAAO,aAAa;AAAA,oBACpB,OAAO,eAAe,MAAM;AAAA,kBAAA;AAAA,gBAC9B;AAAA,cAEJ;AAAA,YAAA;AAEFA,6BAAgB,aAAa,QAAQ,IAAI,eAAe;AAAA,UAAA;AAAA,QAC1D;AAAA,MACF;AAAA,IACF;AAIK,WAAA;AAAA,MACL,OAAO;AAAA,MACP,MAAM,EAAE,GAAGA,kBAAiB,GAAG,SAAS;AAAA,IAC1C;AAAA,EAAA;AAII,QAAA,kBAAuC,EAAE,GAAG,wBAAwB;AAE1E,aAAW,CAAC,WAAW,WAAW,KAAK,OAAO,QAAQ,MAAM,GAAG;AACvD,UAAA,QAAQ,KAAK,SAAS;AACxB,QAAA;AACF,UAAI,SAAS,YAAY,WAAW,EAAE,SAAS,KAAK;AAChD,UAAA,kBAAkB,QAAS,UAAS,MAAM;AAG9C,UAAI,OAAO,QAAQ;AACV,eAAA;AAAA,UACL,OAAO;AAAA,UACP,OAAO,IAAI;AAAA,YACT,gCAAgC,SAAS;AAAA,YACzC,OAAO;AAAA,YACP;AAAA,cACE,OAAO;AAAA,cACP,OAAO;AAAA,cACP,OAAO,OAAO;AAAA,YAAA;AAAA,UAChB;AAAA,QAEJ;AAAA,MAAA;AAGc,sBAAA,SAAS,IAAI,OAAO;AAAA,aAC7B,eAAe;AAGf,aAAA;AAAA,QACL,OAAO;AAAA,QACP,OAAO,IAAI;AAAA,UACT,gCAAgC,SAAS;AAAA,UACzC,CAAC;AAAA,UACD;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,OAAO;AAAA,UAAA;AAAA,QACT;AAAA,MAEJ;AAAA,IAAA;AAAA,EACF;AAIE,MAAA,iBAAiB,cAAc,SAAS,GAAG;AAC7C,eAAW,gBAAgB,eAAe;AAClC,YAAA,cAAc,KAAK,aAAa,QAAQ;AAG9C,UAAI,gBAAgB,QAAW;AAEzB,YAAA,MAAM,QAAQ,KAAK,KAAK,KAAK,KAAK,MAAM,SAAS,GAAG;AAEtD,gBAAM,eAAc,UAAK,MAAM,CAAC,MAAZ,mBAAe;AACnC,cAAI,2CAAa,SAAS;AACxB,kBAAM,eAAe,YAAY;AAIjC,kBAAM,oBACJ,aACG,YAAY,EACZ,SAAS,aAAa,SAAS,YAAA,CAAa,KAC9C,aAAa,kBACZ,aAAa,eAAe;AAAA,cAAK,CAAC,UAChC,aAAa,cAAc,SAAS,MAAM,YAAa,CAAA;AAAA,YACzD;AAEJ,gBAAI,mBAAmB;AACd,qBAAA;AAAA,gBACL,OAAO;AAAA,gBACP,OAAO,IAAI;AAAA,kBACT,4CAA4C,aAAa,QAAQ,MAAM,YAAY;AAAA,kBACnF,CAAC;AAAA,kBACD;AAAA,oBACE,OAAO,aAAa;AAAA,kBAAA;AAAA,gBACtB;AAAA,cAEJ;AAAA,YAAA;AAAA,UACF;AAAA,QACF;AAAA,MACF,OAIK;AAED,YAAA,MAAM,QAAQ,WAAW,GAAG;AAE9B,gBAAM,yBAAgC,CAAC;AACvC,mBAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AACrC,kBAAA,OAAO,YAAY,CAAC;AAC1B,kBAAM,iBAAiB,MAAM;AAAA,cAC3B;AAAA,cACA,aAAa;AAAA,cACb,aAAa;AAAA,cACb,aAAa;AAAA,YACf;AACI,gBAAA,CAAC,eAAe,OAAO;AAClB,qBAAA;AAAA,gBACL,OAAO;AAAA,gBACP,OAAO,IAAI;AAAA,kBACT,4CAA4C,aAAa,QAAQ,cAAc,CAAC,KAAK,eAAe,MAAM,OAAO;AAAA,kBACjH,eAAe,MAAM;AAAA,kBACrB;AAAA,oBACE,OAAO,aAAa;AAAA,oBACpB,OAAO,eAAe,MAAM;AAAA,kBAAA;AAAA,gBAC9B;AAAA,cAEJ;AAAA,YAAA;AAEqB,mCAAA,KAAK,eAAe,IAAI;AAAA,UAAA;AAEjC,0BAAA,aAAa,QAAQ,IAAI;AAAA,QAAA,OACpC;AAEL,gBAAM,iBAAiB,MAAM;AAAA,YAC3B;AAAA,YACA,aAAa;AAAA,YACb,aAAa;AAAA,YACb,aAAa;AAAA,UACf;AACI,cAAA,CAAC,eAAe,OAAO;AAClB,mBAAA;AAAA,cACL,OAAO;AAAA,cACP,OAAO,IAAI;AAAA,gBACT,4CAA4C,aAAa,QAAQ,MAAM,eAAe,MAAM,OAAO;AAAA,gBACnG,eAAe,MAAM;AAAA,gBACrB;AAAA,kBACE,OAAO,aAAa;AAAA,kBACpB,OAAO,eAAe,MAAM;AAAA,gBAAA;AAAA,cAC9B;AAAA,YAEJ;AAAA,UAAA;AAEc,0BAAA,aAAa,QAAQ,IAAI,eAAe;AAAA,QAAA;AAAA,MAC1D;AAAA,IACF;AAAA,EACF;AAGK,SAAA;AAAA,IACL,OAAO;AAAA,IACP,MAAM,EAAE,GAAG,iBAAiB,GAAG,SAAS;AAAA,EAC1C;AACF;AAKA,eAAsB,qBACpB,UACA,QACA,gBACA,eAIA;AAEA,MAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AACtC,WAAA;AAAA,MACL,OAAO;AAAA,MACP,OAAO,IAAI,uBAAuB,aAAa,QAAQ;AAAA,IACzD;AAAA,EAAA;AAIF,QAAM,EAAE,YAAY,SAAS,OAAO,GAAG,KAAS,IAAA;AAEhD,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AAClB,WAAA;AAAA,MACL,OAAO;AAAA,MACP,OAAO,IAAI;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,IAEJ;AAAA,EAAA;AAIF,QAAM,mBAAgD,CAAC;AAEvD,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AAC/B,UAAA,SAAS,MAAM,CAAC;AACtB,UAAM,aAAa,MAAM;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEI,QAAA,CAAC,WAAW,OAAO;AACd,aAAA;AAAA,QACL,OAAO;AAAA,QACP,OAAO,WAAW;AAAA,MACpB;AAAA,IAAA;AAGe,qBAAA,KAAK,WAAW,IAAI;AAAA,EAAA;AAGhC,SAAA;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AACF;AAKA,eAAsB,uBACpB,UACA,QACA,gBACA,eACA,OAA0B,SAI1B;;AAGE,MAAA,SAAS,SACT,MAAM,QAAQ,SAAS,KAAK,KAC5B,SAAS,MAAM,SAAS,GACxB;AACO,WAAA;AAAA,MACL,OAAO;AAAA,MACP,OAAO,IAAI;AAAA,QACT,SAAS,UAAU,QAAQ;AAAA,QAC3B,SAAS,MAAM;AAAA,MAAA;AAAA,IAEnB;AAAA,EAAA;AAIF,MAAI,CAAC,YAAa,SAAS,SAAS,SAAS,MAAM,WAAW,GAAI;AAChE,QAAI,SAAS,SAAS;AACb,aAAA;AAAA,QACL,OAAO;AAAA,QACP,OAAO,IAAI,yBAAyB,OAAO,CAAC;AAAA,MAC9C;AAAA,IAAA;AAGK,WAAA;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EAAA;AAIF,QAAM,WAAS,cAAS,UAAT,mBAAiB,OAAM;AACtC,QAAM,aAAa,MAAM;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEI,MAAA,CAAC,WAAW,OAAO;AACd,WAAA;AAAA,EAAA;AAGF,SAAA;AAAA,IACL,OAAO;AAAA,IACP,MAAM,WAAW;AAAA,EACnB;AACF;"}
|
|
1
|
+
{"version":3,"file":"validation.js","sources":["../../src/validation.ts"],"sourcesContent":["import type { ODataRecordMetadata } from \"./types\";\nimport { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { FMTable } from \"./orm/table\";\nimport {\n ValidationError,\n ResponseStructureError,\n RecordCountMismatchError,\n} from \"./errors\";\n\n/**\n * Validates and transforms input data for insert/update operations.\n * Applies input validators (writeValidators) to transform user input to database format.\n * Fields without input validators are passed through unchanged.\n *\n * @param data - The input data to validate and transform\n * @param inputSchema - Optional schema containing input validators for each field\n * @returns Transformed data ready to send to the server\n * @throws ValidationError if any field fails validation\n */\nexport async function validateAndTransformInput<T extends Record<string, any>>(\n data: Partial<T>,\n inputSchema?: Partial<Record<string, StandardSchemaV1>>,\n): Promise<Partial<T>> {\n // If no input schema, return data as-is\n if (!inputSchema) {\n return data;\n }\n\n const transformedData: Record<string, any> = { ...data };\n\n // Process each field that has an input validator\n for (const [fieldName, fieldSchema] of Object.entries(inputSchema)) {\n // Skip if no schema for this field\n if (!fieldSchema) continue;\n \n // Only process fields that are present in the input data\n if (fieldName in data) {\n const inputValue = data[fieldName];\n\n try {\n // Run the input validator to transform the value\n let result = fieldSchema[\"~standard\"].validate(inputValue);\n if (result instanceof Promise) {\n result = await result;\n }\n\n // Check for validation errors\n if (result.issues) {\n throw new ValidationError(\n `Input validation failed for field '${fieldName}'`,\n result.issues,\n {\n field: fieldName,\n value: inputValue,\n cause: result.issues,\n },\n );\n }\n\n // Store the transformed value\n transformedData[fieldName] = result.value;\n } catch (error) {\n // If it's already a ValidationError, re-throw it\n if (error instanceof ValidationError) {\n throw error;\n }\n\n // Otherwise, wrap the error\n throw new ValidationError(\n `Input validation failed for field '${fieldName}'`,\n [],\n {\n field: fieldName,\n value: inputValue,\n cause: error,\n },\n );\n }\n }\n }\n\n // Fields without input validators are already in transformedData (passed through)\n return transformedData as Partial<T>;\n}\n\n// Type for expand validation configuration\nexport type ExpandValidationConfig = {\n relation: string;\n targetSchema?: Partial<Record<string, StandardSchemaV1>>;\n targetTable?: FMTable<any, any>;\n table?: FMTable<any, any>; // For transformation\n selectedFields?: string[];\n nestedExpands?: ExpandValidationConfig[];\n};\n\n/**\n * Validates a single record against a schema, only validating selected fields.\n * Also validates expanded relations if expandConfigs are provided.\n */\nexport async function validateRecord<T extends Record<string, any>>(\n record: any,\n schema: Partial<Record<string, StandardSchemaV1>> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n includeSpecialColumns?: boolean,\n): Promise<\n | { valid: true; data: T & ODataRecordMetadata }\n | { valid: false; error: ValidationError }\n> {\n // Extract OData metadata fields (don't validate them - include if present)\n const { \"@id\": id, \"@editLink\": editLink, ...rest } = record;\n\n // Only include metadata fields if they actually exist and have values\n const metadata: Partial<ODataRecordMetadata> = {};\n if (id) metadata[\"@id\"] = id;\n if (editLink) metadata[\"@editLink\"] = editLink;\n\n // If no schema, just return the data with metadata\n // Exclude special columns if includeSpecialColumns is false\n if (!schema) {\n const { ROWID, ROWMODID, ...restWithoutSystemFields } = rest;\n const specialColumns: { ROWID?: number; ROWMODID?: number } = {};\n if (includeSpecialColumns) {\n if (ROWID !== undefined) specialColumns.ROWID = ROWID;\n if (ROWMODID !== undefined) specialColumns.ROWMODID = ROWMODID;\n }\n return {\n valid: true,\n data: {\n ...restWithoutSystemFields,\n ...specialColumns,\n ...metadata,\n } as T & ODataRecordMetadata,\n };\n }\n\n // Extract FileMaker special columns - preserve them if includeSpecialColumns is enabled\n // Note: Special columns are excluded when using single() method (per OData spec behavior)\n const { ROWID, ROWMODID, ...restWithoutSystemFields } = rest;\n const specialColumns: { ROWID?: number; ROWMODID?: number } = {};\n // Only include special columns if explicitly enabled (they're excluded for single() by design)\n if (includeSpecialColumns) {\n if (ROWID !== undefined) specialColumns.ROWID = ROWID;\n if (ROWMODID !== undefined) specialColumns.ROWMODID = ROWMODID;\n }\n\n // If selected fields are specified, validate only those fields\n if (selectedFields && selectedFields.length > 0) {\n const validatedRecord: Record<string, any> = {};\n\n for (const field of selectedFields) {\n const fieldName = String(field);\n const fieldSchema = schema[fieldName];\n\n if (fieldSchema) {\n const input = rest[fieldName];\n try {\n let result = fieldSchema[\"~standard\"].validate(input);\n if (result instanceof Promise) result = await result;\n\n // if the `issues` field exists, the validation failed\n if (result.issues) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n result.issues,\n {\n field: fieldName,\n value: input,\n cause: result.issues,\n },\n ),\n };\n }\n\n validatedRecord[fieldName] = result.value;\n } catch (originalError) {\n // If the validator throws directly, wrap it\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n [],\n {\n field: fieldName,\n value: input,\n cause: originalError,\n },\n ),\n };\n }\n } else {\n // For fields not in schema (like when explicitly selecting ROWID/ROWMODID)\n // Check if it's a special column that was destructured earlier\n if (fieldName === \"ROWID\" || fieldName === \"ROWMODID\") {\n // Use the destructured value since it was removed from rest\n if (fieldName === \"ROWID\" && ROWID !== undefined) {\n validatedRecord[fieldName] = ROWID;\n } else if (fieldName === \"ROWMODID\" && ROWMODID !== undefined) {\n validatedRecord[fieldName] = ROWMODID;\n }\n } else {\n // For other fields not in schema, include them from the original response\n validatedRecord[fieldName] = rest[fieldName];\n }\n }\n }\n\n // Validate expanded relations\n if (expandConfigs && expandConfigs.length > 0) {\n for (const expandConfig of expandConfigs) {\n const expandValue = rest[expandConfig.relation];\n\n // Check if expand field is missing\n if (expandValue === undefined) {\n // Check for inline error array (FileMaker returns errors inline when expand fails)\n if (Array.isArray(rest.error) && rest.error.length > 0) {\n // Extract error message from inline error\n const errorDetail = rest.error[0]?.error;\n if (errorDetail?.message) {\n const errorMessage = errorDetail.message;\n // Check if the error is related to this expand by checking if:\n // 1. The error mentions the relation name, OR\n // 2. The error mentions any of the selected fields\n const isRelatedToExpand =\n errorMessage\n .toLowerCase()\n .includes(expandConfig.relation.toLowerCase()) ||\n (expandConfig.selectedFields &&\n expandConfig.selectedFields.some((field) =>\n errorMessage.toLowerCase().includes(field.toLowerCase()),\n ));\n\n if (isRelatedToExpand) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,\n [],\n {\n field: expandConfig.relation,\n },\n ),\n };\n }\n }\n }\n // If no inline error but expand was expected, that's also an issue\n // However, this might be a legitimate case (e.g., no related records)\n // So we'll only fail if there's an explicit error array\n } else {\n // Original validation logic for when expand exists\n if (Array.isArray(expandValue)) {\n // Validate each item in the expanded array\n const validatedExpandedItems: any[] = [];\n for (let i = 0; i < expandValue.length; i++) {\n const item = expandValue[i];\n const itemValidation = await validateRecord(\n item,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n includeSpecialColumns,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedExpandedItems.push(itemValidation.data);\n }\n validatedRecord[expandConfig.relation] = validatedExpandedItems;\n } else {\n // Single expanded item (shouldn't happen in OData, but handle it)\n const itemValidation = await validateRecord(\n expandValue,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n includeSpecialColumns,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedRecord[expandConfig.relation] = itemValidation.data;\n }\n }\n }\n }\n\n // Merge validated data with metadata and special columns\n return {\n valid: true,\n data: { ...validatedRecord, ...specialColumns, ...metadata } as T &\n ODataRecordMetadata,\n };\n }\n\n // Validate all fields in schema, but exclude ROWID/ROWMODID by default (unless includeSpecialColumns is enabled)\n const validatedRecord: Record<string, any> = { ...restWithoutSystemFields };\n\n for (const [fieldName, fieldSchema] of Object.entries(schema)) {\n // Skip if no schema for this field\n if (!fieldSchema) continue;\n \n const input = rest[fieldName];\n try {\n let result = fieldSchema[\"~standard\"].validate(input);\n if (result instanceof Promise) result = await result;\n\n // if the `issues` field exists, the validation failed\n if (result.issues) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n result.issues,\n {\n field: fieldName,\n value: input,\n cause: result.issues,\n },\n ),\n };\n }\n\n validatedRecord[fieldName] = result.value;\n } catch (originalError) {\n // If the validator throws an error directly, catch and wrap it\n // This preserves the original error instance for instanceof checks\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for field '${fieldName}'`,\n [],\n {\n field: fieldName,\n value: input,\n cause: originalError,\n },\n ),\n };\n }\n }\n\n // Validate expanded relations even when not using selected fields\n if (expandConfigs && expandConfigs.length > 0) {\n for (const expandConfig of expandConfigs) {\n const expandValue = rest[expandConfig.relation];\n\n // Check if expand field is missing\n if (expandValue === undefined) {\n // Check for inline error array (FileMaker returns errors inline when expand fails)\n if (Array.isArray(rest.error) && rest.error.length > 0) {\n // Extract error message from inline error\n const errorDetail = rest.error[0]?.error;\n if (errorDetail?.message) {\n const errorMessage = errorDetail.message;\n // Check if the error is related to this expand by checking if:\n // 1. The error mentions the relation name, OR\n // 2. The error mentions any of the selected fields\n const isRelatedToExpand =\n errorMessage\n .toLowerCase()\n .includes(expandConfig.relation.toLowerCase()) ||\n (expandConfig.selectedFields &&\n expandConfig.selectedFields.some((field) =>\n errorMessage.toLowerCase().includes(field.toLowerCase()),\n ));\n\n if (isRelatedToExpand) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${errorMessage}`,\n [],\n {\n field: expandConfig.relation,\n },\n ),\n };\n }\n }\n }\n // If no inline error but expand was expected, that's also an issue\n // However, this might be a legitimate case (e.g., no related records)\n // So we'll only fail if there's an explicit error array\n } else {\n // Original validation logic for when expand exists\n if (Array.isArray(expandValue)) {\n // Validate each item in the expanded array\n const validatedExpandedItems: any[] = [];\n for (let i = 0; i < expandValue.length; i++) {\n const item = expandValue[i];\n const itemValidation = await validateRecord(\n item,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n includeSpecialColumns,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}' at index ${i}: ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedExpandedItems.push(itemValidation.data);\n }\n validatedRecord[expandConfig.relation] = validatedExpandedItems;\n } else {\n // Single expanded item (shouldn't happen in OData, but handle it)\n const itemValidation = await validateRecord(\n expandValue,\n expandConfig.targetSchema,\n expandConfig.selectedFields as string[] | undefined,\n expandConfig.nestedExpands,\n includeSpecialColumns,\n );\n if (!itemValidation.valid) {\n return {\n valid: false,\n error: new ValidationError(\n `Validation failed for expanded relation '${expandConfig.relation}': ${itemValidation.error.message}`,\n itemValidation.error.issues,\n {\n field: expandConfig.relation,\n cause: itemValidation.error.cause,\n },\n ),\n };\n }\n validatedRecord[expandConfig.relation] = itemValidation.data;\n }\n }\n }\n }\n\n return {\n valid: true,\n data: { ...validatedRecord, ...specialColumns, ...metadata } as T &\n ODataRecordMetadata,\n };\n}\n\n/**\n * Validates a list response against a schema.\n */\nexport async function validateListResponse<T extends Record<string, any>>(\n response: any,\n schema: Partial<Record<string, StandardSchemaV1>> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n includeSpecialColumns?: boolean,\n): Promise<\n | { valid: true; data: (T & ODataRecordMetadata)[] }\n | { valid: false; error: ResponseStructureError | ValidationError }\n> {\n // Check if response has the expected structure\n if (!response || typeof response !== \"object\") {\n return {\n valid: false,\n error: new ResponseStructureError(\"an object\", response),\n };\n }\n\n // Extract @context (for internal validation, but we won't return it)\n const { \"@context\": context, value, ...rest } = response;\n\n if (!Array.isArray(value)) {\n return {\n valid: false,\n error: new ResponseStructureError(\n \"'value' property to be an array\",\n value,\n ),\n };\n }\n\n // Validate each record in the array\n const validatedRecords: (T & ODataRecordMetadata)[] = [];\n\n for (let i = 0; i < value.length; i++) {\n const record = value[i];\n const validation = await validateRecord<T>(\n record,\n schema,\n selectedFields,\n expandConfigs,\n includeSpecialColumns,\n );\n\n if (!validation.valid) {\n return {\n valid: false,\n error: validation.error,\n };\n }\n\n validatedRecords.push(validation.data);\n }\n\n return {\n valid: true,\n data: validatedRecords,\n };\n}\n\n/**\n * Validates a single record response against a schema.\n */\nexport async function validateSingleResponse<T extends Record<string, any>>(\n response: any,\n schema: Partial<Record<string, StandardSchemaV1>> | undefined,\n selectedFields?: (keyof T)[],\n expandConfigs?: ExpandValidationConfig[],\n mode: \"exact\" | \"maybe\" = \"maybe\",\n includeSpecialColumns?: boolean,\n): Promise<\n | { valid: true; data: (T & ODataRecordMetadata) | null }\n | { valid: false; error: RecordCountMismatchError | ValidationError }\n> {\n // Check for multiple records (error in both modes)\n if (\n response.value &&\n Array.isArray(response.value) &&\n response.value.length > 1\n ) {\n return {\n valid: false,\n error: new RecordCountMismatchError(\n mode === \"exact\" ? \"one\" : \"at-most-one\",\n response.value.length,\n ),\n };\n }\n\n // Handle empty responses\n if (!response || (response.value && response.value.length === 0)) {\n if (mode === \"exact\") {\n return {\n valid: false,\n error: new RecordCountMismatchError(\"one\", 0),\n };\n }\n // mode === \"maybe\" - return null for empty\n return {\n valid: true,\n data: null,\n };\n }\n\n // Single record validation\n const record = response.value?.[0] ?? response;\n const validation = await validateRecord<T>(\n record,\n schema,\n selectedFields,\n expandConfigs,\n includeSpecialColumns,\n );\n\n if (!validation.valid) {\n return validation as { valid: false; error: ValidationError };\n }\n\n return {\n valid: true,\n data: validation.data,\n };\n}\n"],"names":["ROWID","ROWMODID","restWithoutSystemFields","specialColumns","validatedRecord"],"mappings":";AAmBsB,eAAA,0BACpB,MACA,aACqB;AAErB,MAAI,CAAC,aAAa;AACT,WAAA;AAAA,EAAA;AAGH,QAAA,kBAAuC,EAAE,GAAG,KAAK;AAGvD,aAAW,CAAC,WAAW,WAAW,KAAK,OAAO,QAAQ,WAAW,GAAG;AAElE,QAAI,CAAC,YAAa;AAGlB,QAAI,aAAa,MAAM;AACf,YAAA,aAAa,KAAK,SAAS;AAE7B,UAAA;AAEF,YAAI,SAAS,YAAY,WAAW,EAAE,SAAS,UAAU;AACzD,YAAI,kBAAkB,SAAS;AAC7B,mBAAS,MAAM;AAAA,QAAA;AAIjB,YAAI,OAAO,QAAQ;AACjB,gBAAM,IAAI;AAAA,YACR,sCAAsC,SAAS;AAAA,YAC/C,OAAO;AAAA,YACP;AAAA,cACE,OAAO;AAAA,cACP,OAAO;AAAA,cACP,OAAO,OAAO;AAAA,YAAA;AAAA,UAElB;AAAA,QAAA;AAIc,wBAAA,SAAS,IAAI,OAAO;AAAA,eAC7B,OAAO;AAEd,YAAI,iBAAiB,iBAAiB;AAC9B,gBAAA;AAAA,QAAA;AAIR,cAAM,IAAI;AAAA,UACR,sCAAsC,SAAS;AAAA,UAC/C,CAAC;AAAA,UACD;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,OAAO;AAAA,UAAA;AAAA,QAEX;AAAA,MAAA;AAAA,IACF;AAAA,EACF;AAIK,SAAA;AACT;AAgBA,eAAsB,eACpB,QACA,QACA,gBACA,eACA,uBAIA;;AAEA,QAAM,EAAE,OAAO,IAAI,aAAa,UAAU,GAAG,SAAS;AAGtD,QAAM,WAAyC,CAAC;AAC5C,MAAA,GAAa,UAAA,KAAK,IAAI;AACtB,MAAA,SAAmB,UAAA,WAAW,IAAI;AAItC,MAAI,CAAC,QAAQ;AACX,UAAM,EAAE,OAAAA,QAAO,UAAAC,WAAU,GAAGC,6BAA4B;AACxD,UAAMC,kBAAwD,CAAC;AAC/D,QAAI,uBAAuB;AACzB,UAAIH,WAAU,OAAWG,iBAAe,QAAQH;AAChD,UAAIC,cAAa,OAAWE,iBAAe,WAAWF;AAAAA,IAAA;AAEjD,WAAA;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,QACJ,GAAGC;AAAAA,QACH,GAAGC;AAAAA,QACH,GAAG;AAAA,MAAA;AAAA,IAEP;AAAA,EAAA;AAKF,QAAM,EAAE,OAAO,UAAU,GAAG,wBAA4B,IAAA;AACxD,QAAM,iBAAwD,CAAC;AAE/D,MAAI,uBAAuB;AACrB,QAAA,UAAU,OAAW,gBAAe,QAAQ;AAC5C,QAAA,aAAa,OAAW,gBAAe,WAAW;AAAA,EAAA;AAIpD,MAAA,kBAAkB,eAAe,SAAS,GAAG;AAC/C,UAAMC,mBAAuC,CAAC;AAE9C,eAAW,SAAS,gBAAgB;AAC5B,YAAA,YAAY,OAAO,KAAK;AACxB,YAAA,cAAc,OAAO,SAAS;AAEpC,UAAI,aAAa;AACT,cAAA,QAAQ,KAAK,SAAS;AACxB,YAAA;AACF,cAAI,SAAS,YAAY,WAAW,EAAE,SAAS,KAAK;AAChD,cAAA,kBAAkB,QAAS,UAAS,MAAM;AAG9C,cAAI,OAAO,QAAQ;AACV,mBAAA;AAAA,cACL,OAAO;AAAA,cACP,OAAO,IAAI;AAAA,gBACT,gCAAgC,SAAS;AAAA,gBACzC,OAAO;AAAA,gBACP;AAAA,kBACE,OAAO;AAAA,kBACP,OAAO;AAAA,kBACP,OAAO,OAAO;AAAA,gBAAA;AAAA,cAChB;AAAA,YAEJ;AAAA,UAAA;AAGFA,2BAAgB,SAAS,IAAI,OAAO;AAAA,iBAC7B,eAAe;AAEf,iBAAA;AAAA,YACL,OAAO;AAAA,YACP,OAAO,IAAI;AAAA,cACT,gCAAgC,SAAS;AAAA,cACzC,CAAC;AAAA,cACD;AAAA,gBACE,OAAO;AAAA,gBACP,OAAO;AAAA,gBACP,OAAO;AAAA,cAAA;AAAA,YACT;AAAA,UAEJ;AAAA,QAAA;AAAA,MACF,OACK;AAGD,YAAA,cAAc,WAAW,cAAc,YAAY;AAEjD,cAAA,cAAc,WAAW,UAAU,QAAW;AAChDA,6BAAgB,SAAS,IAAI;AAAA,UACpB,WAAA,cAAc,cAAc,aAAa,QAAW;AAC7DA,6BAAgB,SAAS,IAAI;AAAA,UAAA;AAAA,QAC/B,OACK;AAELA,2BAAgB,SAAS,IAAI,KAAK,SAAS;AAAA,QAAA;AAAA,MAC7C;AAAA,IACF;AAIE,QAAA,iBAAiB,cAAc,SAAS,GAAG;AAC7C,iBAAW,gBAAgB,eAAe;AAClC,cAAA,cAAc,KAAK,aAAa,QAAQ;AAG9C,YAAI,gBAAgB,QAAW;AAEzB,cAAA,MAAM,QAAQ,KAAK,KAAK,KAAK,KAAK,MAAM,SAAS,GAAG;AAEtD,kBAAM,eAAc,UAAK,MAAM,CAAC,MAAZ,mBAAe;AACnC,gBAAI,2CAAa,SAAS;AACxB,oBAAM,eAAe,YAAY;AAIjC,oBAAM,oBACJ,aACG,YAAY,EACZ,SAAS,aAAa,SAAS,YAAA,CAAa,KAC9C,aAAa,kBACZ,aAAa,eAAe;AAAA,gBAAK,CAAC,UAChC,aAAa,cAAc,SAAS,MAAM,YAAa,CAAA;AAAA,cACzD;AAEJ,kBAAI,mBAAmB;AACd,uBAAA;AAAA,kBACL,OAAO;AAAA,kBACP,OAAO,IAAI;AAAA,oBACT,4CAA4C,aAAa,QAAQ,MAAM,YAAY;AAAA,oBACnF,CAAC;AAAA,oBACD;AAAA,sBACE,OAAO,aAAa;AAAA,oBAAA;AAAA,kBACtB;AAAA,gBAEJ;AAAA,cAAA;AAAA,YACF;AAAA,UACF;AAAA,QACF,OAIK;AAED,cAAA,MAAM,QAAQ,WAAW,GAAG;AAE9B,kBAAM,yBAAgC,CAAC;AACvC,qBAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AACrC,oBAAA,OAAO,YAAY,CAAC;AAC1B,oBAAM,iBAAiB,MAAM;AAAA,gBAC3B;AAAA,gBACA,aAAa;AAAA,gBACb,aAAa;AAAA,gBACb,aAAa;AAAA,gBACb;AAAA,cACF;AACI,kBAAA,CAAC,eAAe,OAAO;AAClB,uBAAA;AAAA,kBACL,OAAO;AAAA,kBACP,OAAO,IAAI;AAAA,oBACT,4CAA4C,aAAa,QAAQ,cAAc,CAAC,KAAK,eAAe,MAAM,OAAO;AAAA,oBACjH,eAAe,MAAM;AAAA,oBACrB;AAAA,sBACE,OAAO,aAAa;AAAA,sBACpB,OAAO,eAAe,MAAM;AAAA,oBAAA;AAAA,kBAC9B;AAAA,gBAEJ;AAAA,cAAA;AAEqB,qCAAA,KAAK,eAAe,IAAI;AAAA,YAAA;AAEjDA,6BAAgB,aAAa,QAAQ,IAAI;AAAA,UAAA,OACpC;AAEL,kBAAM,iBAAiB,MAAM;AAAA,cAC3B;AAAA,cACA,aAAa;AAAA,cACb,aAAa;AAAA,cACb,aAAa;AAAA,cACb;AAAA,YACF;AACI,gBAAA,CAAC,eAAe,OAAO;AAClB,qBAAA;AAAA,gBACL,OAAO;AAAA,gBACP,OAAO,IAAI;AAAA,kBACT,4CAA4C,aAAa,QAAQ,MAAM,eAAe,MAAM,OAAO;AAAA,kBACnG,eAAe,MAAM;AAAA,kBACrB;AAAA,oBACE,OAAO,aAAa;AAAA,oBACpB,OAAO,eAAe,MAAM;AAAA,kBAAA;AAAA,gBAC9B;AAAA,cAEJ;AAAA,YAAA;AAEFA,6BAAgB,aAAa,QAAQ,IAAI,eAAe;AAAA,UAAA;AAAA,QAC1D;AAAA,MACF;AAAA,IACF;AAIK,WAAA;AAAA,MACL,OAAO;AAAA,MACP,MAAM,EAAE,GAAGA,kBAAiB,GAAG,gBAAgB,GAAG,SAAS;AAAA,IAE7D;AAAA,EAAA;AAII,QAAA,kBAAuC,EAAE,GAAG,wBAAwB;AAE1E,aAAW,CAAC,WAAW,WAAW,KAAK,OAAO,QAAQ,MAAM,GAAG;AAE7D,QAAI,CAAC,YAAa;AAEZ,UAAA,QAAQ,KAAK,SAAS;AACxB,QAAA;AACF,UAAI,SAAS,YAAY,WAAW,EAAE,SAAS,KAAK;AAChD,UAAA,kBAAkB,QAAS,UAAS,MAAM;AAG9C,UAAI,OAAO,QAAQ;AACV,eAAA;AAAA,UACL,OAAO;AAAA,UACP,OAAO,IAAI;AAAA,YACT,gCAAgC,SAAS;AAAA,YACzC,OAAO;AAAA,YACP;AAAA,cACE,OAAO;AAAA,cACP,OAAO;AAAA,cACP,OAAO,OAAO;AAAA,YAAA;AAAA,UAChB;AAAA,QAEJ;AAAA,MAAA;AAGc,sBAAA,SAAS,IAAI,OAAO;AAAA,aAC7B,eAAe;AAGf,aAAA;AAAA,QACL,OAAO;AAAA,QACP,OAAO,IAAI;AAAA,UACT,gCAAgC,SAAS;AAAA,UACzC,CAAC;AAAA,UACD;AAAA,YACE,OAAO;AAAA,YACP,OAAO;AAAA,YACP,OAAO;AAAA,UAAA;AAAA,QACT;AAAA,MAEJ;AAAA,IAAA;AAAA,EACF;AAIE,MAAA,iBAAiB,cAAc,SAAS,GAAG;AAC7C,eAAW,gBAAgB,eAAe;AAClC,YAAA,cAAc,KAAK,aAAa,QAAQ;AAG9C,UAAI,gBAAgB,QAAW;AAEzB,YAAA,MAAM,QAAQ,KAAK,KAAK,KAAK,KAAK,MAAM,SAAS,GAAG;AAEtD,gBAAM,eAAc,UAAK,MAAM,CAAC,MAAZ,mBAAe;AACnC,cAAI,2CAAa,SAAS;AACxB,kBAAM,eAAe,YAAY;AAIjC,kBAAM,oBACJ,aACG,YAAY,EACZ,SAAS,aAAa,SAAS,YAAA,CAAa,KAC9C,aAAa,kBACZ,aAAa,eAAe;AAAA,cAAK,CAAC,UAChC,aAAa,cAAc,SAAS,MAAM,YAAa,CAAA;AAAA,YACzD;AAEJ,gBAAI,mBAAmB;AACd,qBAAA;AAAA,gBACL,OAAO;AAAA,gBACP,OAAO,IAAI;AAAA,kBACT,4CAA4C,aAAa,QAAQ,MAAM,YAAY;AAAA,kBACnF,CAAC;AAAA,kBACD;AAAA,oBACE,OAAO,aAAa;AAAA,kBAAA;AAAA,gBACtB;AAAA,cAEJ;AAAA,YAAA;AAAA,UACF;AAAA,QACF;AAAA,MACF,OAIK;AAED,YAAA,MAAM,QAAQ,WAAW,GAAG;AAE9B,gBAAM,yBAAgC,CAAC;AACvC,mBAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AACrC,kBAAA,OAAO,YAAY,CAAC;AAC1B,kBAAM,iBAAiB,MAAM;AAAA,cAC3B;AAAA,cACA,aAAa;AAAA,cACb,aAAa;AAAA,cACb,aAAa;AAAA,cACb;AAAA,YACF;AACI,gBAAA,CAAC,eAAe,OAAO;AAClB,qBAAA;AAAA,gBACL,OAAO;AAAA,gBACP,OAAO,IAAI;AAAA,kBACT,4CAA4C,aAAa,QAAQ,cAAc,CAAC,KAAK,eAAe,MAAM,OAAO;AAAA,kBACjH,eAAe,MAAM;AAAA,kBACrB;AAAA,oBACE,OAAO,aAAa;AAAA,oBACpB,OAAO,eAAe,MAAM;AAAA,kBAAA;AAAA,gBAC9B;AAAA,cAEJ;AAAA,YAAA;AAEqB,mCAAA,KAAK,eAAe,IAAI;AAAA,UAAA;AAEjC,0BAAA,aAAa,QAAQ,IAAI;AAAA,QAAA,OACpC;AAEL,gBAAM,iBAAiB,MAAM;AAAA,YAC3B;AAAA,YACA,aAAa;AAAA,YACb,aAAa;AAAA,YACb,aAAa;AAAA,YACb;AAAA,UACF;AACI,cAAA,CAAC,eAAe,OAAO;AAClB,mBAAA;AAAA,cACL,OAAO;AAAA,cACP,OAAO,IAAI;AAAA,gBACT,4CAA4C,aAAa,QAAQ,MAAM,eAAe,MAAM,OAAO;AAAA,gBACnG,eAAe,MAAM;AAAA,gBACrB;AAAA,kBACE,OAAO,aAAa;AAAA,kBACpB,OAAO,eAAe,MAAM;AAAA,gBAAA;AAAA,cAC9B;AAAA,YAEJ;AAAA,UAAA;AAEc,0BAAA,aAAa,QAAQ,IAAI,eAAe;AAAA,QAAA;AAAA,MAC1D;AAAA,IACF;AAAA,EACF;AAGK,SAAA;AAAA,IACL,OAAO;AAAA,IACP,MAAM,EAAE,GAAG,iBAAiB,GAAG,gBAAgB,GAAG,SAAS;AAAA,EAE7D;AACF;AAKA,eAAsB,qBACpB,UACA,QACA,gBACA,eACA,uBAIA;AAEA,MAAI,CAAC,YAAY,OAAO,aAAa,UAAU;AACtC,WAAA;AAAA,MACL,OAAO;AAAA,MACP,OAAO,IAAI,uBAAuB,aAAa,QAAQ;AAAA,IACzD;AAAA,EAAA;AAIF,QAAM,EAAE,YAAY,SAAS,OAAO,GAAG,KAAS,IAAA;AAEhD,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AAClB,WAAA;AAAA,MACL,OAAO;AAAA,MACP,OAAO,IAAI;AAAA,QACT;AAAA,QACA;AAAA,MAAA;AAAA,IAEJ;AAAA,EAAA;AAIF,QAAM,mBAAgD,CAAC;AAEvD,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AAC/B,UAAA,SAAS,MAAM,CAAC;AACtB,UAAM,aAAa,MAAM;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEI,QAAA,CAAC,WAAW,OAAO;AACd,aAAA;AAAA,QACL,OAAO;AAAA,QACP,OAAO,WAAW;AAAA,MACpB;AAAA,IAAA;AAGe,qBAAA,KAAK,WAAW,IAAI;AAAA,EAAA;AAGhC,SAAA;AAAA,IACL,OAAO;AAAA,IACP,MAAM;AAAA,EACR;AACF;AAKA,eAAsB,uBACpB,UACA,QACA,gBACA,eACA,OAA0B,SAC1B,uBAIA;;AAGE,MAAA,SAAS,SACT,MAAM,QAAQ,SAAS,KAAK,KAC5B,SAAS,MAAM,SAAS,GACxB;AACO,WAAA;AAAA,MACL,OAAO;AAAA,MACP,OAAO,IAAI;AAAA,QACT,SAAS,UAAU,QAAQ;AAAA,QAC3B,SAAS,MAAM;AAAA,MAAA;AAAA,IAEnB;AAAA,EAAA;AAIF,MAAI,CAAC,YAAa,SAAS,SAAS,SAAS,MAAM,WAAW,GAAI;AAChE,QAAI,SAAS,SAAS;AACb,aAAA;AAAA,QACL,OAAO;AAAA,QACP,OAAO,IAAI,yBAAyB,OAAO,CAAC;AAAA,MAC9C;AAAA,IAAA;AAGK,WAAA;AAAA,MACL,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EAAA;AAIF,QAAM,WAAS,cAAS,UAAT,mBAAiB,OAAM;AACtC,QAAM,aAAa,MAAM;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEI,MAAA,CAAC,WAAW,OAAO;AACd,WAAA;AAAA,EAAA;AAGF,SAAA;AAAA,IACL,OAAO;AAAA,IACP,MAAM,WAAW;AAAA,EACnB;AACF;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proofkit/fmodata",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20",
|
|
4
4
|
"description": "FileMaker OData API client",
|
|
5
5
|
"repository": "git@github.com:proofgeist/proofkit.git",
|
|
6
6
|
"author": "Eric <37158449+eluce2@users.noreply.github.com>",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"vite": "^6.3.4",
|
|
64
64
|
"vite-plugin-dts": "^4.5.4",
|
|
65
65
|
"vitest": "^4.0.7",
|
|
66
|
-
"zod": "4.1.
|
|
66
|
+
"zod": "^4.1.13"
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
69
69
|
"node": ">=18.0.0"
|
|
@@ -20,9 +20,13 @@ function getContainerFieldNames(table: FMTable<any, any>): string[] {
|
|
|
20
20
|
* Gets default select fields from a table definition.
|
|
21
21
|
* Returns undefined if defaultSelect is "all".
|
|
22
22
|
* Automatically filters out container fields since they cannot be selected via $select.
|
|
23
|
+
*
|
|
24
|
+
* @param table - The table occurrence
|
|
25
|
+
* @param includeSpecialColumns - If true, includes ROWID and ROWMODID when defaultSelect is "schema"
|
|
23
26
|
*/
|
|
24
27
|
export function getDefaultSelectFields(
|
|
25
28
|
table: FMTable<any, any> | undefined,
|
|
29
|
+
includeSpecialColumns?: boolean,
|
|
26
30
|
): string[] | undefined {
|
|
27
31
|
if (!table) return undefined;
|
|
28
32
|
|
|
@@ -33,7 +37,14 @@ export function getDefaultSelectFields(
|
|
|
33
37
|
const baseTableConfig = getBaseTableConfig(table);
|
|
34
38
|
const allFields = Object.keys(baseTableConfig.schema);
|
|
35
39
|
// Filter out container fields
|
|
36
|
-
|
|
40
|
+
const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];
|
|
41
|
+
|
|
42
|
+
// Add special columns if requested
|
|
43
|
+
if (includeSpecialColumns) {
|
|
44
|
+
fields.push("ROWID", "ROWMODID");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return fields;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
if (Array.isArray(defaultSelect)) {
|
|
@@ -40,7 +40,7 @@ export class ExpandBuilder {
|
|
|
40
40
|
return configs.map((config) => {
|
|
41
41
|
const targetTable = config.targetTable;
|
|
42
42
|
|
|
43
|
-
let targetSchema: Record<string, StandardSchemaV1
|
|
43
|
+
let targetSchema: Partial<Record<string, StandardSchemaV1>> | undefined;
|
|
44
44
|
if (targetTable) {
|
|
45
45
|
const baseTableConfig = getBaseTableConfig(targetTable);
|
|
46
46
|
const containerFields = baseTableConfig.containerFields || [];
|
|
@@ -17,12 +17,18 @@ export function buildSelectExpandQueryString(config: {
|
|
|
17
17
|
table?: FMTable<any, any>;
|
|
18
18
|
useEntityIds: boolean;
|
|
19
19
|
logger: InternalLogger;
|
|
20
|
+
includeSpecialColumns?: boolean;
|
|
20
21
|
}): string {
|
|
21
22
|
const parts: string[] = [];
|
|
22
23
|
const expandBuilder = new ExpandBuilder(config.useEntityIds, config.logger);
|
|
23
24
|
|
|
24
25
|
// Build $select
|
|
25
26
|
if (config.selectedFields && config.selectedFields.length > 0) {
|
|
27
|
+
// Important: do NOT implicitly add system columns (ROWID/ROWMODID) here.
|
|
28
|
+
// - `includeSpecialColumns` controls the Prefer header + response parsing, but should not
|
|
29
|
+
// mutate/expand an explicit `$select` (e.g. when the user calls `.select({ ... })`).
|
|
30
|
+
// - If system columns are desired with `.select()`, they must be explicitly included via
|
|
31
|
+
// the `systemColumns` argument, which will already have added them to `selectedFields`.
|
|
26
32
|
const selectString = formatSelectFields(
|
|
27
33
|
config.selectedFields,
|
|
28
34
|
config.table,
|
|
@@ -17,6 +17,7 @@ export interface ProcessResponseConfig {
|
|
|
17
17
|
expandValidationConfigs?: ExpandValidationConfig[];
|
|
18
18
|
skipValidation?: boolean;
|
|
19
19
|
useEntityIds?: boolean;
|
|
20
|
+
includeSpecialColumns?: boolean;
|
|
20
21
|
// Mapping from field names to output keys (for renamed fields in select)
|
|
21
22
|
fieldMapping?: Record<string, string>;
|
|
22
23
|
}
|
|
@@ -37,6 +38,7 @@ export async function processODataResponse<T>(
|
|
|
37
38
|
expandValidationConfigs,
|
|
38
39
|
skipValidation,
|
|
39
40
|
useEntityIds,
|
|
41
|
+
includeSpecialColumns,
|
|
40
42
|
fieldMapping,
|
|
41
43
|
} = config;
|
|
42
44
|
|
|
@@ -67,6 +69,9 @@ export async function processODataResponse<T>(
|
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
// Validation path
|
|
72
|
+
// Note: Special columns are excluded when using QueryBuilder.single() method,
|
|
73
|
+
// but included for RecordBuilder.get() method (both use singleMode: "exact")
|
|
74
|
+
// The exclusion is handled in QueryBuilder's processQueryResponse, not here
|
|
70
75
|
if (singleMode !== false) {
|
|
71
76
|
const validation = await validateSingleResponse<any>(
|
|
72
77
|
response,
|
|
@@ -74,6 +79,7 @@ export async function processODataResponse<T>(
|
|
|
74
79
|
selectedFields as any,
|
|
75
80
|
expandValidationConfigs,
|
|
76
81
|
singleMode,
|
|
82
|
+
includeSpecialColumns,
|
|
77
83
|
);
|
|
78
84
|
|
|
79
85
|
if (!validation.valid) {
|
|
@@ -96,6 +102,7 @@ export async function processODataResponse<T>(
|
|
|
96
102
|
schema,
|
|
97
103
|
selectedFields as any,
|
|
98
104
|
expandValidationConfigs,
|
|
105
|
+
includeSpecialColumns,
|
|
99
106
|
);
|
|
100
107
|
|
|
101
108
|
if (!validation.valid) {
|
|
@@ -223,6 +230,7 @@ export async function processQueryResponse<T>(
|
|
|
223
230
|
expandConfigs: ExpandConfig[];
|
|
224
231
|
skipValidation?: boolean;
|
|
225
232
|
useEntityIds?: boolean;
|
|
233
|
+
includeSpecialColumns?: boolean;
|
|
226
234
|
// Mapping from field names to output keys (for renamed fields in select)
|
|
227
235
|
fieldMapping?: Record<string, string>;
|
|
228
236
|
logger: InternalLogger;
|
|
@@ -235,6 +243,7 @@ export async function processQueryResponse<T>(
|
|
|
235
243
|
expandConfigs,
|
|
236
244
|
skipValidation,
|
|
237
245
|
useEntityIds,
|
|
246
|
+
includeSpecialColumns,
|
|
238
247
|
fieldMapping,
|
|
239
248
|
logger,
|
|
240
249
|
} = config;
|
|
@@ -258,6 +267,7 @@ export async function processQueryResponse<T>(
|
|
|
258
267
|
expandValidationConfigs,
|
|
259
268
|
skipValidation,
|
|
260
269
|
useEntityIds,
|
|
270
|
+
includeSpecialColumns,
|
|
261
271
|
});
|
|
262
272
|
|
|
263
273
|
// Rename fields if field mapping is provided (for renamed fields in select)
|
package/src/client/database.ts
CHANGED
|
@@ -4,10 +4,26 @@ import { EntitySet } from "./entity-set";
|
|
|
4
4
|
import { BatchBuilder } from "./batch-builder";
|
|
5
5
|
import { SchemaManager } from "./schema-manager";
|
|
6
6
|
import { FMTable } from "../orm/table";
|
|
7
|
+
import { WebhookManager } from "./webhook-builder";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
type MetadataArgs = {
|
|
10
|
+
format?: "xml" | "json";
|
|
11
|
+
/**
|
|
12
|
+
* If provided, only the metadata for the specified table will be returned.
|
|
13
|
+
* Requires FileMaker Server 22.0.4 or later.
|
|
14
|
+
*/
|
|
15
|
+
tableName?: string;
|
|
16
|
+
/**
|
|
17
|
+
* If true, a reduced payload size will be returned by omitting certain annotations.
|
|
18
|
+
*/
|
|
19
|
+
reduceAnnotations?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class Database<IncludeSpecialColumns extends boolean = false> {
|
|
9
23
|
private _useEntityIds: boolean = false;
|
|
24
|
+
private _includeSpecialColumns: IncludeSpecialColumns;
|
|
10
25
|
public readonly schema: SchemaManager;
|
|
26
|
+
public readonly webhook: WebhookManager;
|
|
11
27
|
|
|
12
28
|
constructor(
|
|
13
29
|
private readonly databaseName: string,
|
|
@@ -19,14 +35,24 @@ export class Database {
|
|
|
19
35
|
* If set to false but some occurrences do not use entity IDs, an error will be thrown
|
|
20
36
|
*/
|
|
21
37
|
useEntityIds?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Whether to include special columns (ROWID and ROWMODID) in responses.
|
|
40
|
+
* Note: Special columns are only included when there is no $select query.
|
|
41
|
+
*/
|
|
42
|
+
includeSpecialColumns?: IncludeSpecialColumns;
|
|
22
43
|
},
|
|
23
44
|
) {
|
|
24
45
|
// Initialize schema manager
|
|
25
46
|
this.schema = new SchemaManager(this.databaseName, this.context);
|
|
47
|
+
this.webhook = new WebhookManager(this.databaseName, this.context);
|
|
26
48
|
this._useEntityIds = config?.useEntityIds ?? false;
|
|
49
|
+
this._includeSpecialColumns = (config?.includeSpecialColumns ??
|
|
50
|
+
false) as IncludeSpecialColumns;
|
|
27
51
|
}
|
|
28
52
|
|
|
29
|
-
from<T extends FMTable<any, any>>(
|
|
53
|
+
from<T extends FMTable<any, any>>(
|
|
54
|
+
table: T,
|
|
55
|
+
): EntitySet<T, IncludeSpecialColumns> {
|
|
30
56
|
// Only override database-level useEntityIds if table explicitly sets it
|
|
31
57
|
// (not if it's undefined, which would override the database setting)
|
|
32
58
|
if (
|
|
@@ -37,7 +63,7 @@ export class Database {
|
|
|
37
63
|
this._useEntityIds = tableUseEntityIds;
|
|
38
64
|
}
|
|
39
65
|
}
|
|
40
|
-
return new EntitySet<T>({
|
|
66
|
+
return new EntitySet<T, IncludeSpecialColumns>({
|
|
41
67
|
occurrence: table as T,
|
|
42
68
|
databaseName: this.databaseName,
|
|
43
69
|
context: this.context,
|
|
@@ -49,19 +75,35 @@ export class Database {
|
|
|
49
75
|
* Retrieves the OData metadata for this database.
|
|
50
76
|
* @param args Optional configuration object
|
|
51
77
|
* @param args.format The format to retrieve metadata in. Defaults to "json".
|
|
78
|
+
* @param args.tableName If provided, only the metadata for the specified table will be returned. Requires FileMaker Server 22.0.4 or later.
|
|
79
|
+
* @param args.reduceAnnotations If true, a reduced payload size will be returned by omitting certain annotations.
|
|
52
80
|
* @returns The metadata in the specified format
|
|
53
81
|
*/
|
|
54
|
-
async getMetadata(args: { format: "xml" }): Promise<string>;
|
|
55
|
-
async getMetadata(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
82
|
+
async getMetadata(args: { format: "xml" } & MetadataArgs): Promise<string>;
|
|
83
|
+
async getMetadata(
|
|
84
|
+
args?: { format?: "json" } & MetadataArgs,
|
|
85
|
+
): Promise<Metadata>;
|
|
86
|
+
async getMetadata(args?: MetadataArgs): Promise<string | Metadata> {
|
|
87
|
+
// Build the URL - if tableName is provided, append %23{tableName} to the path
|
|
88
|
+
let url = `/${this.databaseName}/$metadata`;
|
|
89
|
+
if (args?.tableName) {
|
|
90
|
+
url = `/${this.databaseName}/$metadata%23${args.tableName}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Build headers
|
|
94
|
+
const headers: Record<string, string> = {
|
|
95
|
+
Accept: args?.format === "xml" ? "application/xml" : "application/json",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Add Prefer header if reduceAnnotations is true
|
|
99
|
+
if (args?.reduceAnnotations) {
|
|
100
|
+
headers["Prefer"] = 'include-annotations="-*"';
|
|
101
|
+
}
|
|
102
|
+
|
|
59
103
|
const result = await this.context._makeRequest<
|
|
60
104
|
Record<string, Metadata> | string
|
|
61
|
-
>(
|
|
62
|
-
headers
|
|
63
|
-
Accept: args?.format === "xml" ? "application/xml" : "application/json",
|
|
64
|
-
},
|
|
105
|
+
>(url, {
|
|
106
|
+
headers,
|
|
65
107
|
});
|
|
66
108
|
if (result.error) {
|
|
67
109
|
throw result.error;
|
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
ExecutionContext,
|
|
3
3
|
ExecutableBuilder,
|
|
4
4
|
Result,
|
|
5
|
-
|
|
5
|
+
WithSpecialColumns,
|
|
6
6
|
ExecuteOptions,
|
|
7
7
|
ExecuteMethodOptions,
|
|
8
8
|
} from "../types";
|
|
@@ -26,17 +26,21 @@ export class DeleteBuilder<Occ extends FMTable<any, any>> {
|
|
|
26
26
|
private context: ExecutionContext;
|
|
27
27
|
private table: Occ;
|
|
28
28
|
private databaseUseEntityIds: boolean;
|
|
29
|
+
private databaseIncludeSpecialColumns: boolean;
|
|
29
30
|
|
|
30
31
|
constructor(config: {
|
|
31
32
|
occurrence: Occ;
|
|
32
33
|
databaseName: string;
|
|
33
34
|
context: ExecutionContext;
|
|
34
35
|
databaseUseEntityIds?: boolean;
|
|
36
|
+
databaseIncludeSpecialColumns?: boolean;
|
|
35
37
|
}) {
|
|
36
38
|
this.table = config.occurrence;
|
|
37
39
|
this.databaseName = config.databaseName;
|
|
38
40
|
this.context = config.context;
|
|
39
41
|
this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
|
|
42
|
+
this.databaseIncludeSpecialColumns =
|
|
43
|
+
config.databaseIncludeSpecialColumns ?? false;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/**
|
package/src/client/entity-set.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
getDefaultSelect,
|
|
20
20
|
getTableName,
|
|
21
21
|
getTableColumns,
|
|
22
|
+
getTableSchema,
|
|
22
23
|
} from "../orm/table";
|
|
23
24
|
import type { FieldBuilder } from "../orm/field-builders";
|
|
24
25
|
import { createLogger, InternalLogger } from "../logger";
|
|
@@ -41,16 +42,20 @@ type ExtractColumnsFromOcc<T> =
|
|
|
41
42
|
: never
|
|
42
43
|
: never;
|
|
43
44
|
|
|
44
|
-
export class EntitySet<
|
|
45
|
+
export class EntitySet<
|
|
46
|
+
Occ extends FMTable<any, any>,
|
|
47
|
+
DatabaseIncludeSpecialColumns extends boolean = false,
|
|
48
|
+
> {
|
|
45
49
|
private occurrence: Occ;
|
|
46
50
|
private databaseName: string;
|
|
47
51
|
private context: ExecutionContext;
|
|
48
|
-
private database: Database
|
|
52
|
+
private database: Database<DatabaseIncludeSpecialColumns>; // Database instance for accessing occurrences
|
|
49
53
|
private isNavigateFromEntitySet?: boolean;
|
|
50
54
|
private navigateRelation?: string;
|
|
51
55
|
private navigateSourceTableName?: string;
|
|
52
56
|
private navigateBasePath?: string; // Full base path for chained navigations
|
|
53
57
|
private databaseUseEntityIds: boolean;
|
|
58
|
+
private databaseIncludeSpecialColumns: DatabaseIncludeSpecialColumns;
|
|
54
59
|
private logger: InternalLogger;
|
|
55
60
|
|
|
56
61
|
constructor(config: {
|
|
@@ -66,17 +71,23 @@ export class EntitySet<Occ extends FMTable<any, any>> {
|
|
|
66
71
|
// Get useEntityIds from database if available, otherwise default to false
|
|
67
72
|
this.databaseUseEntityIds =
|
|
68
73
|
(config.database as any)?._useEntityIds ?? false;
|
|
74
|
+
// Get includeSpecialColumns from database if available, otherwise default to false
|
|
75
|
+
this.databaseIncludeSpecialColumns =
|
|
76
|
+
(config.database as any)?._includeSpecialColumns ?? false;
|
|
69
77
|
this.logger = config.context?._getLogger?.() ?? createLogger();
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
// Type-only method to help TypeScript infer the schema from table
|
|
73
|
-
static create<
|
|
81
|
+
static create<
|
|
82
|
+
Occ extends FMTable<any, any>,
|
|
83
|
+
DatabaseIncludeSpecialColumns extends boolean = false,
|
|
84
|
+
>(config: {
|
|
74
85
|
occurrence: Occ;
|
|
75
86
|
databaseName: string;
|
|
76
87
|
context: ExecutionContext;
|
|
77
|
-
database: Database
|
|
78
|
-
}): EntitySet<Occ> {
|
|
79
|
-
return new EntitySet<Occ>({
|
|
88
|
+
database: Database<DatabaseIncludeSpecialColumns>;
|
|
89
|
+
}): EntitySet<Occ, DatabaseIncludeSpecialColumns> {
|
|
90
|
+
return new EntitySet<Occ, DatabaseIncludeSpecialColumns>({
|
|
80
91
|
occurrence: config.occurrence,
|
|
81
92
|
databaseName: config.databaseName,
|
|
82
93
|
context: config.context,
|
|
@@ -89,33 +100,30 @@ export class EntitySet<Occ extends FMTable<any, any>> {
|
|
|
89
100
|
keyof InferSchemaOutputFromFMTable<Occ>,
|
|
90
101
|
false,
|
|
91
102
|
false,
|
|
92
|
-
{}
|
|
103
|
+
{},
|
|
104
|
+
DatabaseIncludeSpecialColumns
|
|
93
105
|
> {
|
|
94
|
-
const builder = new QueryBuilder<
|
|
106
|
+
const builder = new QueryBuilder<
|
|
107
|
+
Occ,
|
|
108
|
+
keyof InferSchemaOutputFromFMTable<Occ>,
|
|
109
|
+
false,
|
|
110
|
+
false,
|
|
111
|
+
{},
|
|
112
|
+
DatabaseIncludeSpecialColumns
|
|
113
|
+
>({
|
|
95
114
|
occurrence: this.occurrence as Occ,
|
|
96
115
|
databaseName: this.databaseName,
|
|
97
116
|
context: this.context,
|
|
98
117
|
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
118
|
+
databaseIncludeSpecialColumns: this.databaseIncludeSpecialColumns,
|
|
99
119
|
});
|
|
100
120
|
|
|
101
121
|
// Apply defaultSelect if occurrence exists and select hasn't been called
|
|
102
122
|
if (this.occurrence) {
|
|
103
123
|
// FMTable - access via helper functions
|
|
104
124
|
const defaultSelectValue = getDefaultSelect(this.occurrence);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (tableSchema) {
|
|
109
|
-
// Extract schema from StandardSchemaV1
|
|
110
|
-
const zodSchema = tableSchema["~standard"]?.schema;
|
|
111
|
-
if (
|
|
112
|
-
zodSchema &&
|
|
113
|
-
typeof zodSchema === "object" &&
|
|
114
|
-
"shape" in zodSchema
|
|
115
|
-
) {
|
|
116
|
-
schema = zodSchema.shape as Record<string, StandardSchemaV1>;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
125
|
+
// Schema is stored directly as Partial<Record<keyof TFields, StandardSchemaV1>>
|
|
126
|
+
const schema = getTableSchema(this.occurrence);
|
|
119
127
|
|
|
120
128
|
if (defaultSelectValue === "schema") {
|
|
121
129
|
// Use getTableColumns to get all columns and select them
|
|
@@ -124,12 +132,22 @@ export class EntitySet<Occ extends FMTable<any, any>> {
|
|
|
124
132
|
const allColumns = getTableColumns(
|
|
125
133
|
this.occurrence,
|
|
126
134
|
) as ExtractColumnsFromOcc<Occ>;
|
|
127
|
-
|
|
135
|
+
|
|
136
|
+
// Include special columns if enabled at database level
|
|
137
|
+
const systemColumns = this.databaseIncludeSpecialColumns
|
|
138
|
+
? { ROWID: true, ROWMODID: true }
|
|
139
|
+
: undefined;
|
|
140
|
+
|
|
141
|
+
return builder
|
|
142
|
+
.select(allColumns, systemColumns)
|
|
143
|
+
.top(1000) as QueryBuilder<
|
|
128
144
|
Occ,
|
|
129
145
|
keyof InferSchemaOutputFromFMTable<Occ>,
|
|
130
146
|
false,
|
|
131
147
|
false,
|
|
132
|
-
{}
|
|
148
|
+
{},
|
|
149
|
+
DatabaseIncludeSpecialColumns,
|
|
150
|
+
typeof systemColumns
|
|
133
151
|
>;
|
|
134
152
|
} else if (typeof defaultSelectValue === "object") {
|
|
135
153
|
// defaultSelectValue is a select object (Record<string, Column>)
|
|
@@ -141,7 +159,8 @@ export class EntitySet<Occ extends FMTable<any, any>> {
|
|
|
141
159
|
keyof InferSchemaOutputFromFMTable<Occ>,
|
|
142
160
|
false,
|
|
143
161
|
false,
|
|
144
|
-
{}
|
|
162
|
+
{},
|
|
163
|
+
DatabaseIncludeSpecialColumns
|
|
145
164
|
>;
|
|
146
165
|
}
|
|
147
166
|
// If defaultSelect is "all", no changes needed (current behavior)
|
|
@@ -173,34 +192,31 @@ export class EntitySet<Occ extends FMTable<any, any>> {
|
|
|
173
192
|
false,
|
|
174
193
|
undefined,
|
|
175
194
|
keyof InferSchemaOutputFromFMTable<Occ>,
|
|
176
|
-
{}
|
|
195
|
+
{},
|
|
196
|
+
DatabaseIncludeSpecialColumns
|
|
177
197
|
> {
|
|
178
|
-
const builder = new RecordBuilder<
|
|
198
|
+
const builder = new RecordBuilder<
|
|
199
|
+
Occ,
|
|
200
|
+
false,
|
|
201
|
+
undefined,
|
|
202
|
+
keyof InferSchemaOutputFromFMTable<Occ>,
|
|
203
|
+
{},
|
|
204
|
+
DatabaseIncludeSpecialColumns
|
|
205
|
+
>({
|
|
179
206
|
occurrence: this.occurrence,
|
|
180
207
|
databaseName: this.databaseName,
|
|
181
208
|
context: this.context,
|
|
182
209
|
recordId: id,
|
|
183
210
|
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
211
|
+
databaseIncludeSpecialColumns: this.databaseIncludeSpecialColumns,
|
|
184
212
|
});
|
|
185
213
|
|
|
186
214
|
// Apply defaultSelect if occurrence exists
|
|
187
215
|
if (this.occurrence) {
|
|
188
216
|
// FMTable - access via helper functions
|
|
189
217
|
const defaultSelectValue = getDefaultSelect(this.occurrence);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (tableSchema) {
|
|
194
|
-
// Extract schema from StandardSchemaV1
|
|
195
|
-
const zodSchema = tableSchema["~standard"]?.schema;
|
|
196
|
-
if (
|
|
197
|
-
zodSchema &&
|
|
198
|
-
typeof zodSchema === "object" &&
|
|
199
|
-
"shape" in zodSchema
|
|
200
|
-
) {
|
|
201
|
-
schema = zodSchema.shape as Record<string, StandardSchemaV1>;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
218
|
+
// Schema is stored directly as Partial<Record<keyof TFields, StandardSchemaV1>>
|
|
219
|
+
const schema = getTableSchema(this.occurrence);
|
|
204
220
|
|
|
205
221
|
if (defaultSelectValue === "schema") {
|
|
206
222
|
// Use getTableColumns to get all columns and select them
|
|
@@ -209,7 +225,13 @@ export class EntitySet<Occ extends FMTable<any, any>> {
|
|
|
209
225
|
const allColumns = getTableColumns(
|
|
210
226
|
this.occurrence as any,
|
|
211
227
|
) as ExtractColumnsFromOcc<Occ>;
|
|
212
|
-
|
|
228
|
+
|
|
229
|
+
// Include special columns if enabled at database level
|
|
230
|
+
const systemColumns = this.databaseIncludeSpecialColumns
|
|
231
|
+
? { ROWID: true, ROWMODID: true }
|
|
232
|
+
: undefined;
|
|
233
|
+
|
|
234
|
+
const selectedBuilder = builder.select(allColumns, systemColumns);
|
|
213
235
|
// Propagate navigation context if present
|
|
214
236
|
if (
|
|
215
237
|
this.isNavigateFromEntitySet &&
|
|
@@ -293,6 +315,7 @@ export class EntitySet<Occ extends FMTable<any, any>> {
|
|
|
293
315
|
data: data as any, // Input type is validated/transformed at runtime
|
|
294
316
|
returnPreference: returnPreference as any,
|
|
295
317
|
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
318
|
+
databaseIncludeSpecialColumns: this.databaseIncludeSpecialColumns,
|
|
296
319
|
});
|
|
297
320
|
}
|
|
298
321
|
|
|
@@ -323,6 +346,7 @@ export class EntitySet<Occ extends FMTable<any, any>> {
|
|
|
323
346
|
data: data as any, // Input type is validated/transformed at runtime
|
|
324
347
|
returnPreference: returnPreference as any,
|
|
325
348
|
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
349
|
+
databaseIncludeSpecialColumns: this.databaseIncludeSpecialColumns,
|
|
326
350
|
});
|
|
327
351
|
}
|
|
328
352
|
|
|
@@ -332,13 +356,17 @@ export class EntitySet<Occ extends FMTable<any, any>> {
|
|
|
332
356
|
databaseName: this.databaseName,
|
|
333
357
|
context: this.context,
|
|
334
358
|
databaseUseEntityIds: this.databaseUseEntityIds,
|
|
359
|
+
databaseIncludeSpecialColumns: this.databaseIncludeSpecialColumns,
|
|
335
360
|
}) as any;
|
|
336
361
|
}
|
|
337
362
|
|
|
338
363
|
// Implementation
|
|
339
364
|
navigate<TargetTable extends FMTable<any, any>>(
|
|
340
365
|
targetTable: ValidExpandTarget<Occ, TargetTable>,
|
|
341
|
-
): EntitySet<
|
|
366
|
+
): EntitySet<
|
|
367
|
+
TargetTable extends FMTable<any, any> ? TargetTable : never,
|
|
368
|
+
DatabaseIncludeSpecialColumns
|
|
369
|
+
> {
|
|
342
370
|
// Check if it's an FMTable object or a string
|
|
343
371
|
let relationName: string;
|
|
344
372
|
|
|
@@ -361,7 +389,7 @@ export class EntitySet<Occ extends FMTable<any, any>> {
|
|
|
361
389
|
}
|
|
362
390
|
|
|
363
391
|
// Create EntitySet with target table
|
|
364
|
-
const entitySet = new EntitySet<any>({
|
|
392
|
+
const entitySet = new EntitySet<any, DatabaseIncludeSpecialColumns>({
|
|
365
393
|
occurrence: targetTable,
|
|
366
394
|
databaseName: this.databaseName,
|
|
367
395
|
context: this.context,
|