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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +2 -2
  2. package/dist/esm/client/builders/expand-builder.d.ts +3 -1
  3. package/dist/esm/client/builders/expand-builder.js +3 -2
  4. package/dist/esm/client/builders/expand-builder.js.map +1 -1
  5. package/dist/esm/client/builders/query-string-builder.d.ts +2 -0
  6. package/dist/esm/client/builders/query-string-builder.js +1 -1
  7. package/dist/esm/client/builders/query-string-builder.js.map +1 -1
  8. package/dist/esm/client/builders/response-processor.d.ts +2 -0
  9. package/dist/esm/client/builders/response-processor.js +3 -2
  10. package/dist/esm/client/builders/response-processor.js.map +1 -1
  11. package/dist/esm/client/builders/select-mixin.d.ts +2 -1
  12. package/dist/esm/client/builders/select-mixin.js +2 -2
  13. package/dist/esm/client/builders/select-mixin.js.map +1 -1
  14. package/dist/esm/client/builders/select-utils.d.ts +10 -0
  15. package/dist/esm/client/builders/select-utils.js +10 -2
  16. package/dist/esm/client/builders/select-utils.js.map +1 -1
  17. package/dist/esm/client/entity-set.d.ts +2 -1
  18. package/dist/esm/client/entity-set.js +5 -2
  19. package/dist/esm/client/entity-set.js.map +1 -1
  20. package/dist/esm/client/error-parser.js.map +1 -1
  21. package/dist/esm/client/filemaker-odata.d.ts +8 -0
  22. package/dist/esm/client/filemaker-odata.js +14 -0
  23. package/dist/esm/client/filemaker-odata.js.map +1 -1
  24. package/dist/esm/client/query/query-builder.d.ts +1 -0
  25. package/dist/esm/client/query/query-builder.js +20 -9
  26. package/dist/esm/client/query/query-builder.js.map +1 -1
  27. package/dist/esm/client/query/response-processor.d.ts +2 -0
  28. package/dist/esm/client/record-builder.d.ts +9 -7
  29. package/dist/esm/client/record-builder.js +41 -10
  30. package/dist/esm/client/record-builder.js.map +1 -1
  31. package/dist/esm/index.d.ts +1 -0
  32. package/dist/esm/logger.d.ts +47 -0
  33. package/dist/esm/logger.js +72 -0
  34. package/dist/esm/logger.js.map +1 -0
  35. package/dist/esm/logger.test.d.ts +1 -0
  36. package/dist/esm/orm/operators.js +3 -1
  37. package/dist/esm/orm/operators.js.map +1 -1
  38. package/dist/esm/orm/table.d.ts +1 -1
  39. package/dist/esm/orm/table.js.map +1 -1
  40. package/dist/esm/types.d.ts +2 -0
  41. package/dist/esm/types.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/client/builders/expand-builder.ts +6 -2
  44. package/src/client/builders/query-string-builder.ts +3 -1
  45. package/src/client/builders/response-processor.ts +4 -1
  46. package/src/client/builders/select-mixin.ts +3 -1
  47. package/src/client/builders/select-utils.ts +25 -3
  48. package/src/client/entity-set.ts +7 -11
  49. package/src/client/error-parser.ts +4 -10
  50. package/src/client/filemaker-odata.ts +18 -0
  51. package/src/client/query/query-builder.ts +19 -6
  52. package/src/client/query/response-processor.ts +2 -0
  53. package/src/client/record-builder.ts +68 -28
  54. package/src/index.ts +2 -0
  55. package/src/logger.test.ts +34 -0
  56. package/src/logger.ts +140 -0
  57. package/src/orm/operators.ts +6 -1
  58. package/src/orm/table.ts +1 -1
  59. package/src/types.ts +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sources":["../../src/types.ts"],"sourcesContent":["import { type FFetchOptions } from \"@fetchkit/ffetch\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\n\nexport type Auth = { username: string; password: string } | { apiKey: string };\n\nexport interface ExecutableBuilder<T> {\n execute(): Promise<Result<T>>;\n getRequestConfig(): { method: string; url: string; body?: any };\n\n /**\n * Convert this builder to a native Request object for batch processing.\n * @param baseUrl - The base URL for the OData service\n * @param options - Optional execution options (e.g., includeODataAnnotations)\n * @returns A native Request object\n */\n toRequest(baseUrl: string, options?: ExecuteOptions): Request;\n\n /**\n * Process a raw Response object into a typed Result.\n * This allows builders to apply their own validation and transformation logic.\n * @param response - The native Response object from the batch operation\n * @param options - Optional execution options (e.g., skipValidation, includeODataAnnotations)\n * @returns A typed Result with the builder's expected return type\n */\n processResponse(\n response: Response,\n options?: ExecuteOptions,\n ): Promise<Result<T>>;\n}\n\nexport interface ExecutionContext {\n _makeRequest<T>(\n url: string,\n options?: RequestInit & FFetchOptions & { useEntityIds?: boolean },\n ): Promise<Result<T>>;\n _setUseEntityIds?(useEntityIds: boolean): void;\n _getUseEntityIds?(): boolean;\n _getBaseUrl?(): string;\n}\n\nexport type InferSchemaType<Schema extends Record<string, StandardSchemaV1>> = {\n [K in keyof Schema]: Schema[K] extends StandardSchemaV1<any, infer Output>\n ? Output\n : never;\n};\n\nexport type WithSystemFields<T> =\n T extends Record<string, any>\n ? T & {\n ROWID: number;\n ROWMODID: number;\n }\n : never;\n\n// Helper type to exclude system fields from a union of keys\nexport type ExcludeSystemFields<T extends keyof any> = Exclude<\n T,\n \"ROWID\" | \"ROWMODID\"\n>;\n\n// Helper type to omit system fields from an object type\nexport type OmitSystemFields<T> = Omit<T, \"ROWID\" | \"ROWMODID\">;\n\n// OData record metadata fields (present on each record)\nexport type ODataRecordMetadata = {\n \"@id\": string;\n \"@editLink\": string;\n};\n\n// OData response wrapper (top-level, internal use only)\nexport type ODataListResponse<T> = {\n \"@context\": string;\n value: (T & ODataRecordMetadata)[];\n};\n\nexport type ODataSingleResponse<T> = T &\n ODataRecordMetadata & {\n \"@context\": string;\n };\n\n// OData response for single field values\nexport type ODataFieldResponse<T> = {\n \"@context\": string;\n value: T;\n};\n\n// Result pattern for execute responses\nexport type Result<T, E = import(\"./errors\").FMODataErrorType> =\n | { data: T; error: undefined }\n | { data: undefined; error: E };\n\n// Batch operation result types\nexport type BatchItemResult<T> = {\n data: T | undefined;\n error: import(\"./errors\").FMODataErrorType | undefined;\n status: number; // HTTP status code (0 for truncated)\n};\n\nexport type BatchResult<T extends readonly any[]> = {\n results: { [K in keyof T]: BatchItemResult<T[K]> };\n successCount: number;\n errorCount: number;\n truncated: boolean;\n firstErrorIndex: number | null;\n};\n\n// Make specific keys required, rest optional\nexport type MakeFieldsRequired<T, Keys extends keyof T> = Partial<T> &\n Required<Pick<T, Keys>>;\n\n// Extract keys from schema where validator doesn't allow null/undefined (auto-required fields)\nexport type AutoRequiredKeys<Schema extends Record<string, StandardSchemaV1>> =\n {\n [K in keyof Schema]: Extract<\n StandardSchemaV1.InferOutput<Schema[K]>,\n null | undefined\n > extends never\n ? K\n : never;\n }[keyof Schema];\n\n// Helper type to compute excluded fields (readOnly fields + idField)\nexport type ExcludedFields<\n IdField extends keyof any | undefined,\n ReadOnly extends readonly any[],\n> = IdField extends keyof any ? IdField | ReadOnly[number] : ReadOnly[number];\n\n// Helper type for InsertData computation\ntype ComputeInsertData<\n Schema extends Record<string, StandardSchemaV1>,\n IdField extends keyof Schema | undefined,\n Required extends readonly any[],\n ReadOnly extends readonly any[],\n> = [Required[number]] extends [keyof InferSchemaType<Schema>]\n ? Required extends readonly (keyof InferSchemaType<Schema>)[]\n ? MakeFieldsRequired<\n Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,\n Exclude<\n AutoRequiredKeys<Schema> | Required[number],\n ExcludedFields<IdField, ReadOnly>\n >\n >\n : MakeFieldsRequired<\n Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,\n Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>>\n >\n : MakeFieldsRequired<\n Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,\n Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>>\n >;\n\nexport type ExecuteOptions = {\n includeODataAnnotations?: boolean;\n skipValidation?: boolean;\n /**\n * Overrides the default behavior of the database to use entity IDs (rather than field names) in THIS REQUEST ONLY\n */\n useEntityIds?: boolean;\n};\n\n/**\n * Type for the fetchHandler callback function.\n * This is a convenience type export that matches the fetchHandler signature in FFetchOptions.\n *\n * @example\n * ```typescript\n * import type { FetchHandler } from '@proofkit/fmodata';\n *\n * const myFetchHandler: FetchHandler = (input, init) => {\n * console.log('Custom fetch:', input);\n * return fetch(input, init);\n * };\n *\n * await query.execute({\n * fetchHandler: myFetchHandler\n * });\n * ```\n */\nexport type FetchHandler = (\n input: RequestInfo | URL,\n init?: RequestInit,\n) => Promise<Response>;\n\n/**\n * Combined type for execute() method options.\n *\n * Uses FFetchOptions from @fetchkit/ffetch to ensure proper type inference.\n * FFetchOptions is re-exported in the package to ensure type availability in consuming packages.\n */\nexport type ExecuteMethodOptions<EO extends ExecuteOptions = ExecuteOptions> =\n RequestInit & FFetchOptions & ExecuteOptions & EO;\n\n/**\n * Get the Accept header value based on includeODataAnnotations option\n * @param includeODataAnnotations - Whether to include OData annotations\n * @returns Accept header value\n */\nexport function getAcceptHeader(includeODataAnnotations?: boolean): string {\n return includeODataAnnotations === true\n ? \"application/json\"\n : \"application/json;odata.metadata=none\";\n}\n\nexport type ConditionallyWithODataAnnotations<\n T,\n IncludeODataAnnotations extends boolean,\n> = IncludeODataAnnotations extends true\n ? T & {\n \"@id\": string;\n \"@editLink\": string;\n }\n : T;\n\n// Helper type to extract schema from a FMTable\nexport type ExtractSchemaFromOccurrence<Occ> = Occ extends {\n baseTable: { schema: infer S };\n}\n ? S extends Record<string, StandardSchemaV1>\n ? S\n : Record<string, StandardSchemaV1>\n : Record<string, StandardSchemaV1>;\n\nexport type GenericFieldMetadata = {\n $Nullable?: boolean;\n \"@Index\"?: boolean;\n \"@Calculation\"?: boolean;\n \"@Summary\"?: boolean;\n \"@Global\"?: boolean;\n \"@Org.OData.Core.V1.Permissions\"?: \"Org.OData.Core.V1.Permission@Read\";\n};\n\nexport type StringFieldMetadata = GenericFieldMetadata & {\n $Type: \"Edm.String\";\n $DefaultValue?: \"USER\" | \"USERNAME\" | \"CURRENT_USER\";\n $MaxLength?: number;\n};\n\nexport type DecimalFieldMetadata = GenericFieldMetadata & {\n $Type: \"Edm.Decimal\";\n \"@AutoGenerated\"?: boolean;\n};\n\nexport type DateFieldMetadata = GenericFieldMetadata & {\n $Type: \"Edm.Date\";\n $DefaultValue?: \"CURDATE\" | \"CURRENT_DATE\";\n};\n\nexport type TimeOfDayFieldMetadata = GenericFieldMetadata & {\n $Type: \"Edm.TimeOfDay\";\n $DefaultValue?: \"CURTIME\" | \"CURRENT_TIME\";\n};\n\nexport type DateTimeOffsetFieldMetadata = GenericFieldMetadata & {\n $Type: \"Edm.Date\";\n $DefaultValue?: \"CURTIMESTAMP\" | \"CURRENT_TIMESTAMP\";\n \"@VersionId\"?: boolean;\n};\n\nexport type StreamFieldMetadata = {\n $Type: \"Edm.Stream\";\n $Nullable?: boolean;\n \"@EnclosedPath\": string;\n \"@ExternalOpenPath\": string;\n \"@ExternalSecurePath\"?: string;\n};\n\nexport type FieldMetadata =\n | StringFieldMetadata\n | DecimalFieldMetadata\n | DateFieldMetadata\n | TimeOfDayFieldMetadata\n | DateTimeOffsetFieldMetadata\n | StreamFieldMetadata;\n\nexport type EntityType = {\n $Kind: \"EntityType\";\n $Key: string[];\n} & Record<string, FieldMetadata>;\n\nexport type EntitySet = {\n $Kind: \"EntitySet\";\n $Type: string;\n};\n\nexport type Metadata = Record<string, EntityType | EntitySet>;\n"],"names":[],"mappings":"AAqMO,SAAS,gBAAgB,yBAA2C;AAClE,SAAA,4BAA4B,OAC/B,qBACA;AACN;"}
1
+ {"version":3,"file":"types.js","sources":["../../src/types.ts"],"sourcesContent":["import { type FFetchOptions } from \"@fetchkit/ffetch\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { InternalLogger } from \"./logger\";\n\nexport type Auth = { username: string; password: string } | { apiKey: string };\n\nexport interface ExecutableBuilder<T> {\n execute(): Promise<Result<T>>;\n getRequestConfig(): { method: string; url: string; body?: any };\n\n /**\n * Convert this builder to a native Request object for batch processing.\n * @param baseUrl - The base URL for the OData service\n * @param options - Optional execution options (e.g., includeODataAnnotations)\n * @returns A native Request object\n */\n toRequest(baseUrl: string, options?: ExecuteOptions): Request;\n\n /**\n * Process a raw Response object into a typed Result.\n * This allows builders to apply their own validation and transformation logic.\n * @param response - The native Response object from the batch operation\n * @param options - Optional execution options (e.g., skipValidation, includeODataAnnotations)\n * @returns A typed Result with the builder's expected return type\n */\n processResponse(\n response: Response,\n options?: ExecuteOptions,\n ): Promise<Result<T>>;\n}\n\nexport interface ExecutionContext {\n _makeRequest<T>(\n url: string,\n options?: RequestInit & FFetchOptions & { useEntityIds?: boolean },\n ): Promise<Result<T>>;\n _setUseEntityIds?(useEntityIds: boolean): void;\n _getUseEntityIds?(): boolean;\n _getBaseUrl?(): string;\n _getLogger?(): InternalLogger;\n}\n\nexport type InferSchemaType<Schema extends Record<string, StandardSchemaV1>> = {\n [K in keyof Schema]: Schema[K] extends StandardSchemaV1<any, infer Output>\n ? Output\n : never;\n};\n\nexport type WithSystemFields<T> =\n T extends Record<string, any>\n ? T & {\n ROWID: number;\n ROWMODID: number;\n }\n : never;\n\n// Helper type to exclude system fields from a union of keys\nexport type ExcludeSystemFields<T extends keyof any> = Exclude<\n T,\n \"ROWID\" | \"ROWMODID\"\n>;\n\n// Helper type to omit system fields from an object type\nexport type OmitSystemFields<T> = Omit<T, \"ROWID\" | \"ROWMODID\">;\n\n// OData record metadata fields (present on each record)\nexport type ODataRecordMetadata = {\n \"@id\": string;\n \"@editLink\": string;\n};\n\n// OData response wrapper (top-level, internal use only)\nexport type ODataListResponse<T> = {\n \"@context\": string;\n value: (T & ODataRecordMetadata)[];\n};\n\nexport type ODataSingleResponse<T> = T &\n ODataRecordMetadata & {\n \"@context\": string;\n };\n\n// OData response for single field values\nexport type ODataFieldResponse<T> = {\n \"@context\": string;\n value: T;\n};\n\n// Result pattern for execute responses\nexport type Result<T, E = import(\"./errors\").FMODataErrorType> =\n | { data: T; error: undefined }\n | { data: undefined; error: E };\n\n// Batch operation result types\nexport type BatchItemResult<T> = {\n data: T | undefined;\n error: import(\"./errors\").FMODataErrorType | undefined;\n status: number; // HTTP status code (0 for truncated)\n};\n\nexport type BatchResult<T extends readonly any[]> = {\n results: { [K in keyof T]: BatchItemResult<T[K]> };\n successCount: number;\n errorCount: number;\n truncated: boolean;\n firstErrorIndex: number | null;\n};\n\n// Make specific keys required, rest optional\nexport type MakeFieldsRequired<T, Keys extends keyof T> = Partial<T> &\n Required<Pick<T, Keys>>;\n\n// Extract keys from schema where validator doesn't allow null/undefined (auto-required fields)\nexport type AutoRequiredKeys<Schema extends Record<string, StandardSchemaV1>> =\n {\n [K in keyof Schema]: Extract<\n StandardSchemaV1.InferOutput<Schema[K]>,\n null | undefined\n > extends never\n ? K\n : never;\n }[keyof Schema];\n\n// Helper type to compute excluded fields (readOnly fields + idField)\nexport type ExcludedFields<\n IdField extends keyof any | undefined,\n ReadOnly extends readonly any[],\n> = IdField extends keyof any ? IdField | ReadOnly[number] : ReadOnly[number];\n\n// Helper type for InsertData computation\ntype ComputeInsertData<\n Schema extends Record<string, StandardSchemaV1>,\n IdField extends keyof Schema | undefined,\n Required extends readonly any[],\n ReadOnly extends readonly any[],\n> = [Required[number]] extends [keyof InferSchemaType<Schema>]\n ? Required extends readonly (keyof InferSchemaType<Schema>)[]\n ? MakeFieldsRequired<\n Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,\n Exclude<\n AutoRequiredKeys<Schema> | Required[number],\n ExcludedFields<IdField, ReadOnly>\n >\n >\n : MakeFieldsRequired<\n Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,\n Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>>\n >\n : MakeFieldsRequired<\n Omit<InferSchemaType<Schema>, ExcludedFields<IdField, ReadOnly>>,\n Exclude<AutoRequiredKeys<Schema>, ExcludedFields<IdField, ReadOnly>>\n >;\n\nexport type ExecuteOptions = {\n includeODataAnnotations?: boolean;\n skipValidation?: boolean;\n /**\n * Overrides the default behavior of the database to use entity IDs (rather than field names) in THIS REQUEST ONLY\n */\n useEntityIds?: boolean;\n};\n\n/**\n * Type for the fetchHandler callback function.\n * This is a convenience type export that matches the fetchHandler signature in FFetchOptions.\n *\n * @example\n * ```typescript\n * import type { FetchHandler } from '@proofkit/fmodata';\n *\n * const myFetchHandler: FetchHandler = (input, init) => {\n * console.log('Custom fetch:', input);\n * return fetch(input, init);\n * };\n *\n * await query.execute({\n * fetchHandler: myFetchHandler\n * });\n * ```\n */\nexport type FetchHandler = (\n input: RequestInfo | URL,\n init?: RequestInit,\n) => Promise<Response>;\n\n/**\n * Combined type for execute() method options.\n *\n * Uses FFetchOptions from @fetchkit/ffetch to ensure proper type inference.\n * FFetchOptions is re-exported in the package to ensure type availability in consuming packages.\n */\nexport type ExecuteMethodOptions<EO extends ExecuteOptions = ExecuteOptions> =\n RequestInit & FFetchOptions & ExecuteOptions & EO;\n\n/**\n * Get the Accept header value based on includeODataAnnotations option\n * @param includeODataAnnotations - Whether to include OData annotations\n * @returns Accept header value\n */\nexport function getAcceptHeader(includeODataAnnotations?: boolean): string {\n return includeODataAnnotations === true\n ? \"application/json\"\n : \"application/json;odata.metadata=none\";\n}\n\nexport type ConditionallyWithODataAnnotations<\n T,\n IncludeODataAnnotations extends boolean,\n> = IncludeODataAnnotations extends true\n ? T & {\n \"@id\": string;\n \"@editLink\": string;\n }\n : T;\n\n// Helper type to extract schema from a FMTable\nexport type ExtractSchemaFromOccurrence<Occ> = Occ extends {\n baseTable: { schema: infer S };\n}\n ? S extends Record<string, StandardSchemaV1>\n ? S\n : Record<string, StandardSchemaV1>\n : Record<string, StandardSchemaV1>;\n\nexport type GenericFieldMetadata = {\n $Nullable?: boolean;\n \"@Index\"?: boolean;\n \"@Calculation\"?: boolean;\n \"@Summary\"?: boolean;\n \"@Global\"?: boolean;\n \"@Org.OData.Core.V1.Permissions\"?: \"Org.OData.Core.V1.Permission@Read\";\n};\n\nexport type StringFieldMetadata = GenericFieldMetadata & {\n $Type: \"Edm.String\";\n $DefaultValue?: \"USER\" | \"USERNAME\" | \"CURRENT_USER\";\n $MaxLength?: number;\n};\n\nexport type DecimalFieldMetadata = GenericFieldMetadata & {\n $Type: \"Edm.Decimal\";\n \"@AutoGenerated\"?: boolean;\n};\n\nexport type DateFieldMetadata = GenericFieldMetadata & {\n $Type: \"Edm.Date\";\n $DefaultValue?: \"CURDATE\" | \"CURRENT_DATE\";\n};\n\nexport type TimeOfDayFieldMetadata = GenericFieldMetadata & {\n $Type: \"Edm.TimeOfDay\";\n $DefaultValue?: \"CURTIME\" | \"CURRENT_TIME\";\n};\n\nexport type DateTimeOffsetFieldMetadata = GenericFieldMetadata & {\n $Type: \"Edm.Date\";\n $DefaultValue?: \"CURTIMESTAMP\" | \"CURRENT_TIMESTAMP\";\n \"@VersionId\"?: boolean;\n};\n\nexport type StreamFieldMetadata = {\n $Type: \"Edm.Stream\";\n $Nullable?: boolean;\n \"@EnclosedPath\": string;\n \"@ExternalOpenPath\": string;\n \"@ExternalSecurePath\"?: string;\n};\n\nexport type FieldMetadata =\n | StringFieldMetadata\n | DecimalFieldMetadata\n | DateFieldMetadata\n | TimeOfDayFieldMetadata\n | DateTimeOffsetFieldMetadata\n | StreamFieldMetadata;\n\nexport type EntityType = {\n $Kind: \"EntityType\";\n $Key: string[];\n} & Record<string, FieldMetadata>;\n\nexport type EntitySet = {\n $Kind: \"EntitySet\";\n $Type: string;\n};\n\nexport type Metadata = Record<string, EntityType | EntitySet>;\n"],"names":[],"mappings":"AAuMO,SAAS,gBAAgB,yBAA2C;AAClE,SAAA,4BAA4B,OAC/B,qBACA;AACN;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proofkit/fmodata",
3
- "version": "0.1.0-alpha.16",
3
+ "version": "0.1.0-alpha.18",
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>",
@@ -11,6 +11,7 @@ import type { ExpandValidationConfig } from "../../validation";
11
11
  import type { ExpandConfig } from "./shared-types";
12
12
  import { formatSelectFields } from "./select-utils";
13
13
  import { getDefaultSelectFields } from "./default-select";
14
+ import { InternalLogger } from "../../logger";
14
15
 
15
16
  /**
16
17
  * Builds OData expand query strings and validation configs.
@@ -18,7 +19,10 @@ import { getDefaultSelectFields } from "./default-select";
18
19
  * when using entity IDs.
19
20
  */
20
21
  export class ExpandBuilder {
21
- constructor(private useEntityIds: boolean) {}
22
+ constructor(
23
+ private useEntityIds: boolean,
24
+ private logger: InternalLogger,
25
+ ) {}
22
26
 
23
27
  /**
24
28
  * Builds OData $expand query string from expand configurations.
@@ -90,7 +94,7 @@ export class ExpandBuilder {
90
94
  if (sourceTable) {
91
95
  const navigationPaths = getNavigationPaths(sourceTable);
92
96
  if (navigationPaths && !navigationPaths.includes(relationName)) {
93
- console.warn(
97
+ this.logger.warn(
94
98
  `Cannot expand to "${relationName}". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(", ") : "none"}`,
95
99
  );
96
100
  }
@@ -2,6 +2,7 @@ import type { FMTable } from "../../orm/table";
2
2
  import { ExpandBuilder } from "./expand-builder";
3
3
  import type { ExpandConfig } from "./shared-types";
4
4
  import { formatSelectFields } from "./select-utils";
5
+ import { InternalLogger } from "../../logger";
5
6
 
6
7
  /**
7
8
  * Builds OData query string for $select and $expand parameters.
@@ -15,9 +16,10 @@ export function buildSelectExpandQueryString(config: {
15
16
  expandConfigs: ExpandConfig[];
16
17
  table?: FMTable<any, any>;
17
18
  useEntityIds: boolean;
19
+ logger: InternalLogger;
18
20
  }): string {
19
21
  const parts: string[] = [];
20
- const expandBuilder = new ExpandBuilder(config.useEntityIds);
22
+ const expandBuilder = new ExpandBuilder(config.useEntityIds, config.logger);
21
23
 
22
24
  // Build $select
23
25
  if (config.selectedFields && config.selectedFields.length > 0) {
@@ -7,6 +7,7 @@ import { RecordCountMismatchError } from "../../errors";
7
7
  import { getBaseTableConfig } from "../../orm/table";
8
8
  import { ExpandBuilder } from "./expand-builder";
9
9
  import type { ExpandConfig } from "./shared-types";
10
+ import { InternalLogger } from "../../logger";
10
11
 
11
12
  export interface ProcessResponseConfig {
12
13
  table?: FMTable<any, any>;
@@ -224,6 +225,7 @@ export async function processQueryResponse<T>(
224
225
  useEntityIds?: boolean;
225
226
  // Mapping from field names to output keys (for renamed fields in select)
226
227
  fieldMapping?: Record<string, string>;
228
+ logger: InternalLogger;
227
229
  },
228
230
  ): Promise<Result<any>> {
229
231
  const {
@@ -234,9 +236,10 @@ export async function processQueryResponse<T>(
234
236
  skipValidation,
235
237
  useEntityIds,
236
238
  fieldMapping,
239
+ logger,
237
240
  } = config;
238
241
 
239
- const expandBuilder = new ExpandBuilder(useEntityIds ?? false);
242
+ const expandBuilder = new ExpandBuilder(useEntityIds ?? false, logger);
240
243
  const expandValidationConfigs =
241
244
  expandBuilder.buildValidationConfigs(expandConfigs);
242
245
 
@@ -1,3 +1,4 @@
1
+ import { InternalLogger } from "../../logger";
1
2
  import { isColumn, type Column } from "../../orm/column";
2
3
 
3
4
  /**
@@ -31,6 +32,7 @@ export function processSelectFields(
31
32
  export function processSelectWithRenames<TTableName extends string>(
32
33
  fields: Record<string, Column<any, any, TTableName>>,
33
34
  tableName: string,
35
+ logger: InternalLogger,
34
36
  ): { selectedFields: string[]; fieldMapping: Record<string, string> } {
35
37
  const selectedFields: string[] = [];
36
38
  const fieldMapping: Record<string, string> = {};
@@ -44,7 +46,7 @@ export function processSelectWithRenames<TTableName extends string>(
44
46
 
45
47
  // Warn (not throw) on table mismatch for consistency
46
48
  if (column.tableName !== tableName) {
47
- console.warn(
49
+ logger.warn(
48
50
  `Column ${column.toString()} is from table "${column.tableName}", but query is for table "${tableName}"`,
49
51
  );
50
52
  }
@@ -1,6 +1,30 @@
1
1
  import type { FMTable } from "../../orm/table";
2
2
  import { transformFieldNamesArray } from "../../transform";
3
3
 
4
+ /**
5
+ * Determines if a field name needs to be quoted in OData queries.
6
+ * Per FileMaker docs: field names with special characters (spaces, underscores, etc.) must be quoted.
7
+ * Also quotes "id" as it's an OData reserved word.
8
+ * Entity IDs (FMFID:*, FMTID:*) are not quoted as they're identifiers, not field names.
9
+ *
10
+ * @param fieldName - The field name or identifier to check
11
+ * @returns true if the field name should be quoted in OData queries
12
+ */
13
+ export function needsFieldQuoting(fieldName: string): boolean {
14
+ // Entity IDs are identifiers and don't need quoting
15
+ if (fieldName.startsWith("FMFID:") || fieldName.startsWith("FMTID:")) {
16
+ return false;
17
+ }
18
+ // Always quote "id" as it's an OData reserved word
19
+ if (fieldName === "id") return true;
20
+ // Quote if field name contains spaces, underscores, or other special characters
21
+ return (
22
+ fieldName.includes(" ") ||
23
+ fieldName.includes("_") ||
24
+ !/^[a-zA-Z][a-zA-Z0-9]*$/.test(fieldName)
25
+ );
26
+ }
27
+
4
28
  /**
5
29
  * Formats select fields for use in OData query strings.
6
30
  * - Transforms field names to FMFIDs if using entity IDs
@@ -24,11 +48,9 @@ export function formatSelectFields(
24
48
 
25
49
  return transformedFields
26
50
  .map((field) => {
27
- if (field === "id") return `"id"`;
51
+ if (needsFieldQuoting(field)) return `"${field}"`;
28
52
  const encoded = encodeURIComponent(field);
29
53
  return encoded.replace(/%20/g, " ");
30
54
  })
31
55
  .join(",");
32
56
  }
33
-
34
-
@@ -1,4 +1,4 @@
1
- import type { ExecutionContext, InferSchemaType } from "../types";
1
+ import type { ExecutionContext } from "../types";
2
2
  import type { StandardSchemaV1 } from "@standard-schema/spec";
3
3
  import { QueryBuilder } from "./query/index";
4
4
  import { RecordBuilder } from "./record-builder";
@@ -9,25 +9,19 @@ import { Database } from "./database";
9
9
  import type {
10
10
  FMTable,
11
11
  InferSchemaOutputFromFMTable,
12
- InferInputSchemaFromFMTable,
13
12
  InsertDataFromFMTable,
14
13
  UpdateDataFromFMTable,
15
14
  ValidExpandTarget,
16
- ExtractTableName,
17
- FMTableWithColumns,
18
- InferFieldOutput,
19
15
  ColumnMap,
20
16
  } from "../orm/table";
21
17
  import {
22
18
  FMTable as FMTableClass,
23
19
  getDefaultSelect,
24
- getNavigationPaths,
25
20
  getTableName,
26
21
  getTableColumns,
27
- getTableFields,
28
22
  } from "../orm/table";
29
- import type { Column } from "../orm/column";
30
23
  import type { FieldBuilder } from "../orm/field-builders";
24
+ import { createLogger, InternalLogger } from "../logger";
31
25
 
32
26
  // Helper type to extract defaultSelect from an FMTable
33
27
  // Since TypeScript can't extract Symbol-indexed properties at the type level,
@@ -57,6 +51,7 @@ export class EntitySet<Occ extends FMTable<any, any>> {
57
51
  private navigateSourceTableName?: string;
58
52
  private navigateBasePath?: string; // Full base path for chained navigations
59
53
  private databaseUseEntityIds: boolean;
54
+ private logger: InternalLogger;
60
55
 
61
56
  constructor(config: {
62
57
  occurrence: Occ;
@@ -71,6 +66,7 @@ export class EntitySet<Occ extends FMTable<any, any>> {
71
66
  // Get useEntityIds from database if available, otherwise default to false
72
67
  this.databaseUseEntityIds =
73
68
  (config.database as any)?._useEntityIds ?? false;
69
+ this.logger = config.context?._getLogger?.() ?? createLogger();
74
70
  }
75
71
 
76
72
  // Type-only method to help TypeScript infer the schema from table
@@ -126,7 +122,7 @@ export class EntitySet<Occ extends FMTable<any, any>> {
126
122
  // This is equivalent to select(getTableColumns(occurrence))
127
123
  // Cast to the declared return type - runtime behavior handles the actual selection
128
124
  const allColumns = getTableColumns(
129
- this.occurrence as any,
125
+ this.occurrence,
130
126
  ) as ExtractColumnsFromOcc<Occ>;
131
127
  return builder.select(allColumns).top(1000) as QueryBuilder<
132
128
  Occ,
@@ -175,7 +171,7 @@ export class EntitySet<Occ extends FMTable<any, any>> {
175
171
  ): RecordBuilder<
176
172
  Occ,
177
173
  false,
178
- keyof InferSchemaOutputFromFMTable<Occ>,
174
+ undefined,
179
175
  keyof InferSchemaOutputFromFMTable<Occ>,
180
176
  {}
181
177
  > {
@@ -358,7 +354,7 @@ export class EntitySet<Occ extends FMTable<any, any>> {
358
354
  FMTableClass.Symbol.NavigationPaths
359
355
  ] as readonly string[];
360
356
  if (navigationPaths && !navigationPaths.includes(relationName)) {
361
- console.warn(
357
+ this.logger.warn(
362
358
  `Cannot navigate to "${relationName}". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(", ") : "none"}`,
363
359
  );
364
360
  }
@@ -21,8 +21,10 @@ export async function parseErrorResponse(
21
21
  url: string,
22
22
  ): Promise<FMODataErrorType> {
23
23
  // Try to parse error body if it's JSON
24
- let errorBody: { error?: { code?: string | number; message?: string } } | undefined;
25
-
24
+ let errorBody:
25
+ | { error?: { code?: string | number; message?: string } }
26
+ | undefined;
27
+
26
28
  try {
27
29
  if (response.headers.get("content-type")?.includes("application/json")) {
28
30
  errorBody = await safeJsonParse<typeof errorBody>(response);
@@ -52,11 +54,3 @@ export async function parseErrorResponse(
52
54
  // Fall back to generic HTTPError
53
55
  return new HTTPError(url, response.status, response.statusText, errorBody);
54
56
  }
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
@@ -17,17 +17,21 @@ import {
17
17
  import { Database } from "./database";
18
18
  import { safeJsonParse } from "./sanitize-json";
19
19
  import { get } from "es-toolkit/compat";
20
+ import { createLogger, type Logger, type InternalLogger } from "../logger";
20
21
 
21
22
  export class FMServerConnection implements ExecutionContext {
22
23
  private fetchClient: ReturnType<typeof createClient>;
23
24
  private serverUrl: string;
24
25
  private auth: Auth;
25
26
  private useEntityIds: boolean = false;
27
+ private logger: InternalLogger;
26
28
  constructor(config: {
27
29
  serverUrl: string;
28
30
  auth: Auth;
29
31
  fetchClientOptions?: FFetchOptions;
32
+ logger?: Logger;
30
33
  }) {
34
+ this.logger = createLogger(config.logger);
31
35
  this.fetchClient = createClient({
32
36
  retries: 0,
33
37
  ...config.fetchClientOptions,
@@ -67,6 +71,14 @@ export class FMServerConnection implements ExecutionContext {
67
71
  return `${this.serverUrl}${"apiKey" in this.auth ? `/otto` : ""}/fmi/odata/v4`;
68
72
  }
69
73
 
74
+ /**
75
+ * @internal
76
+ * Gets the logger instance
77
+ */
78
+ _getLogger(): InternalLogger {
79
+ return this.logger;
80
+ }
81
+
70
82
  /**
71
83
  * @internal
72
84
  */
@@ -74,6 +86,7 @@ export class FMServerConnection implements ExecutionContext {
74
86
  url: string,
75
87
  options?: RequestInit & FFetchOptions & { useEntityIds?: boolean },
76
88
  ): Promise<Result<T>> {
89
+ const logger = this._getLogger();
77
90
  const baseUrl = `${this.serverUrl}${"apiKey" in this.auth ? `/otto` : ""}/fmi/odata/v4`;
78
91
  const fullUrl = baseUrl + url;
79
92
 
@@ -94,6 +107,10 @@ export class FMServerConnection implements ExecutionContext {
94
107
  ...(options?.headers || {}),
95
108
  };
96
109
 
110
+ // Prepare loggableHeaders by omitting the Authorization key
111
+ const { Authorization, ...loggableHeaders } = headers;
112
+ logger.debug("Request headers:", loggableHeaders);
113
+
97
114
  // TEMPORARY WORKAROUND: Hopefully this feature will be fixed in the ffetch library
98
115
  // Extract fetchHandler and headers separately, only for tests where we're overriding the fetch handler per-request
99
116
  const fetchHandler = options?.fetchHandler;
@@ -116,6 +133,7 @@ export class FMServerConnection implements ExecutionContext {
116
133
  };
117
134
 
118
135
  const resp = await clientToUse(fullUrl, finalOptions);
136
+ logger.debug(`${finalOptions.method ?? "GET"} ${resp.status} ${fullUrl}`);
119
137
 
120
138
  // Handle HTTP errors
121
139
  if (!resp.ok) {
@@ -45,6 +45,7 @@ import {
45
45
  } from "../builders/index";
46
46
  import { QueryUrlBuilder, type NavigationConfig } from "./url-builder";
47
47
  import type { TypeSafeOrderBy, QueryReturnType } from "./types";
48
+ import { createLogger, InternalLogger } from "../../logger";
48
49
 
49
50
  // Re-export QueryReturnType for backward compatibility
50
51
  export type { QueryReturnType };
@@ -95,6 +96,7 @@ export class QueryBuilder<
95
96
  private urlBuilder: QueryUrlBuilder;
96
97
  // Mapping from field names to output keys (for renamed fields in select)
97
98
  private fieldMapping?: Record<string, string>;
99
+ private logger: InternalLogger;
98
100
 
99
101
  constructor(config: {
100
102
  occurrence: Occ;
@@ -105,8 +107,12 @@ export class QueryBuilder<
105
107
  this.occurrence = config.occurrence;
106
108
  this.databaseName = config.databaseName;
107
109
  this.context = config.context;
110
+ this.logger = config.context?._getLogger?.() ?? createLogger();
108
111
  this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;
109
- this.expandBuilder = new ExpandBuilder(this.databaseUseEntityIds);
112
+ this.expandBuilder = new ExpandBuilder(
113
+ this.databaseUseEntityIds,
114
+ this.logger,
115
+ );
110
116
  this.urlBuilder = new QueryUrlBuilder(
111
117
  this.databaseName,
112
118
  this.occurrence,
@@ -205,12 +211,16 @@ export class QueryBuilder<
205
211
  * @returns QueryBuilder with updated selected fields
206
212
  */
207
213
  select<
208
- TSelect extends Record<string, Column<any, any, ExtractTableName<Occ>, false>>,
214
+ TSelect extends Record<
215
+ string,
216
+ Column<any, any, ExtractTableName<Occ>, false>
217
+ >,
209
218
  >(fields: TSelect): QueryBuilder<Occ, TSelect, SingleMode, IsCount, Expands> {
210
219
  const tableName = getTableName(this.occurrence);
211
220
  const { selectedFields, fieldMapping } = processSelectWithRenames(
212
221
  fields,
213
222
  tableName,
223
+ this.logger,
214
224
  );
215
225
 
216
226
  return this.cloneWithChanges({
@@ -294,7 +304,7 @@ export class QueryBuilder<
294
304
  if (isOrderByExpression(arg)) {
295
305
  // Validate table match
296
306
  if (arg.column.tableName !== tableName) {
297
- console.warn(
307
+ this.logger.warn(
298
308
  `Column ${arg.column.toString()} is from table "${arg.column.tableName}", but query is for table "${tableName}"`,
299
309
  );
300
310
  }
@@ -306,7 +316,7 @@ export class QueryBuilder<
306
316
  } else if (isColumn(arg)) {
307
317
  // Validate table match
308
318
  if (arg.tableName !== tableName) {
309
- console.warn(
319
+ this.logger.warn(
310
320
  `Column ${arg.toString()} is from table "${arg.tableName}", but query is for table "${tableName}"`,
311
321
  );
312
322
  }
@@ -332,7 +342,7 @@ export class QueryBuilder<
332
342
  if (isOrderByExpression(orderBy)) {
333
343
  // Validate table match
334
344
  if (orderBy.column.tableName !== tableName) {
335
- console.warn(
345
+ this.logger.warn(
336
346
  `Column ${orderBy.column.toString()} is from table "${orderBy.column.tableName}", but query is for table "${tableName}"`,
337
347
  );
338
348
  }
@@ -348,7 +358,7 @@ export class QueryBuilder<
348
358
  if (isColumn(orderBy)) {
349
359
  // Validate table match
350
360
  if (orderBy.tableName !== tableName) {
351
- console.warn(
361
+ this.logger.warn(
352
362
  `Column ${orderBy.toString()} is from table "${orderBy.tableName}", but query is for table "${tableName}"`,
353
363
  );
354
364
  }
@@ -530,6 +540,7 @@ export class QueryBuilder<
530
540
  expandConfigs: this.expandConfigs,
531
541
  table: this.occurrence,
532
542
  useEntityIds: this.databaseUseEntityIds,
543
+ logger: this.logger,
533
544
  });
534
545
 
535
546
  // Append select/expand to existing query string
@@ -603,6 +614,7 @@ export class QueryBuilder<
603
614
  skipValidation: options?.skipValidation,
604
615
  useEntityIds: mergedOptions.useEntityIds,
605
616
  fieldMapping: this.fieldMapping,
617
+ logger: this.logger,
606
618
  });
607
619
  }
608
620
 
@@ -712,6 +724,7 @@ export class QueryBuilder<
712
724
  skipValidation: options?.skipValidation,
713
725
  useEntityIds: mergedOptions.useEntityIds,
714
726
  fieldMapping: this.fieldMapping,
727
+ logger: this.logger,
715
728
  });
716
729
  }
717
730
  }
@@ -8,6 +8,7 @@ import { validateListResponse, validateSingleResponse } from "../../validation";
8
8
  import type { ExpandValidationConfig } from "../../validation";
9
9
  import type { ExpandConfig } from "./expand-builder";
10
10
  import { FMTable as FMTableClass } from "../../orm/table";
11
+ import { InternalLogger } from "../../logger";
11
12
 
12
13
  /**
13
14
  * Configuration for processing query responses
@@ -21,6 +22,7 @@ export interface ProcessQueryResponseConfig<T> {
21
22
  useEntityIds?: boolean;
22
23
  // Mapping from field names to output keys (for renamed fields in select)
23
24
  fieldMapping?: Record<string, string>;
25
+ logger: InternalLogger;
24
26
  }
25
27
 
26
28
  /**