@proofkit/fmodata 0.1.0-alpha.0

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 (54) hide show
  1. package/README.md +37 -0
  2. package/dist/esm/client/base-table.d.ts +13 -0
  3. package/dist/esm/client/base-table.js +19 -0
  4. package/dist/esm/client/base-table.js.map +1 -0
  5. package/dist/esm/client/database.d.ts +49 -0
  6. package/dist/esm/client/database.js +90 -0
  7. package/dist/esm/client/database.js.map +1 -0
  8. package/dist/esm/client/delete-builder.d.ts +61 -0
  9. package/dist/esm/client/delete-builder.js +121 -0
  10. package/dist/esm/client/delete-builder.js.map +1 -0
  11. package/dist/esm/client/entity-set.d.ts +43 -0
  12. package/dist/esm/client/entity-set.js +120 -0
  13. package/dist/esm/client/entity-set.js.map +1 -0
  14. package/dist/esm/client/filemaker-odata.d.ts +26 -0
  15. package/dist/esm/client/filemaker-odata.js +85 -0
  16. package/dist/esm/client/filemaker-odata.js.map +1 -0
  17. package/dist/esm/client/insert-builder.d.ts +23 -0
  18. package/dist/esm/client/insert-builder.js +69 -0
  19. package/dist/esm/client/insert-builder.js.map +1 -0
  20. package/dist/esm/client/query-builder.d.ts +94 -0
  21. package/dist/esm/client/query-builder.js +649 -0
  22. package/dist/esm/client/query-builder.js.map +1 -0
  23. package/dist/esm/client/record-builder.d.ts +43 -0
  24. package/dist/esm/client/record-builder.js +121 -0
  25. package/dist/esm/client/record-builder.js.map +1 -0
  26. package/dist/esm/client/table-occurrence.d.ts +25 -0
  27. package/dist/esm/client/table-occurrence.js +47 -0
  28. package/dist/esm/client/table-occurrence.js.map +1 -0
  29. package/dist/esm/client/update-builder.d.ts +69 -0
  30. package/dist/esm/client/update-builder.js +134 -0
  31. package/dist/esm/client/update-builder.js.map +1 -0
  32. package/dist/esm/filter-types.d.ts +76 -0
  33. package/dist/esm/index.d.ts +4 -0
  34. package/dist/esm/index.js +10 -0
  35. package/dist/esm/index.js.map +1 -0
  36. package/dist/esm/types.d.ts +67 -0
  37. package/dist/esm/validation.d.ts +41 -0
  38. package/dist/esm/validation.js +270 -0
  39. package/dist/esm/validation.js.map +1 -0
  40. package/package.json +68 -0
  41. package/src/client/base-table.ts +25 -0
  42. package/src/client/database.ts +177 -0
  43. package/src/client/delete-builder.ts +193 -0
  44. package/src/client/entity-set.ts +310 -0
  45. package/src/client/filemaker-odata.ts +119 -0
  46. package/src/client/insert-builder.ts +93 -0
  47. package/src/client/query-builder.ts +1076 -0
  48. package/src/client/record-builder.ts +240 -0
  49. package/src/client/table-occurrence.ts +100 -0
  50. package/src/client/update-builder.ts +212 -0
  51. package/src/filter-types.ts +97 -0
  52. package/src/index.ts +17 -0
  53. package/src/types.ts +123 -0
  54. package/src/validation.ts +397 -0
@@ -0,0 +1,240 @@
1
+ import type {
2
+ ExecutionContext,
3
+ ExecutableBuilder,
4
+ Result,
5
+ ODataRecordMetadata,
6
+ ODataFieldResponse,
7
+ InferSchemaType,
8
+ } from "../types";
9
+ import type { TableOccurrence } from "./table-occurrence";
10
+ import type { BaseTable } from "./base-table";
11
+ import { QueryBuilder } from "./query-builder";
12
+ import { validateSingleResponse } from "../validation";
13
+ import { type FFetchOptions } from "@fetchkit/ffetch";
14
+ import { z } from "zod/v4";
15
+
16
+ // Helper type to extract schema from a TableOccurrence
17
+ type ExtractSchemaFromOccurrence<O> =
18
+ O extends TableOccurrence<infer BT, any, any, any>
19
+ ? BT extends BaseTable<infer S, any>
20
+ ? S
21
+ : never
22
+ : never;
23
+
24
+ // Helper type to extract navigation relation names from an occurrence
25
+ type ExtractNavigationNames<
26
+ O extends TableOccurrence<any, any, any, any> | undefined,
27
+ > =
28
+ O extends TableOccurrence<any, any, infer Nav, any>
29
+ ? Nav extends Record<string, any>
30
+ ? keyof Nav
31
+ : never
32
+ : never;
33
+
34
+ // Helper type to resolve a navigation item (handles both direct and lazy-loaded)
35
+ type ResolveNavigationItem<T> = T extends () => infer R ? R : T;
36
+
37
+ // Helper type to find target occurrence by relation name
38
+ type FindNavigationTarget<
39
+ O extends TableOccurrence<any, any, any, any> | undefined,
40
+ Name extends string,
41
+ > =
42
+ O extends TableOccurrence<any, any, infer Nav, any>
43
+ ? Name extends keyof Nav
44
+ ? ResolveNavigationItem<Nav[Name]>
45
+ : never
46
+ : never;
47
+
48
+ export class RecordBuilder<
49
+ T extends Record<string, any>,
50
+ IsSingleField extends boolean = false,
51
+ FieldKey extends keyof T = keyof T,
52
+ Occ extends TableOccurrence<any, any, any, any> | undefined =
53
+ | TableOccurrence<any, any, any, any>
54
+ | undefined,
55
+ > implements
56
+ ExecutableBuilder<
57
+ IsSingleField extends true ? T[FieldKey] : T & ODataRecordMetadata
58
+ >
59
+ {
60
+ private occurrence?: Occ;
61
+ private tableName: string;
62
+ private databaseName: string;
63
+ private context: ExecutionContext;
64
+ private recordId: string | number;
65
+ private operation?: "getSingleField" | "navigate";
66
+ private operationParam?: string;
67
+ private isNavigateFromEntitySet?: boolean;
68
+ private navigateRelation?: string;
69
+ private navigateSourceTableName?: string;
70
+
71
+ constructor(config: {
72
+ occurrence?: Occ;
73
+ tableName: string;
74
+ databaseName: string;
75
+ context: ExecutionContext;
76
+ recordId: string | number;
77
+ }) {
78
+ this.occurrence = config.occurrence;
79
+ this.tableName = config.tableName;
80
+ this.databaseName = config.databaseName;
81
+ this.context = config.context;
82
+ this.recordId = config.recordId;
83
+ }
84
+
85
+ getSingleField<K extends keyof T>(field: K): RecordBuilder<T, true, K, Occ> {
86
+ const newBuilder = new RecordBuilder<T, true, K, Occ>({
87
+ occurrence: this.occurrence,
88
+ tableName: this.tableName,
89
+ databaseName: this.databaseName,
90
+ context: this.context,
91
+ recordId: this.recordId,
92
+ });
93
+ newBuilder.operation = "getSingleField";
94
+ newBuilder.operationParam = field.toString();
95
+ // Preserve navigation context
96
+ newBuilder.isNavigateFromEntitySet = this.isNavigateFromEntitySet;
97
+ newBuilder.navigateRelation = this.navigateRelation;
98
+ newBuilder.navigateSourceTableName = this.navigateSourceTableName;
99
+ return newBuilder;
100
+ }
101
+
102
+ // Overload for valid relation names - returns typed QueryBuilder
103
+ navigate<RelationName extends ExtractNavigationNames<Occ>>(
104
+ relationName: RelationName,
105
+ ): QueryBuilder<
106
+ ExtractSchemaFromOccurrence<
107
+ FindNavigationTarget<Occ, RelationName>
108
+ > extends Record<string, z.ZodType>
109
+ ? InferSchemaType<
110
+ ExtractSchemaFromOccurrence<FindNavigationTarget<Occ, RelationName>>
111
+ >
112
+ : Record<string, any>
113
+ >;
114
+ // Overload for arbitrary strings - returns generic QueryBuilder with system fields
115
+ navigate(
116
+ relationName: string,
117
+ ): QueryBuilder<{ ROWID: number; ROWMODID: number; [key: string]: any }>;
118
+ // Implementation
119
+ navigate(relationName: string): QueryBuilder<any> {
120
+ // Use the target occurrence if available, otherwise allow untyped navigation
121
+ // (useful when types might be incomplete)
122
+ const targetOccurrence = this.occurrence?.navigation[relationName];
123
+ const builder = new QueryBuilder<any>({
124
+ occurrence: targetOccurrence,
125
+ tableName: targetOccurrence?.name ?? relationName,
126
+ databaseName: this.databaseName,
127
+ context: this.context,
128
+ });
129
+ // Store the navigation info - we'll use it in execute
130
+ (builder as any).isNavigate = true;
131
+ (builder as any).navigateRecordId = this.recordId;
132
+ (builder as any).navigateRelation = relationName;
133
+
134
+ // If this RecordBuilder came from a navigated EntitySet, we need to preserve that base path
135
+ if (
136
+ this.isNavigateFromEntitySet &&
137
+ this.navigateSourceTableName &&
138
+ this.navigateRelation
139
+ ) {
140
+ // Build the base path: /sourceTable/relation('recordId')/newRelation
141
+ (builder as any).navigateSourceTableName = this.navigateSourceTableName;
142
+ (builder as any).navigateBaseRelation = this.navigateRelation;
143
+ } else {
144
+ // Normal record navigation: /tableName('recordId')/relation
145
+ (builder as any).navigateSourceTableName = this.tableName;
146
+ }
147
+
148
+ return builder;
149
+ }
150
+
151
+ async execute(
152
+ options?: RequestInit & FFetchOptions,
153
+ ): Promise<
154
+ Result<IsSingleField extends true ? T[FieldKey] : T & ODataRecordMetadata>
155
+ > {
156
+ try {
157
+ let url: string;
158
+
159
+ // Build the base URL depending on whether this came from a navigated EntitySet
160
+ if (
161
+ this.isNavigateFromEntitySet &&
162
+ this.navigateSourceTableName &&
163
+ this.navigateRelation
164
+ ) {
165
+ // From navigated EntitySet: /sourceTable/relation('recordId')
166
+ url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;
167
+ } else {
168
+ // Normal record: /tableName('recordId')
169
+ url = `/${this.databaseName}/${this.tableName}('${this.recordId}')`;
170
+ }
171
+
172
+ if (this.operation === "getSingleField" && this.operationParam) {
173
+ url += `/${this.operationParam}`;
174
+ }
175
+
176
+ const response = await this.context._makeRequest(url, options);
177
+
178
+ // Handle single field operation
179
+ if (this.operation === "getSingleField") {
180
+ // Single field returns a JSON object with @context and value
181
+ const fieldResponse = response as ODataFieldResponse<T>;
182
+ return { data: fieldResponse.value as any, error: undefined };
183
+ }
184
+
185
+ // Get schema from occurrence if available
186
+ const schema = this.occurrence?.baseTable?.schema;
187
+
188
+ // Validate the single record response
189
+ const validation = await validateSingleResponse<any>(
190
+ response,
191
+ schema,
192
+ undefined, // No selected fields for record.get()
193
+ undefined, // No expand configs
194
+ "exact", // Expect exactly one record
195
+ );
196
+
197
+ if (!validation.valid) {
198
+ return { data: undefined, error: validation.error };
199
+ }
200
+
201
+ // Handle null response
202
+ if (validation.data === null) {
203
+ return { data: null as any, error: undefined };
204
+ }
205
+
206
+ return { data: validation.data, error: undefined };
207
+ } catch (error) {
208
+ return {
209
+ data: undefined,
210
+ error: error instanceof Error ? error : new Error(String(error)),
211
+ };
212
+ }
213
+ }
214
+
215
+ getRequestConfig(): { method: string; url: string; body?: any } {
216
+ let url: string;
217
+
218
+ // Build the base URL depending on whether this came from a navigated EntitySet
219
+ if (
220
+ this.isNavigateFromEntitySet &&
221
+ this.navigateSourceTableName &&
222
+ this.navigateRelation
223
+ ) {
224
+ // From navigated EntitySet: /sourceTable/relation('recordId')
225
+ url = `/${this.databaseName}/${this.navigateSourceTableName}/${this.navigateRelation}('${this.recordId}')`;
226
+ } else {
227
+ // Normal record: /tableName('recordId')
228
+ url = `/${this.databaseName}/${this.tableName}('${this.recordId}')`;
229
+ }
230
+
231
+ if (this.operation === "getSingleField" && this.operationParam) {
232
+ url += `/${this.operationParam}`;
233
+ }
234
+
235
+ return {
236
+ method: "GET",
237
+ url,
238
+ };
239
+ }
240
+ }
@@ -0,0 +1,100 @@
1
+ import { BaseTable } from "./base-table";
2
+
3
+ // Helper type to extract schema from BaseTable
4
+ type ExtractSchema<BT> =
5
+ BT extends BaseTable<infer S, any, any, any> ? S : never;
6
+
7
+ // Helper type to resolve navigation functions to their return types
8
+ type ResolveNavigation<T> = {
9
+ [K in keyof T]: T[K] extends () => infer R ? R : T[K];
10
+ };
11
+
12
+ // Helper to create a getter-based navigation object
13
+ function createNavigationGetters<
14
+ Nav extends Record<
15
+ string,
16
+ | TableOccurrence<any, any, any, any>
17
+ | (() => TableOccurrence<any, any, any, any>)
18
+ >,
19
+ >(navConfig: Nav): ResolveNavigation<Nav> {
20
+ const result: any = {};
21
+
22
+ for (const key in navConfig) {
23
+ Object.defineProperty(result, key, {
24
+ get() {
25
+ const navItem = navConfig[key];
26
+ return typeof navItem === "function" ? navItem() : navItem;
27
+ },
28
+ enumerable: true,
29
+ configurable: true,
30
+ });
31
+ }
32
+
33
+ return result as ResolveNavigation<Nav>;
34
+ }
35
+
36
+ export class TableOccurrence<
37
+ BT extends BaseTable<any, any, any, any> = any,
38
+ Name extends string = string,
39
+ Nav extends Record<
40
+ string,
41
+ | TableOccurrence<any, any, any, any>
42
+ | (() => TableOccurrence<any, any, any, any>)
43
+ > = {},
44
+ DefSelect extends
45
+ | "all"
46
+ | "schema"
47
+ | readonly (keyof ExtractSchema<BT>)[] = "schema",
48
+ > {
49
+ public readonly name: Name;
50
+ public readonly baseTable: BT;
51
+ private _navigationConfig: Nav;
52
+ public readonly navigation: ResolveNavigation<Nav>;
53
+ public readonly defaultSelect: DefSelect;
54
+
55
+ constructor(config: {
56
+ readonly name: Name;
57
+ readonly baseTable: BT;
58
+ readonly navigation?: Nav;
59
+ readonly defaultSelect?: DefSelect;
60
+ }) {
61
+ this.name = config.name;
62
+ this.baseTable = config.baseTable;
63
+ this._navigationConfig = (config.navigation ?? {}) as Nav;
64
+ this.defaultSelect = (config.defaultSelect ?? "schema") as DefSelect;
65
+
66
+ // Create navigation getters that lazily resolve functions
67
+ this.navigation = createNavigationGetters(this._navigationConfig);
68
+ }
69
+
70
+ addNavigation<
71
+ NewNav extends Record<
72
+ string,
73
+ | TableOccurrence<any, any, any, any>
74
+ | (() => TableOccurrence<any, any, any, any>)
75
+ >,
76
+ >(nav: NewNav): TableOccurrence<BT, Name, Nav & NewNav, DefSelect> {
77
+ return new TableOccurrence({
78
+ name: this.name,
79
+ baseTable: this.baseTable,
80
+ navigation: { ...this._navigationConfig, ...nav } as Nav & NewNav,
81
+ defaultSelect: this.defaultSelect,
82
+ });
83
+ }
84
+ }
85
+
86
+ // Helper function to create TableOccurrence with proper type inference
87
+ export function createTableOccurrence<
88
+ const Name extends string,
89
+ BT extends BaseTable<any, any, any, any>,
90
+ DefSelect extends
91
+ | "all"
92
+ | "schema"
93
+ | readonly (keyof ExtractSchema<BT>)[] = "schema",
94
+ >(config: {
95
+ name: Name;
96
+ baseTable: BT;
97
+ defaultSelect?: DefSelect;
98
+ }): TableOccurrence<BT, Name, {}, DefSelect> {
99
+ return new TableOccurrence(config);
100
+ }
@@ -0,0 +1,212 @@
1
+ import type {
2
+ ExecutionContext,
3
+ ExecutableBuilder,
4
+ Result,
5
+ WithSystemFields,
6
+ } from "../types";
7
+ import type { TableOccurrence } from "./table-occurrence";
8
+ import type { BaseTable } from "./base-table";
9
+ import { QueryBuilder } from "./query-builder";
10
+ import { type FFetchOptions } from "@fetchkit/ffetch";
11
+
12
+ /**
13
+ * Initial update builder returned from EntitySet.update(data)
14
+ * Requires calling .byId() or .where() before .execute() is available
15
+ */
16
+ export class UpdateBuilder<
17
+ T extends Record<string, any>,
18
+ BT extends BaseTable<any, any, any, any>,
19
+ > {
20
+ private tableName: string;
21
+ private databaseName: string;
22
+ private context: ExecutionContext;
23
+ private occurrence?: TableOccurrence<any, any, any, any>;
24
+ private data: Partial<T>;
25
+
26
+ constructor(config: {
27
+ occurrence?: TableOccurrence<any, any, any, any>;
28
+ tableName: string;
29
+ databaseName: string;
30
+ context: ExecutionContext;
31
+ data: Partial<T>;
32
+ }) {
33
+ this.occurrence = config.occurrence;
34
+ this.tableName = config.tableName;
35
+ this.databaseName = config.databaseName;
36
+ this.context = config.context;
37
+ this.data = config.data;
38
+ }
39
+
40
+ /**
41
+ * Update a single record by ID
42
+ * Returns the count of updated records (0 or 1)
43
+ */
44
+ byId(id: string | number): ExecutableUpdateBuilder<T, true> {
45
+ return new ExecutableUpdateBuilder<T, true>({
46
+ occurrence: this.occurrence,
47
+ tableName: this.tableName,
48
+ databaseName: this.databaseName,
49
+ context: this.context,
50
+ data: this.data,
51
+ mode: "byId",
52
+ recordId: id,
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Update records matching a filter query
58
+ * Returns the count of updated records
59
+ * @param fn Callback that receives a QueryBuilder for building the filter
60
+ */
61
+ where(
62
+ fn: (
63
+ q: QueryBuilder<WithSystemFields<T>>,
64
+ ) => QueryBuilder<WithSystemFields<T>>,
65
+ ): ExecutableUpdateBuilder<T, true> {
66
+ // Create a QueryBuilder for the user to configure
67
+ const queryBuilder = new QueryBuilder<
68
+ WithSystemFields<T>,
69
+ keyof WithSystemFields<T>,
70
+ false,
71
+ false,
72
+ undefined
73
+ >({
74
+ occurrence: undefined,
75
+ tableName: this.tableName,
76
+ databaseName: this.databaseName,
77
+ context: this.context,
78
+ });
79
+
80
+ // Let the user configure it
81
+ const configuredBuilder = fn(queryBuilder);
82
+
83
+ return new ExecutableUpdateBuilder<T, true>({
84
+ occurrence: this.occurrence,
85
+ tableName: this.tableName,
86
+ databaseName: this.databaseName,
87
+ context: this.context,
88
+ data: this.data,
89
+ mode: "byFilter",
90
+ queryBuilder: configuredBuilder,
91
+ });
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Executable update builder - has execute() method
97
+ * Returned after calling .byId() or .where()
98
+ * Both modes return the count of updated records
99
+ */
100
+ export class ExecutableUpdateBuilder<
101
+ T extends Record<string, any>,
102
+ IsByFilter extends boolean,
103
+ > implements ExecutableBuilder<{ updatedCount: number }>
104
+ {
105
+ private tableName: string;
106
+ private databaseName: string;
107
+ private context: ExecutionContext;
108
+ private occurrence?: TableOccurrence<any, any, any, any>;
109
+ private data: Partial<T>;
110
+ private mode: "byId" | "byFilter";
111
+ private recordId?: string | number;
112
+ private queryBuilder?: QueryBuilder<any>;
113
+
114
+ constructor(config: {
115
+ occurrence?: TableOccurrence<any, any, any, any>;
116
+ tableName: string;
117
+ databaseName: string;
118
+ context: ExecutionContext;
119
+ data: Partial<T>;
120
+ mode: "byId" | "byFilter";
121
+ recordId?: string | number;
122
+ queryBuilder?: QueryBuilder<any>;
123
+ }) {
124
+ this.occurrence = config.occurrence;
125
+ this.tableName = config.tableName;
126
+ this.databaseName = config.databaseName;
127
+ this.context = config.context;
128
+ this.data = config.data;
129
+ this.mode = config.mode;
130
+ this.recordId = config.recordId;
131
+ this.queryBuilder = config.queryBuilder;
132
+ }
133
+
134
+ async execute(
135
+ options?: RequestInit & FFetchOptions,
136
+ ): Promise<Result<{ updatedCount: number }>> {
137
+ try {
138
+ let url: string;
139
+
140
+ if (this.mode === "byId") {
141
+ // Update single record by ID: PATCH /{database}/{table}('id')
142
+ url = `/${this.databaseName}/${this.tableName}('${this.recordId}')`;
143
+ } else {
144
+ // Update by filter: PATCH /{database}/{table}?$filter=...
145
+ if (!this.queryBuilder) {
146
+ throw new Error("Query builder is required for filter-based update");
147
+ }
148
+
149
+ // Get the query string from the configured QueryBuilder
150
+ const queryString = this.queryBuilder.getQueryString();
151
+ // Remove the leading "/" from the query string as we'll build our own URL
152
+ const queryParams = queryString.startsWith(`/${this.tableName}`)
153
+ ? queryString.slice(`/${this.tableName}`.length)
154
+ : queryString;
155
+
156
+ url = `/${this.databaseName}/${this.tableName}${queryParams}`;
157
+ }
158
+
159
+ // Make PATCH request with JSON body
160
+ const response = await this.context._makeRequest(url, {
161
+ method: "PATCH",
162
+ headers: {
163
+ "Content-Type": "application/json",
164
+ },
165
+ body: JSON.stringify(this.data),
166
+ ...options,
167
+ });
168
+
169
+ // Both byId and byFilter return affected row count
170
+ let updatedCount = 0;
171
+
172
+ if (typeof response === "number") {
173
+ updatedCount = response;
174
+ } else if (response && typeof response === "object") {
175
+ // Check if the response has a count property (fallback)
176
+ updatedCount = (response as any).updatedCount || 0;
177
+ }
178
+
179
+ return { data: { updatedCount }, error: undefined };
180
+ } catch (error) {
181
+ return {
182
+ data: undefined,
183
+ error: error instanceof Error ? error : new Error(String(error)),
184
+ };
185
+ }
186
+ }
187
+
188
+ getRequestConfig(): { method: string; url: string; body?: any } {
189
+ let url: string;
190
+
191
+ if (this.mode === "byId") {
192
+ url = `/${this.databaseName}/${this.tableName}('${this.recordId}')`;
193
+ } else {
194
+ if (!this.queryBuilder) {
195
+ throw new Error("Query builder is required for filter-based update");
196
+ }
197
+
198
+ const queryString = this.queryBuilder.getQueryString();
199
+ const queryParams = queryString.startsWith(`/${this.tableName}`)
200
+ ? queryString.slice(`/${this.tableName}`.length)
201
+ : queryString;
202
+
203
+ url = `/${this.databaseName}/${this.tableName}${queryParams}`;
204
+ }
205
+
206
+ return {
207
+ method: "PATCH",
208
+ url,
209
+ body: JSON.stringify(this.data),
210
+ };
211
+ }
212
+ }
@@ -0,0 +1,97 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+
3
+ // Operator types for each value type
4
+ export type StringOperators =
5
+ | { eq: string | null }
6
+ | { ne: string | null }
7
+ | { gt: string }
8
+ | { ge: string }
9
+ | { lt: string }
10
+ | { le: string }
11
+ | { contains: string }
12
+ | { startswith: string }
13
+ | { endswith: string }
14
+ | { in: string[] };
15
+
16
+ export type NumberOperators =
17
+ | { eq: number | null }
18
+ | { ne: number | null }
19
+ | { gt: number }
20
+ | { ge: number }
21
+ | { lt: number }
22
+ | { le: number }
23
+ | { in: number[] };
24
+
25
+ export type BooleanOperators =
26
+ | { eq: boolean | null }
27
+ | { ne: boolean | null };
28
+
29
+ export type DateOperators =
30
+ | { eq: Date | null }
31
+ | { ne: Date | null }
32
+ | { gt: Date }
33
+ | { ge: Date }
34
+ | { lt: Date }
35
+ | { le: Date }
36
+ | { in: Date[] };
37
+
38
+ // Infer output type from StandardSchemaV1
39
+ export type InferOutput<S> = S extends StandardSchemaV1<any, infer Output>
40
+ ? Output
41
+ : never;
42
+
43
+ // Map inferred types to their operators
44
+ export type OperatorsForType<T> =
45
+ T extends string | null | undefined ? StringOperators :
46
+ T extends number | null | undefined ? NumberOperators :
47
+ T extends boolean | null | undefined ? BooleanOperators :
48
+ T extends Date | null | undefined ? DateOperators :
49
+ never;
50
+
51
+ // Get operators for a schema field
52
+ export type OperatorsForSchemaField<S extends StandardSchemaV1> =
53
+ OperatorsForType<InferOutput<S>>;
54
+
55
+ // Field filter: shorthand, single operator, or operator array
56
+ export type FieldFilter<S extends StandardSchemaV1> =
57
+ | InferOutput<S> // Shorthand: { name: "John" }
58
+ | OperatorsForSchemaField<S> // Single operator: { age: { gt: 18 } }
59
+ | Array<OperatorsForSchemaField<S>>; // Multiple operators: { age: [{ gt: 18 }, { lt: 65 }] }
60
+
61
+ // Logical operators (recursive)
62
+ export type LogicalFilter<Schema extends Record<string, StandardSchemaV1>> = {
63
+ and?: Array<TypedFilter<Schema>>;
64
+ or?: Array<TypedFilter<Schema>>;
65
+ not?: TypedFilter<Schema>;
66
+ };
67
+
68
+ // Helper to check if Schema is exactly Record<string, StandardSchemaV1> (untyped)
69
+ // Uses double extends check to ensure Schema is exactly the generic type, not a more specific type
70
+ type IsUntypedSchema<Schema> =
71
+ [Record<string, StandardSchemaV1>] extends [Schema]
72
+ ? [Schema] extends [Record<string, StandardSchemaV1>]
73
+ ? true
74
+ : false
75
+ : false;
76
+
77
+ // Main filter type
78
+ export type TypedFilter<Schema extends Record<string, StandardSchemaV1>> =
79
+ | LogicalFilter<Schema>
80
+ | (
81
+ IsUntypedSchema<Schema> extends true
82
+ ? {
83
+ // For untyped schemas, allow arbitrary string keys with empty object intersection (preserves autocomplete)
84
+ [key: string]: FieldFilter<any> | any;
85
+ } & {}
86
+ : {
87
+ // For typed schemas, use specific keys (preserves autocomplete)
88
+ [K in keyof Schema]?: FieldFilter<Schema[K]>;
89
+ }
90
+ );
91
+
92
+ // Top-level filter (can be array for implicit AND)
93
+ export type Filter<Schema extends Record<string, StandardSchemaV1>> =
94
+ | TypedFilter<Schema>
95
+ | Array<TypedFilter<Schema>>
96
+ | string; // Escape hatch for raw OData expressions
97
+
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ // Barrel file - exports all public API from the client folder
2
+ export { BaseTable } from "./client/base-table";
3
+ export {
4
+ TableOccurrence,
5
+ createTableOccurrence,
6
+ } from "./client/table-occurrence";
7
+ export { FileMakerOData } from "./client/filemaker-odata";
8
+ export type {
9
+ Filter,
10
+ TypedFilter,
11
+ FieldFilter,
12
+ StringOperators,
13
+ NumberOperators,
14
+ BooleanOperators,
15
+ DateOperators,
16
+ LogicalFilter,
17
+ } from "./filter-types";