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

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 (35) hide show
  1. package/README.md +4 -4
  2. package/dist/esm/client/batch-builder.js.map +1 -1
  3. package/dist/esm/client/batch-request.js.map +1 -1
  4. package/dist/esm/client/builders/default-select.js.map +1 -1
  5. package/dist/esm/client/builders/expand-builder.js.map +1 -1
  6. package/dist/esm/client/builders/query-string-builder.js.map +1 -1
  7. package/dist/esm/client/builders/response-processor.js.map +1 -1
  8. package/dist/esm/client/builders/select-mixin.js.map +1 -1
  9. package/dist/esm/client/builders/select-utils.js.map +1 -1
  10. package/dist/esm/client/builders/table-utils.js.map +1 -1
  11. package/dist/esm/client/database.js.map +1 -1
  12. package/dist/esm/client/delete-builder.js.map +1 -1
  13. package/dist/esm/client/entity-set.js +18 -2
  14. package/dist/esm/client/entity-set.js.map +1 -1
  15. package/dist/esm/client/error-parser.js.map +1 -1
  16. package/dist/esm/client/filemaker-odata.js.map +1 -1
  17. package/dist/esm/client/insert-builder.js.map +1 -1
  18. package/dist/esm/client/query/query-builder.js.map +1 -1
  19. package/dist/esm/client/query/url-builder.js.map +1 -1
  20. package/dist/esm/client/record-builder.js.map +1 -1
  21. package/dist/esm/client/sanitize-json.js.map +1 -1
  22. package/dist/esm/client/schema-manager.js.map +1 -1
  23. package/dist/esm/client/update-builder.js.map +1 -1
  24. package/dist/esm/client/webhook-builder.js.map +1 -1
  25. package/dist/esm/errors.js.map +1 -1
  26. package/dist/esm/logger.js.map +1 -1
  27. package/dist/esm/orm/column.js.map +1 -1
  28. package/dist/esm/orm/field-builders.js.map +1 -1
  29. package/dist/esm/orm/operators.js.map +1 -1
  30. package/dist/esm/orm/table.js.map +1 -1
  31. package/dist/esm/transform.js.map +1 -1
  32. package/dist/esm/types.js.map +1 -1
  33. package/dist/esm/validation.js.map +1 -1
  34. package/package.json +16 -19
  35. package/src/client/entity-set.ts +22 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A strongly-typed FileMaker OData API client.
4
4
 
5
- ⚠️ WARNING: This library is in "alpha" status. It's still in active development and the API is subject to change. Feedback is welcome on the [community forum](https://community.ottomatic.cloud/c/proofkit/13) or on [GitHub](https://github.com/proofgeist/proofkit/issues).
5
+ ⚠️ This library is in **beta** status. The core API is stable but may still see minor changes before 1.0. Feedback is welcome on the [community forum](https://community.ottomatic.cloud/c/proofkit/13) or on [GitHub](https://github.com/proofgeist/proofkit/issues).
6
6
 
7
7
  Roadmap:
8
8
 
@@ -10,13 +10,13 @@ Roadmap:
10
10
  - [x] Batch operations
11
11
  - [ ] Automatically chunk requests into smaller batches (e.g. max 512 inserts per batch)
12
12
  - [x] Schema updates (add/update tables and fields)
13
- - [ ] Proper docs at proofkit.dev
14
- - [ ] @proofkit/typegen integration
13
+ - [x] Proper docs at proofkit.dev
14
+ - [x] @proofkit/typegen integration
15
15
 
16
16
  ## Installation
17
17
 
18
18
  ```bash
19
- pnpm add @proofkit/fmodata@alpha
19
+ pnpm add @proofkit/fmodata@beta
20
20
  ```
21
21
 
22
22
  ## Quick Start
@@ -1 +1 @@
1
- {"version":3,"file":"batch-builder.js","sources":["../../../src/client/batch-builder.ts"],"sourcesContent":["import { BatchTruncatedError } from \"../errors\";\nimport type {\n BatchItemResult,\n BatchResult,\n ExecutableBuilder,\n ExecuteMethodOptions,\n ExecuteOptions,\n ExecutionContext,\n Result,\n} from \"../types\";\nimport { formatBatchRequestFromNative, type ParsedBatchResponse, parseBatchResponse } from \"./batch-request\";\n\n/**\n * Helper type to extract result types from a tuple of ExecutableBuilders.\n * Uses a mapped type which TypeScript 4.1+ can handle for tuples.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\ntype ExtractTupleTypes<T extends readonly ExecutableBuilder<any>[]> = {\n [K in keyof T]: T[K] extends ExecutableBuilder<infer U> ? U : never;\n};\n\n/**\n * Converts a ParsedBatchResponse to a native Response object\n * @param parsed - The parsed batch response\n * @returns A native Response object\n */\nfunction parsedToResponse(parsed: ParsedBatchResponse): Response {\n const headers = new Headers(parsed.headers);\n\n // Handle null body\n if (parsed.body === null || parsed.body === undefined) {\n return new Response(null, {\n status: parsed.status,\n statusText: parsed.statusText,\n headers,\n });\n }\n\n // Convert body to string if it's not already\n const bodyString = typeof parsed.body === \"string\" ? parsed.body : JSON.stringify(parsed.body);\n\n // Handle 204 No Content status - it cannot have a body per HTTP spec\n // If FileMaker returns 204 with a body, treat it as 200\n let status = parsed.status;\n if (status === 204 && bodyString && bodyString.trim() !== \"\") {\n status = 200;\n }\n\n return new Response(status === 204 ? null : bodyString, {\n status,\n statusText: parsed.statusText,\n headers,\n });\n}\n\n/**\n * Builder for batch operations that allows multiple queries to be executed together\n * in a single transactional request.\n *\n * Note: BatchBuilder does not implement ExecutableBuilder because execute() returns\n * BatchResult instead of Result, which is a different return type structure.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\nexport class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]> {\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\n private readonly builders: ExecutableBuilder<any>[];\n private readonly databaseName: string;\n private readonly context: ExecutionContext;\n\n constructor(builders: Builders, databaseName: string, context: ExecutionContext) {\n // Convert readonly tuple to mutable array for dynamic additions\n this.builders = [...builders];\n // Store original tuple for type preservation\n this.databaseName = databaseName;\n this.context = context;\n }\n\n /**\n * Add a request to the batch dynamically.\n * This allows building up batch operations programmatically.\n *\n * @param builder - An executable builder to add to the batch\n * @returns This BatchBuilder for method chaining\n * @example\n * ```ts\n * const batch = db.batch([]);\n * batch.addRequest(db.from('contacts').list());\n * batch.addRequest(db.from('users').list());\n * const result = await batch.execute();\n * ```\n */\n addRequest<T>(builder: ExecutableBuilder<T>): this {\n this.builders.push(builder);\n return this;\n }\n\n /**\n * Get the request configuration for this batch operation.\n * This is used internally by the execution system.\n */\n // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value\n getRequestConfig(): { method: string; url: string; body?: any } {\n // Note: This method is kept for compatibility but batch operations\n // should use execute() directly which handles the full Request/Response flow\n return {\n method: \"POST\",\n url: `/${this.databaseName}/$batch`,\n body: undefined, // Body is constructed in execute()\n };\n }\n\n toRequest(baseUrl: string, _options?: ExecuteOptions): Request {\n // Batch operations are not designed to be nested, but we provide\n // a basic implementation for interface compliance\n const fullUrl = `${baseUrl}/${this.databaseName}/$batch`;\n return new Request(fullUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"multipart/mixed\",\n \"OData-Version\": \"4.0\",\n },\n });\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Generic return type for interface compliance\n processResponse(_response: Response, _options?: ExecuteOptions): Promise<Result<any>> {\n // This should not typically be called for batch operations\n // as they handle their own response processing\n return Promise.resolve({\n data: undefined,\n error: {\n name: \"NotImplementedError\",\n message: \"Batch operations handle response processing internally\",\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n });\n }\n\n /**\n * Execute the batch operation.\n *\n * @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)\n * @returns A BatchResult containing individual results for each operation\n */\n async execute<EO extends ExecuteOptions>(\n options?: ExecuteMethodOptions<EO>,\n ): Promise<BatchResult<ExtractTupleTypes<Builders>>> {\n const baseUrl = this.context._getBaseUrl?.();\n if (!baseUrl) {\n // Return BatchResult with all operations marked as failed\n const errorCount = this.builders.length;\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({\n data: undefined,\n error: {\n name: \"ConfigurationError\",\n message: \"Base URL not available - execution context must implement _getBaseUrl()\",\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n status: 0,\n }));\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount: 0,\n errorCount,\n truncated: false,\n firstErrorIndex: 0,\n };\n }\n\n try {\n // Convert builders to native Request objects\n const requests: Request[] = this.builders.map((builder) => builder.toRequest(baseUrl, options));\n\n // Format batch request (automatically groups mutations into changesets)\n const { body, boundary } = await formatBatchRequestFromNative(requests, baseUrl);\n\n // Execute the batch request\n const response = await this.context._makeRequest<string>(`/${this.databaseName}/$batch`, {\n ...options,\n method: \"POST\",\n headers: {\n ...options?.headers,\n \"Content-Type\": `multipart/mixed; boundary=${boundary}`,\n \"OData-Version\": \"4.0\",\n },\n body,\n });\n\n if (response.error) {\n // Return BatchResult with all operations marked as failed\n const errorCount = this.builders.length;\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({\n data: undefined,\n error: response.error,\n status: 0,\n }));\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount: 0,\n errorCount,\n truncated: false,\n firstErrorIndex: 0,\n };\n }\n\n // Extract the actual boundary from the response\n // FileMaker uses its own boundary, not the one we sent\n const firstLine = response.data.split(\"\\r\\n\")[0] || response.data.split(\"\\n\")[0] || \"\";\n const actualBoundary = firstLine.startsWith(\"--\") ? firstLine.substring(2) : boundary;\n\n // Parse the multipart response\n const contentTypeHeader = `multipart/mixed; boundary=${actualBoundary}`;\n const parsedResponses = parseBatchResponse(response.data, contentTypeHeader);\n\n // Process each response using the corresponding builder\n // Build BatchResult with per-item results\n type _ResultTuple = ExtractTupleTypes<Builders>;\n\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = [];\n let successCount = 0;\n let errorCount = 0;\n let firstErrorIndex: number | null = null;\n const truncated = parsedResponses.length < this.builders.length;\n\n // Process builders sequentially to preserve tuple order\n for (let i = 0; i < this.builders.length; i++) {\n const builder = this.builders[i];\n const parsed = parsedResponses[i];\n\n if (!parsed) {\n // Truncated - operation never executed\n const failedAtIndex = firstErrorIndex ?? i;\n results.push({\n data: undefined,\n error: new BatchTruncatedError(i, failedAtIndex),\n status: 0,\n });\n errorCount++;\n continue;\n }\n\n if (!builder) {\n // Should not happen, but handle gracefully\n results.push({\n data: undefined,\n error: {\n name: \"BatchError\",\n message: `Builder at index ${i} is undefined`,\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n status: parsed.status,\n });\n errorCount++;\n if (firstErrorIndex === null) {\n firstErrorIndex = i;\n }\n continue;\n }\n\n // Convert parsed response to native Response\n const nativeResponse = parsedToResponse(parsed);\n\n // Let the builder process its own response\n const result = await builder.processResponse(nativeResponse, options);\n\n if (result.error) {\n results.push({\n data: undefined,\n error: result.error,\n status: parsed.status,\n });\n errorCount++;\n if (firstErrorIndex === null) {\n firstErrorIndex = i;\n }\n } else {\n results.push({\n data: result.data,\n error: undefined,\n status: parsed.status,\n });\n successCount++;\n }\n }\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount,\n errorCount,\n truncated,\n firstErrorIndex,\n };\n } catch (err) {\n // On exception, return a BatchResult with all operations marked as failed\n const errorCount = this.builders.length;\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({\n data: undefined,\n error: {\n name: \"BatchError\",\n message: err instanceof Error ? err.message : \"Unknown error\",\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n status: 0,\n }));\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount: 0,\n errorCount,\n truncated: false,\n firstErrorIndex: 0,\n };\n }\n }\n}\n"],"names":["errorCount","results"],"mappings":"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"}
1
+ {"version":3,"file":"batch-builder.js","sources":["../../../src/client/batch-builder.ts"],"sourcesContent":["import { BatchTruncatedError } from \"../errors\";\nimport type {\n BatchItemResult,\n BatchResult,\n ExecutableBuilder,\n ExecuteMethodOptions,\n ExecuteOptions,\n ExecutionContext,\n Result,\n} from \"../types\";\nimport { formatBatchRequestFromNative, type ParsedBatchResponse, parseBatchResponse } from \"./batch-request\";\n\n/**\n * Helper type to extract result types from a tuple of ExecutableBuilders.\n * Uses a mapped type which TypeScript 4.1+ can handle for tuples.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\ntype ExtractTupleTypes<T extends readonly ExecutableBuilder<any>[]> = {\n [K in keyof T]: T[K] extends ExecutableBuilder<infer U> ? U : never;\n};\n\n/**\n * Converts a ParsedBatchResponse to a native Response object\n * @param parsed - The parsed batch response\n * @returns A native Response object\n */\nfunction parsedToResponse(parsed: ParsedBatchResponse): Response {\n const headers = new Headers(parsed.headers);\n\n // Handle null body\n if (parsed.body === null || parsed.body === undefined) {\n return new Response(null, {\n status: parsed.status,\n statusText: parsed.statusText,\n headers,\n });\n }\n\n // Convert body to string if it's not already\n const bodyString = typeof parsed.body === \"string\" ? parsed.body : JSON.stringify(parsed.body);\n\n // Handle 204 No Content status - it cannot have a body per HTTP spec\n // If FileMaker returns 204 with a body, treat it as 200\n let status = parsed.status;\n if (status === 204 && bodyString && bodyString.trim() !== \"\") {\n status = 200;\n }\n\n return new Response(status === 204 ? null : bodyString, {\n status,\n statusText: parsed.statusText,\n headers,\n });\n}\n\n/**\n * Builder for batch operations that allows multiple queries to be executed together\n * in a single transactional request.\n *\n * Note: BatchBuilder does not implement ExecutableBuilder because execute() returns\n * BatchResult instead of Result, which is a different return type structure.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\nexport class BatchBuilder<Builders extends readonly ExecutableBuilder<any>[]> {\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\n private readonly builders: ExecutableBuilder<any>[];\n private readonly databaseName: string;\n private readonly context: ExecutionContext;\n\n constructor(builders: Builders, databaseName: string, context: ExecutionContext) {\n // Convert readonly tuple to mutable array for dynamic additions\n this.builders = [...builders];\n // Store original tuple for type preservation\n this.databaseName = databaseName;\n this.context = context;\n }\n\n /**\n * Add a request to the batch dynamically.\n * This allows building up batch operations programmatically.\n *\n * @param builder - An executable builder to add to the batch\n * @returns This BatchBuilder for method chaining\n * @example\n * ```ts\n * const batch = db.batch([]);\n * batch.addRequest(db.from('contacts').list());\n * batch.addRequest(db.from('users').list());\n * const result = await batch.execute();\n * ```\n */\n addRequest<T>(builder: ExecutableBuilder<T>): this {\n this.builders.push(builder);\n return this;\n }\n\n /**\n * Get the request configuration for this batch operation.\n * This is used internally by the execution system.\n */\n // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value\n getRequestConfig(): { method: string; url: string; body?: any } {\n // Note: This method is kept for compatibility but batch operations\n // should use execute() directly which handles the full Request/Response flow\n return {\n method: \"POST\",\n url: `/${this.databaseName}/$batch`,\n body: undefined, // Body is constructed in execute()\n };\n }\n\n toRequest(baseUrl: string, _options?: ExecuteOptions): Request {\n // Batch operations are not designed to be nested, but we provide\n // a basic implementation for interface compliance\n const fullUrl = `${baseUrl}/${this.databaseName}/$batch`;\n return new Request(fullUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"multipart/mixed\",\n \"OData-Version\": \"4.0\",\n },\n });\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Generic return type for interface compliance\n processResponse(_response: Response, _options?: ExecuteOptions): Promise<Result<any>> {\n // This should not typically be called for batch operations\n // as they handle their own response processing\n return Promise.resolve({\n data: undefined,\n error: {\n name: \"NotImplementedError\",\n message: \"Batch operations handle response processing internally\",\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n });\n }\n\n /**\n * Execute the batch operation.\n *\n * @param options - Optional fetch options and batch-specific options (includes beforeRequest hook)\n * @returns A BatchResult containing individual results for each operation\n */\n async execute<EO extends ExecuteOptions>(\n options?: ExecuteMethodOptions<EO>,\n ): Promise<BatchResult<ExtractTupleTypes<Builders>>> {\n const baseUrl = this.context._getBaseUrl?.();\n if (!baseUrl) {\n // Return BatchResult with all operations marked as failed\n const errorCount = this.builders.length;\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({\n data: undefined,\n error: {\n name: \"ConfigurationError\",\n message: \"Base URL not available - execution context must implement _getBaseUrl()\",\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n status: 0,\n }));\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount: 0,\n errorCount,\n truncated: false,\n firstErrorIndex: 0,\n };\n }\n\n try {\n // Convert builders to native Request objects\n const requests: Request[] = this.builders.map((builder) => builder.toRequest(baseUrl, options));\n\n // Format batch request (automatically groups mutations into changesets)\n const { body, boundary } = await formatBatchRequestFromNative(requests, baseUrl);\n\n // Execute the batch request\n const response = await this.context._makeRequest<string>(`/${this.databaseName}/$batch`, {\n ...options,\n method: \"POST\",\n headers: {\n ...options?.headers,\n \"Content-Type\": `multipart/mixed; boundary=${boundary}`,\n \"OData-Version\": \"4.0\",\n },\n body,\n });\n\n if (response.error) {\n // Return BatchResult with all operations marked as failed\n const errorCount = this.builders.length;\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({\n data: undefined,\n error: response.error,\n status: 0,\n }));\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount: 0,\n errorCount,\n truncated: false,\n firstErrorIndex: 0,\n };\n }\n\n // Extract the actual boundary from the response\n // FileMaker uses its own boundary, not the one we sent\n const firstLine = response.data.split(\"\\r\\n\")[0] || response.data.split(\"\\n\")[0] || \"\";\n const actualBoundary = firstLine.startsWith(\"--\") ? firstLine.substring(2) : boundary;\n\n // Parse the multipart response\n const contentTypeHeader = `multipart/mixed; boundary=${actualBoundary}`;\n const parsedResponses = parseBatchResponse(response.data, contentTypeHeader);\n\n // Process each response using the corresponding builder\n // Build BatchResult with per-item results\n type _ResultTuple = ExtractTupleTypes<Builders>;\n\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = [];\n let successCount = 0;\n let errorCount = 0;\n let firstErrorIndex: number | null = null;\n const truncated = parsedResponses.length < this.builders.length;\n\n // Process builders sequentially to preserve tuple order\n for (let i = 0; i < this.builders.length; i++) {\n const builder = this.builders[i];\n const parsed = parsedResponses[i];\n\n if (!parsed) {\n // Truncated - operation never executed\n const failedAtIndex = firstErrorIndex ?? i;\n results.push({\n data: undefined,\n error: new BatchTruncatedError(i, failedAtIndex),\n status: 0,\n });\n errorCount++;\n continue;\n }\n\n if (!builder) {\n // Should not happen, but handle gracefully\n results.push({\n data: undefined,\n error: {\n name: \"BatchError\",\n message: `Builder at index ${i} is undefined`,\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n status: parsed.status,\n });\n errorCount++;\n if (firstErrorIndex === null) {\n firstErrorIndex = i;\n }\n continue;\n }\n\n // Convert parsed response to native Response\n const nativeResponse = parsedToResponse(parsed);\n\n // Let the builder process its own response\n const result = await builder.processResponse(nativeResponse, options);\n\n if (result.error) {\n results.push({\n data: undefined,\n error: result.error,\n status: parsed.status,\n });\n errorCount++;\n if (firstErrorIndex === null) {\n firstErrorIndex = i;\n }\n } else {\n results.push({\n data: result.data,\n error: undefined,\n status: parsed.status,\n });\n successCount++;\n }\n }\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount,\n errorCount,\n truncated,\n firstErrorIndex,\n };\n } catch (err) {\n // On exception, return a BatchResult with all operations marked as failed\n const errorCount = this.builders.length;\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any result type\n const results: BatchItemResult<any>[] = this.builders.map((_, _i) => ({\n data: undefined,\n error: {\n name: \"BatchError\",\n message: err instanceof Error ? err.message : \"Unknown error\",\n timestamp: new Date(),\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for error object\n } as any,\n status: 0,\n }));\n\n return {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for complex generic return type\n results: results as any,\n successCount: 0,\n errorCount,\n truncated: false,\n firstErrorIndex: 0,\n };\n }\n }\n}\n"],"names":["errorCount","results"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"batch-request.js","sources":["../../../src/client/batch-request.ts"],"sourcesContent":["/**\n * Batch Request Utilities\n *\n * Utilities for formatting and parsing OData batch requests using multipart/mixed format.\n * OData batch requests allow bundling multiple operations into a single HTTP request,\n * with support for transactional changesets.\n */\n\nconst BOUNDARY_REGEX = /boundary=([^;]+)/;\nconst HTTP_STATUS_LINE_REGEX = /HTTP\\/\\d\\.\\d\\s+(\\d+)\\s*(.*)/;\nconst CRLF_REGEX = /\\r\\n/;\nconst CHANGESET_CONTENT_TYPE_REGEX = /Content-Type: multipart\\/mixed;\\s*boundary=([^\\r\\n]+)/;\n\nexport interface RequestConfig {\n method: string;\n url: string;\n body?: string;\n headers?: Record<string, string>;\n}\n\nexport interface ParsedBatchResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response body type from OData API\n body: any;\n}\n\n/**\n * Generates a random boundary string for multipart requests\n * @param prefix - Prefix for the boundary (e.g., \"batch_\" or \"changeset_\")\n * @returns A boundary string with the prefix and 32 random hex characters\n */\nexport function generateBoundary(prefix = \"batch_\"): string {\n const randomHex = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join(\"\");\n return `${prefix}${randomHex}`;\n}\n\n/**\n * Converts a native Request object to RequestConfig\n * @param request - Native Request object\n * @returns RequestConfig object\n */\nasync function requestToConfig(request: Request): Promise<RequestConfig> {\n const headers: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headers[key] = value;\n });\n\n let body: string | undefined;\n if (request.body) {\n // Clone the request to read the body without consuming it\n const clonedRequest = request.clone();\n body = await clonedRequest.text();\n }\n\n return {\n method: request.method,\n url: request.url,\n body,\n headers,\n };\n}\n\n/**\n * Formats a single HTTP request for inclusion in a batch\n * @param request - The request configuration\n * @param baseUrl - The base URL to prepend to relative URLs\n * @returns Formatted request string with CRLF line endings\n *\n * Formatting rules for FileMaker OData:\n * - GET (no body): request line → blank → blank\n * - POST/PATCH (with body): request line → headers → blank → body (NO blank after!)\n */\nfunction formatSubRequest(request: RequestConfig, baseUrl: string): string {\n const lines: string[] = [];\n\n // Add required headers for sub-request\n lines.push(\"Content-Type: application/http\");\n lines.push(\"Content-Transfer-Encoding: binary\");\n lines.push(\"\"); // Empty line after multipart headers\n\n // Construct full URL (convert relative to absolute)\n const fullUrl = request.url.startsWith(\"http\") ? request.url : `${baseUrl}${request.url}`;\n\n // Add HTTP request line\n lines.push(`${request.method} ${fullUrl} HTTP/1.1`);\n\n // For requests with body, add headers\n if (request.body) {\n // Add request headers (excluding Authorization - it's in the outer request)\n if (request.headers) {\n for (const [key, value] of Object.entries(request.headers)) {\n if (key.toLowerCase() !== \"authorization\") {\n lines.push(`${key}: ${value}`);\n }\n }\n }\n\n // Check if Content-Type is already set\n const hasContentType =\n request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === \"content-type\");\n\n if (!hasContentType) {\n lines.push(\"Content-Type: application/json\");\n }\n\n // Add Content-Length (required for FileMaker to read the body)\n const hasContentLength =\n request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === \"content-length\");\n\n if (!hasContentLength) {\n lines.push(`Content-Length: ${request.body.length}`);\n }\n\n lines.push(\"\"); // Empty line between headers and body\n lines.push(request.body);\n // NO blank line after body - the boundary comes immediately\n } else {\n // For GET requests (no body), add TWO blank lines\n lines.push(\"\"); // First blank\n lines.push(\"\"); // Second blank\n }\n\n return lines.join(\"\\r\\n\");\n}\n\n/**\n * Formats a changeset containing multiple non-GET operations\n * @param requests - Array of request configurations (should be non-GET)\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param changesetBoundary - Boundary string for the changeset\n * @returns Formatted changeset string with CRLF line endings\n */\nfunction formatChangeset(requests: RequestConfig[], baseUrl: string, changesetBoundary: string): string {\n const lines: string[] = [];\n\n lines.push(`Content-Type: multipart/mixed; boundary=${changesetBoundary}`);\n lines.push(\"\"); // Empty line after headers\n\n // Add each request in the changeset\n for (const request of requests) {\n lines.push(`--${changesetBoundary}`);\n lines.push(formatSubRequest(request, baseUrl));\n }\n\n // Close the changeset\n lines.push(`--${changesetBoundary}--`);\n\n return lines.join(\"\\r\\n\");\n}\n\n/**\n * Formats multiple requests into a batch request body\n * @param requests - Array of request configurations\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param batchBoundary - Optional boundary string for the batch (generated if not provided)\n * @returns Object containing the formatted body and boundary\n */\nexport function formatBatchRequest(\n requests: RequestConfig[],\n baseUrl: string,\n batchBoundary?: string,\n): { body: string; boundary: string } {\n const boundary = batchBoundary || generateBoundary(\"batch_\");\n const lines: string[] = [];\n\n // Group requests: consecutive non-GET operations go into changesets\n let currentChangeset: RequestConfig[] | null = null;\n\n for (const request of requests) {\n if (request.method === \"GET\") {\n // GET operations break changesets and are added individually\n if (currentChangeset) {\n // Close and add the current changeset\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(formatChangeset(currentChangeset, baseUrl, changesetBoundary));\n currentChangeset = null;\n }\n\n // Add GET request\n lines.push(`--${boundary}`);\n lines.push(formatSubRequest(request, baseUrl));\n } else {\n // Non-GET operations: add to current changeset or create new one\n if (!currentChangeset) {\n currentChangeset = [];\n }\n currentChangeset.push(request);\n }\n }\n\n // Add any remaining changeset\n if (currentChangeset) {\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(formatChangeset(currentChangeset, baseUrl, changesetBoundary));\n }\n\n // Close the batch\n lines.push(`--${boundary}--`);\n\n return {\n body: lines.join(\"\\r\\n\"),\n boundary,\n };\n}\n\n/**\n * Formats multiple Request objects into a batch request body\n * Supports explicit changesets via Request arrays\n * @param requests - Array of Request objects or Request arrays (for explicit changesets)\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param batchBoundary - Optional boundary string for the batch (generated if not provided)\n * @returns Promise resolving to object containing the formatted body and boundary\n */\nexport async function formatBatchRequestFromNative(\n requests: Array<Request | Request[]>,\n baseUrl: string,\n batchBoundary?: string,\n): Promise<{ body: string; boundary: string }> {\n const boundary = batchBoundary || generateBoundary(\"batch_\");\n const lines: string[] = [];\n\n for (const item of requests) {\n if (Array.isArray(item)) {\n // Explicit changeset - array of Requests\n const changesetBoundary = generateBoundary(\"changeset_\");\n const changesetConfigs: RequestConfig[] = [];\n\n for (const request of item) {\n changesetConfigs.push(await requestToConfig(request));\n }\n\n lines.push(`--${boundary}`);\n lines.push(formatChangeset(changesetConfigs, baseUrl, changesetBoundary));\n } else {\n // Single request\n const config = await requestToConfig(item);\n\n if (config.method === \"GET\") {\n // GET requests are always individual\n lines.push(`--${boundary}`);\n lines.push(formatSubRequest(config, baseUrl));\n } else {\n // Non-GET operations wrapped in a changeset\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(formatChangeset([config], baseUrl, changesetBoundary));\n }\n }\n }\n\n // Close the batch\n lines.push(`--${boundary}--`);\n\n return {\n body: lines.join(\"\\r\\n\"),\n boundary,\n };\n}\n\n/**\n * Extracts the boundary from a Content-Type header\n * @param contentType - The Content-Type header value\n * @returns The boundary string, or null if not found\n */\nexport function extractBoundary(contentType: string): string | null {\n const match = contentType.match(BOUNDARY_REGEX);\n return match?.[1] ? match[1].trim() : null;\n}\n\n/**\n * Parses an HTTP response line (status line)\n * @param line - The HTTP status line (e.g., \"HTTP/1.1 200 OK\")\n * @returns Object containing status code and status text\n */\nfunction parseStatusLine(line: string): {\n status: number;\n statusText: string;\n} {\n const match = line.match(HTTP_STATUS_LINE_REGEX);\n if (!match?.[1]) {\n return { status: 0, statusText: \"\" };\n }\n return {\n status: Number.parseInt(match[1], 10),\n statusText: match[2]?.trim() || \"\",\n };\n}\n\n/**\n * Parses headers from an array of header lines\n * @param lines - Array of header lines\n * @returns Object containing parsed headers\n */\nfunction parseHeaders(lines: string[]): Record<string, string> {\n const headers: Record<string, string> = {};\n for (const line of lines) {\n const colonIndex = line.indexOf(\":\");\n if (colonIndex > 0) {\n const key = line.substring(0, colonIndex).trim();\n const value = line.substring(colonIndex + 1).trim();\n headers[key.toLowerCase()] = value;\n }\n }\n return headers;\n}\n\n/**\n * Parses a single HTTP response from a batch part\n * @param part - The raw HTTP response string\n * @returns Parsed response object\n */\nfunction parseHttpResponse(part: string): ParsedBatchResponse {\n const lines = part.split(CRLF_REGEX);\n\n // Find the HTTP status line (skip multipart headers)\n let statusLineIndex = -1;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line?.startsWith(\"HTTP/\")) {\n statusLineIndex = i;\n break;\n }\n }\n\n if (statusLineIndex === -1) {\n return {\n status: 0,\n statusText: \"Invalid response\",\n headers: {},\n body: null,\n };\n }\n\n const statusLine = lines[statusLineIndex];\n if (!statusLine) {\n return {\n status: 0,\n statusText: \"Invalid response\",\n headers: {},\n body: null,\n };\n }\n\n const { status, statusText } = parseStatusLine(statusLine);\n\n // Parse headers (between status line and empty line)\n const headerLines: string[] = [];\n let bodyStartIndex = lines.length; // Default to end of lines (no body)\n let foundEmptyLine = false;\n\n for (let i = statusLineIndex + 1; i < lines.length; i++) {\n const line = lines[i];\n if (line === \"\") {\n bodyStartIndex = i + 1;\n foundEmptyLine = true;\n break;\n }\n // Stop at boundary markers (for responses without bodies like 204)\n if (line?.startsWith(\"--\")) {\n break;\n }\n if (line) {\n headerLines.push(line);\n }\n }\n\n const headers = parseHeaders(headerLines);\n\n // Parse body (everything after the empty line, if there was one)\n let bodyText = \"\";\n if (foundEmptyLine && bodyStartIndex < lines.length) {\n const bodyLines = lines.slice(bodyStartIndex);\n // Stop at boundary markers\n const bodyLinesFiltered: string[] = [];\n for (const line of bodyLines) {\n if (line.startsWith(\"--\")) {\n break;\n }\n bodyLinesFiltered.push(line);\n }\n bodyText = bodyLinesFiltered.join(\"\\r\\n\").trim();\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response body type from OData API\n let body: any = null;\n if (bodyText) {\n try {\n body = JSON.parse(bodyText);\n } catch {\n // If not JSON, return as text\n body = bodyText;\n }\n }\n\n return {\n status,\n statusText,\n headers,\n body,\n };\n}\n\n/**\n * Parses a batch response into individual responses\n * @param responseText - The raw batch response text\n * @param contentType - The Content-Type header from the response\n * @returns Array of parsed responses in the same order as the request\n */\nexport function parseBatchResponse(responseText: string, contentType: string): ParsedBatchResponse[] {\n const boundary = extractBoundary(contentType);\n if (!boundary) {\n throw new Error(\"Could not extract boundary from Content-Type header\");\n }\n\n const results: ParsedBatchResponse[] = [];\n\n // Split by boundary (handle both --boundary and --boundary--)\n const boundaryPattern = `--${boundary}`;\n const parts = responseText.split(boundaryPattern);\n\n for (const part of parts) {\n const trimmedPart = part.trim();\n\n // Skip empty parts and the closing boundary marker\n if (!trimmedPart || trimmedPart === \"--\") {\n continue;\n }\n\n // Check if this part is a changeset (nested multipart)\n if (trimmedPart.includes(\"Content-Type: multipart/mixed\")) {\n // Extract the changeset boundary\n const changesetContentTypeMatch = trimmedPart.match(CHANGESET_CONTENT_TYPE_REGEX);\n if (changesetContentTypeMatch) {\n const changesetBoundary = changesetContentTypeMatch?.[1]?.trim();\n const changesetPattern = `--${changesetBoundary}`;\n const changesetParts = trimmedPart.split(changesetPattern);\n\n for (const changesetPart of changesetParts) {\n const trimmedChangesetPart = changesetPart.trim();\n if (!trimmedChangesetPart || trimmedChangesetPart === \"--\") {\n continue;\n }\n\n // Skip the changeset header\n if (trimmedChangesetPart.startsWith(\"Content-Type: multipart/mixed\")) {\n continue;\n }\n\n const response = parseHttpResponse(trimmedChangesetPart);\n if (response.status > 0) {\n results.push(response);\n }\n }\n }\n } else {\n // Regular response (not a changeset)\n const response = parseHttpResponse(trimmedPart);\n if (response.status > 0) {\n results.push(response);\n }\n }\n }\n\n return results;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"batch-request.js","sources":["../../../src/client/batch-request.ts"],"sourcesContent":["/**\n * Batch Request Utilities\n *\n * Utilities for formatting and parsing OData batch requests using multipart/mixed format.\n * OData batch requests allow bundling multiple operations into a single HTTP request,\n * with support for transactional changesets.\n */\n\nconst BOUNDARY_REGEX = /boundary=([^;]+)/;\nconst HTTP_STATUS_LINE_REGEX = /HTTP\\/\\d\\.\\d\\s+(\\d+)\\s*(.*)/;\nconst CRLF_REGEX = /\\r\\n/;\nconst CHANGESET_CONTENT_TYPE_REGEX = /Content-Type: multipart\\/mixed;\\s*boundary=([^\\r\\n]+)/;\n\nexport interface RequestConfig {\n method: string;\n url: string;\n body?: string;\n headers?: Record<string, string>;\n}\n\nexport interface ParsedBatchResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response body type from OData API\n body: any;\n}\n\n/**\n * Generates a random boundary string for multipart requests\n * @param prefix - Prefix for the boundary (e.g., \"batch_\" or \"changeset_\")\n * @returns A boundary string with the prefix and 32 random hex characters\n */\nexport function generateBoundary(prefix = \"batch_\"): string {\n const randomHex = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join(\"\");\n return `${prefix}${randomHex}`;\n}\n\n/**\n * Converts a native Request object to RequestConfig\n * @param request - Native Request object\n * @returns RequestConfig object\n */\nasync function requestToConfig(request: Request): Promise<RequestConfig> {\n const headers: Record<string, string> = {};\n request.headers.forEach((value, key) => {\n headers[key] = value;\n });\n\n let body: string | undefined;\n if (request.body) {\n // Clone the request to read the body without consuming it\n const clonedRequest = request.clone();\n body = await clonedRequest.text();\n }\n\n return {\n method: request.method,\n url: request.url,\n body,\n headers,\n };\n}\n\n/**\n * Formats a single HTTP request for inclusion in a batch\n * @param request - The request configuration\n * @param baseUrl - The base URL to prepend to relative URLs\n * @returns Formatted request string with CRLF line endings\n *\n * Formatting rules for FileMaker OData:\n * - GET (no body): request line → blank → blank\n * - POST/PATCH (with body): request line → headers → blank → body (NO blank after!)\n */\nfunction formatSubRequest(request: RequestConfig, baseUrl: string): string {\n const lines: string[] = [];\n\n // Add required headers for sub-request\n lines.push(\"Content-Type: application/http\");\n lines.push(\"Content-Transfer-Encoding: binary\");\n lines.push(\"\"); // Empty line after multipart headers\n\n // Construct full URL (convert relative to absolute)\n const fullUrl = request.url.startsWith(\"http\") ? request.url : `${baseUrl}${request.url}`;\n\n // Add HTTP request line\n lines.push(`${request.method} ${fullUrl} HTTP/1.1`);\n\n // For requests with body, add headers\n if (request.body) {\n // Add request headers (excluding Authorization - it's in the outer request)\n if (request.headers) {\n for (const [key, value] of Object.entries(request.headers)) {\n if (key.toLowerCase() !== \"authorization\") {\n lines.push(`${key}: ${value}`);\n }\n }\n }\n\n // Check if Content-Type is already set\n const hasContentType =\n request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === \"content-type\");\n\n if (!hasContentType) {\n lines.push(\"Content-Type: application/json\");\n }\n\n // Add Content-Length (required for FileMaker to read the body)\n const hasContentLength =\n request.headers && Object.keys(request.headers).some((k) => k.toLowerCase() === \"content-length\");\n\n if (!hasContentLength) {\n lines.push(`Content-Length: ${request.body.length}`);\n }\n\n lines.push(\"\"); // Empty line between headers and body\n lines.push(request.body);\n // NO blank line after body - the boundary comes immediately\n } else {\n // For GET requests (no body), add TWO blank lines\n lines.push(\"\"); // First blank\n lines.push(\"\"); // Second blank\n }\n\n return lines.join(\"\\r\\n\");\n}\n\n/**\n * Formats a changeset containing multiple non-GET operations\n * @param requests - Array of request configurations (should be non-GET)\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param changesetBoundary - Boundary string for the changeset\n * @returns Formatted changeset string with CRLF line endings\n */\nfunction formatChangeset(requests: RequestConfig[], baseUrl: string, changesetBoundary: string): string {\n const lines: string[] = [];\n\n lines.push(`Content-Type: multipart/mixed; boundary=${changesetBoundary}`);\n lines.push(\"\"); // Empty line after headers\n\n // Add each request in the changeset\n for (const request of requests) {\n lines.push(`--${changesetBoundary}`);\n lines.push(formatSubRequest(request, baseUrl));\n }\n\n // Close the changeset\n lines.push(`--${changesetBoundary}--`);\n\n return lines.join(\"\\r\\n\");\n}\n\n/**\n * Formats multiple requests into a batch request body\n * @param requests - Array of request configurations\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param batchBoundary - Optional boundary string for the batch (generated if not provided)\n * @returns Object containing the formatted body and boundary\n */\nexport function formatBatchRequest(\n requests: RequestConfig[],\n baseUrl: string,\n batchBoundary?: string,\n): { body: string; boundary: string } {\n const boundary = batchBoundary || generateBoundary(\"batch_\");\n const lines: string[] = [];\n\n // Group requests: consecutive non-GET operations go into changesets\n let currentChangeset: RequestConfig[] | null = null;\n\n for (const request of requests) {\n if (request.method === \"GET\") {\n // GET operations break changesets and are added individually\n if (currentChangeset) {\n // Close and add the current changeset\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(formatChangeset(currentChangeset, baseUrl, changesetBoundary));\n currentChangeset = null;\n }\n\n // Add GET request\n lines.push(`--${boundary}`);\n lines.push(formatSubRequest(request, baseUrl));\n } else {\n // Non-GET operations: add to current changeset or create new one\n if (!currentChangeset) {\n currentChangeset = [];\n }\n currentChangeset.push(request);\n }\n }\n\n // Add any remaining changeset\n if (currentChangeset) {\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(formatChangeset(currentChangeset, baseUrl, changesetBoundary));\n }\n\n // Close the batch\n lines.push(`--${boundary}--`);\n\n return {\n body: lines.join(\"\\r\\n\"),\n boundary,\n };\n}\n\n/**\n * Formats multiple Request objects into a batch request body\n * Supports explicit changesets via Request arrays\n * @param requests - Array of Request objects or Request arrays (for explicit changesets)\n * @param baseUrl - The base URL to prepend to relative URLs\n * @param batchBoundary - Optional boundary string for the batch (generated if not provided)\n * @returns Promise resolving to object containing the formatted body and boundary\n */\nexport async function formatBatchRequestFromNative(\n requests: Array<Request | Request[]>,\n baseUrl: string,\n batchBoundary?: string,\n): Promise<{ body: string; boundary: string }> {\n const boundary = batchBoundary || generateBoundary(\"batch_\");\n const lines: string[] = [];\n\n for (const item of requests) {\n if (Array.isArray(item)) {\n // Explicit changeset - array of Requests\n const changesetBoundary = generateBoundary(\"changeset_\");\n const changesetConfigs: RequestConfig[] = [];\n\n for (const request of item) {\n changesetConfigs.push(await requestToConfig(request));\n }\n\n lines.push(`--${boundary}`);\n lines.push(formatChangeset(changesetConfigs, baseUrl, changesetBoundary));\n } else {\n // Single request\n const config = await requestToConfig(item);\n\n if (config.method === \"GET\") {\n // GET requests are always individual\n lines.push(`--${boundary}`);\n lines.push(formatSubRequest(config, baseUrl));\n } else {\n // Non-GET operations wrapped in a changeset\n const changesetBoundary = generateBoundary(\"changeset_\");\n lines.push(`--${boundary}`);\n lines.push(formatChangeset([config], baseUrl, changesetBoundary));\n }\n }\n }\n\n // Close the batch\n lines.push(`--${boundary}--`);\n\n return {\n body: lines.join(\"\\r\\n\"),\n boundary,\n };\n}\n\n/**\n * Extracts the boundary from a Content-Type header\n * @param contentType - The Content-Type header value\n * @returns The boundary string, or null if not found\n */\nexport function extractBoundary(contentType: string): string | null {\n const match = contentType.match(BOUNDARY_REGEX);\n return match?.[1] ? match[1].trim() : null;\n}\n\n/**\n * Parses an HTTP response line (status line)\n * @param line - The HTTP status line (e.g., \"HTTP/1.1 200 OK\")\n * @returns Object containing status code and status text\n */\nfunction parseStatusLine(line: string): {\n status: number;\n statusText: string;\n} {\n const match = line.match(HTTP_STATUS_LINE_REGEX);\n if (!match?.[1]) {\n return { status: 0, statusText: \"\" };\n }\n return {\n status: Number.parseInt(match[1], 10),\n statusText: match[2]?.trim() || \"\",\n };\n}\n\n/**\n * Parses headers from an array of header lines\n * @param lines - Array of header lines\n * @returns Object containing parsed headers\n */\nfunction parseHeaders(lines: string[]): Record<string, string> {\n const headers: Record<string, string> = {};\n for (const line of lines) {\n const colonIndex = line.indexOf(\":\");\n if (colonIndex > 0) {\n const key = line.substring(0, colonIndex).trim();\n const value = line.substring(colonIndex + 1).trim();\n headers[key.toLowerCase()] = value;\n }\n }\n return headers;\n}\n\n/**\n * Parses a single HTTP response from a batch part\n * @param part - The raw HTTP response string\n * @returns Parsed response object\n */\nfunction parseHttpResponse(part: string): ParsedBatchResponse {\n const lines = part.split(CRLF_REGEX);\n\n // Find the HTTP status line (skip multipart headers)\n let statusLineIndex = -1;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n if (line?.startsWith(\"HTTP/\")) {\n statusLineIndex = i;\n break;\n }\n }\n\n if (statusLineIndex === -1) {\n return {\n status: 0,\n statusText: \"Invalid response\",\n headers: {},\n body: null,\n };\n }\n\n const statusLine = lines[statusLineIndex];\n if (!statusLine) {\n return {\n status: 0,\n statusText: \"Invalid response\",\n headers: {},\n body: null,\n };\n }\n\n const { status, statusText } = parseStatusLine(statusLine);\n\n // Parse headers (between status line and empty line)\n const headerLines: string[] = [];\n let bodyStartIndex = lines.length; // Default to end of lines (no body)\n let foundEmptyLine = false;\n\n for (let i = statusLineIndex + 1; i < lines.length; i++) {\n const line = lines[i];\n if (line === \"\") {\n bodyStartIndex = i + 1;\n foundEmptyLine = true;\n break;\n }\n // Stop at boundary markers (for responses without bodies like 204)\n if (line?.startsWith(\"--\")) {\n break;\n }\n if (line) {\n headerLines.push(line);\n }\n }\n\n const headers = parseHeaders(headerLines);\n\n // Parse body (everything after the empty line, if there was one)\n let bodyText = \"\";\n if (foundEmptyLine && bodyStartIndex < lines.length) {\n const bodyLines = lines.slice(bodyStartIndex);\n // Stop at boundary markers\n const bodyLinesFiltered: string[] = [];\n for (const line of bodyLines) {\n if (line.startsWith(\"--\")) {\n break;\n }\n bodyLinesFiltered.push(line);\n }\n bodyText = bodyLinesFiltered.join(\"\\r\\n\").trim();\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response body type from OData API\n let body: any = null;\n if (bodyText) {\n try {\n body = JSON.parse(bodyText);\n } catch {\n // If not JSON, return as text\n body = bodyText;\n }\n }\n\n return {\n status,\n statusText,\n headers,\n body,\n };\n}\n\n/**\n * Parses a batch response into individual responses\n * @param responseText - The raw batch response text\n * @param contentType - The Content-Type header from the response\n * @returns Array of parsed responses in the same order as the request\n */\nexport function parseBatchResponse(responseText: string, contentType: string): ParsedBatchResponse[] {\n const boundary = extractBoundary(contentType);\n if (!boundary) {\n throw new Error(\"Could not extract boundary from Content-Type header\");\n }\n\n const results: ParsedBatchResponse[] = [];\n\n // Split by boundary (handle both --boundary and --boundary--)\n const boundaryPattern = `--${boundary}`;\n const parts = responseText.split(boundaryPattern);\n\n for (const part of parts) {\n const trimmedPart = part.trim();\n\n // Skip empty parts and the closing boundary marker\n if (!trimmedPart || trimmedPart === \"--\") {\n continue;\n }\n\n // Check if this part is a changeset (nested multipart)\n if (trimmedPart.includes(\"Content-Type: multipart/mixed\")) {\n // Extract the changeset boundary\n const changesetContentTypeMatch = trimmedPart.match(CHANGESET_CONTENT_TYPE_REGEX);\n if (changesetContentTypeMatch) {\n const changesetBoundary = changesetContentTypeMatch?.[1]?.trim();\n const changesetPattern = `--${changesetBoundary}`;\n const changesetParts = trimmedPart.split(changesetPattern);\n\n for (const changesetPart of changesetParts) {\n const trimmedChangesetPart = changesetPart.trim();\n if (!trimmedChangesetPart || trimmedChangesetPart === \"--\") {\n continue;\n }\n\n // Skip the changeset header\n if (trimmedChangesetPart.startsWith(\"Content-Type: multipart/mixed\")) {\n continue;\n }\n\n const response = parseHttpResponse(trimmedChangesetPart);\n if (response.status > 0) {\n results.push(response);\n }\n }\n }\n } else {\n // Regular response (not a changeset)\n const response = parseHttpResponse(trimmedPart);\n if (response.status > 0) {\n results.push(response);\n }\n }\n }\n\n return results;\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"default-select.js","sources":["../../../../src/client/builders/default-select.ts"],"sourcesContent":["import { isColumn } from \"../../orm/column\";\nimport type { FMTable } from \"../../orm/table\";\nimport { FMTable as FMTableClass, getBaseTableConfig } from \"../../orm/table\";\n\n/**\n * Helper function to get container field names from a table.\n * Container fields cannot be selected via $select in FileMaker OData API.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\nfunction getContainerFieldNames(table: FMTable<any, any>): string[] {\n const baseTableConfig = getBaseTableConfig(table);\n if (!baseTableConfig?.containerFields) {\n return [];\n }\n return baseTableConfig.containerFields as string[];\n}\n\n/**\n * Gets default select fields from a table definition.\n * Returns undefined if defaultSelect is \"all\".\n * Automatically filters out container fields since they cannot be selected via $select.\n *\n * @param table - The table occurrence\n * @param includeSpecialColumns - If true, includes ROWID and ROWMODID when defaultSelect is \"schema\"\n */\nexport function getDefaultSelectFields(\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table: FMTable<any, any> | undefined,\n includeSpecialColumns?: boolean,\n): string[] | undefined {\n if (!table) {\n return undefined;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const defaultSelect = (table as any)[FMTableClass.Symbol.DefaultSelect];\n const containerFields = getContainerFieldNames(table);\n\n if (defaultSelect === \"schema\") {\n const baseTableConfig = getBaseTableConfig(table);\n const allFields = Object.keys(baseTableConfig.schema);\n // Filter out container fields\n const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];\n\n // Add special columns if requested\n if (includeSpecialColumns) {\n fields.push(\"ROWID\", \"ROWMODID\");\n }\n\n return fields;\n }\n\n if (Array.isArray(defaultSelect)) {\n // Filter out container fields\n return [...new Set(defaultSelect.filter((f) => !containerFields.includes(f)))];\n }\n\n // Check if defaultSelect is a Record<string, Column> (resolved from function)\n if (typeof defaultSelect === \"object\" && defaultSelect !== null && !Array.isArray(defaultSelect)) {\n // Extract field names from Column instances\n const fieldNames: string[] = [];\n for (const value of Object.values(defaultSelect)) {\n if (isColumn(value)) {\n fieldNames.push(value.fieldName);\n }\n }\n if (fieldNames.length > 0) {\n // Filter out container fields\n return [...new Set(fieldNames.filter((f) => !containerFields.includes(f)))];\n }\n }\n\n // defaultSelect is \"all\" or undefined\n return undefined;\n}\n"],"names":["FMTableClass"],"mappings":";;AASA,SAAS,uBAAuB,OAAoC;AAC5D,QAAA,kBAAkB,mBAAmB,KAAK;AAC5C,MAAA,EAAC,mDAAiB,kBAAiB;AACrC,WAAO,CAAC;AAAA,EAAA;AAEV,SAAO,gBAAgB;AACzB;AAUgB,SAAA,uBAEd,OACA,uBACsB;AACtB,MAAI,CAAC,OAAO;AACH,WAAA;AAAA,EAAA;AAIT,QAAM,gBAAiB,MAAcA,QAAa,OAAO,aAAa;AAChE,QAAA,kBAAkB,uBAAuB,KAAK;AAEpD,MAAI,kBAAkB,UAAU;AACxB,UAAA,kBAAkB,mBAAmB,KAAK;AAChD,UAAM,YAAY,OAAO,KAAK,gBAAgB,MAAM;AAEpD,UAAM,SAAS,CAAC,GAAG,IAAI,IAAI,UAAU,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAO1E,WAAA;AAAA,EAAA;AAGL,MAAA,MAAM,QAAQ,aAAa,GAAG;AAEhC,WAAO,CAAC,GAAG,IAAI,IAAI,cAAc,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAAA,EAAA;AAI3E,MAAA,OAAO,kBAAkB,YAAY,kBAAkB,QAAQ,CAAC,MAAM,QAAQ,aAAa,GAAG;AAEhG,UAAM,aAAuB,CAAC;AAC9B,eAAW,SAAS,OAAO,OAAO,aAAa,GAAG;AAC5C,UAAA,SAAS,KAAK,GAAG;AACR,mBAAA,KAAK,MAAM,SAAS;AAAA,MAAA;AAAA,IACjC;AAEE,QAAA,WAAW,SAAS,GAAG;AAEzB,aAAO,CAAC,GAAG,IAAI,IAAI,WAAW,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAAA,IAAA;AAAA,EAC5E;AAIK,SAAA;AACT;"}
1
+ {"version":3,"file":"default-select.js","sources":["../../../../src/client/builders/default-select.ts"],"sourcesContent":["import { isColumn } from \"../../orm/column\";\nimport type { FMTable } from \"../../orm/table\";\nimport { FMTable as FMTableClass, getBaseTableConfig } from \"../../orm/table\";\n\n/**\n * Helper function to get container field names from a table.\n * Container fields cannot be selected via $select in FileMaker OData API.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\nfunction getContainerFieldNames(table: FMTable<any, any>): string[] {\n const baseTableConfig = getBaseTableConfig(table);\n if (!baseTableConfig?.containerFields) {\n return [];\n }\n return baseTableConfig.containerFields as string[];\n}\n\n/**\n * Gets default select fields from a table definition.\n * Returns undefined if defaultSelect is \"all\".\n * Automatically filters out container fields since they cannot be selected via $select.\n *\n * @param table - The table occurrence\n * @param includeSpecialColumns - If true, includes ROWID and ROWMODID when defaultSelect is \"schema\"\n */\nexport function getDefaultSelectFields(\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table: FMTable<any, any> | undefined,\n includeSpecialColumns?: boolean,\n): string[] | undefined {\n if (!table) {\n return undefined;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const defaultSelect = (table as any)[FMTableClass.Symbol.DefaultSelect];\n const containerFields = getContainerFieldNames(table);\n\n if (defaultSelect === \"schema\") {\n const baseTableConfig = getBaseTableConfig(table);\n const allFields = Object.keys(baseTableConfig.schema);\n // Filter out container fields\n const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];\n\n // Add special columns if requested\n if (includeSpecialColumns) {\n fields.push(\"ROWID\", \"ROWMODID\");\n }\n\n return fields;\n }\n\n if (Array.isArray(defaultSelect)) {\n // Filter out container fields\n return [...new Set(defaultSelect.filter((f) => !containerFields.includes(f)))];\n }\n\n // Check if defaultSelect is a Record<string, Column> (resolved from function)\n if (typeof defaultSelect === \"object\" && defaultSelect !== null && !Array.isArray(defaultSelect)) {\n // Extract field names from Column instances\n const fieldNames: string[] = [];\n for (const value of Object.values(defaultSelect)) {\n if (isColumn(value)) {\n fieldNames.push(value.fieldName);\n }\n }\n if (fieldNames.length > 0) {\n // Filter out container fields\n return [...new Set(fieldNames.filter((f) => !containerFields.includes(f)))];\n }\n }\n\n // defaultSelect is \"all\" or undefined\n return undefined;\n}\n"],"names":["FMTableClass"],"mappings":";;AASA,SAAS,uBAAuB,OAAoC;AAClE,QAAM,kBAAkB,mBAAmB,KAAK;AAChD,MAAI,EAAC,mDAAiB,kBAAiB;AACrC,WAAO,CAAA;AAAA,EACT;AACA,SAAO,gBAAgB;AACzB;AAUO,SAAS,uBAEd,OACA,uBACsB;AACtB,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAGA,QAAM,gBAAiB,MAAcA,QAAa,OAAO,aAAa;AACtE,QAAM,kBAAkB,uBAAuB,KAAK;AAEpD,MAAI,kBAAkB,UAAU;AAC9B,UAAM,kBAAkB,mBAAmB,KAAK;AAChD,UAAM,YAAY,OAAO,KAAK,gBAAgB,MAAM;AAEpD,UAAM,SAAS,CAAC,GAAG,IAAI,IAAI,UAAU,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAOjF,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,aAAa,GAAG;AAEhC,WAAO,CAAC,GAAG,IAAI,IAAI,cAAc,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAAA,EAC/E;AAGA,MAAI,OAAO,kBAAkB,YAAY,kBAAkB,QAAQ,CAAC,MAAM,QAAQ,aAAa,GAAG;AAEhG,UAAM,aAAuB,CAAA;AAC7B,eAAW,SAAS,OAAO,OAAO,aAAa,GAAG;AAChD,UAAI,SAAS,KAAK,GAAG;AACnB,mBAAW,KAAK,MAAM,SAAS;AAAA,MACjC;AAAA,IACF;AACA,QAAI,WAAW,SAAS,GAAG;AAEzB,aAAO,CAAC,GAAG,IAAI,IAAI,WAAW,OAAO,CAAC,MAAM,CAAC,gBAAgB,SAAS,CAAC,CAAC,CAAC,CAAC;AAAA,IAC5E;AAAA,EACF;AAGA,SAAO;AACT;"}
@@ -1 +1 @@
1
- {"version":3,"file":"expand-builder.js","sources":["../../../../src/client/builders/expand-builder.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport buildQuery, { type QueryOptions } from \"odata-query\";\nimport type { InternalLogger } from \"../../logger\";\nimport { FMTable, getBaseTableConfig, getNavigationPaths, getTableName } from \"../../orm/table\";\nimport type { ExpandValidationConfig } from \"../../validation\";\nimport { getDefaultSelectFields } from \"./default-select\";\nimport { formatSelectFields } from \"./select-utils\";\nimport type { ExpandConfig } from \"./shared-types\";\n\nconst FILTER_QUERY_REGEX = /\\$filter=([^&]+)/;\n\n/**\n * Builds OData expand query strings and validation configs.\n * Handles nested expands recursively and transforms relation names to FMTIDs\n * when using entity IDs.\n */\nexport class ExpandBuilder {\n private readonly useEntityIds: boolean;\n private readonly logger: InternalLogger;\n\n constructor(useEntityIds: boolean, logger: InternalLogger) {\n this.useEntityIds = useEntityIds;\n this.logger = logger;\n }\n\n /**\n * Builds OData $expand query string from expand configurations.\n */\n buildExpandString(configs: ExpandConfig[]): string {\n if (configs.length === 0) {\n return \"\";\n }\n\n return configs.map((config) => this.buildSingleExpand(config)).join(\",\");\n }\n\n /**\n * Builds validation configs for expanded navigation properties.\n */\n buildValidationConfigs(configs: ExpandConfig[]): ExpandValidationConfig[] {\n return configs.map((config) => {\n const targetTable = config.targetTable;\n\n let targetSchema: Partial<Record<string, StandardSchemaV1>> | undefined;\n if (targetTable) {\n const baseTableConfig = getBaseTableConfig(targetTable);\n const containerFields = baseTableConfig.containerFields || [];\n\n // Filter out container fields from schema\n const schema = { ...baseTableConfig.schema };\n for (const containerField of containerFields) {\n delete schema[containerField as string];\n }\n\n targetSchema = schema;\n }\n\n let selectedFields: string[] | undefined;\n if (config.options?.select) {\n selectedFields = Array.isArray(config.options.select)\n ? config.options.select.map(String)\n : [String(config.options.select)];\n }\n\n // Recursively build validation configs for nested expands\n const nestedExpands = config.nestedExpandConfigs\n ? this.buildValidationConfigs(config.nestedExpandConfigs)\n : undefined;\n\n return {\n relation: config.relation,\n targetSchema,\n targetTable,\n table: targetTable,\n selectedFields,\n nestedExpands,\n };\n });\n }\n\n /**\n * Process an expand() call and return the expand config.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param targetTable - The target table to expand to\n * @param sourceTable - The source table (for validation)\n * @param callback - Optional callback to configure the expand query\n * @param builderFactory - Function that creates a QueryBuilder for the target table\n * @returns ExpandConfig to add to the builder's expandConfigs array\n */\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration, generic Builder type\n processExpand<TargetTable extends FMTable<any, any>, Builder = any>(\n targetTable: TargetTable,\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n sourceTable: FMTable<any, any> | undefined,\n callback?: (builder: Builder) => Builder,\n builderFactory?: () => Builder,\n ): ExpandConfig {\n // Extract name and validate\n const relationName = getTableName(targetTable);\n\n // Runtime validation: Check if relation name is in navigationPaths\n if (sourceTable) {\n const navigationPaths = getNavigationPaths(sourceTable);\n if (navigationPaths && !navigationPaths.includes(relationName)) {\n this.logger.warn(\n `Cannot expand to \"${relationName}\". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(\", \") : \"none\"}`,\n );\n }\n }\n\n if (callback && builderFactory) {\n // Create a new QueryBuilder for the target table\n const targetBuilder = builderFactory();\n\n // Pass to callback and get configured builder\n const configuredBuilder = callback(targetBuilder);\n\n // Extract the builder's query options\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryOptions configuration\n const expandOptions: Partial<QueryOptions<any>> = {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for internal builder property access\n ...(configuredBuilder as any).queryOptions,\n };\n\n // If callback didn't provide select, apply defaultSelect from target table\n if (!expandOptions.select) {\n const defaultFields = getDefaultSelectFields(targetTable);\n if (defaultFields) {\n expandOptions.select = defaultFields;\n }\n }\n\n // If the configured builder has nested expands, we need to include them\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for internal builder property access\n const nestedExpandConfigs = (configuredBuilder as any).expandConfigs;\n if (nestedExpandConfigs?.length > 0) {\n // Build nested expand string from the configured builder's expand configs\n const nestedExpandString = this.buildExpandString(nestedExpandConfigs);\n if (nestedExpandString) {\n // Add nested expand to options\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for expand string\n expandOptions.expand = nestedExpandString as any;\n }\n }\n\n return {\n relation: relationName,\n options: expandOptions,\n targetTable,\n nestedExpandConfigs: nestedExpandConfigs?.length > 0 ? nestedExpandConfigs : undefined,\n };\n }\n // Simple expand without callback - apply defaultSelect if available\n const defaultFields = getDefaultSelectFields(targetTable);\n if (defaultFields) {\n return {\n relation: relationName,\n options: { select: defaultFields },\n targetTable,\n };\n }\n return {\n relation: relationName,\n targetTable,\n };\n }\n\n /**\n * Builds a single expand string with its options.\n */\n private buildSingleExpand(config: ExpandConfig): string {\n const relationName = this.resolveRelationName(config);\n const parts = this.buildExpandParts(config);\n\n if (parts.length === 0) {\n return relationName;\n }\n\n return `${relationName}(${parts.join(\";\")})`;\n }\n\n /**\n * Resolves relation name, using FMTID if entity IDs are enabled.\n */\n private resolveRelationName(config: ExpandConfig): string {\n if (!this.useEntityIds) {\n return config.relation;\n }\n\n const targetTable = config.targetTable;\n if (targetTable && FMTable.Symbol.EntityId in targetTable) {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const tableId = (targetTable as any)[FMTable.Symbol.EntityId] as `FMTID:${string}` | undefined;\n if (tableId) {\n return tableId;\n }\n }\n\n return config.relation;\n }\n\n /**\n * Builds expand parts (select, filter, orderBy, etc.) for a single expand.\n */\n private buildExpandParts(config: ExpandConfig): string[] {\n if (!config.options || Object.keys(config.options).length === 0) {\n return [];\n }\n\n const parts: string[] = [];\n const opts = config.options;\n\n if (opts.select) {\n const selectArray = Array.isArray(opts.select) ? opts.select.map(String) : [String(opts.select)];\n const selectFields = formatSelectFields(selectArray, config.targetTable, this.useEntityIds);\n parts.push(`$select=${selectFields}`);\n }\n\n if (opts.filter) {\n const filterQuery = buildQuery({ filter: opts.filter });\n const match = filterQuery.match(FILTER_QUERY_REGEX);\n if (match) {\n parts.push(`$filter=${match[1]}`);\n }\n }\n\n if (opts.orderBy) {\n const orderByValue = Array.isArray(opts.orderBy) ? opts.orderBy.join(\",\") : String(opts.orderBy);\n parts.push(`$orderby=${orderByValue}`);\n }\n\n if (opts.top !== undefined) {\n parts.push(`$top=${opts.top}`);\n }\n if (opts.skip !== undefined) {\n parts.push(`$skip=${opts.skip}`);\n }\n\n if (opts.expand && typeof opts.expand === \"string\") {\n parts.push(`$expand=${opts.expand}`);\n }\n\n return parts;\n }\n}\n"],"names":["defaultFields"],"mappings":"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"}
1
+ {"version":3,"file":"expand-builder.js","sources":["../../../../src/client/builders/expand-builder.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport buildQuery, { type QueryOptions } from \"odata-query\";\nimport type { InternalLogger } from \"../../logger\";\nimport { FMTable, getBaseTableConfig, getNavigationPaths, getTableName } from \"../../orm/table\";\nimport type { ExpandValidationConfig } from \"../../validation\";\nimport { getDefaultSelectFields } from \"./default-select\";\nimport { formatSelectFields } from \"./select-utils\";\nimport type { ExpandConfig } from \"./shared-types\";\n\nconst FILTER_QUERY_REGEX = /\\$filter=([^&]+)/;\n\n/**\n * Builds OData expand query strings and validation configs.\n * Handles nested expands recursively and transforms relation names to FMTIDs\n * when using entity IDs.\n */\nexport class ExpandBuilder {\n private readonly useEntityIds: boolean;\n private readonly logger: InternalLogger;\n\n constructor(useEntityIds: boolean, logger: InternalLogger) {\n this.useEntityIds = useEntityIds;\n this.logger = logger;\n }\n\n /**\n * Builds OData $expand query string from expand configurations.\n */\n buildExpandString(configs: ExpandConfig[]): string {\n if (configs.length === 0) {\n return \"\";\n }\n\n return configs.map((config) => this.buildSingleExpand(config)).join(\",\");\n }\n\n /**\n * Builds validation configs for expanded navigation properties.\n */\n buildValidationConfigs(configs: ExpandConfig[]): ExpandValidationConfig[] {\n return configs.map((config) => {\n const targetTable = config.targetTable;\n\n let targetSchema: Partial<Record<string, StandardSchemaV1>> | undefined;\n if (targetTable) {\n const baseTableConfig = getBaseTableConfig(targetTable);\n const containerFields = baseTableConfig.containerFields || [];\n\n // Filter out container fields from schema\n const schema = { ...baseTableConfig.schema };\n for (const containerField of containerFields) {\n delete schema[containerField as string];\n }\n\n targetSchema = schema;\n }\n\n let selectedFields: string[] | undefined;\n if (config.options?.select) {\n selectedFields = Array.isArray(config.options.select)\n ? config.options.select.map(String)\n : [String(config.options.select)];\n }\n\n // Recursively build validation configs for nested expands\n const nestedExpands = config.nestedExpandConfigs\n ? this.buildValidationConfigs(config.nestedExpandConfigs)\n : undefined;\n\n return {\n relation: config.relation,\n targetSchema,\n targetTable,\n table: targetTable,\n selectedFields,\n nestedExpands,\n };\n });\n }\n\n /**\n * Process an expand() call and return the expand config.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param targetTable - The target table to expand to\n * @param sourceTable - The source table (for validation)\n * @param callback - Optional callback to configure the expand query\n * @param builderFactory - Function that creates a QueryBuilder for the target table\n * @returns ExpandConfig to add to the builder's expandConfigs array\n */\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration, generic Builder type\n processExpand<TargetTable extends FMTable<any, any>, Builder = any>(\n targetTable: TargetTable,\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n sourceTable: FMTable<any, any> | undefined,\n callback?: (builder: Builder) => Builder,\n builderFactory?: () => Builder,\n ): ExpandConfig {\n // Extract name and validate\n const relationName = getTableName(targetTable);\n\n // Runtime validation: Check if relation name is in navigationPaths\n if (sourceTable) {\n const navigationPaths = getNavigationPaths(sourceTable);\n if (navigationPaths && !navigationPaths.includes(relationName)) {\n this.logger.warn(\n `Cannot expand to \"${relationName}\". Valid navigation paths: ${navigationPaths.length > 0 ? navigationPaths.join(\", \") : \"none\"}`,\n );\n }\n }\n\n if (callback && builderFactory) {\n // Create a new QueryBuilder for the target table\n const targetBuilder = builderFactory();\n\n // Pass to callback and get configured builder\n const configuredBuilder = callback(targetBuilder);\n\n // Extract the builder's query options\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryOptions configuration\n const expandOptions: Partial<QueryOptions<any>> = {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for internal builder property access\n ...(configuredBuilder as any).queryOptions,\n };\n\n // If callback didn't provide select, apply defaultSelect from target table\n if (!expandOptions.select) {\n const defaultFields = getDefaultSelectFields(targetTable);\n if (defaultFields) {\n expandOptions.select = defaultFields;\n }\n }\n\n // If the configured builder has nested expands, we need to include them\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for internal builder property access\n const nestedExpandConfigs = (configuredBuilder as any).expandConfigs;\n if (nestedExpandConfigs?.length > 0) {\n // Build nested expand string from the configured builder's expand configs\n const nestedExpandString = this.buildExpandString(nestedExpandConfigs);\n if (nestedExpandString) {\n // Add nested expand to options\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for expand string\n expandOptions.expand = nestedExpandString as any;\n }\n }\n\n return {\n relation: relationName,\n options: expandOptions,\n targetTable,\n nestedExpandConfigs: nestedExpandConfigs?.length > 0 ? nestedExpandConfigs : undefined,\n };\n }\n // Simple expand without callback - apply defaultSelect if available\n const defaultFields = getDefaultSelectFields(targetTable);\n if (defaultFields) {\n return {\n relation: relationName,\n options: { select: defaultFields },\n targetTable,\n };\n }\n return {\n relation: relationName,\n targetTable,\n };\n }\n\n /**\n * Builds a single expand string with its options.\n */\n private buildSingleExpand(config: ExpandConfig): string {\n const relationName = this.resolveRelationName(config);\n const parts = this.buildExpandParts(config);\n\n if (parts.length === 0) {\n return relationName;\n }\n\n return `${relationName}(${parts.join(\";\")})`;\n }\n\n /**\n * Resolves relation name, using FMTID if entity IDs are enabled.\n */\n private resolveRelationName(config: ExpandConfig): string {\n if (!this.useEntityIds) {\n return config.relation;\n }\n\n const targetTable = config.targetTable;\n if (targetTable && FMTable.Symbol.EntityId in targetTable) {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const tableId = (targetTable as any)[FMTable.Symbol.EntityId] as `FMTID:${string}` | undefined;\n if (tableId) {\n return tableId;\n }\n }\n\n return config.relation;\n }\n\n /**\n * Builds expand parts (select, filter, orderBy, etc.) for a single expand.\n */\n private buildExpandParts(config: ExpandConfig): string[] {\n if (!config.options || Object.keys(config.options).length === 0) {\n return [];\n }\n\n const parts: string[] = [];\n const opts = config.options;\n\n if (opts.select) {\n const selectArray = Array.isArray(opts.select) ? opts.select.map(String) : [String(opts.select)];\n const selectFields = formatSelectFields(selectArray, config.targetTable, this.useEntityIds);\n parts.push(`$select=${selectFields}`);\n }\n\n if (opts.filter) {\n const filterQuery = buildQuery({ filter: opts.filter });\n const match = filterQuery.match(FILTER_QUERY_REGEX);\n if (match) {\n parts.push(`$filter=${match[1]}`);\n }\n }\n\n if (opts.orderBy) {\n const orderByValue = Array.isArray(opts.orderBy) ? opts.orderBy.join(\",\") : String(opts.orderBy);\n parts.push(`$orderby=${orderByValue}`);\n }\n\n if (opts.top !== undefined) {\n parts.push(`$top=${opts.top}`);\n }\n if (opts.skip !== undefined) {\n parts.push(`$skip=${opts.skip}`);\n }\n\n if (opts.expand && typeof opts.expand === \"string\") {\n parts.push(`$expand=${opts.expand}`);\n }\n\n return parts;\n }\n}\n"],"names":["defaultFields"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"query-string-builder.js","sources":["../../../../src/client/builders/query-string-builder.ts"],"sourcesContent":["import type { InternalLogger } from \"../../logger\";\nimport type { FMTable } from \"../../orm/table\";\nimport { ExpandBuilder } from \"./expand-builder\";\nimport { formatSelectFields } from \"./select-utils\";\nimport type { ExpandConfig } from \"./shared-types\";\n\n/**\n * Builds OData query string for $select and $expand parameters.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param config - Configuration object\n * @returns Query string starting with ? or empty string if no parameters\n */\nexport function buildSelectExpandQueryString(config: {\n selectedFields?: string[];\n expandConfigs: ExpandConfig[];\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table?: FMTable<any, any>;\n useEntityIds: boolean;\n logger: InternalLogger;\n includeSpecialColumns?: boolean;\n}): string {\n const parts: string[] = [];\n const expandBuilder = new ExpandBuilder(config.useEntityIds, config.logger);\n\n // Build $select\n if (config.selectedFields && config.selectedFields.length > 0) {\n // Important: do NOT implicitly add system columns (ROWID/ROWMODID) here.\n // - `includeSpecialColumns` controls the Prefer header + response parsing, but should not\n // mutate/expand an explicit `$select` (e.g. when the user calls `.select({ ... })`).\n // - If system columns are desired with `.select()`, they must be explicitly included via\n // the `systemColumns` argument, which will already have added them to `selectedFields`.\n const selectString = formatSelectFields(config.selectedFields, config.table, config.useEntityIds);\n if (selectString) {\n parts.push(`$select=${selectString}`);\n }\n }\n\n // Build $expand\n const expandString = expandBuilder.buildExpandString(config.expandConfigs);\n if (expandString) {\n parts.push(`$expand=${expandString}`);\n }\n\n return parts.length > 0 ? `?${parts.join(\"&\")}` : \"\";\n}\n"],"names":[],"mappings":";;AAaO,SAAS,6BAA6B,QAQlC;AACT,QAAM,QAAkB,CAAC;AACzB,QAAM,gBAAgB,IAAI,cAAc,OAAO,cAAc,OAAO,MAAM;AAG1E,MAAI,OAAO,kBAAkB,OAAO,eAAe,SAAS,GAAG;AAM7D,UAAM,eAAe,mBAAmB,OAAO,gBAAgB,OAAO,OAAO,OAAO,YAAY;AAChG,QAAI,cAAc;AACV,YAAA,KAAK,WAAW,YAAY,EAAE;AAAA,IAAA;AAAA,EACtC;AAIF,QAAM,eAAe,cAAc,kBAAkB,OAAO,aAAa;AACzE,MAAI,cAAc;AACV,UAAA,KAAK,WAAW,YAAY,EAAE;AAAA,EAAA;AAG/B,SAAA,MAAM,SAAS,IAAI,IAAI,MAAM,KAAK,GAAG,CAAC,KAAK;AACpD;"}
1
+ {"version":3,"file":"query-string-builder.js","sources":["../../../../src/client/builders/query-string-builder.ts"],"sourcesContent":["import type { InternalLogger } from \"../../logger\";\nimport type { FMTable } from \"../../orm/table\";\nimport { ExpandBuilder } from \"./expand-builder\";\nimport { formatSelectFields } from \"./select-utils\";\nimport type { ExpandConfig } from \"./shared-types\";\n\n/**\n * Builds OData query string for $select and $expand parameters.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param config - Configuration object\n * @returns Query string starting with ? or empty string if no parameters\n */\nexport function buildSelectExpandQueryString(config: {\n selectedFields?: string[];\n expandConfigs: ExpandConfig[];\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table?: FMTable<any, any>;\n useEntityIds: boolean;\n logger: InternalLogger;\n includeSpecialColumns?: boolean;\n}): string {\n const parts: string[] = [];\n const expandBuilder = new ExpandBuilder(config.useEntityIds, config.logger);\n\n // Build $select\n if (config.selectedFields && config.selectedFields.length > 0) {\n // Important: do NOT implicitly add system columns (ROWID/ROWMODID) here.\n // - `includeSpecialColumns` controls the Prefer header + response parsing, but should not\n // mutate/expand an explicit `$select` (e.g. when the user calls `.select({ ... })`).\n // - If system columns are desired with `.select()`, they must be explicitly included via\n // the `systemColumns` argument, which will already have added them to `selectedFields`.\n const selectString = formatSelectFields(config.selectedFields, config.table, config.useEntityIds);\n if (selectString) {\n parts.push(`$select=${selectString}`);\n }\n }\n\n // Build $expand\n const expandString = expandBuilder.buildExpandString(config.expandConfigs);\n if (expandString) {\n parts.push(`$expand=${expandString}`);\n }\n\n return parts.length > 0 ? `?${parts.join(\"&\")}` : \"\";\n}\n"],"names":[],"mappings":";;AAaO,SAAS,6BAA6B,QAQlC;AACT,QAAM,QAAkB,CAAA;AACxB,QAAM,gBAAgB,IAAI,cAAc,OAAO,cAAc,OAAO,MAAM;AAG1E,MAAI,OAAO,kBAAkB,OAAO,eAAe,SAAS,GAAG;AAM7D,UAAM,eAAe,mBAAmB,OAAO,gBAAgB,OAAO,OAAO,OAAO,YAAY;AAChG,QAAI,cAAc;AAChB,YAAM,KAAK,WAAW,YAAY,EAAE;AAAA,IACtC;AAAA,EACF;AAGA,QAAM,eAAe,cAAc,kBAAkB,OAAO,aAAa;AACzE,MAAI,cAAc;AAChB,UAAM,KAAK,WAAW,YAAY,EAAE;AAAA,EACtC;AAEA,SAAO,MAAM,SAAS,IAAI,IAAI,MAAM,KAAK,GAAG,CAAC,KAAK;AACpD;"}
@@ -1 +1 @@
1
- {"version":3,"file":"response-processor.js","sources":["../../../../src/client/builders/response-processor.ts"],"sourcesContent":["import { RecordCountMismatchError } from \"../../errors\";\nimport type { InternalLogger } from \"../../logger\";\nimport type { FMTable } from \"../../orm/table\";\nimport { getBaseTableConfig } from \"../../orm/table\";\nimport { transformResponseFields } from \"../../transform\";\nimport type { Result } from \"../../types\";\nimport type { ExpandValidationConfig } from \"../../validation\";\nimport { validateListResponse, validateSingleResponse } from \"../../validation\";\nimport { ExpandBuilder } from \"./expand-builder\";\nimport type { ExpandConfig } from \"./shared-types\";\n\nexport interface ProcessResponseConfig {\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table?: FMTable<any, any>;\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic schema shape from table configuration\n schema?: Record<string, any>;\n singleMode: \"exact\" | \"maybe\" | false;\n selectedFields?: string[];\n expandValidationConfigs?: ExpandValidationConfig[];\n skipValidation?: boolean;\n useEntityIds?: boolean;\n includeSpecialColumns?: boolean;\n // Mapping from field names to output keys (for renamed fields in select)\n fieldMapping?: Record<string, string>;\n}\n\n/**\n * Processes OData response with transformation and validation.\n * Shared by QueryBuilder and RecordBuilder.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API\nexport async function processODataResponse<T>(rawResponse: any, config: ProcessResponseConfig): Promise<Result<T>> {\n const {\n table,\n schema,\n singleMode,\n selectedFields,\n expandValidationConfigs,\n skipValidation,\n useEntityIds,\n includeSpecialColumns,\n fieldMapping,\n } = config;\n\n // Transform field IDs back to names if using entity IDs\n let response = rawResponse;\n if (table && useEntityIds) {\n response = transformResponseFields(response, table, expandValidationConfigs);\n }\n\n // Fast path: skip validation\n if (skipValidation) {\n const result = extractRecords(response, singleMode);\n // Rename fields AFTER extraction (but before returning)\n if (result.data && fieldMapping && Object.keys(fieldMapping).length > 0) {\n if (result.error) {\n return { data: undefined, error: result.error } as Result<T>;\n }\n return {\n data: renameFieldsInResponse(result.data, fieldMapping) as T,\n error: undefined,\n };\n }\n return result as Result<T>;\n }\n\n // Validation path\n // Note: Special columns are excluded when using QueryBuilder.single() method,\n // but included for RecordBuilder.get() method (both use singleMode: \"exact\")\n // The exclusion is handled in QueryBuilder's processQueryResponse, not here\n if (singleMode !== false) {\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape\n const validation = await validateSingleResponse<any>(\n response,\n schema,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter\n selectedFields as any,\n expandValidationConfigs,\n singleMode,\n includeSpecialColumns,\n );\n\n if (!validation.valid) {\n return { data: undefined, error: validation.error };\n }\n\n // Rename fields AFTER validation completes\n if (fieldMapping && Object.keys(fieldMapping).length > 0) {\n return {\n data: renameFieldsInResponse(validation.data, fieldMapping) as T,\n error: undefined,\n };\n }\n\n return { data: validation.data as T, error: undefined };\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape\n const validation = await validateListResponse<any>(\n response,\n schema,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter\n selectedFields as any,\n expandValidationConfigs,\n includeSpecialColumns,\n );\n\n if (!validation.valid) {\n return { data: undefined, error: validation.error };\n }\n\n // Rename fields AFTER validation completes\n if (fieldMapping && Object.keys(fieldMapping).length > 0) {\n return {\n data: renameFieldsInResponse(validation.data, fieldMapping) as T,\n error: undefined,\n };\n }\n\n return { data: validation.data as T, error: undefined };\n}\n\n/**\n * Extracts records from response without validation.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API\nfunction extractRecords<T>(response: any, singleMode: \"exact\" | \"maybe\" | false): Result<T> {\n if (singleMode === false) {\n const records = response.value ?? [];\n return { data: records as T, error: undefined };\n }\n\n const records = response.value ?? [response];\n const count = Array.isArray(records) ? records.length : 1;\n\n if (count > 1) {\n return {\n data: undefined,\n error: new RecordCountMismatchError(singleMode === \"exact\" ? \"one\" : \"at-most-one\", count),\n };\n }\n\n if (count === 0) {\n if (singleMode === \"exact\") {\n return { data: undefined, error: new RecordCountMismatchError(\"one\", 0) };\n }\n return { data: null as T, error: undefined };\n }\n\n const record = Array.isArray(records) ? records[0] : records;\n return { data: record as T, error: undefined };\n}\n\n/**\n * Gets schema from a table occurrence, excluding container fields.\n * Container fields are never returned in regular responses (only via getSingleField).\n */\n// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration, dynamic schema shape\nexport function getSchemaFromTable(table: FMTable<any, any> | undefined): Record<string, any> | undefined {\n if (!table) {\n return undefined;\n }\n const baseTableConfig = getBaseTableConfig(table);\n const containerFields = baseTableConfig.containerFields || [];\n\n // Filter out container fields from schema\n const schema = { ...baseTableConfig.schema };\n for (const containerField of containerFields) {\n delete schema[containerField as string];\n }\n\n return schema;\n}\n\n/**\n * Renames fields in response data according to the field mapping.\n * Used when select() is called with renamed fields (e.g., { userEmail: users.email }).\n */\n// biome-ignore lint/suspicious/noExplicitAny: Dynamic response data transformation\nfunction renameFieldsInResponse(data: any, fieldMapping: Record<string, string>): any {\n if (!data || typeof data !== \"object\") {\n return data;\n }\n\n // Handle array responses\n if (Array.isArray(data)) {\n return data.map((item) => renameFieldsInResponse(item, fieldMapping));\n }\n\n // Handle OData list response structure\n if (\"value\" in data && Array.isArray(data.value)) {\n return {\n ...data,\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic record transformation\n value: data.value.map((item: any) => renameFieldsInResponse(item, fieldMapping)),\n };\n }\n\n // Handle single record\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic field transformation\n const renamed: Record<string, any> = {};\n for (const [key, value] of Object.entries(data)) {\n // Check if this field should be renamed\n const outputKey = fieldMapping[key];\n if (outputKey) {\n renamed[outputKey] = value;\n } else {\n renamed[key] = value;\n }\n }\n return renamed;\n}\n\n/**\n * Processes query response with expand configs.\n * This is a convenience wrapper that builds validation configs from expand configs.\n */\nexport async function processQueryResponse<T>(\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API\n response: any,\n config: {\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n occurrence?: FMTable<any, any>;\n singleMode: \"exact\" | \"maybe\" | false;\n queryOptions: { select?: (keyof T)[] | string[] };\n expandConfigs: ExpandConfig[];\n skipValidation?: boolean;\n useEntityIds?: boolean;\n includeSpecialColumns?: boolean;\n // Mapping from field names to output keys (for renamed fields in select)\n fieldMapping?: Record<string, string>;\n logger: InternalLogger;\n },\n // biome-ignore lint/suspicious/noExplicitAny: Generic return type for interface compliance\n): Promise<Result<any>> {\n const {\n occurrence,\n singleMode,\n queryOptions,\n expandConfigs,\n skipValidation,\n useEntityIds,\n includeSpecialColumns,\n fieldMapping,\n logger,\n } = config;\n\n const expandBuilder = new ExpandBuilder(useEntityIds ?? false, logger);\n const expandValidationConfigs = expandBuilder.buildValidationConfigs(expandConfigs);\n\n let selectedFields: string[] | undefined;\n if (queryOptions.select) {\n selectedFields = Array.isArray(queryOptions.select)\n ? queryOptions.select.map(String)\n : [String(queryOptions.select)];\n }\n\n // Process the response first\n let processedResponse = await processODataResponse(response, {\n table: occurrence,\n schema: getSchemaFromTable(occurrence),\n singleMode,\n selectedFields,\n expandValidationConfigs,\n skipValidation,\n useEntityIds,\n includeSpecialColumns,\n });\n\n // Rename fields if field mapping is provided (for renamed fields in select)\n if (processedResponse.data && fieldMapping && Object.keys(fieldMapping).length > 0) {\n processedResponse = {\n ...processedResponse,\n data: renameFieldsInResponse(processedResponse.data, fieldMapping),\n };\n }\n\n return processedResponse;\n}\n"],"names":["validation","records"],"mappings":"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"}
1
+ {"version":3,"file":"response-processor.js","sources":["../../../../src/client/builders/response-processor.ts"],"sourcesContent":["import { RecordCountMismatchError } from \"../../errors\";\nimport type { InternalLogger } from \"../../logger\";\nimport type { FMTable } from \"../../orm/table\";\nimport { getBaseTableConfig } from \"../../orm/table\";\nimport { transformResponseFields } from \"../../transform\";\nimport type { Result } from \"../../types\";\nimport type { ExpandValidationConfig } from \"../../validation\";\nimport { validateListResponse, validateSingleResponse } from \"../../validation\";\nimport { ExpandBuilder } from \"./expand-builder\";\nimport type { ExpandConfig } from \"./shared-types\";\n\nexport interface ProcessResponseConfig {\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table?: FMTable<any, any>;\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic schema shape from table configuration\n schema?: Record<string, any>;\n singleMode: \"exact\" | \"maybe\" | false;\n selectedFields?: string[];\n expandValidationConfigs?: ExpandValidationConfig[];\n skipValidation?: boolean;\n useEntityIds?: boolean;\n includeSpecialColumns?: boolean;\n // Mapping from field names to output keys (for renamed fields in select)\n fieldMapping?: Record<string, string>;\n}\n\n/**\n * Processes OData response with transformation and validation.\n * Shared by QueryBuilder and RecordBuilder.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API\nexport async function processODataResponse<T>(rawResponse: any, config: ProcessResponseConfig): Promise<Result<T>> {\n const {\n table,\n schema,\n singleMode,\n selectedFields,\n expandValidationConfigs,\n skipValidation,\n useEntityIds,\n includeSpecialColumns,\n fieldMapping,\n } = config;\n\n // Transform field IDs back to names if using entity IDs\n let response = rawResponse;\n if (table && useEntityIds) {\n response = transformResponseFields(response, table, expandValidationConfigs);\n }\n\n // Fast path: skip validation\n if (skipValidation) {\n const result = extractRecords(response, singleMode);\n // Rename fields AFTER extraction (but before returning)\n if (result.data && fieldMapping && Object.keys(fieldMapping).length > 0) {\n if (result.error) {\n return { data: undefined, error: result.error } as Result<T>;\n }\n return {\n data: renameFieldsInResponse(result.data, fieldMapping) as T,\n error: undefined,\n };\n }\n return result as Result<T>;\n }\n\n // Validation path\n // Note: Special columns are excluded when using QueryBuilder.single() method,\n // but included for RecordBuilder.get() method (both use singleMode: \"exact\")\n // The exclusion is handled in QueryBuilder's processQueryResponse, not here\n if (singleMode !== false) {\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape\n const validation = await validateSingleResponse<any>(\n response,\n schema,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter\n selectedFields as any,\n expandValidationConfigs,\n singleMode,\n includeSpecialColumns,\n );\n\n if (!validation.valid) {\n return { data: undefined, error: validation.error };\n }\n\n // Rename fields AFTER validation completes\n if (fieldMapping && Object.keys(fieldMapping).length > 0) {\n return {\n data: renameFieldsInResponse(validation.data, fieldMapping) as T,\n error: undefined,\n };\n }\n\n return { data: validation.data as T, error: undefined };\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape\n const validation = await validateListResponse<any>(\n response,\n schema,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic type parameter\n selectedFields as any,\n expandValidationConfigs,\n includeSpecialColumns,\n );\n\n if (!validation.valid) {\n return { data: undefined, error: validation.error };\n }\n\n // Rename fields AFTER validation completes\n if (fieldMapping && Object.keys(fieldMapping).length > 0) {\n return {\n data: renameFieldsInResponse(validation.data, fieldMapping) as T,\n error: undefined,\n };\n }\n\n return { data: validation.data as T, error: undefined };\n}\n\n/**\n * Extracts records from response without validation.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API\nfunction extractRecords<T>(response: any, singleMode: \"exact\" | \"maybe\" | false): Result<T> {\n if (singleMode === false) {\n const records = response.value ?? [];\n return { data: records as T, error: undefined };\n }\n\n const records = response.value ?? [response];\n const count = Array.isArray(records) ? records.length : 1;\n\n if (count > 1) {\n return {\n data: undefined,\n error: new RecordCountMismatchError(singleMode === \"exact\" ? \"one\" : \"at-most-one\", count),\n };\n }\n\n if (count === 0) {\n if (singleMode === \"exact\") {\n return { data: undefined, error: new RecordCountMismatchError(\"one\", 0) };\n }\n return { data: null as T, error: undefined };\n }\n\n const record = Array.isArray(records) ? records[0] : records;\n return { data: record as T, error: undefined };\n}\n\n/**\n * Gets schema from a table occurrence, excluding container fields.\n * Container fields are never returned in regular responses (only via getSingleField).\n */\n// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration, dynamic schema shape\nexport function getSchemaFromTable(table: FMTable<any, any> | undefined): Record<string, any> | undefined {\n if (!table) {\n return undefined;\n }\n const baseTableConfig = getBaseTableConfig(table);\n const containerFields = baseTableConfig.containerFields || [];\n\n // Filter out container fields from schema\n const schema = { ...baseTableConfig.schema };\n for (const containerField of containerFields) {\n delete schema[containerField as string];\n }\n\n return schema;\n}\n\n/**\n * Renames fields in response data according to the field mapping.\n * Used when select() is called with renamed fields (e.g., { userEmail: users.email }).\n */\n// biome-ignore lint/suspicious/noExplicitAny: Dynamic response data transformation\nfunction renameFieldsInResponse(data: any, fieldMapping: Record<string, string>): any {\n if (!data || typeof data !== \"object\") {\n return data;\n }\n\n // Handle array responses\n if (Array.isArray(data)) {\n return data.map((item) => renameFieldsInResponse(item, fieldMapping));\n }\n\n // Handle OData list response structure\n if (\"value\" in data && Array.isArray(data.value)) {\n return {\n ...data,\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic record transformation\n value: data.value.map((item: any) => renameFieldsInResponse(item, fieldMapping)),\n };\n }\n\n // Handle single record\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic field transformation\n const renamed: Record<string, any> = {};\n for (const [key, value] of Object.entries(data)) {\n // Check if this field should be renamed\n const outputKey = fieldMapping[key];\n if (outputKey) {\n renamed[outputKey] = value;\n } else {\n renamed[key] = value;\n }\n }\n return renamed;\n}\n\n/**\n * Processes query response with expand configs.\n * This is a convenience wrapper that builds validation configs from expand configs.\n */\nexport async function processQueryResponse<T>(\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API\n response: any,\n config: {\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n occurrence?: FMTable<any, any>;\n singleMode: \"exact\" | \"maybe\" | false;\n queryOptions: { select?: (keyof T)[] | string[] };\n expandConfigs: ExpandConfig[];\n skipValidation?: boolean;\n useEntityIds?: boolean;\n includeSpecialColumns?: boolean;\n // Mapping from field names to output keys (for renamed fields in select)\n fieldMapping?: Record<string, string>;\n logger: InternalLogger;\n },\n // biome-ignore lint/suspicious/noExplicitAny: Generic return type for interface compliance\n): Promise<Result<any>> {\n const {\n occurrence,\n singleMode,\n queryOptions,\n expandConfigs,\n skipValidation,\n useEntityIds,\n includeSpecialColumns,\n fieldMapping,\n logger,\n } = config;\n\n const expandBuilder = new ExpandBuilder(useEntityIds ?? false, logger);\n const expandValidationConfigs = expandBuilder.buildValidationConfigs(expandConfigs);\n\n let selectedFields: string[] | undefined;\n if (queryOptions.select) {\n selectedFields = Array.isArray(queryOptions.select)\n ? queryOptions.select.map(String)\n : [String(queryOptions.select)];\n }\n\n // Process the response first\n let processedResponse = await processODataResponse(response, {\n table: occurrence,\n schema: getSchemaFromTable(occurrence),\n singleMode,\n selectedFields,\n expandValidationConfigs,\n skipValidation,\n useEntityIds,\n includeSpecialColumns,\n });\n\n // Rename fields if field mapping is provided (for renamed fields in select)\n if (processedResponse.data && fieldMapping && Object.keys(fieldMapping).length > 0) {\n processedResponse = {\n ...processedResponse,\n data: renameFieldsInResponse(processedResponse.data, fieldMapping),\n };\n }\n\n return processedResponse;\n}\n"],"names":["validation","records"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"select-mixin.js","sources":["../../../../src/client/builders/select-mixin.ts"],"sourcesContent":["import type { InternalLogger } from \"../../logger\";\nimport { type Column, isColumn } from \"../../orm/column\";\n\n/**\n * Utility function for processing select() calls.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param fields - Field names or Column references\n * @returns Object with selectedFields array\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\nexport function processSelectFields(...fields: (string | Column<any, any, string>)[]): { selectedFields: string[] } {\n const fieldNames = fields.map((field) => {\n if (isColumn(field)) {\n return field.fieldName as string;\n }\n return String(field);\n });\n return { selectedFields: [...new Set(fieldNames)] };\n}\n\n/**\n * Processes select() calls with field renaming support.\n * Validates columns belong to the correct table and builds field mapping for renamed fields.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param fields - Object mapping output keys to column references\n * @param tableName - Expected table name for validation\n * @returns Object with selectedFields array and fieldMapping for renamed fields\n */\nexport function processSelectWithRenames<TTableName extends string>(\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\n fields: Record<string, Column<any, any, TTableName>>,\n tableName: string,\n logger: InternalLogger,\n): { selectedFields: string[]; fieldMapping: Record<string, string> } {\n const selectedFields: string[] = [];\n const fieldMapping: Record<string, string> = {};\n\n for (const [outputKey, column] of Object.entries(fields)) {\n if (!isColumn(column)) {\n throw new Error(`select() expects column references, but got: ${typeof column}`);\n }\n\n // Warn (not throw) on table mismatch for consistency\n if (column.tableName !== tableName) {\n logger.warn(\n `Column ${column.toString()} is from table \"${column.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n\n const fieldName = column.fieldName;\n selectedFields.push(fieldName);\n\n // Build mapping from field name to output key (only if renamed)\n if (fieldName !== outputKey) {\n fieldMapping[fieldName] = outputKey;\n }\n }\n\n return {\n selectedFields,\n fieldMapping: Object.keys(fieldMapping).length > 0 ? fieldMapping : {},\n };\n}\n"],"names":[],"mappings":";AA8BgB,SAAA,yBAEd,QACA,WACA,QACoE;AACpE,QAAM,iBAA2B,CAAC;AAClC,QAAM,eAAuC,CAAC;AAE9C,aAAW,CAAC,WAAW,MAAM,KAAK,OAAO,QAAQ,MAAM,GAAG;AACpD,QAAA,CAAC,SAAS,MAAM,GAAG;AACrB,YAAM,IAAI,MAAM,gDAAgD,OAAO,MAAM,EAAE;AAAA,IAAA;AAI7E,QAAA,OAAO,cAAc,WAAW;AAC3B,aAAA;AAAA,QACL,UAAU,OAAO,UAAU,mBAAmB,OAAO,SAAS,8BAA8B,SAAS;AAAA,MACvG;AAAA,IAAA;AAGF,UAAM,YAAY,OAAO;AACzB,mBAAe,KAAK,SAAS;AAG7B,QAAI,cAAc,WAAW;AAC3B,mBAAa,SAAS,IAAI;AAAA,IAAA;AAAA,EAC5B;AAGK,SAAA;AAAA,IACL;AAAA,IACA,cAAc,OAAO,KAAK,YAAY,EAAE,SAAS,IAAI,eAAe,CAAA;AAAA,EACtE;AACF;"}
1
+ {"version":3,"file":"select-mixin.js","sources":["../../../../src/client/builders/select-mixin.ts"],"sourcesContent":["import type { InternalLogger } from \"../../logger\";\nimport { type Column, isColumn } from \"../../orm/column\";\n\n/**\n * Utility function for processing select() calls.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param fields - Field names or Column references\n * @returns Object with selectedFields array\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\nexport function processSelectFields(...fields: (string | Column<any, any, string>)[]): { selectedFields: string[] } {\n const fieldNames = fields.map((field) => {\n if (isColumn(field)) {\n return field.fieldName as string;\n }\n return String(field);\n });\n return { selectedFields: [...new Set(fieldNames)] };\n}\n\n/**\n * Processes select() calls with field renaming support.\n * Validates columns belong to the correct table and builds field mapping for renamed fields.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param fields - Object mapping output keys to column references\n * @param tableName - Expected table name for validation\n * @returns Object with selectedFields array and fieldMapping for renamed fields\n */\nexport function processSelectWithRenames<TTableName extends string>(\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration\n fields: Record<string, Column<any, any, TTableName>>,\n tableName: string,\n logger: InternalLogger,\n): { selectedFields: string[]; fieldMapping: Record<string, string> } {\n const selectedFields: string[] = [];\n const fieldMapping: Record<string, string> = {};\n\n for (const [outputKey, column] of Object.entries(fields)) {\n if (!isColumn(column)) {\n throw new Error(`select() expects column references, but got: ${typeof column}`);\n }\n\n // Warn (not throw) on table mismatch for consistency\n if (column.tableName !== tableName) {\n logger.warn(\n `Column ${column.toString()} is from table \"${column.tableName}\", but query is for table \"${tableName}\"`,\n );\n }\n\n const fieldName = column.fieldName;\n selectedFields.push(fieldName);\n\n // Build mapping from field name to output key (only if renamed)\n if (fieldName !== outputKey) {\n fieldMapping[fieldName] = outputKey;\n }\n }\n\n return {\n selectedFields,\n fieldMapping: Object.keys(fieldMapping).length > 0 ? fieldMapping : {},\n };\n}\n"],"names":[],"mappings":";AA8BO,SAAS,yBAEd,QACA,WACA,QACoE;AACpE,QAAM,iBAA2B,CAAA;AACjC,QAAM,eAAuC,CAAA;AAE7C,aAAW,CAAC,WAAW,MAAM,KAAK,OAAO,QAAQ,MAAM,GAAG;AACxD,QAAI,CAAC,SAAS,MAAM,GAAG;AACrB,YAAM,IAAI,MAAM,gDAAgD,OAAO,MAAM,EAAE;AAAA,IACjF;AAGA,QAAI,OAAO,cAAc,WAAW;AAClC,aAAO;AAAA,QACL,UAAU,OAAO,UAAU,mBAAmB,OAAO,SAAS,8BAA8B,SAAS;AAAA,MAAA;AAAA,IAEzG;AAEA,UAAM,YAAY,OAAO;AACzB,mBAAe,KAAK,SAAS;AAG7B,QAAI,cAAc,WAAW;AAC3B,mBAAa,SAAS,IAAI;AAAA,IAC5B;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,cAAc,OAAO,KAAK,YAAY,EAAE,SAAS,IAAI,eAAe,CAAA;AAAA,EAAC;AAEzE;"}
@@ -1 +1 @@
1
- {"version":3,"file":"select-utils.js","sources":["../../../../src/client/builders/select-utils.ts"],"sourcesContent":["import type { FMTable } from \"../../orm/table\";\nimport { transformFieldNamesArray } from \"../../transform\";\n\nconst VALID_FIELD_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9]*$/;\n\n/**\n * Determines if a field name needs to be quoted in OData queries.\n * Per FileMaker docs: field names with special characters (spaces, underscores, etc.) must be quoted.\n * Also quotes \"id\" as it's an OData reserved word.\n * Entity IDs (FMFID:*, FMTID:*) are not quoted as they're identifiers, not field names.\n *\n * @param fieldName - The field name or identifier to check\n * @returns true if the field name should be quoted in OData queries\n */\nexport function needsFieldQuoting(fieldName: string): boolean {\n // Entity IDs are identifiers and don't need quoting\n if (fieldName.startsWith(\"FMFID:\") || fieldName.startsWith(\"FMTID:\")) {\n return false;\n }\n // Always quote \"id\" as it's an OData reserved word\n if (fieldName === \"id\") {\n return true;\n }\n // Quote if field name contains spaces, underscores, or other special characters\n return fieldName.includes(\" \") || fieldName.includes(\"_\") || !VALID_FIELD_NAME_REGEX.test(fieldName);\n}\n\n/**\n * Formats select fields for use in OData query strings.\n * - Transforms field names to FMFIDs if using entity IDs\n * - Wraps \"id\" fields in double quotes (OData reserved)\n * - URL-encodes special characters but preserves spaces\n */\nexport function formatSelectFields(\n select: string[] | readonly string[] | undefined,\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table?: FMTable<any, any>,\n useEntityIds?: boolean,\n): string {\n if (!select || select.length === 0) {\n return \"\";\n }\n\n const selectArray = Array.isArray(select) ? select : [select];\n\n // Transform to field IDs if using entity IDs\n const transformedFields =\n table && useEntityIds ? transformFieldNamesArray(selectArray.map(String), table) : selectArray.map(String);\n\n return transformedFields\n .map((field) => {\n if (needsFieldQuoting(field)) {\n return `\"${field}\"`;\n }\n const encoded = encodeURIComponent(field);\n return encoded.replace(/%20/g, \" \");\n })\n .join(\",\");\n}\n"],"names":[],"mappings":";AAGA,MAAM,yBAAyB;AAWxB,SAAS,kBAAkB,WAA4B;AAE5D,MAAI,UAAU,WAAW,QAAQ,KAAK,UAAU,WAAW,QAAQ,GAAG;AAC7D,WAAA;AAAA,EAAA;AAGT,MAAI,cAAc,MAAM;AACf,WAAA;AAAA,EAAA;AAGF,SAAA,UAAU,SAAS,GAAG,KAAK,UAAU,SAAS,GAAG,KAAK,CAAC,uBAAuB,KAAK,SAAS;AACrG;AAQgB,SAAA,mBACd,QAEA,OACA,cACQ;AACR,MAAI,CAAC,UAAU,OAAO,WAAW,GAAG;AAC3B,WAAA;AAAA,EAAA;AAGT,QAAM,cAAc,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM;AAG5D,QAAM,oBACJ,SAAS,eAAe,yBAAyB,YAAY,IAAI,MAAM,GAAG,KAAK,IAAI,YAAY,IAAI,MAAM;AAEpG,SAAA,kBACJ,IAAI,CAAC,UAAU;AACV,QAAA,kBAAkB,KAAK,GAAG;AAC5B,aAAO,IAAI,KAAK;AAAA,IAAA;AAEZ,UAAA,UAAU,mBAAmB,KAAK;AACjC,WAAA,QAAQ,QAAQ,QAAQ,GAAG;AAAA,EAAA,CACnC,EACA,KAAK,GAAG;AACb;"}
1
+ {"version":3,"file":"select-utils.js","sources":["../../../../src/client/builders/select-utils.ts"],"sourcesContent":["import type { FMTable } from \"../../orm/table\";\nimport { transformFieldNamesArray } from \"../../transform\";\n\nconst VALID_FIELD_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9]*$/;\n\n/**\n * Determines if a field name needs to be quoted in OData queries.\n * Per FileMaker docs: field names with special characters (spaces, underscores, etc.) must be quoted.\n * Also quotes \"id\" as it's an OData reserved word.\n * Entity IDs (FMFID:*, FMTID:*) are not quoted as they're identifiers, not field names.\n *\n * @param fieldName - The field name or identifier to check\n * @returns true if the field name should be quoted in OData queries\n */\nexport function needsFieldQuoting(fieldName: string): boolean {\n // Entity IDs are identifiers and don't need quoting\n if (fieldName.startsWith(\"FMFID:\") || fieldName.startsWith(\"FMTID:\")) {\n return false;\n }\n // Always quote \"id\" as it's an OData reserved word\n if (fieldName === \"id\") {\n return true;\n }\n // Quote if field name contains spaces, underscores, or other special characters\n return fieldName.includes(\" \") || fieldName.includes(\"_\") || !VALID_FIELD_NAME_REGEX.test(fieldName);\n}\n\n/**\n * Formats select fields for use in OData query strings.\n * - Transforms field names to FMFIDs if using entity IDs\n * - Wraps \"id\" fields in double quotes (OData reserved)\n * - URL-encodes special characters but preserves spaces\n */\nexport function formatSelectFields(\n select: string[] | readonly string[] | undefined,\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table?: FMTable<any, any>,\n useEntityIds?: boolean,\n): string {\n if (!select || select.length === 0) {\n return \"\";\n }\n\n const selectArray = Array.isArray(select) ? select : [select];\n\n // Transform to field IDs if using entity IDs\n const transformedFields =\n table && useEntityIds ? transformFieldNamesArray(selectArray.map(String), table) : selectArray.map(String);\n\n return transformedFields\n .map((field) => {\n if (needsFieldQuoting(field)) {\n return `\"${field}\"`;\n }\n const encoded = encodeURIComponent(field);\n return encoded.replace(/%20/g, \" \");\n })\n .join(\",\");\n}\n"],"names":[],"mappings":";AAGA,MAAM,yBAAyB;AAWxB,SAAS,kBAAkB,WAA4B;AAE5D,MAAI,UAAU,WAAW,QAAQ,KAAK,UAAU,WAAW,QAAQ,GAAG;AACpE,WAAO;AAAA,EACT;AAEA,MAAI,cAAc,MAAM;AACtB,WAAO;AAAA,EACT;AAEA,SAAO,UAAU,SAAS,GAAG,KAAK,UAAU,SAAS,GAAG,KAAK,CAAC,uBAAuB,KAAK,SAAS;AACrG;AAQO,SAAS,mBACd,QAEA,OACA,cACQ;AACR,MAAI,CAAC,UAAU,OAAO,WAAW,GAAG;AAClC,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM;AAG5D,QAAM,oBACJ,SAAS,eAAe,yBAAyB,YAAY,IAAI,MAAM,GAAG,KAAK,IAAI,YAAY,IAAI,MAAM;AAE3G,SAAO,kBACJ,IAAI,CAAC,UAAU;AACd,QAAI,kBAAkB,KAAK,GAAG;AAC5B,aAAO,IAAI,KAAK;AAAA,IAClB;AACA,UAAM,UAAU,mBAAmB,KAAK;AACxC,WAAO,QAAQ,QAAQ,QAAQ,GAAG;AAAA,EACpC,CAAC,EACA,KAAK,GAAG;AACb;"}
@@ -1 +1 @@
1
- {"version":3,"file":"table-utils.js","sources":["../../../../src/client/builders/table-utils.ts"],"sourcesContent":["import type { FFetchOptions } from \"@fetchkit/ffetch\";\nimport type { FMTable } from \"../../orm/table\";\nimport { getTableId as getTableIdHelper, getTableName, isUsingEntityIds } from \"../../orm/table\";\nimport type { ExecuteOptions, ExecutionContext } from \"../../types\";\nimport { getAcceptHeader } from \"../../types\";\n\n/**\n * Resolves table identifier based on entity ID settings.\n * Used by both QueryBuilder and RecordBuilder.\n */\nexport function resolveTableId(\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table: FMTable<any, any> | undefined,\n fallbackTableName: string,\n context: ExecutionContext,\n useEntityIdsOverride?: boolean,\n): string {\n if (!table) {\n return fallbackTableName;\n }\n\n const contextDefault = context._getUseEntityIds?.() ?? false;\n const shouldUseIds = useEntityIdsOverride ?? contextDefault;\n\n if (shouldUseIds) {\n if (!isUsingEntityIds(table)) {\n throw new Error(`useEntityIds is true but table \"${getTableName(table)}\" does not have entity IDs configured`);\n }\n return getTableIdHelper(table);\n }\n\n return getTableName(table);\n}\n\n/**\n * Merges database-level useEntityIds with per-request options.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape\nexport function mergeEntityIdOptions<T extends Record<string, any>>(\n options: T | undefined,\n databaseDefault: boolean,\n): T & { useEntityIds?: boolean } {\n return {\n ...options,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for optional property access\n useEntityIds: (options as any)?.useEntityIds ?? databaseDefault,\n } as T & { useEntityIds?: boolean };\n}\n\n/**\n * Type-safe helper for merging execute options with entity ID settings\n */\nexport function mergeExecuteOptions(\n options: (RequestInit & FFetchOptions & ExecuteOptions) | undefined,\n databaseUseEntityIds: boolean,\n): RequestInit & FFetchOptions & { useEntityIds?: boolean } {\n return mergeEntityIdOptions(options, databaseUseEntityIds);\n}\n\n/**\n * Creates an OData Request object with proper headers.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param baseUrl - Base URL for the request\n * @param config - Request configuration with method and url\n * @param options - Optional execution options\n * @returns Request object ready to use\n */\nexport function createODataRequest(\n baseUrl: string,\n config: { method: string; url: string },\n options?: { includeODataAnnotations?: boolean },\n): Request {\n const fullUrl = `${baseUrl}${config.url}`;\n\n return new Request(fullUrl, {\n method: config.method,\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: getAcceptHeader(options?.includeODataAnnotations),\n },\n });\n}\n"],"names":["getTableIdHelper"],"mappings":";;AAUO,SAAS,eAEd,OACA,mBACA,SACA,sBACQ;;AACR,MAAI,CAAC,OAAO;AACH,WAAA;AAAA,EAAA;AAGH,QAAA,mBAAiB,aAAQ,qBAAR,qCAAgC;AACvD,QAAM,eAAe,wBAAwB;AAE7C,MAAI,cAAc;AACZ,QAAA,CAAC,iBAAiB,KAAK,GAAG;AAC5B,YAAM,IAAI,MAAM,mCAAmC,aAAa,KAAK,CAAC,uCAAuC;AAAA,IAAA;AAE/G,WAAOA,WAAiB,KAAK;AAAA,EAAA;AAG/B,SAAO,aAAa,KAAK;AAC3B;AAMgB,SAAA,qBACd,SACA,iBACgC;AACzB,SAAA;AAAA,IACL,GAAG;AAAA;AAAA,IAEH,eAAe,mCAAiB,iBAAgB;AAAA,EAClD;AACF;AAKgB,SAAA,oBACd,SACA,sBAC0D;AACnD,SAAA,qBAAqB,SAAS,oBAAoB;AAC3D;AAWgB,SAAA,mBACd,SACA,QACA,SACS;AACT,QAAM,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG;AAEhC,SAAA,IAAI,QAAQ,SAAS;AAAA,IAC1B,QAAQ,OAAO;AAAA,IACf,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,QAAQ,gBAAgB,mCAAS,uBAAuB;AAAA,IAAA;AAAA,EAC1D,CACD;AACH;"}
1
+ {"version":3,"file":"table-utils.js","sources":["../../../../src/client/builders/table-utils.ts"],"sourcesContent":["import type { FFetchOptions } from \"@fetchkit/ffetch\";\nimport type { FMTable } from \"../../orm/table\";\nimport { getTableId as getTableIdHelper, getTableName, isUsingEntityIds } from \"../../orm/table\";\nimport type { ExecuteOptions, ExecutionContext } from \"../../types\";\nimport { getAcceptHeader } from \"../../types\";\n\n/**\n * Resolves table identifier based on entity ID settings.\n * Used by both QueryBuilder and RecordBuilder.\n */\nexport function resolveTableId(\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n table: FMTable<any, any> | undefined,\n fallbackTableName: string,\n context: ExecutionContext,\n useEntityIdsOverride?: boolean,\n): string {\n if (!table) {\n return fallbackTableName;\n }\n\n const contextDefault = context._getUseEntityIds?.() ?? false;\n const shouldUseIds = useEntityIdsOverride ?? contextDefault;\n\n if (shouldUseIds) {\n if (!isUsingEntityIds(table)) {\n throw new Error(`useEntityIds is true but table \"${getTableName(table)}\" does not have entity IDs configured`);\n }\n return getTableIdHelper(table);\n }\n\n return getTableName(table);\n}\n\n/**\n * Merges database-level useEntityIds with per-request options.\n */\n// biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape\nexport function mergeEntityIdOptions<T extends Record<string, any>>(\n options: T | undefined,\n databaseDefault: boolean,\n): T & { useEntityIds?: boolean } {\n return {\n ...options,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for optional property access\n useEntityIds: (options as any)?.useEntityIds ?? databaseDefault,\n } as T & { useEntityIds?: boolean };\n}\n\n/**\n * Type-safe helper for merging execute options with entity ID settings\n */\nexport function mergeExecuteOptions(\n options: (RequestInit & FFetchOptions & ExecuteOptions) | undefined,\n databaseUseEntityIds: boolean,\n): RequestInit & FFetchOptions & { useEntityIds?: boolean } {\n return mergeEntityIdOptions(options, databaseUseEntityIds);\n}\n\n/**\n * Creates an OData Request object with proper headers.\n * Used by both QueryBuilder and RecordBuilder to eliminate duplication.\n *\n * @param baseUrl - Base URL for the request\n * @param config - Request configuration with method and url\n * @param options - Optional execution options\n * @returns Request object ready to use\n */\nexport function createODataRequest(\n baseUrl: string,\n config: { method: string; url: string },\n options?: { includeODataAnnotations?: boolean },\n): Request {\n const fullUrl = `${baseUrl}${config.url}`;\n\n return new Request(fullUrl, {\n method: config.method,\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: getAcceptHeader(options?.includeODataAnnotations),\n },\n });\n}\n"],"names":["getTableIdHelper"],"mappings":";;AAUO,SAAS,eAEd,OACA,mBACA,SACA,sBACQ;;AACR,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,QAAM,mBAAiB,aAAQ,qBAAR,qCAAgC;AACvD,QAAM,eAAe,wBAAwB;AAE7C,MAAI,cAAc;AAChB,QAAI,CAAC,iBAAiB,KAAK,GAAG;AAC5B,YAAM,IAAI,MAAM,mCAAmC,aAAa,KAAK,CAAC,uCAAuC;AAAA,IAC/G;AACA,WAAOA,WAAiB,KAAK;AAAA,EAC/B;AAEA,SAAO,aAAa,KAAK;AAC3B;AAMO,SAAS,qBACd,SACA,iBACgC;AAChC,SAAO;AAAA,IACL,GAAG;AAAA;AAAA,IAEH,eAAe,mCAAiB,iBAAgB;AAAA,EAAA;AAEpD;AAKO,SAAS,oBACd,SACA,sBAC0D;AAC1D,SAAO,qBAAqB,SAAS,oBAAoB;AAC3D;AAWO,SAAS,mBACd,SACA,QACA,SACS;AACT,QAAM,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG;AAEvC,SAAO,IAAI,QAAQ,SAAS;AAAA,IAC1B,QAAQ,OAAO;AAAA,IACf,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,QAAQ,gBAAgB,mCAAS,uBAAuB;AAAA,IAAA;AAAA,EAC1D,CACD;AACH;"}
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sources":["../../../src/client/database.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { FMTable } from \"../orm/table\";\nimport type { ExecutableBuilder, ExecutionContext, Metadata } from \"../types\";\nimport { BatchBuilder } from \"./batch-builder\";\nimport { EntitySet } from \"./entity-set\";\nimport { SchemaManager } from \"./schema-manager\";\nimport { WebhookManager } from \"./webhook-builder\";\n\ninterface MetadataArgs {\n format?: \"xml\" | \"json\";\n /**\n * If provided, only the metadata for the specified table will be returned.\n * Requires FileMaker Server 22.0.4 or later.\n */\n tableName?: string;\n /**\n * If true, a reduced payload size will be returned by omitting certain annotations.\n */\n reduceAnnotations?: boolean;\n}\n\nexport class Database<IncludeSpecialColumns extends boolean = false> {\n readonly schema: SchemaManager;\n readonly webhook: WebhookManager;\n private readonly databaseName: string;\n private readonly context: ExecutionContext;\n private _useEntityIds: boolean;\n private readonly _includeSpecialColumns: IncludeSpecialColumns;\n\n constructor(\n databaseName: string,\n context: ExecutionContext,\n config?: {\n /**\n * Whether to use entity IDs instead of field names in the actual requests to the server\n * Defaults to true if all occurrences use entity IDs, false otherwise\n * If set to false but some occurrences do not use entity IDs, an error will be thrown\n */\n useEntityIds?: boolean;\n /**\n * Whether to include special columns (ROWID and ROWMODID) in responses.\n * Note: Special columns are only included when there is no $select query.\n */\n includeSpecialColumns?: IncludeSpecialColumns;\n },\n ) {\n this.databaseName = databaseName;\n this.context = context;\n // Initialize schema manager\n this.schema = new SchemaManager(this.databaseName, this.context);\n this.webhook = new WebhookManager(this.databaseName, this.context);\n this._useEntityIds = config?.useEntityIds ?? false;\n this._includeSpecialColumns = (config?.includeSpecialColumns ?? false) as IncludeSpecialColumns;\n }\n\n /**\n * @internal Used by EntitySet to access database configuration\n */\n get _getUseEntityIds(): boolean {\n return this._useEntityIds;\n }\n\n /**\n * @internal Used by EntitySet to access database configuration\n */\n get _getIncludeSpecialColumns(): IncludeSpecialColumns {\n return this._includeSpecialColumns;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n from<T extends FMTable<any, any>>(table: T): EntitySet<T, IncludeSpecialColumns> {\n // Only override database-level useEntityIds if table explicitly sets it\n // (not if it's undefined, which would override the database setting)\n if (Object.hasOwn(table, FMTable.Symbol.UseEntityIds)) {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const tableUseEntityIds = (table as any)[FMTable.Symbol.UseEntityIds];\n if (typeof tableUseEntityIds === \"boolean\") {\n this._useEntityIds = tableUseEntityIds;\n }\n }\n return new EntitySet<T, IncludeSpecialColumns>({\n occurrence: table as T,\n databaseName: this.databaseName,\n context: this.context,\n database: this,\n });\n }\n\n /**\n * Retrieves the OData metadata for this database.\n * @param args Optional configuration object\n * @param args.format The format to retrieve metadata in. Defaults to \"json\".\n * @param args.tableName If provided, only the metadata for the specified table will be returned. Requires FileMaker Server 22.0.4 or later.\n * @param args.reduceAnnotations If true, a reduced payload size will be returned by omitting certain annotations.\n * @returns The metadata in the specified format\n */\n async getMetadata(args: { format: \"xml\" } & MetadataArgs): Promise<string>;\n async getMetadata(args?: { format?: \"json\" } & MetadataArgs): Promise<Metadata>;\n async getMetadata(args?: MetadataArgs): Promise<string | Metadata> {\n // Build the URL - if tableName is provided, append %23{tableName} to the path\n let url = `/${this.databaseName}/$metadata`;\n if (args?.tableName) {\n url = `/${this.databaseName}/$metadata%23${args.tableName}`;\n }\n\n // Build headers\n const headers: Record<string, string> = {\n Accept: args?.format === \"xml\" ? \"application/xml\" : \"application/json\",\n };\n\n // Add Prefer header if reduceAnnotations is true\n if (args?.reduceAnnotations) {\n headers.Prefer = 'include-annotations=\"-*\"';\n }\n\n const result = await this.context._makeRequest<Record<string, Metadata> | string>(url, {\n headers,\n });\n if (result.error) {\n throw result.error;\n }\n\n if (args?.format === \"json\") {\n const data = result.data as Record<string, Metadata>;\n const metadata = data[this.databaseName];\n if (!metadata) {\n throw new Error(`Metadata for database \"${this.databaseName}\" not found in response`);\n }\n return metadata;\n }\n return result.data as string;\n }\n\n /**\n * Lists all available tables (entity sets) in this database.\n * @returns Promise resolving to an array of table names\n */\n async listTableNames(): Promise<string[]> {\n const result = await this.context._makeRequest<{\n value?: Array<{ name: string }>;\n }>(`/${this.databaseName}`);\n if (result.error) {\n throw result.error;\n }\n if (result.data.value && Array.isArray(result.data.value)) {\n return result.data.value.map((item) => item.name);\n }\n return [];\n }\n\n /**\n * Executes a FileMaker script.\n * @param scriptName - The name of the script to execute (must be valid according to OData rules)\n * @param options - Optional script parameter and result schema\n * @returns Promise resolving to script execution result\n */\n // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer\n async runScript<ResultSchema extends StandardSchemaV1<string, any> = never>(\n scriptName: string,\n options?: {\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape\n scriptParam?: string | number | Record<string, any>;\n resultSchema?: ResultSchema;\n },\n ): Promise<\n [ResultSchema] extends [never]\n ? { resultCode: number; result?: string }\n : ResultSchema extends StandardSchemaV1<string, infer Output>\n ? { resultCode: number; result: Output }\n : { resultCode: number; result?: string }\n > {\n const body: { scriptParameterValue?: unknown } = {};\n if (options?.scriptParam !== undefined) {\n body.scriptParameterValue = options.scriptParam;\n }\n\n const result = await this.context._makeRequest<{\n scriptResult: {\n code: number;\n resultParameter?: string;\n };\n }>(`/${this.databaseName}/Script.${scriptName}`, {\n method: \"POST\",\n body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,\n });\n\n if (result.error) {\n throw result.error;\n }\n\n const response = result.data;\n\n // If resultSchema is provided, validate the result through it\n if (options?.resultSchema && response.scriptResult !== undefined) {\n const validationResult = options.resultSchema[\"~standard\"].validate(response.scriptResult.resultParameter);\n // Handle both sync and async validation\n const result = validationResult instanceof Promise ? await validationResult : validationResult;\n\n if (result.issues) {\n throw new Error(`Script result validation failed: ${JSON.stringify(result.issues)}`);\n }\n\n return {\n resultCode: response.scriptResult.code,\n result: result.value,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type\n } as any;\n }\n\n return {\n resultCode: response.scriptResult.code,\n result: response.scriptResult.resultParameter,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type\n } as any;\n }\n\n /**\n * Create a batch operation builder that allows multiple queries to be executed together\n * in a single atomic request. All operations succeed or fail together (transactional).\n *\n * @param builders - Array of executable query builders to batch\n * @returns A BatchBuilder that can be executed\n * @example\n * ```ts\n * const result = await db.batch([\n * db.from('contacts').list().top(5),\n * db.from('users').list().top(5),\n * db.from('contacts').insert({ name: 'John' })\n * ]).execute();\n *\n * if (result.data) {\n * const [contacts, users, insertResult] = result.data;\n * }\n * ```\n */\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\n batch<const Builders extends readonly ExecutableBuilder<any>[]>(builders: Builders): BatchBuilder<Builders> {\n return new BatchBuilder(builders, this.databaseName, this.context);\n }\n}\n"],"names":["result"],"mappings":"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"}
1
+ {"version":3,"file":"database.js","sources":["../../../src/client/database.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { FMTable } from \"../orm/table\";\nimport type { ExecutableBuilder, ExecutionContext, Metadata } from \"../types\";\nimport { BatchBuilder } from \"./batch-builder\";\nimport { EntitySet } from \"./entity-set\";\nimport { SchemaManager } from \"./schema-manager\";\nimport { WebhookManager } from \"./webhook-builder\";\n\ninterface MetadataArgs {\n format?: \"xml\" | \"json\";\n /**\n * If provided, only the metadata for the specified table will be returned.\n * Requires FileMaker Server 22.0.4 or later.\n */\n tableName?: string;\n /**\n * If true, a reduced payload size will be returned by omitting certain annotations.\n */\n reduceAnnotations?: boolean;\n}\n\nexport class Database<IncludeSpecialColumns extends boolean = false> {\n readonly schema: SchemaManager;\n readonly webhook: WebhookManager;\n private readonly databaseName: string;\n private readonly context: ExecutionContext;\n private _useEntityIds: boolean;\n private readonly _includeSpecialColumns: IncludeSpecialColumns;\n\n constructor(\n databaseName: string,\n context: ExecutionContext,\n config?: {\n /**\n * Whether to use entity IDs instead of field names in the actual requests to the server\n * Defaults to true if all occurrences use entity IDs, false otherwise\n * If set to false but some occurrences do not use entity IDs, an error will be thrown\n */\n useEntityIds?: boolean;\n /**\n * Whether to include special columns (ROWID and ROWMODID) in responses.\n * Note: Special columns are only included when there is no $select query.\n */\n includeSpecialColumns?: IncludeSpecialColumns;\n },\n ) {\n this.databaseName = databaseName;\n this.context = context;\n // Initialize schema manager\n this.schema = new SchemaManager(this.databaseName, this.context);\n this.webhook = new WebhookManager(this.databaseName, this.context);\n this._useEntityIds = config?.useEntityIds ?? false;\n this._includeSpecialColumns = (config?.includeSpecialColumns ?? false) as IncludeSpecialColumns;\n }\n\n /**\n * @internal Used by EntitySet to access database configuration\n */\n get _getUseEntityIds(): boolean {\n return this._useEntityIds;\n }\n\n /**\n * @internal Used by EntitySet to access database configuration\n */\n get _getIncludeSpecialColumns(): IncludeSpecialColumns {\n return this._includeSpecialColumns;\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\n from<T extends FMTable<any, any>>(table: T): EntitySet<T, IncludeSpecialColumns> {\n // Only override database-level useEntityIds if table explicitly sets it\n // (not if it's undefined, which would override the database setting)\n if (Object.hasOwn(table, FMTable.Symbol.UseEntityIds)) {\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access\n const tableUseEntityIds = (table as any)[FMTable.Symbol.UseEntityIds];\n if (typeof tableUseEntityIds === \"boolean\") {\n this._useEntityIds = tableUseEntityIds;\n }\n }\n return new EntitySet<T, IncludeSpecialColumns>({\n occurrence: table as T,\n databaseName: this.databaseName,\n context: this.context,\n database: this,\n });\n }\n\n /**\n * Retrieves the OData metadata for this database.\n * @param args Optional configuration object\n * @param args.format The format to retrieve metadata in. Defaults to \"json\".\n * @param args.tableName If provided, only the metadata for the specified table will be returned. Requires FileMaker Server 22.0.4 or later.\n * @param args.reduceAnnotations If true, a reduced payload size will be returned by omitting certain annotations.\n * @returns The metadata in the specified format\n */\n async getMetadata(args: { format: \"xml\" } & MetadataArgs): Promise<string>;\n async getMetadata(args?: { format?: \"json\" } & MetadataArgs): Promise<Metadata>;\n async getMetadata(args?: MetadataArgs): Promise<string | Metadata> {\n // Build the URL - if tableName is provided, append %23{tableName} to the path\n let url = `/${this.databaseName}/$metadata`;\n if (args?.tableName) {\n url = `/${this.databaseName}/$metadata%23${args.tableName}`;\n }\n\n // Build headers\n const headers: Record<string, string> = {\n Accept: args?.format === \"xml\" ? \"application/xml\" : \"application/json\",\n };\n\n // Add Prefer header if reduceAnnotations is true\n if (args?.reduceAnnotations) {\n headers.Prefer = 'include-annotations=\"-*\"';\n }\n\n const result = await this.context._makeRequest<Record<string, Metadata> | string>(url, {\n headers,\n });\n if (result.error) {\n throw result.error;\n }\n\n if (args?.format === \"json\") {\n const data = result.data as Record<string, Metadata>;\n const metadata = data[this.databaseName];\n if (!metadata) {\n throw new Error(`Metadata for database \"${this.databaseName}\" not found in response`);\n }\n return metadata;\n }\n return result.data as string;\n }\n\n /**\n * Lists all available tables (entity sets) in this database.\n * @returns Promise resolving to an array of table names\n */\n async listTableNames(): Promise<string[]> {\n const result = await this.context._makeRequest<{\n value?: Array<{ name: string }>;\n }>(`/${this.databaseName}`);\n if (result.error) {\n throw result.error;\n }\n if (result.data.value && Array.isArray(result.data.value)) {\n return result.data.value.map((item) => item.name);\n }\n return [];\n }\n\n /**\n * Executes a FileMaker script.\n * @param scriptName - The name of the script to execute (must be valid according to OData rules)\n * @param options - Optional script parameter and result schema\n * @returns Promise resolving to script execution result\n */\n // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer\n async runScript<ResultSchema extends StandardSchemaV1<string, any> = never>(\n scriptName: string,\n options?: {\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape\n scriptParam?: string | number | Record<string, any>;\n resultSchema?: ResultSchema;\n },\n ): Promise<\n [ResultSchema] extends [never]\n ? { resultCode: number; result?: string }\n : ResultSchema extends StandardSchemaV1<string, infer Output>\n ? { resultCode: number; result: Output }\n : { resultCode: number; result?: string }\n > {\n const body: { scriptParameterValue?: unknown } = {};\n if (options?.scriptParam !== undefined) {\n body.scriptParameterValue = options.scriptParam;\n }\n\n const result = await this.context._makeRequest<{\n scriptResult: {\n code: number;\n resultParameter?: string;\n };\n }>(`/${this.databaseName}/Script.${scriptName}`, {\n method: \"POST\",\n body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,\n });\n\n if (result.error) {\n throw result.error;\n }\n\n const response = result.data;\n\n // If resultSchema is provided, validate the result through it\n if (options?.resultSchema && response.scriptResult !== undefined) {\n const validationResult = options.resultSchema[\"~standard\"].validate(response.scriptResult.resultParameter);\n // Handle both sync and async validation\n const result = validationResult instanceof Promise ? await validationResult : validationResult;\n\n if (result.issues) {\n throw new Error(`Script result validation failed: ${JSON.stringify(result.issues)}`);\n }\n\n return {\n resultCode: response.scriptResult.code,\n result: result.value,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type\n } as any;\n }\n\n return {\n resultCode: response.scriptResult.code,\n result: response.scriptResult.resultParameter,\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type\n } as any;\n }\n\n /**\n * Create a batch operation builder that allows multiple queries to be executed together\n * in a single atomic request. All operations succeed or fail together (transactional).\n *\n * @param builders - Array of executable query builders to batch\n * @returns A BatchBuilder that can be executed\n * @example\n * ```ts\n * const result = await db.batch([\n * db.from('contacts').list().top(5),\n * db.from('users').list().top(5),\n * db.from('contacts').insert({ name: 'John' })\n * ]).execute();\n *\n * if (result.data) {\n * const [contacts, users, insertResult] = result.data;\n * }\n * ```\n */\n // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type\n batch<const Builders extends readonly ExecutableBuilder<any>[]>(builders: Builders): BatchBuilder<Builders> {\n return new BatchBuilder(builders, this.databaseName, this.context);\n }\n}\n"],"names":["result"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"delete-builder.js","sources":["../../../src/client/delete-builder.ts"],"sourcesContent":["import type { FFetchOptions } from \"@fetchkit/ffetch\";\nimport type { FMTable } from \"../orm/table\";\nimport { getTableId as getTableIdHelper, getTableName, isUsingEntityIds } from \"../orm/table\";\nimport type { ExecutableBuilder, ExecuteMethodOptions, ExecuteOptions, ExecutionContext, Result } from \"../types\";\nimport { getAcceptHeader } from \"../types\";\nimport { parseErrorResponse } from \"./error-parser\";\nimport { QueryBuilder } from \"./query-builder\";\n\n/**\n * Initial delete builder returned from EntitySet.delete()\n * Requires calling .byId() or .where() before .execute() is available\n */\n// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\nexport class DeleteBuilder<Occ extends FMTable<any, any>> {\n private readonly databaseName: string;\n private readonly context: ExecutionContext;\n private readonly table: Occ;\n private readonly databaseUseEntityIds: boolean;\n private readonly databaseIncludeSpecialColumns: boolean;\n\n constructor(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n databaseUseEntityIds?: boolean;\n databaseIncludeSpecialColumns?: boolean;\n }) {\n this.table = config.occurrence;\n this.databaseName = config.databaseName;\n this.context = config.context;\n this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;\n this.databaseIncludeSpecialColumns = config.databaseIncludeSpecialColumns ?? false;\n }\n\n /**\n * Delete a single record by ID\n */\n byId(id: string | number): ExecutableDeleteBuilder<Occ> {\n return new ExecutableDeleteBuilder<Occ>({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n mode: \"byId\",\n recordId: id,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n }\n\n /**\n * Delete records matching a filter query\n * @param fn Callback that receives a QueryBuilder for building the filter\n */\n where(fn: (q: QueryBuilder<Occ>) => QueryBuilder<Occ>): ExecutableDeleteBuilder<Occ> {\n // Create a QueryBuilder for the user to configure\n const queryBuilder = new QueryBuilder<Occ>({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n });\n\n // Let the user configure it\n const configuredBuilder = fn(queryBuilder);\n\n return new ExecutableDeleteBuilder<Occ>({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n mode: \"byFilter\",\n queryBuilder: configuredBuilder,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n }\n}\n\n/**\n * Executable delete builder - has execute() method\n * Returned after calling .byId() or .where()\n */\n// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\nexport class ExecutableDeleteBuilder<Occ extends FMTable<any, any>>\n implements ExecutableBuilder<{ deletedCount: number }>\n{\n private readonly databaseName: string;\n private readonly context: ExecutionContext;\n private readonly table: Occ;\n private readonly mode: \"byId\" | \"byFilter\";\n private readonly recordId?: string | number;\n private readonly queryBuilder?: QueryBuilder<Occ>;\n private readonly databaseUseEntityIds: boolean;\n\n constructor(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n mode: \"byId\" | \"byFilter\";\n recordId?: string | number;\n queryBuilder?: QueryBuilder<Occ>;\n databaseUseEntityIds?: boolean;\n }) {\n this.table = config.occurrence;\n this.databaseName = config.databaseName;\n this.context = config.context;\n this.mode = config.mode;\n this.recordId = config.recordId;\n this.queryBuilder = config.queryBuilder;\n this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;\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 // If useEntityIds is not set in options, use the database-level setting\n return {\n ...options,\n useEntityIds: options?.useEntityIds ?? this.databaseUseEntityIds,\n };\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 const contextDefault = this.context._getUseEntityIds?.() ?? false;\n const shouldUseIds = useEntityIds ?? contextDefault;\n\n if (shouldUseIds) {\n if (!isUsingEntityIds(this.table)) {\n throw new Error(\n `useEntityIds is true but table \"${getTableName(this.table)}\" does not have entity IDs configured`,\n );\n }\n return getTableIdHelper(this.table);\n }\n\n return getTableName(this.table);\n }\n\n async execute(options?: ExecuteMethodOptions<ExecuteOptions>): Promise<Result<{ deletedCount: number }>> {\n // Merge database-level useEntityIds with per-request options\n const mergedOptions = this.mergeExecuteOptions(options);\n\n // Get table identifier with override support\n const tableId = this.getTableId(mergedOptions.useEntityIds);\n\n let url: string;\n\n if (this.mode === \"byId\") {\n // Delete single record by ID: DELETE /{database}/{table}('id')\n url = `/${this.databaseName}/${tableId}('${this.recordId}')`;\n } else {\n // Delete by filter: DELETE /{database}/{table}?$filter=...\n if (!this.queryBuilder) {\n throw new Error(\"Query builder is required for filter-based delete\");\n }\n\n // Get the query string from the configured QueryBuilder\n const queryString = this.queryBuilder.getQueryString();\n // Remove the leading \"/\" and table name from the query string as we'll build our own URL\n const tableName = getTableName(this.table);\n let queryParams: string;\n if (queryString.startsWith(`/${tableId}`)) {\n queryParams = queryString.slice(`/${tableId}`.length);\n } else if (queryString.startsWith(`/${tableName}`)) {\n queryParams = queryString.slice(`/${tableName}`.length);\n } else {\n queryParams = queryString;\n }\n\n url = `/${this.databaseName}/${tableId}${queryParams}`;\n }\n\n // Make DELETE request\n const result = await this.context._makeRequest(url, {\n method: \"DELETE\",\n ...mergedOptions,\n });\n\n if (result.error) {\n return { data: undefined, error: result.error };\n }\n\n const response = result.data;\n\n // OData returns 204 No Content with fmodata.affected_rows header\n // The _makeRequest should handle extracting the header value\n // For now, we'll check if response contains the count\n let deletedCount = 0;\n\n if (typeof response === \"number\") {\n deletedCount = response;\n } else if (response && typeof response === \"object\") {\n // Check if the response has a count property (fallback)\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API\n deletedCount = (response as any).deletedCount || 0;\n }\n\n return { data: { deletedCount }, error: undefined };\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value\n getRequestConfig(): { method: string; url: string; body?: any } {\n // For batch operations, use database-level setting (no per-request override available here)\n const tableId = this.getTableId(this.databaseUseEntityIds);\n\n let url: string;\n\n if (this.mode === \"byId\") {\n url = `/${this.databaseName}/${tableId}('${this.recordId}')`;\n } else {\n if (!this.queryBuilder) {\n throw new Error(\"Query builder is required for filter-based delete\");\n }\n\n const queryString = this.queryBuilder.getQueryString();\n const tableName = getTableName(this.table);\n let queryParams: string;\n if (queryString.startsWith(`/${tableId}`)) {\n queryParams = queryString.slice(`/${tableId}`.length);\n } else if (queryString.startsWith(`/${tableName}`)) {\n queryParams = queryString.slice(`/${tableName}`.length);\n } else {\n queryParams = queryString;\n }\n\n url = `/${this.databaseName}/${tableId}${queryParams}`;\n }\n\n return {\n method: \"DELETE\",\n url,\n };\n }\n\n toRequest(baseUrl: string, options?: ExecuteOptions): Request {\n const config = this.getRequestConfig();\n const fullUrl = `${baseUrl}${config.url}`;\n\n return new Request(fullUrl, {\n method: config.method,\n headers: {\n Accept: getAcceptHeader(options?.includeODataAnnotations),\n },\n });\n }\n\n async processResponse(response: Response, _options?: ExecuteOptions): Promise<Result<{ deletedCount: number }>> {\n // Check for error responses (important for batch operations)\n if (!response.ok) {\n const tableName = getTableName(this.table);\n const error = await parseErrorResponse(response, response.url || `/${this.databaseName}/${tableName}`);\n return { data: undefined, error };\n }\n\n // Check for empty response (204 No Content)\n const text = await response.text();\n if (!text || text.trim() === \"\") {\n // For 204 No Content, check the fmodata.affected_rows header\n const affectedRows = response.headers.get(\"fmodata.affected_rows\");\n const deletedCount = affectedRows ? Number.parseInt(affectedRows, 10) : 1;\n return { data: { deletedCount }, error: undefined };\n }\n\n const rawResponse = JSON.parse(text);\n\n // OData returns 204 No Content with fmodata.affected_rows header\n // The _makeRequest should handle extracting the header value\n // For now, we'll check if response contains the count\n let deletedCount = 0;\n\n if (typeof rawResponse === \"number\") {\n deletedCount = rawResponse;\n } else if (rawResponse && typeof rawResponse === \"object\") {\n // Check if the response has a count property (fallback)\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API\n deletedCount = (rawResponse as any).deletedCount || 0;\n }\n\n return { data: { deletedCount }, error: undefined };\n }\n}\n"],"names":["getTableIdHelper","deletedCount"],"mappings":"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"}
1
+ {"version":3,"file":"delete-builder.js","sources":["../../../src/client/delete-builder.ts"],"sourcesContent":["import type { FFetchOptions } from \"@fetchkit/ffetch\";\nimport type { FMTable } from \"../orm/table\";\nimport { getTableId as getTableIdHelper, getTableName, isUsingEntityIds } from \"../orm/table\";\nimport type { ExecutableBuilder, ExecuteMethodOptions, ExecuteOptions, ExecutionContext, Result } from \"../types\";\nimport { getAcceptHeader } from \"../types\";\nimport { parseErrorResponse } from \"./error-parser\";\nimport { QueryBuilder } from \"./query-builder\";\n\n/**\n * Initial delete builder returned from EntitySet.delete()\n * Requires calling .byId() or .where() before .execute() is available\n */\n// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\nexport class DeleteBuilder<Occ extends FMTable<any, any>> {\n private readonly databaseName: string;\n private readonly context: ExecutionContext;\n private readonly table: Occ;\n private readonly databaseUseEntityIds: boolean;\n private readonly databaseIncludeSpecialColumns: boolean;\n\n constructor(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n databaseUseEntityIds?: boolean;\n databaseIncludeSpecialColumns?: boolean;\n }) {\n this.table = config.occurrence;\n this.databaseName = config.databaseName;\n this.context = config.context;\n this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;\n this.databaseIncludeSpecialColumns = config.databaseIncludeSpecialColumns ?? false;\n }\n\n /**\n * Delete a single record by ID\n */\n byId(id: string | number): ExecutableDeleteBuilder<Occ> {\n return new ExecutableDeleteBuilder<Occ>({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n mode: \"byId\",\n recordId: id,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n }\n\n /**\n * Delete records matching a filter query\n * @param fn Callback that receives a QueryBuilder for building the filter\n */\n where(fn: (q: QueryBuilder<Occ>) => QueryBuilder<Occ>): ExecutableDeleteBuilder<Occ> {\n // Create a QueryBuilder for the user to configure\n const queryBuilder = new QueryBuilder<Occ>({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n });\n\n // Let the user configure it\n const configuredBuilder = fn(queryBuilder);\n\n return new ExecutableDeleteBuilder<Occ>({\n occurrence: this.table,\n databaseName: this.databaseName,\n context: this.context,\n mode: \"byFilter\",\n queryBuilder: configuredBuilder,\n databaseUseEntityIds: this.databaseUseEntityIds,\n });\n }\n}\n\n/**\n * Executable delete builder - has execute() method\n * Returned after calling .byId() or .where()\n */\n// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration\nexport class ExecutableDeleteBuilder<Occ extends FMTable<any, any>>\n implements ExecutableBuilder<{ deletedCount: number }>\n{\n private readonly databaseName: string;\n private readonly context: ExecutionContext;\n private readonly table: Occ;\n private readonly mode: \"byId\" | \"byFilter\";\n private readonly recordId?: string | number;\n private readonly queryBuilder?: QueryBuilder<Occ>;\n private readonly databaseUseEntityIds: boolean;\n\n constructor(config: {\n occurrence: Occ;\n databaseName: string;\n context: ExecutionContext;\n mode: \"byId\" | \"byFilter\";\n recordId?: string | number;\n queryBuilder?: QueryBuilder<Occ>;\n databaseUseEntityIds?: boolean;\n }) {\n this.table = config.occurrence;\n this.databaseName = config.databaseName;\n this.context = config.context;\n this.mode = config.mode;\n this.recordId = config.recordId;\n this.queryBuilder = config.queryBuilder;\n this.databaseUseEntityIds = config.databaseUseEntityIds ?? false;\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 // If useEntityIds is not set in options, use the database-level setting\n return {\n ...options,\n useEntityIds: options?.useEntityIds ?? this.databaseUseEntityIds,\n };\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 const contextDefault = this.context._getUseEntityIds?.() ?? false;\n const shouldUseIds = useEntityIds ?? contextDefault;\n\n if (shouldUseIds) {\n if (!isUsingEntityIds(this.table)) {\n throw new Error(\n `useEntityIds is true but table \"${getTableName(this.table)}\" does not have entity IDs configured`,\n );\n }\n return getTableIdHelper(this.table);\n }\n\n return getTableName(this.table);\n }\n\n async execute(options?: ExecuteMethodOptions<ExecuteOptions>): Promise<Result<{ deletedCount: number }>> {\n // Merge database-level useEntityIds with per-request options\n const mergedOptions = this.mergeExecuteOptions(options);\n\n // Get table identifier with override support\n const tableId = this.getTableId(mergedOptions.useEntityIds);\n\n let url: string;\n\n if (this.mode === \"byId\") {\n // Delete single record by ID: DELETE /{database}/{table}('id')\n url = `/${this.databaseName}/${tableId}('${this.recordId}')`;\n } else {\n // Delete by filter: DELETE /{database}/{table}?$filter=...\n if (!this.queryBuilder) {\n throw new Error(\"Query builder is required for filter-based delete\");\n }\n\n // Get the query string from the configured QueryBuilder\n const queryString = this.queryBuilder.getQueryString();\n // Remove the leading \"/\" and table name from the query string as we'll build our own URL\n const tableName = getTableName(this.table);\n let queryParams: string;\n if (queryString.startsWith(`/${tableId}`)) {\n queryParams = queryString.slice(`/${tableId}`.length);\n } else if (queryString.startsWith(`/${tableName}`)) {\n queryParams = queryString.slice(`/${tableName}`.length);\n } else {\n queryParams = queryString;\n }\n\n url = `/${this.databaseName}/${tableId}${queryParams}`;\n }\n\n // Make DELETE request\n const result = await this.context._makeRequest(url, {\n method: \"DELETE\",\n ...mergedOptions,\n });\n\n if (result.error) {\n return { data: undefined, error: result.error };\n }\n\n const response = result.data;\n\n // OData returns 204 No Content with fmodata.affected_rows header\n // The _makeRequest should handle extracting the header value\n // For now, we'll check if response contains the count\n let deletedCount = 0;\n\n if (typeof response === \"number\") {\n deletedCount = response;\n } else if (response && typeof response === \"object\") {\n // Check if the response has a count property (fallback)\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API\n deletedCount = (response as any).deletedCount || 0;\n }\n\n return { data: { deletedCount }, error: undefined };\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: Request body can be any JSON-serializable value\n getRequestConfig(): { method: string; url: string; body?: any } {\n // For batch operations, use database-level setting (no per-request override available here)\n const tableId = this.getTableId(this.databaseUseEntityIds);\n\n let url: string;\n\n if (this.mode === \"byId\") {\n url = `/${this.databaseName}/${tableId}('${this.recordId}')`;\n } else {\n if (!this.queryBuilder) {\n throw new Error(\"Query builder is required for filter-based delete\");\n }\n\n const queryString = this.queryBuilder.getQueryString();\n const tableName = getTableName(this.table);\n let queryParams: string;\n if (queryString.startsWith(`/${tableId}`)) {\n queryParams = queryString.slice(`/${tableId}`.length);\n } else if (queryString.startsWith(`/${tableName}`)) {\n queryParams = queryString.slice(`/${tableName}`.length);\n } else {\n queryParams = queryString;\n }\n\n url = `/${this.databaseName}/${tableId}${queryParams}`;\n }\n\n return {\n method: \"DELETE\",\n url,\n };\n }\n\n toRequest(baseUrl: string, options?: ExecuteOptions): Request {\n const config = this.getRequestConfig();\n const fullUrl = `${baseUrl}${config.url}`;\n\n return new Request(fullUrl, {\n method: config.method,\n headers: {\n Accept: getAcceptHeader(options?.includeODataAnnotations),\n },\n });\n }\n\n async processResponse(response: Response, _options?: ExecuteOptions): Promise<Result<{ deletedCount: number }>> {\n // Check for error responses (important for batch operations)\n if (!response.ok) {\n const tableName = getTableName(this.table);\n const error = await parseErrorResponse(response, response.url || `/${this.databaseName}/${tableName}`);\n return { data: undefined, error };\n }\n\n // Check for empty response (204 No Content)\n const text = await response.text();\n if (!text || text.trim() === \"\") {\n // For 204 No Content, check the fmodata.affected_rows header\n const affectedRows = response.headers.get(\"fmodata.affected_rows\");\n const deletedCount = affectedRows ? Number.parseInt(affectedRows, 10) : 1;\n return { data: { deletedCount }, error: undefined };\n }\n\n const rawResponse = JSON.parse(text);\n\n // OData returns 204 No Content with fmodata.affected_rows header\n // The _makeRequest should handle extracting the header value\n // For now, we'll check if response contains the count\n let deletedCount = 0;\n\n if (typeof rawResponse === \"number\") {\n deletedCount = rawResponse;\n } else if (rawResponse && typeof rawResponse === \"object\") {\n // Check if the response has a count property (fallback)\n // biome-ignore lint/suspicious/noExplicitAny: Dynamic response type from OData API\n deletedCount = (rawResponse as any).deletedCount || 0;\n }\n\n return { data: { deletedCount }, error: undefined };\n }\n}\n"],"names":["getTableIdHelper","deletedCount"],"mappings":"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"}