@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.
- package/README.md +37 -0
- package/dist/esm/client/base-table.d.ts +13 -0
- package/dist/esm/client/base-table.js +19 -0
- package/dist/esm/client/base-table.js.map +1 -0
- package/dist/esm/client/database.d.ts +49 -0
- package/dist/esm/client/database.js +90 -0
- package/dist/esm/client/database.js.map +1 -0
- package/dist/esm/client/delete-builder.d.ts +61 -0
- package/dist/esm/client/delete-builder.js +121 -0
- package/dist/esm/client/delete-builder.js.map +1 -0
- package/dist/esm/client/entity-set.d.ts +43 -0
- package/dist/esm/client/entity-set.js +120 -0
- package/dist/esm/client/entity-set.js.map +1 -0
- package/dist/esm/client/filemaker-odata.d.ts +26 -0
- package/dist/esm/client/filemaker-odata.js +85 -0
- package/dist/esm/client/filemaker-odata.js.map +1 -0
- package/dist/esm/client/insert-builder.d.ts +23 -0
- package/dist/esm/client/insert-builder.js +69 -0
- package/dist/esm/client/insert-builder.js.map +1 -0
- package/dist/esm/client/query-builder.d.ts +94 -0
- package/dist/esm/client/query-builder.js +649 -0
- package/dist/esm/client/query-builder.js.map +1 -0
- package/dist/esm/client/record-builder.d.ts +43 -0
- package/dist/esm/client/record-builder.js +121 -0
- package/dist/esm/client/record-builder.js.map +1 -0
- package/dist/esm/client/table-occurrence.d.ts +25 -0
- package/dist/esm/client/table-occurrence.js +47 -0
- package/dist/esm/client/table-occurrence.js.map +1 -0
- package/dist/esm/client/update-builder.d.ts +69 -0
- package/dist/esm/client/update-builder.js +134 -0
- package/dist/esm/client/update-builder.js.map +1 -0
- package/dist/esm/filter-types.d.ts +76 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/types.d.ts +67 -0
- package/dist/esm/validation.d.ts +41 -0
- package/dist/esm/validation.js +270 -0
- package/dist/esm/validation.js.map +1 -0
- package/package.json +68 -0
- package/src/client/base-table.ts +25 -0
- package/src/client/database.ts +177 -0
- package/src/client/delete-builder.ts +193 -0
- package/src/client/entity-set.ts +310 -0
- package/src/client/filemaker-odata.ts +119 -0
- package/src/client/insert-builder.ts +93 -0
- package/src/client/query-builder.ts +1076 -0
- package/src/client/record-builder.ts +240 -0
- package/src/client/table-occurrence.ts +100 -0
- package/src/client/update-builder.ts +212 -0
- package/src/filter-types.ts +97 -0
- package/src/index.ts +17 -0
- package/src/types.ts +123 -0
- 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
|
+
}
|