@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,177 @@
1
+ import { z } from "zod/v4";
2
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
3
+ import type { ExecutionContext } from "../types";
4
+ import type { BaseTable } from "./base-table";
5
+ import type { TableOccurrence } from "./table-occurrence";
6
+ import { EntitySet } from "./entity-set";
7
+
8
+ // Helper type to extract schema from a TableOccurrence
9
+ type ExtractSchemaFromOccurrence<O> =
10
+ O extends TableOccurrence<infer BT, any, any, any>
11
+ ? BT extends BaseTable<infer S, any>
12
+ ? S
13
+ : never
14
+ : never;
15
+
16
+ // Helper type to find an occurrence by name in the occurrences tuple
17
+ type FindOccurrenceByName<
18
+ Occurrences extends readonly TableOccurrence<any, any, any, any>[],
19
+ Name extends string,
20
+ > = Occurrences extends readonly [
21
+ infer First,
22
+ ...infer Rest extends readonly TableOccurrence<any, any, any, any>[],
23
+ ]
24
+ ? First extends TableOccurrence<any, any, any, any>
25
+ ? First["name"] extends Name
26
+ ? First
27
+ : FindOccurrenceByName<Rest, Name>
28
+ : never
29
+ : never;
30
+
31
+ // Helper type to extract all occurrence names from the tuple
32
+ type ExtractOccurrenceNames<
33
+ Occurrences extends readonly TableOccurrence<any, any, any, any>[],
34
+ > = Occurrences extends readonly []
35
+ ? string // If no occurrences, allow any string
36
+ : Occurrences[number]["name"]; // Otherwise, extract union of names
37
+
38
+ export class Database<
39
+ Occurrences extends readonly TableOccurrence<
40
+ any,
41
+ any,
42
+ any,
43
+ any
44
+ >[] = readonly [],
45
+ > {
46
+ private occurrenceMap: Map<string, TableOccurrence<any, any, any, any>>;
47
+
48
+ constructor(
49
+ private readonly databaseName: string,
50
+ private readonly context: ExecutionContext,
51
+ config?: { occurrences?: Occurrences },
52
+ ) {
53
+ this.occurrenceMap = new Map();
54
+ if (config?.occurrences) {
55
+ for (const occ of config.occurrences) {
56
+ this.occurrenceMap.set(occ.name, occ);
57
+ }
58
+ }
59
+ }
60
+
61
+ from<Name extends ExtractOccurrenceNames<Occurrences> | (string & {})>(
62
+ name: Name,
63
+ ): Occurrences extends readonly []
64
+ ? EntitySet<Record<string, z.ZodTypeAny>, undefined>
65
+ : Name extends ExtractOccurrenceNames<Occurrences>
66
+ ? EntitySet<
67
+ ExtractSchemaFromOccurrence<FindOccurrenceByName<Occurrences, Name>>,
68
+ FindOccurrenceByName<Occurrences, Name>
69
+ >
70
+ : EntitySet<Record<string, z.ZodTypeAny>, undefined> {
71
+ const occurrence = this.occurrenceMap.get(name as string);
72
+
73
+ if (occurrence) {
74
+ // Use EntitySet.create to preserve types better
75
+ type OccType = FindOccurrenceByName<Occurrences, Name>;
76
+ type SchemaType = ExtractSchemaFromOccurrence<OccType>;
77
+
78
+ return EntitySet.create<SchemaType, OccType>({
79
+ occurrence: occurrence as any,
80
+ tableName: name as string,
81
+ databaseName: this.databaseName,
82
+ context: this.context,
83
+ }) as any;
84
+ } else {
85
+ // Return untyped EntitySet for dynamic table access
86
+ return new EntitySet<Record<string, z.ZodTypeAny>, undefined>({
87
+ tableName: name as string,
88
+ databaseName: this.databaseName,
89
+ context: this.context,
90
+ }) as any;
91
+ }
92
+ }
93
+
94
+ // Example method showing how to use the request method
95
+ async getMetadata() {
96
+ return this.context._makeRequest(`/${this.databaseName}/$metadata`);
97
+ }
98
+
99
+ /**
100
+ * Lists all available tables (entity sets) in this database.
101
+ * @returns Promise resolving to an array of table names
102
+ */
103
+ async listTableNames(): Promise<string[]> {
104
+ const response = (await this.context._makeRequest(
105
+ `/${this.databaseName}`,
106
+ )) as {
107
+ value?: Array<{ name: string }>;
108
+ };
109
+ if (response.value && Array.isArray(response.value)) {
110
+ return response.value.map((item) => item.name);
111
+ }
112
+ return [];
113
+ }
114
+
115
+ /**
116
+ * Executes a FileMaker script.
117
+ * @param scriptName - The name of the script to execute (must be valid according to OData rules)
118
+ * @param options - Optional script parameter and result schema
119
+ * @returns Promise resolving to script execution result
120
+ */
121
+ async runScript<ResultSchema extends StandardSchemaV1<string, any> = never>(
122
+ scriptName: string,
123
+ options?: {
124
+ scriptParam?: string | number | Record<string, any>;
125
+ resultSchema?: ResultSchema;
126
+ },
127
+ ): Promise<
128
+ [ResultSchema] extends [never]
129
+ ? { resultCode: number; result?: string }
130
+ : ResultSchema extends StandardSchemaV1<string, infer Output>
131
+ ? { resultCode: number; result: Output }
132
+ : { resultCode: number; result?: string }
133
+ > {
134
+ const body: { scriptParameterValue?: unknown } = {};
135
+ if (options?.scriptParam !== undefined) {
136
+ body.scriptParameterValue = options.scriptParam;
137
+ }
138
+
139
+ const response = await this.context._makeRequest<{
140
+ scriptResult: {
141
+ code: number;
142
+ resultParameter?: string;
143
+ };
144
+ }>(`/${this.databaseName}/Script.${scriptName}`, {
145
+ method: "POST",
146
+ body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,
147
+ });
148
+
149
+ // If resultSchema is provided, validate the result through it
150
+ if (options?.resultSchema && response.scriptResult !== undefined) {
151
+ const validationResult = options.resultSchema["~standard"].validate(
152
+ response.scriptResult.resultParameter,
153
+ );
154
+ // Handle both sync and async validation
155
+ const result =
156
+ validationResult instanceof Promise
157
+ ? await validationResult
158
+ : validationResult;
159
+
160
+ if (result.issues) {
161
+ throw new Error(
162
+ `Script result validation failed: ${JSON.stringify(result.issues)}`,
163
+ );
164
+ }
165
+
166
+ return {
167
+ resultCode: response.scriptResult.code,
168
+ result: result.value,
169
+ } as any;
170
+ }
171
+
172
+ return {
173
+ resultCode: response.scriptResult.code,
174
+ result: response.scriptResult.resultParameter,
175
+ } as any;
176
+ }
177
+ }
@@ -0,0 +1,193 @@
1
+ import type {
2
+ ExecutionContext,
3
+ ExecutableBuilder,
4
+ Result,
5
+ WithSystemFields,
6
+ } from "../types";
7
+ import type { TableOccurrence } from "./table-occurrence";
8
+ import { QueryBuilder } from "./query-builder";
9
+ import { type FFetchOptions } from "@fetchkit/ffetch";
10
+ import buildQuery from "odata-query";
11
+
12
+ /**
13
+ * Initial delete builder returned from EntitySet.delete()
14
+ * Requires calling .byId() or .where() before .execute() is available
15
+ */
16
+ export class DeleteBuilder<T extends Record<string, any>> {
17
+ private tableName: string;
18
+ private databaseName: string;
19
+ private context: ExecutionContext;
20
+ private occurrence?: TableOccurrence<any, any, any, any>;
21
+
22
+ constructor(config: {
23
+ occurrence?: TableOccurrence<any, any, any, any>;
24
+ tableName: string;
25
+ databaseName: string;
26
+ context: ExecutionContext;
27
+ }) {
28
+ this.occurrence = config.occurrence;
29
+ this.tableName = config.tableName;
30
+ this.databaseName = config.databaseName;
31
+ this.context = config.context;
32
+ }
33
+
34
+ /**
35
+ * Delete a single record by ID
36
+ */
37
+ byId(id: string | number): ExecutableDeleteBuilder<T> {
38
+ return new ExecutableDeleteBuilder<T>({
39
+ occurrence: this.occurrence,
40
+ tableName: this.tableName,
41
+ databaseName: this.databaseName,
42
+ context: this.context,
43
+ mode: "byId",
44
+ recordId: id,
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Delete records matching a filter query
50
+ * @param fn Callback that receives a QueryBuilder for building the filter
51
+ */
52
+ where(
53
+ fn: (
54
+ q: QueryBuilder<WithSystemFields<T>>,
55
+ ) => QueryBuilder<WithSystemFields<T>>,
56
+ ): ExecutableDeleteBuilder<T> {
57
+ // Create a QueryBuilder for the user to configure
58
+ const queryBuilder = new QueryBuilder<
59
+ WithSystemFields<T>,
60
+ keyof WithSystemFields<T>,
61
+ false,
62
+ false,
63
+ undefined
64
+ >({
65
+ occurrence: undefined,
66
+ tableName: this.tableName,
67
+ databaseName: this.databaseName,
68
+ context: this.context,
69
+ });
70
+
71
+ // Let the user configure it
72
+ const configuredBuilder = fn(queryBuilder);
73
+
74
+ return new ExecutableDeleteBuilder<T>({
75
+ occurrence: this.occurrence,
76
+ tableName: this.tableName,
77
+ databaseName: this.databaseName,
78
+ context: this.context,
79
+ mode: "byFilter",
80
+ queryBuilder: configuredBuilder,
81
+ });
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Executable delete builder - has execute() method
87
+ * Returned after calling .byId() or .where()
88
+ */
89
+ export class ExecutableDeleteBuilder<T extends Record<string, any>>
90
+ implements ExecutableBuilder<{ deletedCount: number }>
91
+ {
92
+ private tableName: string;
93
+ private databaseName: string;
94
+ private context: ExecutionContext;
95
+ private occurrence?: TableOccurrence<any, any, any, any>;
96
+ private mode: "byId" | "byFilter";
97
+ private recordId?: string | number;
98
+ private queryBuilder?: QueryBuilder<any>;
99
+
100
+ constructor(config: {
101
+ occurrence?: TableOccurrence<any, any, any, any>;
102
+ tableName: string;
103
+ databaseName: string;
104
+ context: ExecutionContext;
105
+ mode: "byId" | "byFilter";
106
+ recordId?: string | number;
107
+ queryBuilder?: QueryBuilder<any>;
108
+ }) {
109
+ this.occurrence = config.occurrence;
110
+ this.tableName = config.tableName;
111
+ this.databaseName = config.databaseName;
112
+ this.context = config.context;
113
+ this.mode = config.mode;
114
+ this.recordId = config.recordId;
115
+ this.queryBuilder = config.queryBuilder;
116
+ }
117
+
118
+ async execute(
119
+ options?: RequestInit & FFetchOptions,
120
+ ): Promise<Result<{ deletedCount: number }>> {
121
+ try {
122
+ let url: string;
123
+
124
+ if (this.mode === "byId") {
125
+ // Delete single record by ID: DELETE /{database}/{table}('id')
126
+ url = `/${this.databaseName}/${this.tableName}('${this.recordId}')`;
127
+ } else {
128
+ // Delete by filter: DELETE /{database}/{table}?$filter=...
129
+ if (!this.queryBuilder) {
130
+ throw new Error("Query builder is required for filter-based delete");
131
+ }
132
+
133
+ // Get the query string from the configured QueryBuilder
134
+ const queryString = this.queryBuilder.getQueryString();
135
+ // Remove the leading "/" from the query string as we'll build our own URL
136
+ const queryParams = queryString.startsWith(`/${this.tableName}`)
137
+ ? queryString.slice(`/${this.tableName}`.length)
138
+ : queryString;
139
+
140
+ url = `/${this.databaseName}/${this.tableName}${queryParams}`;
141
+ }
142
+
143
+ // Make DELETE request
144
+ const response = await this.context._makeRequest(url, {
145
+ method: "DELETE",
146
+ ...options,
147
+ });
148
+
149
+ // OData returns 204 No Content with fmodata.affected_rows header
150
+ // The _makeRequest should handle extracting the header value
151
+ // For now, we'll check if response contains the count
152
+ let deletedCount = 0;
153
+
154
+ if (typeof response === "number") {
155
+ deletedCount = response;
156
+ } else if (response && typeof response === "object") {
157
+ // Check if the response has a count property (fallback)
158
+ deletedCount = (response as any).deletedCount || 0;
159
+ }
160
+
161
+ return { data: { deletedCount }, error: undefined };
162
+ } catch (error) {
163
+ return {
164
+ data: undefined,
165
+ error: error instanceof Error ? error : new Error(String(error)),
166
+ };
167
+ }
168
+ }
169
+
170
+ getRequestConfig(): { method: string; url: string; body?: any } {
171
+ let url: string;
172
+
173
+ if (this.mode === "byId") {
174
+ url = `/${this.databaseName}/${this.tableName}('${this.recordId}')`;
175
+ } else {
176
+ if (!this.queryBuilder) {
177
+ throw new Error("Query builder is required for filter-based delete");
178
+ }
179
+
180
+ const queryString = this.queryBuilder.getQueryString();
181
+ const queryParams = queryString.startsWith(`/${this.tableName}`)
182
+ ? queryString.slice(`/${this.tableName}`.length)
183
+ : queryString;
184
+
185
+ url = `/${this.databaseName}/${this.tableName}${queryParams}`;
186
+ }
187
+
188
+ return {
189
+ method: "DELETE",
190
+ url,
191
+ };
192
+ }
193
+ }
@@ -0,0 +1,310 @@
1
+ import { z } from "zod/v4";
2
+ import type {
3
+ ExecutionContext,
4
+ InferSchemaType,
5
+ WithSystemFields,
6
+ InsertData,
7
+ UpdateData,
8
+ } from "../types";
9
+ import type { BaseTable } from "./base-table";
10
+ import type { TableOccurrence } from "./table-occurrence";
11
+ import { QueryBuilder } from "./query-builder";
12
+ import { RecordBuilder } from "./record-builder";
13
+ import { InsertBuilder } from "./insert-builder";
14
+ import { DeleteBuilder } from "./delete-builder";
15
+ import { UpdateBuilder } from "./update-builder";
16
+
17
+ // Helper type to extract navigation relation names from an occurrence
18
+ type ExtractNavigationNames<
19
+ O extends TableOccurrence<any, any, any, any> | undefined,
20
+ > =
21
+ O extends TableOccurrence<any, any, infer Nav, any>
22
+ ? Nav extends Record<string, any>
23
+ ? keyof Nav & string
24
+ : never
25
+ : never;
26
+
27
+ // Helper type to extract schema from a TableOccurrence
28
+ type ExtractSchemaFromOccurrence<O> =
29
+ O extends TableOccurrence<infer BT, any, any, any>
30
+ ? BT extends BaseTable<infer S, any>
31
+ ? S
32
+ : never
33
+ : never;
34
+
35
+ // Helper type to extract defaultSelect from a TableOccurrence
36
+ type ExtractDefaultSelect<O> =
37
+ O extends TableOccurrence<infer BT, any, any, infer DefSelect>
38
+ ? BT extends BaseTable<infer S, any>
39
+ ? DefSelect extends "all"
40
+ ? keyof S
41
+ : DefSelect extends "schema"
42
+ ? keyof S
43
+ : DefSelect extends readonly (infer K)[]
44
+ ? K & keyof S
45
+ : keyof S
46
+ : never
47
+ : never;
48
+
49
+ // Helper type to resolve a navigation item (handles both direct and lazy-loaded)
50
+ type ResolveNavigationItem<T> = T extends () => infer R ? R : T;
51
+
52
+ // Helper type to find target occurrence by relation name
53
+ type FindNavigationTarget<
54
+ O extends TableOccurrence<any, any, any, any> | undefined,
55
+ Name extends string,
56
+ > =
57
+ O extends TableOccurrence<any, any, infer Nav, any>
58
+ ? Nav extends Record<string, any>
59
+ ? Name extends keyof Nav
60
+ ? ResolveNavigationItem<Nav[Name]>
61
+ : TableOccurrence<
62
+ BaseTable<Record<string, z.ZodTypeAny>, any>,
63
+ any,
64
+ any,
65
+ any
66
+ >
67
+ : TableOccurrence<
68
+ BaseTable<Record<string, z.ZodTypeAny>, any>,
69
+ any,
70
+ any,
71
+ any
72
+ >
73
+ : TableOccurrence<
74
+ BaseTable<Record<string, z.ZodTypeAny>, any>,
75
+ any,
76
+ any,
77
+ any
78
+ >;
79
+
80
+ // Helper type to get the inferred schema type from a target occurrence
81
+ type GetTargetSchemaType<
82
+ O extends TableOccurrence<any, any, any, any> | undefined,
83
+ Rel extends string,
84
+ > = [FindNavigationTarget<O, Rel>] extends [
85
+ TableOccurrence<infer BT, any, any, any>,
86
+ ]
87
+ ? [BT] extends [BaseTable<infer S, any>]
88
+ ? [S] extends [Record<string, z.ZodType>]
89
+ ? InferSchemaType<S>
90
+ : Record<string, any>
91
+ : Record<string, any>
92
+ : Record<string, any>;
93
+
94
+ export class EntitySet<
95
+ Schema extends Record<string, z.ZodType> = any,
96
+ Occ extends TableOccurrence<any, any, any, any> | undefined = undefined,
97
+ > {
98
+ private occurrence?: Occ;
99
+ private tableName: string;
100
+ private databaseName: string;
101
+ private context: ExecutionContext;
102
+ private isNavigateFromEntitySet?: boolean;
103
+ private navigateRelation?: string;
104
+ private navigateSourceTableName?: string;
105
+
106
+ constructor(config: {
107
+ occurrence?: Occ;
108
+ tableName: string;
109
+ databaseName: string;
110
+ context: ExecutionContext;
111
+ }) {
112
+ this.occurrence = config.occurrence;
113
+ this.tableName = config.tableName;
114
+ this.databaseName = config.databaseName;
115
+ this.context = config.context;
116
+ }
117
+
118
+ // Type-only method to help TypeScript infer the schema from occurrence
119
+ static create<
120
+ OccurrenceSchema extends Record<string, z.ZodType>,
121
+ Occ extends
122
+ | TableOccurrence<BaseTable<OccurrenceSchema, any>, any, any, any>
123
+ | undefined = undefined,
124
+ >(config: {
125
+ occurrence?: Occ;
126
+ tableName: string;
127
+ databaseName: string;
128
+ context: ExecutionContext;
129
+ }): EntitySet<OccurrenceSchema, Occ> {
130
+ return new EntitySet<OccurrenceSchema, Occ>({
131
+ occurrence: config.occurrence,
132
+ tableName: config.tableName,
133
+ databaseName: config.databaseName,
134
+ context: config.context,
135
+ });
136
+ }
137
+
138
+ list(): QueryBuilder<
139
+ InferSchemaType<Schema>,
140
+ Occ extends TableOccurrence<any, any, any, any>
141
+ ? ExtractDefaultSelect<Occ>
142
+ : keyof InferSchemaType<Schema>,
143
+ false,
144
+ false,
145
+ Occ
146
+ > {
147
+ const builder = new QueryBuilder<
148
+ InferSchemaType<Schema>,
149
+ Occ extends TableOccurrence<any, any, any, any>
150
+ ? ExtractDefaultSelect<Occ>
151
+ : keyof InferSchemaType<Schema>,
152
+ false,
153
+ false,
154
+ Occ
155
+ >({
156
+ occurrence: this.occurrence as Occ,
157
+ tableName: this.tableName,
158
+ databaseName: this.databaseName,
159
+ context: this.context,
160
+ });
161
+
162
+ // Apply defaultSelect if occurrence exists and select hasn't been called
163
+ if (this.occurrence) {
164
+ const defaultSelect = this.occurrence.defaultSelect;
165
+
166
+ if (defaultSelect === "schema") {
167
+ // Extract field names from schema
168
+ const schema = this.occurrence.baseTable.schema;
169
+ const fields = Object.keys(schema) as (keyof InferSchemaType<Schema>)[];
170
+ // Deduplicate fields (same as select method)
171
+ const uniqueFields = [...new Set(fields)];
172
+ return builder.select(...uniqueFields);
173
+ } else if (Array.isArray(defaultSelect)) {
174
+ // Use the provided field names, deduplicated
175
+ const uniqueFields = [
176
+ ...new Set(defaultSelect),
177
+ ] as (keyof InferSchemaType<Schema>)[];
178
+ return builder.select(...uniqueFields);
179
+ }
180
+ // If defaultSelect is "all", no changes needed (current behavior)
181
+ }
182
+
183
+ // Propagate navigation context if present
184
+ if (this.isNavigateFromEntitySet) {
185
+ (builder as any).isNavigate = true;
186
+ (builder as any).navigateRelation = this.navigateRelation;
187
+ (builder as any).navigateSourceTableName = this.navigateSourceTableName;
188
+ // navigateRecordId is intentionally not set (undefined) to indicate navigation from EntitySet
189
+ }
190
+ return builder;
191
+ }
192
+
193
+ get(
194
+ id: string | number,
195
+ ): RecordBuilder<
196
+ InferSchemaType<Schema>,
197
+ false,
198
+ keyof InferSchemaType<Schema>,
199
+ Occ
200
+ > {
201
+ const builder = new RecordBuilder<
202
+ InferSchemaType<Schema>,
203
+ false,
204
+ keyof InferSchemaType<Schema>,
205
+ Occ
206
+ >({
207
+ occurrence: this.occurrence,
208
+ tableName: this.tableName,
209
+ databaseName: this.databaseName,
210
+ context: this.context,
211
+ recordId: id,
212
+ });
213
+ // Propagate navigation context if present
214
+ if (this.isNavigateFromEntitySet) {
215
+ (builder as any).isNavigateFromEntitySet = true;
216
+ (builder as any).navigateRelation = this.navigateRelation;
217
+ (builder as any).navigateSourceTableName = this.navigateSourceTableName;
218
+ }
219
+ return builder;
220
+ }
221
+
222
+ insert(
223
+ data: Occ extends TableOccurrence<infer BT, any, any, any>
224
+ ? BT extends BaseTable<any, any, any, any>
225
+ ? InsertData<BT>
226
+ : Partial<InferSchemaType<Schema>>
227
+ : Partial<InferSchemaType<Schema>>,
228
+ ): InsertBuilder<InferSchemaType<Schema>, Occ> {
229
+ return new InsertBuilder<InferSchemaType<Schema>, Occ>({
230
+ occurrence: this.occurrence,
231
+ tableName: this.tableName,
232
+ databaseName: this.databaseName,
233
+ context: this.context,
234
+ data: data as Partial<InferSchemaType<Schema>>,
235
+ });
236
+ }
237
+
238
+ update(
239
+ data: Occ extends TableOccurrence<infer BT, any, any, any>
240
+ ? BT extends BaseTable<any, any, any, any>
241
+ ? UpdateData<BT>
242
+ : Partial<InferSchemaType<Schema>>
243
+ : Partial<InferSchemaType<Schema>>,
244
+ ): UpdateBuilder<
245
+ InferSchemaType<Schema>,
246
+ Occ extends TableOccurrence<infer BT, any, any, any>
247
+ ? BT extends BaseTable<any, any, any, any>
248
+ ? BT
249
+ : BaseTable<Schema, any, any, any>
250
+ : BaseTable<Schema, any, any, any>
251
+ > {
252
+ return new UpdateBuilder<
253
+ InferSchemaType<Schema>,
254
+ Occ extends TableOccurrence<infer BT, any, any, any>
255
+ ? BT extends BaseTable<any, any, any, any>
256
+ ? BT
257
+ : BaseTable<Schema, any, any, any>
258
+ : BaseTable<Schema, any, any, any>
259
+ >({
260
+ occurrence: this.occurrence,
261
+ tableName: this.tableName,
262
+ databaseName: this.databaseName,
263
+ context: this.context,
264
+ data: data as Partial<InferSchemaType<Schema>>,
265
+ });
266
+ }
267
+
268
+ delete(): DeleteBuilder<InferSchemaType<Schema>> {
269
+ return new DeleteBuilder<InferSchemaType<Schema>>({
270
+ occurrence: this.occurrence,
271
+ tableName: this.tableName,
272
+ databaseName: this.databaseName,
273
+ context: this.context,
274
+ });
275
+ }
276
+
277
+ // Overload for valid relation names - returns typed EntitySet
278
+ navigate<RelationName extends ExtractNavigationNames<Occ>>(
279
+ relationName: RelationName,
280
+ ): EntitySet<
281
+ ExtractSchemaFromOccurrence<
282
+ FindNavigationTarget<Occ, RelationName>
283
+ > extends Record<string, z.ZodType>
284
+ ? ExtractSchemaFromOccurrence<FindNavigationTarget<Occ, RelationName>>
285
+ : Record<string, z.ZodTypeAny>,
286
+ FindNavigationTarget<Occ, RelationName>
287
+ >;
288
+ // Overload for arbitrary strings - returns generic EntitySet
289
+ navigate(
290
+ relationName: string,
291
+ ): EntitySet<Record<string, z.ZodTypeAny>, undefined>;
292
+ // Implementation
293
+ navigate(relationName: string): EntitySet<any, any> {
294
+ // Use the target occurrence if available, otherwise allow untyped navigation
295
+ // (useful when types might be incomplete)
296
+ const targetOccurrence = this.occurrence?.navigation[relationName];
297
+ const entitySet = new EntitySet<any, any>({
298
+ occurrence: targetOccurrence,
299
+ tableName: targetOccurrence?.name ?? relationName,
300
+ databaseName: this.databaseName,
301
+ context: this.context,
302
+ });
303
+ // Store the navigation info in the EntitySet
304
+ // We'll need to pass this through when creating QueryBuilders
305
+ (entitySet as any).isNavigateFromEntitySet = true;
306
+ (entitySet as any).navigateRelation = relationName;
307
+ (entitySet as any).navigateSourceTableName = this.tableName;
308
+ return entitySet;
309
+ }
310
+ }