@proofkit/fmodata 0.1.0-alpha.18 → 0.1.0-alpha.19

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.
@@ -32,13 +32,14 @@ class ExpandBuilder {
32
32
  targetSchema = schema;
33
33
  }
34
34
  const selectedFields = ((_a = config.options) == null ? void 0 : _a.select) ? Array.isArray(config.options.select) ? config.options.select.map(String) : [String(config.options.select)] : void 0;
35
+ const nestedExpands = config.nestedExpandConfigs ? this.buildValidationConfigs(config.nestedExpandConfigs) : void 0;
35
36
  return {
36
37
  relation: config.relation,
37
38
  targetSchema,
38
39
  targetTable,
39
40
  table: targetTable,
40
41
  selectedFields,
41
- nestedExpands: void 0
42
+ nestedExpands
42
43
  };
43
44
  });
44
45
  }
@@ -53,7 +54,6 @@ class ExpandBuilder {
53
54
  * @returns ExpandConfig to add to the builder's expandConfigs array
54
55
  */
55
56
  processExpand(targetTable, sourceTable, callback, builderFactory) {
56
- var _a;
57
57
  const relationName = getTableName(targetTable);
58
58
  if (sourceTable) {
59
59
  const navigationPaths = getNavigationPaths(sourceTable);
@@ -75,10 +75,9 @@ class ExpandBuilder {
75
75
  expandOptions.select = defaultFields;
76
76
  }
77
77
  }
78
- if (((_a = configuredBuilder.expandConfigs) == null ? void 0 : _a.length) > 0) {
79
- const nestedExpandString = this.buildExpandString(
80
- configuredBuilder.expandConfigs
81
- );
78
+ const nestedExpandConfigs = configuredBuilder.expandConfigs;
79
+ if ((nestedExpandConfigs == null ? void 0 : nestedExpandConfigs.length) > 0) {
80
+ const nestedExpandString = this.buildExpandString(nestedExpandConfigs);
82
81
  if (nestedExpandString) {
83
82
  expandOptions.expand = nestedExpandString;
84
83
  }
@@ -86,7 +85,8 @@ class ExpandBuilder {
86
85
  return {
87
86
  relation: relationName,
88
87
  options: expandOptions,
89
- targetTable
88
+ targetTable,
89
+ nestedExpandConfigs: (nestedExpandConfigs == null ? void 0 : nestedExpandConfigs.length) > 0 ? nestedExpandConfigs : void 0
90
90
  };
91
91
  } else {
92
92
  const defaultFields = getDefaultSelectFields(targetTable);
@@ -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\";\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
+ {"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 // Recursively build validation configs for nested expands\n const nestedExpands = config.nestedExpandConfigs\n ? this.buildValidationConfigs(config.nestedExpandConfigs)\n : undefined;\n\n return {\n relation: config.relation,\n targetSchema,\n targetTable,\n table: targetTable,\n selectedFields,\n nestedExpands,\n };\n });\n }\n\n /**\n * Process an expand() call and return the expand config.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param targetTable - The target table to expand to\n * @param sourceTable - The source table (for validation)\n * @param callback - Optional callback to configure the expand query\n * @param builderFactory - Function that creates a QueryBuilder for the target table\n * @returns ExpandConfig to add to the builder's expandConfigs array\n */\n 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 const nestedExpandConfigs = (configuredBuilder as any).expandConfigs;\n if (nestedExpandConfigs?.length > 0) {\n // Build nested expand string from the configured builder's expand configs\n const nestedExpandString = this.buildExpandString(nestedExpandConfigs);\n if (nestedExpandString) {\n // Add nested expand to options\n expandOptions.expand = nestedExpandString as any;\n }\n }\n\n return {\n relation: relationName,\n options: expandOptions,\n targetTable,\n nestedExpandConfigs: nestedExpandConfigs?.length > 0 ? nestedExpandConfigs : undefined,\n };\n } 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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"}
@@ -8,6 +8,7 @@ export type ExpandConfig = {
8
8
  relation: string;
9
9
  options?: Partial<QueryOptions<any>>;
10
10
  targetTable?: FMTable<any, any>;
11
+ nestedExpandConfigs?: ExpandConfig[];
11
12
  };
12
13
  /**
13
14
  * Type to represent expanded relations in return types
@@ -102,10 +102,11 @@ export declare class QueryBuilder<Occ extends FMTable<any, any>, Selected extend
102
102
  ]): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands>;
103
103
  top(count: number): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands>;
104
104
  skip(count: number): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands>;
105
- expand<TargetTable extends FMTable<any, any>>(targetTable: ValidExpandTarget<Occ, TargetTable>, callback?: (builder: QueryBuilder<TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false>) => QueryBuilder<TargetTable, any, any, any, any>): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands & {
105
+ expand<TargetTable extends FMTable<any, any>, TSelected extends keyof InferSchemaOutputFromFMTable<TargetTable> | Record<string, Column<any, any, ExtractTableName<TargetTable>>> = keyof InferSchemaOutputFromFMTable<TargetTable>, TNestedExpands extends ExpandedRelations = {}>(targetTable: ValidExpandTarget<Occ, TargetTable>, callback?: (builder: QueryBuilder<TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false, {}>) => QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands>): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands & {
106
106
  [K in ExtractTableName<TargetTable>]: {
107
107
  schema: InferSchemaOutputFromFMTable<TargetTable>;
108
- selected: keyof InferSchemaOutputFromFMTable<TargetTable>;
108
+ selected: TSelected;
109
+ nested: TNestedExpands;
109
110
  };
110
111
  }>;
111
112
  single(): QueryBuilder<Occ, Selected, "exact", IsCount, Expands>;
@@ -1 +1 @@
1
- {"version":3,"file":"query-builder.js","sources":["../../../../src/client/query/query-builder.ts"],"sourcesContent":["import { QueryOptions } from \"odata-query\";\nimport buildQuery from \"odata-query\";\nimport type {\n ExecutionContext,\n ExecutableBuilder,\n Result,\n ExecuteOptions,\n ConditionallyWithODataAnnotations,\n ExtractSchemaFromOccurrence,\n ExecuteMethodOptions,\n} from \"../../types\";\nimport { RecordCountMismatchError } from \"../../errors\";\nimport { type FFetchOptions } from \"@fetchkit/ffetch\";\nimport {\n transformFieldNamesArray,\n transformOrderByField,\n} from \"../../transform\";\nimport { safeJsonParse } from \"../sanitize-json\";\nimport { parseErrorResponse } from \"../error-parser\";\nimport { isColumn, type Column } from \"../../orm/column\";\nimport {\n FilterExpression,\n OrderByExpression,\n isOrderByExpression,\n} from \"../../orm/operators\";\nimport {\n FMTable,\n type InferSchemaOutputFromFMTable,\n type ValidExpandTarget,\n type ExtractTableName,\n type ValidateNoContainerFields,\n getTableName,\n} from \"../../orm/table\";\nimport {\n ExpandBuilder,\n type ExpandConfig,\n type ExpandedRelations,\n resolveTableId,\n mergeExecuteOptions,\n formatSelectFields,\n processQueryResponse,\n processSelectWithRenames,\n buildSelectExpandQueryString,\n createODataRequest,\n} from \"../builders/index\";\nimport { QueryUrlBuilder, type NavigationConfig } from \"./url-builder\";\nimport type { TypeSafeOrderBy, QueryReturnType } from \"./types\";\nimport { createLogger, InternalLogger } from \"../../logger\";\n\n// Re-export QueryReturnType for backward compatibility\nexport type { QueryReturnType };\n\n/**\n * Default maximum number of records to return in a list query.\n * This prevents stack overflow issues with large datasets while still\n * allowing substantial data retrieval. Users can override with .top().\n */\nconst DEFAULT_TOP = 1000;\n\nexport type { TypeSafeOrderBy, ExpandedRelations };\n\nexport class QueryBuilder<\n Occ extends FMTable<any, any>,\n Selected extends\n | keyof InferSchemaOutputFromFMTable<Occ>\n | Record<\n string,\n Column<any, any, ExtractTableName<Occ>>\n > = keyof InferSchemaOutputFromFMTable<Occ>,\n SingleMode extends \"exact\" | \"maybe\" | false = false,\n IsCount extends boolean = false,\n Expands extends ExpandedRelations = {},\n> implements\n ExecutableBuilder<\n QueryReturnType<\n InferSchemaOutputFromFMTable<Occ>,\n Selected,\n SingleMode,\n IsCount,\n Expands\n >\n >\n{\n private queryOptions: Partial<\n QueryOptions<InferSchemaOutputFromFMTable<Occ>>\n > = {};\n private expandConfigs: ExpandConfig[] = [];\n private singleMode: SingleMode = false as SingleMode;\n private isCountMode = false as IsCount;\n private occurrence: Occ;\n private databaseName: string;\n private context: ExecutionContext;\n private navigation?: NavigationConfig;\n private databaseUseEntityIds: boolean;\n private expandBuilder: ExpandBuilder;\n private urlBuilder: QueryUrlBuilder;\n // Mapping from field names to output keys (for renamed fields in select)\n private fieldMapping?: Record<string, string>;\n private logger: InternalLogger;\n\n constructor(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n databaseUseEntityIds?: boolean;\n }) {\n this.occurrence = config.occurrence;\n this.databaseName = config.databaseName;\n this.context = config.context;\n this.logger = config.context?._getLogger?.() ?? createLogger();\n this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;\n this.expandBuilder = new ExpandBuilder(\n this.databaseUseEntityIds,\n this.logger,\n );\n this.urlBuilder = new QueryUrlBuilder(\n this.databaseName,\n this.occurrence,\n this.context,\n );\n }\n\n /**\n * Helper to merge database-level useEntityIds with per-request options\n */\n private mergeExecuteOptions(\n options?: RequestInit & FFetchOptions & ExecuteOptions,\n ): RequestInit & FFetchOptions & { useEntityIds?: boolean } {\n return mergeExecuteOptions(options, this.databaseUseEntityIds);\n }\n\n /**\n * Gets the FMTable instance\n */\n private getTable(): FMTable<any, any> | undefined {\n return this.occurrence;\n }\n\n /**\n * Gets the table ID (FMTID) if using entity IDs, otherwise returns the table name\n * @param useEntityIds - Optional override for entity ID usage\n */\n private getTableIdOrName(useEntityIds?: boolean): string {\n return resolveTableId(\n this.occurrence,\n getTableName(this.occurrence),\n this.context,\n useEntityIds,\n );\n }\n\n /**\n * Creates a new QueryBuilder with modified configuration.\n * Used by single(), maybeSingle(), count(), and select() to create new instances.\n */\n private cloneWithChanges<\n NewSelected extends\n | keyof InferSchemaOutputFromFMTable<Occ>\n | Record<string, Column<any, any, ExtractTableName<Occ>>> = Selected,\n NewSingle extends \"exact\" | \"maybe\" | false = SingleMode,\n NewCount extends boolean = IsCount,\n >(changes: {\n selectedFields?: NewSelected;\n singleMode?: NewSingle;\n isCountMode?: NewCount;\n queryOptions?: Partial<QueryOptions<InferSchemaOutputFromFMTable<Occ>>>;\n fieldMapping?: Record<string, string>;\n }): QueryBuilder<Occ, NewSelected, NewSingle, NewCount, Expands> {\n const newBuilder = new QueryBuilder<\n Occ,\n NewSelected,\n NewSingle,\n NewCount,\n Expands\n >({\n occurrence: this.occurrence,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n newBuilder.queryOptions = {\n ...this.queryOptions,\n ...changes.queryOptions,\n };\n newBuilder.expandConfigs = [...this.expandConfigs];\n newBuilder.singleMode = (changes.singleMode ?? this.singleMode) as any;\n newBuilder.isCountMode = (changes.isCountMode ?? this.isCountMode) as any;\n newBuilder.fieldMapping = changes.fieldMapping ?? this.fieldMapping;\n // Copy navigation metadata\n newBuilder.navigation = this.navigation;\n newBuilder.urlBuilder = new QueryUrlBuilder(\n this.databaseName,\n this.occurrence,\n this.context,\n );\n return newBuilder;\n }\n\n /**\n * Select fields using column references.\n * Allows renaming fields by using different keys in the object.\n * Container fields cannot be selected and will cause a type error.\n *\n * @example\n * db.from(users).list().select({\n * name: users.name,\n * userEmail: users.email // renamed!\n * })\n *\n * @param fields - Object mapping output keys to column references (container fields excluded)\n * @returns QueryBuilder with updated selected fields\n */\n select<\n TSelect extends Record<\n string,\n Column<any, any, ExtractTableName<Occ>, false>\n >,\n >(fields: TSelect): QueryBuilder<Occ, TSelect, SingleMode, IsCount, Expands> {\n const tableName = getTableName(this.occurrence);\n const { selectedFields, fieldMapping } = processSelectWithRenames(\n fields,\n tableName,\n this.logger,\n );\n\n return this.cloneWithChanges({\n selectedFields: fields as any,\n queryOptions: {\n select: selectedFields,\n },\n fieldMapping:\n Object.keys(fieldMapping).length > 0 ? fieldMapping : undefined,\n });\n }\n\n /**\n * Filter results using operator expressions (new ORM-style API).\n * Supports eq, gt, lt, and, or, etc. operators with Column references.\n * Also supports raw OData filter strings as an escape hatch.\n *\n * @example\n * .where(eq(users.hobby, \"reading\"))\n * .where(and(eq(users.active, true), gt(users.age, 18)))\n * .where(\"status eq 'active'\") // Raw OData string escape hatch\n */\n where(\n expression: FilterExpression | string,\n ): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands> {\n // Handle raw string filters (escape hatch)\n if (typeof expression === \"string\") {\n this.queryOptions.filter = expression;\n return this;\n }\n // Convert FilterExpression to OData filter string\n const filterString = expression.toODataFilter(this.databaseUseEntityIds);\n this.queryOptions.filter = filterString;\n return this;\n }\n\n /**\n * Specify the sort order for query results.\n *\n * @example Single field (ascending by default)\n * ```ts\n * .orderBy(\"name\")\n * .orderBy(users.name) // Column reference\n * .orderBy(asc(users.name)) // Explicit ascending\n * ```\n *\n * @example Single field with explicit direction\n * ```ts\n * .orderBy([\"name\", \"desc\"])\n * .orderBy([users.name, \"desc\"]) // Column reference\n * .orderBy(desc(users.name)) // Explicit descending\n * ```\n *\n * @example Multiple fields with directions\n * ```ts\n * .orderBy([[\"name\", \"asc\"], [\"createdAt\", \"desc\"]])\n * .orderBy([[users.name, \"asc\"], [users.createdAt, \"desc\"]]) // Column references\n * .orderBy(users.name, desc(users.age)) // Variadic with helpers\n * ```\n */\n orderBy(\n ...orderByArgs:\n | [\n | TypeSafeOrderBy<InferSchemaOutputFromFMTable<Occ>>\n | Column<any, any, ExtractTableName<Occ>>\n | OrderByExpression<ExtractTableName<Occ>>,\n ]\n | [\n Column<any, any, ExtractTableName<Occ>>,\n ...Array<\n | Column<any, any, ExtractTableName<Occ>>\n | OrderByExpression<ExtractTableName<Occ>>\n >,\n ]\n ): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands> {\n const tableName = getTableName(this.occurrence);\n\n // Handle variadic arguments (multiple fields)\n if (orderByArgs.length > 1) {\n const orderByParts = orderByArgs.map((arg) => {\n if (isOrderByExpression(arg)) {\n // Validate table match\n if (arg.column.tableName !== tableName) {\n this.logger.warn(\n `Column ${arg.column.toString()} is from table \"${arg.column.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n const fieldName = arg.column.fieldName;\n const transformedField = this.occurrence\n ? transformOrderByField(fieldName, this.occurrence)\n : fieldName;\n return `${transformedField} ${arg.direction}`;\n } else if (isColumn(arg)) {\n // Validate table match\n if (arg.tableName !== tableName) {\n this.logger.warn(\n `Column ${arg.toString()} is from table \"${arg.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n const fieldName = arg.fieldName;\n const transformedField = this.occurrence\n ? transformOrderByField(fieldName, this.occurrence)\n : fieldName;\n return transformedField; // Default to ascending\n } else {\n throw new Error(\n \"Variadic orderBy() only accepts Column or OrderByExpression arguments\",\n );\n }\n });\n this.queryOptions.orderBy = orderByParts;\n return this;\n }\n\n // Handle single argument\n const orderBy = orderByArgs[0];\n\n // Handle OrderByExpression\n if (isOrderByExpression(orderBy)) {\n // Validate table match\n if (orderBy.column.tableName !== tableName) {\n this.logger.warn(\n `Column ${orderBy.column.toString()} is from table \"${orderBy.column.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n const fieldName = orderBy.column.fieldName;\n const transformedField = this.occurrence\n ? transformOrderByField(fieldName, this.occurrence)\n : fieldName;\n this.queryOptions.orderBy = `${transformedField} ${orderBy.direction}`;\n return this;\n }\n\n // Handle Column references\n if (isColumn(orderBy)) {\n // Validate table match\n if (orderBy.tableName !== tableName) {\n this.logger.warn(\n `Column ${orderBy.toString()} is from table \"${orderBy.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n // Single Column reference without direction (defaults to ascending)\n const fieldName = orderBy.fieldName;\n this.queryOptions.orderBy = this.occurrence\n ? transformOrderByField(fieldName, this.occurrence)\n : fieldName;\n return this;\n }\n // Transform field names to FMFIDs if using entity IDs\n if (this.occurrence && orderBy) {\n if (Array.isArray(orderBy)) {\n // Check if it's a single tuple [field, direction] or array of tuples\n if (\n orderBy.length === 2 &&\n (typeof orderBy[0] === \"string\" || isColumn(orderBy[0])) &&\n (orderBy[1] === \"asc\" || orderBy[1] === \"desc\")\n ) {\n // Single tuple: [field, direction] or [column, direction]\n const field = isColumn(orderBy[0])\n ? orderBy[0].fieldName\n : orderBy[0];\n const direction = orderBy[1] as \"asc\" | \"desc\";\n this.queryOptions.orderBy = `${transformOrderByField(field, this.occurrence)} ${direction}`;\n } else {\n // Array of tuples: [[field, dir], [field, dir], ...]\n this.queryOptions.orderBy = (\n orderBy as Array<[any, \"asc\" | \"desc\"]>\n ).map(([fieldOrCol, direction]) => {\n const field = isColumn(fieldOrCol)\n ? fieldOrCol.fieldName\n : String(fieldOrCol);\n const transformedField = transformOrderByField(\n field,\n this.occurrence!,\n );\n return `${transformedField} ${direction}`;\n });\n }\n } else {\n // Single field name (string)\n this.queryOptions.orderBy = transformOrderByField(\n String(orderBy),\n this.occurrence,\n );\n }\n } else {\n // No occurrence/baseTable - pass through as-is\n if (Array.isArray(orderBy)) {\n if (\n orderBy.length === 2 &&\n (typeof orderBy[0] === \"string\" || isColumn(orderBy[0])) &&\n (orderBy[1] === \"asc\" || orderBy[1] === \"desc\")\n ) {\n // Single tuple: [field, direction] or [column, direction]\n const field = isColumn(orderBy[0])\n ? orderBy[0].fieldName\n : orderBy[0];\n const direction = orderBy[1] as \"asc\" | \"desc\";\n this.queryOptions.orderBy = `${field} ${direction}`;\n } else {\n // Array of tuples\n this.queryOptions.orderBy = (\n orderBy as Array<[any, \"asc\" | \"desc\"]>\n ).map(([fieldOrCol, direction]) => {\n const field = isColumn(fieldOrCol)\n ? fieldOrCol.fieldName\n : String(fieldOrCol);\n return `${field} ${direction}`;\n });\n }\n } else {\n this.queryOptions.orderBy = orderBy;\n }\n }\n return this;\n }\n\n top(\n count: number,\n ): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands> {\n this.queryOptions.top = count;\n return this;\n }\n\n skip(\n count: number,\n ): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands> {\n this.queryOptions.skip = count;\n return this;\n }\n\n expand<TargetTable extends FMTable<any, any>>(\n targetTable: ValidExpandTarget<Occ, TargetTable>,\n callback?: (\n builder: QueryBuilder<\n TargetTable,\n keyof InferSchemaOutputFromFMTable<TargetTable>,\n false,\n false\n >,\n ) => QueryBuilder<TargetTable, any, any, any, any>,\n ): QueryBuilder<\n Occ,\n Selected,\n SingleMode,\n IsCount,\n Expands & {\n [K in ExtractTableName<TargetTable>]: {\n schema: InferSchemaOutputFromFMTable<TargetTable>;\n selected: keyof InferSchemaOutputFromFMTable<TargetTable>;\n };\n }\n > {\n // Use ExpandBuilder.processExpand to handle the expand logic\n type TargetBuilder = QueryBuilder<\n TargetTable,\n keyof InferSchemaOutputFromFMTable<TargetTable>,\n false,\n false\n >;\n const expandConfig = this.expandBuilder.processExpand<\n TargetTable,\n TargetBuilder\n >(\n targetTable,\n this.occurrence,\n callback,\n () =>\n new QueryBuilder<TargetTable>({\n occurrence: targetTable,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n }),\n );\n\n this.expandConfigs.push(expandConfig);\n return this as any;\n }\n\n single(): QueryBuilder<Occ, Selected, \"exact\", IsCount, Expands> {\n return this.cloneWithChanges({ singleMode: \"exact\" as const });\n }\n\n maybeSingle(): QueryBuilder<Occ, Selected, \"maybe\", IsCount, Expands> {\n return this.cloneWithChanges({ singleMode: \"maybe\" as const });\n }\n\n count(): QueryBuilder<Occ, Selected, SingleMode, true, Expands> {\n return this.cloneWithChanges({\n isCountMode: true as const,\n queryOptions: { count: true },\n });\n }\n\n /**\n * Builds the OData query string from current query options and expand configs.\n */\n private buildQueryString(): string {\n // Build query without expand and select (we'll add them manually if using entity IDs)\n const queryOptionsWithoutExpandAndSelect = { ...this.queryOptions };\n const originalSelect = queryOptionsWithoutExpandAndSelect.select;\n delete queryOptionsWithoutExpandAndSelect.expand;\n delete queryOptionsWithoutExpandAndSelect.select;\n\n let queryString = buildQuery(queryOptionsWithoutExpandAndSelect);\n\n // Use shared helper for select/expand portion\n const selectArray = originalSelect\n ? Array.isArray(originalSelect)\n ? originalSelect.map(String)\n : [String(originalSelect)]\n : undefined;\n\n const selectExpandString = buildSelectExpandQueryString({\n selectedFields: selectArray,\n expandConfigs: this.expandConfigs,\n table: this.occurrence,\n useEntityIds: this.databaseUseEntityIds,\n logger: this.logger,\n });\n\n // Append select/expand to existing query string\n if (selectExpandString) {\n // Strip leading ? from helper result and append with appropriate separator\n const params = selectExpandString.startsWith(\"?\")\n ? selectExpandString.slice(1)\n : selectExpandString;\n const separator = queryString.includes(\"?\") ? \"&\" : \"?\";\n queryString = `${queryString}${separator}${params}`;\n }\n\n return queryString;\n }\n\n async execute<EO extends ExecuteOptions>(\n options?: ExecuteMethodOptions<EO>,\n ): Promise<\n Result<\n ConditionallyWithODataAnnotations<\n QueryReturnType<\n InferSchemaOutputFromFMTable<Occ>,\n Selected,\n SingleMode,\n IsCount,\n Expands\n >,\n EO[\"includeODataAnnotations\"] extends true ? true : false\n >\n >\n > {\n const mergedOptions = this.mergeExecuteOptions(options);\n const queryString = this.buildQueryString();\n\n // Handle $count endpoint\n if (this.isCountMode) {\n const url = this.urlBuilder.build(queryString, {\n isCount: true,\n useEntityIds: mergedOptions.useEntityIds,\n navigation: this.navigation,\n });\n const result = await this.context._makeRequest(url, mergedOptions);\n\n if (result.error) {\n return { data: undefined, error: result.error };\n }\n\n // OData returns count as a string, convert to number\n const count =\n typeof result.data === \"string\" ? Number(result.data) : result.data;\n return { data: count as number, error: undefined } as any;\n }\n\n const url = this.urlBuilder.build(queryString, {\n isCount: this.isCountMode,\n useEntityIds: mergedOptions.useEntityIds,\n navigation: this.navigation,\n });\n\n const result = await this.context._makeRequest(url, mergedOptions);\n\n if (result.error) {\n return { data: undefined, error: result.error };\n }\n\n return processQueryResponse(result.data, {\n occurrence: this.occurrence,\n singleMode: this.singleMode,\n queryOptions: this.queryOptions as any,\n expandConfigs: this.expandConfigs,\n skipValidation: options?.skipValidation,\n useEntityIds: mergedOptions.useEntityIds,\n fieldMapping: this.fieldMapping,\n logger: this.logger,\n });\n }\n\n getQueryString(): string {\n const queryString = this.buildQueryString();\n return this.urlBuilder.buildPath(queryString, {\n useEntityIds: this.databaseUseEntityIds,\n navigation: this.navigation,\n });\n }\n\n getRequestConfig(): { method: string; url: string; body?: any } {\n const queryString = this.buildQueryString();\n const url = this.urlBuilder.build(queryString, {\n isCount: this.isCountMode,\n useEntityIds: this.databaseUseEntityIds,\n navigation: this.navigation,\n });\n\n return {\n method: \"GET\",\n url,\n };\n }\n\n toRequest(baseUrl: string, options?: ExecuteOptions): Request {\n const config = this.getRequestConfig();\n return createODataRequest(baseUrl, config, options);\n }\n\n async processResponse(\n response: Response,\n options?: ExecuteOptions,\n ): Promise<\n Result<\n QueryReturnType<\n InferSchemaOutputFromFMTable<Occ>,\n Selected,\n SingleMode,\n IsCount,\n Expands\n >\n >\n > {\n // Check for error responses (important for batch operations)\n if (!response.ok) {\n const error = await parseErrorResponse(\n response,\n response.url ||\n `/${this.databaseName}/${getTableName(this.occurrence)}`,\n );\n return { data: undefined, error };\n }\n\n // Handle 204 No Content (shouldn't happen for queries, but handle it gracefully)\n if (response.status === 204) {\n // Return empty list for list queries, null for single queries\n if (this.singleMode !== false) {\n if (this.singleMode === \"maybe\") {\n return { data: null as any, error: undefined };\n }\n return {\n data: undefined,\n error: new RecordCountMismatchError(\"one\", 0),\n };\n }\n return { data: [] as any, error: undefined };\n }\n\n // Parse the response body (using safeJsonParse to handle FileMaker's invalid JSON with unquoted ? values)\n let rawData;\n try {\n rawData = await safeJsonParse(response);\n } catch (err) {\n // Check if it's an empty body error (common with 204 responses)\n if (err instanceof SyntaxError && response.status === 204) {\n // Handled above, but just in case\n return { data: [] as any, error: undefined };\n }\n return {\n data: undefined,\n error: {\n name: \"ResponseParseError\",\n message: `Failed to parse response JSON: ${err instanceof Error ? err.message : \"Unknown error\"}`,\n timestamp: new Date(),\n } as any,\n };\n }\n\n if (!rawData) {\n return {\n data: undefined,\n error: {\n name: \"ResponseError\",\n message: \"Response body was empty or null\",\n timestamp: new Date(),\n } as any,\n };\n }\n\n const mergedOptions = this.mergeExecuteOptions(options);\n return processQueryResponse(rawData, {\n occurrence: this.occurrence,\n singleMode: this.singleMode,\n queryOptions: this.queryOptions as any,\n expandConfigs: this.expandConfigs,\n skipValidation: options?.skipValidation,\n useEntityIds: mergedOptions.useEntityIds,\n fieldMapping: this.fieldMapping,\n logger: this.logger,\n });\n }\n}\n"],"names":["url","result"],"mappings":"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"}
1
+ {"version":3,"file":"query-builder.js","sources":["../../../../src/client/query/query-builder.ts"],"sourcesContent":["import { QueryOptions } from \"odata-query\";\nimport buildQuery from \"odata-query\";\nimport type {\n ExecutionContext,\n ExecutableBuilder,\n Result,\n ExecuteOptions,\n ConditionallyWithODataAnnotations,\n ExtractSchemaFromOccurrence,\n ExecuteMethodOptions,\n} from \"../../types\";\nimport { RecordCountMismatchError } from \"../../errors\";\nimport { type FFetchOptions } from \"@fetchkit/ffetch\";\nimport {\n transformFieldNamesArray,\n transformOrderByField,\n} from \"../../transform\";\nimport { safeJsonParse } from \"../sanitize-json\";\nimport { parseErrorResponse } from \"../error-parser\";\nimport { isColumn, type Column } from \"../../orm/column\";\nimport {\n FilterExpression,\n OrderByExpression,\n isOrderByExpression,\n} from \"../../orm/operators\";\nimport {\n FMTable,\n type InferSchemaOutputFromFMTable,\n type ValidExpandTarget,\n type ExtractTableName,\n type ValidateNoContainerFields,\n getTableName,\n} from \"../../orm/table\";\nimport {\n ExpandBuilder,\n type ExpandConfig,\n type ExpandedRelations,\n resolveTableId,\n mergeExecuteOptions,\n formatSelectFields,\n processQueryResponse,\n processSelectWithRenames,\n buildSelectExpandQueryString,\n createODataRequest,\n} from \"../builders/index\";\nimport { QueryUrlBuilder, type NavigationConfig } from \"./url-builder\";\nimport type { TypeSafeOrderBy, QueryReturnType } from \"./types\";\nimport { createLogger, InternalLogger } from \"../../logger\";\n\n// Re-export QueryReturnType for backward compatibility\nexport type { QueryReturnType };\n\n/**\n * Default maximum number of records to return in a list query.\n * This prevents stack overflow issues with large datasets while still\n * allowing substantial data retrieval. Users can override with .top().\n */\nconst DEFAULT_TOP = 1000;\n\nexport type { TypeSafeOrderBy, ExpandedRelations };\n\nexport class QueryBuilder<\n Occ extends FMTable<any, any>,\n Selected extends\n | keyof InferSchemaOutputFromFMTable<Occ>\n | Record<\n string,\n Column<any, any, ExtractTableName<Occ>>\n > = keyof InferSchemaOutputFromFMTable<Occ>,\n SingleMode extends \"exact\" | \"maybe\" | false = false,\n IsCount extends boolean = false,\n Expands extends ExpandedRelations = {},\n> implements\n ExecutableBuilder<\n QueryReturnType<\n InferSchemaOutputFromFMTable<Occ>,\n Selected,\n SingleMode,\n IsCount,\n Expands\n >\n >\n{\n private queryOptions: Partial<\n QueryOptions<InferSchemaOutputFromFMTable<Occ>>\n > = {};\n private expandConfigs: ExpandConfig[] = [];\n private singleMode: SingleMode = false as SingleMode;\n private isCountMode = false as IsCount;\n private occurrence: Occ;\n private databaseName: string;\n private context: ExecutionContext;\n private navigation?: NavigationConfig;\n private databaseUseEntityIds: boolean;\n private expandBuilder: ExpandBuilder;\n private urlBuilder: QueryUrlBuilder;\n // Mapping from field names to output keys (for renamed fields in select)\n private fieldMapping?: Record<string, string>;\n private logger: InternalLogger;\n\n constructor(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n databaseUseEntityIds?: boolean;\n }) {\n this.occurrence = config.occurrence;\n this.databaseName = config.databaseName;\n this.context = config.context;\n this.logger = config.context?._getLogger?.() ?? createLogger();\n this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;\n this.expandBuilder = new ExpandBuilder(\n this.databaseUseEntityIds,\n this.logger,\n );\n this.urlBuilder = new QueryUrlBuilder(\n this.databaseName,\n this.occurrence,\n this.context,\n );\n }\n\n /**\n * Helper to merge database-level useEntityIds with per-request options\n */\n private mergeExecuteOptions(\n options?: RequestInit & FFetchOptions & ExecuteOptions,\n ): RequestInit & FFetchOptions & { useEntityIds?: boolean } {\n return mergeExecuteOptions(options, this.databaseUseEntityIds);\n }\n\n /**\n * Gets the FMTable instance\n */\n private getTable(): FMTable<any, any> | undefined {\n return this.occurrence;\n }\n\n /**\n * Gets the table ID (FMTID) if using entity IDs, otherwise returns the table name\n * @param useEntityIds - Optional override for entity ID usage\n */\n private getTableIdOrName(useEntityIds?: boolean): string {\n return resolveTableId(\n this.occurrence,\n getTableName(this.occurrence),\n this.context,\n useEntityIds,\n );\n }\n\n /**\n * Creates a new QueryBuilder with modified configuration.\n * Used by single(), maybeSingle(), count(), and select() to create new instances.\n */\n private cloneWithChanges<\n NewSelected extends\n | keyof InferSchemaOutputFromFMTable<Occ>\n | Record<string, Column<any, any, ExtractTableName<Occ>>> = Selected,\n NewSingle extends \"exact\" | \"maybe\" | false = SingleMode,\n NewCount extends boolean = IsCount,\n >(changes: {\n selectedFields?: NewSelected;\n singleMode?: NewSingle;\n isCountMode?: NewCount;\n queryOptions?: Partial<QueryOptions<InferSchemaOutputFromFMTable<Occ>>>;\n fieldMapping?: Record<string, string>;\n }): QueryBuilder<Occ, NewSelected, NewSingle, NewCount, Expands> {\n const newBuilder = new QueryBuilder<\n Occ,\n NewSelected,\n NewSingle,\n NewCount,\n Expands\n >({\n occurrence: this.occurrence,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n newBuilder.queryOptions = {\n ...this.queryOptions,\n ...changes.queryOptions,\n };\n newBuilder.expandConfigs = [...this.expandConfigs];\n newBuilder.singleMode = (changes.singleMode ?? this.singleMode) as any;\n newBuilder.isCountMode = (changes.isCountMode ?? this.isCountMode) as any;\n newBuilder.fieldMapping = changes.fieldMapping ?? this.fieldMapping;\n // Copy navigation metadata\n newBuilder.navigation = this.navigation;\n newBuilder.urlBuilder = new QueryUrlBuilder(\n this.databaseName,\n this.occurrence,\n this.context,\n );\n return newBuilder;\n }\n\n /**\n * Select fields using column references.\n * Allows renaming fields by using different keys in the object.\n * Container fields cannot be selected and will cause a type error.\n *\n * @example\n * db.from(users).list().select({\n * name: users.name,\n * userEmail: users.email // renamed!\n * })\n *\n * @param fields - Object mapping output keys to column references (container fields excluded)\n * @returns QueryBuilder with updated selected fields\n */\n select<\n TSelect extends Record<\n string,\n Column<any, any, ExtractTableName<Occ>, false>\n >,\n >(fields: TSelect): QueryBuilder<Occ, TSelect, SingleMode, IsCount, Expands> {\n const tableName = getTableName(this.occurrence);\n const { selectedFields, fieldMapping } = processSelectWithRenames(\n fields,\n tableName,\n this.logger,\n );\n\n return this.cloneWithChanges({\n selectedFields: fields as any,\n queryOptions: {\n select: selectedFields,\n },\n fieldMapping:\n Object.keys(fieldMapping).length > 0 ? fieldMapping : undefined,\n });\n }\n\n /**\n * Filter results using operator expressions (new ORM-style API).\n * Supports eq, gt, lt, and, or, etc. operators with Column references.\n * Also supports raw OData filter strings as an escape hatch.\n *\n * @example\n * .where(eq(users.hobby, \"reading\"))\n * .where(and(eq(users.active, true), gt(users.age, 18)))\n * .where(\"status eq 'active'\") // Raw OData string escape hatch\n */\n where(\n expression: FilterExpression | string,\n ): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands> {\n // Handle raw string filters (escape hatch)\n if (typeof expression === \"string\") {\n this.queryOptions.filter = expression;\n return this;\n }\n // Convert FilterExpression to OData filter string\n const filterString = expression.toODataFilter(this.databaseUseEntityIds);\n this.queryOptions.filter = filterString;\n return this;\n }\n\n /**\n * Specify the sort order for query results.\n *\n * @example Single field (ascending by default)\n * ```ts\n * .orderBy(\"name\")\n * .orderBy(users.name) // Column reference\n * .orderBy(asc(users.name)) // Explicit ascending\n * ```\n *\n * @example Single field with explicit direction\n * ```ts\n * .orderBy([\"name\", \"desc\"])\n * .orderBy([users.name, \"desc\"]) // Column reference\n * .orderBy(desc(users.name)) // Explicit descending\n * ```\n *\n * @example Multiple fields with directions\n * ```ts\n * .orderBy([[\"name\", \"asc\"], [\"createdAt\", \"desc\"]])\n * .orderBy([[users.name, \"asc\"], [users.createdAt, \"desc\"]]) // Column references\n * .orderBy(users.name, desc(users.age)) // Variadic with helpers\n * ```\n */\n orderBy(\n ...orderByArgs:\n | [\n | TypeSafeOrderBy<InferSchemaOutputFromFMTable<Occ>>\n | Column<any, any, ExtractTableName<Occ>>\n | OrderByExpression<ExtractTableName<Occ>>,\n ]\n | [\n Column<any, any, ExtractTableName<Occ>>,\n ...Array<\n | Column<any, any, ExtractTableName<Occ>>\n | OrderByExpression<ExtractTableName<Occ>>\n >,\n ]\n ): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands> {\n const tableName = getTableName(this.occurrence);\n\n // Handle variadic arguments (multiple fields)\n if (orderByArgs.length > 1) {\n const orderByParts = orderByArgs.map((arg) => {\n if (isOrderByExpression(arg)) {\n // Validate table match\n if (arg.column.tableName !== tableName) {\n this.logger.warn(\n `Column ${arg.column.toString()} is from table \"${arg.column.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n const fieldName = arg.column.fieldName;\n const transformedField = this.occurrence\n ? transformOrderByField(fieldName, this.occurrence)\n : fieldName;\n return `${transformedField} ${arg.direction}`;\n } else if (isColumn(arg)) {\n // Validate table match\n if (arg.tableName !== tableName) {\n this.logger.warn(\n `Column ${arg.toString()} is from table \"${arg.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n const fieldName = arg.fieldName;\n const transformedField = this.occurrence\n ? transformOrderByField(fieldName, this.occurrence)\n : fieldName;\n return transformedField; // Default to ascending\n } else {\n throw new Error(\n \"Variadic orderBy() only accepts Column or OrderByExpression arguments\",\n );\n }\n });\n this.queryOptions.orderBy = orderByParts;\n return this;\n }\n\n // Handle single argument\n const orderBy = orderByArgs[0];\n\n // Handle OrderByExpression\n if (isOrderByExpression(orderBy)) {\n // Validate table match\n if (orderBy.column.tableName !== tableName) {\n this.logger.warn(\n `Column ${orderBy.column.toString()} is from table \"${orderBy.column.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n const fieldName = orderBy.column.fieldName;\n const transformedField = this.occurrence\n ? transformOrderByField(fieldName, this.occurrence)\n : fieldName;\n this.queryOptions.orderBy = `${transformedField} ${orderBy.direction}`;\n return this;\n }\n\n // Handle Column references\n if (isColumn(orderBy)) {\n // Validate table match\n if (orderBy.tableName !== tableName) {\n this.logger.warn(\n `Column ${orderBy.toString()} is from table \"${orderBy.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n // Single Column reference without direction (defaults to ascending)\n const fieldName = orderBy.fieldName;\n this.queryOptions.orderBy = this.occurrence\n ? transformOrderByField(fieldName, this.occurrence)\n : fieldName;\n return this;\n }\n // Transform field names to FMFIDs if using entity IDs\n if (this.occurrence && orderBy) {\n if (Array.isArray(orderBy)) {\n // Check if it's a single tuple [field, direction] or array of tuples\n if (\n orderBy.length === 2 &&\n (typeof orderBy[0] === \"string\" || isColumn(orderBy[0])) &&\n (orderBy[1] === \"asc\" || orderBy[1] === \"desc\")\n ) {\n // Single tuple: [field, direction] or [column, direction]\n const field = isColumn(orderBy[0])\n ? orderBy[0].fieldName\n : orderBy[0];\n const direction = orderBy[1] as \"asc\" | \"desc\";\n this.queryOptions.orderBy = `${transformOrderByField(field, this.occurrence)} ${direction}`;\n } else {\n // Array of tuples: [[field, dir], [field, dir], ...]\n this.queryOptions.orderBy = (\n orderBy as Array<[any, \"asc\" | \"desc\"]>\n ).map(([fieldOrCol, direction]) => {\n const field = isColumn(fieldOrCol)\n ? fieldOrCol.fieldName\n : String(fieldOrCol);\n const transformedField = transformOrderByField(\n field,\n this.occurrence!,\n );\n return `${transformedField} ${direction}`;\n });\n }\n } else {\n // Single field name (string)\n this.queryOptions.orderBy = transformOrderByField(\n String(orderBy),\n this.occurrence,\n );\n }\n } else {\n // No occurrence/baseTable - pass through as-is\n if (Array.isArray(orderBy)) {\n if (\n orderBy.length === 2 &&\n (typeof orderBy[0] === \"string\" || isColumn(orderBy[0])) &&\n (orderBy[1] === \"asc\" || orderBy[1] === \"desc\")\n ) {\n // Single tuple: [field, direction] or [column, direction]\n const field = isColumn(orderBy[0])\n ? orderBy[0].fieldName\n : orderBy[0];\n const direction = orderBy[1] as \"asc\" | \"desc\";\n this.queryOptions.orderBy = `${field} ${direction}`;\n } else {\n // Array of tuples\n this.queryOptions.orderBy = (\n orderBy as Array<[any, \"asc\" | \"desc\"]>\n ).map(([fieldOrCol, direction]) => {\n const field = isColumn(fieldOrCol)\n ? fieldOrCol.fieldName\n : String(fieldOrCol);\n return `${field} ${direction}`;\n });\n }\n } else {\n this.queryOptions.orderBy = orderBy;\n }\n }\n return this;\n }\n\n top(\n count: number,\n ): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands> {\n this.queryOptions.top = count;\n return this;\n }\n\n skip(\n count: number,\n ): QueryBuilder<Occ, Selected, SingleMode, IsCount, Expands> {\n this.queryOptions.skip = count;\n return this;\n }\n\n expand<\n TargetTable extends FMTable<any, any>,\n TSelected extends\n | keyof InferSchemaOutputFromFMTable<TargetTable>\n | Record<\n string,\n Column<any, any, ExtractTableName<TargetTable>>\n > = keyof InferSchemaOutputFromFMTable<TargetTable>,\n TNestedExpands extends ExpandedRelations = {},\n >(\n targetTable: ValidExpandTarget<Occ, TargetTable>,\n callback?: (\n builder: QueryBuilder<\n TargetTable,\n keyof InferSchemaOutputFromFMTable<TargetTable>,\n false,\n false,\n {}\n >,\n ) => QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands>,\n ): QueryBuilder<\n Occ,\n Selected,\n SingleMode,\n IsCount,\n Expands & {\n [K in ExtractTableName<TargetTable>]: {\n schema: InferSchemaOutputFromFMTable<TargetTable>;\n selected: TSelected;\n nested: TNestedExpands;\n };\n }\n > {\n // Use ExpandBuilder.processExpand to handle the expand logic\n type TargetBuilder = QueryBuilder<\n TargetTable,\n keyof InferSchemaOutputFromFMTable<TargetTable>,\n false,\n false,\n {}\n >;\n const expandConfig = this.expandBuilder.processExpand<\n TargetTable,\n TargetBuilder\n >(\n targetTable,\n this.occurrence,\n callback as ((builder: TargetBuilder) => TargetBuilder) | undefined,\n () =>\n new QueryBuilder<TargetTable>({\n occurrence: targetTable,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n }),\n );\n\n this.expandConfigs.push(expandConfig);\n return this as any;\n }\n\n single(): QueryBuilder<Occ, Selected, \"exact\", IsCount, Expands> {\n return this.cloneWithChanges({ singleMode: \"exact\" as const });\n }\n\n maybeSingle(): QueryBuilder<Occ, Selected, \"maybe\", IsCount, Expands> {\n return this.cloneWithChanges({ singleMode: \"maybe\" as const });\n }\n\n count(): QueryBuilder<Occ, Selected, SingleMode, true, Expands> {\n return this.cloneWithChanges({\n isCountMode: true as const,\n queryOptions: { count: true },\n });\n }\n\n /**\n * Builds the OData query string from current query options and expand configs.\n */\n private buildQueryString(): string {\n // Build query without expand and select (we'll add them manually if using entity IDs)\n const queryOptionsWithoutExpandAndSelect = { ...this.queryOptions };\n const originalSelect = queryOptionsWithoutExpandAndSelect.select;\n delete queryOptionsWithoutExpandAndSelect.expand;\n delete queryOptionsWithoutExpandAndSelect.select;\n\n let queryString = buildQuery(queryOptionsWithoutExpandAndSelect);\n\n // Use shared helper for select/expand portion\n const selectArray = originalSelect\n ? Array.isArray(originalSelect)\n ? originalSelect.map(String)\n : [String(originalSelect)]\n : undefined;\n\n const selectExpandString = buildSelectExpandQueryString({\n selectedFields: selectArray,\n expandConfigs: this.expandConfigs,\n table: this.occurrence,\n useEntityIds: this.databaseUseEntityIds,\n logger: this.logger,\n });\n\n // Append select/expand to existing query string\n if (selectExpandString) {\n // Strip leading ? from helper result and append with appropriate separator\n const params = selectExpandString.startsWith(\"?\")\n ? selectExpandString.slice(1)\n : selectExpandString;\n const separator = queryString.includes(\"?\") ? \"&\" : \"?\";\n queryString = `${queryString}${separator}${params}`;\n }\n\n return queryString;\n }\n\n async execute<EO extends ExecuteOptions>(\n options?: ExecuteMethodOptions<EO>,\n ): Promise<\n Result<\n ConditionallyWithODataAnnotations<\n QueryReturnType<\n InferSchemaOutputFromFMTable<Occ>,\n Selected,\n SingleMode,\n IsCount,\n Expands\n >,\n EO[\"includeODataAnnotations\"] extends true ? true : false\n >\n >\n > {\n const mergedOptions = this.mergeExecuteOptions(options);\n const queryString = this.buildQueryString();\n\n // Handle $count endpoint\n if (this.isCountMode) {\n const url = this.urlBuilder.build(queryString, {\n isCount: true,\n useEntityIds: mergedOptions.useEntityIds,\n navigation: this.navigation,\n });\n const result = await this.context._makeRequest(url, mergedOptions);\n\n if (result.error) {\n return { data: undefined, error: result.error };\n }\n\n // OData returns count as a string, convert to number\n const count =\n typeof result.data === \"string\" ? Number(result.data) : result.data;\n return { data: count as number, error: undefined } as any;\n }\n\n const url = this.urlBuilder.build(queryString, {\n isCount: this.isCountMode,\n useEntityIds: mergedOptions.useEntityIds,\n navigation: this.navigation,\n });\n\n const result = await this.context._makeRequest(url, mergedOptions);\n\n if (result.error) {\n return { data: undefined, error: result.error };\n }\n\n return processQueryResponse(result.data, {\n occurrence: this.occurrence,\n singleMode: this.singleMode,\n queryOptions: this.queryOptions as any,\n expandConfigs: this.expandConfigs,\n skipValidation: options?.skipValidation,\n useEntityIds: mergedOptions.useEntityIds,\n fieldMapping: this.fieldMapping,\n logger: this.logger,\n });\n }\n\n getQueryString(): string {\n const queryString = this.buildQueryString();\n return this.urlBuilder.buildPath(queryString, {\n useEntityIds: this.databaseUseEntityIds,\n navigation: this.navigation,\n });\n }\n\n getRequestConfig(): { method: string; url: string; body?: any } {\n const queryString = this.buildQueryString();\n const url = this.urlBuilder.build(queryString, {\n isCount: this.isCountMode,\n useEntityIds: this.databaseUseEntityIds,\n navigation: this.navigation,\n });\n\n return {\n method: \"GET\",\n url,\n };\n }\n\n toRequest(baseUrl: string, options?: ExecuteOptions): Request {\n const config = this.getRequestConfig();\n return createODataRequest(baseUrl, config, options);\n }\n\n async processResponse(\n response: Response,\n options?: ExecuteOptions,\n ): Promise<\n Result<\n QueryReturnType<\n InferSchemaOutputFromFMTable<Occ>,\n Selected,\n SingleMode,\n IsCount,\n Expands\n >\n >\n > {\n // Check for error responses (important for batch operations)\n if (!response.ok) {\n const error = await parseErrorResponse(\n response,\n response.url ||\n `/${this.databaseName}/${getTableName(this.occurrence)}`,\n );\n return { data: undefined, error };\n }\n\n // Handle 204 No Content (shouldn't happen for queries, but handle it gracefully)\n if (response.status === 204) {\n // Return empty list for list queries, null for single queries\n if (this.singleMode !== false) {\n if (this.singleMode === \"maybe\") {\n return { data: null as any, error: undefined };\n }\n return {\n data: undefined,\n error: new RecordCountMismatchError(\"one\", 0),\n };\n }\n return { data: [] as any, error: undefined };\n }\n\n // Parse the response body (using safeJsonParse to handle FileMaker's invalid JSON with unquoted ? values)\n let rawData;\n try {\n rawData = await safeJsonParse(response);\n } catch (err) {\n // Check if it's an empty body error (common with 204 responses)\n if (err instanceof SyntaxError && response.status === 204) {\n // Handled above, but just in case\n return { data: [] as any, error: undefined };\n }\n return {\n data: undefined,\n error: {\n name: \"ResponseParseError\",\n message: `Failed to parse response JSON: ${err instanceof Error ? err.message : \"Unknown error\"}`,\n timestamp: new Date(),\n } as any,\n };\n }\n\n if (!rawData) {\n return {\n data: undefined,\n error: {\n name: \"ResponseError\",\n message: \"Response body was empty or null\",\n timestamp: new Date(),\n } as any,\n };\n }\n\n const mergedOptions = this.mergeExecuteOptions(options);\n return processQueryResponse(rawData, {\n occurrence: this.occurrence,\n singleMode: this.singleMode,\n queryOptions: this.queryOptions as any,\n expandConfigs: this.expandConfigs,\n skipValidation: options?.skipValidation,\n useEntityIds: mergedOptions.useEntityIds,\n fieldMapping: this.fieldMapping,\n logger: this.logger,\n });\n }\n}\n"],"names":["url","result"],"mappings":"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"}
@@ -19,6 +19,7 @@ export type ExpandConfig = {
19
19
  export type ExpandedRelations = Record<string, {
20
20
  schema: any;
21
21
  selected: any;
22
+ nested?: ExpandedRelations;
22
23
  }>;
23
24
  /**
24
25
  * Extract the value type from a Column.
@@ -32,21 +33,24 @@ type ExtractColumnType<C> = C extends Column<infer T, any, any, any> ? T : never
32
33
  type MapSelectToReturnType<TSelect extends Record<string, Column<any, any, any, any>>, TSchema extends Record<string, any>> = {
33
34
  [K in keyof TSelect]: ExtractColumnType<TSelect[K]>;
34
35
  };
36
+ /**
37
+ * Helper: Resolve a single expand's return type, including nested expands
38
+ */
39
+ export type ResolveExpandType<Exp extends {
40
+ schema: any;
41
+ selected: any;
42
+ nested?: ExpandedRelations;
43
+ }> = // Handle the selected fields
44
+ (Exp["selected"] extends Record<string, Column<any, any, any, any>> ? MapSelectToReturnType<Exp["selected"], Exp["schema"]> : Exp["selected"] extends keyof Exp["schema"] ? Pick<Exp["schema"], Exp["selected"]> : Exp["schema"]) & (Exp["nested"] extends ExpandedRelations ? ResolveExpandedRelations<Exp["nested"]> : {});
45
+ /**
46
+ * Helper: Resolve all expanded relations recursively
47
+ */
48
+ export type ResolveExpandedRelations<Exps extends ExpandedRelations> = {
49
+ [K in keyof Exps]: ResolveExpandType<Exps[K]>[];
50
+ };
35
51
  export type QueryReturnType<T extends Record<string, any>, Selected extends keyof T | Record<string, Column<any, any, any, any>>, SingleMode extends "exact" | "maybe" | false, IsCount extends boolean, Expands extends ExpandedRelations> = IsCount extends true ? number : [
36
52
  Selected
37
- ] extends [Record<string, Column<any, any, any, any>>] ? SingleMode extends "exact" ? MapSelectToReturnType<Selected, T> & {
38
- [K in keyof Expands]: Pick<Expands[K]["schema"], Expands[K]["selected"]>[];
39
- } : SingleMode extends "maybe" ? (MapSelectToReturnType<Selected, T> & {
40
- [K in keyof Expands]: Pick<Expands[K]["schema"], Expands[K]["selected"]>[];
41
- }) | null : (MapSelectToReturnType<Selected, T> & {
42
- [K in keyof Expands]: Pick<Expands[K]["schema"], Expands[K]["selected"]>[];
43
- })[] : [
53
+ ] extends [Record<string, Column<any, any, any, any>>] ? SingleMode extends "exact" ? MapSelectToReturnType<Selected, T> & ResolveExpandedRelations<Expands> : SingleMode extends "maybe" ? (MapSelectToReturnType<Selected, T> & ResolveExpandedRelations<Expands>) | null : (MapSelectToReturnType<Selected, T> & ResolveExpandedRelations<Expands>)[] : [
44
54
  Selected
45
- ] extends [keyof T] ? SingleMode extends "exact" ? Pick<T, Selected> & {
46
- [K in keyof Expands]: Pick<Expands[K]["schema"], Expands[K]["selected"]>[];
47
- } : SingleMode extends "maybe" ? (Pick<T, Selected> & {
48
- [K in keyof Expands]: Pick<Expands[K]["schema"], Expands[K]["selected"]>[];
49
- }) | null : (Pick<T, Selected> & {
50
- [K in keyof Expands]: Pick<Expands[K]["schema"], Expands[K]["selected"]>[];
51
- })[] : never;
55
+ ] extends [keyof T] ? SingleMode extends "exact" ? Pick<T, Selected> & ResolveExpandedRelations<Expands> : SingleMode extends "maybe" ? (Pick<T, Selected> & ResolveExpandedRelations<Expands>) | null : (Pick<T, Selected> & ResolveExpandedRelations<Expands>)[] : never;
52
56
  export {};
@@ -3,6 +3,7 @@ import { FMTable, InferSchemaOutputFromFMTable, ValidExpandTarget, ExtractTableN
3
3
  import { QueryBuilder } from './query-builder.js';
4
4
  import { Column } from '../orm/column.js';
5
5
  import { ExpandedRelations } from './builders/index.js';
6
+ import { ResolveExpandedRelations } from './query/types.js';
6
7
  /**
7
8
  * Extract the value type from a Column.
8
9
  * This uses the phantom type stored in Column to get the actual value type.
@@ -17,13 +18,9 @@ type MapSelectToReturnType<TSelect extends Record<string, Column<any, any, any,
17
18
  };
18
19
  export type RecordReturnType<Schema extends Record<string, any>, IsSingleField extends boolean, FieldColumn extends Column<any, any, any, any> | undefined, Selected extends keyof Schema | Record<string, Column<any, any, ExtractTableName<FMTable<any, any>>>>, Expands extends ExpandedRelations> = IsSingleField extends true ? FieldColumn extends Column<infer TOutput, any, any, any> ? TOutput : never : [
19
20
  Selected
20
- ] extends [Record<string, Column<any, any, any, any>>] ? MapSelectToReturnType<Selected, Schema> & {
21
- [K in keyof Expands]: Pick<Expands[K]["schema"], Expands[K]["selected"]>[];
22
- } : [
21
+ ] extends [Record<string, Column<any, any, any, any>>] ? MapSelectToReturnType<Selected, Schema> & ResolveExpandedRelations<Expands> : [
23
22
  Selected
24
- ] extends [keyof Schema] ? Pick<Schema, Selected> & {
25
- [K in keyof Expands]: Pick<Expands[K]["schema"], Expands[K]["selected"]>[];
26
- } : never;
23
+ ] extends [keyof Schema] ? Pick<Schema, Selected> & ResolveExpandedRelations<Expands> : never;
27
24
  export declare class RecordBuilder<Occ extends FMTable<any, any> = FMTable<any, any>, IsSingleField extends boolean = false, FieldColumn extends Column<any, any, any, any> | undefined = undefined, Selected extends keyof InferSchemaOutputFromFMTable<NonNullable<Occ>> | Record<string, Column<any, any, ExtractTableName<NonNullable<Occ>>>> = keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>, Expands extends ExpandedRelations = {}> implements ExecutableBuilder<RecordReturnType<InferSchemaOutputFromFMTable<NonNullable<Occ>>, IsSingleField, FieldColumn, Selected, Expands>> {
28
25
  private table;
29
26
  private databaseName;
@@ -92,10 +89,11 @@ export declare class RecordBuilder<Occ extends FMTable<any, any> = FMTable<any,
92
89
  * .execute();
93
90
  * ```
94
91
  */
95
- expand<TargetTable extends FMTable<any, any>>(targetTable: ValidExpandTarget<Occ, TargetTable>, callback?: (builder: QueryBuilder<TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false>) => QueryBuilder<TargetTable, any, any, any, any>): RecordBuilder<Occ, false, FieldColumn, Selected, Expands & {
92
+ expand<TargetTable extends FMTable<any, any>, TSelected extends keyof InferSchemaOutputFromFMTable<TargetTable> | Record<string, Column<any, any, ExtractTableName<TargetTable>>> = keyof InferSchemaOutputFromFMTable<TargetTable>, TNestedExpands extends ExpandedRelations = {}>(targetTable: ValidExpandTarget<Occ, TargetTable>, callback?: (builder: QueryBuilder<TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false, {}>) => QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands>): RecordBuilder<Occ, false, FieldColumn, Selected, Expands & {
96
93
  [K in ExtractTableName<TargetTable>]: {
97
94
  schema: InferSchemaOutputFromFMTable<TargetTable>;
98
- selected: keyof InferSchemaOutputFromFMTable<TargetTable>;
95
+ selected: TSelected;
96
+ nested: TNestedExpands;
99
97
  };
100
98
  }>;
101
99
  navigate<TargetTable extends FMTable<any, any>>(targetTable: ValidExpandTarget<Occ, TargetTable>): QueryBuilder<TargetTable, keyof InferSchemaOutputFromFMTable<TargetTable>, false, false>;
@@ -1 +1 @@
1
- {"version":3,"file":"record-builder.js","sources":["../../../src/client/record-builder.ts"],"sourcesContent":["import type {\n ExecutionContext,\n ExecutableBuilder,\n Result,\n ODataFieldResponse,\n ExecuteOptions,\n ConditionallyWithODataAnnotations,\n ExecuteMethodOptions,\n} from \"../types\";\nimport type {\n FMTable,\n InferSchemaOutputFromFMTable,\n ValidExpandTarget,\n ExtractTableName,\n ValidateNoContainerFields,\n} from \"../orm/table\";\nimport { getTableName, getNavigationPaths } from \"../orm/table\";\nimport { safeJsonParse } from \"./sanitize-json\";\nimport { parseErrorResponse } from \"./error-parser\";\nimport { QueryBuilder } from \"./query-builder\";\nimport { type FFetchOptions } from \"@fetchkit/ffetch\";\nimport { isColumn, type Column } from \"../orm/column\";\nimport {\n type ExpandConfig,\n type ExpandedRelations,\n ExpandBuilder,\n resolveTableId,\n mergeExecuteOptions,\n processODataResponse,\n getSchemaFromTable,\n processSelectWithRenames,\n buildSelectExpandQueryString,\n createODataRequest,\n} from \"./builders/index\";\nimport { createLogger, InternalLogger, Logger } from \"../logger\";\n\n/**\n * Extract the value type from a Column.\n * This uses the phantom type stored in Column to get the actual value type.\n */\ntype ExtractColumnType<C> = C extends Column<infer T, any> ? T : never;\n\n/**\n * Map a select object to its return type.\n * For each key in the select object, extract the type from the corresponding Column.\n */\ntype MapSelectToReturnType<\n TSelect extends Record<string, Column<any, any, any, any>>,\n TSchema extends Record<string, any>,\n> = {\n [K in keyof TSelect]: ExtractColumnType<TSelect[K]>;\n};\n\n// Return type for RecordBuilder execute\nexport type RecordReturnType<\n Schema extends Record<string, any>,\n IsSingleField extends boolean,\n FieldColumn extends Column<any, any, any, any> | undefined,\n Selected extends\n | keyof Schema\n | Record<string, Column<any, any, ExtractTableName<FMTable<any, any>>>>,\n Expands extends ExpandedRelations,\n> = IsSingleField extends true\n ? FieldColumn extends Column<infer TOutput, any, any, any>\n ? TOutput\n : never\n : // Use tuple wrapping [Selected] extends [...] to prevent distribution over unions\n [Selected] extends [Record<string, Column<any, any, any, any>>]\n ? MapSelectToReturnType<Selected, Schema> & {\n [K in keyof Expands]: Pick<\n Expands[K][\"schema\"],\n Expands[K][\"selected\"]\n >[];\n }\n : // Use tuple wrapping to prevent distribution over union of keys\n [Selected] extends [keyof Schema]\n ? Pick<Schema, Selected> & {\n [K in keyof Expands]: Pick<\n Expands[K][\"schema\"],\n Expands[K][\"selected\"]\n >[];\n }\n : never;\n\nexport class RecordBuilder<\n Occ extends FMTable<any, any> = FMTable<any, any>,\n IsSingleField extends boolean = false,\n FieldColumn extends Column<any, any, any, any> | undefined = undefined,\n Selected extends\n | keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>\n | Record<\n string,\n Column<any, any, ExtractTableName<NonNullable<Occ>>>\n > = keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n Expands extends ExpandedRelations = {},\n> implements\n ExecutableBuilder<\n RecordReturnType<\n InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n IsSingleField,\n FieldColumn,\n Selected,\n Expands\n >\n >\n{\n private table: Occ;\n private databaseName: string;\n private context: ExecutionContext;\n private recordId: string | number;\n private operation?: \"getSingleField\" | \"navigate\";\n private operationParam?: string;\n private operationColumn?: Column<any, any, any, any>;\n private isNavigateFromEntitySet?: boolean;\n private navigateRelation?: string;\n private navigateSourceTableName?: string;\n\n private databaseUseEntityIds: boolean;\n\n // Properties for select/expand support\n private selectedFields?: string[];\n private expandConfigs: ExpandConfig[] = [];\n // Mapping from field names to output keys (for renamed fields in select)\n private fieldMapping?: Record<string, string>;\n\n private logger: InternalLogger;\n\n constructor(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n recordId: string | number;\n databaseUseEntityIds?: boolean;\n }) {\n this.table = config.occurrence;\n this.databaseName = config.databaseName;\n this.context = config.context;\n this.recordId = config.recordId;\n this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;\n this.logger = config.context?._getLogger?.() ?? createLogger();\n }\n\n /**\n * Helper to merge database-level useEntityIds with per-request options\n */\n private mergeExecuteOptions(\n options?: RequestInit & FFetchOptions & ExecuteOptions,\n ): RequestInit & FFetchOptions & { useEntityIds?: boolean } {\n return mergeExecuteOptions(options, this.databaseUseEntityIds);\n }\n\n /**\n * Gets the table ID (FMTID) if using entity IDs, otherwise returns the table name\n * @param useEntityIds - Optional override for entity ID usage\n */\n private getTableId(useEntityIds?: boolean): string {\n if (!this.table) {\n throw new Error(\"Table occurrence is required\");\n }\n return resolveTableId(\n this.table,\n getTableName(this.table),\n this.context,\n useEntityIds,\n );\n }\n\n /**\n * Creates a new RecordBuilder with modified configuration.\n * Used by select() to create new instances.\n */\n private cloneWithChanges<\n NewSelected extends\n | keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>\n | Record<\n string,\n Column<any, any, ExtractTableName<NonNullable<Occ>>>\n > = Selected,\n >(changes: {\n selectedFields?: string[];\n fieldMapping?: Record<string, string>;\n }): RecordBuilder<Occ, false, FieldColumn, NewSelected, Expands> {\n const newBuilder = new RecordBuilder<\n Occ,\n false,\n FieldColumn,\n NewSelected,\n Expands\n >({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n recordId: this.recordId,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n newBuilder.selectedFields = changes.selectedFields ?? this.selectedFields;\n newBuilder.fieldMapping = changes.fieldMapping ?? this.fieldMapping;\n newBuilder.expandConfigs = [...this.expandConfigs];\n // Preserve navigation context\n newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;\n newBuilder.navigateRelation = this.navigateRelation;\n newBuilder.navigateSourceTableName = this.navigateSourceTableName;\n newBuilder.operationColumn = this.operationColumn;\n return newBuilder;\n }\n\n getSingleField<\n TColumn extends Column<any, any, ExtractTableName<NonNullable<Occ>>, any>,\n >(\n column: TColumn,\n ): RecordBuilder<\n Occ,\n true,\n TColumn,\n keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n {}\n > {\n // Runtime validation: ensure column is from the correct table\n const tableName = getTableName(this.table);\n if (!column.isFromTable(tableName)) {\n throw new Error(\n `Column ${column.toString()} is not from table ${tableName}`,\n );\n }\n\n const newBuilder = new RecordBuilder<\n Occ,\n true,\n TColumn,\n keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n {}\n >({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n recordId: this.recordId,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n newBuilder.operation = \"getSingleField\";\n newBuilder.operationColumn = column;\n newBuilder.operationParam = column.getFieldIdentifier(\n this.databaseUseEntityIds,\n );\n // Preserve navigation context\n newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;\n newBuilder.navigateRelation = this.navigateRelation;\n newBuilder.navigateSourceTableName = this.navigateSourceTableName;\n return newBuilder;\n }\n\n /**\n * Select fields using column references.\n * Allows renaming fields by using different keys in the object.\n * Container fields cannot be selected and will cause a type error.\n *\n * @example\n * db.from(contacts).get(\"uuid\").select({\n * name: contacts.name,\n * userEmail: contacts.email // renamed!\n * })\n *\n * @param fields - Object mapping output keys to column references (container fields excluded)\n * @returns RecordBuilder with updated selected fields\n */\n select<\n TSelect extends Record<\n string,\n Column<any, any, ExtractTableName<Occ>, false>\n >,\n >(fields: TSelect): RecordBuilder<Occ, false, FieldColumn, TSelect, Expands> {\n const tableName = getTableName(this.table);\n const { selectedFields, fieldMapping } = processSelectWithRenames(\n fields,\n tableName,\n this.logger,\n );\n\n return this.cloneWithChanges({\n selectedFields,\n fieldMapping:\n Object.keys(fieldMapping).length > 0 ? fieldMapping : undefined,\n }) as any;\n }\n\n /**\n * Expand a navigation property to include related records.\n * Supports nested select, filter, orderBy, and expand operations.\n *\n * @example\n * ```typescript\n * // Simple expand with FMTable object\n * const contact = await db.from(contacts).get(\"uuid\").expand(users).execute();\n *\n * // Expand with select\n * const contact = await db.from(contacts).get(\"uuid\")\n * .expand(users, b => b.select({ username: users.username, email: users.email }))\n * .execute();\n * ```\n */\n expand<TargetTable extends FMTable<any, any>>(\n targetTable: ValidExpandTarget<Occ, TargetTable>,\n callback?: (\n builder: QueryBuilder<\n TargetTable,\n keyof InferSchemaOutputFromFMTable<TargetTable>,\n false,\n false\n >,\n ) => QueryBuilder<TargetTable, any, any, any, any>,\n ): RecordBuilder<\n Occ,\n false,\n FieldColumn,\n Selected,\n Expands & {\n [K in ExtractTableName<TargetTable>]: {\n schema: InferSchemaOutputFromFMTable<TargetTable>;\n selected: keyof InferSchemaOutputFromFMTable<TargetTable>;\n };\n }\n > {\n // Create new builder with updated types\n const newBuilder = new RecordBuilder<\n Occ,\n false,\n FieldColumn,\n Selected,\n any\n >({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n recordId: this.recordId,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n\n // Copy existing state\n newBuilder.selectedFields = this.selectedFields;\n newBuilder.fieldMapping = this.fieldMapping;\n newBuilder.expandConfigs = [...this.expandConfigs];\n newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;\n newBuilder.navigateRelation = this.navigateRelation;\n newBuilder.navigateSourceTableName = this.navigateSourceTableName;\n newBuilder.operationColumn = this.operationColumn;\n\n // Use ExpandBuilder.processExpand to handle the expand logic\n const expandBuilder = new ExpandBuilder(\n this.databaseUseEntityIds,\n this.logger,\n );\n type TargetBuilder = QueryBuilder<\n TargetTable,\n keyof InferSchemaOutputFromFMTable<TargetTable>,\n false,\n false\n >;\n const expandConfig = expandBuilder.processExpand<\n TargetTable,\n TargetBuilder\n >(\n targetTable,\n this.table ?? undefined,\n callback,\n () =>\n new QueryBuilder<TargetTable>({\n occurrence: targetTable,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n }),\n );\n\n newBuilder.expandConfigs.push(expandConfig);\n return newBuilder as any;\n }\n\n navigate<TargetTable extends FMTable<any, any>>(\n targetTable: ValidExpandTarget<Occ, TargetTable>,\n ): QueryBuilder<\n TargetTable,\n keyof InferSchemaOutputFromFMTable<TargetTable>,\n false,\n false\n > {\n // Extract name and validate\n const relationName = getTableName(targetTable);\n\n // Runtime validation: Check if relation name is in navigationPaths\n if (this.table) {\n const navigationPaths = getNavigationPaths(this.table);\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 QueryBuilder with target table\n const builder = new QueryBuilder<TargetTable>({\n occurrence: targetTable,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n\n // Store the navigation info - we'll use it in execute\n // Use relation name as-is (entity ID handling is done in QueryBuilder)\n const relationId = relationName;\n\n // If this RecordBuilder came from a navigated EntitySet, we need to preserve that base path\n let sourceTableName: string;\n let baseRelation: string | undefined;\n if (\n this.isNavigateFromEntitySet &&\n this.navigateSourceTableName &&\n this.navigateRelation\n ) {\n // Build the base path: /sourceTable/relation('recordId')/newRelation\n sourceTableName = this.navigateSourceTableName;\n baseRelation = this.navigateRelation;\n } else {\n // Normal record navigation: /tableName('recordId')/relation\n // Use table ID if available, otherwise table name\n if (!this.table) {\n throw new Error(\"Table occurrence is required for navigation\");\n }\n sourceTableName = resolveTableId(\n this.table,\n getTableName(this.table),\n this.context,\n this.databaseUseEntityIds,\n );\n }\n\n (builder as any).navigation = {\n recordId: this.recordId,\n relation: relationId,\n sourceTableName,\n baseRelation,\n };\n\n return builder;\n }\n\n /**\n * Builds the complete query string including $select and $expand parameters.\n */\n private buildQueryString(): string {\n return buildSelectExpandQueryString({\n selectedFields: this.selectedFields,\n expandConfigs: this.expandConfigs,\n table: this.table,\n useEntityIds: this.databaseUseEntityIds,\n logger: this.logger,\n });\n }\n\n async execute<EO extends ExecuteOptions>(\n options?: ExecuteMethodOptions<EO>,\n ): Promise<\n Result<\n ConditionallyWithODataAnnotations<\n RecordReturnType<\n InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n IsSingleField,\n FieldColumn,\n Selected,\n Expands\n >,\n EO[\"includeODataAnnotations\"] extends true ? true : false\n >\n >\n > {\n let url: string;\n\n // Build the base URL depending on whether this came from a navigated EntitySet\n if (\n this.isNavigateFromEntitySet &&\n this.navigateSourceTableName &&\n this.navigateRelation\n ) {\n // From navigated EntitySet: /sourceTable/relation('recordId')\n url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;\n } else {\n // Normal record: /tableName('recordId') - use FMTID if configured\n const tableId = this.getTableId(\n options?.useEntityIds ?? this.databaseUseEntityIds,\n );\n url = `/${this.databaseName}/${tableId}('${this.recordId}')`;\n }\n\n if (this.operation === \"getSingleField\" && this.operationParam) {\n url += `/${this.operationParam}`;\n } else {\n // Add query string for select/expand (only when not getting a single field)\n const queryString = this.buildQueryString();\n url += queryString;\n }\n\n const mergedOptions = this.mergeExecuteOptions(options);\n const result = await this.context._makeRequest(url, mergedOptions);\n\n if (result.error) {\n return { data: undefined, error: result.error };\n }\n\n let response = result.data;\n\n // Handle single field operation\n if (this.operation === \"getSingleField\") {\n // Single field returns a JSON object with @context and value\n // The type is extracted from the Column stored in FieldColumn generic\n const fieldResponse = response as ODataFieldResponse<any>;\n return { data: fieldResponse.value as any, error: undefined };\n }\n\n // Use shared response processor\n const expandBuilder = new ExpandBuilder(\n mergedOptions.useEntityIds ?? false,\n this.logger,\n );\n const expandValidationConfigs = expandBuilder.buildValidationConfigs(\n this.expandConfigs,\n );\n\n return processODataResponse(response, {\n table: this.table,\n schema: getSchemaFromTable(this.table),\n singleMode: \"exact\",\n selectedFields: this.selectedFields,\n expandValidationConfigs,\n skipValidation: options?.skipValidation,\n useEntityIds: mergedOptions.useEntityIds,\n fieldMapping: this.fieldMapping,\n });\n }\n\n getRequestConfig(): { method: string; url: string; body?: any } {\n let url: string;\n\n // Build the base URL depending on whether this came from a navigated EntitySet\n if (\n this.isNavigateFromEntitySet &&\n this.navigateSourceTableName &&\n this.navigateRelation\n ) {\n // From navigated EntitySet: /sourceTable/relation('recordId')\n url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;\n } else {\n // For batch operations, use database-level setting (no per-request override available here)\n const tableId = this.getTableId(this.databaseUseEntityIds);\n url = `/${this.databaseName}/${tableId}('${this.recordId}')`;\n }\n\n if (this.operation === \"getSingleField\" && this.operationColumn) {\n // Use the column's getFieldIdentifier to support entity IDs\n url += `/${this.operationColumn.getFieldIdentifier(\n this.databaseUseEntityIds,\n )}`;\n } else if (this.operation === \"getSingleField\" && this.operationParam) {\n // Fallback for backwards compatibility (shouldn't happen in normal flow)\n url += `/${this.operationParam}`;\n } else {\n // Add query string for select/expand (only when not getting a single field)\n const queryString = this.buildQueryString();\n url += queryString;\n }\n\n return {\n method: \"GET\",\n url,\n };\n }\n\n /**\n * Returns the query string for this record builder (for testing purposes).\n */\n getQueryString(): string {\n let path: string;\n\n // Build the path depending on navigation context\n if (\n this.isNavigateFromEntitySet &&\n this.navigateSourceTableName &&\n this.navigateRelation\n ) {\n path = `/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;\n } else {\n // Use getTableId to respect entity ID settings (same as getRequestConfig)\n const tableId = this.getTableId(this.databaseUseEntityIds);\n path = `/${tableId}('${this.recordId}')`;\n }\n\n if (this.operation === \"getSingleField\" && this.operationColumn) {\n return `${path}/${this.operationColumn.getFieldIdentifier(\n this.databaseUseEntityIds,\n )}`;\n } else if (this.operation === \"getSingleField\" && this.operationParam) {\n // Fallback for backwards compatibility (shouldn't happen in normal flow)\n return `${path}/${this.operationParam}`;\n }\n\n const queryString = this.buildQueryString();\n return `${path}${queryString}`;\n }\n\n toRequest(baseUrl: string, options?: ExecuteOptions): Request {\n const config = this.getRequestConfig();\n return createODataRequest(baseUrl, config, options);\n }\n\n async processResponse(\n response: Response,\n options?: ExecuteOptions,\n ): Promise<\n Result<\n RecordReturnType<\n InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n IsSingleField,\n FieldColumn,\n Selected,\n Expands\n >\n >\n > {\n // Check for error responses (important for batch operations)\n if (!response.ok) {\n const tableName = this.table ? getTableName(this.table) : \"unknown\";\n const error = await parseErrorResponse(\n response,\n response.url || `/${this.databaseName}/${tableName}`,\n );\n return { data: undefined, error };\n }\n\n // Use safeJsonParse to handle FileMaker's invalid JSON with unquoted ? values\n const rawResponse = await safeJsonParse(response);\n\n // Handle single field operation\n if (this.operation === \"getSingleField\") {\n // Single field returns a JSON object with @context and value\n // The type is extracted from the Column stored in FieldColumn generic\n const fieldResponse = rawResponse as ODataFieldResponse<any>;\n return { data: fieldResponse.value as any, error: undefined };\n }\n\n // Use shared response processor\n const mergedOptions = mergeExecuteOptions(\n options,\n this.databaseUseEntityIds,\n );\n const expandBuilder = new ExpandBuilder(\n mergedOptions.useEntityIds ?? false,\n this.logger,\n );\n const expandValidationConfigs = expandBuilder.buildValidationConfigs(\n this.expandConfigs,\n );\n\n return processODataResponse(rawResponse, {\n table: this.table,\n schema: getSchemaFromTable(this.table),\n singleMode: \"exact\",\n selectedFields: this.selectedFields,\n expandValidationConfigs,\n skipValidation: options?.skipValidation,\n useEntityIds: mergedOptions.useEntityIds,\n fieldMapping: this.fieldMapping,\n });\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"record-builder.js","sources":["../../../src/client/record-builder.ts"],"sourcesContent":["import type {\n ExecutionContext,\n ExecutableBuilder,\n Result,\n ODataFieldResponse,\n ExecuteOptions,\n ConditionallyWithODataAnnotations,\n ExecuteMethodOptions,\n} from \"../types\";\nimport type {\n FMTable,\n InferSchemaOutputFromFMTable,\n ValidExpandTarget,\n ExtractTableName,\n ValidateNoContainerFields,\n} from \"../orm/table\";\nimport { getTableName, getNavigationPaths } from \"../orm/table\";\nimport { safeJsonParse } from \"./sanitize-json\";\nimport { parseErrorResponse } from \"./error-parser\";\nimport { QueryBuilder } from \"./query-builder\";\nimport { type FFetchOptions } from \"@fetchkit/ffetch\";\nimport { isColumn, type Column } from \"../orm/column\";\nimport {\n type ExpandConfig,\n type ExpandedRelations,\n ExpandBuilder,\n resolveTableId,\n mergeExecuteOptions,\n processODataResponse,\n getSchemaFromTable,\n processSelectWithRenames,\n buildSelectExpandQueryString,\n createODataRequest,\n} from \"./builders/index\";\nimport {\n type ResolveExpandedRelations,\n type ResolveExpandType,\n} from \"./query/types\";\nimport { createLogger, InternalLogger, Logger } from \"../logger\";\n\n/**\n * Extract the value type from a Column.\n * This uses the phantom type stored in Column to get the actual value type.\n */\ntype ExtractColumnType<C> = C extends Column<infer T, any> ? T : never;\n\n/**\n * Map a select object to its return type.\n * For each key in the select object, extract the type from the corresponding Column.\n */\ntype MapSelectToReturnType<\n TSelect extends Record<string, Column<any, any, any, any>>,\n TSchema extends Record<string, any>,\n> = {\n [K in keyof TSelect]: ExtractColumnType<TSelect[K]>;\n};\n\n// Return type for RecordBuilder execute\nexport type RecordReturnType<\n Schema extends Record<string, any>,\n IsSingleField extends boolean,\n FieldColumn extends Column<any, any, any, any> | undefined,\n Selected extends\n | keyof Schema\n | Record<string, Column<any, any, ExtractTableName<FMTable<any, any>>>>,\n Expands extends ExpandedRelations,\n> = IsSingleField extends true\n ? FieldColumn extends Column<infer TOutput, any, any, any>\n ? TOutput\n : never\n : // Use tuple wrapping [Selected] extends [...] to prevent distribution over unions\n [Selected] extends [Record<string, Column<any, any, any, any>>]\n ? MapSelectToReturnType<Selected, Schema> &\n ResolveExpandedRelations<Expands>\n : // Use tuple wrapping to prevent distribution over union of keys\n [Selected] extends [keyof Schema]\n ? Pick<Schema, Selected> & ResolveExpandedRelations<Expands>\n : never;\n\nexport class RecordBuilder<\n Occ extends FMTable<any, any> = FMTable<any, any>,\n IsSingleField extends boolean = false,\n FieldColumn extends Column<any, any, any, any> | undefined = undefined,\n Selected extends\n | keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>\n | Record<\n string,\n Column<any, any, ExtractTableName<NonNullable<Occ>>>\n > = keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n Expands extends ExpandedRelations = {},\n> implements\n ExecutableBuilder<\n RecordReturnType<\n InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n IsSingleField,\n FieldColumn,\n Selected,\n Expands\n >\n >\n{\n private table: Occ;\n private databaseName: string;\n private context: ExecutionContext;\n private recordId: string | number;\n private operation?: \"getSingleField\" | \"navigate\";\n private operationParam?: string;\n private operationColumn?: Column<any, any, any, any>;\n private isNavigateFromEntitySet?: boolean;\n private navigateRelation?: string;\n private navigateSourceTableName?: string;\n\n private databaseUseEntityIds: boolean;\n\n // Properties for select/expand support\n private selectedFields?: string[];\n private expandConfigs: ExpandConfig[] = [];\n // Mapping from field names to output keys (for renamed fields in select)\n private fieldMapping?: Record<string, string>;\n\n private logger: InternalLogger;\n\n constructor(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n recordId: string | number;\n databaseUseEntityIds?: boolean;\n }) {\n this.table = config.occurrence;\n this.databaseName = config.databaseName;\n this.context = config.context;\n this.recordId = config.recordId;\n this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;\n this.logger = config.context?._getLogger?.() ?? createLogger();\n }\n\n /**\n * Helper to merge database-level useEntityIds with per-request options\n */\n private mergeExecuteOptions(\n options?: RequestInit & FFetchOptions & ExecuteOptions,\n ): RequestInit & FFetchOptions & { useEntityIds?: boolean } {\n return mergeExecuteOptions(options, this.databaseUseEntityIds);\n }\n\n /**\n * Gets the table ID (FMTID) if using entity IDs, otherwise returns the table name\n * @param useEntityIds - Optional override for entity ID usage\n */\n private getTableId(useEntityIds?: boolean): string {\n if (!this.table) {\n throw new Error(\"Table occurrence is required\");\n }\n return resolveTableId(\n this.table,\n getTableName(this.table),\n this.context,\n useEntityIds,\n );\n }\n\n /**\n * Creates a new RecordBuilder with modified configuration.\n * Used by select() to create new instances.\n */\n private cloneWithChanges<\n NewSelected extends\n | keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>\n | Record<\n string,\n Column<any, any, ExtractTableName<NonNullable<Occ>>>\n > = Selected,\n >(changes: {\n selectedFields?: string[];\n fieldMapping?: Record<string, string>;\n }): RecordBuilder<Occ, false, FieldColumn, NewSelected, Expands> {\n const newBuilder = new RecordBuilder<\n Occ,\n false,\n FieldColumn,\n NewSelected,\n Expands\n >({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n recordId: this.recordId,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n newBuilder.selectedFields = changes.selectedFields ?? this.selectedFields;\n newBuilder.fieldMapping = changes.fieldMapping ?? this.fieldMapping;\n newBuilder.expandConfigs = [...this.expandConfigs];\n // Preserve navigation context\n newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;\n newBuilder.navigateRelation = this.navigateRelation;\n newBuilder.navigateSourceTableName = this.navigateSourceTableName;\n newBuilder.operationColumn = this.operationColumn;\n return newBuilder;\n }\n\n getSingleField<\n TColumn extends Column<any, any, ExtractTableName<NonNullable<Occ>>, any>,\n >(\n column: TColumn,\n ): RecordBuilder<\n Occ,\n true,\n TColumn,\n keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n {}\n > {\n // Runtime validation: ensure column is from the correct table\n const tableName = getTableName(this.table);\n if (!column.isFromTable(tableName)) {\n throw new Error(\n `Column ${column.toString()} is not from table ${tableName}`,\n );\n }\n\n const newBuilder = new RecordBuilder<\n Occ,\n true,\n TColumn,\n keyof InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n {}\n >({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n recordId: this.recordId,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n newBuilder.operation = \"getSingleField\";\n newBuilder.operationColumn = column;\n newBuilder.operationParam = column.getFieldIdentifier(\n this.databaseUseEntityIds,\n );\n // Preserve navigation context\n newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;\n newBuilder.navigateRelation = this.navigateRelation;\n newBuilder.navigateSourceTableName = this.navigateSourceTableName;\n return newBuilder;\n }\n\n /**\n * Select fields using column references.\n * Allows renaming fields by using different keys in the object.\n * Container fields cannot be selected and will cause a type error.\n *\n * @example\n * db.from(contacts).get(\"uuid\").select({\n * name: contacts.name,\n * userEmail: contacts.email // renamed!\n * })\n *\n * @param fields - Object mapping output keys to column references (container fields excluded)\n * @returns RecordBuilder with updated selected fields\n */\n select<\n TSelect extends Record<\n string,\n Column<any, any, ExtractTableName<Occ>, false>\n >,\n >(fields: TSelect): RecordBuilder<Occ, false, FieldColumn, TSelect, Expands> {\n const tableName = getTableName(this.table);\n const { selectedFields, fieldMapping } = processSelectWithRenames(\n fields,\n tableName,\n this.logger,\n );\n\n return this.cloneWithChanges({\n selectedFields,\n fieldMapping:\n Object.keys(fieldMapping).length > 0 ? fieldMapping : undefined,\n }) as any;\n }\n\n /**\n * Expand a navigation property to include related records.\n * Supports nested select, filter, orderBy, and expand operations.\n *\n * @example\n * ```typescript\n * // Simple expand with FMTable object\n * const contact = await db.from(contacts).get(\"uuid\").expand(users).execute();\n *\n * // Expand with select\n * const contact = await db.from(contacts).get(\"uuid\")\n * .expand(users, b => b.select({ username: users.username, email: users.email }))\n * .execute();\n * ```\n */\n expand<\n TargetTable extends FMTable<any, any>,\n TSelected extends\n | keyof InferSchemaOutputFromFMTable<TargetTable>\n | Record<\n string,\n Column<any, any, ExtractTableName<TargetTable>>\n > = keyof InferSchemaOutputFromFMTable<TargetTable>,\n TNestedExpands extends ExpandedRelations = {},\n >(\n targetTable: ValidExpandTarget<Occ, TargetTable>,\n callback?: (\n builder: QueryBuilder<\n TargetTable,\n keyof InferSchemaOutputFromFMTable<TargetTable>,\n false,\n false,\n {}\n >,\n ) => QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands>,\n ): RecordBuilder<\n Occ,\n false,\n FieldColumn,\n Selected,\n Expands & {\n [K in ExtractTableName<TargetTable>]: {\n schema: InferSchemaOutputFromFMTable<TargetTable>;\n selected: TSelected;\n nested: TNestedExpands;\n };\n }\n > {\n // Create new builder with updated types\n const newBuilder = new RecordBuilder<\n Occ,\n false,\n FieldColumn,\n Selected,\n any\n >({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n recordId: this.recordId,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n\n // Copy existing state\n newBuilder.selectedFields = this.selectedFields;\n newBuilder.fieldMapping = this.fieldMapping;\n newBuilder.expandConfigs = [...this.expandConfigs];\n newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;\n newBuilder.navigateRelation = this.navigateRelation;\n newBuilder.navigateSourceTableName = this.navigateSourceTableName;\n newBuilder.operationColumn = this.operationColumn;\n\n // Use ExpandBuilder.processExpand to handle the expand logic\n const expandBuilder = new ExpandBuilder(\n this.databaseUseEntityIds,\n this.logger,\n );\n type TargetBuilder = QueryBuilder<\n TargetTable,\n keyof InferSchemaOutputFromFMTable<TargetTable>,\n false,\n false,\n {}\n >;\n const expandConfig = expandBuilder.processExpand<\n TargetTable,\n TargetBuilder\n >(\n targetTable,\n this.table ?? undefined,\n callback as ((builder: TargetBuilder) => TargetBuilder) | undefined,\n () =>\n new QueryBuilder<TargetTable>({\n occurrence: targetTable,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n }),\n );\n\n newBuilder.expandConfigs.push(expandConfig);\n return newBuilder as any;\n }\n\n navigate<TargetTable extends FMTable<any, any>>(\n targetTable: ValidExpandTarget<Occ, TargetTable>,\n ): QueryBuilder<\n TargetTable,\n keyof InferSchemaOutputFromFMTable<TargetTable>,\n false,\n false\n > {\n // Extract name and validate\n const relationName = getTableName(targetTable);\n\n // Runtime validation: Check if relation name is in navigationPaths\n if (this.table) {\n const navigationPaths = getNavigationPaths(this.table);\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 QueryBuilder with target table\n const builder = new QueryBuilder<TargetTable>({\n occurrence: targetTable,\n databaseName: this.databaseName,\n context: this.context,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n\n // Store the navigation info - we'll use it in execute\n // Use relation name as-is (entity ID handling is done in QueryBuilder)\n const relationId = relationName;\n\n // If this RecordBuilder came from a navigated EntitySet, we need to preserve that base path\n let sourceTableName: string;\n let baseRelation: string | undefined;\n if (\n this.isNavigateFromEntitySet &&\n this.navigateSourceTableName &&\n this.navigateRelation\n ) {\n // Build the base path: /sourceTable/relation('recordId')/newRelation\n sourceTableName = this.navigateSourceTableName;\n baseRelation = this.navigateRelation;\n } else {\n // Normal record navigation: /tableName('recordId')/relation\n // Use table ID if available, otherwise table name\n if (!this.table) {\n throw new Error(\"Table occurrence is required for navigation\");\n }\n sourceTableName = resolveTableId(\n this.table,\n getTableName(this.table),\n this.context,\n this.databaseUseEntityIds,\n );\n }\n\n (builder as any).navigation = {\n recordId: this.recordId,\n relation: relationId,\n sourceTableName,\n baseRelation,\n };\n\n return builder;\n }\n\n /**\n * Builds the complete query string including $select and $expand parameters.\n */\n private buildQueryString(): string {\n return buildSelectExpandQueryString({\n selectedFields: this.selectedFields,\n expandConfigs: this.expandConfigs,\n table: this.table,\n useEntityIds: this.databaseUseEntityIds,\n logger: this.logger,\n });\n }\n\n async execute<EO extends ExecuteOptions>(\n options?: ExecuteMethodOptions<EO>,\n ): Promise<\n Result<\n ConditionallyWithODataAnnotations<\n RecordReturnType<\n InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n IsSingleField,\n FieldColumn,\n Selected,\n Expands\n >,\n EO[\"includeODataAnnotations\"] extends true ? true : false\n >\n >\n > {\n let url: string;\n\n // Build the base URL depending on whether this came from a navigated EntitySet\n if (\n this.isNavigateFromEntitySet &&\n this.navigateSourceTableName &&\n this.navigateRelation\n ) {\n // From navigated EntitySet: /sourceTable/relation('recordId')\n url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;\n } else {\n // Normal record: /tableName('recordId') - use FMTID if configured\n const tableId = this.getTableId(\n options?.useEntityIds ?? this.databaseUseEntityIds,\n );\n url = `/${this.databaseName}/${tableId}('${this.recordId}')`;\n }\n\n if (this.operation === \"getSingleField\" && this.operationParam) {\n url += `/${this.operationParam}`;\n } else {\n // Add query string for select/expand (only when not getting a single field)\n const queryString = this.buildQueryString();\n url += queryString;\n }\n\n const mergedOptions = this.mergeExecuteOptions(options);\n const result = await this.context._makeRequest(url, mergedOptions);\n\n if (result.error) {\n return { data: undefined, error: result.error };\n }\n\n let response = result.data;\n\n // Handle single field operation\n if (this.operation === \"getSingleField\") {\n // Single field returns a JSON object with @context and value\n // The type is extracted from the Column stored in FieldColumn generic\n const fieldResponse = response as ODataFieldResponse<any>;\n return { data: fieldResponse.value as any, error: undefined };\n }\n\n // Use shared response processor\n const expandBuilder = new ExpandBuilder(\n mergedOptions.useEntityIds ?? false,\n this.logger,\n );\n const expandValidationConfigs = expandBuilder.buildValidationConfigs(\n this.expandConfigs,\n );\n\n return processODataResponse(response, {\n table: this.table,\n schema: getSchemaFromTable(this.table),\n singleMode: \"exact\",\n selectedFields: this.selectedFields,\n expandValidationConfigs,\n skipValidation: options?.skipValidation,\n useEntityIds: mergedOptions.useEntityIds,\n fieldMapping: this.fieldMapping,\n });\n }\n\n getRequestConfig(): { method: string; url: string; body?: any } {\n let url: string;\n\n // Build the base URL depending on whether this came from a navigated EntitySet\n if (\n this.isNavigateFromEntitySet &&\n this.navigateSourceTableName &&\n this.navigateRelation\n ) {\n // From navigated EntitySet: /sourceTable/relation('recordId')\n url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;\n } else {\n // For batch operations, use database-level setting (no per-request override available here)\n const tableId = this.getTableId(this.databaseUseEntityIds);\n url = `/${this.databaseName}/${tableId}('${this.recordId}')`;\n }\n\n if (this.operation === \"getSingleField\" && this.operationColumn) {\n // Use the column's getFieldIdentifier to support entity IDs\n url += `/${this.operationColumn.getFieldIdentifier(\n this.databaseUseEntityIds,\n )}`;\n } else if (this.operation === \"getSingleField\" && this.operationParam) {\n // Fallback for backwards compatibility (shouldn't happen in normal flow)\n url += `/${this.operationParam}`;\n } else {\n // Add query string for select/expand (only when not getting a single field)\n const queryString = this.buildQueryString();\n url += queryString;\n }\n\n return {\n method: \"GET\",\n url,\n };\n }\n\n /**\n * Returns the query string for this record builder (for testing purposes).\n */\n getQueryString(): string {\n let path: string;\n\n // Build the path depending on navigation context\n if (\n this.isNavigateFromEntitySet &&\n this.navigateSourceTableName &&\n this.navigateRelation\n ) {\n path = `/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;\n } else {\n // Use getTableId to respect entity ID settings (same as getRequestConfig)\n const tableId = this.getTableId(this.databaseUseEntityIds);\n path = `/${tableId}('${this.recordId}')`;\n }\n\n if (this.operation === \"getSingleField\" && this.operationColumn) {\n return `${path}/${this.operationColumn.getFieldIdentifier(\n this.databaseUseEntityIds,\n )}`;\n } else if (this.operation === \"getSingleField\" && this.operationParam) {\n // Fallback for backwards compatibility (shouldn't happen in normal flow)\n return `${path}/${this.operationParam}`;\n }\n\n const queryString = this.buildQueryString();\n return `${path}${queryString}`;\n }\n\n toRequest(baseUrl: string, options?: ExecuteOptions): Request {\n const config = this.getRequestConfig();\n return createODataRequest(baseUrl, config, options);\n }\n\n async processResponse(\n response: Response,\n options?: ExecuteOptions,\n ): Promise<\n Result<\n RecordReturnType<\n InferSchemaOutputFromFMTable<NonNullable<Occ>>,\n IsSingleField,\n FieldColumn,\n Selected,\n Expands\n >\n >\n > {\n // Check for error responses (important for batch operations)\n if (!response.ok) {\n const tableName = this.table ? getTableName(this.table) : \"unknown\";\n const error = await parseErrorResponse(\n response,\n response.url || `/${this.databaseName}/${tableName}`,\n );\n return { data: undefined, error };\n }\n\n // Use safeJsonParse to handle FileMaker's invalid JSON with unquoted ? values\n const rawResponse = await safeJsonParse(response);\n\n // Handle single field operation\n if (this.operation === \"getSingleField\") {\n // Single field returns a JSON object with @context and value\n // The type is extracted from the Column stored in FieldColumn generic\n const fieldResponse = rawResponse as ODataFieldResponse<any>;\n return { data: fieldResponse.value as any, error: undefined };\n }\n\n // Use shared response processor\n const mergedOptions = mergeExecuteOptions(\n options,\n this.databaseUseEntityIds,\n );\n const expandBuilder = new ExpandBuilder(\n mergedOptions.useEntityIds ?? false,\n this.logger,\n );\n const expandValidationConfigs = expandBuilder.buildValidationConfigs(\n this.expandConfigs,\n );\n\n return processODataResponse(rawResponse, {\n table: this.table,\n schema: getSchemaFromTable(this.table),\n singleMode: \"exact\",\n selectedFields: this.selectedFields,\n expandValidationConfigs,\n skipValidation: options?.skipValidation,\n useEntityIds: mergedOptions.useEntityIds,\n fieldMapping: this.fieldMapping,\n });\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proofkit/fmodata",
3
- "version": "0.1.0-alpha.18",
3
+ "version": "0.1.0-alpha.19",
4
4
  "description": "FileMaker OData API client",
5
5
  "repository": "git@github.com:proofgeist/proofkit.git",
6
6
  "author": "Eric <37158449+eluce2@users.noreply.github.com>",
@@ -60,13 +60,18 @@ export class ExpandBuilder {
60
60
  : [String(config.options.select)]
61
61
  : undefined;
62
62
 
63
+ // Recursively build validation configs for nested expands
64
+ const nestedExpands = config.nestedExpandConfigs
65
+ ? this.buildValidationConfigs(config.nestedExpandConfigs)
66
+ : undefined;
67
+
63
68
  return {
64
69
  relation: config.relation,
65
70
  targetSchema,
66
71
  targetTable,
67
72
  table: targetTable,
68
73
  selectedFields,
69
- nestedExpands: undefined,
74
+ nestedExpands,
70
75
  };
71
76
  });
72
77
  }
@@ -121,11 +126,10 @@ export class ExpandBuilder {
121
126
  }
122
127
 
123
128
  // If the configured builder has nested expands, we need to include them
124
- if ((configuredBuilder as any).expandConfigs?.length > 0) {
129
+ const nestedExpandConfigs = (configuredBuilder as any).expandConfigs;
130
+ if (nestedExpandConfigs?.length > 0) {
125
131
  // Build nested expand string from the configured builder's expand configs
126
- const nestedExpandString = this.buildExpandString(
127
- (configuredBuilder as any).expandConfigs,
128
- );
132
+ const nestedExpandString = this.buildExpandString(nestedExpandConfigs);
129
133
  if (nestedExpandString) {
130
134
  // Add nested expand to options
131
135
  expandOptions.expand = nestedExpandString as any;
@@ -136,6 +140,7 @@ export class ExpandBuilder {
136
140
  relation: relationName,
137
141
  options: expandOptions,
138
142
  targetTable,
143
+ nestedExpandConfigs: nestedExpandConfigs?.length > 0 ? nestedExpandConfigs : undefined,
139
144
  };
140
145
  } else {
141
146
  // Simple expand without callback - apply defaultSelect if available
@@ -9,6 +9,7 @@ export type ExpandConfig = {
9
9
  relation: string;
10
10
  options?: Partial<QueryOptions<any>>;
11
11
  targetTable?: FMTable<any, any>;
12
+ nestedExpandConfigs?: ExpandConfig[];
12
13
  };
13
14
 
14
15
  /**
@@ -452,16 +452,26 @@ export class QueryBuilder<
452
452
  return this;
453
453
  }
454
454
 
455
- expand<TargetTable extends FMTable<any, any>>(
455
+ expand<
456
+ TargetTable extends FMTable<any, any>,
457
+ TSelected extends
458
+ | keyof InferSchemaOutputFromFMTable<TargetTable>
459
+ | Record<
460
+ string,
461
+ Column<any, any, ExtractTableName<TargetTable>>
462
+ > = keyof InferSchemaOutputFromFMTable<TargetTable>,
463
+ TNestedExpands extends ExpandedRelations = {},
464
+ >(
456
465
  targetTable: ValidExpandTarget<Occ, TargetTable>,
457
466
  callback?: (
458
467
  builder: QueryBuilder<
459
468
  TargetTable,
460
469
  keyof InferSchemaOutputFromFMTable<TargetTable>,
461
470
  false,
462
- false
471
+ false,
472
+ {}
463
473
  >,
464
- ) => QueryBuilder<TargetTable, any, any, any, any>,
474
+ ) => QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands>,
465
475
  ): QueryBuilder<
466
476
  Occ,
467
477
  Selected,
@@ -470,7 +480,8 @@ export class QueryBuilder<
470
480
  Expands & {
471
481
  [K in ExtractTableName<TargetTable>]: {
472
482
  schema: InferSchemaOutputFromFMTable<TargetTable>;
473
- selected: keyof InferSchemaOutputFromFMTable<TargetTable>;
483
+ selected: TSelected;
484
+ nested: TNestedExpands;
474
485
  };
475
486
  }
476
487
  > {
@@ -479,7 +490,8 @@ export class QueryBuilder<
479
490
  TargetTable,
480
491
  keyof InferSchemaOutputFromFMTable<TargetTable>,
481
492
  false,
482
- false
493
+ false,
494
+ {}
483
495
  >;
484
496
  const expandConfig = this.expandBuilder.processExpand<
485
497
  TargetTable,
@@ -487,7 +499,7 @@ export class QueryBuilder<
487
499
  >(
488
500
  targetTable,
489
501
  this.occurrence,
490
- callback,
502
+ callback as ((builder: TargetBuilder) => TargetBuilder) | undefined,
491
503
  () =>
492
504
  new QueryBuilder<TargetTable>({
493
505
  occurrence: targetTable,
@@ -24,7 +24,10 @@ export type ExpandConfig = {
24
24
  };
25
25
 
26
26
  // Type to represent expanded relations
27
- export type ExpandedRelations = Record<string, { schema: any; selected: any }>;
27
+ export type ExpandedRelations = Record<
28
+ string,
29
+ { schema: any; selected: any; nested?: ExpandedRelations }
30
+ >;
28
31
 
29
32
  /**
30
33
  * Extract the value type from a Column.
@@ -44,6 +47,29 @@ type MapSelectToReturnType<
44
47
  [K in keyof TSelect]: ExtractColumnType<TSelect[K]>;
45
48
  };
46
49
 
50
+ /**
51
+ * Helper: Resolve a single expand's return type, including nested expands
52
+ */
53
+ export type ResolveExpandType<
54
+ Exp extends { schema: any; selected: any; nested?: ExpandedRelations },
55
+ > = // Handle the selected fields
56
+ (Exp["selected"] extends Record<string, Column<any, any, any, any>>
57
+ ? MapSelectToReturnType<Exp["selected"], Exp["schema"]>
58
+ : Exp["selected"] extends keyof Exp["schema"]
59
+ ? Pick<Exp["schema"], Exp["selected"]>
60
+ : Exp["schema"]) &
61
+ // Recursively handle nested expands
62
+ (Exp["nested"] extends ExpandedRelations
63
+ ? ResolveExpandedRelations<Exp["nested"]>
64
+ : {});
65
+
66
+ /**
67
+ * Helper: Resolve all expanded relations recursively
68
+ */
69
+ export type ResolveExpandedRelations<Exps extends ExpandedRelations> = {
70
+ [K in keyof Exps]: ResolveExpandType<Exps[K]>[];
71
+ };
72
+
47
73
  export type QueryReturnType<
48
74
  T extends Record<string, any>,
49
75
  Selected extends keyof T | Record<string, Column<any, any, any, any>>,
@@ -55,49 +81,19 @@ export type QueryReturnType<
55
81
  : // Use tuple wrapping [Selected] extends [...] to prevent distribution over unions
56
82
  [Selected] extends [Record<string, Column<any, any, any, any>>]
57
83
  ? SingleMode extends "exact"
58
- ? MapSelectToReturnType<Selected, T> & {
59
- [K in keyof Expands]: Pick<
60
- Expands[K]["schema"],
61
- Expands[K]["selected"]
62
- >[];
63
- }
84
+ ? MapSelectToReturnType<Selected, T> & ResolveExpandedRelations<Expands>
64
85
  : SingleMode extends "maybe"
65
86
  ?
66
- | (MapSelectToReturnType<Selected, T> & {
67
- [K in keyof Expands]: Pick<
68
- Expands[K]["schema"],
69
- Expands[K]["selected"]
70
- >[];
71
- })
87
+ | (MapSelectToReturnType<Selected, T> &
88
+ ResolveExpandedRelations<Expands>)
72
89
  | null
73
- : (MapSelectToReturnType<Selected, T> & {
74
- [K in keyof Expands]: Pick<
75
- Expands[K]["schema"],
76
- Expands[K]["selected"]
77
- >[];
78
- })[]
90
+ : (MapSelectToReturnType<Selected, T> &
91
+ ResolveExpandedRelations<Expands>)[]
79
92
  : // Use tuple wrapping to prevent distribution over union of keys
80
93
  [Selected] extends [keyof T]
81
94
  ? SingleMode extends "exact"
82
- ? Pick<T, Selected> & {
83
- [K in keyof Expands]: Pick<
84
- Expands[K]["schema"],
85
- Expands[K]["selected"]
86
- >[];
87
- }
95
+ ? Pick<T, Selected> & ResolveExpandedRelations<Expands>
88
96
  : SingleMode extends "maybe"
89
- ?
90
- | (Pick<T, Selected> & {
91
- [K in keyof Expands]: Pick<
92
- Expands[K]["schema"],
93
- Expands[K]["selected"]
94
- >[];
95
- })
96
- | null
97
- : (Pick<T, Selected> & {
98
- [K in keyof Expands]: Pick<
99
- Expands[K]["schema"],
100
- Expands[K]["selected"]
101
- >[];
102
- })[]
97
+ ? (Pick<T, Selected> & ResolveExpandedRelations<Expands>) | null
98
+ : (Pick<T, Selected> & ResolveExpandedRelations<Expands>)[]
103
99
  : never;
@@ -32,6 +32,10 @@ import {
32
32
  buildSelectExpandQueryString,
33
33
  createODataRequest,
34
34
  } from "./builders/index";
35
+ import {
36
+ type ResolveExpandedRelations,
37
+ type ResolveExpandType,
38
+ } from "./query/types";
35
39
  import { createLogger, InternalLogger, Logger } from "../logger";
36
40
 
37
41
  /**
@@ -66,20 +70,11 @@ export type RecordReturnType<
66
70
  : never
67
71
  : // Use tuple wrapping [Selected] extends [...] to prevent distribution over unions
68
72
  [Selected] extends [Record<string, Column<any, any, any, any>>]
69
- ? MapSelectToReturnType<Selected, Schema> & {
70
- [K in keyof Expands]: Pick<
71
- Expands[K]["schema"],
72
- Expands[K]["selected"]
73
- >[];
74
- }
73
+ ? MapSelectToReturnType<Selected, Schema> &
74
+ ResolveExpandedRelations<Expands>
75
75
  : // Use tuple wrapping to prevent distribution over union of keys
76
76
  [Selected] extends [keyof Schema]
77
- ? Pick<Schema, Selected> & {
78
- [K in keyof Expands]: Pick<
79
- Expands[K]["schema"],
80
- Expands[K]["selected"]
81
- >[];
82
- }
77
+ ? Pick<Schema, Selected> & ResolveExpandedRelations<Expands>
83
78
  : never;
84
79
 
85
80
  export class RecordBuilder<
@@ -297,16 +292,26 @@ export class RecordBuilder<
297
292
  * .execute();
298
293
  * ```
299
294
  */
300
- expand<TargetTable extends FMTable<any, any>>(
295
+ expand<
296
+ TargetTable extends FMTable<any, any>,
297
+ TSelected extends
298
+ | keyof InferSchemaOutputFromFMTable<TargetTable>
299
+ | Record<
300
+ string,
301
+ Column<any, any, ExtractTableName<TargetTable>>
302
+ > = keyof InferSchemaOutputFromFMTable<TargetTable>,
303
+ TNestedExpands extends ExpandedRelations = {},
304
+ >(
301
305
  targetTable: ValidExpandTarget<Occ, TargetTable>,
302
306
  callback?: (
303
307
  builder: QueryBuilder<
304
308
  TargetTable,
305
309
  keyof InferSchemaOutputFromFMTable<TargetTable>,
306
310
  false,
307
- false
311
+ false,
312
+ {}
308
313
  >,
309
- ) => QueryBuilder<TargetTable, any, any, any, any>,
314
+ ) => QueryBuilder<TargetTable, TSelected, any, any, TNestedExpands>,
310
315
  ): RecordBuilder<
311
316
  Occ,
312
317
  false,
@@ -315,7 +320,8 @@ export class RecordBuilder<
315
320
  Expands & {
316
321
  [K in ExtractTableName<TargetTable>]: {
317
322
  schema: InferSchemaOutputFromFMTable<TargetTable>;
318
- selected: keyof InferSchemaOutputFromFMTable<TargetTable>;
323
+ selected: TSelected;
324
+ nested: TNestedExpands;
319
325
  };
320
326
  }
321
327
  > {
@@ -352,7 +358,8 @@ export class RecordBuilder<
352
358
  TargetTable,
353
359
  keyof InferSchemaOutputFromFMTable<TargetTable>,
354
360
  false,
355
- false
361
+ false,
362
+ {}
356
363
  >;
357
364
  const expandConfig = expandBuilder.processExpand<
358
365
  TargetTable,
@@ -360,7 +367,7 @@ export class RecordBuilder<
360
367
  >(
361
368
  targetTable,
362
369
  this.table ?? undefined,
363
- callback,
370
+ callback as ((builder: TargetBuilder) => TargetBuilder) | undefined,
364
371
  () =>
365
372
  new QueryBuilder<TargetTable>({
366
373
  occurrence: targetTable,