@proofkit/fmodata 0.1.0-beta.24 → 0.1.0-beta.25
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/dist/esm/client/builders/default-select.js +1 -1
- package/dist/esm/client/builders/default-select.js.map +1 -1
- package/dist/esm/client/builders/expand-builder.js +3 -1
- package/dist/esm/client/builders/expand-builder.js.map +1 -1
- package/dist/esm/client/database.d.ts +11 -1
- package/dist/esm/client/database.js +13 -0
- package/dist/esm/client/database.js.map +1 -1
- package/dist/esm/client/filemaker-odata.d.ts +2 -0
- package/dist/esm/client/filemaker-odata.js +3 -0
- package/dist/esm/client/filemaker-odata.js.map +1 -1
- package/package.json +1 -1
- package/src/client/builders/default-select.ts +3 -1
- package/src/client/builders/expand-builder.ts +3 -1
- package/src/client/database.ts +17 -1
- package/src/client/filemaker-odata.ts +3 -0
|
@@ -17,7 +17,7 @@ function getDefaultSelectFields(table, includeSpecialColumns) {
|
|
|
17
17
|
const baseTableConfig = getBaseTableConfig(table);
|
|
18
18
|
const allFields = Object.keys(baseTableConfig.schema);
|
|
19
19
|
const fields = [...new Set(allFields.filter((f) => !containerFields.includes(f)))];
|
|
20
|
-
return fields;
|
|
20
|
+
return fields.length > 0 ? fields : void 0;
|
|
21
21
|
}
|
|
22
22
|
if (Array.isArray(defaultSelect)) {
|
|
23
23
|
return [...new Set(defaultSelect.filter((f) => !containerFields.includes(f)))];
|
|
@@ -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;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;
|
|
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 undefined (meaning \"all\") when schema has no fields with validators,\n // rather than an empty array which would generate an empty $select=\n return fields.length > 0 ? fields : undefined;\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;AASjF,WAAO,OAAO,SAAS,IAAI,SAAS;AAAA,EACtC;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;"}
|
|
@@ -154,7 +154,9 @@ class ExpandBuilder {
|
|
|
154
154
|
if (opts.select) {
|
|
155
155
|
const selectArray = Array.isArray(opts.select) ? opts.select.map(String) : [String(opts.select)];
|
|
156
156
|
const selectFields = formatSelectFields(selectArray, config.targetTable, this.useEntityIds);
|
|
157
|
-
|
|
157
|
+
if (selectFields) {
|
|
158
|
+
parts.push(`$select=${selectFields}`);
|
|
159
|
+
}
|
|
158
160
|
}
|
|
159
161
|
if (opts.filter) {
|
|
160
162
|
const filterQuery = buildQuery({ filter: opts.filter });
|
|
@@ -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 if (selectFields) {\n parts.push(`$select=${selectFields}`);\n }\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,6 +1,7 @@
|
|
|
1
|
+
import { FFetchOptions } from '@fetchkit/ffetch';
|
|
1
2
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
3
|
import { FMTable } from '../orm/table.js';
|
|
3
|
-
import { ExecutableBuilder, ExecutionContext, Metadata } from '../types.js';
|
|
4
|
+
import { ExecutableBuilder, ExecutionContext, Metadata, Result } from '../types.js';
|
|
4
5
|
import { BatchBuilder } from './batch-builder.js';
|
|
5
6
|
import { EntitySet } from './entity-set.js';
|
|
6
7
|
import { SchemaManager } from './schema-manager.js';
|
|
@@ -37,6 +38,10 @@ export declare class Database<IncludeSpecialColumns extends boolean = false> {
|
|
|
37
38
|
*/
|
|
38
39
|
includeSpecialColumns?: IncludeSpecialColumns;
|
|
39
40
|
});
|
|
41
|
+
/**
|
|
42
|
+
* @internal Used by adapter packages to access the database filename.
|
|
43
|
+
*/
|
|
44
|
+
get _getDatabaseName(): string;
|
|
40
45
|
/**
|
|
41
46
|
* @internal Used by EntitySet to access database configuration
|
|
42
47
|
*/
|
|
@@ -45,6 +50,11 @@ export declare class Database<IncludeSpecialColumns extends boolean = false> {
|
|
|
45
50
|
* @internal Used by EntitySet to access database configuration
|
|
46
51
|
*/
|
|
47
52
|
get _getIncludeSpecialColumns(): IncludeSpecialColumns;
|
|
53
|
+
/**
|
|
54
|
+
* @internal Used by adapter packages for raw OData requests.
|
|
55
|
+
* Delegates to the connection's _makeRequest with the database name prepended.
|
|
56
|
+
*/
|
|
57
|
+
_makeRequest<T>(path: string, options?: RequestInit & FFetchOptions): Promise<Result<T>>;
|
|
48
58
|
from<T extends FMTable<any, any>>(table: T): EntitySet<T, IncludeSpecialColumns>;
|
|
49
59
|
/**
|
|
50
60
|
* Retrieves the OData metadata for this database.
|
|
@@ -21,6 +21,12 @@ class Database {
|
|
|
21
21
|
this._useEntityIds = (config == null ? void 0 : config.useEntityIds) ?? false;
|
|
22
22
|
this._includeSpecialColumns = (config == null ? void 0 : config.includeSpecialColumns) ?? false;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* @internal Used by adapter packages to access the database filename.
|
|
26
|
+
*/
|
|
27
|
+
get _getDatabaseName() {
|
|
28
|
+
return this.databaseName;
|
|
29
|
+
}
|
|
24
30
|
/**
|
|
25
31
|
* @internal Used by EntitySet to access database configuration
|
|
26
32
|
*/
|
|
@@ -33,6 +39,13 @@ class Database {
|
|
|
33
39
|
get _getIncludeSpecialColumns() {
|
|
34
40
|
return this._includeSpecialColumns;
|
|
35
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* @internal Used by adapter packages for raw OData requests.
|
|
44
|
+
* Delegates to the connection's _makeRequest with the database name prepended.
|
|
45
|
+
*/
|
|
46
|
+
_makeRequest(path, options) {
|
|
47
|
+
return this.context._makeRequest(`/${this.databaseName}${path}`, options);
|
|
48
|
+
}
|
|
36
49
|
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
|
|
37
50
|
from(table) {
|
|
38
51
|
if (Object.hasOwn(table, FMTable.Symbol.UseEntityIds)) {
|
|
@@ -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 { FFetchOptions } from \"@fetchkit/ffetch\";\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { FMTable } from \"../orm/table\";\nimport type { ExecutableBuilder, ExecutionContext, Metadata, Result } 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 adapter packages to access the database filename.\n */\n get _getDatabaseName(): string {\n return this.databaseName;\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 /**\n * @internal Used by adapter packages for raw OData requests.\n * Delegates to the connection's _makeRequest with the database name prepended.\n */\n _makeRequest<T>(path: string, options?: RequestInit & FFetchOptions): Promise<Result<T>> {\n return this.context._makeRequest<T>(`/${this.databaseName}${path}`, options);\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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"}
|
|
@@ -9,6 +9,8 @@ export declare class FMServerConnection implements ExecutionContext {
|
|
|
9
9
|
private useEntityIds;
|
|
10
10
|
private includeSpecialColumns;
|
|
11
11
|
private readonly logger;
|
|
12
|
+
/** @internal Stored so credential-override flows can inherit non-auth config. */
|
|
13
|
+
readonly _fetchClientOptions: FFetchOptions | undefined;
|
|
12
14
|
constructor(config: {
|
|
13
15
|
serverUrl: string;
|
|
14
16
|
auth: Auth;
|
|
@@ -17,7 +17,10 @@ class FMServerConnection {
|
|
|
17
17
|
__publicField(this, "useEntityIds", false);
|
|
18
18
|
__publicField(this, "includeSpecialColumns", false);
|
|
19
19
|
__publicField(this, "logger");
|
|
20
|
+
/** @internal Stored so credential-override flows can inherit non-auth config. */
|
|
21
|
+
__publicField(this, "_fetchClientOptions");
|
|
20
22
|
this.logger = createLogger(config.logger);
|
|
23
|
+
this._fetchClientOptions = config.fetchClientOptions;
|
|
21
24
|
this.fetchClient = createClient({
|
|
22
25
|
retries: 0,
|
|
23
26
|
...config.fetchClientOptions
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filemaker-odata.js","sources":["../../../src/client/filemaker-odata.ts"],"sourcesContent":["import createClient, {\n AbortError,\n CircuitOpenError,\n type FFetchOptions,\n NetworkError,\n RetryLimitError,\n TimeoutError,\n} from \"@fetchkit/ffetch\";\nimport { get } from \"es-toolkit/compat\";\nimport { HTTPError, ODataError, ResponseParseError, SchemaLockedError } from \"../errors\";\nimport { createLogger, type InternalLogger, type Logger } from \"../logger\";\nimport type { Auth, ExecutionContext, Result } from \"../types\";\nimport { getAcceptHeader } from \"../types\";\nimport { Database } from \"./database\";\nimport { safeJsonParse } from \"./sanitize-json\";\n\nconst TRAILING_SLASH_REGEX = /\\/+$/;\n\nexport class FMServerConnection implements ExecutionContext {\n private readonly fetchClient: ReturnType<typeof createClient>;\n private readonly serverUrl: string;\n private readonly auth: Auth;\n private useEntityIds = false;\n private includeSpecialColumns = false;\n private readonly logger: InternalLogger;\n constructor(config: {\n serverUrl: string;\n auth: Auth;\n fetchClientOptions?: FFetchOptions;\n logger?: Logger;\n }) {\n this.logger = createLogger(config.logger);\n this.fetchClient = createClient({\n retries: 0,\n ...config.fetchClientOptions,\n });\n // Ensure the URL uses https://, is valid, and has no trailing slash\n const url = new URL(config.serverUrl);\n if (url.protocol !== \"https:\") {\n url.protocol = \"https:\";\n }\n // Remove any trailing slash from pathname\n url.pathname = url.pathname.replace(TRAILING_SLASH_REGEX, \"\");\n this.serverUrl = url.toString().replace(TRAILING_SLASH_REGEX, \"\");\n this.auth = config.auth;\n }\n\n /**\n * @internal\n * Sets whether to use FileMaker entity IDs (FMFID/FMTID) in requests\n */\n _setUseEntityIds(useEntityIds: boolean): void {\n this.useEntityIds = useEntityIds;\n }\n\n /**\n * @internal\n * Gets whether to use FileMaker entity IDs (FMFID/FMTID) in requests\n */\n _getUseEntityIds(): boolean {\n return this.useEntityIds;\n }\n\n /**\n * @internal\n * Sets whether to include special columns (ROWID and ROWMODID) in requests\n */\n _setIncludeSpecialColumns(includeSpecialColumns: boolean): void {\n this.includeSpecialColumns = includeSpecialColumns;\n }\n\n /**\n * @internal\n * Gets whether to include special columns (ROWID and ROWMODID) in requests\n */\n _getIncludeSpecialColumns(): boolean {\n return this.includeSpecialColumns;\n }\n\n /**\n * @internal\n * Gets the base URL for OData requests\n */\n _getBaseUrl(): string {\n return `${this.serverUrl}${\"apiKey\" in this.auth ? \"/otto\" : \"\"}/fmi/odata/v4`;\n }\n\n /**\n * @internal\n * Gets the logger instance\n */\n _getLogger(): InternalLogger {\n return this.logger;\n }\n\n /**\n * @internal\n */\n async _makeRequest<T>(\n url: string,\n options?: RequestInit &\n FFetchOptions & {\n useEntityIds?: boolean;\n includeSpecialColumns?: boolean;\n },\n ): Promise<Result<T>> {\n const logger = this._getLogger();\n const baseUrl = `${this.serverUrl}${\"apiKey\" in this.auth ? \"/otto\" : \"\"}/fmi/odata/v4`;\n const fullUrl = baseUrl + url;\n\n // Use per-request override if provided, otherwise use the database-level setting\n const useEntityIds = options?.useEntityIds ?? this.useEntityIds;\n const includeSpecialColumns = options?.includeSpecialColumns ?? this.includeSpecialColumns;\n\n // Get includeODataAnnotations from options (it's passed through from execute options)\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for optional property access\n const includeODataAnnotations = (options as any)?.includeODataAnnotations;\n\n // Build Prefer header as comma-separated list when multiple preferences are set\n const preferValues: string[] = [];\n if (useEntityIds) {\n preferValues.push(\"fmodata.entity-ids\");\n }\n if (includeSpecialColumns) {\n preferValues.push(\"fmodata.include-specialcolumns\");\n }\n\n const headers = {\n Authorization:\n \"apiKey\" in this.auth\n ? `Bearer ${this.auth.apiKey}`\n : `Basic ${btoa(`${this.auth.username}:${this.auth.password}`)}`,\n \"Content-Type\": \"application/json\",\n Accept: getAcceptHeader(includeODataAnnotations),\n ...(preferValues.length > 0 ? { Prefer: preferValues.join(\", \") } : {}),\n ...(options?.headers || {}),\n };\n\n // Prepare loggableHeaders by omitting the Authorization key\n const { Authorization, ...loggableHeaders } = headers;\n logger.debug(\"Request headers:\", loggableHeaders);\n\n // TEMPORARY WORKAROUND: Hopefully this feature will be fixed in the ffetch library\n // Extract fetchHandler and headers separately, only for tests where we're overriding the fetch handler per-request\n const fetchHandler = options?.fetchHandler;\n const { headers: _headers, fetchHandler: _fetchHandler, ...restOptions } = options || {};\n\n // If fetchHandler is provided, create a temporary client with it\n // Otherwise use the existing client\n const clientToUse = fetchHandler ? createClient({ retries: 0, fetchHandler }) : this.fetchClient;\n\n try {\n const finalOptions = {\n ...restOptions,\n headers,\n };\n\n const resp = await clientToUse(fullUrl, finalOptions);\n logger.debug(`${finalOptions.method ?? \"GET\"} ${resp.status} ${fullUrl}`);\n\n // Handle HTTP errors\n if (!resp.ok) {\n // Try to parse error body if it's JSON\n let errorBody: { error?: { code?: string | number; message?: string } } | undefined;\n try {\n if (resp.headers.get(\"content-type\")?.includes(\"application/json\")) {\n errorBody = await safeJsonParse<typeof errorBody>(resp);\n }\n } catch {\n // Ignore JSON parse errors\n }\n\n // Check if it's an OData error response\n if (errorBody?.error) {\n const errorCode = errorBody.error.code;\n const errorMessage = errorBody.error.message || resp.statusText;\n\n // Check for schema locked error (code 303)\n if (errorCode === \"303\" || errorCode === 303) {\n return {\n data: undefined,\n error: new SchemaLockedError(fullUrl, errorMessage, errorBody.error),\n };\n }\n\n return {\n data: undefined,\n error: new ODataError(fullUrl, errorMessage, String(errorCode), errorBody.error),\n };\n }\n\n return {\n data: undefined,\n error: new HTTPError(fullUrl, resp.status, resp.statusText, errorBody),\n };\n }\n\n // Check for affected rows header (for DELETE and bulk PATCH operations)\n // FileMaker may return this with 204 No Content or 200 OK\n const affectedRows = resp.headers.get(\"fmodata.affected_rows\");\n if (affectedRows !== null) {\n return { data: Number.parseInt(affectedRows, 10) as T, error: undefined };\n }\n\n // Handle 204 No Content with no body\n if (resp.status === 204) {\n // Check for Location header (used for insert with return=minimal)\n // Use optional chaining for safety with mocks that might not have proper headers\n const locationHeader = resp.headers?.get?.(\"Location\") || resp.headers?.get?.(\"location\");\n if (locationHeader) {\n // Return the location header so InsertBuilder can extract ROWID\n return { data: { _location: locationHeader } as T, error: undefined };\n }\n return { data: 0 as T, error: undefined };\n }\n\n // Parse response\n if (resp.headers.get(\"content-type\")?.includes(\"application/json\")) {\n const data = await safeJsonParse<T & { error?: { code?: string | number; message?: string } }>(resp);\n\n // Check for embedded OData errors\n if (get(data, \"error\", null)) {\n const errorCode = get(data, \"error.code\", null);\n const errorMessage = get(data, \"error.message\", \"Unknown OData error\");\n\n // Check for schema locked error (code 303)\n if (errorCode === \"303\" || errorCode === 303) {\n return {\n data: undefined,\n error: new SchemaLockedError(fullUrl, errorMessage, data.error),\n };\n }\n\n return {\n data: undefined,\n error: new ODataError(fullUrl, errorMessage, String(errorCode), data.error),\n };\n }\n\n return { data: data as T, error: undefined };\n }\n\n return { data: (await resp.text()) as T, error: undefined };\n } catch (err) {\n // Map ffetch errors - return them directly (no re-wrapping)\n if (\n err instanceof TimeoutError ||\n err instanceof AbortError ||\n err instanceof NetworkError ||\n err instanceof RetryLimitError ||\n err instanceof CircuitOpenError\n ) {\n return { data: undefined, error: err };\n }\n\n // Handle JSON parse errors (ResponseParseError from safeJsonParse)\n if (err instanceof ResponseParseError) {\n return { data: undefined, error: err };\n }\n\n // Unknown error - wrap it as NetworkError\n return {\n data: undefined,\n error: new NetworkError(fullUrl, err),\n };\n }\n }\n\n database<IncludeSpecialColumns extends boolean = false>(\n name: string,\n config?: {\n useEntityIds?: boolean;\n includeSpecialColumns?: IncludeSpecialColumns;\n },\n ): Database<IncludeSpecialColumns> {\n return new Database<IncludeSpecialColumns>(name, this, config);\n }\n\n /**\n * Lists all available databases from the FileMaker OData service.\n * @returns Promise resolving to an array of database names\n */\n async listDatabaseNames(): Promise<string[]> {\n const result = await this._makeRequest<{\n value?: Array<{ name: string }>;\n }>(\"/$metadata\", { headers: { Accept: \"application/json\" } });\n if (result.error) {\n throw result.error;\n }\n if (result.data.value && Array.isArray(result.data.value)) {\n return result.data.value.map((item) => item.name);\n }\n return [];\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"filemaker-odata.js","sources":["../../../src/client/filemaker-odata.ts"],"sourcesContent":["import createClient, {\n AbortError,\n CircuitOpenError,\n type FFetchOptions,\n NetworkError,\n RetryLimitError,\n TimeoutError,\n} from \"@fetchkit/ffetch\";\nimport { get } from \"es-toolkit/compat\";\nimport { HTTPError, ODataError, ResponseParseError, SchemaLockedError } from \"../errors\";\nimport { createLogger, type InternalLogger, type Logger } from \"../logger\";\nimport type { Auth, ExecutionContext, Result } from \"../types\";\nimport { getAcceptHeader } from \"../types\";\nimport { Database } from \"./database\";\nimport { safeJsonParse } from \"./sanitize-json\";\n\nconst TRAILING_SLASH_REGEX = /\\/+$/;\n\nexport class FMServerConnection implements ExecutionContext {\n private readonly fetchClient: ReturnType<typeof createClient>;\n private readonly serverUrl: string;\n private readonly auth: Auth;\n private useEntityIds = false;\n private includeSpecialColumns = false;\n private readonly logger: InternalLogger;\n /** @internal Stored so credential-override flows can inherit non-auth config. */\n readonly _fetchClientOptions: FFetchOptions | undefined;\n constructor(config: {\n serverUrl: string;\n auth: Auth;\n fetchClientOptions?: FFetchOptions;\n logger?: Logger;\n }) {\n this.logger = createLogger(config.logger);\n this._fetchClientOptions = config.fetchClientOptions;\n this.fetchClient = createClient({\n retries: 0,\n ...config.fetchClientOptions,\n });\n // Ensure the URL uses https://, is valid, and has no trailing slash\n const url = new URL(config.serverUrl);\n if (url.protocol !== \"https:\") {\n url.protocol = \"https:\";\n }\n // Remove any trailing slash from pathname\n url.pathname = url.pathname.replace(TRAILING_SLASH_REGEX, \"\");\n this.serverUrl = url.toString().replace(TRAILING_SLASH_REGEX, \"\");\n this.auth = config.auth;\n }\n\n /**\n * @internal\n * Sets whether to use FileMaker entity IDs (FMFID/FMTID) in requests\n */\n _setUseEntityIds(useEntityIds: boolean): void {\n this.useEntityIds = useEntityIds;\n }\n\n /**\n * @internal\n * Gets whether to use FileMaker entity IDs (FMFID/FMTID) in requests\n */\n _getUseEntityIds(): boolean {\n return this.useEntityIds;\n }\n\n /**\n * @internal\n * Sets whether to include special columns (ROWID and ROWMODID) in requests\n */\n _setIncludeSpecialColumns(includeSpecialColumns: boolean): void {\n this.includeSpecialColumns = includeSpecialColumns;\n }\n\n /**\n * @internal\n * Gets whether to include special columns (ROWID and ROWMODID) in requests\n */\n _getIncludeSpecialColumns(): boolean {\n return this.includeSpecialColumns;\n }\n\n /**\n * @internal\n * Gets the base URL for OData requests\n */\n _getBaseUrl(): string {\n return `${this.serverUrl}${\"apiKey\" in this.auth ? \"/otto\" : \"\"}/fmi/odata/v4`;\n }\n\n /**\n * @internal\n * Gets the logger instance\n */\n _getLogger(): InternalLogger {\n return this.logger;\n }\n\n /**\n * @internal\n */\n async _makeRequest<T>(\n url: string,\n options?: RequestInit &\n FFetchOptions & {\n useEntityIds?: boolean;\n includeSpecialColumns?: boolean;\n },\n ): Promise<Result<T>> {\n const logger = this._getLogger();\n const baseUrl = `${this.serverUrl}${\"apiKey\" in this.auth ? \"/otto\" : \"\"}/fmi/odata/v4`;\n const fullUrl = baseUrl + url;\n\n // Use per-request override if provided, otherwise use the database-level setting\n const useEntityIds = options?.useEntityIds ?? this.useEntityIds;\n const includeSpecialColumns = options?.includeSpecialColumns ?? this.includeSpecialColumns;\n\n // Get includeODataAnnotations from options (it's passed through from execute options)\n // biome-ignore lint/suspicious/noExplicitAny: Type assertion for optional property access\n const includeODataAnnotations = (options as any)?.includeODataAnnotations;\n\n // Build Prefer header as comma-separated list when multiple preferences are set\n const preferValues: string[] = [];\n if (useEntityIds) {\n preferValues.push(\"fmodata.entity-ids\");\n }\n if (includeSpecialColumns) {\n preferValues.push(\"fmodata.include-specialcolumns\");\n }\n\n const headers = {\n Authorization:\n \"apiKey\" in this.auth\n ? `Bearer ${this.auth.apiKey}`\n : `Basic ${btoa(`${this.auth.username}:${this.auth.password}`)}`,\n \"Content-Type\": \"application/json\",\n Accept: getAcceptHeader(includeODataAnnotations),\n ...(preferValues.length > 0 ? { Prefer: preferValues.join(\", \") } : {}),\n ...(options?.headers || {}),\n };\n\n // Prepare loggableHeaders by omitting the Authorization key\n const { Authorization, ...loggableHeaders } = headers;\n logger.debug(\"Request headers:\", loggableHeaders);\n\n // TEMPORARY WORKAROUND: Hopefully this feature will be fixed in the ffetch library\n // Extract fetchHandler and headers separately, only for tests where we're overriding the fetch handler per-request\n const fetchHandler = options?.fetchHandler;\n const { headers: _headers, fetchHandler: _fetchHandler, ...restOptions } = options || {};\n\n // If fetchHandler is provided, create a temporary client with it\n // Otherwise use the existing client\n const clientToUse = fetchHandler ? createClient({ retries: 0, fetchHandler }) : this.fetchClient;\n\n try {\n const finalOptions = {\n ...restOptions,\n headers,\n };\n\n const resp = await clientToUse(fullUrl, finalOptions);\n logger.debug(`${finalOptions.method ?? \"GET\"} ${resp.status} ${fullUrl}`);\n\n // Handle HTTP errors\n if (!resp.ok) {\n // Try to parse error body if it's JSON\n let errorBody: { error?: { code?: string | number; message?: string } } | undefined;\n try {\n if (resp.headers.get(\"content-type\")?.includes(\"application/json\")) {\n errorBody = await safeJsonParse<typeof errorBody>(resp);\n }\n } catch {\n // Ignore JSON parse errors\n }\n\n // Check if it's an OData error response\n if (errorBody?.error) {\n const errorCode = errorBody.error.code;\n const errorMessage = errorBody.error.message || resp.statusText;\n\n // Check for schema locked error (code 303)\n if (errorCode === \"303\" || errorCode === 303) {\n return {\n data: undefined,\n error: new SchemaLockedError(fullUrl, errorMessage, errorBody.error),\n };\n }\n\n return {\n data: undefined,\n error: new ODataError(fullUrl, errorMessage, String(errorCode), errorBody.error),\n };\n }\n\n return {\n data: undefined,\n error: new HTTPError(fullUrl, resp.status, resp.statusText, errorBody),\n };\n }\n\n // Check for affected rows header (for DELETE and bulk PATCH operations)\n // FileMaker may return this with 204 No Content or 200 OK\n const affectedRows = resp.headers.get(\"fmodata.affected_rows\");\n if (affectedRows !== null) {\n return { data: Number.parseInt(affectedRows, 10) as T, error: undefined };\n }\n\n // Handle 204 No Content with no body\n if (resp.status === 204) {\n // Check for Location header (used for insert with return=minimal)\n // Use optional chaining for safety with mocks that might not have proper headers\n const locationHeader = resp.headers?.get?.(\"Location\") || resp.headers?.get?.(\"location\");\n if (locationHeader) {\n // Return the location header so InsertBuilder can extract ROWID\n return { data: { _location: locationHeader } as T, error: undefined };\n }\n return { data: 0 as T, error: undefined };\n }\n\n // Parse response\n if (resp.headers.get(\"content-type\")?.includes(\"application/json\")) {\n const data = await safeJsonParse<T & { error?: { code?: string | number; message?: string } }>(resp);\n\n // Check for embedded OData errors\n if (get(data, \"error\", null)) {\n const errorCode = get(data, \"error.code\", null);\n const errorMessage = get(data, \"error.message\", \"Unknown OData error\");\n\n // Check for schema locked error (code 303)\n if (errorCode === \"303\" || errorCode === 303) {\n return {\n data: undefined,\n error: new SchemaLockedError(fullUrl, errorMessage, data.error),\n };\n }\n\n return {\n data: undefined,\n error: new ODataError(fullUrl, errorMessage, String(errorCode), data.error),\n };\n }\n\n return { data: data as T, error: undefined };\n }\n\n return { data: (await resp.text()) as T, error: undefined };\n } catch (err) {\n // Map ffetch errors - return them directly (no re-wrapping)\n if (\n err instanceof TimeoutError ||\n err instanceof AbortError ||\n err instanceof NetworkError ||\n err instanceof RetryLimitError ||\n err instanceof CircuitOpenError\n ) {\n return { data: undefined, error: err };\n }\n\n // Handle JSON parse errors (ResponseParseError from safeJsonParse)\n if (err instanceof ResponseParseError) {\n return { data: undefined, error: err };\n }\n\n // Unknown error - wrap it as NetworkError\n return {\n data: undefined,\n error: new NetworkError(fullUrl, err),\n };\n }\n }\n\n database<IncludeSpecialColumns extends boolean = false>(\n name: string,\n config?: {\n useEntityIds?: boolean;\n includeSpecialColumns?: IncludeSpecialColumns;\n },\n ): Database<IncludeSpecialColumns> {\n return new Database<IncludeSpecialColumns>(name, this, config);\n }\n\n /**\n * Lists all available databases from the FileMaker OData service.\n * @returns Promise resolving to an array of database names\n */\n async listDatabaseNames(): Promise<string[]> {\n const result = await this._makeRequest<{\n value?: Array<{ name: string }>;\n }>(\"/$metadata\", { headers: { Accept: \"application/json\" } });\n if (result.error) {\n throw result.error;\n }\n if (result.data.value && Array.isArray(result.data.value)) {\n return result.data.value.map((item) => item.name);\n }\n return [];\n }\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -47,7 +47,9 @@ export function getDefaultSelectFields(
|
|
|
47
47
|
fields.push("ROWID", "ROWMODID");
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
// Return undefined (meaning "all") when schema has no fields with validators,
|
|
51
|
+
// rather than an empty array which would generate an empty $select=
|
|
52
|
+
return fields.length > 0 ? fields : undefined;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
if (Array.isArray(defaultSelect)) {
|
|
@@ -214,7 +214,9 @@ export class ExpandBuilder {
|
|
|
214
214
|
if (opts.select) {
|
|
215
215
|
const selectArray = Array.isArray(opts.select) ? opts.select.map(String) : [String(opts.select)];
|
|
216
216
|
const selectFields = formatSelectFields(selectArray, config.targetTable, this.useEntityIds);
|
|
217
|
-
|
|
217
|
+
if (selectFields) {
|
|
218
|
+
parts.push(`$select=${selectFields}`);
|
|
219
|
+
}
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
if (opts.filter) {
|
package/src/client/database.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { FFetchOptions } from "@fetchkit/ffetch";
|
|
1
2
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
3
|
import { FMTable } from "../orm/table";
|
|
3
|
-
import type { ExecutableBuilder, ExecutionContext, Metadata } from "../types";
|
|
4
|
+
import type { ExecutableBuilder, ExecutionContext, Metadata, Result } from "../types";
|
|
4
5
|
import { BatchBuilder } from "./batch-builder";
|
|
5
6
|
import { EntitySet } from "./entity-set";
|
|
6
7
|
import { SchemaManager } from "./schema-manager";
|
|
@@ -53,6 +54,13 @@ export class Database<IncludeSpecialColumns extends boolean = false> {
|
|
|
53
54
|
this._includeSpecialColumns = (config?.includeSpecialColumns ?? false) as IncludeSpecialColumns;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* @internal Used by adapter packages to access the database filename.
|
|
59
|
+
*/
|
|
60
|
+
get _getDatabaseName(): string {
|
|
61
|
+
return this.databaseName;
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
/**
|
|
57
65
|
* @internal Used by EntitySet to access database configuration
|
|
58
66
|
*/
|
|
@@ -67,6 +75,14 @@ export class Database<IncludeSpecialColumns extends boolean = false> {
|
|
|
67
75
|
return this._includeSpecialColumns;
|
|
68
76
|
}
|
|
69
77
|
|
|
78
|
+
/**
|
|
79
|
+
* @internal Used by adapter packages for raw OData requests.
|
|
80
|
+
* Delegates to the connection's _makeRequest with the database name prepended.
|
|
81
|
+
*/
|
|
82
|
+
_makeRequest<T>(path: string, options?: RequestInit & FFetchOptions): Promise<Result<T>> {
|
|
83
|
+
return this.context._makeRequest<T>(`/${this.databaseName}${path}`, options);
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
// biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
|
|
71
87
|
from<T extends FMTable<any, any>>(table: T): EntitySet<T, IncludeSpecialColumns> {
|
|
72
88
|
// Only override database-level useEntityIds if table explicitly sets it
|
|
@@ -23,6 +23,8 @@ export class FMServerConnection implements ExecutionContext {
|
|
|
23
23
|
private useEntityIds = false;
|
|
24
24
|
private includeSpecialColumns = false;
|
|
25
25
|
private readonly logger: InternalLogger;
|
|
26
|
+
/** @internal Stored so credential-override flows can inherit non-auth config. */
|
|
27
|
+
readonly _fetchClientOptions: FFetchOptions | undefined;
|
|
26
28
|
constructor(config: {
|
|
27
29
|
serverUrl: string;
|
|
28
30
|
auth: Auth;
|
|
@@ -30,6 +32,7 @@ export class FMServerConnection implements ExecutionContext {
|
|
|
30
32
|
logger?: Logger;
|
|
31
33
|
}) {
|
|
32
34
|
this.logger = createLogger(config.logger);
|
|
35
|
+
this._fetchClientOptions = config.fetchClientOptions;
|
|
33
36
|
this.fetchClient = createClient({
|
|
34
37
|
retries: 0,
|
|
35
38
|
...config.fetchClientOptions,
|