@proofkit/fmodata 0.1.0-alpha.16 → 0.1.0-alpha.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +2 -2
  2. package/dist/esm/client/builders/expand-builder.d.ts +3 -1
  3. package/dist/esm/client/builders/expand-builder.js +3 -2
  4. package/dist/esm/client/builders/expand-builder.js.map +1 -1
  5. package/dist/esm/client/builders/query-string-builder.d.ts +2 -0
  6. package/dist/esm/client/builders/query-string-builder.js +1 -1
  7. package/dist/esm/client/builders/query-string-builder.js.map +1 -1
  8. package/dist/esm/client/builders/response-processor.d.ts +2 -0
  9. package/dist/esm/client/builders/response-processor.js +3 -2
  10. package/dist/esm/client/builders/response-processor.js.map +1 -1
  11. package/dist/esm/client/builders/select-mixin.d.ts +2 -1
  12. package/dist/esm/client/builders/select-mixin.js +2 -2
  13. package/dist/esm/client/builders/select-mixin.js.map +1 -1
  14. package/dist/esm/client/entity-set.d.ts +2 -1
  15. package/dist/esm/client/entity-set.js +5 -2
  16. package/dist/esm/client/entity-set.js.map +1 -1
  17. package/dist/esm/client/filemaker-odata.d.ts +8 -0
  18. package/dist/esm/client/filemaker-odata.js +14 -0
  19. package/dist/esm/client/filemaker-odata.js.map +1 -1
  20. package/dist/esm/client/query/query-builder.d.ts +1 -0
  21. package/dist/esm/client/query/query-builder.js +20 -9
  22. package/dist/esm/client/query/query-builder.js.map +1 -1
  23. package/dist/esm/client/query/response-processor.d.ts +2 -0
  24. package/dist/esm/client/record-builder.d.ts +9 -7
  25. package/dist/esm/client/record-builder.js +41 -10
  26. package/dist/esm/client/record-builder.js.map +1 -1
  27. package/dist/esm/index.d.ts +1 -0
  28. package/dist/esm/logger.d.ts +47 -0
  29. package/dist/esm/logger.js +72 -0
  30. package/dist/esm/logger.js.map +1 -0
  31. package/dist/esm/logger.test.d.ts +1 -0
  32. package/dist/esm/types.d.ts +2 -0
  33. package/dist/esm/types.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/client/builders/expand-builder.ts +6 -2
  36. package/src/client/builders/query-string-builder.ts +3 -1
  37. package/src/client/builders/response-processor.ts +4 -1
  38. package/src/client/builders/select-mixin.ts +3 -1
  39. package/src/client/entity-set.ts +5 -2
  40. package/src/client/filemaker-odata.ts +18 -0
  41. package/src/client/query/query-builder.ts +19 -6
  42. package/src/client/query/response-processor.ts +2 -0
  43. package/src/client/record-builder.ts +68 -28
  44. package/src/index.ts +2 -0
  45. package/src/logger.test.ts +34 -0
  46. package/src/logger.ts +140 -0
  47. package/src/types.ts +2 -0
package/README.md CHANGED
@@ -262,9 +262,9 @@ Get a single field value:
262
262
 
263
263
  ```typescript
264
264
  const result = await db
265
- .from("users")
265
+ .from(users)
266
266
  .get("user-123")
267
- .getSingleField("email")
267
+ .getSingleField(users.email)
268
268
  .execute();
269
269
 
270
270
  if (result.data) {
@@ -1,6 +1,7 @@
1
1
  import { FMTable } from '../../orm/table.js';
2
2
  import { ExpandValidationConfig } from '../../validation.js';
3
3
  import { ExpandConfig } from './shared-types.js';
4
+ import { InternalLogger } from '../../logger.js';
4
5
  /**
5
6
  * Builds OData expand query strings and validation configs.
6
7
  * Handles nested expands recursively and transforms relation names to FMTIDs
@@ -8,7 +9,8 @@ import { ExpandConfig } from './shared-types.js';
8
9
  */
9
10
  export declare class ExpandBuilder {
10
11
  private useEntityIds;
11
- constructor(useEntityIds: boolean);
12
+ private logger;
13
+ constructor(useEntityIds: boolean, logger: InternalLogger);
12
14
  /**
13
15
  * Builds OData $expand query string from expand configurations.
14
16
  */
@@ -3,8 +3,9 @@ import { getBaseTableConfig, getTableName, getNavigationPaths, FMTable } from ".
3
3
  import { formatSelectFields } from "./select-utils.js";
4
4
  import { getDefaultSelectFields } from "./default-select.js";
5
5
  class ExpandBuilder {
6
- constructor(useEntityIds) {
6
+ constructor(useEntityIds, logger) {
7
7
  this.useEntityIds = useEntityIds;
8
+ this.logger = logger;
8
9
  }
9
10
  /**
10
11
  * Builds OData $expand query string from expand configurations.
@@ -57,7 +58,7 @@ class ExpandBuilder {
57
58
  if (sourceTable) {
58
59
  const navigationPaths = getNavigationPaths(sourceTable);
59
60
  if (navigationPaths && !navigationPaths.includes(relationName)) {
60
- console.warn(
61
+ this.logger.warn(
61
62
  `Cannot expand to "${relationName}". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(", ") : "none"}`
62
63
  );
63
64
  }
@@ -1 +1 @@
1
- {"version":3,"file":"expand-builder.js","sources":["../../../../src/client/builders/expand-builder.ts"],"sourcesContent":["import { QueryOptions } from \"odata-query\";\nimport buildQuery from \"odata-query\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { FMTable } from \"../../orm/table\";\nimport {\n getBaseTableConfig,\n getTableName,\n getNavigationPaths,\n} from \"../../orm/table\";\nimport type { ExpandValidationConfig } from \"../../validation\";\nimport type { ExpandConfig } from \"./shared-types\";\nimport { formatSelectFields } from \"./select-utils\";\nimport { getDefaultSelectFields } from \"./default-select\";\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 constructor(private useEntityIds: boolean) {}\n\n /**\n * Builds OData $expand query string from expand configurations.\n */\n buildExpandString(configs: ExpandConfig[]): string {\n if (configs.length === 0) return \"\";\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: 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 const selectedFields = config.options?.select\n ? Array.isArray(config.options.select)\n ? config.options.select.map(String)\n : [String(config.options.select)]\n : undefined;\n\n return {\n relation: config.relation,\n targetSchema,\n targetTable,\n table: targetTable,\n selectedFields,\n nestedExpands: undefined,\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 processExpand<TargetTable extends FMTable<any, any>, Builder = any>(\n targetTable: TargetTable,\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 console.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 const expandOptions: Partial<QueryOptions<any>> = {\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 if ((configuredBuilder as any).expandConfigs?.length > 0) {\n // Build nested expand string from the configured builder's expand configs\n const nestedExpandString = this.buildExpandString(\n (configuredBuilder as any).expandConfigs,\n );\n if (nestedExpandString) {\n // Add nested expand to options\n expandOptions.expand = nestedExpandString as any;\n }\n }\n\n return {\n relation: relationName,\n options: expandOptions,\n targetTable,\n };\n } else {\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 } else {\n return {\n relation: relationName,\n targetTable,\n };\n }\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 const tableId = (targetTable as any)[FMTable.Symbol.EntityId] as\n | `FMTID:${string}`\n | 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)\n ? opts.select.map(String)\n : [String(opts.select)];\n const selectFields = formatSelectFields(\n selectArray,\n config.targetTable,\n this.useEntityIds,\n );\n parts.push(`$select=${selectFields}`);\n }\n\n if (opts.filter) {\n const filterQuery = buildQuery({ filter: opts.filter });\n const match = filterQuery.match(/\\$filter=([^&]+)/);\n if (match) parts.push(`$filter=${match[1]}`);\n }\n\n if (opts.orderBy) {\n const orderByValue = Array.isArray(opts.orderBy)\n ? opts.orderBy.join(\",\")\n : String(opts.orderBy);\n parts.push(`$orderby=${orderByValue}`);\n }\n\n if (opts.top !== undefined) parts.push(`$top=${opts.top}`);\n if (opts.skip !== undefined) parts.push(`$skip=${opts.skip}`);\n\n if (opts.expand) {\n if (typeof opts.expand === \"string\") {\n parts.push(`$expand=${opts.expand}`);\n }\n }\n\n return parts;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"expand-builder.js","sources":["../../../../src/client/builders/expand-builder.ts"],"sourcesContent":["import { QueryOptions } from \"odata-query\";\nimport buildQuery from \"odata-query\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { FMTable } from \"../../orm/table\";\nimport {\n getBaseTableConfig,\n getTableName,\n getNavigationPaths,\n} from \"../../orm/table\";\nimport type { ExpandValidationConfig } from \"../../validation\";\nimport type { ExpandConfig } from \"./shared-types\";\nimport { formatSelectFields } from \"./select-utils\";\nimport { getDefaultSelectFields } from \"./default-select\";\nimport { InternalLogger } from \"../../logger\";\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 constructor(\n private useEntityIds: boolean,\n private logger: InternalLogger,\n ) {}\n\n /**\n * Builds OData $expand query string from expand configurations.\n */\n buildExpandString(configs: ExpandConfig[]): string {\n if (configs.length === 0) return \"\";\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: 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 const selectedFields = config.options?.select\n ? Array.isArray(config.options.select)\n ? config.options.select.map(String)\n : [String(config.options.select)]\n : undefined;\n\n return {\n relation: config.relation,\n targetSchema,\n targetTable,\n table: targetTable,\n selectedFields,\n nestedExpands: undefined,\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 processExpand<TargetTable extends FMTable<any, any>, Builder = any>(\n targetTable: TargetTable,\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 const expandOptions: Partial<QueryOptions<any>> = {\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 if ((configuredBuilder as any).expandConfigs?.length > 0) {\n // Build nested expand string from the configured builder's expand configs\n const nestedExpandString = this.buildExpandString(\n (configuredBuilder as any).expandConfigs,\n );\n if (nestedExpandString) {\n // Add nested expand to options\n expandOptions.expand = nestedExpandString as any;\n }\n }\n\n return {\n relation: relationName,\n options: expandOptions,\n targetTable,\n };\n } else {\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 } else {\n return {\n relation: relationName,\n targetTable,\n };\n }\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 const tableId = (targetTable as any)[FMTable.Symbol.EntityId] as\n | `FMTID:${string}`\n | 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)\n ? opts.select.map(String)\n : [String(opts.select)];\n const selectFields = formatSelectFields(\n selectArray,\n config.targetTable,\n this.useEntityIds,\n );\n parts.push(`$select=${selectFields}`);\n }\n\n if (opts.filter) {\n const filterQuery = buildQuery({ filter: opts.filter });\n const match = filterQuery.match(/\\$filter=([^&]+)/);\n if (match) parts.push(`$filter=${match[1]}`);\n }\n\n if (opts.orderBy) {\n const orderByValue = Array.isArray(opts.orderBy)\n ? opts.orderBy.join(\",\")\n : String(opts.orderBy);\n parts.push(`$orderby=${orderByValue}`);\n }\n\n if (opts.top !== undefined) parts.push(`$top=${opts.top}`);\n if (opts.skip !== undefined) parts.push(`$skip=${opts.skip}`);\n\n if (opts.expand) {\n if (typeof opts.expand === \"string\") {\n parts.push(`$expand=${opts.expand}`);\n }\n }\n\n return parts;\n }\n}\n"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { FMTable } from '../../orm/table.js';
2
2
  import { ExpandConfig } from './shared-types.js';
3
+ import { InternalLogger } from '../../logger.js';
3
4
  /**
4
5
  * Builds OData query string for $select and $expand parameters.
5
6
  * Used by both QueryBuilder and RecordBuilder to eliminate duplication.
@@ -12,4 +13,5 @@ export declare function buildSelectExpandQueryString(config: {
12
13
  expandConfigs: ExpandConfig[];
13
14
  table?: FMTable<any, any>;
14
15
  useEntityIds: boolean;
16
+ logger: InternalLogger;
15
17
  }): string;
@@ -2,7 +2,7 @@ import { ExpandBuilder } from "./expand-builder.js";
2
2
  import { formatSelectFields } from "./select-utils.js";
3
3
  function buildSelectExpandQueryString(config) {
4
4
  const parts = [];
5
- const expandBuilder = new ExpandBuilder(config.useEntityIds);
5
+ const expandBuilder = new ExpandBuilder(config.useEntityIds, config.logger);
6
6
  if (config.selectedFields && config.selectedFields.length > 0) {
7
7
  const selectString = formatSelectFields(
8
8
  config.selectedFields,
@@ -1 +1 @@
1
- {"version":3,"file":"query-string-builder.js","sources":["../../../../src/client/builders/query-string-builder.ts"],"sourcesContent":["import type { FMTable } from \"../../orm/table\";\nimport { ExpandBuilder } from \"./expand-builder\";\nimport type { ExpandConfig } from \"./shared-types\";\nimport { formatSelectFields } from \"./select-utils\";\n\n/**\n * Builds OData query string for $select and $expand parameters.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param config - Configuration object\n * @returns Query string starting with ? or empty string if no parameters\n */\nexport function buildSelectExpandQueryString(config: {\n selectedFields?: string[];\n expandConfigs: ExpandConfig[];\n table?: FMTable<any, any>;\n useEntityIds: boolean;\n}): string {\n const parts: string[] = [];\n const expandBuilder = new ExpandBuilder(config.useEntityIds);\n\n // Build $select\n if (config.selectedFields && config.selectedFields.length > 0) {\n const selectString = formatSelectFields(\n config.selectedFields,\n config.table,\n config.useEntityIds,\n );\n if (selectString) {\n parts.push(`$select=${selectString}`);\n }\n }\n\n // Build $expand\n const expandString = expandBuilder.buildExpandString(config.expandConfigs);\n if (expandString) {\n parts.push(`$expand=${expandString}`);\n }\n\n return parts.length > 0 ? `?${parts.join(\"&\")}` : \"\";\n}\n"],"names":[],"mappings":";;AAYO,SAAS,6BAA6B,QAKlC;AACT,QAAM,QAAkB,CAAC;AACzB,QAAM,gBAAgB,IAAI,cAAc,OAAO,YAAY;AAG3D,MAAI,OAAO,kBAAkB,OAAO,eAAe,SAAS,GAAG;AAC7D,UAAM,eAAe;AAAA,MACnB,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,IACT;AACA,QAAI,cAAc;AACV,YAAA,KAAK,WAAW,YAAY,EAAE;AAAA,IAAA;AAAA,EACtC;AAIF,QAAM,eAAe,cAAc,kBAAkB,OAAO,aAAa;AACzE,MAAI,cAAc;AACV,UAAA,KAAK,WAAW,YAAY,EAAE;AAAA,EAAA;AAG/B,SAAA,MAAM,SAAS,IAAI,IAAI,MAAM,KAAK,GAAG,CAAC,KAAK;AACpD;"}
1
+ {"version":3,"file":"query-string-builder.js","sources":["../../../../src/client/builders/query-string-builder.ts"],"sourcesContent":["import type { FMTable } from \"../../orm/table\";\nimport { ExpandBuilder } from \"./expand-builder\";\nimport type { ExpandConfig } from \"./shared-types\";\nimport { formatSelectFields } from \"./select-utils\";\nimport { InternalLogger } from \"../../logger\";\n\n/**\n * Builds OData query string for $select and $expand parameters.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param config - Configuration object\n * @returns Query string starting with ? or empty string if no parameters\n */\nexport function buildSelectExpandQueryString(config: {\n selectedFields?: string[];\n expandConfigs: ExpandConfig[];\n table?: FMTable<any, any>;\n useEntityIds: boolean;\n logger: InternalLogger;\n}): string {\n const parts: string[] = [];\n const expandBuilder = new ExpandBuilder(config.useEntityIds, config.logger);\n\n // Build $select\n if (config.selectedFields && config.selectedFields.length > 0) {\n const selectString = formatSelectFields(\n config.selectedFields,\n config.table,\n config.useEntityIds,\n );\n if (selectString) {\n parts.push(`$select=${selectString}`);\n }\n }\n\n // Build $expand\n const expandString = expandBuilder.buildExpandString(config.expandConfigs);\n if (expandString) {\n parts.push(`$expand=${expandString}`);\n }\n\n return parts.length > 0 ? `?${parts.join(\"&\")}` : \"\";\n}\n"],"names":[],"mappings":";;AAaO,SAAS,6BAA6B,QAMlC;AACT,QAAM,QAAkB,CAAC;AACzB,QAAM,gBAAgB,IAAI,cAAc,OAAO,cAAc,OAAO,MAAM;AAG1E,MAAI,OAAO,kBAAkB,OAAO,eAAe,SAAS,GAAG;AAC7D,UAAM,eAAe;AAAA,MACnB,OAAO;AAAA,MACP,OAAO;AAAA,MACP,OAAO;AAAA,IACT;AACA,QAAI,cAAc;AACV,YAAA,KAAK,WAAW,YAAY,EAAE;AAAA,IAAA;AAAA,EACtC;AAIF,QAAM,eAAe,cAAc,kBAAkB,OAAO,aAAa;AACzE,MAAI,cAAc;AACV,UAAA,KAAK,WAAW,YAAY,EAAE;AAAA,EAAA;AAG/B,SAAA,MAAM,SAAS,IAAI,IAAI,MAAM,KAAK,GAAG,CAAC,KAAK;AACpD;"}
@@ -2,6 +2,7 @@ import { FMTable } from '../../orm/table.js';
2
2
  import { Result } from '../../types.js';
3
3
  import { ExpandValidationConfig } from '../../validation.js';
4
4
  import { ExpandConfig } from './shared-types.js';
5
+ import { InternalLogger } from '../../logger.js';
5
6
  export interface ProcessResponseConfig {
6
7
  table?: FMTable<any, any>;
7
8
  schema?: Record<string, any>;
@@ -36,4 +37,5 @@ export declare function processQueryResponse<T>(response: any, config: {
36
37
  skipValidation?: boolean;
37
38
  useEntityIds?: boolean;
38
39
  fieldMapping?: Record<string, string>;
40
+ logger: InternalLogger;
39
41
  }): Promise<Result<any>>;
@@ -140,9 +140,10 @@ async function processQueryResponse(response, config) {
140
140
  expandConfigs,
141
141
  skipValidation,
142
142
  useEntityIds,
143
- fieldMapping
143
+ fieldMapping,
144
+ logger
144
145
  } = config;
145
- const expandBuilder = new ExpandBuilder(useEntityIds ?? false);
146
+ const expandBuilder = new ExpandBuilder(useEntityIds ?? false, logger);
146
147
  const expandValidationConfigs = expandBuilder.buildValidationConfigs(expandConfigs);
147
148
  const selectedFields = queryOptions.select ? Array.isArray(queryOptions.select) ? queryOptions.select.map(String) : [String(queryOptions.select)] : void 0;
148
149
  let processedResponse = await processODataResponse(response, {
@@ -1 +1 @@
1
- {"version":3,"file":"response-processor.js","sources":["../../../../src/client/builders/response-processor.ts"],"sourcesContent":["import type { FMTable } from \"../../orm/table\";\nimport type { Result } from \"../../types\";\nimport type { ExpandValidationConfig } from \"../../validation\";\nimport { validateSingleResponse, validateListResponse } from \"../../validation\";\nimport { transformResponseFields } from \"../../transform\";\nimport { RecordCountMismatchError } from \"../../errors\";\nimport { getBaseTableConfig } from \"../../orm/table\";\nimport { ExpandBuilder } from \"./expand-builder\";\nimport type { ExpandConfig } from \"./shared-types\";\n\nexport interface ProcessResponseConfig {\n table?: FMTable<any, any>;\n schema?: Record<string, any>;\n singleMode: \"exact\" | \"maybe\" | false;\n selectedFields?: string[];\n expandValidationConfigs?: ExpandValidationConfig[];\n skipValidation?: boolean;\n useEntityIds?: boolean;\n // Mapping from field names to output keys (for renamed fields in select)\n fieldMapping?: Record<string, string>;\n}\n\n/**\n * Processes OData response with transformation and validation.\n * Shared by QueryBuilder and RecordBuilder.\n */\nexport async function processODataResponse<T>(\n rawResponse: any,\n config: ProcessResponseConfig,\n): Promise<Result<T>> {\n const {\n table,\n schema,\n singleMode,\n selectedFields,\n expandValidationConfigs,\n skipValidation,\n useEntityIds,\n fieldMapping,\n } = config;\n\n // Transform field IDs back to names if using entity IDs\n let response = rawResponse;\n if (table && useEntityIds) {\n response = transformResponseFields(\n response,\n table,\n expandValidationConfigs,\n );\n }\n\n // Fast path: skip validation\n if (skipValidation) {\n const result = extractRecords(response, singleMode);\n // Rename fields AFTER extraction (but before returning)\n if (result.data && fieldMapping && Object.keys(fieldMapping).length > 0) {\n if (result.error) {\n return { data: undefined, error: result.error } as Result<T>;\n }\n return {\n data: renameFieldsInResponse(result.data, fieldMapping) as T,\n error: undefined,\n };\n }\n return result as Result<T>;\n }\n\n // Validation path\n if (singleMode !== false) {\n const validation = await validateSingleResponse<any>(\n response,\n schema,\n selectedFields as any,\n expandValidationConfigs,\n singleMode,\n );\n\n if (!validation.valid) {\n return { data: undefined, error: validation.error };\n }\n\n // Rename fields AFTER validation completes\n if (fieldMapping && Object.keys(fieldMapping).length > 0) {\n return {\n data: renameFieldsInResponse(validation.data, fieldMapping) as T,\n error: undefined,\n };\n }\n\n return { data: validation.data as T, error: undefined };\n }\n\n const validation = await validateListResponse<any>(\n response,\n schema,\n selectedFields as any,\n expandValidationConfigs,\n );\n\n if (!validation.valid) {\n return { data: undefined, error: validation.error };\n }\n\n // Rename fields AFTER validation completes\n if (fieldMapping && Object.keys(fieldMapping).length > 0) {\n return {\n data: renameFieldsInResponse(validation.data, fieldMapping) as T,\n error: undefined,\n };\n }\n\n return { data: validation.data as T, error: undefined };\n}\n\n/**\n * Extracts records from response without validation.\n */\nfunction extractRecords<T>(\n response: any,\n singleMode: \"exact\" | \"maybe\" | false,\n): Result<T> {\n if (singleMode === false) {\n const records = response.value ?? [];\n return { data: records as T, error: undefined };\n }\n\n const records = response.value ?? [response];\n const count = Array.isArray(records) ? records.length : 1;\n\n if (count > 1) {\n return {\n data: undefined,\n error: new RecordCountMismatchError(\n singleMode === \"exact\" ? \"one\" : \"at-most-one\",\n count,\n ),\n };\n }\n\n if (count === 0) {\n if (singleMode === \"exact\") {\n return { data: undefined, error: new RecordCountMismatchError(\"one\", 0) };\n }\n return { data: null as T, error: undefined };\n }\n\n const record = Array.isArray(records) ? records[0] : records;\n return { data: record as T, error: undefined };\n}\n\n/**\n * Gets schema from a table occurrence, excluding container fields.\n * Container fields are never returned in regular responses (only via getSingleField).\n */\nexport function getSchemaFromTable(\n table: FMTable<any, any> | undefined,\n): Record<string, any> | undefined {\n if (!table) return undefined;\n const baseTableConfig = getBaseTableConfig(table);\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 return schema;\n}\n\n/**\n * Renames fields in response data according to the field mapping.\n * Used when select() is called with renamed fields (e.g., { userEmail: users.email }).\n */\nfunction renameFieldsInResponse(\n data: any,\n fieldMapping: Record<string, string>,\n): any {\n if (!data || typeof data !== \"object\") {\n return data;\n }\n\n // Handle array responses\n if (Array.isArray(data)) {\n return data.map((item) => renameFieldsInResponse(item, fieldMapping));\n }\n\n // Handle OData list response structure\n if (\"value\" in data && Array.isArray(data.value)) {\n return {\n ...data,\n value: data.value.map((item: any) =>\n renameFieldsInResponse(item, fieldMapping),\n ),\n };\n }\n\n // Handle single record\n const renamed: Record<string, any> = {};\n for (const [key, value] of Object.entries(data)) {\n // Check if this field should be renamed\n const outputKey = fieldMapping[key];\n if (outputKey) {\n renamed[outputKey] = value;\n } else {\n renamed[key] = value;\n }\n }\n return renamed;\n}\n\n/**\n * Processes query response with expand configs.\n * This is a convenience wrapper that builds validation configs from expand configs.\n */\nexport async function processQueryResponse<T>(\n response: any,\n config: {\n occurrence?: FMTable<any, any>;\n singleMode: \"exact\" | \"maybe\" | false;\n queryOptions: { select?: (keyof T)[] | string[] };\n expandConfigs: ExpandConfig[];\n skipValidation?: boolean;\n useEntityIds?: boolean;\n // Mapping from field names to output keys (for renamed fields in select)\n fieldMapping?: Record<string, string>;\n },\n): Promise<Result<any>> {\n const {\n occurrence,\n singleMode,\n queryOptions,\n expandConfigs,\n skipValidation,\n useEntityIds,\n fieldMapping,\n } = config;\n\n const expandBuilder = new ExpandBuilder(useEntityIds ?? false);\n const expandValidationConfigs =\n expandBuilder.buildValidationConfigs(expandConfigs);\n\n const selectedFields = queryOptions.select\n ? Array.isArray(queryOptions.select)\n ? queryOptions.select.map(String)\n : [String(queryOptions.select)]\n : undefined;\n\n // Process the response first\n let processedResponse = await processODataResponse(response, {\n table: occurrence,\n schema: getSchemaFromTable(occurrence),\n singleMode,\n selectedFields,\n expandValidationConfigs,\n skipValidation,\n useEntityIds,\n });\n\n // Rename fields if field mapping is provided (for renamed fields in select)\n if (\n processedResponse.data &&\n fieldMapping &&\n Object.keys(fieldMapping).length > 0\n ) {\n processedResponse = {\n ...processedResponse,\n data: renameFieldsInResponse(processedResponse.data, fieldMapping),\n };\n }\n\n return processedResponse;\n}\n"],"names":["validation","records"],"mappings":"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"}
1
+ {"version":3,"file":"response-processor.js","sources":["../../../../src/client/builders/response-processor.ts"],"sourcesContent":["import type { FMTable } from \"../../orm/table\";\nimport type { Result } from \"../../types\";\nimport type { ExpandValidationConfig } from \"../../validation\";\nimport { validateSingleResponse, validateListResponse } from \"../../validation\";\nimport { transformResponseFields } from \"../../transform\";\nimport { RecordCountMismatchError } from \"../../errors\";\nimport { getBaseTableConfig } from \"../../orm/table\";\nimport { ExpandBuilder } from \"./expand-builder\";\nimport type { ExpandConfig } from \"./shared-types\";\nimport { InternalLogger } from \"../../logger\";\n\nexport interface ProcessResponseConfig {\n table?: FMTable<any, any>;\n schema?: Record<string, any>;\n singleMode: \"exact\" | \"maybe\" | false;\n selectedFields?: string[];\n expandValidationConfigs?: ExpandValidationConfig[];\n skipValidation?: boolean;\n useEntityIds?: boolean;\n // Mapping from field names to output keys (for renamed fields in select)\n fieldMapping?: Record<string, string>;\n}\n\n/**\n * Processes OData response with transformation and validation.\n * Shared by QueryBuilder and RecordBuilder.\n */\nexport async function processODataResponse<T>(\n rawResponse: any,\n config: ProcessResponseConfig,\n): Promise<Result<T>> {\n const {\n table,\n schema,\n singleMode,\n selectedFields,\n expandValidationConfigs,\n skipValidation,\n useEntityIds,\n fieldMapping,\n } = config;\n\n // Transform field IDs back to names if using entity IDs\n let response = rawResponse;\n if (table && useEntityIds) {\n response = transformResponseFields(\n response,\n table,\n expandValidationConfigs,\n );\n }\n\n // Fast path: skip validation\n if (skipValidation) {\n const result = extractRecords(response, singleMode);\n // Rename fields AFTER extraction (but before returning)\n if (result.data && fieldMapping && Object.keys(fieldMapping).length > 0) {\n if (result.error) {\n return { data: undefined, error: result.error } as Result<T>;\n }\n return {\n data: renameFieldsInResponse(result.data, fieldMapping) as T,\n error: undefined,\n };\n }\n return result as Result<T>;\n }\n\n // Validation path\n if (singleMode !== false) {\n const validation = await validateSingleResponse<any>(\n response,\n schema,\n selectedFields as any,\n expandValidationConfigs,\n singleMode,\n );\n\n if (!validation.valid) {\n return { data: undefined, error: validation.error };\n }\n\n // Rename fields AFTER validation completes\n if (fieldMapping && Object.keys(fieldMapping).length > 0) {\n return {\n data: renameFieldsInResponse(validation.data, fieldMapping) as T,\n error: undefined,\n };\n }\n\n return { data: validation.data as T, error: undefined };\n }\n\n const validation = await validateListResponse<any>(\n response,\n schema,\n selectedFields as any,\n expandValidationConfigs,\n );\n\n if (!validation.valid) {\n return { data: undefined, error: validation.error };\n }\n\n // Rename fields AFTER validation completes\n if (fieldMapping && Object.keys(fieldMapping).length > 0) {\n return {\n data: renameFieldsInResponse(validation.data, fieldMapping) as T,\n error: undefined,\n };\n }\n\n return { data: validation.data as T, error: undefined };\n}\n\n/**\n * Extracts records from response without validation.\n */\nfunction extractRecords<T>(\n response: any,\n singleMode: \"exact\" | \"maybe\" | false,\n): Result<T> {\n if (singleMode === false) {\n const records = response.value ?? [];\n return { data: records as T, error: undefined };\n }\n\n const records = response.value ?? [response];\n const count = Array.isArray(records) ? records.length : 1;\n\n if (count > 1) {\n return {\n data: undefined,\n error: new RecordCountMismatchError(\n singleMode === \"exact\" ? \"one\" : \"at-most-one\",\n count,\n ),\n };\n }\n\n if (count === 0) {\n if (singleMode === \"exact\") {\n return { data: undefined, error: new RecordCountMismatchError(\"one\", 0) };\n }\n return { data: null as T, error: undefined };\n }\n\n const record = Array.isArray(records) ? records[0] : records;\n return { data: record as T, error: undefined };\n}\n\n/**\n * Gets schema from a table occurrence, excluding container fields.\n * Container fields are never returned in regular responses (only via getSingleField).\n */\nexport function getSchemaFromTable(\n table: FMTable<any, any> | undefined,\n): Record<string, any> | undefined {\n if (!table) return undefined;\n const baseTableConfig = getBaseTableConfig(table);\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 return schema;\n}\n\n/**\n * Renames fields in response data according to the field mapping.\n * Used when select() is called with renamed fields (e.g., { userEmail: users.email }).\n */\nfunction renameFieldsInResponse(\n data: any,\n fieldMapping: Record<string, string>,\n): any {\n if (!data || typeof data !== \"object\") {\n return data;\n }\n\n // Handle array responses\n if (Array.isArray(data)) {\n return data.map((item) => renameFieldsInResponse(item, fieldMapping));\n }\n\n // Handle OData list response structure\n if (\"value\" in data && Array.isArray(data.value)) {\n return {\n ...data,\n value: data.value.map((item: any) =>\n renameFieldsInResponse(item, fieldMapping),\n ),\n };\n }\n\n // Handle single record\n const renamed: Record<string, any> = {};\n for (const [key, value] of Object.entries(data)) {\n // Check if this field should be renamed\n const outputKey = fieldMapping[key];\n if (outputKey) {\n renamed[outputKey] = value;\n } else {\n renamed[key] = value;\n }\n }\n return renamed;\n}\n\n/**\n * Processes query response with expand configs.\n * This is a convenience wrapper that builds validation configs from expand configs.\n */\nexport async function processQueryResponse<T>(\n response: any,\n config: {\n occurrence?: FMTable<any, any>;\n singleMode: \"exact\" | \"maybe\" | false;\n queryOptions: { select?: (keyof T)[] | string[] };\n expandConfigs: ExpandConfig[];\n skipValidation?: boolean;\n useEntityIds?: boolean;\n // Mapping from field names to output keys (for renamed fields in select)\n fieldMapping?: Record<string, string>;\n logger: InternalLogger;\n },\n): Promise<Result<any>> {\n const {\n occurrence,\n singleMode,\n queryOptions,\n expandConfigs,\n skipValidation,\n useEntityIds,\n fieldMapping,\n logger,\n } = config;\n\n const expandBuilder = new ExpandBuilder(useEntityIds ?? false, logger);\n const expandValidationConfigs =\n expandBuilder.buildValidationConfigs(expandConfigs);\n\n const selectedFields = queryOptions.select\n ? Array.isArray(queryOptions.select)\n ? queryOptions.select.map(String)\n : [String(queryOptions.select)]\n : undefined;\n\n // Process the response first\n let processedResponse = await processODataResponse(response, {\n table: occurrence,\n schema: getSchemaFromTable(occurrence),\n singleMode,\n selectedFields,\n expandValidationConfigs,\n skipValidation,\n useEntityIds,\n });\n\n // Rename fields if field mapping is provided (for renamed fields in select)\n if (\n processedResponse.data &&\n fieldMapping &&\n Object.keys(fieldMapping).length > 0\n ) {\n processedResponse = {\n ...processedResponse,\n data: renameFieldsInResponse(processedResponse.data, fieldMapping),\n };\n }\n\n return processedResponse;\n}\n"],"names":["validation","records"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { InternalLogger } from '../../logger.js';
1
2
  import { Column } from '../../orm/column.js';
2
3
  /**
3
4
  * Utility function for processing select() calls.
@@ -18,7 +19,7 @@ export declare function processSelectFields(...fields: (string | Column<any, any
18
19
  * @param tableName - Expected table name for validation
19
20
  * @returns Object with selectedFields array and fieldMapping for renamed fields
20
21
  */
21
- export declare function processSelectWithRenames<TTableName extends string>(fields: Record<string, Column<any, any, TTableName>>, tableName: string): {
22
+ export declare function processSelectWithRenames<TTableName extends string>(fields: Record<string, Column<any, any, TTableName>>, tableName: string, logger: InternalLogger): {
22
23
  selectedFields: string[];
23
24
  fieldMapping: Record<string, string>;
24
25
  };
@@ -1,5 +1,5 @@
1
1
  import { isColumn } from "../../orm/column.js";
2
- function processSelectWithRenames(fields, tableName) {
2
+ function processSelectWithRenames(fields, tableName, logger) {
3
3
  const selectedFields = [];
4
4
  const fieldMapping = {};
5
5
  for (const [outputKey, column] of Object.entries(fields)) {
@@ -9,7 +9,7 @@ function processSelectWithRenames(fields, tableName) {
9
9
  );
10
10
  }
11
11
  if (column.tableName !== tableName) {
12
- console.warn(
12
+ logger.warn(
13
13
  `Column ${column.toString()} is from table "${column.tableName}", but query is for table "${tableName}"`
14
14
  );
15
15
  }
@@ -1 +1 @@
1
- {"version":3,"file":"select-mixin.js","sources":["../../../../src/client/builders/select-mixin.ts"],"sourcesContent":["import { isColumn, type Column } from \"../../orm/column\";\n\n/**\n * Utility function for processing select() calls.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param fields - Field names or Column references\n * @returns Object with selectedFields array\n */\nexport function processSelectFields(\n ...fields: (string | Column<any, any, string>)[]\n): { selectedFields: string[] } {\n const fieldNames = fields.map((field) => {\n if (isColumn(field)) {\n return field.fieldName as string;\n }\n return String(field);\n });\n return { selectedFields: [...new Set(fieldNames)] };\n}\n\n/**\n * Processes select() calls with field renaming support.\n * Validates columns belong to the correct table and builds field mapping for renamed fields.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param fields - Object mapping output keys to column references\n * @param tableName - Expected table name for validation\n * @returns Object with selectedFields array and fieldMapping for renamed fields\n */\nexport function processSelectWithRenames<TTableName extends string>(\n fields: Record<string, Column<any, any, TTableName>>,\n tableName: string,\n): { selectedFields: string[]; fieldMapping: Record<string, string> } {\n const selectedFields: string[] = [];\n const fieldMapping: Record<string, string> = {};\n\n for (const [outputKey, column] of Object.entries(fields)) {\n if (!isColumn(column)) {\n throw new Error(\n `select() expects column references, but got: ${typeof column}`,\n );\n }\n\n // Warn (not throw) on table mismatch for consistency\n if (column.tableName !== tableName) {\n console.warn(\n `Column ${column.toString()} is from table \"${column.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n\n const fieldName = column.fieldName;\n selectedFields.push(fieldName);\n\n // Build mapping from field name to output key (only if renamed)\n if (fieldName !== outputKey) {\n fieldMapping[fieldName] = outputKey;\n }\n }\n\n return {\n selectedFields,\n fieldMapping: Object.keys(fieldMapping).length > 0 ? fieldMapping : {},\n };\n}\n\n/**\n * Legacy class name for backward compatibility.\n * @deprecated Use processSelectFields function instead\n */\nexport class SelectMixin {\n static processSelect = processSelectFields;\n}\n"],"names":[],"mappings":";AA8BgB,SAAA,yBACd,QACA,WACoE;AACpE,QAAM,iBAA2B,CAAC;AAClC,QAAM,eAAuC,CAAC;AAE9C,aAAW,CAAC,WAAW,MAAM,KAAK,OAAO,QAAQ,MAAM,GAAG;AACpD,QAAA,CAAC,SAAS,MAAM,GAAG;AACrB,YAAM,IAAI;AAAA,QACR,gDAAgD,OAAO,MAAM;AAAA,MAC/D;AAAA,IAAA;AAIE,QAAA,OAAO,cAAc,WAAW;AAC1B,cAAA;AAAA,QACN,UAAU,OAAO,UAAU,mBAAmB,OAAO,SAAS,8BAA8B,SAAS;AAAA,MACvG;AAAA,IAAA;AAGF,UAAM,YAAY,OAAO;AACzB,mBAAe,KAAK,SAAS;AAG7B,QAAI,cAAc,WAAW;AAC3B,mBAAa,SAAS,IAAI;AAAA,IAAA;AAAA,EAC5B;AAGK,SAAA;AAAA,IACL;AAAA,IACA,cAAc,OAAO,KAAK,YAAY,EAAE,SAAS,IAAI,eAAe,CAAA;AAAA,EACtE;AACF;"}
1
+ {"version":3,"file":"select-mixin.js","sources":["../../../../src/client/builders/select-mixin.ts"],"sourcesContent":["import { InternalLogger } from \"../../logger\";\nimport { isColumn, type Column } from \"../../orm/column\";\n\n/**\n * Utility function for processing select() calls.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param fields - Field names or Column references\n * @returns Object with selectedFields array\n */\nexport function processSelectFields(\n ...fields: (string | Column<any, any, string>)[]\n): { selectedFields: string[] } {\n const fieldNames = fields.map((field) => {\n if (isColumn(field)) {\n return field.fieldName as string;\n }\n return String(field);\n });\n return { selectedFields: [...new Set(fieldNames)] };\n}\n\n/**\n * Processes select() calls with field renaming support.\n * Validates columns belong to the correct table and builds field mapping for renamed fields.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param fields - Object mapping output keys to column references\n * @param tableName - Expected table name for validation\n * @returns Object with selectedFields array and fieldMapping for renamed fields\n */\nexport function processSelectWithRenames<TTableName extends string>(\n fields: Record<string, Column<any, any, TTableName>>,\n tableName: string,\n logger: InternalLogger,\n): { selectedFields: string[]; fieldMapping: Record<string, string> } {\n const selectedFields: string[] = [];\n const fieldMapping: Record<string, string> = {};\n\n for (const [outputKey, column] of Object.entries(fields)) {\n if (!isColumn(column)) {\n throw new Error(\n `select() expects column references, but got: ${typeof column}`,\n );\n }\n\n // Warn (not throw) on table mismatch for consistency\n if (column.tableName !== tableName) {\n logger.warn(\n `Column ${column.toString()} is from table \"${column.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n\n const fieldName = column.fieldName;\n selectedFields.push(fieldName);\n\n // Build mapping from field name to output key (only if renamed)\n if (fieldName !== outputKey) {\n fieldMapping[fieldName] = outputKey;\n }\n }\n\n return {\n selectedFields,\n fieldMapping: Object.keys(fieldMapping).length > 0 ? fieldMapping : {},\n };\n}\n\n/**\n * Legacy class name for backward compatibility.\n * @deprecated Use processSelectFields function instead\n */\nexport class SelectMixin {\n static processSelect = processSelectFields;\n}\n"],"names":[],"mappings":";AA+BgB,SAAA,yBACd,QACA,WACA,QACoE;AACpE,QAAM,iBAA2B,CAAC;AAClC,QAAM,eAAuC,CAAC;AAE9C,aAAW,CAAC,WAAW,MAAM,KAAK,OAAO,QAAQ,MAAM,GAAG;AACpD,QAAA,CAAC,SAAS,MAAM,GAAG;AACrB,YAAM,IAAI;AAAA,QACR,gDAAgD,OAAO,MAAM;AAAA,MAC/D;AAAA,IAAA;AAIE,QAAA,OAAO,cAAc,WAAW;AAC3B,aAAA;AAAA,QACL,UAAU,OAAO,UAAU,mBAAmB,OAAO,SAAS,8BAA8B,SAAS;AAAA,MACvG;AAAA,IAAA;AAGF,UAAM,YAAY,OAAO;AACzB,mBAAe,KAAK,SAAS;AAG7B,QAAI,cAAc,WAAW;AAC3B,mBAAa,SAAS,IAAI;AAAA,IAAA;AAAA,EAC5B;AAGK,SAAA;AAAA,IACL;AAAA,IACA,cAAc,OAAO,KAAK,YAAY,EAAE,SAAS,IAAI,eAAe,CAAA;AAAA,EACtE;AACF;"}
@@ -16,6 +16,7 @@ export declare class EntitySet<Occ extends FMTable<any, any>> {
16
16
  private navigateSourceTableName?;
17
17
  private navigateBasePath?;
18
18
  private databaseUseEntityIds;
19
+ private logger;
19
20
  constructor(config: {
20
21
  occurrence: Occ;
21
22
  databaseName: string;
@@ -29,7 +30,7 @@ export declare class EntitySet<Occ extends FMTable<any, any>> {
29
30
  database: Database;
30
31
  }): EntitySet<Occ>;
31
32
  list(): QueryBuilder<Occ, keyof InferSchemaOutputFromFMTable<Occ>, false, false, {}>;
32
- get(id: string | number): RecordBuilder<Occ, false, keyof InferSchemaOutputFromFMTable<Occ>, keyof InferSchemaOutputFromFMTable<Occ>, {}>;
33
+ get(id: string | number): RecordBuilder<Occ, false, undefined, keyof InferSchemaOutputFromFMTable<Occ>, {}>;
33
34
  insert(data: InsertDataFromFMTable<Occ>, options: {
34
35
  returnFullRecord: false;
35
36
  }): InsertBuilder<Occ, "minimal">;
@@ -7,6 +7,7 @@ import { InsertBuilder } from "./insert-builder.js";
7
7
  import { DeleteBuilder } from "./delete-builder.js";
8
8
  import { UpdateBuilder } from "./update-builder.js";
9
9
  import { getDefaultSelect, FMTable, getTableColumns, getTableName } from "../orm/table.js";
10
+ import { createLogger } from "../logger.js";
10
11
  class EntitySet {
11
12
  constructor(config) {
12
13
  __publicField(this, "occurrence");
@@ -20,12 +21,14 @@ class EntitySet {
20
21
  __publicField(this, "navigateBasePath");
21
22
  // Full base path for chained navigations
22
23
  __publicField(this, "databaseUseEntityIds");
23
- var _a;
24
+ __publicField(this, "logger");
25
+ var _a, _b, _c;
24
26
  this.occurrence = config.occurrence;
25
27
  this.databaseName = config.databaseName;
26
28
  this.context = config.context;
27
29
  this.database = config.database;
28
30
  this.databaseUseEntityIds = ((_a = config.database) == null ? void 0 : _a._useEntityIds) ?? false;
31
+ this.logger = ((_c = (_b = config.context) == null ? void 0 : _b._getLogger) == null ? void 0 : _c.call(_b)) ?? createLogger();
29
32
  }
30
33
  // Type-only method to help TypeScript infer the schema from table
31
34
  static create(config) {
@@ -167,7 +170,7 @@ class EntitySet {
167
170
  if (this.occurrence && FMTable.Symbol.NavigationPaths in this.occurrence) {
168
171
  const navigationPaths = this.occurrence[FMTable.Symbol.NavigationPaths];
169
172
  if (navigationPaths && !navigationPaths.includes(relationName)) {
170
- console.warn(
173
+ this.logger.warn(
171
174
  `Cannot navigate to "${relationName}". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(", ") : "none"}`
172
175
  );
173
176
  }
@@ -1 +1 @@
1
- {"version":3,"file":"entity-set.js","sources":["../../../src/client/entity-set.ts"],"sourcesContent":["import type { ExecutionContext, InferSchemaType } from \"../types\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { QueryBuilder } from \"./query/index\";\nimport { RecordBuilder } from \"./record-builder\";\nimport { InsertBuilder } from \"./insert-builder\";\nimport { DeleteBuilder } from \"./delete-builder\";\nimport { UpdateBuilder } from \"./update-builder\";\nimport { Database } from \"./database\";\nimport type {\n FMTable,\n InferSchemaOutputFromFMTable,\n InferInputSchemaFromFMTable,\n InsertDataFromFMTable,\n UpdateDataFromFMTable,\n ValidExpandTarget,\n ExtractTableName,\n FMTableWithColumns,\n InferFieldOutput,\n ColumnMap,\n} from \"../orm/table\";\nimport {\n FMTable as FMTableClass,\n getDefaultSelect,\n getNavigationPaths,\n getTableName,\n getTableColumns,\n getTableFields,\n} from \"../orm/table\";\nimport type { Column } from \"../orm/column\";\nimport type { FieldBuilder } from \"../orm/field-builders\";\n\n// Helper type to extract defaultSelect from an FMTable\n// Since TypeScript can't extract Symbol-indexed properties at the type level,\n// we simplify to return keyof InferSchemaFromFMTable<O> when O is an FMTable.\n// The actual defaultSelect logic is handled at runtime.\ntype ExtractDefaultSelect<O> =\n O extends FMTable<any, any> ? keyof InferSchemaOutputFromFMTable<O> : never;\n\n/**\n * Helper type to extract properly-typed columns from an FMTable.\n * This preserves the specific column types instead of widening to `any`.\n */\ntype ExtractColumnsFromOcc<T> =\n T extends FMTable<infer TFields, infer TName, any>\n ? TFields extends Record<string, FieldBuilder<any, any, any, any>>\n ? ColumnMap<TFields, TName>\n : never\n : never;\n\nexport class EntitySet<Occ extends FMTable<any, any>> {\n private occurrence: Occ;\n private databaseName: string;\n private context: ExecutionContext;\n private database: Database; // Database instance for accessing occurrences\n private isNavigateFromEntitySet?: boolean;\n private navigateRelation?: string;\n private navigateSourceTableName?: string;\n private navigateBasePath?: string; // Full base path for chained navigations\n private databaseUseEntityIds: boolean;\n\n constructor(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n database?: any;\n }) {\n this.occurrence = config.occurrence;\n this.databaseName = config.databaseName;\n this.context = config.context;\n this.database = config.database;\n // Get useEntityIds from database if available, otherwise default to false\n this.databaseUseEntityIds =\n (config.database as any)?._useEntityIds ?? false;\n }\n\n // Type-only method to help TypeScript infer the schema from table\n static create<Occ extends FMTable<any, any>>(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n database: Database;\n }): EntitySet<Occ> {\n return new EntitySet<Occ>({\n occurrence: config.occurrence,\n databaseName: config.databaseName,\n context: config.context,\n database: config.database,\n });\n }\n\n list(): QueryBuilder<\n Occ,\n keyof InferSchemaOutputFromFMTable<Occ>,\n false,\n false,\n {}\n > {\n const builder = new QueryBuilder<Occ>({\n occurrence: this.occurrence as Occ,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n\n // Apply defaultSelect if occurrence exists and select hasn't been called\n if (this.occurrence) {\n // FMTable - access via helper functions\n const defaultSelectValue = getDefaultSelect(this.occurrence);\n const tableSchema = (this.occurrence as any)[FMTableClass.Symbol.Schema];\n let schema: Record<string, StandardSchemaV1> | undefined;\n\n if (tableSchema) {\n // Extract schema from StandardSchemaV1\n const zodSchema = tableSchema[\"~standard\"]?.schema;\n if (\n zodSchema &&\n typeof zodSchema === \"object\" &&\n \"shape\" in zodSchema\n ) {\n schema = zodSchema.shape as Record<string, StandardSchemaV1>;\n }\n }\n\n if (defaultSelectValue === \"schema\") {\n // Use getTableColumns to get all columns and select them\n // This is equivalent to select(getTableColumns(occurrence))\n // Cast to the declared return type - runtime behavior handles the actual selection\n const allColumns = getTableColumns(\n this.occurrence as any,\n ) as ExtractColumnsFromOcc<Occ>;\n return builder.select(allColumns).top(1000) as QueryBuilder<\n Occ,\n keyof InferSchemaOutputFromFMTable<Occ>,\n false,\n false,\n {}\n >;\n } else if (typeof defaultSelectValue === \"object\") {\n // defaultSelectValue is a select object (Record<string, Column>)\n // Cast to the declared return type - runtime behavior handles the actual selection\n return builder\n .select(defaultSelectValue as ExtractColumnsFromOcc<Occ>)\n .top(1000) as QueryBuilder<\n Occ,\n keyof InferSchemaOutputFromFMTable<Occ>,\n false,\n false,\n {}\n >;\n }\n // If defaultSelect is \"all\", no changes needed (current behavior)\n }\n\n // Propagate navigation context if present\n if (\n this.isNavigateFromEntitySet &&\n this.navigateRelation &&\n this.navigateSourceTableName\n ) {\n (builder as any).navigation = {\n relation: this.navigateRelation,\n sourceTableName: this.navigateSourceTableName,\n basePath: this.navigateBasePath,\n // recordId is intentionally not set (undefined) to indicate navigation from EntitySet\n };\n }\n\n // Apply default pagination limit of 1000 records to prevent stack overflow\n // with large datasets. Users can override with .top() if needed.\n return builder.top(1000);\n }\n\n get(\n id: string | number,\n ): RecordBuilder<\n Occ,\n false,\n keyof InferSchemaOutputFromFMTable<Occ>,\n keyof InferSchemaOutputFromFMTable<Occ>,\n {}\n > {\n const builder = new RecordBuilder<Occ>({\n occurrence: this.occurrence,\n databaseName: this.databaseName,\n context: this.context,\n recordId: id,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n\n // Apply defaultSelect if occurrence exists\n if (this.occurrence) {\n // FMTable - access via helper functions\n const defaultSelectValue = getDefaultSelect(this.occurrence);\n const tableSchema = (this.occurrence as any)[FMTableClass.Symbol.Schema];\n let schema: Record<string, StandardSchemaV1> | undefined;\n\n if (tableSchema) {\n // Extract schema from StandardSchemaV1\n const zodSchema = tableSchema[\"~standard\"]?.schema;\n if (\n zodSchema &&\n typeof zodSchema === \"object\" &&\n \"shape\" in zodSchema\n ) {\n schema = zodSchema.shape as Record<string, StandardSchemaV1>;\n }\n }\n\n if (defaultSelectValue === \"schema\") {\n // Use getTableColumns to get all columns and select them\n // This is equivalent to select(getTableColumns(occurrence))\n // Use ExtractColumnsFromOcc to preserve the properly-typed column types\n const allColumns = getTableColumns(\n this.occurrence as any,\n ) as ExtractColumnsFromOcc<Occ>;\n const selectedBuilder = builder.select(allColumns);\n // Propagate navigation context if present\n if (\n this.isNavigateFromEntitySet &&\n this.navigateRelation &&\n this.navigateSourceTableName\n ) {\n (selectedBuilder as any).navigation = {\n relation: this.navigateRelation,\n sourceTableName: this.navigateSourceTableName,\n basePath: this.navigateBasePath,\n };\n }\n return selectedBuilder as any;\n } else if (\n typeof defaultSelectValue === \"object\" &&\n defaultSelectValue !== null &&\n !Array.isArray(defaultSelectValue)\n ) {\n // defaultSelectValue is a select object (Record<string, Column>)\n // Use it directly with select()\n // Use ExtractColumnsFromOcc to preserve the properly-typed column types\n const selectedBuilder = builder.select(\n defaultSelectValue as ExtractColumnsFromOcc<Occ>,\n );\n // Propagate navigation context if present\n if (\n this.isNavigateFromEntitySet &&\n this.navigateRelation &&\n this.navigateSourceTableName\n ) {\n (selectedBuilder as any).navigation = {\n relation: this.navigateRelation,\n sourceTableName: this.navigateSourceTableName,\n basePath: this.navigateBasePath,\n };\n }\n return selectedBuilder as any;\n }\n // If defaultSelect is \"all\", no changes needed (current behavior)\n }\n\n // Propagate navigation context if present\n if (\n this.isNavigateFromEntitySet &&\n this.navigateRelation &&\n this.navigateSourceTableName\n ) {\n (builder as any).navigation = {\n relation: this.navigateRelation,\n sourceTableName: this.navigateSourceTableName,\n basePath: this.navigateBasePath,\n };\n }\n return builder as any;\n }\n\n // Overload: when returnFullRecord is false\n insert(\n data: InsertDataFromFMTable<Occ>,\n options: { returnFullRecord: false },\n ): InsertBuilder<Occ, \"minimal\">;\n\n // Overload: when returnFullRecord is true or omitted (default)\n insert(\n data: InsertDataFromFMTable<Occ>,\n options?: { returnFullRecord?: true },\n ): InsertBuilder<Occ, \"representation\">;\n\n // Implementation\n insert(\n data: InsertDataFromFMTable<Occ>,\n options?: { returnFullRecord?: boolean },\n ): InsertBuilder<Occ, \"minimal\" | \"representation\"> {\n const returnPreference =\n options?.returnFullRecord === false ? \"minimal\" : \"representation\";\n\n return new InsertBuilder<Occ, typeof returnPreference>({\n occurrence: this.occurrence,\n databaseName: this.databaseName,\n context: this.context,\n data: data as any, // Input type is validated/transformed at runtime\n returnPreference: returnPreference as any,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n }\n\n // Overload: when returnFullRecord is explicitly true\n update(\n data: UpdateDataFromFMTable<Occ>,\n options: { returnFullRecord: true },\n ): UpdateBuilder<Occ, \"representation\">;\n\n // Overload: when returnFullRecord is false or omitted (default)\n update(\n data: UpdateDataFromFMTable<Occ>,\n options?: { returnFullRecord?: false },\n ): UpdateBuilder<Occ, \"minimal\">;\n\n // Implementation\n update(\n data: UpdateDataFromFMTable<Occ>,\n options?: { returnFullRecord?: boolean },\n ): UpdateBuilder<Occ, \"minimal\" | \"representation\"> {\n const returnPreference =\n options?.returnFullRecord === true ? \"representation\" : \"minimal\";\n\n return new UpdateBuilder<Occ, typeof returnPreference>({\n occurrence: this.occurrence,\n databaseName: this.databaseName,\n context: this.context,\n data: data as any, // Input type is validated/transformed at runtime\n returnPreference: returnPreference as any,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n }\n\n delete(): DeleteBuilder<Occ> {\n return new DeleteBuilder<Occ>({\n occurrence: this.occurrence,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n }) as any;\n }\n\n // Implementation\n navigate<TargetTable extends FMTable<any, any>>(\n targetTable: ValidExpandTarget<Occ, TargetTable>,\n ): EntitySet<TargetTable extends FMTable<any, any> ? TargetTable : never> {\n // Check if it's an FMTable object or a string\n let relationName: string;\n\n // FMTable object - extract name and validate\n relationName = getTableName(targetTable);\n\n // Runtime validation: Check if relation name is in navigationPaths\n if (\n this.occurrence &&\n FMTableClass.Symbol.NavigationPaths in this.occurrence\n ) {\n const navigationPaths = (this.occurrence as any)[\n FMTableClass.Symbol.NavigationPaths\n ] as readonly string[];\n if (navigationPaths && !navigationPaths.includes(relationName)) {\n console.warn(\n `Cannot navigate to \"${relationName}\". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(\", \") : \"none\"}`,\n );\n }\n }\n\n // Create EntitySet with target table\n const entitySet = new EntitySet<any>({\n occurrence: targetTable,\n databaseName: this.databaseName,\n context: this.context,\n database: this.database,\n });\n // Store the navigation info in the EntitySet\n (entitySet as any).isNavigateFromEntitySet = true;\n (entitySet as any).navigateRelation = relationName;\n\n // Build the full base path for chained navigations\n if (this.isNavigateFromEntitySet && this.navigateBasePath) {\n // Already have a base path from previous navigation - extend it with current relation\n (entitySet as any).navigateBasePath =\n `${this.navigateBasePath}/${this.navigateRelation}`;\n (entitySet as any).navigateSourceTableName = this.navigateSourceTableName;\n } else if (this.isNavigateFromEntitySet && this.navigateRelation) {\n // First chained navigation - create base path from source/relation\n (entitySet as any).navigateBasePath =\n `${this.navigateSourceTableName}/${this.navigateRelation}`;\n (entitySet as any).navigateSourceTableName = this.navigateSourceTableName;\n } else {\n // Initial navigation - source is just the table name\n (entitySet as any).navigateSourceTableName = getTableName(\n this.occurrence,\n );\n }\n return entitySet;\n }\n}\n"],"names":["FMTableClass"],"mappings":"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"}
1
+ {"version":3,"file":"entity-set.js","sources":["../../../src/client/entity-set.ts"],"sourcesContent":["import type { ExecutionContext, InferSchemaType } from \"../types\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { QueryBuilder } from \"./query/index\";\nimport { RecordBuilder } from \"./record-builder\";\nimport { InsertBuilder } from \"./insert-builder\";\nimport { DeleteBuilder } from \"./delete-builder\";\nimport { UpdateBuilder } from \"./update-builder\";\nimport { Database } from \"./database\";\nimport type {\n FMTable,\n InferSchemaOutputFromFMTable,\n InferInputSchemaFromFMTable,\n InsertDataFromFMTable,\n UpdateDataFromFMTable,\n ValidExpandTarget,\n ExtractTableName,\n FMTableWithColumns,\n InferFieldOutput,\n ColumnMap,\n} from \"../orm/table\";\nimport {\n FMTable as FMTableClass,\n getDefaultSelect,\n getNavigationPaths,\n getTableName,\n getTableColumns,\n getTableFields,\n} from \"../orm/table\";\nimport type { Column } from \"../orm/column\";\nimport type { FieldBuilder } from \"../orm/field-builders\";\nimport { createLogger, InternalLogger } from \"../logger\";\n\n// Helper type to extract defaultSelect from an FMTable\n// Since TypeScript can't extract Symbol-indexed properties at the type level,\n// we simplify to return keyof InferSchemaFromFMTable<O> when O is an FMTable.\n// The actual defaultSelect logic is handled at runtime.\ntype ExtractDefaultSelect<O> =\n O extends FMTable<any, any> ? keyof InferSchemaOutputFromFMTable<O> : never;\n\n/**\n * Helper type to extract properly-typed columns from an FMTable.\n * This preserves the specific column types instead of widening to `any`.\n */\ntype ExtractColumnsFromOcc<T> =\n T extends FMTable<infer TFields, infer TName, any>\n ? TFields extends Record<string, FieldBuilder<any, any, any, any>>\n ? ColumnMap<TFields, TName>\n : never\n : never;\n\nexport class EntitySet<Occ extends FMTable<any, any>> {\n private occurrence: Occ;\n private databaseName: string;\n private context: ExecutionContext;\n private database: Database; // Database instance for accessing occurrences\n private isNavigateFromEntitySet?: boolean;\n private navigateRelation?: string;\n private navigateSourceTableName?: string;\n private navigateBasePath?: string; // Full base path for chained navigations\n private databaseUseEntityIds: boolean;\n private logger: InternalLogger;\n\n constructor(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n database?: any;\n }) {\n this.occurrence = config.occurrence;\n this.databaseName = config.databaseName;\n this.context = config.context;\n this.database = config.database;\n // Get useEntityIds from database if available, otherwise default to false\n this.databaseUseEntityIds =\n (config.database as any)?._useEntityIds ?? false;\n this.logger = config.context?._getLogger?.() ?? createLogger();\n }\n\n // Type-only method to help TypeScript infer the schema from table\n static create<Occ extends FMTable<any, any>>(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n database: Database;\n }): EntitySet<Occ> {\n return new EntitySet<Occ>({\n occurrence: config.occurrence,\n databaseName: config.databaseName,\n context: config.context,\n database: config.database,\n });\n }\n\n list(): QueryBuilder<\n Occ,\n keyof InferSchemaOutputFromFMTable<Occ>,\n false,\n false,\n {}\n > {\n const builder = new QueryBuilder<Occ>({\n occurrence: this.occurrence as Occ,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n\n // Apply defaultSelect if occurrence exists and select hasn't been called\n if (this.occurrence) {\n // FMTable - access via helper functions\n const defaultSelectValue = getDefaultSelect(this.occurrence);\n const tableSchema = (this.occurrence as any)[FMTableClass.Symbol.Schema];\n let schema: Record<string, StandardSchemaV1> | undefined;\n\n if (tableSchema) {\n // Extract schema from StandardSchemaV1\n const zodSchema = tableSchema[\"~standard\"]?.schema;\n if (\n zodSchema &&\n typeof zodSchema === \"object\" &&\n \"shape\" in zodSchema\n ) {\n schema = zodSchema.shape as Record<string, StandardSchemaV1>;\n }\n }\n\n if (defaultSelectValue === \"schema\") {\n // Use getTableColumns to get all columns and select them\n // This is equivalent to select(getTableColumns(occurrence))\n // Cast to the declared return type - runtime behavior handles the actual selection\n const allColumns = getTableColumns(\n this.occurrence as any,\n ) as ExtractColumnsFromOcc<Occ>;\n return builder.select(allColumns).top(1000) as QueryBuilder<\n Occ,\n keyof InferSchemaOutputFromFMTable<Occ>,\n false,\n false,\n {}\n >;\n } else if (typeof defaultSelectValue === \"object\") {\n // defaultSelectValue is a select object (Record<string, Column>)\n // Cast to the declared return type - runtime behavior handles the actual selection\n return builder\n .select(defaultSelectValue as ExtractColumnsFromOcc<Occ>)\n .top(1000) as QueryBuilder<\n Occ,\n keyof InferSchemaOutputFromFMTable<Occ>,\n false,\n false,\n {}\n >;\n }\n // If defaultSelect is \"all\", no changes needed (current behavior)\n }\n\n // Propagate navigation context if present\n if (\n this.isNavigateFromEntitySet &&\n this.navigateRelation &&\n this.navigateSourceTableName\n ) {\n (builder as any).navigation = {\n relation: this.navigateRelation,\n sourceTableName: this.navigateSourceTableName,\n basePath: this.navigateBasePath,\n // recordId is intentionally not set (undefined) to indicate navigation from EntitySet\n };\n }\n\n // Apply default pagination limit of 1000 records to prevent stack overflow\n // with large datasets. Users can override with .top() if needed.\n return builder.top(1000);\n }\n\n get(\n id: string | number,\n ): RecordBuilder<\n Occ,\n false,\n undefined,\n keyof InferSchemaOutputFromFMTable<Occ>,\n {}\n > {\n const builder = new RecordBuilder<Occ>({\n occurrence: this.occurrence,\n databaseName: this.databaseName,\n context: this.context,\n recordId: id,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n\n // Apply defaultSelect if occurrence exists\n if (this.occurrence) {\n // FMTable - access via helper functions\n const defaultSelectValue = getDefaultSelect(this.occurrence);\n const tableSchema = (this.occurrence as any)[FMTableClass.Symbol.Schema];\n let schema: Record<string, StandardSchemaV1> | undefined;\n\n if (tableSchema) {\n // Extract schema from StandardSchemaV1\n const zodSchema = tableSchema[\"~standard\"]?.schema;\n if (\n zodSchema &&\n typeof zodSchema === \"object\" &&\n \"shape\" in zodSchema\n ) {\n schema = zodSchema.shape as Record<string, StandardSchemaV1>;\n }\n }\n\n if (defaultSelectValue === \"schema\") {\n // Use getTableColumns to get all columns and select them\n // This is equivalent to select(getTableColumns(occurrence))\n // Use ExtractColumnsFromOcc to preserve the properly-typed column types\n const allColumns = getTableColumns(\n this.occurrence as any,\n ) as ExtractColumnsFromOcc<Occ>;\n const selectedBuilder = builder.select(allColumns);\n // Propagate navigation context if present\n if (\n this.isNavigateFromEntitySet &&\n this.navigateRelation &&\n this.navigateSourceTableName\n ) {\n (selectedBuilder as any).navigation = {\n relation: this.navigateRelation,\n sourceTableName: this.navigateSourceTableName,\n basePath: this.navigateBasePath,\n };\n }\n return selectedBuilder as any;\n } else if (\n typeof defaultSelectValue === \"object\" &&\n defaultSelectValue !== null &&\n !Array.isArray(defaultSelectValue)\n ) {\n // defaultSelectValue is a select object (Record<string, Column>)\n // Use it directly with select()\n // Use ExtractColumnsFromOcc to preserve the properly-typed column types\n const selectedBuilder = builder.select(\n defaultSelectValue as ExtractColumnsFromOcc<Occ>,\n );\n // Propagate navigation context if present\n if (\n this.isNavigateFromEntitySet &&\n this.navigateRelation &&\n this.navigateSourceTableName\n ) {\n (selectedBuilder as any).navigation = {\n relation: this.navigateRelation,\n sourceTableName: this.navigateSourceTableName,\n basePath: this.navigateBasePath,\n };\n }\n return selectedBuilder as any;\n }\n // If defaultSelect is \"all\", no changes needed (current behavior)\n }\n\n // Propagate navigation context if present\n if (\n this.isNavigateFromEntitySet &&\n this.navigateRelation &&\n this.navigateSourceTableName\n ) {\n (builder as any).navigation = {\n relation: this.navigateRelation,\n sourceTableName: this.navigateSourceTableName,\n basePath: this.navigateBasePath,\n };\n }\n return builder as any;\n }\n\n // Overload: when returnFullRecord is false\n insert(\n data: InsertDataFromFMTable<Occ>,\n options: { returnFullRecord: false },\n ): InsertBuilder<Occ, \"minimal\">;\n\n // Overload: when returnFullRecord is true or omitted (default)\n insert(\n data: InsertDataFromFMTable<Occ>,\n options?: { returnFullRecord?: true },\n ): InsertBuilder<Occ, \"representation\">;\n\n // Implementation\n insert(\n data: InsertDataFromFMTable<Occ>,\n options?: { returnFullRecord?: boolean },\n ): InsertBuilder<Occ, \"minimal\" | \"representation\"> {\n const returnPreference =\n options?.returnFullRecord === false ? \"minimal\" : \"representation\";\n\n return new InsertBuilder<Occ, typeof returnPreference>({\n occurrence: this.occurrence,\n databaseName: this.databaseName,\n context: this.context,\n data: data as any, // Input type is validated/transformed at runtime\n returnPreference: returnPreference as any,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n }\n\n // Overload: when returnFullRecord is explicitly true\n update(\n data: UpdateDataFromFMTable<Occ>,\n options: { returnFullRecord: true },\n ): UpdateBuilder<Occ, \"representation\">;\n\n // Overload: when returnFullRecord is false or omitted (default)\n update(\n data: UpdateDataFromFMTable<Occ>,\n options?: { returnFullRecord?: false },\n ): UpdateBuilder<Occ, \"minimal\">;\n\n // Implementation\n update(\n data: UpdateDataFromFMTable<Occ>,\n options?: { returnFullRecord?: boolean },\n ): UpdateBuilder<Occ, \"minimal\" | \"representation\"> {\n const returnPreference =\n options?.returnFullRecord === true ? \"representation\" : \"minimal\";\n\n return new UpdateBuilder<Occ, typeof returnPreference>({\n occurrence: this.occurrence,\n databaseName: this.databaseName,\n context: this.context,\n data: data as any, // Input type is validated/transformed at runtime\n returnPreference: returnPreference as any,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n }\n\n delete(): DeleteBuilder<Occ> {\n return new DeleteBuilder<Occ>({\n occurrence: this.occurrence,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n }) as any;\n }\n\n // Implementation\n navigate<TargetTable extends FMTable<any, any>>(\n targetTable: ValidExpandTarget<Occ, TargetTable>,\n ): EntitySet<TargetTable extends FMTable<any, any> ? TargetTable : never> {\n // Check if it's an FMTable object or a string\n let relationName: string;\n\n // FMTable object - extract name and validate\n relationName = getTableName(targetTable);\n\n // Runtime validation: Check if relation name is in navigationPaths\n if (\n this.occurrence &&\n FMTableClass.Symbol.NavigationPaths in this.occurrence\n ) {\n const navigationPaths = (this.occurrence as any)[\n FMTableClass.Symbol.NavigationPaths\n ] as readonly string[];\n if (navigationPaths && !navigationPaths.includes(relationName)) {\n this.logger.warn(\n `Cannot navigate to \"${relationName}\". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(\", \") : \"none\"}`,\n );\n }\n }\n\n // Create EntitySet with target table\n const entitySet = new EntitySet<any>({\n occurrence: targetTable,\n databaseName: this.databaseName,\n context: this.context,\n database: this.database,\n });\n // Store the navigation info in the EntitySet\n (entitySet as any).isNavigateFromEntitySet = true;\n (entitySet as any).navigateRelation = relationName;\n\n // Build the full base path for chained navigations\n if (this.isNavigateFromEntitySet && this.navigateBasePath) {\n // Already have a base path from previous navigation - extend it with current relation\n (entitySet as any).navigateBasePath =\n `${this.navigateBasePath}/${this.navigateRelation}`;\n (entitySet as any).navigateSourceTableName = this.navigateSourceTableName;\n } else if (this.isNavigateFromEntitySet && this.navigateRelation) {\n // First chained navigation - create base path from source/relation\n (entitySet as any).navigateBasePath =\n `${this.navigateSourceTableName}/${this.navigateRelation}`;\n (entitySet as any).navigateSourceTableName = this.navigateSourceTableName;\n } else {\n // Initial navigation - source is just the table name\n (entitySet as any).navigateSourceTableName = getTableName(\n this.occurrence,\n );\n }\n return entitySet;\n }\n}\n"],"names":["FMTableClass"],"mappings":"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"}
@@ -1,15 +1,18 @@
1
1
  import { FFetchOptions } from '@fetchkit/ffetch';
2
2
  import { Auth, ExecutionContext, Result } from '../types.js';
3
3
  import { Database } from './database.js';
4
+ import { Logger, InternalLogger } from '../logger.js';
4
5
  export declare class FMServerConnection implements ExecutionContext {
5
6
  private fetchClient;
6
7
  private serverUrl;
7
8
  private auth;
8
9
  private useEntityIds;
10
+ private logger;
9
11
  constructor(config: {
10
12
  serverUrl: string;
11
13
  auth: Auth;
12
14
  fetchClientOptions?: FFetchOptions;
15
+ logger?: Logger;
13
16
  });
14
17
  /**
15
18
  * @internal
@@ -26,6 +29,11 @@ export declare class FMServerConnection implements ExecutionContext {
26
29
  * Gets the base URL for OData requests
27
30
  */
28
31
  _getBaseUrl(): string;
32
+ /**
33
+ * @internal
34
+ * Gets the logger instance
35
+ */
36
+ _getLogger(): InternalLogger;
29
37
  /**
30
38
  * @internal
31
39
  */
@@ -7,12 +7,15 @@ import { SchemaLockedError, ODataError, HTTPError, ResponseParseError } from "..
7
7
  import { Database } from "./database.js";
8
8
  import { safeJsonParse } from "./sanitize-json.js";
9
9
  import { get } from "es-toolkit/compat";
10
+ import { createLogger } from "../logger.js";
10
11
  class FMServerConnection {
11
12
  constructor(config) {
12
13
  __publicField(this, "fetchClient");
13
14
  __publicField(this, "serverUrl");
14
15
  __publicField(this, "auth");
15
16
  __publicField(this, "useEntityIds", false);
17
+ __publicField(this, "logger");
18
+ this.logger = createLogger(config.logger);
16
19
  this.fetchClient = createClient({
17
20
  retries: 0,
18
21
  ...config.fetchClientOptions
@@ -46,11 +49,19 @@ class FMServerConnection {
46
49
  _getBaseUrl() {
47
50
  return `${this.serverUrl}${"apiKey" in this.auth ? `/otto` : ""}/fmi/odata/v4`;
48
51
  }
52
+ /**
53
+ * @internal
54
+ * Gets the logger instance
55
+ */
56
+ _getLogger() {
57
+ return this.logger;
58
+ }
49
59
  /**
50
60
  * @internal
51
61
  */
52
62
  async _makeRequest(url, options) {
53
63
  var _a, _b, _c, _d, _e, _f;
64
+ const logger = this._getLogger();
54
65
  const baseUrl = `${this.serverUrl}${"apiKey" in this.auth ? `/otto` : ""}/fmi/odata/v4`;
55
66
  const fullUrl = baseUrl + url;
56
67
  const useEntityIds = (options == null ? void 0 : options.useEntityIds) ?? this.useEntityIds;
@@ -62,6 +73,8 @@ class FMServerConnection {
62
73
  ...useEntityIds ? { Prefer: "fmodata.entity-ids" } : {},
63
74
  ...(options == null ? void 0 : options.headers) || {}
64
75
  };
76
+ const { Authorization, ...loggableHeaders } = headers;
77
+ logger.debug("Request headers:", loggableHeaders);
65
78
  const fetchHandler = options == null ? void 0 : options.fetchHandler;
66
79
  const {
67
80
  headers: _headers,
@@ -75,6 +88,7 @@ class FMServerConnection {
75
88
  headers
76
89
  };
77
90
  const resp = await clientToUse(fullUrl, finalOptions);
91
+ logger.debug(`${finalOptions.method ?? "GET"} ${resp.status} ${fullUrl}`);
78
92
  if (!resp.ok) {
79
93
  let errorBody;
80
94
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"filemaker-odata.js","sources":["../../../src/client/filemaker-odata.ts"],"sourcesContent":["import createClient, {\n FFetchOptions,\n TimeoutError,\n AbortError,\n NetworkError,\n RetryLimitError,\n CircuitOpenError,\n} from \"@fetchkit/ffetch\";\nimport type { Auth, ExecutionContext, Result } from \"../types\";\nimport { getAcceptHeader } from \"../types\";\nimport {\n HTTPError,\n ODataError,\n SchemaLockedError,\n ResponseParseError,\n} from \"../errors\";\nimport { Database } from \"./database\";\nimport { safeJsonParse } from \"./sanitize-json\";\nimport { get } from \"es-toolkit/compat\";\n\nexport class FMServerConnection implements ExecutionContext {\n private fetchClient: ReturnType<typeof createClient>;\n private serverUrl: string;\n private auth: Auth;\n private useEntityIds: boolean = false;\n constructor(config: {\n serverUrl: string;\n auth: Auth;\n fetchClientOptions?: FFetchOptions;\n }) {\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(/\\/+$/, \"\");\n this.serverUrl = url.toString().replace(/\\/+$/, \"\");\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 * 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 */\n async _makeRequest<T>(\n url: string,\n options?: RequestInit & FFetchOptions & { useEntityIds?: boolean },\n ): Promise<Result<T>> {\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\n // Get includeODataAnnotations from options (it's passed through from execute options)\n const includeODataAnnotations = (options as any)?.includeODataAnnotations;\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 ...(useEntityIds ? { Prefer: \"fmodata.entity-ids\" } : {}),\n ...(options?.headers || {}),\n };\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 {\n headers: _headers,\n fetchHandler: _fetchHandler,\n ...restOptions\n } = options || {};\n\n // If fetchHandler is provided, create a temporary client with it\n // Otherwise use the existing client\n const clientToUse = fetchHandler\n ? createClient({ retries: 0, fetchHandler })\n : this.fetchClient;\n\n try {\n const finalOptions = {\n ...restOptions,\n headers,\n };\n\n const resp = await clientToUse(fullUrl, finalOptions);\n\n // Handle HTTP errors\n if (!resp.ok) {\n // Try to parse error body if it's JSON\n let errorBody:\n | { error?: { code?: string | number; message?: string } }\n | 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(\n fullUrl,\n errorMessage,\n errorBody.error,\n ),\n };\n }\n\n return {\n data: undefined,\n error: new ODataError(\n fullUrl,\n errorMessage,\n String(errorCode),\n errorBody.error,\n ),\n };\n }\n\n return {\n data: undefined,\n error: new HTTPError(\n fullUrl,\n resp.status,\n resp.statusText,\n errorBody,\n ),\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: 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 =\n 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<\n T & { error?: { code?: string | number; message?: string } }\n >(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(\n data,\n \"error.message\",\n \"Unknown OData error\",\n );\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(\n fullUrl,\n errorMessage,\n String(errorCode),\n data.error,\n ),\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(\n name: string,\n config?: {\n useEntityIds?: boolean;\n },\n ): Database {\n return new Database(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 }>(\"/\");\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 FFetchOptions,\n TimeoutError,\n AbortError,\n NetworkError,\n RetryLimitError,\n CircuitOpenError,\n} from \"@fetchkit/ffetch\";\nimport type { Auth, ExecutionContext, Result } from \"../types\";\nimport { getAcceptHeader } from \"../types\";\nimport {\n HTTPError,\n ODataError,\n SchemaLockedError,\n ResponseParseError,\n} from \"../errors\";\nimport { Database } from \"./database\";\nimport { safeJsonParse } from \"./sanitize-json\";\nimport { get } from \"es-toolkit/compat\";\nimport { createLogger, type Logger, type InternalLogger } from \"../logger\";\n\nexport class FMServerConnection implements ExecutionContext {\n private fetchClient: ReturnType<typeof createClient>;\n private serverUrl: string;\n private auth: Auth;\n private useEntityIds: boolean = false;\n private 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(/\\/+$/, \"\");\n this.serverUrl = url.toString().replace(/\\/+$/, \"\");\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 * 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 & FFetchOptions & { useEntityIds?: boolean },\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\n // Get includeODataAnnotations from options (it's passed through from execute options)\n const includeODataAnnotations = (options as any)?.includeODataAnnotations;\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 ...(useEntityIds ? { Prefer: \"fmodata.entity-ids\" } : {}),\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 {\n headers: _headers,\n fetchHandler: _fetchHandler,\n ...restOptions\n } = options || {};\n\n // If fetchHandler is provided, create a temporary client with it\n // Otherwise use the existing client\n const clientToUse = fetchHandler\n ? createClient({ retries: 0, fetchHandler })\n : 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:\n | { error?: { code?: string | number; message?: string } }\n | 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(\n fullUrl,\n errorMessage,\n errorBody.error,\n ),\n };\n }\n\n return {\n data: undefined,\n error: new ODataError(\n fullUrl,\n errorMessage,\n String(errorCode),\n errorBody.error,\n ),\n };\n }\n\n return {\n data: undefined,\n error: new HTTPError(\n fullUrl,\n resp.status,\n resp.statusText,\n errorBody,\n ),\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: 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 =\n 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<\n T & { error?: { code?: string | number; message?: string } }\n >(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(\n data,\n \"error.message\",\n \"Unknown OData error\",\n );\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(\n fullUrl,\n errorMessage,\n String(errorCode),\n data.error,\n ),\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(\n name: string,\n config?: {\n useEntityIds?: boolean;\n },\n ): Database {\n return new Database(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 }>(\"/\");\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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"}
@@ -19,6 +19,7 @@ export declare class QueryBuilder<Occ extends FMTable<any, any>, Selected extend
19
19
  private expandBuilder;
20
20
  private urlBuilder;
21
21
  private fieldMapping?;
22
+ private logger;
22
23
  constructor(config: {
23
24
  occurrence: Occ;
24
25
  databaseName: string;