@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.
Files changed (74) hide show
  1. package/README.md +198 -0
  2. package/dist/esm/client/builders/default-select.d.ts +4 -1
  3. package/dist/esm/client/builders/default-select.js +3 -2
  4. package/dist/esm/client/builders/default-select.js.map +1 -1
  5. package/dist/esm/client/builders/expand-builder.js.map +1 -1
  6. package/dist/esm/client/builders/query-string-builder.d.ts +1 -0
  7. package/dist/esm/client/builders/query-string-builder.js.map +1 -1
  8. package/dist/esm/client/builders/response-processor.d.ts +2 -0
  9. package/dist/esm/client/builders/response-processor.js +8 -3
  10. package/dist/esm/client/builders/response-processor.js.map +1 -1
  11. package/dist/esm/client/database.d.ts +27 -4
  12. package/dist/esm/client/database.js +17 -4
  13. package/dist/esm/client/database.js.map +1 -1
  14. package/dist/esm/client/delete-builder.d.ts +2 -0
  15. package/dist/esm/client/delete-builder.js +2 -0
  16. package/dist/esm/client/delete-builder.js.map +1 -1
  17. package/dist/esm/client/entity-set.d.ts +8 -7
  18. package/dist/esm/client/entity-set.js +21 -26
  19. package/dist/esm/client/entity-set.js.map +1 -1
  20. package/dist/esm/client/error-parser.js.map +1 -1
  21. package/dist/esm/client/filemaker-odata.d.ts +15 -2
  22. package/dist/esm/client/filemaker-odata.js +25 -2
  23. package/dist/esm/client/filemaker-odata.js.map +1 -1
  24. package/dist/esm/client/insert-builder.d.ts +2 -0
  25. package/dist/esm/client/insert-builder.js +2 -0
  26. package/dist/esm/client/insert-builder.js.map +1 -1
  27. package/dist/esm/client/query/query-builder.d.ts +26 -15
  28. package/dist/esm/client/query/query-builder.js +43 -9
  29. package/dist/esm/client/query/query-builder.js.map +1 -1
  30. package/dist/esm/client/query/response-processor.d.ts +1 -0
  31. package/dist/esm/client/query/types.d.ts +24 -3
  32. package/dist/esm/client/record-builder.d.ts +24 -13
  33. package/dist/esm/client/record-builder.js +50 -17
  34. package/dist/esm/client/record-builder.js.map +1 -1
  35. package/dist/esm/client/update-builder.d.ts +2 -0
  36. package/dist/esm/client/update-builder.js +2 -0
  37. package/dist/esm/client/update-builder.js.map +1 -1
  38. package/dist/esm/client/webhook-builder.d.ts +126 -0
  39. package/dist/esm/client/webhook-builder.js +197 -0
  40. package/dist/esm/client/webhook-builder.js.map +1 -0
  41. package/dist/esm/index.d.ts +1 -0
  42. package/dist/esm/orm/field-builders.d.ts +12 -2
  43. package/dist/esm/orm/field-builders.js +18 -2
  44. package/dist/esm/orm/field-builders.js.map +1 -1
  45. package/dist/esm/orm/table.d.ts +23 -10
  46. package/dist/esm/orm/table.js +17 -30
  47. package/dist/esm/orm/table.js.map +1 -1
  48. package/dist/esm/types.d.ts +32 -2
  49. package/dist/esm/types.js.map +1 -1
  50. package/dist/esm/validation.d.ts +5 -5
  51. package/dist/esm/validation.js +44 -13
  52. package/dist/esm/validation.js.map +1 -1
  53. package/package.json +2 -2
  54. package/src/client/builders/default-select.ts +12 -1
  55. package/src/client/builders/expand-builder.ts +1 -1
  56. package/src/client/builders/query-string-builder.ts +6 -0
  57. package/src/client/builders/response-processor.ts +10 -0
  58. package/src/client/database.ts +54 -12
  59. package/src/client/delete-builder.ts +5 -1
  60. package/src/client/entity-set.ts +72 -44
  61. package/src/client/error-parser.ts +3 -0
  62. package/src/client/filemaker-odata.ts +39 -6
  63. package/src/client/insert-builder.ts +4 -0
  64. package/src/client/query/query-builder.ts +198 -35
  65. package/src/client/query/response-processor.ts +15 -25
  66. package/src/client/query/types.ts +35 -6
  67. package/src/client/record-builder.ts +159 -32
  68. package/src/client/update-builder.ts +4 -1
  69. package/src/client/webhook-builder.ts +285 -0
  70. package/src/index.ts +6 -0
  71. package/src/orm/field-builders.ts +24 -2
  72. package/src/orm/table.ts +40 -48
  73. package/src/types.ts +62 -6
  74. 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":"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"}
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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proofkit/fmodata",
3
- "version": "0.1.0-alpha.19",
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.12"
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
- return [...new Set(allFields.filter((f) => !containerFields.includes(f)))];
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> | undefined;
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)
@@ -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
- export class Database {
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>>(table: T): EntitySet<T> {
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(args?: { format?: "json" }): Promise<Metadata>;
56
- async getMetadata(args?: {
57
- format?: "xml" | "json";
58
- }): Promise<string | Metadata> {
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
- >(`/${this.databaseName}/$metadata`, {
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
- WithSystemFields,
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
  /**
@@ -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<Occ extends FMTable<any, any>> {
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; // Database instance for accessing occurrences
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<Occ extends FMTable<any, any>>(config: {
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<Occ>({
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
- const tableSchema = (this.occurrence as any)[FMTableClass.Symbol.Schema];
106
- let schema: Record<string, StandardSchemaV1> | undefined;
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
- return builder.select(allColumns).top(1000) as QueryBuilder<
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<Occ>({
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
- const tableSchema = (this.occurrence as any)[FMTableClass.Symbol.Schema];
191
- let schema: Record<string, StandardSchemaV1> | undefined;
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
- const selectedBuilder = builder.select(allColumns);
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<TargetTable extends FMTable<any, any> ? TargetTable : never> {
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,