@proofkit/fmodata 0.1.0-alpha.9 → 0.1.0-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +651 -449
  3. package/dist/esm/client/batch-builder.d.ts +10 -9
  4. package/dist/esm/client/batch-builder.js +119 -56
  5. package/dist/esm/client/batch-builder.js.map +1 -1
  6. package/dist/esm/client/batch-request.js +16 -21
  7. package/dist/esm/client/batch-request.js.map +1 -1
  8. package/dist/esm/client/builders/default-select.d.ts +10 -0
  9. package/dist/esm/client/builders/default-select.js +41 -0
  10. package/dist/esm/client/builders/default-select.js.map +1 -0
  11. package/dist/esm/client/builders/expand-builder.d.ts +45 -0
  12. package/dist/esm/client/builders/expand-builder.js +185 -0
  13. package/dist/esm/client/builders/expand-builder.js.map +1 -0
  14. package/dist/esm/client/builders/index.d.ts +9 -0
  15. package/dist/esm/client/builders/query-string-builder.d.ts +18 -0
  16. package/dist/esm/client/builders/query-string-builder.js +21 -0
  17. package/dist/esm/client/builders/query-string-builder.js.map +1 -0
  18. package/dist/esm/client/builders/response-processor.d.ts +43 -0
  19. package/dist/esm/client/builders/response-processor.js +175 -0
  20. package/dist/esm/client/builders/response-processor.js.map +1 -0
  21. package/dist/esm/client/builders/select-mixin.d.ts +25 -0
  22. package/dist/esm/client/builders/select-mixin.js +28 -0
  23. package/dist/esm/client/builders/select-mixin.js.map +1 -0
  24. package/dist/esm/client/builders/select-utils.d.ts +18 -0
  25. package/dist/esm/client/builders/select-utils.js +30 -0
  26. package/dist/esm/client/builders/select-utils.js.map +1 -0
  27. package/dist/esm/client/builders/shared-types.d.ts +40 -0
  28. package/dist/esm/client/builders/table-utils.d.ts +35 -0
  29. package/dist/esm/client/builders/table-utils.js +44 -0
  30. package/dist/esm/client/builders/table-utils.js.map +1 -0
  31. package/dist/esm/client/database.d.ts +34 -22
  32. package/dist/esm/client/database.js +48 -84
  33. package/dist/esm/client/database.js.map +1 -1
  34. package/dist/esm/client/delete-builder.d.ts +25 -30
  35. package/dist/esm/client/delete-builder.js +45 -30
  36. package/dist/esm/client/delete-builder.js.map +1 -1
  37. package/dist/esm/client/entity-set.d.ts +35 -43
  38. package/dist/esm/client/entity-set.js +110 -52
  39. package/dist/esm/client/entity-set.js.map +1 -1
  40. package/dist/esm/client/error-parser.d.ts +12 -0
  41. package/dist/esm/client/error-parser.js +25 -0
  42. package/dist/esm/client/error-parser.js.map +1 -0
  43. package/dist/esm/client/filemaker-odata.d.ts +26 -7
  44. package/dist/esm/client/filemaker-odata.js +65 -42
  45. package/dist/esm/client/filemaker-odata.js.map +1 -1
  46. package/dist/esm/client/insert-builder.d.ts +19 -24
  47. package/dist/esm/client/insert-builder.js +94 -58
  48. package/dist/esm/client/insert-builder.js.map +1 -1
  49. package/dist/esm/client/query/expand-builder.d.ts +35 -0
  50. package/dist/esm/client/query/index.d.ts +4 -0
  51. package/dist/esm/client/query/query-builder.d.ts +132 -0
  52. package/dist/esm/client/query/query-builder.js +456 -0
  53. package/dist/esm/client/query/query-builder.js.map +1 -0
  54. package/dist/esm/client/query/response-processor.d.ts +25 -0
  55. package/dist/esm/client/query/types.d.ts +77 -0
  56. package/dist/esm/client/query/url-builder.d.ts +71 -0
  57. package/dist/esm/client/query/url-builder.js +100 -0
  58. package/dist/esm/client/query/url-builder.js.map +1 -0
  59. package/dist/esm/client/query-builder.d.ts +2 -115
  60. package/dist/esm/client/record-builder.d.ts +108 -36
  61. package/dist/esm/client/record-builder.js +284 -119
  62. package/dist/esm/client/record-builder.js.map +1 -1
  63. package/dist/esm/client/response-processor.d.ts +4 -9
  64. package/dist/esm/client/sanitize-json.d.ts +35 -0
  65. package/dist/esm/client/sanitize-json.js +27 -0
  66. package/dist/esm/client/sanitize-json.js.map +1 -0
  67. package/dist/esm/client/schema-manager.d.ts +5 -5
  68. package/dist/esm/client/schema-manager.js +45 -31
  69. package/dist/esm/client/schema-manager.js.map +1 -1
  70. package/dist/esm/client/update-builder.d.ts +34 -40
  71. package/dist/esm/client/update-builder.js +99 -58
  72. package/dist/esm/client/update-builder.js.map +1 -1
  73. package/dist/esm/client/webhook-builder.d.ts +126 -0
  74. package/dist/esm/client/webhook-builder.js +189 -0
  75. package/dist/esm/client/webhook-builder.js.map +1 -0
  76. package/dist/esm/errors.d.ts +19 -2
  77. package/dist/esm/errors.js +39 -4
  78. package/dist/esm/errors.js.map +1 -1
  79. package/dist/esm/index.d.ts +10 -8
  80. package/dist/esm/index.js +40 -10
  81. package/dist/esm/index.js.map +1 -1
  82. package/dist/esm/logger.d.ts +47 -0
  83. package/dist/esm/logger.js +69 -0
  84. package/dist/esm/logger.js.map +1 -0
  85. package/dist/esm/logger.test.d.ts +1 -0
  86. package/dist/esm/orm/column.d.ts +62 -0
  87. package/dist/esm/orm/column.js +63 -0
  88. package/dist/esm/orm/column.js.map +1 -0
  89. package/dist/esm/orm/field-builders.d.ts +164 -0
  90. package/dist/esm/orm/field-builders.js +158 -0
  91. package/dist/esm/orm/field-builders.js.map +1 -0
  92. package/dist/esm/orm/index.d.ts +5 -0
  93. package/dist/esm/orm/operators.d.ts +173 -0
  94. package/dist/esm/orm/operators.js +260 -0
  95. package/dist/esm/orm/operators.js.map +1 -0
  96. package/dist/esm/orm/table.d.ts +355 -0
  97. package/dist/esm/orm/table.js +202 -0
  98. package/dist/esm/orm/table.js.map +1 -0
  99. package/dist/esm/transform.d.ts +20 -21
  100. package/dist/esm/transform.js +44 -45
  101. package/dist/esm/transform.js.map +1 -1
  102. package/dist/esm/types.d.ts +96 -30
  103. package/dist/esm/types.js +7 -0
  104. package/dist/esm/types.js.map +1 -0
  105. package/dist/esm/validation.d.ts +22 -12
  106. package/dist/esm/validation.js +132 -85
  107. package/dist/esm/validation.js.map +1 -1
  108. package/package.json +28 -20
  109. package/src/client/batch-builder.ts +153 -89
  110. package/src/client/batch-request.ts +25 -41
  111. package/src/client/builders/default-select.ts +75 -0
  112. package/src/client/builders/expand-builder.ts +246 -0
  113. package/src/client/builders/index.ts +11 -0
  114. package/src/client/builders/query-string-builder.ts +46 -0
  115. package/src/client/builders/response-processor.ts +279 -0
  116. package/src/client/builders/select-mixin.ts +65 -0
  117. package/src/client/builders/select-utils.ts +59 -0
  118. package/src/client/builders/shared-types.ts +45 -0
  119. package/src/client/builders/table-utils.ts +83 -0
  120. package/src/client/database.ts +89 -183
  121. package/src/client/delete-builder.ts +74 -84
  122. package/src/client/entity-set.ts +266 -293
  123. package/src/client/error-parser.ts +41 -0
  124. package/src/client/filemaker-odata.ts +98 -66
  125. package/src/client/insert-builder.ts +157 -118
  126. package/src/client/query/expand-builder.ts +160 -0
  127. package/src/client/query/index.ts +14 -0
  128. package/src/client/query/query-builder.ts +729 -0
  129. package/src/client/query/response-processor.ts +226 -0
  130. package/src/client/query/types.ts +126 -0
  131. package/src/client/query/url-builder.ts +151 -0
  132. package/src/client/query-builder.ts +10 -1455
  133. package/src/client/record-builder.ts +575 -240
  134. package/src/client/response-processor.ts +15 -42
  135. package/src/client/sanitize-json.ts +64 -0
  136. package/src/client/schema-manager.ts +61 -76
  137. package/src/client/update-builder.ts +161 -143
  138. package/src/client/webhook-builder.ts +265 -0
  139. package/src/errors.ts +49 -16
  140. package/src/index.ts +99 -54
  141. package/src/logger.test.ts +34 -0
  142. package/src/logger.ts +116 -0
  143. package/src/orm/column.ts +106 -0
  144. package/src/orm/field-builders.ts +250 -0
  145. package/src/orm/index.ts +61 -0
  146. package/src/orm/operators.ts +473 -0
  147. package/src/orm/table.ts +741 -0
  148. package/src/transform.ts +90 -70
  149. package/src/types.ts +154 -113
  150. package/src/validation.ts +200 -115
  151. package/dist/esm/client/base-table.d.ts +0 -125
  152. package/dist/esm/client/base-table.js +0 -57
  153. package/dist/esm/client/base-table.js.map +0 -1
  154. package/dist/esm/client/query-builder.js +0 -896
  155. package/dist/esm/client/query-builder.js.map +0 -1
  156. package/dist/esm/client/table-occurrence.d.ts +0 -72
  157. package/dist/esm/client/table-occurrence.js +0 -74
  158. package/dist/esm/client/table-occurrence.js.map +0 -1
  159. package/dist/esm/filter-types.d.ts +0 -76
  160. package/src/client/base-table.ts +0 -175
  161. package/src/client/query-builder.ts.bak +0 -1457
  162. package/src/client/table-occurrence.ts +0 -175
  163. package/src/filter-types.ts +0 -97
@@ -0,0 +1,59 @@
1
+ import type { FMTable } from "../../orm/table";
2
+ import { transformFieldNamesArray } from "../../transform";
3
+
4
+ const VALID_FIELD_NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9]*$/;
5
+
6
+ /**
7
+ * Determines if a field name needs to be quoted in OData queries.
8
+ * Per FileMaker docs: field names with special characters (spaces, underscores, etc.) must be quoted.
9
+ * Also quotes "id" as it's an OData reserved word.
10
+ * Entity IDs (FMFID:*, FMTID:*) are not quoted as they're identifiers, not field names.
11
+ *
12
+ * @param fieldName - The field name or identifier to check
13
+ * @returns true if the field name should be quoted in OData queries
14
+ */
15
+ export function needsFieldQuoting(fieldName: string): boolean {
16
+ // Entity IDs are identifiers and don't need quoting
17
+ if (fieldName.startsWith("FMFID:") || fieldName.startsWith("FMTID:")) {
18
+ return false;
19
+ }
20
+ // Always quote "id" as it's an OData reserved word
21
+ if (fieldName === "id") {
22
+ return true;
23
+ }
24
+ // Quote if field name contains spaces, underscores, or other special characters
25
+ return fieldName.includes(" ") || fieldName.includes("_") || !VALID_FIELD_NAME_REGEX.test(fieldName);
26
+ }
27
+
28
+ /**
29
+ * Formats select fields for use in OData query strings.
30
+ * - Transforms field names to FMFIDs if using entity IDs
31
+ * - Wraps "id" fields in double quotes (OData reserved)
32
+ * - URL-encodes special characters but preserves spaces
33
+ */
34
+ export function formatSelectFields(
35
+ select: string[] | readonly string[] | undefined,
36
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
37
+ table?: FMTable<any, any>,
38
+ useEntityIds?: boolean,
39
+ ): string {
40
+ if (!select || select.length === 0) {
41
+ return "";
42
+ }
43
+
44
+ const selectArray = Array.isArray(select) ? select : [select];
45
+
46
+ // Transform to field IDs if using entity IDs
47
+ const transformedFields =
48
+ table && useEntityIds ? transformFieldNamesArray(selectArray.map(String), table) : selectArray.map(String);
49
+
50
+ return transformedFields
51
+ .map((field) => {
52
+ if (needsFieldQuoting(field)) {
53
+ return `"${field}"`;
54
+ }
55
+ const encoded = encodeURIComponent(field);
56
+ return encoded.replace(/%20/g, " ");
57
+ })
58
+ .join(",");
59
+ }
@@ -0,0 +1,45 @@
1
+ import type { QueryOptions } from "odata-query";
2
+ import type { FMTable } from "../../orm/table";
3
+ import type { ExecutionContext } from "../../types";
4
+
5
+ /**
6
+ * Expand configuration used by both QueryBuilder and RecordBuilder
7
+ */
8
+ export interface ExpandConfig {
9
+ relation: string;
10
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any QueryOptions configuration
11
+ options?: Partial<QueryOptions<any>>;
12
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
13
+ targetTable?: FMTable<any, any>;
14
+ nestedExpandConfigs?: ExpandConfig[];
15
+ }
16
+
17
+ /**
18
+ * Type to represent expanded relations in return types
19
+ */
20
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic schema and selected types from user input
21
+ export type ExpandedRelations = Record<string, { schema: any; selected: any }>;
22
+
23
+ /**
24
+ * Navigation context shared between builders
25
+ */
26
+ export interface NavigationContext {
27
+ isNavigate?: boolean;
28
+ navigateRecordId?: string | number;
29
+ navigateRelation?: string;
30
+ navigateSourceTableName?: string;
31
+ navigateBaseRelation?: string;
32
+ navigateBasePath?: string;
33
+ }
34
+
35
+ /**
36
+ * Common builder configuration
37
+ */
38
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
39
+ export interface BuilderConfig<Occ extends FMTable<any, any> | undefined> {
40
+ occurrence?: Occ;
41
+ tableName: string;
42
+ databaseName: string;
43
+ context: ExecutionContext;
44
+ databaseUseEntityIds?: boolean;
45
+ }
@@ -0,0 +1,83 @@
1
+ import type { FFetchOptions } from "@fetchkit/ffetch";
2
+ import type { FMTable } from "../../orm/table";
3
+ import { getTableId as getTableIdHelper, getTableName, isUsingEntityIds } from "../../orm/table";
4
+ import type { ExecuteOptions, ExecutionContext } from "../../types";
5
+ import { getAcceptHeader } from "../../types";
6
+
7
+ /**
8
+ * Resolves table identifier based on entity ID settings.
9
+ * Used by both QueryBuilder and RecordBuilder.
10
+ */
11
+ export function resolveTableId(
12
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
13
+ table: FMTable<any, any> | undefined,
14
+ fallbackTableName: string,
15
+ context: ExecutionContext,
16
+ useEntityIdsOverride?: boolean,
17
+ ): string {
18
+ if (!table) {
19
+ return fallbackTableName;
20
+ }
21
+
22
+ const contextDefault = context._getUseEntityIds?.() ?? false;
23
+ const shouldUseIds = useEntityIdsOverride ?? contextDefault;
24
+
25
+ if (shouldUseIds) {
26
+ if (!isUsingEntityIds(table)) {
27
+ throw new Error(`useEntityIds is true but table "${getTableName(table)}" does not have entity IDs configured`);
28
+ }
29
+ return getTableIdHelper(table);
30
+ }
31
+
32
+ return getTableName(table);
33
+ }
34
+
35
+ /**
36
+ * Merges database-level useEntityIds with per-request options.
37
+ */
38
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape
39
+ export function mergeEntityIdOptions<T extends Record<string, any>>(
40
+ options: T | undefined,
41
+ databaseDefault: boolean,
42
+ ): T & { useEntityIds?: boolean } {
43
+ return {
44
+ ...options,
45
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for optional property access
46
+ useEntityIds: (options as any)?.useEntityIds ?? databaseDefault,
47
+ } as T & { useEntityIds?: boolean };
48
+ }
49
+
50
+ /**
51
+ * Type-safe helper for merging execute options with entity ID settings
52
+ */
53
+ export function mergeExecuteOptions(
54
+ options: (RequestInit & FFetchOptions & ExecuteOptions) | undefined,
55
+ databaseUseEntityIds: boolean,
56
+ ): RequestInit & FFetchOptions & { useEntityIds?: boolean } {
57
+ return mergeEntityIdOptions(options, databaseUseEntityIds);
58
+ }
59
+
60
+ /**
61
+ * Creates an OData Request object with proper headers.
62
+ * Used by both QueryBuilder and RecordBuilder to eliminate duplication.
63
+ *
64
+ * @param baseUrl - Base URL for the request
65
+ * @param config - Request configuration with method and url
66
+ * @param options - Optional execution options
67
+ * @returns Request object ready to use
68
+ */
69
+ export function createODataRequest(
70
+ baseUrl: string,
71
+ config: { method: string; url: string },
72
+ options?: { includeODataAnnotations?: boolean },
73
+ ): Request {
74
+ const fullUrl = `${baseUrl}${config.url}`;
75
+
76
+ return new Request(fullUrl, {
77
+ method: config.method,
78
+ headers: {
79
+ "Content-Type": "application/json",
80
+ Accept: getAcceptHeader(options?.includeODataAnnotations),
81
+ },
82
+ });
83
+ }
@@ -1,208 +1,120 @@
1
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
2
- import type { ExecutionContext, ExecutableBuilder, Metadata } from "../types";
3
- import type { BaseTable } from "./base-table";
4
- import type { TableOccurrence } from "./table-occurrence";
5
- import { EntitySet } from "./entity-set";
2
+ import { FMTable } from "../orm/table";
3
+ import type { ExecutableBuilder, ExecutionContext, Metadata } from "../types";
6
4
  import { BatchBuilder } from "./batch-builder";
5
+ import { EntitySet } from "./entity-set";
7
6
  import { SchemaManager } from "./schema-manager";
7
+ import { WebhookManager } from "./webhook-builder";
8
8
 
9
- // Helper type to extract schema from a TableOccurrence
10
- type ExtractSchemaFromOccurrence<O> =
11
- O extends TableOccurrence<infer BT, any, any, any>
12
- ? BT extends BaseTable<infer S, any, any, any>
13
- ? S
14
- : never
15
- : never;
16
-
17
- // Helper type to find an occurrence by name in the occurrences tuple
18
- type FindOccurrenceByName<
19
- Occurrences extends readonly TableOccurrence<any, any, any, any>[],
20
- Name extends string,
21
- > = Occurrences extends readonly [
22
- infer First,
23
- ...infer Rest extends readonly TableOccurrence<any, any, any, any>[],
24
- ]
25
- ? First extends TableOccurrence<any, any, any, any>
26
- ? First["name"] extends Name
27
- ? First
28
- : FindOccurrenceByName<Rest, Name>
29
- : never
30
- : never;
31
-
32
- // Helper type to extract all occurrence names from the tuple
33
- type ExtractOccurrenceNames<
34
- Occurrences extends readonly TableOccurrence<any, any, any, any>[],
35
- > = Occurrences extends readonly []
36
- ? string // If no occurrences, allow any string
37
- : Occurrences[number]["name"]; // Otherwise, extract union of names
9
+ interface MetadataArgs {
10
+ format?: "xml" | "json";
11
+ /**
12
+ * If provided, only the metadata for the specified table will be returned.
13
+ * Requires FileMaker Server 22.0.4 or later.
14
+ */
15
+ tableName?: string;
16
+ /**
17
+ * If true, a reduced payload size will be returned by omitting certain annotations.
18
+ */
19
+ reduceAnnotations?: boolean;
20
+ }
38
21
 
39
- export class Database<
40
- Occurrences extends readonly TableOccurrence<
41
- any,
42
- any,
43
- any,
44
- any
45
- >[] = readonly [],
46
- > {
47
- private occurrenceMap: Map<string, TableOccurrence<any, any, any, any>>;
48
- private _useEntityIds: boolean = false;
49
- public readonly schema: SchemaManager;
22
+ export class Database<IncludeSpecialColumns extends boolean = false> {
23
+ readonly schema: SchemaManager;
24
+ readonly webhook: WebhookManager;
25
+ private readonly databaseName: string;
26
+ private readonly context: ExecutionContext;
27
+ private _useEntityIds: boolean;
28
+ private readonly _includeSpecialColumns: IncludeSpecialColumns;
50
29
 
51
30
  constructor(
52
- private readonly databaseName: string,
53
- private readonly context: ExecutionContext,
31
+ databaseName: string,
32
+ context: ExecutionContext,
54
33
  config?: {
55
- occurrences?: Occurrences | undefined;
56
34
  /**
57
35
  * Whether to use entity IDs instead of field names in the actual requests to the server
58
36
  * Defaults to true if all occurrences use entity IDs, false otherwise
59
37
  * If set to false but some occurrences do not use entity IDs, an error will be thrown
60
38
  */
61
39
  useEntityIds?: boolean;
40
+ /**
41
+ * Whether to include special columns (ROWID and ROWMODID) in responses.
42
+ * Note: Special columns are only included when there is no $select query.
43
+ */
44
+ includeSpecialColumns?: IncludeSpecialColumns;
62
45
  },
63
46
  ) {
64
- this.occurrenceMap = new Map();
65
- if (config?.occurrences) {
66
- // Validate consistency: either all occurrences use entity IDs or none do
67
- const occurrencesWithIds: string[] = [];
68
- const occurrencesWithoutIds: string[] = [];
69
-
70
- for (const occ of config.occurrences) {
71
- this.occurrenceMap.set(occ.name, occ);
72
-
73
- const hasTableId = occ.isUsingTableId();
74
- const hasFieldIds = occ.baseTable.isUsingFieldIds();
75
-
76
- // An occurrence uses entity IDs if it has both fmtId and fmfIds
77
- if (hasTableId && hasFieldIds) {
78
- occurrencesWithIds.push(occ.name);
79
- } else if (!hasTableId && !hasFieldIds) {
80
- occurrencesWithoutIds.push(occ.name);
81
- } else {
82
- // Partial entity ID usage (only one of fmtId or fmfIds) - this is an error
83
- throw new Error(
84
- `TableOccurrence "${occ.name}" has inconsistent entity ID configuration. ` +
85
- `Both fmtId (${hasTableId ? "present" : "missing"}) and fmfIds (${hasFieldIds ? "present" : "missing"}) must be defined together.`,
86
- );
87
- }
88
- }
89
-
90
- // Determine default value: true if all occurrences use entity IDs, false otherwise
91
- const allOccurrencesUseEntityIds =
92
- occurrencesWithIds.length > 0 && occurrencesWithoutIds.length === 0;
93
- const hasMixedUsage =
94
- occurrencesWithIds.length > 0 && occurrencesWithoutIds.length > 0;
95
-
96
- // Handle explicit useEntityIds config
97
- if (config.useEntityIds !== undefined) {
98
- if (config.useEntityIds === false) {
99
- // If explicitly set to false, allow mixed usage and use false
100
- this._useEntityIds = false;
101
- } else if (config.useEntityIds === true) {
102
- // If explicitly set to true, validate that all occurrences use entity IDs
103
- if (hasMixedUsage || occurrencesWithoutIds.length > 0) {
104
- throw new Error(
105
- `useEntityIds is set to true but some occurrences do not use entity IDs. ` +
106
- `Occurrences without entity IDs: [${occurrencesWithoutIds.join(", ")}]. ` +
107
- `Either set useEntityIds to false or configure all occurrences with entity IDs.`,
108
- );
109
- }
110
- this._useEntityIds = true;
111
- }
112
- } else {
113
- // Default: true if all occurrences use entity IDs, false otherwise
114
- // But throw error if there's mixed usage when using defaults
115
- if (hasMixedUsage) {
116
- throw new Error(
117
- `Cannot mix TableOccurrence instances with and without entity IDs in the same database. ` +
118
- `Occurrences with entity IDs: [${occurrencesWithIds.join(", ")}]. ` +
119
- `Occurrences without entity IDs: [${occurrencesWithoutIds.join(", ")}]. ` +
120
- `Either all table occurrences must use entity IDs (fmtId + fmfIds), none should, or explicitly set useEntityIds to false.`,
121
- );
122
- }
123
- this._useEntityIds = allOccurrencesUseEntityIds;
124
- }
125
- } else {
126
- // No occurrences provided, use explicit config or default to false
127
- this._useEntityIds = config?.useEntityIds ?? false;
128
- }
129
-
130
- // Inform the execution context whether to use entity IDs
131
- if (this.context._setUseEntityIds) {
132
- this.context._setUseEntityIds(this._useEntityIds);
133
- }
134
-
47
+ this.databaseName = databaseName;
48
+ this.context = context;
135
49
  // Initialize schema manager
136
50
  this.schema = new SchemaManager(this.databaseName, this.context);
51
+ this.webhook = new WebhookManager(this.databaseName, this.context);
52
+ this._useEntityIds = config?.useEntityIds ?? false;
53
+ this._includeSpecialColumns = (config?.includeSpecialColumns ?? false) as IncludeSpecialColumns;
137
54
  }
138
55
 
139
56
  /**
140
- * Returns true if any table occurrence in this database is using entity IDs.
57
+ * @internal Used by EntitySet to access database configuration
141
58
  */
142
- isUsingEntityIds(): boolean {
59
+ get _getUseEntityIds(): boolean {
143
60
  return this._useEntityIds;
144
61
  }
145
62
 
146
63
  /**
147
- * Gets a table occurrence by name.
148
- * @internal
64
+ * @internal Used by EntitySet to access database configuration
149
65
  */
150
- getOccurrence(name: string): TableOccurrence<any, any, any, any> | undefined {
151
- return this.occurrenceMap.get(name);
66
+ get _getIncludeSpecialColumns(): IncludeSpecialColumns {
67
+ return this._includeSpecialColumns;
152
68
  }
153
69
 
154
- from<Name extends ExtractOccurrenceNames<Occurrences> | (string & {})>(
155
- name: Name,
156
- ): Occurrences extends readonly []
157
- ? EntitySet<Record<string, StandardSchemaV1>, undefined>
158
- : Name extends ExtractOccurrenceNames<Occurrences>
159
- ? EntitySet<
160
- ExtractSchemaFromOccurrence<FindOccurrenceByName<Occurrences, Name>>,
161
- FindOccurrenceByName<Occurrences, Name>
162
- >
163
- : EntitySet<Record<string, StandardSchemaV1>, undefined> {
164
- const occurrence = this.occurrenceMap.get(name as string);
165
-
166
- if (occurrence) {
167
- // Use EntitySet.create to preserve types better
168
- type OccType = FindOccurrenceByName<Occurrences, Name>;
169
- type SchemaType = ExtractSchemaFromOccurrence<OccType>;
170
-
171
- return EntitySet.create<SchemaType, OccType>({
172
- occurrence: occurrence as OccType,
173
- tableName: name as string,
174
- databaseName: this.databaseName,
175
- context: this.context,
176
- database: this,
177
- }) as any;
178
- } else {
179
- // Return untyped EntitySet for dynamic table access
180
- return new EntitySet<Record<string, StandardSchemaV1>, undefined>({
181
- tableName: name as string,
182
- databaseName: this.databaseName,
183
- context: this.context,
184
- database: this,
185
- }) as any;
70
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
71
+ from<T extends FMTable<any, any>>(table: T): EntitySet<T, IncludeSpecialColumns> {
72
+ // Only override database-level useEntityIds if table explicitly sets it
73
+ // (not if it's undefined, which would override the database setting)
74
+ if (Object.hasOwn(table, FMTable.Symbol.UseEntityIds)) {
75
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for Symbol property access
76
+ const tableUseEntityIds = (table as any)[FMTable.Symbol.UseEntityIds];
77
+ if (typeof tableUseEntityIds === "boolean") {
78
+ this._useEntityIds = tableUseEntityIds;
79
+ }
186
80
  }
81
+ return new EntitySet<T, IncludeSpecialColumns>({
82
+ occurrence: table as T,
83
+ databaseName: this.databaseName,
84
+ context: this.context,
85
+ database: this,
86
+ });
187
87
  }
188
88
 
189
89
  /**
190
90
  * Retrieves the OData metadata for this database.
191
91
  * @param args Optional configuration object
192
92
  * @param args.format The format to retrieve metadata in. Defaults to "json".
93
+ * @param args.tableName If provided, only the metadata for the specified table will be returned. Requires FileMaker Server 22.0.4 or later.
94
+ * @param args.reduceAnnotations If true, a reduced payload size will be returned by omitting certain annotations.
193
95
  * @returns The metadata in the specified format
194
96
  */
195
- async getMetadata(args: { format: "xml" }): Promise<string>;
196
- async getMetadata(args?: { format?: "json" }): Promise<Metadata>;
197
- async getMetadata(args?: {
198
- format?: "xml" | "json";
199
- }): Promise<string | Metadata> {
200
- const result = await this.context._makeRequest<
201
- Record<string, Metadata> | string
202
- >(`/${this.databaseName}/$metadata`, {
203
- headers: {
204
- Accept: args?.format === "xml" ? "application/xml" : "application/json",
205
- },
97
+ async getMetadata(args: { format: "xml" } & MetadataArgs): Promise<string>;
98
+ async getMetadata(args?: { format?: "json" } & MetadataArgs): Promise<Metadata>;
99
+ async getMetadata(args?: MetadataArgs): Promise<string | Metadata> {
100
+ // Build the URL - if tableName is provided, append %23{tableName} to the path
101
+ let url = `/${this.databaseName}/$metadata`;
102
+ if (args?.tableName) {
103
+ url = `/${this.databaseName}/$metadata%23${args.tableName}`;
104
+ }
105
+
106
+ // Build headers
107
+ const headers: Record<string, string> = {
108
+ Accept: args?.format === "xml" ? "application/xml" : "application/json",
109
+ };
110
+
111
+ // Add Prefer header if reduceAnnotations is true
112
+ if (args?.reduceAnnotations) {
113
+ headers.Prefer = 'include-annotations="-*"';
114
+ }
115
+
116
+ const result = await this.context._makeRequest<Record<string, Metadata> | string>(url, {
117
+ headers,
206
118
  });
207
119
  if (result.error) {
208
120
  throw result.error;
@@ -212,9 +124,7 @@ export class Database<
212
124
  const data = result.data as Record<string, Metadata>;
213
125
  const metadata = data[this.databaseName];
214
126
  if (!metadata) {
215
- throw new Error(
216
- `Metadata for database "${this.databaseName}" not found in response`,
217
- );
127
+ throw new Error(`Metadata for database "${this.databaseName}" not found in response`);
218
128
  }
219
129
  return metadata;
220
130
  }
@@ -244,9 +154,11 @@ export class Database<
244
154
  * @param options - Optional script parameter and result schema
245
155
  * @returns Promise resolving to script execution result
246
156
  */
157
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
247
158
  async runScript<ResultSchema extends StandardSchemaV1<string, any> = never>(
248
159
  scriptName: string,
249
160
  options?: {
161
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any record shape
250
162
  scriptParam?: string | number | Record<string, any>;
251
163
  resultSchema?: ResultSchema;
252
164
  },
@@ -280,30 +192,25 @@ export class Database<
280
192
 
281
193
  // If resultSchema is provided, validate the result through it
282
194
  if (options?.resultSchema && response.scriptResult !== undefined) {
283
- const validationResult = options.resultSchema["~standard"].validate(
284
- response.scriptResult.resultParameter,
285
- );
195
+ const validationResult = options.resultSchema["~standard"].validate(response.scriptResult.resultParameter);
286
196
  // Handle both sync and async validation
287
- const result =
288
- validationResult instanceof Promise
289
- ? await validationResult
290
- : validationResult;
197
+ const result = validationResult instanceof Promise ? await validationResult : validationResult;
291
198
 
292
199
  if (result.issues) {
293
- throw new Error(
294
- `Script result validation failed: ${JSON.stringify(result.issues)}`,
295
- );
200
+ throw new Error(`Script result validation failed: ${JSON.stringify(result.issues)}`);
296
201
  }
297
202
 
298
203
  return {
299
204
  resultCode: response.scriptResult.code,
300
205
  result: result.value,
206
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
301
207
  } as any;
302
208
  }
303
209
 
304
210
  return {
305
211
  resultCode: response.scriptResult.code,
306
212
  result: response.scriptResult.resultParameter,
213
+ // biome-ignore lint/suspicious/noExplicitAny: Type assertion for generic return type
307
214
  } as any;
308
215
  }
309
216
 
@@ -326,9 +233,8 @@ export class Database<
326
233
  * }
327
234
  * ```
328
235
  */
329
- batch<const Builders extends readonly ExecutableBuilder<any>[]>(
330
- builders: Builders,
331
- ): BatchBuilder<Builders> {
236
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any ExecutableBuilder result type
237
+ batch<const Builders extends readonly ExecutableBuilder<any>[]>(builders: Builders): BatchBuilder<Builders> {
332
238
  return new BatchBuilder(builders, this.databaseName, this.context);
333
239
  }
334
240
  }