@proofkit/fmodata 0.1.0-alpha.8 → 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 -166
  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,741 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import { Column } from "./column";
3
+ import type { ContainerDbType, FieldBuilder, FieldBuilder as FieldBuilderType } from "./field-builders";
4
+ // import { z } from "zod/v4";
5
+
6
+ /**
7
+ * Extract the output type from a FieldBuilder.
8
+ * This is what you get when reading from the database.
9
+ *
10
+ * This type extracts the TOutput type parameter, which is set by readValidator()
11
+ * and represents the transformed/validated output type.
12
+ */
13
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
14
+ export type InferFieldOutput<F> = F extends FieldBuilder<infer TOutput, any, any, any> ? TOutput : never;
15
+
16
+ /**
17
+ * Extract the input type from a FieldBuilder.
18
+ * This is what you pass when writing to the database.
19
+ *
20
+ * This type extracts the TInput type parameter, which is set by writeValidator()
21
+ * and represents the transformed/validated input type.
22
+ */
23
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
24
+ type InferFieldInput<F> = F extends FieldBuilder<any, infer TInput, any, any> ? TInput : never;
25
+
26
+ /**
27
+ * Build a schema type from field builders (output/read types).
28
+ */
29
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
30
+ type InferSchemaFromFields<TFields extends Record<string, FieldBuilder<any, any, any, any>>> = {
31
+ [K in keyof TFields]: InferFieldOutput<TFields[K]>;
32
+ };
33
+
34
+ /**
35
+ * Build an input schema type from field builders (input/write types).
36
+ * Used for insert and update operations.
37
+ */
38
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
39
+ type InferInputSchemaFromFields<TFields extends Record<string, FieldBuilder<any, any, any, any>>> = {
40
+ [K in keyof TFields]: InferFieldInput<TFields[K]>;
41
+ };
42
+
43
+ /**
44
+ * Check if a field is a container field by inspecting its TDbType.
45
+ * Container fields have a branded TDbType that extends ContainerDbType.
46
+ */
47
+ type IsContainerField<F> =
48
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
49
+ F extends FieldBuilder<any, any, infer TDbType, any>
50
+ ? NonNullable<TDbType> extends ContainerDbType
51
+ ? true
52
+ : false
53
+ : false;
54
+
55
+ /**
56
+ * Extract only selectable (non-container) field keys from a fields record.
57
+ * Container fields are excluded because they cannot be selected via $select in FileMaker OData.
58
+ */
59
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
60
+ type SelectableFieldKeys<TFields extends Record<string, FieldBuilder<any, any, any, any>>> = {
61
+ [K in keyof TFields]: IsContainerField<TFields[K]> extends true ? never : K;
62
+ }[keyof TFields];
63
+
64
+ /**
65
+ * Build a schema type excluding container fields (for query return types).
66
+ * This is used to ensure container fields don't appear in the return type
67
+ * when using defaultSelect: "schema" or "all".
68
+ */
69
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
70
+ type _InferSelectableSchemaFromFields<TFields extends Record<string, FieldBuilder<any, any, any, any>>> = {
71
+ [K in SelectableFieldKeys<TFields>]: InferFieldOutput<TFields[K]>;
72
+ };
73
+
74
+ /**
75
+ * Internal Symbols for table properties (hidden from IDE autocomplete).
76
+ * These are used to store internal configuration that shouldn't be visible
77
+ * when users access table columns.
78
+ * @internal - Not exported from public API, only accessible via FMTable.Symbol
79
+ */
80
+ const FMTableName = Symbol.for("fmodata:FMTableName");
81
+ const FMTableEntityId = Symbol.for("fmodata:FMTableEntityId");
82
+ const FMTableSchema = Symbol.for("fmodata:FMTableSchema");
83
+ const FMTableFields = Symbol.for("fmodata:FMTableFields");
84
+ const FMTableNavigationPaths = Symbol.for("fmodata:FMTableNavigationPaths");
85
+ const FMTableDefaultSelect = Symbol.for("fmodata:FMTableDefaultSelect");
86
+ const FMTableBaseTableConfig = Symbol.for("fmodata:FMTableBaseTableConfig");
87
+ const FMTableUseEntityIds = Symbol.for("fmodata:FMTableUseEntityIds");
88
+ const FMTableComment = Symbol.for("fmodata:FMTableComment");
89
+
90
+ /**
91
+ * Base table class with Symbol-based internal properties.
92
+ * This follows the Drizzle ORM pattern where internal configuration
93
+ * is stored via Symbols, keeping it hidden from IDE autocomplete.
94
+ */
95
+ export class FMTable<
96
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration, default allows untyped tables
97
+ TFields extends Record<string, FieldBuilder<any, any, any, any>> = any,
98
+ TName extends string = string,
99
+ TNavigationPaths extends readonly string[] = readonly string[],
100
+ > {
101
+ /**
102
+ * Internal Symbols for accessing table metadata.
103
+ * @internal - Not intended for public use. Access table properties via columns instead.
104
+ */
105
+ static readonly Symbol = {
106
+ Name: FMTableName,
107
+ EntityId: FMTableEntityId,
108
+ UseEntityIds: FMTableUseEntityIds,
109
+ Schema: FMTableSchema,
110
+ Fields: FMTableFields,
111
+ NavigationPaths: FMTableNavigationPaths,
112
+ DefaultSelect: FMTableDefaultSelect,
113
+ BaseTableConfig: FMTableBaseTableConfig,
114
+ Comment: FMTableComment,
115
+ };
116
+
117
+ /** @internal */
118
+ [FMTableName]: TName;
119
+
120
+ /** @internal */
121
+ [FMTableEntityId]?: `FMTID:${string}`;
122
+
123
+ /** @internal */
124
+ [FMTableUseEntityIds]?: boolean;
125
+
126
+ /** @internal */
127
+ [FMTableComment]?: string;
128
+
129
+ /** @internal */
130
+ [FMTableSchema]: Partial<Record<keyof TFields, StandardSchemaV1>>;
131
+
132
+ /** @internal */
133
+ [FMTableFields]: TFields;
134
+
135
+ /** @internal */
136
+ [FMTableNavigationPaths]: TNavigationPaths;
137
+
138
+ /** @internal */
139
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
140
+ [FMTableDefaultSelect]: "all" | "schema" | Record<string, Column<any, any, TName>>;
141
+
142
+ /** @internal */
143
+ [FMTableBaseTableConfig]: {
144
+ schema: Partial<Record<keyof TFields, StandardSchemaV1>>;
145
+ inputSchema?: Partial<Record<keyof TFields, StandardSchemaV1>>;
146
+ idField?: keyof TFields;
147
+ required: readonly (keyof TFields)[];
148
+ readOnly: readonly (keyof TFields)[];
149
+ containerFields: readonly (keyof TFields)[];
150
+ fmfIds?: Record<keyof TFields, `FMFID:${string}`>;
151
+ };
152
+
153
+ constructor(config: {
154
+ name: TName;
155
+ entityId?: `FMTID:${string}`;
156
+ useEntityIds?: boolean;
157
+ comment?: string;
158
+ schema: Partial<Record<keyof TFields, StandardSchemaV1>>;
159
+ fields: TFields;
160
+ navigationPaths: TNavigationPaths;
161
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
162
+ defaultSelect: "all" | "schema" | Record<string, Column<any, any, TName>>;
163
+ baseTableConfig: {
164
+ schema: Partial<Record<keyof TFields, StandardSchemaV1>>;
165
+ inputSchema?: Partial<Record<keyof TFields, StandardSchemaV1>>;
166
+ idField?: keyof TFields;
167
+ required: readonly (keyof TFields)[];
168
+ readOnly: readonly (keyof TFields)[];
169
+ containerFields: readonly (keyof TFields)[];
170
+ fmfIds?: Record<keyof TFields, `FMFID:${string}`>;
171
+ };
172
+ }) {
173
+ this[FMTableName] = config.name;
174
+ this[FMTableEntityId] = config.entityId;
175
+ this[FMTableUseEntityIds] = config.useEntityIds;
176
+ this[FMTableComment] = config.comment;
177
+ this[FMTableSchema] = config.schema;
178
+ this[FMTableFields] = config.fields;
179
+ this[FMTableNavigationPaths] = config.navigationPaths;
180
+ this[FMTableDefaultSelect] = config.defaultSelect;
181
+ this[FMTableBaseTableConfig] = config.baseTableConfig;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Type helper to extract the column map from fields.
187
+ * Table name is baked into each column type for validation.
188
+ * Container fields are marked with IsContainer=true.
189
+ * Columns include both output type (for reading) and input type (for writing/filtering).
190
+ */
191
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
192
+ export type ColumnMap<TFields extends Record<string, FieldBuilder<any, any, any, any>>, TName extends string> = {
193
+ [K in keyof TFields]: Column<
194
+ InferFieldOutput<TFields[K]>,
195
+ InferFieldInput<TFields[K]>,
196
+ TName,
197
+ IsContainerField<TFields[K]>
198
+ >;
199
+ };
200
+
201
+ /**
202
+ * Extract only selectable (non-container) columns from a table.
203
+ * This is used to prevent selecting container fields in queries.
204
+ */
205
+ export type SelectableColumnMap<
206
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
207
+ TFields extends Record<string, FieldBuilder<any, any, any, any>>,
208
+ TName extends string,
209
+ > = {
210
+ [K in SelectableFieldKeys<TFields>]: Column<InferFieldOutput<TFields[K]>, InferFieldInput<TFields[K]>, TName, false>;
211
+ };
212
+
213
+ /**
214
+ * Validates that a select object doesn't contain container field columns.
215
+ * Returns never if any container fields are found, otherwise returns the original type.
216
+ */
217
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
218
+ export type ValidateNoContainerFields<TSelect extends Record<string, Column<any, any, any, any>>> = {
219
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
220
+ [K in keyof TSelect]: TSelect[K] extends Column<any, any, any, true> ? never : TSelect[K];
221
+ } extends TSelect
222
+ ? TSelect
223
+ : {
224
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
225
+ [K in keyof TSelect]: TSelect[K] extends Column<any, any, any, true>
226
+ ? "❌ Container fields cannot be selected. Use .getSingleField() instead."
227
+ : TSelect[K];
228
+ };
229
+
230
+ /**
231
+ * Extract the keys from a defaultSelect function's return type.
232
+ * Used to infer which fields are selected by default for type narrowing.
233
+ */
234
+ type _ExtractDefaultSelectKeys<
235
+ TDefaultSelect,
236
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
237
+ TFields extends Record<string, FieldBuilder<any, any, any, any>>,
238
+ TName extends string,
239
+ > = TDefaultSelect extends (columns: ColumnMap<TFields, TName>) => infer R
240
+ ? keyof R
241
+ : TDefaultSelect extends "schema"
242
+ ? keyof TFields
243
+ : keyof TFields; // "all" defaults to all keys
244
+
245
+ /**
246
+ * Complete table type with both metadata (via Symbols) and column accessors.
247
+ * This is the return type of fmTableOccurrence - users see columns directly,
248
+ * but internal config is hidden via Symbols.
249
+ */
250
+ export type FMTableWithColumns<
251
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
252
+ TFields extends Record<string, FieldBuilder<any, any, any, any>>,
253
+ TName extends string,
254
+ TNavigationPaths extends readonly string[] = readonly string[],
255
+ > = FMTable<TFields, TName, TNavigationPaths> & ColumnMap<TFields, TName>;
256
+
257
+ /**
258
+ * Options for fmTableOccurrence function.
259
+ * Provides autocomplete-friendly typing while preserving inference for navigationPaths.
260
+ */
261
+ export interface FMTableOccurrenceOptions<
262
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
263
+ TFields extends Record<string, FieldBuilder<any, any, any, any>>,
264
+ TName extends string,
265
+ > {
266
+ /** The entity ID (FMTID) for this table occurrence */
267
+ entityId?: `FMTID:${string}`;
268
+
269
+ /** The comment for this table */
270
+ comment?: string;
271
+
272
+ /**
273
+ * Default select behavior:
274
+ * - "all": Select all fields (including related tables)
275
+ * - "schema": Select only schema-defined fields (default)
276
+ * - function: Custom selection from columns
277
+ */
278
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
279
+ defaultSelect?: "all" | "schema" | ((columns: ColumnMap<TFields, TName>) => Record<string, Column<any, any, TName>>);
280
+
281
+ /** Navigation paths available from this table (for expand operations) */
282
+ navigationPaths?: readonly string[];
283
+
284
+ /** Whether to use entity IDs (FMTID/FMFID) instead of names in queries */
285
+ useEntityIds?: boolean;
286
+ }
287
+
288
+ /**
289
+ * Create a table occurrence with field builders.
290
+ * This is the main API for defining tables in the new ORM style.
291
+ *
292
+ * @example
293
+ * const users = fmTableOccurrence("users", {
294
+ * id: textField().primaryKey().entityId("FMFID:1"),
295
+ * name: textField().notNull().entityId("FMFID:6"),
296
+ * active: numberField()
297
+ * .outputValidator(z.coerce.boolean())
298
+ * .inputValidator(z.boolean().transform(v => v ? 1 : 0))
299
+ * .entityId("FMFID:7"),
300
+ * }, {
301
+ * entityId: "FMTID:100",
302
+ * defaultSelect: "schema",
303
+ * navigationPaths: ["contacts"],
304
+ * });
305
+ *
306
+ * // Access columns
307
+ * users.id // Column<string, "id">
308
+ * users.name // Column<string, "name">
309
+ *
310
+ * // Use in queries
311
+ * db.from(users).select("id", "name").where(eq(users.active, true))
312
+ */
313
+ export function fmTableOccurrence<
314
+ const TName extends string,
315
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any FieldBuilder configuration
316
+ const TFields extends Record<string, FieldBuilder<any, any, any, any>>,
317
+ const TNavPaths extends readonly string[] = readonly [],
318
+ >(
319
+ name: TName,
320
+ fields: TFields,
321
+ options?: FMTableOccurrenceOptions<TFields, TName> & {
322
+ /** Navigation paths available from this table (for expand operations) */
323
+ navigationPaths?: TNavPaths;
324
+ },
325
+ ): FMTableWithColumns<TFields, TName, TNavPaths> {
326
+ // Extract configuration from field builders
327
+ const fieldConfigs = Object.entries(fields).map(([fieldName, builder]) => ({
328
+ fieldName,
329
+ // biome-ignore lint/suspicious/noExplicitAny: Internal property access for builder pattern
330
+ config: (builder as any)._getConfig(),
331
+ }));
332
+
333
+ // Find primary key field
334
+ const primaryKeyField = fieldConfigs.find((f) => f.config.primaryKey);
335
+ const idField = primaryKeyField?.fieldName;
336
+
337
+ // Collect required fields (notNull fields)
338
+ const required = fieldConfigs.filter((f) => f.config.notNull).map((f) => f.fieldName);
339
+
340
+ // Collect read-only fields
341
+ const readOnly = fieldConfigs.filter((f) => f.config.readOnly).map((f) => f.fieldName);
342
+
343
+ // Collect container fields (cannot be selected via $select)
344
+ const containerFields = fieldConfigs.filter((f) => f.config.fieldType === "container").map((f) => f.fieldName);
345
+
346
+ // Collect entity IDs
347
+ const fmfIds: Record<string, `FMFID:${string}`> = {};
348
+ for (const { fieldName, config } of fieldConfigs) {
349
+ if (config.entityId) {
350
+ fmfIds[fieldName] = config.entityId;
351
+ }
352
+ }
353
+
354
+ // Build Zod schema from field builders (output/read validators)
355
+ const outputSchema: Partial<Record<keyof TFields, StandardSchemaV1>> = {};
356
+ // Build input schema from field builders (input/write validators)
357
+ const inputSchema: Record<string, StandardSchemaV1> = {};
358
+
359
+ for (const { fieldName, config } of fieldConfigs) {
360
+ // Use outputValidator if provided
361
+ if (config.outputValidator) {
362
+ outputSchema[fieldName as keyof TFields] = config.outputValidator;
363
+ }
364
+
365
+ // Store inputValidator if provided (for write operations)
366
+ if (config.inputValidator) {
367
+ inputSchema[fieldName] = config.inputValidator;
368
+ }
369
+ }
370
+
371
+ // Build BaseTable-compatible config
372
+ const baseTableConfig = {
373
+ schema: outputSchema as Partial<Record<keyof TFields, StandardSchemaV1>>,
374
+ inputSchema:
375
+ Object.keys(inputSchema).length > 0
376
+ ? (inputSchema as Partial<Record<keyof TFields, StandardSchemaV1>>)
377
+ : undefined,
378
+ idField: idField as keyof TFields | undefined,
379
+ required: required as readonly (keyof TFields)[],
380
+ readOnly: readOnly as readonly (keyof TFields)[],
381
+ containerFields: containerFields as readonly (keyof TFields)[],
382
+ fmfIds: (Object.keys(fmfIds).length > 0 ? fmfIds : undefined) as
383
+ | Record<keyof TFields, `FMFID:${string}`>
384
+ | undefined,
385
+ };
386
+
387
+ // Create column instances
388
+ const columns = {} as ColumnMap<TFields, TName>;
389
+ for (const [fieldName, builder] of Object.entries(fields)) {
390
+ // biome-ignore lint/suspicious/noExplicitAny: Internal property access for builder pattern
391
+ const config = (builder as any)._getConfig();
392
+ // biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
393
+ (columns as any)[fieldName] = new Column({
394
+ fieldName: String(fieldName),
395
+ entityId: config.entityId,
396
+ tableName: name,
397
+ tableEntityId: options?.entityId,
398
+ inputValidator: config.inputValidator,
399
+ });
400
+ }
401
+
402
+ // Resolve defaultSelect: if it's a function, call it with columns; otherwise use as-is
403
+ const defaultSelectOption = options?.defaultSelect ?? "schema";
404
+ // biome-ignore lint/suspicious/noExplicitAny: Generic constraint accepting any Column configuration
405
+ const resolvedDefaultSelect: "all" | "schema" | Record<string, Column<any, any, TName>> =
406
+ typeof defaultSelectOption === "function"
407
+ ? defaultSelectOption(columns as ColumnMap<TFields, TName>)
408
+ : defaultSelectOption;
409
+
410
+ // Create the FMTable instance with Symbol-based internal properties
411
+ const navigationPaths = (options?.navigationPaths ?? []) as TNavPaths;
412
+ const table = new FMTable<TFields, TName, TNavPaths>({
413
+ name,
414
+ entityId: options?.entityId,
415
+ useEntityIds: options?.useEntityIds,
416
+ comment: options?.comment,
417
+ schema: outputSchema,
418
+ fields,
419
+ navigationPaths,
420
+ defaultSelect: resolvedDefaultSelect,
421
+ baseTableConfig,
422
+ });
423
+
424
+ // Assign columns to the table instance (making them accessible directly)
425
+ Object.assign(table, columns);
426
+
427
+ return table as FMTableWithColumns<TFields, TName, TNavPaths>;
428
+ }
429
+
430
+ // /**
431
+ // * Type guard to check if a value is a TableOccurrence or FMTable.
432
+ // * Supports both Symbol-based (new) and underscore-prefixed (legacy) formats.
433
+ // */
434
+ // function isTableOccurrence(value: any): value is TableOccurrence {
435
+ // if (!value || typeof value !== "object") {
436
+ // return false;
437
+ // }
438
+
439
+ // // Check for Symbol-based format (new FMTable class)
440
+ // if (
441
+ // FMTableName in value &&
442
+ // FMTableSchema in value &&
443
+ // FMTableFields in value
444
+ // ) {
445
+ // return typeof value[FMTableName] === "string";
446
+ // }
447
+
448
+ // // Check for underscore-prefixed format (legacy interface)
449
+ // if ("_name" in value && "_schema" in value && "_fields" in value) {
450
+ // return typeof value._name === "string";
451
+ // }
452
+
453
+ // return false;
454
+ // }
455
+
456
+ /**
457
+ * Helper to extract the schema type from a TableOccurrence or FMTable.
458
+ */
459
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
460
+ export type InferTableSchema<T> = T extends FMTable<infer TFields, any> ? InferSchemaFromFields<TFields> : never;
461
+
462
+ /**
463
+ * Extract the schema type from an FMTable instance.
464
+ * This is used to infer the schema from table objects passed to db.from(), expand(), etc.
465
+ */
466
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
467
+ export type InferSchemaOutputFromFMTable<T extends FMTable<any, any>> =
468
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
469
+ T extends FMTable<infer TFields, any> ? InferSchemaFromFields<TFields> : never;
470
+
471
+ /**
472
+ * Extract the input schema type from an FMTable instance.
473
+ * This is used for insert and update operations where we need write types.
474
+ */
475
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
476
+ export type InferInputSchemaFromFMTable<T extends FMTable<any, any>> =
477
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
478
+ T extends FMTable<infer TFields, any> ? InferInputSchemaFromFields<TFields> : never;
479
+
480
+ /**
481
+ * Helper type to check if a FieldBuilder's input type excludes null and undefined.
482
+ * This checks the TInput type parameter, which preserves nullability from notNull().
483
+ */
484
+ type FieldInputExcludesNullish<F> =
485
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
486
+ F extends FieldBuilder<any, infer TInput, any>
487
+ ? null extends TInput
488
+ ? false
489
+ : undefined extends TInput
490
+ ? false
491
+ : true
492
+ : false;
493
+
494
+ /**
495
+ * Check if a FieldBuilder is readOnly at the type level
496
+ */
497
+ type IsFieldReadOnly<F> =
498
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
499
+ F extends FieldBuilderType<any, any, any, infer ReadOnly> ? (ReadOnly extends true ? true : false) : false;
500
+
501
+ /**
502
+ * Compute insert data type from FMTable, making notNull fields required.
503
+ * Fields are required if their FieldBuilder's TInput type excludes null/undefined.
504
+ * All other fields are optional (can be omitted).
505
+ * readOnly fields are excluded (including primaryKey/idField since they're automatically readOnly).
506
+ */
507
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
508
+ export type InsertDataFromFMTable<T extends FMTable<any, any>> =
509
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
510
+ T extends FMTable<infer TFields, any>
511
+ ? {
512
+ [K in keyof TFields as IsFieldReadOnly<TFields[K]> extends true
513
+ ? never
514
+ : FieldInputExcludesNullish<TFields[K]> extends true
515
+ ? K
516
+ : never]: InferFieldInput<TFields[K]>;
517
+ } & {
518
+ [K in keyof TFields as IsFieldReadOnly<TFields[K]> extends true
519
+ ? never
520
+ : FieldInputExcludesNullish<TFields[K]> extends true
521
+ ? never
522
+ : K]?: InferFieldInput<TFields[K]>;
523
+ }
524
+ : never;
525
+
526
+ /**
527
+ * Compute update data type from FMTable.
528
+ * All fields are optional, but readOnly fields are excluded (including primaryKey/idField).
529
+ */
530
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
531
+ export type UpdateDataFromFMTable<T extends FMTable<any, any>> =
532
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
533
+ T extends FMTable<infer TFields, any>
534
+ ? {
535
+ [K in keyof TFields as IsFieldReadOnly<TFields[K]> extends true ? never : K]?: InferFieldInput<TFields[K]>;
536
+ }
537
+ : never;
538
+
539
+ /**
540
+ * Extract the table name type from an FMTable.
541
+ * This is a workaround since we can't directly index Symbols in types.
542
+ */
543
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration, required for type inference with infer
544
+ export type ExtractTableName<T extends FMTable<any, any>> = T extends FMTable<any, infer Name> ? Name : never;
545
+
546
+ /**
547
+ * Validates that a target table's name matches one of the source table's navigationPaths.
548
+ * Used to ensure type-safe expand/navigate operations.
549
+ */
550
+ export type ValidExpandTarget<
551
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
552
+ SourceTable extends FMTable<any, any, any> | undefined,
553
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
554
+ TargetTable extends FMTable<any, any, any>,
555
+ // biome-ignore lint/suspicious/noExplicitAny: Required for type inference with infer
556
+ > = SourceTable extends FMTable<any, any, infer SourceNavPaths>
557
+ ? ExtractTableName<TargetTable> extends SourceNavPaths[number]
558
+ ? TargetTable
559
+ : never
560
+ : TargetTable;
561
+
562
+ // ============================================================================
563
+ // Helper Functions for Accessing FMTable Internal Properties
564
+ // ============================================================================
565
+
566
+ /**
567
+ * Get the table name from an FMTable instance.
568
+ * @param table - FMTable instance
569
+ * @returns The table name
570
+ */
571
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
572
+ export function getTableName<T extends FMTable<any, any>>(table: T): string {
573
+ return table[FMTableName];
574
+ }
575
+
576
+ /**
577
+ * Get the entity ID (FMTID) from an FMTable instance.
578
+ * @param table - FMTable instance
579
+ * @returns The entity ID or undefined if not using entity IDs
580
+ */
581
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
582
+ export function getTableEntityId<T extends FMTable<any, any>>(table: T): string | undefined {
583
+ return table[FMTableEntityId];
584
+ }
585
+
586
+ /**
587
+ * Get the schema validator from an FMTable instance.
588
+ * @param table - FMTable instance
589
+ * @returns The StandardSchemaV1 validator record (partial - only fields with validators)
590
+ */
591
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
592
+ export function getTableSchema<T extends FMTable<any, any>>(
593
+ table: T,
594
+ ): Partial<Record<keyof T[typeof FMTableFields], StandardSchemaV1>> {
595
+ return table[FMTableSchema];
596
+ }
597
+
598
+ /**
599
+ * Get the fields from an FMTable instance.
600
+ * @param table - FMTable instance
601
+ * @returns The fields record
602
+ */
603
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
604
+ export function getTableFields<T extends FMTable<any, any>>(table: T) {
605
+ return table[FMTableFields];
606
+ }
607
+
608
+ /**
609
+ * Get the navigation paths from an FMTable instance.
610
+ * @param table - FMTable instance
611
+ * @returns Array of navigation path names
612
+ */
613
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
614
+ export function getNavigationPaths<T extends FMTable<any, any>>(table: T): readonly string[] {
615
+ return table[FMTableNavigationPaths];
616
+ }
617
+
618
+ /**
619
+ * Get the default select configuration from an FMTable instance.
620
+ * @param table - FMTable instance
621
+ * @returns Default select configuration
622
+ */
623
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
624
+ export function getDefaultSelect<T extends FMTable<any, any>>(table: T) {
625
+ return table[FMTableDefaultSelect];
626
+ }
627
+
628
+ /**
629
+ * Get the base table configuration from an FMTable instance.
630
+ * This provides access to schema, idField, required fields, readOnly fields, and field IDs.
631
+ * @param table - FMTable instance
632
+ * @returns Base table configuration object
633
+ */
634
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
635
+ export function getBaseTableConfig<T extends FMTable<any, any>>(table: T) {
636
+ return table[FMTableBaseTableConfig];
637
+ }
638
+
639
+ /**
640
+ * Check if an FMTable instance is using entity IDs (both FMTID and FMFIDs).
641
+ * @param table - FMTable instance
642
+ * @returns True if using entity IDs, false otherwise
643
+ */
644
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
645
+ export function isUsingEntityIds<T extends FMTable<any, any>>(table: T): boolean {
646
+ return table[FMTableEntityId] !== undefined && table[FMTableBaseTableConfig].fmfIds !== undefined;
647
+ }
648
+
649
+ /**
650
+ * Get the field ID (FMFID) for a given field name, or the field name itself if not using IDs.
651
+ * @param table - FMTable instance
652
+ * @param fieldName - Field name to get the ID for
653
+ * @returns The FMFID string or the original field name
654
+ */
655
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
656
+ export function getFieldId<T extends FMTable<any, any>>(table: T, fieldName: string): string {
657
+ const config = table[FMTableBaseTableConfig];
658
+ if (config.fmfIds && fieldName in config.fmfIds) {
659
+ const fieldId = config.fmfIds[fieldName];
660
+ if (fieldId) {
661
+ return fieldId;
662
+ }
663
+ }
664
+ return fieldName;
665
+ }
666
+
667
+ /**
668
+ * Get the field name for a given field ID (FMFID), or the ID itself if not found.
669
+ * @param table - FMTable instance
670
+ * @param fieldId - The FMFID to get the field name for
671
+ * @returns The field name or the original ID
672
+ */
673
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
674
+ export function getFieldName<T extends FMTable<any, any>>(table: T, fieldId: string): string {
675
+ const config = table[FMTableBaseTableConfig];
676
+ if (config.fmfIds) {
677
+ for (const [fieldName, fmfId] of Object.entries(config.fmfIds)) {
678
+ if (fmfId === fieldId) {
679
+ return fieldName;
680
+ }
681
+ }
682
+ }
683
+ return fieldId;
684
+ }
685
+ /**
686
+ * Get the table ID (FMTID or name) from an FMTable instance.
687
+ * Returns the FMTID if available, otherwise returns the table name.
688
+ * @param table - FMTable instance
689
+ * @returns The FMTID string or the table name
690
+ */
691
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
692
+ export function getTableId<T extends FMTable<any, any>>(table: T): string {
693
+ return table[FMTableEntityId] ?? table[FMTableName];
694
+ }
695
+
696
+ /**
697
+ * Get the comment from an FMTable instance.
698
+ * @param table - FMTable instance
699
+ * @returns The comment string or undefined if not set
700
+ */
701
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
702
+ export function getTableComment<T extends FMTable<any, any>>(table: T): string | undefined {
703
+ return table[FMTableComment];
704
+ }
705
+
706
+ /**
707
+ * Get all columns from a table as an object.
708
+ * Useful for selecting all fields except some using destructuring.
709
+ *
710
+ * @example
711
+ * const { password, ...cols } = getTableColumns(users)
712
+ * db.from(users).list().select(cols)
713
+ *
714
+ * @param table - FMTable instance
715
+ * @returns Object with all columns from the table
716
+ */
717
+ // biome-ignore lint/suspicious/noExplicitAny: Accepts any FMTable configuration
718
+ export function getTableColumns<T extends FMTable<any, any>>(
719
+ table: T,
720
+ ): ColumnMap<T[typeof FMTableFields], ExtractTableName<T>> {
721
+ const fields = table[FMTableFields];
722
+ const tableName = table[FMTableName];
723
+ const tableEntityId = table[FMTableEntityId];
724
+ const baseConfig = table[FMTableBaseTableConfig];
725
+
726
+ const columns = {} as ColumnMap<T[typeof FMTableFields], ExtractTableName<T>>;
727
+ for (const [fieldName, builder] of Object.entries(fields)) {
728
+ // biome-ignore lint/suspicious/noExplicitAny: Internal property access for builder pattern
729
+ const config = (builder as any)._getConfig();
730
+ // biome-ignore lint/suspicious/noExplicitAny: Mutation of readonly properties for builder pattern
731
+ (columns as any)[fieldName] = new Column({
732
+ fieldName: String(fieldName),
733
+ entityId: baseConfig.fmfIds?.[fieldName],
734
+ tableName,
735
+ tableEntityId,
736
+ inputValidator: config.inputValidator,
737
+ });
738
+ }
739
+
740
+ return columns;
741
+ }