@proofkit/fmodata 0.1.0-alpha.13 → 0.1.0-alpha.14

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 (143) hide show
  1. package/README.md +489 -334
  2. package/dist/esm/client/batch-builder.d.ts +7 -4
  3. package/dist/esm/client/batch-builder.js +84 -25
  4. package/dist/esm/client/batch-builder.js.map +1 -1
  5. package/dist/esm/client/builders/default-select.d.ts +7 -0
  6. package/dist/esm/client/builders/default-select.js +42 -0
  7. package/dist/esm/client/builders/default-select.js.map +1 -0
  8. package/dist/esm/client/builders/expand-builder.d.ts +43 -0
  9. package/dist/esm/client/builders/expand-builder.js +173 -0
  10. package/dist/esm/client/builders/expand-builder.js.map +1 -0
  11. package/dist/esm/client/builders/index.d.ts +8 -0
  12. package/dist/esm/client/builders/query-string-builder.d.ts +15 -0
  13. package/dist/esm/client/builders/query-string-builder.js +25 -0
  14. package/dist/esm/client/builders/query-string-builder.js.map +1 -0
  15. package/dist/esm/client/builders/response-processor.d.ts +39 -0
  16. package/dist/esm/client/builders/response-processor.js +170 -0
  17. package/dist/esm/client/builders/response-processor.js.map +1 -0
  18. package/dist/esm/client/builders/select-mixin.d.ts +31 -0
  19. package/dist/esm/client/builders/select-mixin.js +30 -0
  20. package/dist/esm/client/builders/select-mixin.js.map +1 -0
  21. package/dist/esm/client/builders/select-utils.d.ts +8 -0
  22. package/dist/esm/client/builders/select-utils.js +15 -0
  23. package/dist/esm/client/builders/select-utils.js.map +1 -0
  24. package/dist/esm/client/builders/shared-types.d.ts +39 -0
  25. package/dist/esm/client/builders/table-utils.d.ts +35 -0
  26. package/dist/esm/client/builders/table-utils.js +45 -0
  27. package/dist/esm/client/builders/table-utils.js.map +1 -0
  28. package/dist/esm/client/database.d.ts +3 -22
  29. package/dist/esm/client/database.js +14 -76
  30. package/dist/esm/client/database.js.map +1 -1
  31. package/dist/esm/client/delete-builder.d.ts +11 -15
  32. package/dist/esm/client/delete-builder.js +26 -26
  33. package/dist/esm/client/delete-builder.js.map +1 -1
  34. package/dist/esm/client/entity-set.d.ts +32 -32
  35. package/dist/esm/client/entity-set.js +92 -69
  36. package/dist/esm/client/entity-set.js.map +1 -1
  37. package/dist/esm/client/error-parser.d.ts +12 -0
  38. package/dist/esm/client/error-parser.js +30 -0
  39. package/dist/esm/client/error-parser.js.map +1 -0
  40. package/dist/esm/client/filemaker-odata.d.ts +2 -4
  41. package/dist/esm/client/filemaker-odata.js +1 -5
  42. package/dist/esm/client/filemaker-odata.js.map +1 -1
  43. package/dist/esm/client/insert-builder.d.ts +7 -9
  44. package/dist/esm/client/insert-builder.js +70 -24
  45. package/dist/esm/client/insert-builder.js.map +1 -1
  46. package/dist/esm/client/query/expand-builder.d.ts +35 -0
  47. package/dist/esm/client/query/index.d.ts +3 -0
  48. package/dist/esm/client/query/query-builder.d.ts +134 -0
  49. package/dist/esm/client/query/query-builder.js +505 -0
  50. package/dist/esm/client/query/query-builder.js.map +1 -0
  51. package/dist/esm/client/query/response-processor.d.ts +22 -0
  52. package/dist/esm/client/query/types.d.ts +52 -0
  53. package/dist/esm/client/query/url-builder.d.ts +71 -0
  54. package/dist/esm/client/query/url-builder.js +107 -0
  55. package/dist/esm/client/query/url-builder.js.map +1 -0
  56. package/dist/esm/client/query-builder.d.ts +1 -111
  57. package/dist/esm/client/record-builder.d.ts +56 -63
  58. package/dist/esm/client/record-builder.js +158 -297
  59. package/dist/esm/client/record-builder.js.map +1 -1
  60. package/dist/esm/client/response-processor.d.ts +3 -3
  61. package/dist/esm/client/update-builder.d.ts +16 -21
  62. package/dist/esm/client/update-builder.js +56 -30
  63. package/dist/esm/client/update-builder.js.map +1 -1
  64. package/dist/esm/errors.d.ts +8 -1
  65. package/dist/esm/errors.js +17 -0
  66. package/dist/esm/errors.js.map +1 -1
  67. package/dist/esm/index.d.ts +3 -7
  68. package/dist/esm/index.js +37 -8
  69. package/dist/esm/index.js.map +1 -1
  70. package/dist/esm/orm/column.d.ts +45 -0
  71. package/dist/esm/orm/column.js +59 -0
  72. package/dist/esm/orm/column.js.map +1 -0
  73. package/dist/esm/orm/field-builders.d.ts +154 -0
  74. package/dist/esm/orm/field-builders.js +152 -0
  75. package/dist/esm/orm/field-builders.js.map +1 -0
  76. package/dist/esm/orm/index.d.ts +4 -0
  77. package/dist/esm/orm/operators.d.ts +175 -0
  78. package/dist/esm/orm/operators.js +221 -0
  79. package/dist/esm/orm/operators.js.map +1 -0
  80. package/dist/esm/orm/table.d.ts +341 -0
  81. package/dist/esm/orm/table.js +211 -0
  82. package/dist/esm/orm/table.js.map +1 -0
  83. package/dist/esm/transform.d.ts +20 -21
  84. package/dist/esm/transform.js +34 -34
  85. package/dist/esm/transform.js.map +1 -1
  86. package/dist/esm/types.d.ts +16 -13
  87. package/dist/esm/types.js.map +1 -1
  88. package/dist/esm/validation.d.ts +14 -4
  89. package/dist/esm/validation.js +45 -1
  90. package/dist/esm/validation.js.map +1 -1
  91. package/package.json +20 -17
  92. package/src/client/batch-builder.ts +100 -32
  93. package/src/client/builders/default-select.ts +69 -0
  94. package/src/client/builders/expand-builder.ts +236 -0
  95. package/src/client/builders/index.ts +11 -0
  96. package/src/client/builders/query-string-builder.ts +41 -0
  97. package/src/client/builders/response-processor.ts +273 -0
  98. package/src/client/builders/select-mixin.ts +74 -0
  99. package/src/client/builders/select-utils.ts +34 -0
  100. package/src/client/builders/shared-types.ts +41 -0
  101. package/src/client/builders/table-utils.ts +87 -0
  102. package/src/client/database.ts +19 -160
  103. package/src/client/delete-builder.ts +46 -51
  104. package/src/client/entity-set.ts +227 -302
  105. package/src/client/error-parser.ts +59 -0
  106. package/src/client/filemaker-odata.ts +3 -14
  107. package/src/client/insert-builder.ts +124 -43
  108. package/src/client/query/expand-builder.ts +164 -0
  109. package/src/client/query/index.ts +13 -0
  110. package/src/client/query/query-builder.ts +816 -0
  111. package/src/client/query/response-processor.ts +244 -0
  112. package/src/client/query/types.ts +102 -0
  113. package/src/client/query/url-builder.ts +179 -0
  114. package/src/client/query-builder.ts +8 -1454
  115. package/src/client/record-builder.ts +325 -585
  116. package/src/client/response-processor.ts +4 -5
  117. package/src/client/update-builder.ts +102 -73
  118. package/src/errors.ts +22 -1
  119. package/src/index.ts +55 -5
  120. package/src/orm/column.ts +78 -0
  121. package/src/orm/field-builders.ts +296 -0
  122. package/src/orm/index.ts +60 -0
  123. package/src/orm/operators.ts +428 -0
  124. package/src/orm/table.ts +759 -0
  125. package/src/transform.ts +62 -48
  126. package/src/types.ts +20 -63
  127. package/src/validation.ts +76 -4
  128. package/LICENSE.md +0 -21
  129. package/dist/esm/client/base-table.d.ts +0 -128
  130. package/dist/esm/client/base-table.js +0 -57
  131. package/dist/esm/client/base-table.js.map +0 -1
  132. package/dist/esm/client/build-occurrences.d.ts +0 -74
  133. package/dist/esm/client/build-occurrences.js +0 -31
  134. package/dist/esm/client/build-occurrences.js.map +0 -1
  135. package/dist/esm/client/query-builder.js +0 -900
  136. package/dist/esm/client/query-builder.js.map +0 -1
  137. package/dist/esm/client/table-occurrence.d.ts +0 -86
  138. package/dist/esm/client/table-occurrence.js +0 -58
  139. package/dist/esm/client/table-occurrence.js.map +0 -1
  140. package/src/client/base-table.ts +0 -178
  141. package/src/client/build-occurrences.ts +0 -155
  142. package/src/client/query-builder.ts.bak +0 -1457
  143. package/src/client/table-occurrence.ts +0 -156
@@ -1,5 +1,5 @@
1
1
  import type { StandardSchemaV1 } from "@standard-schema/spec";
2
- import type { BaseTable } from "./base-table";
2
+ import type { FMTable } from "../orm/table";
3
3
  import type { ExecuteOptions } from "../types";
4
4
  import type { ExpandValidationConfig } from "../validation";
5
5
  import { ValidationError, ResponseStructureError } from "../errors";
@@ -23,16 +23,15 @@ export type ODataRecordResponse<T = unknown> = ODataResponse<
23
23
  }
24
24
  >;
25
25
 
26
-
27
26
  /**
28
- * Transform field IDs back to names using the base table configuration
27
+ * Transform field IDs back to names using the table configuration
29
28
  */
30
29
  export function applyFieldTransformation<T extends Record<string, unknown>>(
31
30
  response: ODataResponse<T> | ODataListResponse<T>,
32
- baseTable: BaseTable<Record<string, StandardSchemaV1>, any, any, any>,
31
+ table: FMTable<any, any>,
33
32
  expandConfigs?: ExpandValidationConfig[],
34
33
  ): ODataResponse<T> | ODataListResponse<T> {
35
- return transformResponseFields(response, baseTable, expandConfigs) as
34
+ return transformResponseFields(response, table, expandConfigs) as
36
35
  | ODataResponse<T>
37
36
  | ODataListResponse<T>;
38
37
  }
@@ -6,45 +6,44 @@ import type {
6
6
  ExecuteOptions,
7
7
  } from "../types";
8
8
  import { getAcceptHeader } from "../types";
9
- import type { TableOccurrence } from "./table-occurrence";
10
- import type { BaseTable } from "./base-table";
9
+ import type { FMTable, InferSchemaOutputFromFMTable } from "../orm/table";
10
+ import {
11
+ getTableName,
12
+ getTableId as getTableIdHelper,
13
+ getBaseTableConfig,
14
+ isUsingEntityIds,
15
+ } from "../orm/table";
11
16
  import { QueryBuilder } from "./query-builder";
12
17
  import { type FFetchOptions } from "@fetchkit/ffetch";
13
- import {
14
- transformFieldNamesToIds,
15
- transformTableName,
16
- getTableIdentifiers,
17
- } from "../transform";
18
+ import { transformFieldNamesToIds } from "../transform";
19
+ import { parseErrorResponse } from "./error-parser";
20
+ import { validateAndTransformInput } from "../validation";
18
21
 
19
22
  /**
20
23
  * Initial update builder returned from EntitySet.update(data)
21
24
  * Requires calling .byId() or .where() before .execute() is available
22
25
  */
23
26
  export class UpdateBuilder<
24
- T extends Record<string, any>,
25
- BT extends BaseTable<any, any, any, any>,
27
+ Occ extends FMTable<any, any>,
26
28
  ReturnPreference extends "minimal" | "representation" = "minimal",
27
29
  > {
28
- private tableName: string;
29
30
  private databaseName: string;
30
31
  private context: ExecutionContext;
31
- private occurrence?: TableOccurrence<any, any, any, any>;
32
- private data: Partial<T>;
32
+ private table: Occ;
33
+ private data: Partial<InferSchemaOutputFromFMTable<Occ>>;
33
34
  private returnPreference: ReturnPreference;
34
35
 
35
36
  private databaseUseEntityIds: boolean;
36
37
 
37
38
  constructor(config: {
38
- occurrence?: TableOccurrence<any, any, any, any>;
39
- tableName: string;
39
+ occurrence: Occ;
40
40
  databaseName: string;
41
41
  context: ExecutionContext;
42
- data: Partial<T>;
42
+ data: Partial<InferSchemaOutputFromFMTable<Occ>>;
43
43
  returnPreference: ReturnPreference;
44
44
  databaseUseEntityIds?: boolean;
45
45
  }) {
46
- this.occurrence = config.occurrence;
47
- this.tableName = config.tableName;
46
+ this.table = config.occurrence;
48
47
  this.databaseName = config.databaseName;
49
48
  this.context = config.context;
50
49
  this.data = config.data;
@@ -58,10 +57,9 @@ export class UpdateBuilder<
58
57
  */
59
58
  byId(
60
59
  id: string | number,
61
- ): ExecutableUpdateBuilder<T, true, ReturnPreference> {
62
- return new ExecutableUpdateBuilder<T, true, ReturnPreference>({
63
- occurrence: this.occurrence,
64
- tableName: this.tableName,
60
+ ): ExecutableUpdateBuilder<Occ, true, ReturnPreference> {
61
+ return new ExecutableUpdateBuilder<Occ, true, ReturnPreference>({
62
+ occurrence: this.table,
65
63
  databaseName: this.databaseName,
66
64
  context: this.context,
67
65
  data: this.data,
@@ -79,19 +77,12 @@ export class UpdateBuilder<
79
77
  */
80
78
  where(
81
79
  fn: (
82
- q: QueryBuilder<WithSystemFields<T>>,
83
- ) => QueryBuilder<WithSystemFields<T>>,
84
- ): ExecutableUpdateBuilder<T, true, ReturnPreference> {
80
+ q: QueryBuilder<Occ>,
81
+ ) => QueryBuilder<Occ>,
82
+ ): ExecutableUpdateBuilder<Occ, true, ReturnPreference> {
85
83
  // Create a QueryBuilder for the user to configure
86
- const queryBuilder = new QueryBuilder<
87
- WithSystemFields<T>,
88
- keyof WithSystemFields<T>,
89
- false,
90
- false,
91
- undefined
92
- >({
93
- occurrence: undefined,
94
- tableName: this.tableName,
84
+ const queryBuilder = new QueryBuilder<Occ>({
85
+ occurrence: this.table,
95
86
  databaseName: this.databaseName,
96
87
  context: this.context,
97
88
  });
@@ -99,9 +90,8 @@ export class UpdateBuilder<
99
90
  // Let the user configure it
100
91
  const configuredBuilder = fn(queryBuilder);
101
92
 
102
- return new ExecutableUpdateBuilder<T, true, ReturnPreference>({
103
- occurrence: this.occurrence,
104
- tableName: this.tableName,
93
+ return new ExecutableUpdateBuilder<Occ, true, ReturnPreference>({
94
+ occurrence: this.table,
105
95
  databaseName: this.databaseName,
106
96
  context: this.context,
107
97
  data: this.data,
@@ -119,39 +109,36 @@ export class UpdateBuilder<
119
109
  * Can return either updated count or full record based on returnFullRecord option
120
110
  */
121
111
  export class ExecutableUpdateBuilder<
122
- T extends Record<string, any>,
112
+ Occ extends FMTable<any, any>,
123
113
  IsByFilter extends boolean,
124
114
  ReturnPreference extends "minimal" | "representation" = "minimal",
125
115
  > implements
126
116
  ExecutableBuilder<
127
- ReturnPreference extends "minimal" ? { updatedCount: number } : T
117
+ ReturnPreference extends "minimal" ? { updatedCount: number } : InferSchemaOutputFromFMTable<Occ>
128
118
  >
129
119
  {
130
- private tableName: string;
131
120
  private databaseName: string;
132
121
  private context: ExecutionContext;
133
- private occurrence?: TableOccurrence<any, any, any, any>;
134
- private data: Partial<T>;
122
+ private table: Occ;
123
+ private data: Partial<InferSchemaOutputFromFMTable<Occ>>;
135
124
  private mode: "byId" | "byFilter";
136
125
  private recordId?: string | number;
137
- private queryBuilder?: QueryBuilder<any>;
126
+ private queryBuilder?: QueryBuilder<Occ>;
138
127
  private returnPreference: ReturnPreference;
139
128
  private databaseUseEntityIds: boolean;
140
129
 
141
130
  constructor(config: {
142
- occurrence?: TableOccurrence<any, any, any, any>;
143
- tableName: string;
131
+ occurrence: Occ;
144
132
  databaseName: string;
145
133
  context: ExecutionContext;
146
- data: Partial<T>;
134
+ data: Partial<InferSchemaOutputFromFMTable<Occ>>;
147
135
  mode: "byId" | "byFilter";
148
136
  recordId?: string | number;
149
- queryBuilder?: QueryBuilder<any>;
137
+ queryBuilder?: QueryBuilder<Occ>;
150
138
  returnPreference: ReturnPreference;
151
139
  databaseUseEntityIds?: boolean;
152
140
  }) {
153
- this.occurrence = config.occurrence;
154
- this.tableName = config.tableName;
141
+ this.table = config.occurrence;
155
142
  this.databaseName = config.databaseName;
156
143
  this.context = config.context;
157
144
  this.data = config.data;
@@ -180,30 +167,25 @@ export class ExecutableUpdateBuilder<
180
167
  * @param useEntityIds - Optional override for entity ID usage
181
168
  */
182
169
  private getTableId(useEntityIds?: boolean): string {
183
- if (!this.occurrence) {
184
- return this.tableName;
185
- }
186
-
187
170
  const contextDefault = this.context._getUseEntityIds?.() ?? false;
188
171
  const shouldUseIds = useEntityIds ?? contextDefault;
189
172
 
190
173
  if (shouldUseIds) {
191
- const identifiers = getTableIdentifiers(this.occurrence);
192
- if (!identifiers.id) {
174
+ if (!isUsingEntityIds(this.table)) {
193
175
  throw new Error(
194
- `useEntityIds is true but TableOccurrence "${identifiers.name}" does not have an fmtId defined`,
176
+ `useEntityIds is true but table "${getTableName(this.table)}" does not have entity IDs configured`,
195
177
  );
196
178
  }
197
- return identifiers.id;
179
+ return getTableIdHelper(this.table);
198
180
  }
199
181
 
200
- return this.occurrence.getTableName();
182
+ return getTableName(this.table);
201
183
  }
202
184
 
203
185
  async execute(
204
186
  options?: RequestInit & FFetchOptions & { useEntityIds?: boolean },
205
187
  ): Promise<
206
- Result<ReturnPreference extends "minimal" ? { updatedCount: number } : T>
188
+ Result<ReturnPreference extends "minimal" ? { updatedCount: number } : InferSchemaOutputFromFMTable<Occ>>
207
189
  > {
208
190
  // Merge database-level useEntityIds with per-request options
209
191
  const mergedOptions = this.mergeExecuteOptions(options);
@@ -211,14 +193,31 @@ export class ExecutableUpdateBuilder<
211
193
  // Get table identifier with override support
212
194
  const tableId = this.getTableId(mergedOptions.useEntityIds);
213
195
 
196
+ // Validate and transform input data using input validators (writeValidators)
197
+ let validatedData = this.data;
198
+ if (this.table) {
199
+ const baseTableConfig = getBaseTableConfig(this.table);
200
+ const inputSchema = baseTableConfig.inputSchema;
201
+
202
+ try {
203
+ validatedData = await validateAndTransformInput(this.data, inputSchema);
204
+ } catch (error) {
205
+ // If validation fails, return error immediately
206
+ return {
207
+ data: undefined,
208
+ error: error instanceof Error ? error : new Error(String(error)),
209
+ } as any;
210
+ }
211
+ }
212
+
214
213
  // Transform field names to FMFIDs if using entity IDs
215
214
  // Only transform if useEntityIds resolves to true (respects per-request override)
216
215
  const shouldUseIds = mergedOptions.useEntityIds ?? false;
217
216
 
218
217
  const transformedData =
219
- this.occurrence?.baseTable && shouldUseIds
220
- ? transformFieldNamesToIds(this.data, this.occurrence.baseTable)
221
- : this.data;
218
+ this.table && shouldUseIds
219
+ ? transformFieldNamesToIds(validatedData, this.table)
220
+ : validatedData;
222
221
 
223
222
  let url: string;
224
223
 
@@ -235,10 +234,11 @@ export class ExecutableUpdateBuilder<
235
234
  const queryString = this.queryBuilder.getQueryString();
236
235
  // The query string will have the tableId already transformed by QueryBuilder
237
236
  // Remove the leading "/" and table name from the query string as we'll build our own URL
237
+ const tableName = getTableName(this.table);
238
238
  const queryParams = queryString.startsWith(`/${tableId}`)
239
239
  ? queryString.slice(`/${tableId}`.length)
240
- : queryString.startsWith(`/${this.tableName}`)
241
- ? queryString.slice(`/${this.tableName}`.length)
240
+ : queryString.startsWith(`/${tableName}`)
241
+ ? queryString.slice(`/${tableName}`.length)
242
242
  : queryString;
243
243
 
244
244
  url = `/${this.databaseName}/${tableId}${queryParams}`;
@@ -273,7 +273,7 @@ export class ExecutableUpdateBuilder<
273
273
  return {
274
274
  data: response as ReturnPreference extends "minimal"
275
275
  ? { updatedCount: number }
276
- : T,
276
+ : InferSchemaOutputFromFMTable<Occ>,
277
277
  error: undefined,
278
278
  };
279
279
  } else {
@@ -290,7 +290,7 @@ export class ExecutableUpdateBuilder<
290
290
  return {
291
291
  data: { updatedCount } as ReturnPreference extends "minimal"
292
292
  ? { updatedCount: number }
293
- : T,
293
+ : InferSchemaOutputFromFMTable<Occ>,
294
294
  error: undefined,
295
295
  };
296
296
  }
@@ -298,12 +298,13 @@ export class ExecutableUpdateBuilder<
298
298
 
299
299
  getRequestConfig(): { method: string; url: string; body?: any } {
300
300
  // For batch operations, use database-level setting (no per-request override available here)
301
+ // Note: Input validation happens in execute() and processResponse() for batch operations
301
302
  const tableId = this.getTableId(this.databaseUseEntityIds);
302
303
 
303
304
  // Transform field names to FMFIDs if using entity IDs
304
305
  const transformedData =
305
- this.occurrence?.baseTable && this.databaseUseEntityIds
306
- ? transformFieldNamesToIds(this.data, this.occurrence.baseTable)
306
+ this.table && this.databaseUseEntityIds
307
+ ? transformFieldNamesToIds(this.data, this.table)
307
308
  : this.data;
308
309
 
309
310
  let url: string;
@@ -316,10 +317,11 @@ export class ExecutableUpdateBuilder<
316
317
  }
317
318
 
318
319
  const queryString = this.queryBuilder.getQueryString();
320
+ const tableName = getTableName(this.table);
319
321
  const queryParams = queryString.startsWith(`/${tableId}`)
320
322
  ? queryString.slice(`/${tableId}`.length)
321
- : queryString.startsWith(`/${this.tableName}`)
322
- ? queryString.slice(`/${this.tableName}`.length)
323
+ : queryString.startsWith(`/${tableName}`)
324
+ ? queryString.slice(`/${tableName}`.length)
323
325
  : queryString;
324
326
 
325
327
  url = `/${this.databaseName}/${tableId}${queryParams}`;
@@ -350,8 +352,18 @@ export class ExecutableUpdateBuilder<
350
352
  response: Response,
351
353
  options?: ExecuteOptions,
352
354
  ): Promise<
353
- Result<ReturnPreference extends "minimal" ? { updatedCount: number } : T>
355
+ Result<ReturnPreference extends "minimal" ? { updatedCount: number } : InferSchemaOutputFromFMTable<Occ>>
354
356
  > {
357
+ // Check for error responses (important for batch operations)
358
+ if (!response.ok) {
359
+ const tableName = getTableName(this.table);
360
+ const error = await parseErrorResponse(
361
+ response,
362
+ response.url || `/${this.databaseName}/${tableName}`,
363
+ );
364
+ return { data: undefined, error };
365
+ }
366
+
355
367
  // Check for empty response (204 No Content)
356
368
  const text = await response.text();
357
369
  if (!text || text.trim() === "") {
@@ -361,20 +373,37 @@ export class ExecutableUpdateBuilder<
361
373
  return {
362
374
  data: { updatedCount } as ReturnPreference extends "minimal"
363
375
  ? { updatedCount: number }
364
- : T,
376
+ : InferSchemaOutputFromFMTable<Occ>,
365
377
  error: undefined,
366
378
  };
367
379
  }
368
380
 
369
381
  const rawResponse = JSON.parse(text);
370
382
 
383
+ // Validate and transform input data using input validators (writeValidators)
384
+ // This is needed for processResponse because it's called from batch operations
385
+ // where the data hasn't been validated yet
386
+ let validatedData = this.data;
387
+ if (this.table) {
388
+ const baseTableConfig = getBaseTableConfig(this.table);
389
+ const inputSchema = baseTableConfig.inputSchema;
390
+ try {
391
+ validatedData = await validateAndTransformInput(this.data, inputSchema);
392
+ } catch (error) {
393
+ return {
394
+ data: undefined,
395
+ error: error instanceof Error ? error : new Error(String(error)),
396
+ } as any;
397
+ }
398
+ }
399
+
371
400
  // Handle based on return preference
372
401
  if (this.returnPreference === "representation") {
373
402
  // Return the full updated record
374
403
  return {
375
404
  data: rawResponse as ReturnPreference extends "minimal"
376
405
  ? { updatedCount: number }
377
- : T,
406
+ : InferSchemaOutputFromFMTable<Occ>,
378
407
  error: undefined,
379
408
  };
380
409
  } else {
@@ -391,7 +420,7 @@ export class ExecutableUpdateBuilder<
391
420
  return {
392
421
  data: { updatedCount } as ReturnPreference extends "minimal"
393
422
  ? { updatedCount: number }
394
- : T,
423
+ : InferSchemaOutputFromFMTable<Occ>,
395
424
  error: undefined,
396
425
  };
397
426
  }
package/src/errors.ts CHANGED
@@ -167,6 +167,20 @@ export class ResponseParseError extends FMODataError {
167
167
  }
168
168
  }
169
169
 
170
+ export class BatchTruncatedError extends FMODataError {
171
+ readonly kind = "BatchTruncatedError" as const;
172
+ readonly operationIndex: number;
173
+ readonly failedAtIndex: number;
174
+
175
+ constructor(operationIndex: number, failedAtIndex: number) {
176
+ super(
177
+ `Operation ${operationIndex} was not executed because operation ${failedAtIndex} failed`,
178
+ );
179
+ this.operationIndex = operationIndex;
180
+ this.failedAtIndex = failedAtIndex;
181
+ }
182
+ }
183
+
170
184
  // ============================================
171
185
  // Type Guards
172
186
  // ============================================
@@ -207,6 +221,12 @@ export function isResponseParseError(
207
221
  return error instanceof ResponseParseError;
208
222
  }
209
223
 
224
+ export function isBatchTruncatedError(
225
+ error: unknown,
226
+ ): error is BatchTruncatedError {
227
+ return error instanceof BatchTruncatedError;
228
+ }
229
+
210
230
  export function isFMODataError(error: unknown): error is FMODataError {
211
231
  return error instanceof FMODataError;
212
232
  }
@@ -237,4 +257,5 @@ export type FMODataErrorType =
237
257
  | ResponseStructureError
238
258
  | RecordCountMismatchError
239
259
  | InvalidLocationHeaderError
240
- | ResponseParseError;
260
+ | ResponseParseError
261
+ | BatchTruncatedError;
package/src/index.ts CHANGED
@@ -1,14 +1,60 @@
1
1
  // Barrel file - exports all public API from the client folder
2
2
 
3
3
  // Main API - use these functions to create tables and occurrences
4
- export { defineBaseTable } from "./client/base-table";
5
- export { defineTableOccurrence } from "./client/table-occurrence";
6
- export { buildOccurrences } from "./client/build-occurrences";
7
4
  export { FMServerConnection } from "./client/filemaker-odata";
8
5
 
6
+ // NEW ORM API - Drizzle-inspired field builders and operators
7
+ export {
8
+ // Field builders
9
+ textField,
10
+ numberField,
11
+ dateField,
12
+ timeField,
13
+ timestampField,
14
+ containerField,
15
+ calcField,
16
+ type FieldBuilder,
17
+ // Table definition
18
+ fmTableOccurrence,
19
+ FMTable,
20
+ type FMTableWithColumns as TableOccurrenceResult,
21
+ type InferTableSchema,
22
+ // Table helper functions
23
+ // getTableFields,
24
+ // getDefaultSelect,
25
+ // getBaseTableConfig,
26
+ // getFieldId,
27
+ // getFieldName,
28
+ // getTableId,
29
+ getTableColumns,
30
+ // Column references
31
+ type Column,
32
+ isColumn,
33
+ // Filter operators
34
+ type FilterExpression,
35
+ eq,
36
+ ne,
37
+ gt,
38
+ gte,
39
+ lt,
40
+ lte,
41
+ contains,
42
+ startsWith,
43
+ endsWith,
44
+ inArray,
45
+ notInArray,
46
+ isNull,
47
+ isNotNull,
48
+ and,
49
+ or,
50
+ not,
51
+ // OrderBy operators
52
+ type OrderByExpression,
53
+ asc,
54
+ desc,
55
+ } from "./orm/index";
56
+
9
57
  // Type-only exports - for type annotations only, not direct instantiation
10
- export type { BaseTable } from "./client/base-table";
11
- export type { TableOccurrence } from "./client/table-occurrence";
12
58
  export type { Database } from "./client/database";
13
59
  export type { EntitySet } from "./client/entity-set";
14
60
  export type {
@@ -25,6 +71,8 @@ export type {
25
71
  // Utility types for type annotations
26
72
  export type {
27
73
  Result,
74
+ BatchResult,
75
+ BatchItemResult,
28
76
  InferSchemaType,
29
77
  InsertData,
30
78
  UpdateData,
@@ -63,6 +111,7 @@ export {
63
111
  ResponseStructureError,
64
112
  RecordCountMismatchError,
65
113
  ResponseParseError,
114
+ BatchTruncatedError,
66
115
  isHTTPError,
67
116
  isValidationError,
68
117
  isODataError,
@@ -70,6 +119,7 @@ export {
70
119
  isResponseStructureError,
71
120
  isRecordCountMismatchError,
72
121
  isResponseParseError,
122
+ isBatchTruncatedError,
73
123
  isFMODataError,
74
124
  } from "./errors";
75
125
 
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Column represents a type-safe reference to a table field.
3
+ * Used in queries, filters, and operators to provide autocomplete and type checking.
4
+ *
5
+ * @template T - The TypeScript type of the column's value
6
+ * @template TableName - The table name as a string literal type (for validation)
7
+ * @template IsContainer - Whether this column represents a container field (cannot be selected)
8
+ */
9
+ export class Column<
10
+ T = any,
11
+ TableName extends string = string,
12
+ IsContainer extends boolean = false,
13
+ > {
14
+ readonly fieldName: string;
15
+ readonly entityId?: `FMFID:${string}`;
16
+ readonly tableName: TableName;
17
+ readonly tableEntityId?: `FMTID:${string}`;
18
+
19
+ // Phantom type for TypeScript inference - never actually holds a value
20
+ readonly _phantom!: T;
21
+ readonly _isContainer!: IsContainer;
22
+
23
+ constructor(config: {
24
+ fieldName: string;
25
+ entityId?: `FMFID:${string}`;
26
+ tableName: TableName;
27
+ tableEntityId?: `FMTID:${string}`;
28
+ }) {
29
+ this.fieldName = config.fieldName;
30
+ this.entityId = config.entityId;
31
+ this.tableName = config.tableName;
32
+ this.tableEntityId = config.tableEntityId;
33
+ }
34
+
35
+ /**
36
+ * Get the field identifier (entity ID if available, otherwise field name).
37
+ * Used when building OData queries.
38
+ */
39
+ getFieldIdentifier(useEntityIds?: boolean): string {
40
+ if (useEntityIds && this.entityId) {
41
+ return this.entityId;
42
+ }
43
+ return this.fieldName;
44
+ }
45
+
46
+ /**
47
+ * Get the table identifier (entity ID if available, otherwise table name).
48
+ * Used when building OData queries.
49
+ */
50
+ getTableIdentifier(useEntityIds?: boolean): string {
51
+ if (useEntityIds && this.tableEntityId) {
52
+ return this.tableEntityId;
53
+ }
54
+ return this.tableName;
55
+ }
56
+
57
+ /**
58
+ * Check if this column is from a specific table.
59
+ * Useful for validation in cross-table operations.
60
+ */
61
+ isFromTable(tableName: string): boolean {
62
+ return this.tableName === tableName;
63
+ }
64
+
65
+ /**
66
+ * Create a string representation for debugging.
67
+ */
68
+ toString(): string {
69
+ return `${this.tableName}.${this.fieldName}`;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Type guard to check if a value is a Column instance.
75
+ */
76
+ export function isColumn(value: any): value is Column<any, any, any> {
77
+ return value instanceof Column;
78
+ }