@proofkit/fmodata 0.1.0-beta.24 → 0.1.0-beta.26

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.
@@ -17,7 +17,7 @@ function getDefaultSelectFields(table, includeSpecialColumns) {
17
17
  const baseTableConfig = getBaseTableConfig(table);
18
18
  const allFields = Object.keys(baseTableConfig.schema);
19
19
  const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];
20
- return fields;
20
+ return fields.length > 0 ? fields : void 0;
21
21
  }
22
22
  if (Array.isArray(defaultSelect)) {
23
23
  return [...new Set(defaultSelect.filter((f) => !containerFields.includes(f)))];
@@ -1 +1 @@
1
- {"version":3,"file":"default-select.js","sources":["../../../../src/client/builders/default-select.ts"],"sourcesContent":["import { isColumn } from \"../../orm/column\";\nimport type { FMTable } from \"../../orm/table\";\nimport { FMTable as FMTableClass, getBaseTableConfig } from \"../../orm/table\";\n\n/**\n * Helper function to get container field names from a table.\n * Container fields cannot be selected via $select in FileMaker OData API.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\nfunction getContainerFieldNames(table: FMTable<any, any>): string[] {\n const baseTableConfig = getBaseTableConfig(table);\n if (!baseTableConfig?.containerFields) {\n return [];\n }\n return baseTableConfig.containerFields as string[];\n}\n\n/**\n * Gets default select fields from a table definition.\n * Returns undefined if defaultSelect is \"all\".\n * Automatically filters out container fields since they cannot be selected via $select.\n *\n * @param table - The table occurrence\n * @param includeSpecialColumns - If true, includes ROWID and ROWMODID when defaultSelect is \"schema\"\n */\nexport function getDefaultSelectFields(\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table: FMTable<any, any> | undefined,\n includeSpecialColumns?: boolean,\n): string[] | undefined {\n if (!table) {\n return undefined;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const defaultSelect = (table as any)[FMTableClass.Symbol.DefaultSelect];\n const containerFields = getContainerFieldNames(table);\n\n if (defaultSelect === \"schema\") {\n const baseTableConfig = getBaseTableConfig(table);\n const allFields = Object.keys(baseTableConfig.schema);\n // Filter out container fields\n const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];\n\n // Add special columns if requested\n if (includeSpecialColumns) {\n fields.push(\"ROWID\", \"ROWMODID\");\n }\n\n return fields;\n }\n\n if (Array.isArray(defaultSelect)) {\n // Filter out container fields\n return [...new Set(defaultSelect.filter((f) => !containerFields.includes(f)))];\n }\n\n // Check if defaultSelect is a Record<string, Column> (resolved from function)\n if (typeof defaultSelect === \"object\" && defaultSelect !== null && !Array.isArray(defaultSelect)) {\n // Extract field names from Column instances\n const fieldNames: string[] = [];\n for (const value of Object.values(defaultSelect)) {\n if (isColumn(value)) {\n fieldNames.push(value.fieldName);\n }\n }\n if (fieldNames.length > 0) {\n // Filter out container fields\n return [...new Set(fieldNames.filter((f) => !containerFields.includes(f)))];\n }\n }\n\n // defaultSelect is \"all\" or undefined\n return undefined;\n}\n"],"names":["FMTableClass"],"mappings":";;AASA,SAAS,uBAAuB,OAAoC;AAClE,QAAM,kBAAkB,mBAAmB,KAAK;AAChD,MAAI,EAAC,mDAAiB,kBAAiB;AACrC,WAAO,CAAA;AAAA,EACT;AACA,SAAO,gBAAgB;AACzB;AAUO,SAAS,uBAEd,OACA,uBACsB;AACtB,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAGA,QAAM,gBAAiB,MAAcA,QAAa,OAAO,aAAa;AACtE,QAAM,kBAAkB,uBAAuB,KAAK;AAEpD,MAAI,kBAAkB,UAAU;AAC9B,UAAM,kBAAkB,mBAAmB,KAAK;AAChD,UAAM,YAAY,OAAO,KAAK,gBAAgB,MAAM;AAEpD,UAAM,SAAS,CAAC,GAAG,IAAI,IAAI,UAAU,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAOjF,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,aAAa,GAAG;AAEhC,WAAO,CAAC,GAAG,IAAI,IAAI,cAAc,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAAA,EAC/E;AAGA,MAAI,OAAO,kBAAkB,YAAY,kBAAkB,QAAQ,CAAC,MAAM,QAAQ,aAAa,GAAG;AAEhG,UAAM,aAAuB,CAAA;AAC7B,eAAW,SAAS,OAAO,OAAO,aAAa,GAAG;AAChD,UAAI,SAAS,KAAK,GAAG;AACnB,mBAAW,KAAK,MAAM,SAAS;AAAA,MACjC;AAAA,IACF;AACA,QAAI,WAAW,SAAS,GAAG;AAEzB,aAAO,CAAC,GAAG,IAAI,IAAI,WAAW,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAAA,IAC5E;AAAA,EACF;AAGA,SAAO;AACT;"}
1
+ {"version":3,"file":"default-select.js","sources":["../../../../src/client/builders/default-select.ts"],"sourcesContent":["import { isColumn } from \"../../orm/column\";\nimport type { FMTable } from \"../../orm/table\";\nimport { FMTable as FMTableClass, getBaseTableConfig } from \"../../orm/table\";\n\n/**\n * Helper function to get container field names from a table.\n * Container fields cannot be selected via $select in FileMaker OData API.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\nfunction getContainerFieldNames(table: FMTable<any, any>): string[] {\n const baseTableConfig = getBaseTableConfig(table);\n if (!baseTableConfig?.containerFields) {\n return [];\n }\n return baseTableConfig.containerFields as string[];\n}\n\n/**\n * Gets default select fields from a table definition.\n * Returns undefined if defaultSelect is \"all\".\n * Automatically filters out container fields since they cannot be selected via $select.\n *\n * @param table - The table occurrence\n * @param includeSpecialColumns - If true, includes ROWID and ROWMODID when defaultSelect is \"schema\"\n */\nexport function getDefaultSelectFields(\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table: FMTable<any, any> | undefined,\n includeSpecialColumns?: boolean,\n): string[] | undefined {\n if (!table) {\n return undefined;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const defaultSelect = (table as any)[FMTableClass.Symbol.DefaultSelect];\n const containerFields = getContainerFieldNames(table);\n\n if (defaultSelect === \"schema\") {\n const baseTableConfig = getBaseTableConfig(table);\n const allFields = Object.keys(baseTableConfig.schema);\n // Filter out container fields\n const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];\n\n // Add special columns if requested\n if (includeSpecialColumns) {\n fields.push(\"ROWID\", \"ROWMODID\");\n }\n\n // Return undefined (meaning \"all\") when schema has no fields with validators,\n // rather than an empty array which would generate an empty $select=\n return fields.length > 0 ? fields : undefined;\n }\n\n if (Array.isArray(defaultSelect)) {\n // Filter out container fields\n return [...new Set(defaultSelect.filter((f) => !containerFields.includes(f)))];\n }\n\n // Check if defaultSelect is a Record<string, Column> (resolved from function)\n if (typeof defaultSelect === \"object\" && defaultSelect !== null && !Array.isArray(defaultSelect)) {\n // Extract field names from Column instances\n const fieldNames: string[] = [];\n for (const value of Object.values(defaultSelect)) {\n if (isColumn(value)) {\n fieldNames.push(value.fieldName);\n }\n }\n if (fieldNames.length > 0) {\n // Filter out container fields\n return [...new Set(fieldNames.filter((f) => !containerFields.includes(f)))];\n }\n }\n\n // defaultSelect is \"all\" or undefined\n return undefined;\n}\n"],"names":["FMTableClass"],"mappings":";;AASA,SAAS,uBAAuB,OAAoC;AAClE,QAAM,kBAAkB,mBAAmB,KAAK;AAChD,MAAI,EAAC,mDAAiB,kBAAiB;AACrC,WAAO,CAAA;AAAA,EACT;AACA,SAAO,gBAAgB;AACzB;AAUO,SAAS,uBAEd,OACA,uBACsB;AACtB,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAGA,QAAM,gBAAiB,MAAcA,QAAa,OAAO,aAAa;AACtE,QAAM,kBAAkB,uBAAuB,KAAK;AAEpD,MAAI,kBAAkB,UAAU;AAC9B,UAAM,kBAAkB,mBAAmB,KAAK;AAChD,UAAM,YAAY,OAAO,KAAK,gBAAgB,MAAM;AAEpD,UAAM,SAAS,CAAC,GAAG,IAAI,IAAI,UAAU,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AASjF,WAAO,OAAO,SAAS,IAAI,SAAS;AAAA,EACtC;AAEA,MAAI,MAAM,QAAQ,aAAa,GAAG;AAEhC,WAAO,CAAC,GAAG,IAAI,IAAI,cAAc,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAAA,EAC/E;AAGA,MAAI,OAAO,kBAAkB,YAAY,kBAAkB,QAAQ,CAAC,MAAM,QAAQ,aAAa,GAAG;AAEhG,UAAM,aAAuB,CAAA;AAC7B,eAAW,SAAS,OAAO,OAAO,aAAa,GAAG;AAChD,UAAI,SAAS,KAAK,GAAG;AACnB,mBAAW,KAAK,MAAM,SAAS;AAAA,MACjC;AAAA,IACF;AACA,QAAI,WAAW,SAAS,GAAG;AAEzB,aAAO,CAAC,GAAG,IAAI,IAAI,WAAW,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAAA,IAC5E;AAAA,EACF;AAGA,SAAO;AACT;"}
@@ -154,7 +154,9 @@ class ExpandBuilder {
154
154
  if (opts.select) {
155
155
  const selectArray = Array.isArray(opts.select) ? opts.select.map(String) : [String(opts.select)];
156
156
  const selectFields = formatSelectFields(selectArray, config.targetTable, this.useEntityIds);
157
- parts.push(`$select=${selectFields}`);
157
+ if (selectFields) {
158
+ parts.push(`$select=${selectFields}`);
159
+ }
158
160
  }
159
161
  if (opts.filter) {
160
162
  const filterQuery = buildQuery({ filter: opts.filter });
@@ -1 +1 @@
1
- {"version":3,"file":"expand-builder.js","sources":["../../../../src/client/builders/expand-builder.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport buildQuery, { type QueryOptions } from \"odata-query\";\nimport type { InternalLogger } from \"../../logger\";\nimport { FMTable, getBaseTableConfig, getNavigationPaths, getTableName } from \"../../orm/table\";\nimport type { ExpandValidationConfig } from \"../../validation\";\nimport { getDefaultSelectFields } from \"./default-select\";\nimport { formatSelectFields } from \"./select-utils\";\nimport type { ExpandConfig } from \"./shared-types\";\n\nconst FILTER_QUERY_REGEX = /\\$filter=([^&]+)/;\n\n/**\n * Builds OData expand query strings and validation configs.\n * Handles nested expands recursively and transforms relation names to FMTIDs\n * when using entity IDs.\n */\nexport class ExpandBuilder {\n private readonly useEntityIds: boolean;\n private readonly logger: InternalLogger;\n\n constructor(useEntityIds: boolean, logger: InternalLogger) {\n this.useEntityIds = useEntityIds;\n this.logger = logger;\n }\n\n /**\n * Builds OData $expand query string from expand configurations.\n */\n buildExpandString(configs: ExpandConfig[]): string {\n if (configs.length === 0) {\n return \"\";\n }\n\n return configs.map((config) => this.buildSingleExpand(config)).join(\",\");\n }\n\n /**\n * Builds validation configs for expanded navigation properties.\n */\n buildValidationConfigs(configs: ExpandConfig[]): ExpandValidationConfig[] {\n return configs.map((config) => {\n const targetTable = config.targetTable;\n\n let targetSchema: Partial<Record<string, StandardSchemaV1>> | undefined;\n if (targetTable) {\n const baseTableConfig = getBaseTableConfig(targetTable);\n const containerFields = baseTableConfig.containerFields || [];\n\n // Filter out container fields from schema\n const schema = { ...baseTableConfig.schema };\n for (const containerField of containerFields) {\n delete schema[containerField as string];\n }\n\n targetSchema = schema;\n }\n\n let selectedFields: string[] | undefined;\n if (config.options?.select) {\n selectedFields = Array.isArray(config.options.select)\n ? config.options.select.map(String)\n : [String(config.options.select)];\n }\n\n // Recursively build validation configs for nested expands\n const nestedExpands = config.nestedExpandConfigs\n ? this.buildValidationConfigs(config.nestedExpandConfigs)\n : undefined;\n\n return {\n relation: config.relation,\n targetSchema,\n targetTable,\n table: targetTable,\n selectedFields,\n nestedExpands,\n };\n });\n }\n\n /**\n * Process an expand() call and return the expand config.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param targetTable - The target table to expand to\n * @param sourceTable - The source table (for validation)\n * @param callback - Optional callback to configure the expand query\n * @param builderFactory - Function that creates a QueryBuilder for the target table\n * @returns ExpandConfig to add to the builder's expandConfigs array\n */\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration, generic Builder type\n processExpand<TargetTable extends FMTable<any, any>, Builder = any>(\n targetTable: TargetTable,\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n sourceTable: FMTable<any, any> | undefined,\n callback?: (builder: Builder) => Builder,\n builderFactory?: () => Builder,\n ): ExpandConfig {\n // Extract name and validate\n const relationName = getTableName(targetTable);\n\n // Runtime validation: Check if relation name is in navigationPaths\n if (sourceTable) {\n const navigationPaths = getNavigationPaths(sourceTable);\n if (navigationPaths && !navigationPaths.includes(relationName)) {\n this.logger.warn(\n `Cannot expand to \"${relationName}\". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(\", \") : \"none\"}`,\n );\n }\n }\n\n if (callback && builderFactory) {\n // Create a new QueryBuilder for the target table\n const targetBuilder = builderFactory();\n\n // Pass to callback and get configured builder\n const configuredBuilder = callback(targetBuilder);\n\n // Extract the builder's query options\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryOptions configuration\n const expandOptions: Partial<QueryOptions<any>> = {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for internal builder property access\n ...(configuredBuilder as any).queryOptions,\n };\n\n // If callback didn't provide select, apply defaultSelect from target table\n if (!expandOptions.select) {\n const defaultFields = getDefaultSelectFields(targetTable);\n if (defaultFields) {\n expandOptions.select = defaultFields;\n }\n }\n\n // If the configured builder has nested expands, we need to include them\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for internal builder property access\n const nestedExpandConfigs = (configuredBuilder as any).expandConfigs;\n if (nestedExpandConfigs?.length > 0) {\n // Build nested expand string from the configured builder's expand configs\n const nestedExpandString = this.buildExpandString(nestedExpandConfigs);\n if (nestedExpandString) {\n // Add nested expand to options\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for expand string\n expandOptions.expand = nestedExpandString as any;\n }\n }\n\n return {\n relation: relationName,\n options: expandOptions,\n targetTable,\n nestedExpandConfigs: nestedExpandConfigs?.length > 0 ? nestedExpandConfigs : undefined,\n };\n }\n // Simple expand without callback - apply defaultSelect if available\n const defaultFields = getDefaultSelectFields(targetTable);\n if (defaultFields) {\n return {\n relation: relationName,\n options: { select: defaultFields },\n targetTable,\n };\n }\n return {\n relation: relationName,\n targetTable,\n };\n }\n\n /**\n * Builds a single expand string with its options.\n */\n private buildSingleExpand(config: ExpandConfig): string {\n const relationName = this.resolveRelationName(config);\n const parts = this.buildExpandParts(config);\n\n if (parts.length === 0) {\n return relationName;\n }\n\n return `${relationName}(${parts.join(\";\")})`;\n }\n\n /**\n * Resolves relation name, using FMTID if entity IDs are enabled.\n */\n private resolveRelationName(config: ExpandConfig): string {\n if (!this.useEntityIds) {\n return config.relation;\n }\n\n const targetTable = config.targetTable;\n if (targetTable && FMTable.Symbol.EntityId in targetTable) {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const tableId = (targetTable as any)[FMTable.Symbol.EntityId] as `FMTID:${string}` | undefined;\n if (tableId) {\n return tableId;\n }\n }\n\n return config.relation;\n }\n\n /**\n * Builds expand parts (select, filter, orderBy, etc.) for a single expand.\n */\n private buildExpandParts(config: ExpandConfig): string[] {\n if (!config.options || Object.keys(config.options).length === 0) {\n return [];\n }\n\n const parts: string[] = [];\n const opts = config.options;\n\n if (opts.select) {\n const selectArray = Array.isArray(opts.select) ? opts.select.map(String) : [String(opts.select)];\n const selectFields = formatSelectFields(selectArray, config.targetTable, this.useEntityIds);\n parts.push(`$select=${selectFields}`);\n }\n\n if (opts.filter) {\n const filterQuery = buildQuery({ filter: opts.filter });\n const match = filterQuery.match(FILTER_QUERY_REGEX);\n if (match) {\n parts.push(`$filter=${match[1]}`);\n }\n }\n\n if (opts.orderBy) {\n const orderByValue = Array.isArray(opts.orderBy) ? opts.orderBy.join(\",\") : String(opts.orderBy);\n parts.push(`$orderby=${orderByValue}`);\n }\n\n if (opts.top !== undefined) {\n parts.push(`$top=${opts.top}`);\n }\n if (opts.skip !== undefined) {\n parts.push(`$skip=${opts.skip}`);\n }\n\n if (opts.expand && typeof opts.expand === \"string\") {\n parts.push(`$expand=${opts.expand}`);\n }\n\n return parts;\n }\n}\n"],"names":["defaultFields"],"mappings":"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"}
1
+ {"version":3,"file":"expand-builder.js","sources":["../../../../src/client/builders/expand-builder.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport buildQuery, { type QueryOptions } from \"odata-query\";\nimport type { InternalLogger } from \"../../logger\";\nimport { FMTable, getBaseTableConfig, getNavigationPaths, getTableName } from \"../../orm/table\";\nimport type { ExpandValidationConfig } from \"../../validation\";\nimport { getDefaultSelectFields } from \"./default-select\";\nimport { formatSelectFields } from \"./select-utils\";\nimport type { ExpandConfig } from \"./shared-types\";\n\nconst FILTER_QUERY_REGEX = /\\$filter=([^&]+)/;\n\n/**\n * Builds OData expand query strings and validation configs.\n * Handles nested expands recursively and transforms relation names to FMTIDs\n * when using entity IDs.\n */\nexport class ExpandBuilder {\n private readonly useEntityIds: boolean;\n private readonly logger: InternalLogger;\n\n constructor(useEntityIds: boolean, logger: InternalLogger) {\n this.useEntityIds = useEntityIds;\n this.logger = logger;\n }\n\n /**\n * Builds OData $expand query string from expand configurations.\n */\n buildExpandString(configs: ExpandConfig[]): string {\n if (configs.length === 0) {\n return \"\";\n }\n\n return configs.map((config) => this.buildSingleExpand(config)).join(\",\");\n }\n\n /**\n * Builds validation configs for expanded navigation properties.\n */\n buildValidationConfigs(configs: ExpandConfig[]): ExpandValidationConfig[] {\n return configs.map((config) => {\n const targetTable = config.targetTable;\n\n let targetSchema: Partial<Record<string, StandardSchemaV1>> | undefined;\n if (targetTable) {\n const baseTableConfig = getBaseTableConfig(targetTable);\n const containerFields = baseTableConfig.containerFields || [];\n\n // Filter out container fields from schema\n const schema = { ...baseTableConfig.schema };\n for (const containerField of containerFields) {\n delete schema[containerField as string];\n }\n\n targetSchema = schema;\n }\n\n let selectedFields: string[] | undefined;\n if (config.options?.select) {\n selectedFields = Array.isArray(config.options.select)\n ? config.options.select.map(String)\n : [String(config.options.select)];\n }\n\n // Recursively build validation configs for nested expands\n const nestedExpands = config.nestedExpandConfigs\n ? this.buildValidationConfigs(config.nestedExpandConfigs)\n : undefined;\n\n return {\n relation: config.relation,\n targetSchema,\n targetTable,\n table: targetTable,\n selectedFields,\n nestedExpands,\n };\n });\n }\n\n /**\n * Process an expand() call and return the expand config.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param targetTable - The target table to expand to\n * @param sourceTable - The source table (for validation)\n * @param callback - Optional callback to configure the expand query\n * @param builderFactory - Function that creates a QueryBuilder for the target table\n * @returns ExpandConfig to add to the builder's expandConfigs array\n */\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration, generic Builder type\n processExpand<TargetTable extends FMTable<any, any>, Builder = any>(\n targetTable: TargetTable,\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n sourceTable: FMTable<any, any> | undefined,\n callback?: (builder: Builder) => Builder,\n builderFactory?: () => Builder,\n ): ExpandConfig {\n // Extract name and validate\n const relationName = getTableName(targetTable);\n\n // Runtime validation: Check if relation name is in navigationPaths\n if (sourceTable) {\n const navigationPaths = getNavigationPaths(sourceTable);\n if (navigationPaths && !navigationPaths.includes(relationName)) {\n this.logger.warn(\n `Cannot expand to \"${relationName}\". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(\", \") : \"none\"}`,\n );\n }\n }\n\n if (callback && builderFactory) {\n // Create a new QueryBuilder for the target table\n const targetBuilder = builderFactory();\n\n // Pass to callback and get configured builder\n const configuredBuilder = callback(targetBuilder);\n\n // Extract the builder's query options\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryOptions configuration\n const expandOptions: Partial<QueryOptions<any>> = {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for internal builder property access\n ...(configuredBuilder as any).queryOptions,\n };\n\n // If callback didn't provide select, apply defaultSelect from target table\n if (!expandOptions.select) {\n const defaultFields = getDefaultSelectFields(targetTable);\n if (defaultFields) {\n expandOptions.select = defaultFields;\n }\n }\n\n // If the configured builder has nested expands, we need to include them\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for internal builder property access\n const nestedExpandConfigs = (configuredBuilder as any).expandConfigs;\n if (nestedExpandConfigs?.length > 0) {\n // Build nested expand string from the configured builder's expand configs\n const nestedExpandString = this.buildExpandString(nestedExpandConfigs);\n if (nestedExpandString) {\n // Add nested expand to options\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for expand string\n expandOptions.expand = nestedExpandString as any;\n }\n }\n\n return {\n relation: relationName,\n options: expandOptions,\n targetTable,\n nestedExpandConfigs: nestedExpandConfigs?.length > 0 ? nestedExpandConfigs : undefined,\n };\n }\n // Simple expand without callback - apply defaultSelect if available\n const defaultFields = getDefaultSelectFields(targetTable);\n if (defaultFields) {\n return {\n relation: relationName,\n options: { select: defaultFields },\n targetTable,\n };\n }\n return {\n relation: relationName,\n targetTable,\n };\n }\n\n /**\n * Builds a single expand string with its options.\n */\n private buildSingleExpand(config: ExpandConfig): string {\n const relationName = this.resolveRelationName(config);\n const parts = this.buildExpandParts(config);\n\n if (parts.length === 0) {\n return relationName;\n }\n\n return `${relationName}(${parts.join(\";\")})`;\n }\n\n /**\n * Resolves relation name, using FMTID if entity IDs are enabled.\n */\n private resolveRelationName(config: ExpandConfig): string {\n if (!this.useEntityIds) {\n return config.relation;\n }\n\n const targetTable = config.targetTable;\n if (targetTable && FMTable.Symbol.EntityId in targetTable) {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const tableId = (targetTable as any)[FMTable.Symbol.EntityId] as `FMTID:${string}` | undefined;\n if (tableId) {\n return tableId;\n }\n }\n\n return config.relation;\n }\n\n /**\n * Builds expand parts (select, filter, orderBy, etc.) for a single expand.\n */\n private buildExpandParts(config: ExpandConfig): string[] {\n if (!config.options || Object.keys(config.options).length === 0) {\n return [];\n }\n\n const parts: string[] = [];\n const opts = config.options;\n\n if (opts.select) {\n const selectArray = Array.isArray(opts.select) ? opts.select.map(String) : [String(opts.select)];\n const selectFields = formatSelectFields(selectArray, config.targetTable, this.useEntityIds);\n if (selectFields) {\n parts.push(`$select=${selectFields}`);\n }\n }\n\n if (opts.filter) {\n const filterQuery = buildQuery({ filter: opts.filter });\n const match = filterQuery.match(FILTER_QUERY_REGEX);\n if (match) {\n parts.push(`$filter=${match[1]}`);\n }\n }\n\n if (opts.orderBy) {\n const orderByValue = Array.isArray(opts.orderBy) ? opts.orderBy.join(\",\") : String(opts.orderBy);\n parts.push(`$orderby=${orderByValue}`);\n }\n\n if (opts.top !== undefined) {\n parts.push(`$top=${opts.top}`);\n }\n if (opts.skip !== undefined) {\n parts.push(`$skip=${opts.skip}`);\n }\n\n if (opts.expand && typeof opts.expand === \"string\") {\n parts.push(`$expand=${opts.expand}`);\n }\n\n return parts;\n }\n}\n"],"names":["defaultFields"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
+ import { FFetchOptions } from '@fetchkit/ffetch';
1
2
  import { StandardSchemaV1 } from '@standard-schema/spec';
2
3
  import { FMTable } from '../orm/table.js';
3
- import { ExecutableBuilder, ExecutionContext, Metadata } from '../types.js';
4
+ import { ExecutableBuilder, ExecutionContext, Metadata, Result } from '../types.js';
4
5
  import { BatchBuilder } from './batch-builder.js';
5
6
  import { EntitySet } from './entity-set.js';
6
7
  import { SchemaManager } from './schema-manager.js';
@@ -37,6 +38,10 @@ export declare class Database<IncludeSpecialColumns extends boolean = false> {
37
38
  */
38
39
  includeSpecialColumns?: IncludeSpecialColumns;
39
40
  });
41
+ /**
42
+ * @internal Used by adapter packages to access the database filename.
43
+ */
44
+ get _getDatabaseName(): string;
40
45
  /**
41
46
  * @internal Used by EntitySet to access database configuration
42
47
  */
@@ -45,6 +50,11 @@ export declare class Database<IncludeSpecialColumns extends boolean = false> {
45
50
  * @internal Used by EntitySet to access database configuration
46
51
  */
47
52
  get _getIncludeSpecialColumns(): IncludeSpecialColumns;
53
+ /**
54
+ * @internal Used by adapter packages for raw OData requests.
55
+ * Delegates to the connection's _makeRequest with the database name prepended.
56
+ */
57
+ _makeRequest<T>(path: string, options?: RequestInit & FFetchOptions): Promise<Result<T>>;
48
58
  from<T extends FMTable<any, any>>(table: T): EntitySet<T, IncludeSpecialColumns>;
49
59
  /**
50
60
  * Retrieves the OData metadata for this database.
@@ -21,6 +21,12 @@ class Database {
21
21
  this._useEntityIds = (config == null ? void 0 : config.useEntityIds) ?? false;
22
22
  this._includeSpecialColumns = (config == null ? void 0 : config.includeSpecialColumns) ?? false;
23
23
  }
24
+ /**
25
+ * @internal Used by adapter packages to access the database filename.
26
+ */
27
+ get _getDatabaseName() {
28
+ return this.databaseName;
29
+ }
24
30
  /**
25
31
  * @internal Used by EntitySet to access database configuration
26
32
  */
@@ -33,6 +39,13 @@ class Database {
33
39
  get _getIncludeSpecialColumns() {
34
40
  return this._includeSpecialColumns;
35
41
  }
42
+ /**
43
+ * @internal Used by adapter packages for raw OData requests.
44
+ * Delegates to the connection's _makeRequest with the database name prepended.
45
+ */
46
+ _makeRequest(path, options) {
47
+ return this.context._makeRequest(`/${this.databaseName}${path}`, options);
48
+ }
36
49
  // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
37
50
  from(table) {
38
51
  if (Object.hasOwn(table, FMTable.Symbol.UseEntityIds)) {
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sources":["../../../src/client/database.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { FMTable } from \"../orm/table\";\nimport type { ExecutableBuilder, ExecutionContext, Metadata } from \"../types\";\nimport { BatchBuilder } from \"./batch-builder\";\nimport { EntitySet } from \"./entity-set\";\nimport { SchemaManager } from \"./schema-manager\";\nimport { WebhookManager } from \"./webhook-builder\";\n\ninterface MetadataArgs {\n format?: \"xml\" | \"json\";\n /**\n * If provided, only the metadata for the specified table will be returned.\n * Requires FileMaker Server 22.0.4 or later.\n */\n tableName?: string;\n /**\n * If true, a reduced payload size will be returned by omitting certain annotations.\n */\n reduceAnnotations?: boolean;\n}\n\nexport class Database<IncludeSpecialColumns extends boolean = false> {\n readonly schema: SchemaManager;\n readonly webhook: WebhookManager;\n private readonly databaseName: string;\n private readonly context: ExecutionContext;\n private _useEntityIds: boolean;\n private readonly _includeSpecialColumns: IncludeSpecialColumns;\n\n constructor(\n databaseName: string,\n context: ExecutionContext,\n config?: {\n /**\n * Whether to use entity IDs instead of field names in the actual requests to the server\n * Defaults to true if all occurrences use entity IDs, false otherwise\n * If set to false but some occurrences do not use entity IDs, an error will be thrown\n */\n useEntityIds?: boolean;\n /**\n * Whether to include special columns (ROWID and ROWMODID) in responses.\n * Note: Special columns are only included when there is no $select query.\n */\n includeSpecialColumns?: IncludeSpecialColumns;\n },\n ) {\n this.databaseName = databaseName;\n this.context = context;\n // Initialize schema manager\n this.schema = new SchemaManager(this.databaseName, this.context);\n this.webhook = new WebhookManager(this.databaseName, this.context);\n this._useEntityIds = config?.useEntityIds ?? false;\n this._includeSpecialColumns = (config?.includeSpecialColumns ?? false) as IncludeSpecialColumns;\n }\n\n /**\n * @internal Used by EntitySet to access database configuration\n */\n get _getUseEntityIds(): boolean {\n return this._useEntityIds;\n }\n\n /**\n * @internal Used by EntitySet to access database configuration\n */\n get _getIncludeSpecialColumns(): IncludeSpecialColumns {\n return this._includeSpecialColumns;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n from<T extends FMTable<any, any>>(table: T): EntitySet<T, IncludeSpecialColumns> {\n // Only override database-level useEntityIds if table explicitly sets it\n // (not if it's undefined, which would override the database setting)\n if (Object.hasOwn(table, FMTable.Symbol.UseEntityIds)) {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const tableUseEntityIds = (table as any)[FMTable.Symbol.UseEntityIds];\n if (typeof tableUseEntityIds === \"boolean\") {\n this._useEntityIds = tableUseEntityIds;\n }\n }\n return new EntitySet<T, IncludeSpecialColumns>({\n occurrence: table as T,\n databaseName: this.databaseName,\n context: this.context,\n database: this,\n });\n }\n\n /**\n * Retrieves the OData metadata for this database.\n * @param args Optional configuration object\n * @param args.format The format to retrieve metadata in. Defaults to \"json\".\n * @param args.tableName If provided, only the metadata for the specified table will be returned. Requires FileMaker Server 22.0.4 or later.\n * @param args.reduceAnnotations If true, a reduced payload size will be returned by omitting certain annotations.\n * @returns The metadata in the specified format\n */\n async getMetadata(args: { format: \"xml\" } & MetadataArgs): Promise<string>;\n async getMetadata(args?: { format?: \"json\" } & MetadataArgs): Promise<Metadata>;\n async getMetadata(args?: MetadataArgs): Promise<string | Metadata> {\n // Build the URL - if tableName is provided, append %23{tableName} to the path\n let url = `/${this.databaseName}/$metadata`;\n if (args?.tableName) {\n url = `/${this.databaseName}/$metadata%23${args.tableName}`;\n }\n\n // Build headers\n const headers: Record<string, string> = {\n Accept: args?.format === \"xml\" ? \"application/xml\" : \"application/json\",\n };\n\n // Add Prefer header if reduceAnnotations is true\n if (args?.reduceAnnotations) {\n headers.Prefer = 'include-annotations=\"-*\"';\n }\n\n const result = await this.context._makeRequest<Record<string, Metadata> | string>(url, {\n headers,\n });\n if (result.error) {\n throw result.error;\n }\n\n if (args?.format === \"json\") {\n const data = result.data as Record<string, Metadata>;\n const metadata = data[this.databaseName];\n if (!metadata) {\n throw new Error(`Metadata for database \"${this.databaseName}\" not found in response`);\n }\n return metadata;\n }\n return result.data as string;\n }\n\n /**\n * Lists all available tables (entity sets) in this database.\n * @returns Promise resolving to an array of table names\n */\n async listTableNames(): Promise<string[]> {\n const result = await this.context._makeRequest<{\n value?: Array<{ name: string }>;\n }>(`/${this.databaseName}`);\n if (result.error) {\n throw result.error;\n }\n if (result.data.value && Array.isArray(result.data.value)) {\n return result.data.value.map((item) => item.name);\n }\n return [];\n }\n\n /**\n * Executes a FileMaker script.\n * @param scriptName - The name of the script to execute (must be valid according to OData rules)\n * @param options - Optional script parameter and result schema\n * @returns Promise resolving to script execution result\n */\n // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer\n async runScript<ResultSchema extends StandardSchemaV1<string, any> = never>(\n scriptName: string,\n options?: {\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape\n scriptParam?: string | number | Record<string, any>;\n resultSchema?: ResultSchema;\n },\n ): Promise<\n [ResultSchema] extends [never]\n ? { resultCode: number; result?: string }\n : ResultSchema extends StandardSchemaV1<string, infer Output>\n ? { resultCode: number; result: Output }\n : { resultCode: number; result?: string }\n > {\n const body: { scriptParameterValue?: unknown } = {};\n if (options?.scriptParam !== undefined) {\n body.scriptParameterValue = options.scriptParam;\n }\n\n const result = await this.context._makeRequest<{\n scriptResult: {\n code: number;\n resultParameter?: string;\n };\n }>(`/${this.databaseName}/Script.${scriptName}`, {\n method: \"POST\",\n body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,\n });\n\n if (result.error) {\n throw result.error;\n }\n\n const response = result.data;\n\n // If resultSchema is provided, validate the result through it\n if (options?.resultSchema && response.scriptResult !== undefined) {\n const validationResult = options.resultSchema[\"~standard\"].validate(response.scriptResult.resultParameter);\n // Handle both sync and async validation\n const result = validationResult instanceof Promise ? await validationResult : validationResult;\n\n if (result.issues) {\n throw new Error(`Script result validation failed: ${JSON.stringify(result.issues)}`);\n }\n\n return {\n resultCode: response.scriptResult.code,\n result: result.value,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type\n } as any;\n }\n\n return {\n resultCode: response.scriptResult.code,\n result: response.scriptResult.resultParameter,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type\n } as any;\n }\n\n /**\n * Create a batch operation builder that allows multiple queries to be executed together\n * in a single atomic request. All operations succeed or fail together (transactional).\n *\n * @param builders - Array of executable query builders to batch\n * @returns A BatchBuilder that can be executed\n * @example\n * ```ts\n * const result = await db.batch([\n * db.from('contacts').list().top(5),\n * db.from('users').list().top(5),\n * db.from('contacts').insert({ name: 'John' })\n * ]).execute();\n *\n * if (result.data) {\n * const [contacts, users, insertResult] = result.data;\n * }\n * ```\n */\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\n batch<const Builders extends readonly ExecutableBuilder<any>[]>(builders: Builders): BatchBuilder<Builders> {\n return new BatchBuilder(builders, this.databaseName, this.context);\n }\n}\n"],"names":["result"],"mappings":"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"}
1
+ {"version":3,"file":"database.js","sources":["../../../src/client/database.ts"],"sourcesContent":["import type { FFetchOptions } from \"@fetchkit/ffetch\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { FMTable } from \"../orm/table\";\nimport type { ExecutableBuilder, ExecutionContext, Metadata, Result } from \"../types\";\nimport { BatchBuilder } from \"./batch-builder\";\nimport { EntitySet } from \"./entity-set\";\nimport { SchemaManager } from \"./schema-manager\";\nimport { WebhookManager } from \"./webhook-builder\";\n\ninterface MetadataArgs {\n format?: \"xml\" | \"json\";\n /**\n * If provided, only the metadata for the specified table will be returned.\n * Requires FileMaker Server 22.0.4 or later.\n */\n tableName?: string;\n /**\n * If true, a reduced payload size will be returned by omitting certain annotations.\n */\n reduceAnnotations?: boolean;\n}\n\nexport class Database<IncludeSpecialColumns extends boolean = false> {\n readonly schema: SchemaManager;\n readonly webhook: WebhookManager;\n private readonly databaseName: string;\n private readonly context: ExecutionContext;\n private _useEntityIds: boolean;\n private readonly _includeSpecialColumns: IncludeSpecialColumns;\n\n constructor(\n databaseName: string,\n context: ExecutionContext,\n config?: {\n /**\n * Whether to use entity IDs instead of field names in the actual requests to the server\n * Defaults to true if all occurrences use entity IDs, false otherwise\n * If set to false but some occurrences do not use entity IDs, an error will be thrown\n */\n useEntityIds?: boolean;\n /**\n * Whether to include special columns (ROWID and ROWMODID) in responses.\n * Note: Special columns are only included when there is no $select query.\n */\n includeSpecialColumns?: IncludeSpecialColumns;\n },\n ) {\n this.databaseName = databaseName;\n this.context = context;\n // Initialize schema manager\n this.schema = new SchemaManager(this.databaseName, this.context);\n this.webhook = new WebhookManager(this.databaseName, this.context);\n this._useEntityIds = config?.useEntityIds ?? false;\n this._includeSpecialColumns = (config?.includeSpecialColumns ?? false) as IncludeSpecialColumns;\n }\n\n /**\n * @internal Used by adapter packages to access the database filename.\n */\n get _getDatabaseName(): string {\n return this.databaseName;\n }\n\n /**\n * @internal Used by EntitySet to access database configuration\n */\n get _getUseEntityIds(): boolean {\n return this._useEntityIds;\n }\n\n /**\n * @internal Used by EntitySet to access database configuration\n */\n get _getIncludeSpecialColumns(): IncludeSpecialColumns {\n return this._includeSpecialColumns;\n }\n\n /**\n * @internal Used by adapter packages for raw OData requests.\n * Delegates to the connection's _makeRequest with the database name prepended.\n */\n _makeRequest<T>(path: string, options?: RequestInit & FFetchOptions): Promise<Result<T>> {\n return this.context._makeRequest<T>(`/${this.databaseName}${path}`, options);\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n from<T extends FMTable<any, any>>(table: T): EntitySet<T, IncludeSpecialColumns> {\n // Only override database-level useEntityIds if table explicitly sets it\n // (not if it's undefined, which would override the database setting)\n if (Object.hasOwn(table, FMTable.Symbol.UseEntityIds)) {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const tableUseEntityIds = (table as any)[FMTable.Symbol.UseEntityIds];\n if (typeof tableUseEntityIds === \"boolean\") {\n this._useEntityIds = tableUseEntityIds;\n }\n }\n return new EntitySet<T, IncludeSpecialColumns>({\n occurrence: table as T,\n databaseName: this.databaseName,\n context: this.context,\n database: this,\n });\n }\n\n /**\n * Retrieves the OData metadata for this database.\n * @param args Optional configuration object\n * @param args.format The format to retrieve metadata in. Defaults to \"json\".\n * @param args.tableName If provided, only the metadata for the specified table will be returned. Requires FileMaker Server 22.0.4 or later.\n * @param args.reduceAnnotations If true, a reduced payload size will be returned by omitting certain annotations.\n * @returns The metadata in the specified format\n */\n async getMetadata(args: { format: \"xml\" } & MetadataArgs): Promise<string>;\n async getMetadata(args?: { format?: \"json\" } & MetadataArgs): Promise<Metadata>;\n async getMetadata(args?: MetadataArgs): Promise<string | Metadata> {\n // Build the URL - if tableName is provided, append %23{tableName} to the path\n let url = `/${this.databaseName}/$metadata`;\n if (args?.tableName) {\n url = `/${this.databaseName}/$metadata%23${args.tableName}`;\n }\n\n // Build headers\n const headers: Record<string, string> = {\n Accept: args?.format === \"xml\" ? \"application/xml\" : \"application/json\",\n };\n\n // Add Prefer header if reduceAnnotations is true\n if (args?.reduceAnnotations) {\n headers.Prefer = 'include-annotations=\"-*\"';\n }\n\n const result = await this.context._makeRequest<Record<string, Metadata> | string>(url, {\n headers,\n });\n if (result.error) {\n throw result.error;\n }\n\n if (args?.format === \"json\") {\n const data = result.data as Record<string, Metadata>;\n const metadata = data[this.databaseName];\n if (!metadata) {\n throw new Error(`Metadata for database \"${this.databaseName}\" not found in response`);\n }\n return metadata;\n }\n return result.data as string;\n }\n\n /**\n * Lists all available tables (entity sets) in this database.\n * @returns Promise resolving to an array of table names\n */\n async listTableNames(): Promise<string[]> {\n const result = await this.context._makeRequest<{\n value?: Array<{ name: string }>;\n }>(`/${this.databaseName}`);\n if (result.error) {\n throw result.error;\n }\n if (result.data.value && Array.isArray(result.data.value)) {\n return result.data.value.map((item) => item.name);\n }\n return [];\n }\n\n /**\n * Executes a FileMaker script.\n * @param scriptName - The name of the script to execute (must be valid according to OData rules)\n * @param options - Optional script parameter and result schema\n * @returns Promise resolving to script execution result\n */\n // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer\n async runScript<ResultSchema extends StandardSchemaV1<string, any> = never>(\n scriptName: string,\n options?: {\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape\n scriptParam?: string | number | Record<string, any>;\n resultSchema?: ResultSchema;\n },\n ): Promise<\n [ResultSchema] extends [never]\n ? { resultCode: number; result?: string }\n : ResultSchema extends StandardSchemaV1<string, infer Output>\n ? { resultCode: number; result: Output }\n : { resultCode: number; result?: string }\n > {\n const body: { scriptParameterValue?: unknown } = {};\n if (options?.scriptParam !== undefined) {\n body.scriptParameterValue = options.scriptParam;\n }\n\n const result = await this.context._makeRequest<{\n scriptResult: {\n code: number;\n resultParameter?: string;\n };\n }>(`/${this.databaseName}/Script.${scriptName}`, {\n method: \"POST\",\n body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,\n });\n\n if (result.error) {\n throw result.error;\n }\n\n const response = result.data;\n\n // If resultSchema is provided, validate the result through it\n if (options?.resultSchema && response.scriptResult !== undefined) {\n const validationResult = options.resultSchema[\"~standard\"].validate(response.scriptResult.resultParameter);\n // Handle both sync and async validation\n const result = validationResult instanceof Promise ? await validationResult : validationResult;\n\n if (result.issues) {\n throw new Error(`Script result validation failed: ${JSON.stringify(result.issues)}`);\n }\n\n return {\n resultCode: response.scriptResult.code,\n result: result.value,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type\n } as any;\n }\n\n return {\n resultCode: response.scriptResult.code,\n result: response.scriptResult.resultParameter,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type\n } as any;\n }\n\n /**\n * Create a batch operation builder that allows multiple queries to be executed together\n * in a single atomic request. All operations succeed or fail together (transactional).\n *\n * @param builders - Array of executable query builders to batch\n * @returns A BatchBuilder that can be executed\n * @example\n * ```ts\n * const result = await db.batch([\n * db.from('contacts').list().top(5),\n * db.from('users').list().top(5),\n * db.from('contacts').insert({ name: 'John' })\n * ]).execute();\n *\n * if (result.data) {\n * const [contacts, users, insertResult] = result.data;\n * }\n * ```\n */\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\n batch<const Builders extends readonly ExecutableBuilder<any>[]>(builders: Builders): BatchBuilder<Builders> {\n return new BatchBuilder(builders, this.databaseName, this.context);\n }\n}\n"],"names":["result"],"mappings":"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"}
@@ -9,6 +9,8 @@ export declare class FMServerConnection implements ExecutionContext {
9
9
  private useEntityIds;
10
10
  private includeSpecialColumns;
11
11
  private readonly logger;
12
+ /** @internal Stored so credential-override flows can inherit non-auth config. */
13
+ readonly _fetchClientOptions: FFetchOptions | undefined;
12
14
  constructor(config: {
13
15
  serverUrl: string;
14
16
  auth: Auth;
@@ -17,7 +17,10 @@ class FMServerConnection {
17
17
  __publicField(this, "useEntityIds", false);
18
18
  __publicField(this, "includeSpecialColumns", false);
19
19
  __publicField(this, "logger");
20
+ /** @internal Stored so credential-override flows can inherit non-auth config. */
21
+ __publicField(this, "_fetchClientOptions");
20
22
  this.logger = createLogger(config.logger);
23
+ this._fetchClientOptions = config.fetchClientOptions;
21
24
  this.fetchClient = createClient({
22
25
  retries: 0,
23
26
  ...config.fetchClientOptions
@@ -1 +1 @@
1
- {"version":3,"file":"filemaker-odata.js","sources":["../../../src/client/filemaker-odata.ts"],"sourcesContent":["import createClient, {\n AbortError,\n CircuitOpenError,\n type FFetchOptions,\n NetworkError,\n RetryLimitError,\n TimeoutError,\n} from \"@fetchkit/ffetch\";\nimport { get } from \"es-toolkit/compat\";\nimport { HTTPError, ODataError, ResponseParseError, SchemaLockedError } from \"../errors\";\nimport { createLogger, type InternalLogger, type Logger } from \"../logger\";\nimport type { Auth, ExecutionContext, Result } from \"../types\";\nimport { getAcceptHeader } from \"../types\";\nimport { Database } from \"./database\";\nimport { safeJsonParse } from \"./sanitize-json\";\n\nconst TRAILING_SLASH_REGEX = /\\/+$/;\n\nexport class FMServerConnection implements ExecutionContext {\n private readonly fetchClient: ReturnType<typeof createClient>;\n private readonly serverUrl: string;\n private readonly auth: Auth;\n private useEntityIds = false;\n private includeSpecialColumns = false;\n private readonly logger: InternalLogger;\n constructor(config: {\n serverUrl: string;\n auth: Auth;\n fetchClientOptions?: FFetchOptions;\n logger?: Logger;\n }) {\n this.logger = createLogger(config.logger);\n this.fetchClient = createClient({\n retries: 0,\n ...config.fetchClientOptions,\n });\n // Ensure the URL uses https://, is valid, and has no trailing slash\n const url = new URL(config.serverUrl);\n if (url.protocol !== \"https:\") {\n url.protocol = \"https:\";\n }\n // Remove any trailing slash from pathname\n url.pathname = url.pathname.replace(TRAILING_SLASH_REGEX, \"\");\n this.serverUrl = url.toString().replace(TRAILING_SLASH_REGEX, \"\");\n this.auth = config.auth;\n }\n\n /**\n * @internal\n * Sets whether to use FileMaker entity IDs (FMFID/FMTID) in requests\n */\n _setUseEntityIds(useEntityIds: boolean): void {\n this.useEntityIds = useEntityIds;\n }\n\n /**\n * @internal\n * Gets whether to use FileMaker entity IDs (FMFID/FMTID) in requests\n */\n _getUseEntityIds(): boolean {\n return this.useEntityIds;\n }\n\n /**\n * @internal\n * Sets whether to include special columns (ROWID and ROWMODID) in requests\n */\n _setIncludeSpecialColumns(includeSpecialColumns: boolean): void {\n this.includeSpecialColumns = includeSpecialColumns;\n }\n\n /**\n * @internal\n * Gets whether to include special columns (ROWID and ROWMODID) in requests\n */\n _getIncludeSpecialColumns(): boolean {\n return this.includeSpecialColumns;\n }\n\n /**\n * @internal\n * Gets the base URL for OData requests\n */\n _getBaseUrl(): string {\n return `${this.serverUrl}${\"apiKey\" in this.auth ? \"/otto\" : \"\"}/fmi/odata/v4`;\n }\n\n /**\n * @internal\n * Gets the logger instance\n */\n _getLogger(): InternalLogger {\n return this.logger;\n }\n\n /**\n * @internal\n */\n async _makeRequest<T>(\n url: string,\n options?: RequestInit &\n FFetchOptions & {\n useEntityIds?: boolean;\n includeSpecialColumns?: boolean;\n },\n ): Promise<Result<T>> {\n const logger = this._getLogger();\n const baseUrl = `${this.serverUrl}${\"apiKey\" in this.auth ? \"/otto\" : \"\"}/fmi/odata/v4`;\n const fullUrl = baseUrl + url;\n\n // Use per-request override if provided, otherwise use the database-level setting\n const useEntityIds = options?.useEntityIds ?? this.useEntityIds;\n const includeSpecialColumns = options?.includeSpecialColumns ?? this.includeSpecialColumns;\n\n // Get includeODataAnnotations from options (it's passed through from execute options)\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for optional property access\n const includeODataAnnotations = (options as any)?.includeODataAnnotations;\n\n // Build Prefer header as comma-separated list when multiple preferences are set\n const preferValues: string[] = [];\n if (useEntityIds) {\n preferValues.push(\"fmodata.entity-ids\");\n }\n if (includeSpecialColumns) {\n preferValues.push(\"fmodata.include-specialcolumns\");\n }\n\n const headers = {\n Authorization:\n \"apiKey\" in this.auth\n ? `Bearer ${this.auth.apiKey}`\n : `Basic ${btoa(`${this.auth.username}:${this.auth.password}`)}`,\n \"Content-Type\": \"application/json\",\n Accept: getAcceptHeader(includeODataAnnotations),\n ...(preferValues.length > 0 ? { Prefer: preferValues.join(\", \") } : {}),\n ...(options?.headers || {}),\n };\n\n // Prepare loggableHeaders by omitting the Authorization key\n const { Authorization, ...loggableHeaders } = headers;\n logger.debug(\"Request headers:\", loggableHeaders);\n\n // TEMPORARY WORKAROUND: Hopefully this feature will be fixed in the ffetch library\n // Extract fetchHandler and headers separately, only for tests where we're overriding the fetch handler per-request\n const fetchHandler = options?.fetchHandler;\n const { headers: _headers, fetchHandler: _fetchHandler, ...restOptions } = options || {};\n\n // If fetchHandler is provided, create a temporary client with it\n // Otherwise use the existing client\n const clientToUse = fetchHandler ? createClient({ retries: 0, fetchHandler }) : this.fetchClient;\n\n try {\n const finalOptions = {\n ...restOptions,\n headers,\n };\n\n const resp = await clientToUse(fullUrl, finalOptions);\n logger.debug(`${finalOptions.method ?? \"GET\"} ${resp.status} ${fullUrl}`);\n\n // Handle HTTP errors\n if (!resp.ok) {\n // Try to parse error body if it's JSON\n let errorBody: { error?: { code?: string | number; message?: string } } | undefined;\n try {\n if (resp.headers.get(\"content-type\")?.includes(\"application/json\")) {\n errorBody = await safeJsonParse<typeof errorBody>(resp);\n }\n } catch {\n // Ignore JSON parse errors\n }\n\n // Check if it's an OData error response\n if (errorBody?.error) {\n const errorCode = errorBody.error.code;\n const errorMessage = errorBody.error.message || resp.statusText;\n\n // Check for schema locked error (code 303)\n if (errorCode === \"303\" || errorCode === 303) {\n return {\n data: undefined,\n error: new SchemaLockedError(fullUrl, errorMessage, errorBody.error),\n };\n }\n\n return {\n data: undefined,\n error: new ODataError(fullUrl, errorMessage, String(errorCode), errorBody.error),\n };\n }\n\n return {\n data: undefined,\n error: new HTTPError(fullUrl, resp.status, resp.statusText, errorBody),\n };\n }\n\n // Check for affected rows header (for DELETE and bulk PATCH operations)\n // FileMaker may return this with 204 No Content or 200 OK\n const affectedRows = resp.headers.get(\"fmodata.affected_rows\");\n if (affectedRows !== null) {\n return { data: Number.parseInt(affectedRows, 10) as T, error: undefined };\n }\n\n // Handle 204 No Content with no body\n if (resp.status === 204) {\n // Check for Location header (used for insert with return=minimal)\n // Use optional chaining for safety with mocks that might not have proper headers\n const locationHeader = resp.headers?.get?.(\"Location\") || resp.headers?.get?.(\"location\");\n if (locationHeader) {\n // Return the location header so InsertBuilder can extract ROWID\n return { data: { _location: locationHeader } as T, error: undefined };\n }\n return { data: 0 as T, error: undefined };\n }\n\n // Parse response\n if (resp.headers.get(\"content-type\")?.includes(\"application/json\")) {\n const data = await safeJsonParse<T & { error?: { code?: string | number; message?: string } }>(resp);\n\n // Check for embedded OData errors\n if (get(data, \"error\", null)) {\n const errorCode = get(data, \"error.code\", null);\n const errorMessage = get(data, \"error.message\", \"Unknown OData error\");\n\n // Check for schema locked error (code 303)\n if (errorCode === \"303\" || errorCode === 303) {\n return {\n data: undefined,\n error: new SchemaLockedError(fullUrl, errorMessage, data.error),\n };\n }\n\n return {\n data: undefined,\n error: new ODataError(fullUrl, errorMessage, String(errorCode), data.error),\n };\n }\n\n return { data: data as T, error: undefined };\n }\n\n return { data: (await resp.text()) as T, error: undefined };\n } catch (err) {\n // Map ffetch errors - return them directly (no re-wrapping)\n if (\n err instanceof TimeoutError ||\n err instanceof AbortError ||\n err instanceof NetworkError ||\n err instanceof RetryLimitError ||\n err instanceof CircuitOpenError\n ) {\n return { data: undefined, error: err };\n }\n\n // Handle JSON parse errors (ResponseParseError from safeJsonParse)\n if (err instanceof ResponseParseError) {\n return { data: undefined, error: err };\n }\n\n // Unknown error - wrap it as NetworkError\n return {\n data: undefined,\n error: new NetworkError(fullUrl, err),\n };\n }\n }\n\n database<IncludeSpecialColumns extends boolean = false>(\n name: string,\n config?: {\n useEntityIds?: boolean;\n includeSpecialColumns?: IncludeSpecialColumns;\n },\n ): Database<IncludeSpecialColumns> {\n return new Database<IncludeSpecialColumns>(name, this, config);\n }\n\n /**\n * Lists all available databases from the FileMaker OData service.\n * @returns Promise resolving to an array of database names\n */\n async listDatabaseNames(): Promise<string[]> {\n const result = await this._makeRequest<{\n value?: Array<{ name: string }>;\n }>(\"/$metadata\", { headers: { Accept: \"application/json\" } });\n if (result.error) {\n throw result.error;\n }\n if (result.data.value && Array.isArray(result.data.value)) {\n return result.data.value.map((item) => item.name);\n }\n return [];\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"filemaker-odata.js","sources":["../../../src/client/filemaker-odata.ts"],"sourcesContent":["import createClient, {\n AbortError,\n CircuitOpenError,\n type FFetchOptions,\n NetworkError,\n RetryLimitError,\n TimeoutError,\n} from \"@fetchkit/ffetch\";\nimport { get } from \"es-toolkit/compat\";\nimport { HTTPError, ODataError, ResponseParseError, SchemaLockedError } from \"../errors\";\nimport { createLogger, type InternalLogger, type Logger } from \"../logger\";\nimport type { Auth, ExecutionContext, Result } from \"../types\";\nimport { getAcceptHeader } from \"../types\";\nimport { Database } from \"./database\";\nimport { safeJsonParse } from \"./sanitize-json\";\n\nconst TRAILING_SLASH_REGEX = /\\/+$/;\n\nexport class FMServerConnection implements ExecutionContext {\n private readonly fetchClient: ReturnType<typeof createClient>;\n private readonly serverUrl: string;\n private readonly auth: Auth;\n private useEntityIds = false;\n private includeSpecialColumns = false;\n private readonly logger: InternalLogger;\n /** @internal Stored so credential-override flows can inherit non-auth config. */\n readonly _fetchClientOptions: FFetchOptions | undefined;\n constructor(config: {\n serverUrl: string;\n auth: Auth;\n fetchClientOptions?: FFetchOptions;\n logger?: Logger;\n }) {\n this.logger = createLogger(config.logger);\n this._fetchClientOptions = config.fetchClientOptions;\n this.fetchClient = createClient({\n retries: 0,\n ...config.fetchClientOptions,\n });\n // Ensure the URL uses https://, is valid, and has no trailing slash\n const url = new URL(config.serverUrl);\n if (url.protocol !== \"https:\") {\n url.protocol = \"https:\";\n }\n // Remove any trailing slash from pathname\n url.pathname = url.pathname.replace(TRAILING_SLASH_REGEX, \"\");\n this.serverUrl = url.toString().replace(TRAILING_SLASH_REGEX, \"\");\n this.auth = config.auth;\n }\n\n /**\n * @internal\n * Sets whether to use FileMaker entity IDs (FMFID/FMTID) in requests\n */\n _setUseEntityIds(useEntityIds: boolean): void {\n this.useEntityIds = useEntityIds;\n }\n\n /**\n * @internal\n * Gets whether to use FileMaker entity IDs (FMFID/FMTID) in requests\n */\n _getUseEntityIds(): boolean {\n return this.useEntityIds;\n }\n\n /**\n * @internal\n * Sets whether to include special columns (ROWID and ROWMODID) in requests\n */\n _setIncludeSpecialColumns(includeSpecialColumns: boolean): void {\n this.includeSpecialColumns = includeSpecialColumns;\n }\n\n /**\n * @internal\n * Gets whether to include special columns (ROWID and ROWMODID) in requests\n */\n _getIncludeSpecialColumns(): boolean {\n return this.includeSpecialColumns;\n }\n\n /**\n * @internal\n * Gets the base URL for OData requests\n */\n _getBaseUrl(): string {\n return `${this.serverUrl}${\"apiKey\" in this.auth ? \"/otto\" : \"\"}/fmi/odata/v4`;\n }\n\n /**\n * @internal\n * Gets the logger instance\n */\n _getLogger(): InternalLogger {\n return this.logger;\n }\n\n /**\n * @internal\n */\n async _makeRequest<T>(\n url: string,\n options?: RequestInit &\n FFetchOptions & {\n useEntityIds?: boolean;\n includeSpecialColumns?: boolean;\n },\n ): Promise<Result<T>> {\n const logger = this._getLogger();\n const baseUrl = `${this.serverUrl}${\"apiKey\" in this.auth ? \"/otto\" : \"\"}/fmi/odata/v4`;\n const fullUrl = baseUrl + url;\n\n // Use per-request override if provided, otherwise use the database-level setting\n const useEntityIds = options?.useEntityIds ?? this.useEntityIds;\n const includeSpecialColumns = options?.includeSpecialColumns ?? this.includeSpecialColumns;\n\n // Get includeODataAnnotations from options (it's passed through from execute options)\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for optional property access\n const includeODataAnnotations = (options as any)?.includeODataAnnotations;\n\n // Build Prefer header as comma-separated list when multiple preferences are set\n const preferValues: string[] = [];\n if (useEntityIds) {\n preferValues.push(\"fmodata.entity-ids\");\n }\n if (includeSpecialColumns) {\n preferValues.push(\"fmodata.include-specialcolumns\");\n }\n\n const headers = {\n Authorization:\n \"apiKey\" in this.auth\n ? `Bearer ${this.auth.apiKey}`\n : `Basic ${btoa(`${this.auth.username}:${this.auth.password}`)}`,\n \"Content-Type\": \"application/json\",\n Accept: getAcceptHeader(includeODataAnnotations),\n ...(preferValues.length > 0 ? { Prefer: preferValues.join(\", \") } : {}),\n ...(options?.headers || {}),\n };\n\n // Prepare loggableHeaders by omitting the Authorization key\n const { Authorization, ...loggableHeaders } = headers;\n logger.debug(\"Request headers:\", loggableHeaders);\n\n // TEMPORARY WORKAROUND: Hopefully this feature will be fixed in the ffetch library\n // Extract fetchHandler and headers separately, only for tests where we're overriding the fetch handler per-request\n const fetchHandler = options?.fetchHandler;\n const { headers: _headers, fetchHandler: _fetchHandler, ...restOptions } = options || {};\n\n // If fetchHandler is provided, create a temporary client with it\n // Otherwise use the existing client\n const clientToUse = fetchHandler ? createClient({ retries: 0, fetchHandler }) : this.fetchClient;\n\n try {\n const finalOptions = {\n ...restOptions,\n headers,\n };\n\n const resp = await clientToUse(fullUrl, finalOptions);\n logger.debug(`${finalOptions.method ?? \"GET\"} ${resp.status} ${fullUrl}`);\n\n // Handle HTTP errors\n if (!resp.ok) {\n // Try to parse error body if it's JSON\n let errorBody: { error?: { code?: string | number; message?: string } } | undefined;\n try {\n if (resp.headers.get(\"content-type\")?.includes(\"application/json\")) {\n errorBody = await safeJsonParse<typeof errorBody>(resp);\n }\n } catch {\n // Ignore JSON parse errors\n }\n\n // Check if it's an OData error response\n if (errorBody?.error) {\n const errorCode = errorBody.error.code;\n const errorMessage = errorBody.error.message || resp.statusText;\n\n // Check for schema locked error (code 303)\n if (errorCode === \"303\" || errorCode === 303) {\n return {\n data: undefined,\n error: new SchemaLockedError(fullUrl, errorMessage, errorBody.error),\n };\n }\n\n return {\n data: undefined,\n error: new ODataError(fullUrl, errorMessage, String(errorCode), errorBody.error),\n };\n }\n\n return {\n data: undefined,\n error: new HTTPError(fullUrl, resp.status, resp.statusText, errorBody),\n };\n }\n\n // Check for affected rows header (for DELETE and bulk PATCH operations)\n // FileMaker may return this with 204 No Content or 200 OK\n const affectedRows = resp.headers.get(\"fmodata.affected_rows\");\n if (affectedRows !== null) {\n return { data: Number.parseInt(affectedRows, 10) as T, error: undefined };\n }\n\n // Handle 204 No Content with no body\n if (resp.status === 204) {\n // Check for Location header (used for insert with return=minimal)\n // Use optional chaining for safety with mocks that might not have proper headers\n const locationHeader = resp.headers?.get?.(\"Location\") || resp.headers?.get?.(\"location\");\n if (locationHeader) {\n // Return the location header so InsertBuilder can extract ROWID\n return { data: { _location: locationHeader } as T, error: undefined };\n }\n return { data: 0 as T, error: undefined };\n }\n\n // Parse response\n if (resp.headers.get(\"content-type\")?.includes(\"application/json\")) {\n const data = await safeJsonParse<T & { error?: { code?: string | number; message?: string } }>(resp);\n\n // Check for embedded OData errors\n if (get(data, \"error\", null)) {\n const errorCode = get(data, \"error.code\", null);\n const errorMessage = get(data, \"error.message\", \"Unknown OData error\");\n\n // Check for schema locked error (code 303)\n if (errorCode === \"303\" || errorCode === 303) {\n return {\n data: undefined,\n error: new SchemaLockedError(fullUrl, errorMessage, data.error),\n };\n }\n\n return {\n data: undefined,\n error: new ODataError(fullUrl, errorMessage, String(errorCode), data.error),\n };\n }\n\n return { data: data as T, error: undefined };\n }\n\n return { data: (await resp.text()) as T, error: undefined };\n } catch (err) {\n // Map ffetch errors - return them directly (no re-wrapping)\n if (\n err instanceof TimeoutError ||\n err instanceof AbortError ||\n err instanceof NetworkError ||\n err instanceof RetryLimitError ||\n err instanceof CircuitOpenError\n ) {\n return { data: undefined, error: err };\n }\n\n // Handle JSON parse errors (ResponseParseError from safeJsonParse)\n if (err instanceof ResponseParseError) {\n return { data: undefined, error: err };\n }\n\n // Unknown error - wrap it as NetworkError\n return {\n data: undefined,\n error: new NetworkError(fullUrl, err),\n };\n }\n }\n\n database<IncludeSpecialColumns extends boolean = false>(\n name: string,\n config?: {\n useEntityIds?: boolean;\n includeSpecialColumns?: IncludeSpecialColumns;\n },\n ): Database<IncludeSpecialColumns> {\n return new Database<IncludeSpecialColumns>(name, this, config);\n }\n\n /**\n * Lists all available databases from the FileMaker OData service.\n * @returns Promise resolving to an array of database names\n */\n async listDatabaseNames(): Promise<string[]> {\n const result = await this._makeRequest<{\n value?: Array<{ name: string }>;\n }>(\"/$metadata\", { headers: { Accept: \"application/json\" } });\n if (result.error) {\n throw result.error;\n }\n if (result.data.value && Array.isArray(result.data.value)) {\n return result.data.value.map((item) => item.name);\n }\n return [];\n }\n}\n"],"names":[],"mappings":"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"}
@@ -9,5 +9,5 @@ export type { Webhook, WebhookAddResponse, WebhookInfo, WebhookListResponse, } f
9
9
  export type { FMODataErrorType } from './errors.js';
10
10
  export { BatchTruncatedError, FMODataError, HTTPError, isBatchTruncatedError, isFMODataError, isHTTPError, isODataError, isRecordCountMismatchError, isResponseParseError, isResponseStructureError, isSchemaLockedError, isValidationError, ODataError, RecordCountMismatchError, ResponseParseError, ResponseStructureError, SchemaLockedError, ValidationError, } from './errors.js';
11
11
  export type { Logger } from './logger.js';
12
- export { and, asc, type Column, calcField, containerField, contains, dateField, desc, endsWith, eq, type FieldBuilder, type FilterExpression, FMTable, type FMTableWithColumns as TableOccurrenceResult, fmTableOccurrence, getTableColumns, gt, gte, type InferTableSchema, inArray, isColumn, isNotNull, isNull, lt, lte, ne, not, notInArray, numberField, type OrderByExpression, or, startsWith, textField, timeField, timestampField, } from './orm/index.js';
12
+ export { and, asc, type Column, type ColumnFunction, calcField, containerField, contains, dateField, desc, endsWith, eq, type FieldBuilder, type FilterExpression, FMTable, type FMTableWithColumns as TableOccurrenceResult, fmTableOccurrence, getTableColumns, gt, gte, type InferTableSchema, inArray, isColumn, isColumnFunction, isNotNull, isNull, lt, lte, matchesPattern, ne, not, notInArray, numberField, type OrderByExpression, or, startsWith, textField, timeField, timestampField, tolower, toupper, trim, } from './orm/index.js';
13
13
  export type { BatchItemResult, BatchResult, ExecuteMethodOptions, ExecuteOptions, FetchHandler, InferSchemaType, Metadata, ODataRecordMetadata, Result, } from './types.js';
package/dist/esm/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { AbortError, CircuitOpenError, NetworkError, RetryLimitError, TimeoutError } from "@fetchkit/ffetch";
2
2
  import { FMServerConnection } from "./client/filemaker-odata.js";
3
3
  import { BatchTruncatedError, FMODataError, HTTPError, ODataError, RecordCountMismatchError, ResponseParseError, ResponseStructureError, SchemaLockedError, ValidationError, isBatchTruncatedError, isFMODataError, isHTTPError, isODataError, isRecordCountMismatchError, isResponseParseError, isResponseStructureError, isSchemaLockedError, isValidationError } from "./errors.js";
4
- import { isColumn } from "./orm/column.js";
4
+ import { isColumn, isColumnFunction } from "./orm/column.js";
5
5
  import { calcField, containerField, dateField, numberField, textField, timeField, timestampField } from "./orm/field-builders.js";
6
- import { and, asc, contains, desc, endsWith, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, not, notInArray, or, startsWith } from "./orm/operators.js";
6
+ import { and, asc, contains, desc, endsWith, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, matchesPattern, ne, not, notInArray, or, startsWith, tolower, toupper, trim } from "./orm/operators.js";
7
7
  import { FMTable, fmTableOccurrence, getTableColumns } from "./orm/table.js";
8
8
  export {
9
9
  AbortError,
@@ -38,6 +38,7 @@ export {
38
38
  inArray,
39
39
  isBatchTruncatedError,
40
40
  isColumn,
41
+ isColumnFunction,
41
42
  isFMODataError,
42
43
  isHTTPError,
43
44
  isNotNull,
@@ -50,6 +51,7 @@ export {
50
51
  isValidationError,
51
52
  lt,
52
53
  lte,
54
+ matchesPattern,
53
55
  ne,
54
56
  not,
55
57
  notInArray,
@@ -58,6 +60,9 @@ export {
58
60
  startsWith,
59
61
  textField,
60
62
  timeField,
61
- timestampField
63
+ timestampField,
64
+ tolower,
65
+ toupper,
66
+ trim
62
67
  };
63
68
  //# sourceMappingURL=index.js.map
@@ -48,6 +48,21 @@ export declare class Column<TOutput = any, TInput = TOutput, TableName extends s
48
48
  * Type guard to check if a value is a Column instance.
49
49
  */
50
50
  export declare function isColumn(value: any): value is Column<any, any, any, any>;
51
+ /**
52
+ * ColumnFunction wraps a Column with an OData string function (tolower, toupper, trim).
53
+ * Since it extends Column, it passes `isColumn()` checks and works with all existing operators.
54
+ * Supports nesting: `tolower(trim(col))` → `tolower(trim(name))`.
55
+ */
56
+ export declare class ColumnFunction<TOutput = any, TInput = TOutput, TableName extends string = string, IsContainer extends boolean = false> extends Column<TOutput, TInput, TableName, IsContainer> {
57
+ readonly fnName: string;
58
+ readonly innerColumn: Column<TOutput, TInput, TableName, IsContainer>;
59
+ constructor(fnName: string, innerColumn: Column<TOutput, TInput, TableName, IsContainer>);
60
+ toFilterString(useEntityIds?: boolean): string;
61
+ }
62
+ /**
63
+ * Type guard to check if a value is a ColumnFunction instance.
64
+ */
65
+ export declare function isColumnFunction(value: any): value is ColumnFunction<any, any, any, any>;
51
66
  /**
52
67
  * Create a Column with proper type inference from the inputValidator.
53
68
  * This helper ensures TypeScript can infer TInput from the validator's input type.
@@ -1,6 +1,7 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+ import { needsFieldQuoting } from "../client/builders/select-utils.js";
4
5
  class Column {
5
6
  constructor(config) {
6
7
  __publicField(this, "fieldName");
@@ -56,8 +57,36 @@ class Column {
56
57
  function isColumn(value) {
57
58
  return value instanceof Column;
58
59
  }
60
+ class ColumnFunction extends Column {
61
+ constructor(fnName, innerColumn) {
62
+ super({
63
+ fieldName: innerColumn.fieldName,
64
+ entityId: innerColumn.entityId,
65
+ tableName: innerColumn.tableName,
66
+ tableEntityId: innerColumn.tableEntityId,
67
+ inputValidator: innerColumn.inputValidator
68
+ });
69
+ __publicField(this, "fnName");
70
+ __publicField(this, "innerColumn");
71
+ this.fnName = fnName;
72
+ this.innerColumn = innerColumn;
73
+ }
74
+ toFilterString(useEntityIds) {
75
+ if (isColumnFunction(this.innerColumn)) {
76
+ return `${this.fnName}(${this.innerColumn.toFilterString(useEntityIds)})`;
77
+ }
78
+ const fieldIdentifier = this.innerColumn.getFieldIdentifier(useEntityIds);
79
+ const quoted = needsFieldQuoting(fieldIdentifier) ? `"${fieldIdentifier}"` : fieldIdentifier;
80
+ return `${this.fnName}(${quoted})`;
81
+ }
82
+ }
83
+ function isColumnFunction(value) {
84
+ return value instanceof ColumnFunction;
85
+ }
59
86
  export {
60
87
  Column,
61
- isColumn
88
+ ColumnFunction,
89
+ isColumn,
90
+ isColumnFunction
62
91
  };
63
92
  //# sourceMappingURL=column.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"column.js","sources":["../../../src/orm/column.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\n/**\n * Column represents a type-safe reference to a table field.\n * Used in queries, filters, and operators to provide autocomplete and type checking.\n *\n * @template TOutput - The TypeScript type when reading from the database (output type)\n * @template TInput - The TypeScript type when writing to the database (input type, for filters)\n * @template TableName - The table name as a string literal type (for validation)\n * @template IsContainer - Whether this column represents a container field (cannot be selected)\n */\nexport class Column<\n // biome-ignore lint/suspicious/noExplicitAny: Default type parameter for flexibility\n TOutput = any,\n TInput = TOutput,\n TableName extends string = string,\n IsContainer extends boolean = false,\n> {\n readonly fieldName: string;\n readonly entityId?: `FMFID:${string}`;\n readonly tableName: TableName;\n readonly tableEntityId?: `FMTID:${string}`;\n // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer\n readonly inputValidator?: StandardSchemaV1<TInput, any>;\n\n // Phantom types for TypeScript inference - never actually hold values\n readonly _phantomOutput!: TOutput;\n readonly _phantomInput!: TInput;\n readonly _isContainer!: IsContainer;\n\n constructor(config: {\n fieldName: string;\n entityId?: `FMFID:${string}`;\n tableName: TableName;\n tableEntityId?: `FMTID:${string}`;\n // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer\n inputValidator?: StandardSchemaV1<TInput, any>;\n }) {\n this.fieldName = config.fieldName;\n this.entityId = config.entityId;\n this.tableName = config.tableName;\n this.tableEntityId = config.tableEntityId;\n this.inputValidator = config.inputValidator;\n }\n\n /**\n * Get the field identifier (entity ID if available, otherwise field name).\n * Used when building OData queries.\n */\n getFieldIdentifier(useEntityIds?: boolean): string {\n if (useEntityIds && this.entityId) {\n return this.entityId;\n }\n return this.fieldName;\n }\n\n /**\n * Get the table identifier (entity ID if available, otherwise table name).\n * Used when building OData queries.\n */\n getTableIdentifier(useEntityIds?: boolean): string {\n if (useEntityIds && this.tableEntityId) {\n return this.tableEntityId;\n }\n return this.tableName;\n }\n\n /**\n * Check if this column is from a specific table.\n * Useful for validation in cross-table operations.\n */\n isFromTable(tableName: string): boolean {\n return this.tableName === tableName;\n }\n\n /**\n * Create a string representation for debugging.\n */\n toString(): string {\n return `${this.tableName}.${this.fieldName}`;\n }\n}\n\n/**\n * Type guard to check if a value is a Column instance.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Type guard accepting any value type, generic constraint accepting any Column configuration\nexport function isColumn(value: any): value is Column<any, any, any, any> {\n return value instanceof Column;\n}\n\n/**\n * Create a Column with proper type inference from the inputValidator.\n * This helper ensures TypeScript can infer TInput from the validator's input type.\n * @internal\n */\nexport function createColumn<TOutput, TInput, TName extends string, IsContainer extends boolean = false>(config: {\n fieldName: string;\n entityId?: `FMFID:${string}`;\n tableName: TName;\n tableEntityId?: `FMTID:${string}`;\n // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer\n inputValidator?: StandardSchemaV1<TInput, any>;\n}): Column<TOutput, TInput, TName, IsContainer> {\n return new Column(config) as Column<TOutput, TInput, TName, IsContainer>;\n}\n"],"names":[],"mappings":";;;AAWO,MAAM,OAMX;AAAA,EAaA,YAAY,QAOT;AAnBM;AACA;AACA;AACA;AAEA;AAAA;AAGA;AAAA;AACA;AACA;AAUP,SAAK,YAAY,OAAO;AACxB,SAAK,WAAW,OAAO;AACvB,SAAK,YAAY,OAAO;AACxB,SAAK,gBAAgB,OAAO;AAC5B,SAAK,iBAAiB,OAAO;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,mBAAmB,cAAgC;AACjD,QAAI,gBAAgB,KAAK,UAAU;AACjC,aAAO,KAAK;AAAA,IACd;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,mBAAmB,cAAgC;AACjD,QAAI,gBAAgB,KAAK,eAAe;AACtC,aAAO,KAAK;AAAA,IACd;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,YAAY,WAA4B;AACtC,WAAO,KAAK,cAAc;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKA,WAAmB;AACjB,WAAO,GAAG,KAAK,SAAS,IAAI,KAAK,SAAS;AAAA,EAC5C;AACF;AAMO,SAAS,SAAS,OAAiD;AACxE,SAAO,iBAAiB;AAC1B;"}
1
+ {"version":3,"file":"column.js","sources":["../../../src/orm/column.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { needsFieldQuoting } from \"../client/builders/select-utils\";\n\n/**\n * Column represents a type-safe reference to a table field.\n * Used in queries, filters, and operators to provide autocomplete and type checking.\n *\n * @template TOutput - The TypeScript type when reading from the database (output type)\n * @template TInput - The TypeScript type when writing to the database (input type, for filters)\n * @template TableName - The table name as a string literal type (for validation)\n * @template IsContainer - Whether this column represents a container field (cannot be selected)\n */\nexport class Column<\n // biome-ignore lint/suspicious/noExplicitAny: Default type parameter for flexibility\n TOutput = any,\n TInput = TOutput,\n TableName extends string = string,\n IsContainer extends boolean = false,\n> {\n readonly fieldName: string;\n readonly entityId?: `FMFID:${string}`;\n readonly tableName: TableName;\n readonly tableEntityId?: `FMTID:${string}`;\n // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer\n readonly inputValidator?: StandardSchemaV1<TInput, any>;\n\n // Phantom types for TypeScript inference - never actually hold values\n readonly _phantomOutput!: TOutput;\n readonly _phantomInput!: TInput;\n readonly _isContainer!: IsContainer;\n\n constructor(config: {\n fieldName: string;\n entityId?: `FMFID:${string}`;\n tableName: TableName;\n tableEntityId?: `FMTID:${string}`;\n // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer\n inputValidator?: StandardSchemaV1<TInput, any>;\n }) {\n this.fieldName = config.fieldName;\n this.entityId = config.entityId;\n this.tableName = config.tableName;\n this.tableEntityId = config.tableEntityId;\n this.inputValidator = config.inputValidator;\n }\n\n /**\n * Get the field identifier (entity ID if available, otherwise field name).\n * Used when building OData queries.\n */\n getFieldIdentifier(useEntityIds?: boolean): string {\n if (useEntityIds && this.entityId) {\n return this.entityId;\n }\n return this.fieldName;\n }\n\n /**\n * Get the table identifier (entity ID if available, otherwise table name).\n * Used when building OData queries.\n */\n getTableIdentifier(useEntityIds?: boolean): string {\n if (useEntityIds && this.tableEntityId) {\n return this.tableEntityId;\n }\n return this.tableName;\n }\n\n /**\n * Check if this column is from a specific table.\n * Useful for validation in cross-table operations.\n */\n isFromTable(tableName: string): boolean {\n return this.tableName === tableName;\n }\n\n /**\n * Create a string representation for debugging.\n */\n toString(): string {\n return `${this.tableName}.${this.fieldName}`;\n }\n}\n\n/**\n * Type guard to check if a value is a Column instance.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Type guard accepting any value type, generic constraint accepting any Column configuration\nexport function isColumn(value: any): value is Column<any, any, any, any> {\n return value instanceof Column;\n}\n\n/**\n * ColumnFunction wraps a Column with an OData string function (tolower, toupper, trim).\n * Since it extends Column, it passes `isColumn()` checks and works with all existing operators.\n * Supports nesting: `tolower(trim(col))` → `tolower(trim(name))`.\n */\nexport class ColumnFunction<\n // biome-ignore lint/suspicious/noExplicitAny: Default type parameter for flexibility\n TOutput = any,\n TInput = TOutput,\n TableName extends string = string,\n IsContainer extends boolean = false,\n> extends Column<TOutput, TInput, TableName, IsContainer> {\n readonly fnName: string;\n readonly innerColumn: Column<TOutput, TInput, TableName, IsContainer>;\n\n constructor(\n fnName: string,\n innerColumn: Column<TOutput, TInput, TableName, IsContainer>,\n ) {\n super({\n fieldName: innerColumn.fieldName,\n entityId: innerColumn.entityId,\n tableName: innerColumn.tableName,\n tableEntityId: innerColumn.tableEntityId,\n inputValidator: innerColumn.inputValidator,\n });\n this.fnName = fnName;\n this.innerColumn = innerColumn;\n }\n\n toFilterString(useEntityIds?: boolean): string {\n if (isColumnFunction(this.innerColumn)) {\n return `${this.fnName}(${this.innerColumn.toFilterString(useEntityIds)})`;\n }\n const fieldIdentifier = this.innerColumn.getFieldIdentifier(useEntityIds);\n const quoted = needsFieldQuoting(fieldIdentifier)\n ? `\"${fieldIdentifier}\"`\n : fieldIdentifier;\n return `${this.fnName}(${quoted})`;\n }\n}\n\n/**\n * Type guard to check if a value is a ColumnFunction instance.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Type guard accepting any value type\nexport function isColumnFunction(value: any): value is ColumnFunction<any, any, any, any> {\n return value instanceof ColumnFunction;\n}\n\n/**\n * Create a Column with proper type inference from the inputValidator.\n * This helper ensures TypeScript can infer TInput from the validator's input type.\n * @internal\n */\nexport function createColumn<TOutput, TInput, TName extends string, IsContainer extends boolean = false>(config: {\n fieldName: string;\n entityId?: `FMFID:${string}`;\n tableName: TName;\n tableEntityId?: `FMTID:${string}`;\n // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer\n inputValidator?: StandardSchemaV1<TInput, any>;\n}): Column<TOutput, TInput, TName, IsContainer> {\n return new Column(config) as Column<TOutput, TInput, TName, IsContainer>;\n}\n"],"names":[],"mappings":";;;;AAYO,MAAM,OAMX;AAAA,EAaA,YAAY,QAOT;AAnBM;AACA;AACA;AACA;AAEA;AAAA;AAGA;AAAA;AACA;AACA;AAUP,SAAK,YAAY,OAAO;AACxB,SAAK,WAAW,OAAO;AACvB,SAAK,YAAY,OAAO;AACxB,SAAK,gBAAgB,OAAO;AAC5B,SAAK,iBAAiB,OAAO;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,mBAAmB,cAAgC;AACjD,QAAI,gBAAgB,KAAK,UAAU;AACjC,aAAO,KAAK;AAAA,IACd;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,mBAAmB,cAAgC;AACjD,QAAI,gBAAgB,KAAK,eAAe;AACtC,aAAO,KAAK;AAAA,IACd;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,YAAY,WAA4B;AACtC,WAAO,KAAK,cAAc;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA,EAKA,WAAmB;AACjB,WAAO,GAAG,KAAK,SAAS,IAAI,KAAK,SAAS;AAAA,EAC5C;AACF;AAMO,SAAS,SAAS,OAAiD;AACxE,SAAO,iBAAiB;AAC1B;AAOO,MAAM,uBAMH,OAAgD;AAAA,EAIxD,YACE,QACA,aACA;AACA,UAAM;AAAA,MACJ,WAAW,YAAY;AAAA,MACvB,UAAU,YAAY;AAAA,MACtB,WAAW,YAAY;AAAA,MACvB,eAAe,YAAY;AAAA,MAC3B,gBAAgB,YAAY;AAAA,IAAA,CAC7B;AAbM;AACA;AAaP,SAAK,SAAS;AACd,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,eAAe,cAAgC;AAC7C,QAAI,iBAAiB,KAAK,WAAW,GAAG;AACtC,aAAO,GAAG,KAAK,MAAM,IAAI,KAAK,YAAY,eAAe,YAAY,CAAC;AAAA,IACxE;AACA,UAAM,kBAAkB,KAAK,YAAY,mBAAmB,YAAY;AACxE,UAAM,SAAS,kBAAkB,eAAe,IAC5C,IAAI,eAAe,MACnB;AACJ,WAAO,GAAG,KAAK,MAAM,IAAI,MAAM;AAAA,EACjC;AACF;AAMO,SAAS,iBAAiB,OAAyD;AACxF,SAAO,iBAAiB;AAC1B;"}
@@ -1,5 +1,5 @@
1
1
  /** biome-ignore-all lint/performance/noBarrelFile: Re-exporting all ORM utilities */
2
- export { Column, isColumn } from './column.js';
2
+ export { Column, ColumnFunction, isColumn, isColumnFunction } from './column.js';
3
3
  export { type ContainerDbType, calcField, containerField, dateField, FieldBuilder, numberField, textField, timeField, timestampField, } from './field-builders.js';
4
- export { and, asc, contains, desc, endsWith, eq, FilterExpression, gt, gte, inArray, isNotNull, isNull, isOrderByExpression, lt, lte, ne, not, notInArray, OrderByExpression, or, startsWith, } from './operators.js';
4
+ export { and, asc, contains, desc, endsWith, eq, FilterExpression, gt, gte, inArray, isNotNull, isNull, isOrderByExpression, lt, lte, matchesPattern, ne, not, notInArray, OrderByExpression, or, startsWith, tolower, toupper, trim, } from './operators.js';
5
5
  export { FMTable, type FMTableWithColumns, fmTableOccurrence, getBaseTableConfig, getDefaultSelect, getFieldId, getFieldName, getTableColumns, getTableEntityId, getTableId, getTableName, type InferTableSchema, isUsingEntityIds, } from './table.js';
@@ -1,4 +1,4 @@
1
- import { Column } from './column.js';
1
+ import { Column, ColumnFunction } from './column.js';
2
2
  /**
3
3
  * FilterExpression represents a filter condition that can be used in where() clauses.
4
4
  * Internal representation of operator expressions that get converted to OData filter syntax.
@@ -87,6 +87,34 @@ export declare function startsWith<TOutput, TInput>(column: Column<TOutput, TInp
87
87
  * endsWith(users.email, "@example.com") // email ends with "@example.com"
88
88
  */
89
89
  export declare function endsWith<TOutput, TInput>(column: Column<TOutput, TInput>, value: NoInfer<TInput>): FilterExpression;
90
+ /**
91
+ * Matches pattern operator - checks if a string column matches a regex pattern.
92
+ *
93
+ * @example
94
+ * matchesPattern(users.name, "^A.*e$") // name matches regex pattern
95
+ */
96
+ export declare function matchesPattern<TOutput extends string | null, TInput>(column: Column<TOutput, TInput>, pattern: string): FilterExpression;
97
+ /**
98
+ * Wraps a column with OData `tolower()` for case-insensitive comparisons.
99
+ *
100
+ * @example
101
+ * eq(tolower(users.name), "john") // tolower(name) eq 'john'
102
+ */
103
+ export declare function tolower<TOutput extends string | null, TInput, TableName extends string, IsContainer extends boolean>(column: Column<TOutput, TInput, TableName, IsContainer>): ColumnFunction<TOutput, TInput, TableName, IsContainer>;
104
+ /**
105
+ * Wraps a column with OData `toupper()` for case-insensitive comparisons.
106
+ *
107
+ * @example
108
+ * eq(toupper(users.name), "JOHN") // toupper(name) eq 'JOHN'
109
+ */
110
+ export declare function toupper<TOutput extends string | null, TInput, TableName extends string, IsContainer extends boolean>(column: Column<TOutput, TInput, TableName, IsContainer>): ColumnFunction<TOutput, TInput, TableName, IsContainer>;
111
+ /**
112
+ * Wraps a column with OData `trim()` to remove leading/trailing whitespace.
113
+ *
114
+ * @example
115
+ * eq(trim(users.name), "John") // trim(name) eq 'John'
116
+ */
117
+ export declare function trim<TOutput extends string | null, TInput, TableName extends string, IsContainer extends boolean>(column: Column<TOutput, TInput, TableName, IsContainer>): ColumnFunction<TOutput, TInput, TableName, IsContainer>;
90
118
  /**
91
119
  * In array operator - checks if column value is in an array of values.
92
120
  *
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { needsFieldQuoting } from "../client/builders/select-utils.js";
5
- import { isColumn } from "./column.js";
5
+ import { ColumnFunction, isColumnFunction, isColumn } from "./column.js";
6
6
  class FilterExpression {
7
7
  // biome-ignore lint/suspicious/noExplicitAny: Operands can be Column, FilterExpression, or any value type
8
8
  constructor(operator, operands) {
@@ -42,6 +42,8 @@ class FilterExpression {
42
42
  return this._functionOp("startswith", useEntityIds);
43
43
  case "endsWith":
44
44
  return this._functionOp("endswith", useEntityIds);
45
+ case "matchesPattern":
46
+ return this._functionOp("matchesPattern", useEntityIds);
45
47
  // Null checks
46
48
  case "isNull":
47
49
  return this._isNullOp(useEntityIds);
@@ -124,6 +126,9 @@ class FilterExpression {
124
126
  throw new Error("NOT operator requires a FilterExpression operand");
125
127
  }
126
128
  _operandToString(operand, useEntityIds, column) {
129
+ if (isColumnFunction(operand)) {
130
+ return operand.toFilterString(useEntityIds);
131
+ }
127
132
  if (isColumn(operand)) {
128
133
  const fieldIdentifier = operand.getFieldIdentifier(useEntityIds);
129
134
  return needsFieldQuoting(fieldIdentifier) ? `"${fieldIdentifier}"` : fieldIdentifier;
@@ -182,6 +187,18 @@ function startsWith(column, value) {
182
187
  function endsWith(column, value) {
183
188
  return new FilterExpression("endsWith", [column, value]);
184
189
  }
190
+ function matchesPattern(column, pattern) {
191
+ return new FilterExpression("matchesPattern", [column, pattern]);
192
+ }
193
+ function tolower(column) {
194
+ return new ColumnFunction("tolower", column);
195
+ }
196
+ function toupper(column) {
197
+ return new ColumnFunction("toupper", column);
198
+ }
199
+ function trim(column) {
200
+ return new ColumnFunction("trim", column);
201
+ }
185
202
  function inArray(column, values) {
186
203
  return new FilterExpression("in", [column, values]);
187
204
  }
@@ -251,10 +268,14 @@ export {
251
268
  isOrderByExpression,
252
269
  lt,
253
270
  lte,
271
+ matchesPattern,
254
272
  ne,
255
273
  not,
256
274
  notInArray,
257
275
  or,
258
- startsWith
276
+ startsWith,
277
+ tolower,
278
+ toupper,
279
+ trim
259
280
  };
260
281
  //# sourceMappingURL=operators.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"operators.js","sources":["../../../src/orm/operators.ts"],"sourcesContent":["import { needsFieldQuoting } from \"../client/builders/select-utils\";\nimport type { Column } from \"./column\";\nimport { isColumn } from \"./column\";\n\n/**\n * FilterExpression represents a filter condition that can be used in where() clauses.\n * Internal representation of operator expressions that get converted to OData filter syntax.\n */\nexport class FilterExpression {\n readonly operator: string;\n // biome-ignore lint/suspicious/noExplicitAny: Operands can be Column, FilterExpression, or any value type\n readonly operands: (Column | any | FilterExpression)[];\n\n // biome-ignore lint/suspicious/noExplicitAny: Operands can be Column, FilterExpression, or any value type\n constructor(operator: string, operands: (Column | any | FilterExpression)[]) {\n this.operator = operator;\n this.operands = operands;\n }\n\n /**\n * Convert this expression to OData filter syntax.\n * @internal Used by QueryBuilder\n */\n toODataFilter(useEntityIds?: boolean): string {\n switch (this.operator) {\n // Comparison operators\n case \"eq\":\n return this._binaryOp(\"eq\", useEntityIds);\n case \"ne\":\n return this._binaryOp(\"ne\", useEntityIds);\n case \"gt\":\n return this._binaryOp(\"gt\", useEntityIds);\n case \"gte\":\n return this._binaryOp(\"ge\", useEntityIds);\n case \"lt\":\n return this._binaryOp(\"lt\", useEntityIds);\n case \"lte\":\n return this._binaryOp(\"le\", useEntityIds);\n case \"in\":\n return this._inOp(useEntityIds);\n case \"notIn\":\n return this._notInOp(useEntityIds);\n\n // String operators\n case \"contains\":\n return this._functionOp(\"contains\", useEntityIds);\n case \"startsWith\":\n return this._functionOp(\"startswith\", useEntityIds);\n case \"endsWith\":\n return this._functionOp(\"endswith\", useEntityIds);\n\n // Null checks\n case \"isNull\":\n return this._isNullOp(useEntityIds);\n case \"isNotNull\":\n return this._isNotNullOp(useEntityIds);\n\n // Logical operators\n case \"and\":\n return this._logicalOp(\"and\", useEntityIds);\n case \"or\":\n return this._logicalOp(\"or\", useEntityIds);\n case \"not\":\n return this._notOp(useEntityIds);\n\n default:\n throw new Error(`Unknown operator: ${this.operator}`);\n }\n }\n\n private _binaryOp(op: string, useEntityIds?: boolean): string {\n const [left, right] = this.operands;\n // For binary ops, the column is typically the first operand and value is the second\n // But we also support column-to-column comparisons, so check both\n let columnForValue: typeof left | typeof right | undefined;\n if (isColumn(left) && !isColumn(right)) {\n columnForValue = left;\n } else if (isColumn(right) && !isColumn(left)) {\n columnForValue = right;\n } else {\n columnForValue = undefined;\n }\n const leftStr = this._operandToString(left, useEntityIds, columnForValue);\n const rightStr = this._operandToString(right, useEntityIds, columnForValue);\n return `${leftStr} ${op} ${rightStr}`;\n }\n\n private _functionOp(fnName: string, useEntityIds?: boolean): string {\n const [column, value] = this.operands;\n const columnInstance = isColumn(column) ? column : undefined;\n const columnStr = this._operandToString(column, useEntityIds);\n const valueStr = this._operandToString(value, useEntityIds, columnInstance);\n return `${fnName}(${columnStr}, ${valueStr})`;\n }\n\n private _inOp(useEntityIds?: boolean): string {\n const [column, values] = this.operands;\n const columnInstance = isColumn(column) ? column : undefined;\n const columnStr = this._operandToString(column, useEntityIds);\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic array of values from user input\n const valuesStr = (values as any[]).map((v) => this._operandToString(v, useEntityIds, columnInstance)).join(\", \");\n return `${columnStr} in (${valuesStr})`;\n }\n\n private _notInOp(useEntityIds?: boolean): string {\n const [column, values] = this.operands;\n const columnInstance = isColumn(column) ? column : undefined;\n const columnStr = this._operandToString(column, useEntityIds);\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic array of values from user input\n const valuesStr = (values as any[]).map((v) => this._operandToString(v, useEntityIds, columnInstance)).join(\", \");\n return `not (${columnStr} in (${valuesStr}))`;\n }\n\n private _isNullOp(useEntityIds?: boolean): string {\n const [column] = this.operands;\n const columnStr = this._operandToString(column, useEntityIds);\n return `${columnStr} eq null`;\n }\n\n private _isNotNullOp(useEntityIds?: boolean): string {\n const [column] = this.operands;\n const columnStr = this._operandToString(column, useEntityIds);\n return `${columnStr} ne null`;\n }\n\n private _logicalOp(op: string, useEntityIds?: boolean): string {\n const expressions = this.operands.map((expr) => {\n if (expr instanceof FilterExpression) {\n const innerExpr = expr.toODataFilter(useEntityIds);\n // Wrap in parens if it's a logical expression to ensure precedence\n if (expr.operator === \"and\" || expr.operator === \"or\") {\n return `(${innerExpr})`;\n }\n return innerExpr;\n }\n throw new Error(\"Logical operators require FilterExpression operands\");\n });\n return expressions.join(` ${op} `);\n }\n\n private _notOp(useEntityIds?: boolean): string {\n const [expr] = this.operands;\n if (expr instanceof FilterExpression) {\n return `not (${expr.toODataFilter(useEntityIds)})`;\n }\n throw new Error(\"NOT operator requires a FilterExpression operand\");\n }\n\n private _operandToString(\n // biome-ignore lint/suspicious/noExplicitAny: Operand can be Column, FilterExpression, or any value type\n operand: any,\n useEntityIds?: boolean, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\n column?: Column<any, any, any, any>,\n ): string {\n if (isColumn(operand)) {\n const fieldIdentifier = operand.getFieldIdentifier(useEntityIds);\n // Quote field names in OData filters per FileMaker OData API requirements\n return needsFieldQuoting(fieldIdentifier) ? `\"${fieldIdentifier}\"` : fieldIdentifier;\n }\n\n // If we have a column with an input validator, apply it to transform the value\n let value = operand;\n if (column?.inputValidator) {\n try {\n const result = column.inputValidator[\"~standard\"].validate(value);\n // Handle async validators (though they shouldn't be async for filters)\n if (result instanceof Promise) {\n // For filters, we can't use async validators, so skip transformation\n // This is a limitation - async validators won't work in filters\n value = operand;\n } else if (\"issues\" in result && result.issues) {\n // Validation failed, use original value\n value = operand;\n } else if (\"value\" in result) {\n // Validation succeeded, use transformed value\n value = result.value;\n }\n } catch (_error) {\n // If validation throws, use the original value (will likely cause a query error)\n // This maintains backward compatibility and allows the server to handle validation\n value = operand;\n }\n }\n\n if (typeof value === \"string\") {\n return `'${value.replace(/'/g, \"''\")}'`; // Escape single quotes\n }\n if (value === null || value === undefined) {\n return \"null\";\n }\n if (value instanceof Date) {\n return value.toISOString();\n }\n return String(value);\n }\n}\n\n// ============================================================================\n// Comparison Operators\n// ============================================================================\n\n/**\n * Equal operator - checks if column equals a value or another column.\n *\n * @example\n * eq(users.name, \"John\") // name equals \"John\"\n * eq(users.id, contacts.id_user) // cross-table comparison\n */\nexport function eq<TOutput, TInput>(\n column1: Column<TOutput, TInput>,\n column2: Column<TOutput, TInput> | NoInfer<TInput>,\n): FilterExpression;\n// biome-ignore lint/suspicious/noExplicitAny: Implementation signature for overloads\nexport function eq(column: Column, value: any): FilterExpression {\n return new FilterExpression(\"eq\", [column, value]);\n}\n\n/**\n * Not equal operator - checks if column does not equal a value or another column.\n *\n * @example\n * ne(users.status, \"inactive\") // status not equal to \"inactive\"\n * ne(users.id, contacts.id_user) // cross-table comparison\n */\nexport function ne<TOutput, TInput>(\n column1: Column<TOutput, TInput>,\n column2: Column<TOutput, TInput> | NoInfer<TInput>,\n): FilterExpression;\n// biome-ignore lint/suspicious/noExplicitAny: Implementation signature for overloads\nexport function ne(column: Column, value: any): FilterExpression {\n return new FilterExpression(\"ne\", [column, value]);\n}\n\n/**\n * Greater than operator - checks if column is greater than a value.\n *\n * @example\n * gt(users.age, 18) // age greater than 18\n */\nexport function gt<TOutput extends number | string | Date | null, TInput>(\n column: Column<TOutput, TInput>,\n value: NoInfer<TInput>,\n): FilterExpression {\n return new FilterExpression(\"gt\", [column, value]);\n}\n\n/**\n * Greater than or equal operator - checks if column is >= a value.\n *\n * @example\n * gte(users.age, 18) // age >= 18\n */\nexport function gte<TOutput extends number | string | Date | null, TInput>(\n column: Column<TOutput, TInput>,\n value: NoInfer<TInput>,\n): FilterExpression {\n return new FilterExpression(\"gte\", [column, value]);\n}\n\n/**\n * Less than operator - checks if column is less than a value.\n *\n * @example\n * lt(users.age, 65) // age less than 65\n */\nexport function lt<TOutput extends number | string | Date | null, TInput>(\n column: Column<TOutput, TInput>,\n value: NoInfer<TInput>,\n): FilterExpression {\n return new FilterExpression(\"lt\", [column, value]);\n}\n\n/**\n * Less than or equal operator - checks if column is <= a value.\n *\n * @example\n * lte(users.age, 65) // age <= 65\n */\nexport function lte<TOutput extends number | string | Date | null, TInput>(\n column: Column<TOutput, TInput>,\n value: NoInfer<TInput>,\n): FilterExpression {\n return new FilterExpression(\"lte\", [column, value]);\n}\n\n// ============================================================================\n// String Operators\n// ============================================================================\n\n/**\n * Contains operator - checks if a string column contains a substring.\n *\n * @example\n * contains(users.name, \"John\") // name contains \"John\"\n */\nexport function contains<TOutput, TInput>(column: Column<TOutput, TInput>, value: NoInfer<TInput>): FilterExpression {\n return new FilterExpression(\"contains\", [column, value]);\n}\n\n/**\n * Starts with operator - checks if a string column starts with a prefix.\n *\n * @example\n * startsWith(users.email, \"admin\") // email starts with \"admin\"\n */\nexport function startsWith<TOutput, TInput>(column: Column<TOutput, TInput>, value: NoInfer<TInput>): FilterExpression {\n return new FilterExpression(\"startsWith\", [column, value]);\n}\n\n/**\n * Ends with operator - checks if a string column ends with a suffix.\n *\n * @example\n * endsWith(users.email, \"@example.com\") // email ends with \"@example.com\"\n */\nexport function endsWith<TOutput, TInput>(column: Column<TOutput, TInput>, value: NoInfer<TInput>): FilterExpression {\n return new FilterExpression(\"endsWith\", [column, value]);\n}\n\n// ============================================================================\n// Array Operators\n// ============================================================================\n\n/**\n * In array operator - checks if column value is in an array of values.\n *\n * @example\n * inArray(users.status, [\"active\", \"pending\"]) // status is \"active\" or \"pending\"\n */\nexport function inArray<TOutput, TInput>(column: Column<TOutput, TInput>, values: NoInfer<TInput>[]): FilterExpression {\n return new FilterExpression(\"in\", [column, values]);\n}\n\n/**\n * Not in array operator - checks if column value is not in an array of values.\n *\n * @example\n * notInArray(users.status, [\"deleted\", \"banned\"]) // status is neither \"deleted\" nor \"banned\"\n */\nexport function notInArray<TOutput, TInput>(\n column: Column<TOutput, TInput>,\n values: NoInfer<TInput>[],\n): FilterExpression {\n return new FilterExpression(\"notIn\", [column, values]);\n}\n\n// ============================================================================\n// Null Check Operators\n// ============================================================================\n\n/**\n * Is null operator - checks if column value is null.\n *\n * @example\n * isNull(users.deletedAt) // deletedAt is null\n */\nexport function isNull<TOutput, TInput>(column: Column<TOutput, TInput>): FilterExpression {\n return new FilterExpression(\"isNull\", [column]);\n}\n\n/**\n * Is not null operator - checks if column value is not null.\n *\n * @example\n * isNotNull(users.email) // email is not null\n */\nexport function isNotNull<TOutput, TInput>(column: Column<TOutput, TInput>): FilterExpression {\n return new FilterExpression(\"isNotNull\", [column]);\n}\n\n// ============================================================================\n// Logical Operators\n// ============================================================================\n\n/**\n * AND operator - combines multiple filter expressions with logical AND.\n * All expressions must be true for the record to match.\n *\n * @example\n * and(\n * eq(users.active, true),\n * gt(users.age, 18)\n * ) // active is true AND age > 18\n */\nexport function and(...expressions: FilterExpression[]): FilterExpression {\n if (expressions.length === 0) {\n throw new Error(\"AND operator requires at least one expression\");\n }\n if (expressions.length === 1 && expressions[0] !== undefined) {\n return expressions[0];\n }\n return new FilterExpression(\"and\", expressions);\n}\n\n/**\n * OR operator - combines multiple filter expressions with logical OR.\n * At least one expression must be true for the record to match.\n *\n * @example\n * or(\n * eq(users.role, \"admin\"),\n * eq(users.role, \"moderator\")\n * ) // role is \"admin\" OR \"moderator\"\n */\nexport function or(...expressions: FilterExpression[]): FilterExpression {\n if (expressions.length === 0) {\n throw new Error(\"OR operator requires at least one expression\");\n }\n if (expressions.length === 1 && expressions[0] !== undefined) {\n return expressions[0];\n }\n return new FilterExpression(\"or\", expressions);\n}\n\n/**\n * NOT operator - negates a filter expression.\n *\n * @example\n * not(eq(users.status, \"deleted\")) // status is NOT \"deleted\"\n */\nexport function not(expression: FilterExpression): FilterExpression {\n return new FilterExpression(\"not\", [expression]);\n}\n\n// ============================================================================\n// OrderBy Operators\n// ============================================================================\n\n/**\n * OrderByExpression represents a sort order specification for a column.\n * Used in orderBy() clauses to provide type-safe sorting with direction.\n */\nexport class OrderByExpression<TableName extends string = string> {\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\n readonly column: Column<any, any, TableName>;\n readonly direction: \"asc\" | \"desc\";\n\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\n constructor(column: Column<any, any, TableName>, direction: \"asc\" | \"desc\") {\n this.column = column;\n this.direction = direction;\n }\n}\n\n/**\n * Type guard to check if a value is an OrderByExpression instance.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Type guard accepting any value type\nexport function isOrderByExpression(value: any): value is OrderByExpression {\n return value instanceof OrderByExpression;\n}\n\n/**\n * Ascending order operator - sorts a column in ascending order.\n *\n * @example\n * asc(users.name) // Sort by name ascending\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\nexport function asc<TableName extends string>(column: Column<any, any, TableName>): OrderByExpression<TableName> {\n return new OrderByExpression(column, \"asc\");\n}\n\n/**\n * Descending order operator - sorts a column in descending order.\n *\n * @example\n * desc(users.age) // Sort by age descending\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\nexport function desc<TableName extends string>(column: Column<any, any, TableName>): OrderByExpression<TableName> {\n return new OrderByExpression(column, \"desc\");\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"operators.js","sources":["../../../src/orm/operators.ts"],"sourcesContent":["import { needsFieldQuoting } from \"../client/builders/select-utils\";\nimport { type Column, ColumnFunction, isColumn, isColumnFunction } from \"./column\";\n\n/**\n * FilterExpression represents a filter condition that can be used in where() clauses.\n * Internal representation of operator expressions that get converted to OData filter syntax.\n */\nexport class FilterExpression {\n readonly operator: string;\n // biome-ignore lint/suspicious/noExplicitAny: Operands can be Column, FilterExpression, or any value type\n readonly operands: (Column | any | FilterExpression)[];\n\n // biome-ignore lint/suspicious/noExplicitAny: Operands can be Column, FilterExpression, or any value type\n constructor(operator: string, operands: (Column | any | FilterExpression)[]) {\n this.operator = operator;\n this.operands = operands;\n }\n\n /**\n * Convert this expression to OData filter syntax.\n * @internal Used by QueryBuilder\n */\n toODataFilter(useEntityIds?: boolean): string {\n switch (this.operator) {\n // Comparison operators\n case \"eq\":\n return this._binaryOp(\"eq\", useEntityIds);\n case \"ne\":\n return this._binaryOp(\"ne\", useEntityIds);\n case \"gt\":\n return this._binaryOp(\"gt\", useEntityIds);\n case \"gte\":\n return this._binaryOp(\"ge\", useEntityIds);\n case \"lt\":\n return this._binaryOp(\"lt\", useEntityIds);\n case \"lte\":\n return this._binaryOp(\"le\", useEntityIds);\n case \"in\":\n return this._inOp(useEntityIds);\n case \"notIn\":\n return this._notInOp(useEntityIds);\n\n // String operators\n case \"contains\":\n return this._functionOp(\"contains\", useEntityIds);\n case \"startsWith\":\n return this._functionOp(\"startswith\", useEntityIds);\n case \"endsWith\":\n return this._functionOp(\"endswith\", useEntityIds);\n case \"matchesPattern\":\n return this._functionOp(\"matchesPattern\", useEntityIds);\n\n // Null checks\n case \"isNull\":\n return this._isNullOp(useEntityIds);\n case \"isNotNull\":\n return this._isNotNullOp(useEntityIds);\n\n // Logical operators\n case \"and\":\n return this._logicalOp(\"and\", useEntityIds);\n case \"or\":\n return this._logicalOp(\"or\", useEntityIds);\n case \"not\":\n return this._notOp(useEntityIds);\n\n default:\n throw new Error(`Unknown operator: ${this.operator}`);\n }\n }\n\n private _binaryOp(op: string, useEntityIds?: boolean): string {\n const [left, right] = this.operands;\n // For binary ops, the column is typically the first operand and value is the second\n // But we also support column-to-column comparisons, so check both\n let columnForValue: typeof left | typeof right | undefined;\n if (isColumn(left) && !isColumn(right)) {\n columnForValue = left;\n } else if (isColumn(right) && !isColumn(left)) {\n columnForValue = right;\n } else {\n columnForValue = undefined;\n }\n const leftStr = this._operandToString(left, useEntityIds, columnForValue);\n const rightStr = this._operandToString(right, useEntityIds, columnForValue);\n return `${leftStr} ${op} ${rightStr}`;\n }\n\n private _functionOp(fnName: string, useEntityIds?: boolean): string {\n const [column, value] = this.operands;\n const columnInstance = isColumn(column) ? column : undefined;\n const columnStr = this._operandToString(column, useEntityIds);\n const valueStr = this._operandToString(value, useEntityIds, columnInstance);\n return `${fnName}(${columnStr}, ${valueStr})`;\n }\n\n private _inOp(useEntityIds?: boolean): string {\n const [column, values] = this.operands;\n const columnInstance = isColumn(column) ? column : undefined;\n const columnStr = this._operandToString(column, useEntityIds);\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic array of values from user input\n const valuesStr = (values as any[]).map((v) => this._operandToString(v, useEntityIds, columnInstance)).join(\", \");\n return `${columnStr} in (${valuesStr})`;\n }\n\n private _notInOp(useEntityIds?: boolean): string {\n const [column, values] = this.operands;\n const columnInstance = isColumn(column) ? column : undefined;\n const columnStr = this._operandToString(column, useEntityIds);\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic array of values from user input\n const valuesStr = (values as any[]).map((v) => this._operandToString(v, useEntityIds, columnInstance)).join(\", \");\n return `not (${columnStr} in (${valuesStr}))`;\n }\n\n private _isNullOp(useEntityIds?: boolean): string {\n const [column] = this.operands;\n const columnStr = this._operandToString(column, useEntityIds);\n return `${columnStr} eq null`;\n }\n\n private _isNotNullOp(useEntityIds?: boolean): string {\n const [column] = this.operands;\n const columnStr = this._operandToString(column, useEntityIds);\n return `${columnStr} ne null`;\n }\n\n private _logicalOp(op: string, useEntityIds?: boolean): string {\n const expressions = this.operands.map((expr) => {\n if (expr instanceof FilterExpression) {\n const innerExpr = expr.toODataFilter(useEntityIds);\n // Wrap in parens if it's a logical expression to ensure precedence\n if (expr.operator === \"and\" || expr.operator === \"or\") {\n return `(${innerExpr})`;\n }\n return innerExpr;\n }\n throw new Error(\"Logical operators require FilterExpression operands\");\n });\n return expressions.join(` ${op} `);\n }\n\n private _notOp(useEntityIds?: boolean): string {\n const [expr] = this.operands;\n if (expr instanceof FilterExpression) {\n return `not (${expr.toODataFilter(useEntityIds)})`;\n }\n throw new Error(\"NOT operator requires a FilterExpression operand\");\n }\n\n private _operandToString(\n // biome-ignore lint/suspicious/noExplicitAny: Operand can be Column, FilterExpression, or any value type\n operand: any,\n useEntityIds?: boolean, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\n column?: Column<any, any, any, any>,\n ): string {\n if (isColumnFunction(operand)) {\n return operand.toFilterString(useEntityIds);\n }\n\n if (isColumn(operand)) {\n const fieldIdentifier = operand.getFieldIdentifier(useEntityIds);\n // Quote field names in OData filters per FileMaker OData API requirements\n return needsFieldQuoting(fieldIdentifier) ? `\"${fieldIdentifier}\"` : fieldIdentifier;\n }\n\n // If we have a column with an input validator, apply it to transform the value\n let value = operand;\n if (column?.inputValidator) {\n try {\n const result = column.inputValidator[\"~standard\"].validate(value);\n // Handle async validators (though they shouldn't be async for filters)\n if (result instanceof Promise) {\n // For filters, we can't use async validators, so skip transformation\n // This is a limitation - async validators won't work in filters\n value = operand;\n } else if (\"issues\" in result && result.issues) {\n // Validation failed, use original value\n value = operand;\n } else if (\"value\" in result) {\n // Validation succeeded, use transformed value\n value = result.value;\n }\n } catch (_error) {\n // If validation throws, use the original value (will likely cause a query error)\n // This maintains backward compatibility and allows the server to handle validation\n value = operand;\n }\n }\n\n if (typeof value === \"string\") {\n return `'${value.replace(/'/g, \"''\")}'`; // Escape single quotes\n }\n if (value === null || value === undefined) {\n return \"null\";\n }\n if (value instanceof Date) {\n return value.toISOString();\n }\n return String(value);\n }\n}\n\n// ============================================================================\n// Comparison Operators\n// ============================================================================\n\n/**\n * Equal operator - checks if column equals a value or another column.\n *\n * @example\n * eq(users.name, \"John\") // name equals \"John\"\n * eq(users.id, contacts.id_user) // cross-table comparison\n */\nexport function eq<TOutput, TInput>(\n column1: Column<TOutput, TInput>,\n column2: Column<TOutput, TInput> | NoInfer<TInput>,\n): FilterExpression;\n// biome-ignore lint/suspicious/noExplicitAny: Implementation signature for overloads\nexport function eq(column: Column, value: any): FilterExpression {\n return new FilterExpression(\"eq\", [column, value]);\n}\n\n/**\n * Not equal operator - checks if column does not equal a value or another column.\n *\n * @example\n * ne(users.status, \"inactive\") // status not equal to \"inactive\"\n * ne(users.id, contacts.id_user) // cross-table comparison\n */\nexport function ne<TOutput, TInput>(\n column1: Column<TOutput, TInput>,\n column2: Column<TOutput, TInput> | NoInfer<TInput>,\n): FilterExpression;\n// biome-ignore lint/suspicious/noExplicitAny: Implementation signature for overloads\nexport function ne(column: Column, value: any): FilterExpression {\n return new FilterExpression(\"ne\", [column, value]);\n}\n\n/**\n * Greater than operator - checks if column is greater than a value.\n *\n * @example\n * gt(users.age, 18) // age greater than 18\n */\nexport function gt<TOutput extends number | string | Date | null, TInput>(\n column: Column<TOutput, TInput>,\n value: NoInfer<TInput>,\n): FilterExpression {\n return new FilterExpression(\"gt\", [column, value]);\n}\n\n/**\n * Greater than or equal operator - checks if column is >= a value.\n *\n * @example\n * gte(users.age, 18) // age >= 18\n */\nexport function gte<TOutput extends number | string | Date | null, TInput>(\n column: Column<TOutput, TInput>,\n value: NoInfer<TInput>,\n): FilterExpression {\n return new FilterExpression(\"gte\", [column, value]);\n}\n\n/**\n * Less than operator - checks if column is less than a value.\n *\n * @example\n * lt(users.age, 65) // age less than 65\n */\nexport function lt<TOutput extends number | string | Date | null, TInput>(\n column: Column<TOutput, TInput>,\n value: NoInfer<TInput>,\n): FilterExpression {\n return new FilterExpression(\"lt\", [column, value]);\n}\n\n/**\n * Less than or equal operator - checks if column is <= a value.\n *\n * @example\n * lte(users.age, 65) // age <= 65\n */\nexport function lte<TOutput extends number | string | Date | null, TInput>(\n column: Column<TOutput, TInput>,\n value: NoInfer<TInput>,\n): FilterExpression {\n return new FilterExpression(\"lte\", [column, value]);\n}\n\n// ============================================================================\n// String Operators\n// ============================================================================\n\n/**\n * Contains operator - checks if a string column contains a substring.\n *\n * @example\n * contains(users.name, \"John\") // name contains \"John\"\n */\nexport function contains<TOutput, TInput>(column: Column<TOutput, TInput>, value: NoInfer<TInput>): FilterExpression {\n return new FilterExpression(\"contains\", [column, value]);\n}\n\n/**\n * Starts with operator - checks if a string column starts with a prefix.\n *\n * @example\n * startsWith(users.email, \"admin\") // email starts with \"admin\"\n */\nexport function startsWith<TOutput, TInput>(column: Column<TOutput, TInput>, value: NoInfer<TInput>): FilterExpression {\n return new FilterExpression(\"startsWith\", [column, value]);\n}\n\n/**\n * Ends with operator - checks if a string column ends with a suffix.\n *\n * @example\n * endsWith(users.email, \"@example.com\") // email ends with \"@example.com\"\n */\nexport function endsWith<TOutput, TInput>(column: Column<TOutput, TInput>, value: NoInfer<TInput>): FilterExpression {\n return new FilterExpression(\"endsWith\", [column, value]);\n}\n\n/**\n * Matches pattern operator - checks if a string column matches a regex pattern.\n *\n * @example\n * matchesPattern(users.name, \"^A.*e$\") // name matches regex pattern\n */\nexport function matchesPattern<TOutput extends string | null, TInput>(\n column: Column<TOutput, TInput>,\n pattern: string,\n): FilterExpression {\n return new FilterExpression(\"matchesPattern\", [column, pattern]);\n}\n\n// ============================================================================\n// String Transform Functions\n// ============================================================================\n\n/**\n * Wraps a column with OData `tolower()` for case-insensitive comparisons.\n *\n * @example\n * eq(tolower(users.name), \"john\") // tolower(name) eq 'john'\n */\nexport function tolower<TOutput extends string | null, TInput, TableName extends string, IsContainer extends boolean>(\n column: Column<TOutput, TInput, TableName, IsContainer>,\n): ColumnFunction<TOutput, TInput, TableName, IsContainer> {\n return new ColumnFunction(\"tolower\", column);\n}\n\n/**\n * Wraps a column with OData `toupper()` for case-insensitive comparisons.\n *\n * @example\n * eq(toupper(users.name), \"JOHN\") // toupper(name) eq 'JOHN'\n */\nexport function toupper<TOutput extends string | null, TInput, TableName extends string, IsContainer extends boolean>(\n column: Column<TOutput, TInput, TableName, IsContainer>,\n): ColumnFunction<TOutput, TInput, TableName, IsContainer> {\n return new ColumnFunction(\"toupper\", column);\n}\n\n/**\n * Wraps a column with OData `trim()` to remove leading/trailing whitespace.\n *\n * @example\n * eq(trim(users.name), \"John\") // trim(name) eq 'John'\n */\nexport function trim<TOutput extends string | null, TInput, TableName extends string, IsContainer extends boolean>(\n column: Column<TOutput, TInput, TableName, IsContainer>,\n): ColumnFunction<TOutput, TInput, TableName, IsContainer> {\n return new ColumnFunction(\"trim\", column);\n}\n\n// ============================================================================\n// Array Operators\n// ============================================================================\n\n/**\n * In array operator - checks if column value is in an array of values.\n *\n * @example\n * inArray(users.status, [\"active\", \"pending\"]) // status is \"active\" or \"pending\"\n */\nexport function inArray<TOutput, TInput>(column: Column<TOutput, TInput>, values: NoInfer<TInput>[]): FilterExpression {\n return new FilterExpression(\"in\", [column, values]);\n}\n\n/**\n * Not in array operator - checks if column value is not in an array of values.\n *\n * @example\n * notInArray(users.status, [\"deleted\", \"banned\"]) // status is neither \"deleted\" nor \"banned\"\n */\nexport function notInArray<TOutput, TInput>(\n column: Column<TOutput, TInput>,\n values: NoInfer<TInput>[],\n): FilterExpression {\n return new FilterExpression(\"notIn\", [column, values]);\n}\n\n// ============================================================================\n// Null Check Operators\n// ============================================================================\n\n/**\n * Is null operator - checks if column value is null.\n *\n * @example\n * isNull(users.deletedAt) // deletedAt is null\n */\nexport function isNull<TOutput, TInput>(column: Column<TOutput, TInput>): FilterExpression {\n return new FilterExpression(\"isNull\", [column]);\n}\n\n/**\n * Is not null operator - checks if column value is not null.\n *\n * @example\n * isNotNull(users.email) // email is not null\n */\nexport function isNotNull<TOutput, TInput>(column: Column<TOutput, TInput>): FilterExpression {\n return new FilterExpression(\"isNotNull\", [column]);\n}\n\n// ============================================================================\n// Logical Operators\n// ============================================================================\n\n/**\n * AND operator - combines multiple filter expressions with logical AND.\n * All expressions must be true for the record to match.\n *\n * @example\n * and(\n * eq(users.active, true),\n * gt(users.age, 18)\n * ) // active is true AND age > 18\n */\nexport function and(...expressions: FilterExpression[]): FilterExpression {\n if (expressions.length === 0) {\n throw new Error(\"AND operator requires at least one expression\");\n }\n if (expressions.length === 1 && expressions[0] !== undefined) {\n return expressions[0];\n }\n return new FilterExpression(\"and\", expressions);\n}\n\n/**\n * OR operator - combines multiple filter expressions with logical OR.\n * At least one expression must be true for the record to match.\n *\n * @example\n * or(\n * eq(users.role, \"admin\"),\n * eq(users.role, \"moderator\")\n * ) // role is \"admin\" OR \"moderator\"\n */\nexport function or(...expressions: FilterExpression[]): FilterExpression {\n if (expressions.length === 0) {\n throw new Error(\"OR operator requires at least one expression\");\n }\n if (expressions.length === 1 && expressions[0] !== undefined) {\n return expressions[0];\n }\n return new FilterExpression(\"or\", expressions);\n}\n\n/**\n * NOT operator - negates a filter expression.\n *\n * @example\n * not(eq(users.status, \"deleted\")) // status is NOT \"deleted\"\n */\nexport function not(expression: FilterExpression): FilterExpression {\n return new FilterExpression(\"not\", [expression]);\n}\n\n// ============================================================================\n// OrderBy Operators\n// ============================================================================\n\n/**\n * OrderByExpression represents a sort order specification for a column.\n * Used in orderBy() clauses to provide type-safe sorting with direction.\n */\nexport class OrderByExpression<TableName extends string = string> {\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\n readonly column: Column<any, any, TableName>;\n readonly direction: \"asc\" | \"desc\";\n\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\n constructor(column: Column<any, any, TableName>, direction: \"asc\" | \"desc\") {\n this.column = column;\n this.direction = direction;\n }\n}\n\n/**\n * Type guard to check if a value is an OrderByExpression instance.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Type guard accepting any value type\nexport function isOrderByExpression(value: any): value is OrderByExpression {\n return value instanceof OrderByExpression;\n}\n\n/**\n * Ascending order operator - sorts a column in ascending order.\n *\n * @example\n * asc(users.name) // Sort by name ascending\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\nexport function asc<TableName extends string>(column: Column<any, any, TableName>): OrderByExpression<TableName> {\n return new OrderByExpression(column, \"asc\");\n}\n\n/**\n * Descending order operator - sorts a column in descending order.\n *\n * @example\n * desc(users.age) // Sort by age descending\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\nexport function desc<TableName extends string>(column: Column<any, any, TableName>): OrderByExpression<TableName> {\n return new OrderByExpression(column, \"desc\");\n}\n"],"names":[],"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-beta.24",
3
+ "version": "0.1.0-beta.26",
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>",
@@ -47,7 +47,9 @@ export function getDefaultSelectFields(
47
47
  fields.push("ROWID", "ROWMODID");
48
48
  }
49
49
 
50
- return fields;
50
+ // Return undefined (meaning "all") when schema has no fields with validators,
51
+ // rather than an empty array which would generate an empty $select=
52
+ return fields.length > 0 ? fields : undefined;
51
53
  }
52
54
 
53
55
  if (Array.isArray(defaultSelect)) {
@@ -214,7 +214,9 @@ export class ExpandBuilder {
214
214
  if (opts.select) {
215
215
  const selectArray = Array.isArray(opts.select) ? opts.select.map(String) : [String(opts.select)];
216
216
  const selectFields = formatSelectFields(selectArray, config.targetTable, this.useEntityIds);
217
- parts.push(`$select=${selectFields}`);
217
+ if (selectFields) {
218
+ parts.push(`$select=${selectFields}`);
219
+ }
218
220
  }
219
221
 
220
222
  if (opts.filter) {
@@ -1,6 +1,7 @@
1
+ import type { FFetchOptions } from "@fetchkit/ffetch";
1
2
  import type { StandardSchemaV1 } from "@standard-schema/spec";
2
3
  import { FMTable } from "../orm/table";
3
- import type { ExecutableBuilder, ExecutionContext, Metadata } from "../types";
4
+ import type { ExecutableBuilder, ExecutionContext, Metadata, Result } from "../types";
4
5
  import { BatchBuilder } from "./batch-builder";
5
6
  import { EntitySet } from "./entity-set";
6
7
  import { SchemaManager } from "./schema-manager";
@@ -53,6 +54,13 @@ export class Database<IncludeSpecialColumns extends boolean = false> {
53
54
  this._includeSpecialColumns = (config?.includeSpecialColumns ?? false) as IncludeSpecialColumns;
54
55
  }
55
56
 
57
+ /**
58
+ * @internal Used by adapter packages to access the database filename.
59
+ */
60
+ get _getDatabaseName(): string {
61
+ return this.databaseName;
62
+ }
63
+
56
64
  /**
57
65
  * @internal Used by EntitySet to access database configuration
58
66
  */
@@ -67,6 +75,14 @@ export class Database<IncludeSpecialColumns extends boolean = false> {
67
75
  return this._includeSpecialColumns;
68
76
  }
69
77
 
78
+ /**
79
+ * @internal Used by adapter packages for raw OData requests.
80
+ * Delegates to the connection's _makeRequest with the database name prepended.
81
+ */
82
+ _makeRequest<T>(path: string, options?: RequestInit & FFetchOptions): Promise<Result<T>> {
83
+ return this.context._makeRequest<T>(`/${this.databaseName}${path}`, options);
84
+ }
85
+
70
86
  // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
71
87
  from<T extends FMTable<any, any>>(table: T): EntitySet<T, IncludeSpecialColumns> {
72
88
  // Only override database-level useEntityIds if table explicitly sets it
@@ -23,6 +23,8 @@ export class FMServerConnection implements ExecutionContext {
23
23
  private useEntityIds = false;
24
24
  private includeSpecialColumns = false;
25
25
  private readonly logger: InternalLogger;
26
+ /** @internal Stored so credential-override flows can inherit non-auth config. */
27
+ readonly _fetchClientOptions: FFetchOptions | undefined;
26
28
  constructor(config: {
27
29
  serverUrl: string;
28
30
  auth: Auth;
@@ -30,6 +32,7 @@ export class FMServerConnection implements ExecutionContext {
30
32
  logger?: Logger;
31
33
  }) {
32
34
  this.logger = createLogger(config.logger);
35
+ this._fetchClientOptions = config.fetchClientOptions;
33
36
  this.fetchClient = createClient({
34
37
  retries: 0,
35
38
  ...config.fetchClientOptions,
package/src/index.ts CHANGED
@@ -61,6 +61,7 @@ export {
61
61
  asc,
62
62
  // Column references
63
63
  type Column,
64
+ type ColumnFunction,
64
65
  calcField,
65
66
  containerField,
66
67
  contains,
@@ -88,10 +89,12 @@ export {
88
89
  type InferTableSchema,
89
90
  inArray,
90
91
  isColumn,
92
+ isColumnFunction,
91
93
  isNotNull,
92
94
  isNull,
93
95
  lt,
94
96
  lte,
97
+ matchesPattern,
95
98
  ne,
96
99
  not,
97
100
  notInArray,
@@ -104,6 +107,9 @@ export {
104
107
  textField,
105
108
  timeField,
106
109
  timestampField,
110
+ tolower,
111
+ toupper,
112
+ trim,
107
113
  } from "./orm/index";
108
114
  // Utility types for type annotations
109
115
  export type {
package/src/orm/column.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import { needsFieldQuoting } from "../client/builders/select-utils";
2
3
 
3
4
  /**
4
5
  * Column represents a type-safe reference to a table field.
@@ -89,6 +90,56 @@ export function isColumn(value: any): value is Column<any, any, any, any> {
89
90
  return value instanceof Column;
90
91
  }
91
92
 
93
+ /**
94
+ * ColumnFunction wraps a Column with an OData string function (tolower, toupper, trim).
95
+ * Since it extends Column, it passes `isColumn()` checks and works with all existing operators.
96
+ * Supports nesting: `tolower(trim(col))` → `tolower(trim(name))`.
97
+ */
98
+ export class ColumnFunction<
99
+ // biome-ignore lint/suspicious/noExplicitAny: Default type parameter for flexibility
100
+ TOutput = any,
101
+ TInput = TOutput,
102
+ TableName extends string = string,
103
+ IsContainer extends boolean = false,
104
+ > extends Column<TOutput, TInput, TableName, IsContainer> {
105
+ readonly fnName: string;
106
+ readonly innerColumn: Column<TOutput, TInput, TableName, IsContainer>;
107
+
108
+ constructor(
109
+ fnName: string,
110
+ innerColumn: Column<TOutput, TInput, TableName, IsContainer>,
111
+ ) {
112
+ super({
113
+ fieldName: innerColumn.fieldName,
114
+ entityId: innerColumn.entityId,
115
+ tableName: innerColumn.tableName,
116
+ tableEntityId: innerColumn.tableEntityId,
117
+ inputValidator: innerColumn.inputValidator,
118
+ });
119
+ this.fnName = fnName;
120
+ this.innerColumn = innerColumn;
121
+ }
122
+
123
+ toFilterString(useEntityIds?: boolean): string {
124
+ if (isColumnFunction(this.innerColumn)) {
125
+ return `${this.fnName}(${this.innerColumn.toFilterString(useEntityIds)})`;
126
+ }
127
+ const fieldIdentifier = this.innerColumn.getFieldIdentifier(useEntityIds);
128
+ const quoted = needsFieldQuoting(fieldIdentifier)
129
+ ? `"${fieldIdentifier}"`
130
+ : fieldIdentifier;
131
+ return `${this.fnName}(${quoted})`;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Type guard to check if a value is a ColumnFunction instance.
137
+ */
138
+ // biome-ignore lint/suspicious/noExplicitAny: Type guard accepting any value type
139
+ export function isColumnFunction(value: any): value is ColumnFunction<any, any, any, any> {
140
+ return value instanceof ColumnFunction;
141
+ }
142
+
92
143
  /**
93
144
  * Create a Column with proper type inference from the inputValidator.
94
145
  * This helper ensures TypeScript can infer TInput from the validator's input type.
package/src/orm/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Field builders - main API for defining table schemas
3
3
 
4
4
  // Column references - used in queries and filters
5
- export { Column, isColumn } from "./column";
5
+ export { Column, ColumnFunction, isColumn, isColumnFunction } from "./column";
6
6
  export {
7
7
  type ContainerDbType,
8
8
  calcField,
@@ -32,6 +32,7 @@ export {
32
32
  isOrderByExpression,
33
33
  lt,
34
34
  lte,
35
+ matchesPattern,
35
36
  ne,
36
37
  not,
37
38
  notInArray,
@@ -39,6 +40,9 @@ export {
39
40
  OrderByExpression,
40
41
  or,
41
42
  startsWith,
43
+ tolower,
44
+ toupper,
45
+ trim,
42
46
  } from "./operators";
43
47
 
44
48
  // Table definition - fmTableOccurrence function
@@ -1,6 +1,5 @@
1
1
  import { needsFieldQuoting } from "../client/builders/select-utils";
2
- import type { Column } from "./column";
3
- import { isColumn } from "./column";
2
+ import { type Column, ColumnFunction, isColumn, isColumnFunction } from "./column";
4
3
 
5
4
  /**
6
5
  * FilterExpression represents a filter condition that can be used in where() clauses.
@@ -48,6 +47,8 @@ export class FilterExpression {
48
47
  return this._functionOp("startswith", useEntityIds);
49
48
  case "endsWith":
50
49
  return this._functionOp("endswith", useEntityIds);
50
+ case "matchesPattern":
51
+ return this._functionOp("matchesPattern", useEntityIds);
51
52
 
52
53
  // Null checks
53
54
  case "isNull":
@@ -152,6 +153,10 @@ export class FilterExpression {
152
153
  useEntityIds?: boolean, // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
153
154
  column?: Column<any, any, any, any>,
154
155
  ): string {
156
+ if (isColumnFunction(operand)) {
157
+ return operand.toFilterString(useEntityIds);
158
+ }
159
+
155
160
  if (isColumn(operand)) {
156
161
  const fieldIdentifier = operand.getFieldIdentifier(useEntityIds);
157
162
  // Quote field names in OData filters per FileMaker OData API requirements
@@ -317,6 +322,59 @@ export function endsWith<TOutput, TInput>(column: Column<TOutput, TInput>, value
317
322
  return new FilterExpression("endsWith", [column, value]);
318
323
  }
319
324
 
325
+ /**
326
+ * Matches pattern operator - checks if a string column matches a regex pattern.
327
+ *
328
+ * @example
329
+ * matchesPattern(users.name, "^A.*e$") // name matches regex pattern
330
+ */
331
+ export function matchesPattern<TOutput extends string | null, TInput>(
332
+ column: Column<TOutput, TInput>,
333
+ pattern: string,
334
+ ): FilterExpression {
335
+ return new FilterExpression("matchesPattern", [column, pattern]);
336
+ }
337
+
338
+ // ============================================================================
339
+ // String Transform Functions
340
+ // ============================================================================
341
+
342
+ /**
343
+ * Wraps a column with OData `tolower()` for case-insensitive comparisons.
344
+ *
345
+ * @example
346
+ * eq(tolower(users.name), "john") // tolower(name) eq 'john'
347
+ */
348
+ export function tolower<TOutput extends string | null, TInput, TableName extends string, IsContainer extends boolean>(
349
+ column: Column<TOutput, TInput, TableName, IsContainer>,
350
+ ): ColumnFunction<TOutput, TInput, TableName, IsContainer> {
351
+ return new ColumnFunction("tolower", column);
352
+ }
353
+
354
+ /**
355
+ * Wraps a column with OData `toupper()` for case-insensitive comparisons.
356
+ *
357
+ * @example
358
+ * eq(toupper(users.name), "JOHN") // toupper(name) eq 'JOHN'
359
+ */
360
+ export function toupper<TOutput extends string | null, TInput, TableName extends string, IsContainer extends boolean>(
361
+ column: Column<TOutput, TInput, TableName, IsContainer>,
362
+ ): ColumnFunction<TOutput, TInput, TableName, IsContainer> {
363
+ return new ColumnFunction("toupper", column);
364
+ }
365
+
366
+ /**
367
+ * Wraps a column with OData `trim()` to remove leading/trailing whitespace.
368
+ *
369
+ * @example
370
+ * eq(trim(users.name), "John") // trim(name) eq 'John'
371
+ */
372
+ export function trim<TOutput extends string | null, TInput, TableName extends string, IsContainer extends boolean>(
373
+ column: Column<TOutput, TInput, TableName, IsContainer>,
374
+ ): ColumnFunction<TOutput, TInput, TableName, IsContainer> {
375
+ return new ColumnFunction("trim", column);
376
+ }
377
+
320
378
  // ============================================================================
321
379
  // Array Operators
322
380
  // ============================================================================