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