@an5/orm 1.3.2 → 1.3.3
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 +74 -11
- package/dist/cleanup.d.ts +1 -0
- package/dist/cleanup.js +177 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +147 -0
- package/dist/generator/src/attributes.d.ts +57 -0
- package/dist/generator/src/attributes.js +225 -0
- package/dist/generator/src/code-generator.js +49 -16
- package/dist/generator/src/config.d.ts +2 -2
- package/dist/generator/src/config.js +4 -3
- package/dist/generator/src/dialect.d.ts +52 -1
- package/dist/generator/src/dialect.js +136 -3
- package/dist/generator/src/dotnet-generator.js +67 -23
- package/dist/generator/src/golang-generator.d.ts +8 -0
- package/dist/generator/src/golang-generator.js +86 -21
- package/dist/generator/src/java-generator.d.ts +8 -0
- package/dist/generator/src/java-generator.js +108 -3
- package/dist/generator/src/kotlin-generator.js +51 -0
- package/dist/generator/src/parser.d.ts +1 -0
- package/dist/generator/src/parser.js +90 -35
- package/dist/generator/src/pull-schema.d.ts +54 -0
- package/dist/generator/src/pull-schema.js +116 -0
- package/dist/generator/src/push-apply.d.ts +4 -2
- package/dist/generator/src/push-apply.js +53 -4
- package/dist/generator/src/push-schema.d.ts +21 -9
- package/dist/generator/src/push-schema.js +132 -32
- package/dist/generator/src/python-generator.js +35 -22
- package/dist/generator/src/rust-generator.js +79 -74
- package/dist/generator/src/swift-generator.js +54 -3
- package/dist/generator/src/types.d.ts +10 -0
- package/dist/generator/src/view-order.d.ts +33 -0
- package/dist/generator/src/view-order.js +123 -0
- package/dist/migrate.d.ts +12 -0
- package/dist/migrate.js +442 -0
- package/dist/migration-core.d.ts +32 -3
- package/dist/migration-core.js +213 -43
- package/dist/provider-support.d.ts +0 -1
- package/dist/provider-support.js +0 -1
- package/dist/pull.d.ts +1 -0
- package/dist/pull.js +425 -0
- package/dist/push.d.ts +1 -0
- package/dist/push.js +93 -0
- package/dist/seed.d.ts +1 -0
- package/dist/seed.js +33 -0
- package/generator/src/attributes.ts +231 -0
- package/generator/src/code-generator.ts +47 -16
- package/generator/src/config.ts +5 -3
- package/generator/src/dialect.ts +188 -5
- package/generator/src/dotnet-generator.ts +68 -24
- package/generator/src/golang-generator.ts +89 -22
- package/generator/src/java-generator.ts +109 -3
- package/generator/src/kotlin-generator.ts +51 -0
- package/generator/src/parser.ts +102 -30
- package/generator/src/pull-schema.ts +127 -0
- package/generator/src/push-apply.ts +62 -5
- package/generator/src/push-schema.ts +160 -32
- package/generator/src/python-generator.ts +33 -20
- package/generator/src/rust-generator.ts +79 -73
- package/generator/src/swift-generator.ts +55 -4
- package/generator/src/types.ts +10 -0
- package/generator/src/view-order.ts +133 -0
- package/package.json +19 -14
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* lands in. It is kept here rather than inline in `push.ts` so the mapping from
|
|
8
8
|
* schema text to a column can be tested without a database.
|
|
9
9
|
*/
|
|
10
|
+
import {
|
|
11
|
+
attributeArgs,
|
|
12
|
+
hasAttribute,
|
|
13
|
+
parseIndexDirective,
|
|
14
|
+
splitSchemaLines,
|
|
15
|
+
stringOption,
|
|
16
|
+
unquote,
|
|
17
|
+
} from './attributes';
|
|
10
18
|
import {
|
|
11
19
|
FieldTypeIssue,
|
|
12
20
|
Provider,
|
|
@@ -22,6 +30,8 @@ export interface PushField {
|
|
|
22
30
|
isOptional: boolean;
|
|
23
31
|
isId: boolean;
|
|
24
32
|
isUnique: boolean;
|
|
33
|
+
/** `@unique(map: "…")`, so push and migrate create the same artifact name. */
|
|
34
|
+
uniqueName?: string | undefined;
|
|
25
35
|
/** The `@default(...)` expression, not SQL: only the dialect knows the spelling. */
|
|
26
36
|
defaultExpr?: string | undefined;
|
|
27
37
|
}
|
|
@@ -40,6 +50,22 @@ export interface PushModel {
|
|
|
40
50
|
indexes: PushIndex[];
|
|
41
51
|
}
|
|
42
52
|
|
|
53
|
+
/**
|
|
54
|
+
* A `view` block, as push needs it.
|
|
55
|
+
*
|
|
56
|
+
* `fields` describe the view's result columns for validation and generated
|
|
57
|
+
* clients; the DDL itself is `sql`, the `@@sql("...")` body.
|
|
58
|
+
*/
|
|
59
|
+
export interface PushView {
|
|
60
|
+
name: string;
|
|
61
|
+
/** The database view name; `@@map` may rename it independently. */
|
|
62
|
+
viewName: string;
|
|
63
|
+
schema: string;
|
|
64
|
+
declaredSchema?: string | undefined;
|
|
65
|
+
fields: PushField[];
|
|
66
|
+
sql: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
/**
|
|
44
70
|
* The schema a model lands in when the schema file does not say: none, for every
|
|
45
71
|
* provider, so the name goes wherever the connection resolves.
|
|
@@ -71,25 +97,22 @@ export interface PushIndex {
|
|
|
71
97
|
|
|
72
98
|
export interface PushSchema {
|
|
73
99
|
models: PushModel[];
|
|
100
|
+
/** `view` blocks, applied after the tables they read from. */
|
|
101
|
+
views: PushView[];
|
|
74
102
|
/** Field types the provider does not have; empty when the schema is valid. */
|
|
75
103
|
issues: FieldTypeIssue[];
|
|
76
104
|
}
|
|
77
105
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const options = line.match(DIRECTIVE)?.[2] ?? '';
|
|
83
|
-
const name = options.match(/\bmap\s*:\s*"([^"]+)"/)?.[1];
|
|
84
|
-
const include = options.match(/\binclude\s*:\s*\[([\w,\s]+)\]/)?.[1];
|
|
85
|
-
const filter = options.match(/\bfilter\s*:\s*"([^"]+)"/)?.[1];
|
|
86
|
-
const opts = options.match(/\boptions\s*:\s*"([^"]+)"/)?.[1];
|
|
106
|
+
/** Reads `@@unique(…)`/`@@index(…)` into the shape push and migrate share. */
|
|
107
|
+
function parseDirective(line: string, name: '@@unique' | '@@index'): PushIndex | null {
|
|
108
|
+
const parsed = parseIndexDirective(attributeArgs(line, name));
|
|
109
|
+
if (!parsed) return null;
|
|
87
110
|
return {
|
|
88
|
-
fields: fields
|
|
89
|
-
...(name ? { name } : {}),
|
|
90
|
-
...(
|
|
91
|
-
...(filter ? { filter } : {}),
|
|
92
|
-
...(
|
|
111
|
+
fields: parsed.fields,
|
|
112
|
+
...(parsed.name ? { name: parsed.name } : {}),
|
|
113
|
+
...(parsed.includeFields ? { includeFields: parsed.includeFields } : {}),
|
|
114
|
+
...(parsed.filter !== undefined ? { filter: parsed.filter } : {}),
|
|
115
|
+
...(parsed.options !== undefined ? { options: parsed.options } : {}),
|
|
93
116
|
};
|
|
94
117
|
}
|
|
95
118
|
|
|
@@ -122,11 +145,14 @@ export function directiveName(
|
|
|
122
145
|
*/
|
|
123
146
|
export function parsePushSchema(text: string, provider: Provider): PushSchema {
|
|
124
147
|
const models: PushModel[] = [];
|
|
148
|
+
const views: PushView[] = [];
|
|
125
149
|
const issues: FieldTypeIssue[] = [];
|
|
126
150
|
const modelRefs: Array<{ model: string; field: string; type: string }> = [];
|
|
151
|
+
const viewRefs: Array<{ view: string; field: string; type: string }> = [];
|
|
127
152
|
let current: PushModel | null = null;
|
|
153
|
+
let currentView: PushView | null = null;
|
|
128
154
|
|
|
129
|
-
for (const raw of text
|
|
155
|
+
for (const raw of splitSchemaLines(text)) {
|
|
130
156
|
const line = raw.trim();
|
|
131
157
|
if (!line || line.startsWith('//')) continue;
|
|
132
158
|
|
|
@@ -140,37 +166,92 @@ export function parsePushSchema(text: string, provider: Provider): PushSchema {
|
|
|
140
166
|
compoundUniques: [],
|
|
141
167
|
indexes: [],
|
|
142
168
|
};
|
|
169
|
+
currentView = null;
|
|
143
170
|
models.push(current);
|
|
144
171
|
continue;
|
|
145
172
|
}
|
|
146
173
|
|
|
174
|
+
const viewHeader = line.match(/^view\s+(\w+)\s*\{/);
|
|
175
|
+
if (viewHeader) {
|
|
176
|
+
currentView = {
|
|
177
|
+
name: viewHeader[1],
|
|
178
|
+
viewName: viewHeader[1].toLowerCase() + 's',
|
|
179
|
+
schema: defaultSchemaFor(provider),
|
|
180
|
+
fields: [],
|
|
181
|
+
sql: '',
|
|
182
|
+
};
|
|
183
|
+
current = null;
|
|
184
|
+
views.push(currentView);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
147
188
|
if (line === '}') {
|
|
148
189
|
current = null;
|
|
190
|
+
currentView = null;
|
|
149
191
|
continue;
|
|
150
192
|
}
|
|
193
|
+
|
|
194
|
+
if (currentView) {
|
|
195
|
+
if (line.startsWith('@@sql') || line.startsWith('@@definition')) {
|
|
196
|
+
const attribute = line.startsWith('@@sql') ? '@@sql' : '@@definition';
|
|
197
|
+
const sql = unquote(attributeArgs(line, attribute));
|
|
198
|
+
if (sql !== undefined) currentView.sql = sql.trim();
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (line.startsWith('@@map')) {
|
|
202
|
+
const mapped = unquote(attributeArgs(line, '@@map'));
|
|
203
|
+
if (mapped) currentView.viewName = mapped;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (line.startsWith('@@schema')) {
|
|
207
|
+
const declared = unquote(attributeArgs(line, '@@schema'));
|
|
208
|
+
if (declared !== undefined) {
|
|
209
|
+
currentView.schema = declared.trim();
|
|
210
|
+
currentView.declaredSchema = currentView.schema;
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (line.startsWith('@@')) continue;
|
|
215
|
+
|
|
216
|
+
const head = readFieldLineHead(line);
|
|
217
|
+
if (!head) continue;
|
|
218
|
+
if (!resolveFieldType(head.type, provider)) {
|
|
219
|
+
// A view's fields are columns of a result set, never relations.
|
|
220
|
+
viewRefs.push({ view: currentView.name, field: head.name, type: head.type });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
currentView.fields.push({
|
|
224
|
+
name: head.name,
|
|
225
|
+
sqlType: head.type.toUpperCase(),
|
|
226
|
+
isOptional: head.isOptional,
|
|
227
|
+
isId: false,
|
|
228
|
+
isUnique: false,
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
151
233
|
if (!current) continue;
|
|
152
234
|
|
|
153
235
|
if (line.startsWith('@@map')) {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
236
|
+
const mapped = unquote(attributeArgs(line, '@@map'));
|
|
237
|
+
if (mapped) current.tableName = mapped;
|
|
156
238
|
continue;
|
|
157
239
|
}
|
|
158
240
|
if (line.startsWith('@@schema')) {
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
current.schema = match[1].trim();
|
|
241
|
+
// An empty string is meaningful — `@@schema("")` says "no schema" for databases
|
|
242
|
+
// that have none — so it has to survive as `''` rather than being dropped.
|
|
243
|
+
const declared = unquote(attributeArgs(line, '@@schema'));
|
|
244
|
+
if (declared !== undefined) {
|
|
245
|
+
current.schema = declared.trim();
|
|
165
246
|
current.declaredSchema = current.schema;
|
|
166
247
|
}
|
|
167
248
|
continue;
|
|
168
249
|
}
|
|
169
250
|
if (line.startsWith('@@unique') || line.startsWith('@@index')) {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
251
|
+
const isUnique = line.startsWith('@@unique');
|
|
252
|
+
const definition = parseDirective(line, isUnique ? '@@unique' : '@@index');
|
|
253
|
+
if (definition) {
|
|
254
|
+
if (isUnique) current.compoundUniques.push(definition);
|
|
174
255
|
else current.indexes.push(definition);
|
|
175
256
|
}
|
|
176
257
|
continue;
|
|
@@ -187,17 +268,19 @@ export function parsePushSchema(text: string, provider: Provider): PushSchema {
|
|
|
187
268
|
|
|
188
269
|
// The type is kept as written (upper-cased) because that is what the database
|
|
189
270
|
// is told; which types exist is what `resolveFieldType` just checked.
|
|
271
|
+
const uniqueArgs = attributeArgs(line, '@unique');
|
|
190
272
|
current.fields.push({
|
|
191
273
|
name: head.name,
|
|
192
274
|
sqlType: head.type.toUpperCase(),
|
|
193
275
|
isOptional: head.isOptional,
|
|
194
|
-
isId: line
|
|
195
|
-
isUnique: line
|
|
276
|
+
isId: hasAttribute(line, '@id'),
|
|
277
|
+
isUnique: hasAttribute(line, '@unique'),
|
|
278
|
+
...(uniqueArgs ? { uniqueName: stringOption(uniqueArgs, 'map') } : {}),
|
|
196
279
|
// The expression rather than SQL: `uuid()` and `autoincrement()` are spelled
|
|
197
280
|
// differently per provider, and only the dialect knows how. `@updatedAt` is
|
|
198
281
|
// `now()` under another name.
|
|
199
|
-
defaultExpr: line
|
|
200
|
-
?? (line
|
|
282
|
+
defaultExpr: attributeArgs(line, '@default')
|
|
283
|
+
?? (hasAttribute(line, '@updatedAt') ? 'now()' : undefined),
|
|
201
284
|
});
|
|
202
285
|
}
|
|
203
286
|
|
|
@@ -216,8 +299,42 @@ export function parsePushSchema(text: string, provider: Provider): PushSchema {
|
|
|
216
299
|
}
|
|
217
300
|
}
|
|
218
301
|
|
|
302
|
+
for (const view of views) {
|
|
303
|
+
if (view.declaredSchema === 'dbo' && provider !== 'mssql') {
|
|
304
|
+
issues.push({
|
|
305
|
+
path: view.name,
|
|
306
|
+
message:
|
|
307
|
+
`schema "dbo" does not exist in ${provider}; remove the @@schema directive or set it ` +
|
|
308
|
+
'to the schema this database uses',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (!view.sql.trim()) {
|
|
312
|
+
issues.push({ path: view.name, message: 'a view needs @@sql("SELECT ...")' });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// A `@unique` without `map:` must still get the deterministic name `db:migrate`
|
|
317
|
+
// expects, or push leaves a database-named constraint that the next diff cannot
|
|
318
|
+
// recognise and adds a second one for the same column.
|
|
319
|
+
for (const model of models) {
|
|
320
|
+
const table = safeIdentifierName(model.tableName);
|
|
321
|
+
for (const field of model.fields) {
|
|
322
|
+
if (field.isUnique && !field.isId && !field.uniqueName) {
|
|
323
|
+
field.uniqueName = `UQ_${table}_${safeIdentifierName(field.name)}`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
219
328
|
const modelNames = models.map((model) => model.name);
|
|
329
|
+
const viewNames = new Set(views.map((view) => view.name));
|
|
220
330
|
for (const ref of modelRefs) {
|
|
331
|
+
if (viewNames.has(ref.type)) {
|
|
332
|
+
issues.push({
|
|
333
|
+
path: `${ref.model}.${ref.field}`,
|
|
334
|
+
message: `"${ref.type}" is a view and cannot be the target of a relation; select from it directly instead`,
|
|
335
|
+
});
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
221
338
|
if (!modelNames.includes(ref.type)) {
|
|
222
339
|
issues.push({
|
|
223
340
|
path: `${ref.model}.${ref.field}`,
|
|
@@ -225,8 +342,14 @@ export function parsePushSchema(text: string, provider: Provider): PushSchema {
|
|
|
225
342
|
});
|
|
226
343
|
}
|
|
227
344
|
}
|
|
345
|
+
for (const ref of viewRefs) {
|
|
346
|
+
issues.push({
|
|
347
|
+
path: `${ref.view}.${ref.field}`,
|
|
348
|
+
message: unknownFieldTypeMessage(ref.type, provider, modelNames),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
228
351
|
|
|
229
|
-
return { models, issues };
|
|
352
|
+
return { models, views, issues };
|
|
230
353
|
}
|
|
231
354
|
|
|
232
355
|
/**
|
|
@@ -247,4 +370,9 @@ export function safeIdentifierName(raw: string): string {
|
|
|
247
370
|
|
|
248
371
|
export function qualifiedTableName(model: PushModel): string {
|
|
249
372
|
return model.schema ? `${model.schema}.${model.tableName}` : model.tableName;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** The view name to create, qualified only when the view declared a schema. */
|
|
376
|
+
export function qualifiedViewName(view: PushView): string {
|
|
377
|
+
return view.schema ? `${view.schema}.${view.viewName}` : view.viewName;
|
|
250
378
|
}
|
|
@@ -105,27 +105,28 @@ export class PythonGenerator {
|
|
|
105
105
|
}
|
|
106
106
|
content += '\n';
|
|
107
107
|
|
|
108
|
-
// CreateInput
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
108
|
+
// CreateInput / UpdateInput describe writes; a view has none.
|
|
109
|
+
if (!model.isView) {
|
|
110
|
+
const requiredFields = model.fields.filter(f => !f.isId && !f.isOptional && !f.hasDefault);
|
|
111
|
+
const optionalFields = model.fields.filter(f => !f.isId && (f.isOptional || f.hasDefault));
|
|
112
|
+
content += `@dataclass\nclass ${name}CreateInput:\n """Typed data for creating a new ${name} record."""\n`;
|
|
113
|
+
for (const f of requiredFields) {
|
|
114
|
+
content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
|
|
115
|
+
}
|
|
116
|
+
for (const f of optionalFields) {
|
|
117
|
+
content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
|
|
118
|
+
}
|
|
119
|
+
if (requiredFields.length === 0 && optionalFields.length === 0) content += ' pass\n';
|
|
120
|
+
content += '\n';
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
const updateFields = model.fields.filter(f => !f.isId);
|
|
123
|
+
content += `@dataclass\nclass ${name}UpdateInput:\n """Typed data for updating an existing ${name} record."""\n`;
|
|
124
|
+
for (const f of updateFields) {
|
|
125
|
+
content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
|
|
126
|
+
}
|
|
127
|
+
if (updateFields.length === 0) content += ' pass\n';
|
|
128
|
+
content += '\n';
|
|
126
129
|
}
|
|
127
|
-
if (updateFields.length === 0) content += ' pass\n';
|
|
128
|
-
content += '\n';
|
|
129
130
|
|
|
130
131
|
// FindManyArgs
|
|
131
132
|
content += `@dataclass\nclass ${name}FindManyArgs:\n """ORM-style args for ${name}.find_many()."""\n`;
|
|
@@ -328,6 +329,7 @@ export class PythonGenerator {
|
|
|
328
329
|
}
|
|
329
330
|
|
|
330
331
|
private generateClient(models: Model[], outputDir: string) {
|
|
332
|
+
const hasViews = models.some((model) => model.isView);
|
|
331
333
|
let content = '# This file is auto-generated. Do not edit directly.\n';
|
|
332
334
|
content += 'import os\n';
|
|
333
335
|
content += 'from typing import Dict, List, Optional, Any, Callable, TYPE_CHECKING\n\n';
|
|
@@ -337,7 +339,7 @@ export class PythonGenerator {
|
|
|
337
339
|
// generated output dir, so no relative-import fallback here — a
|
|
338
340
|
// relative fallback would be unresolvable and trip Pylance
|
|
339
341
|
// `reportMissingImports` on every generated client.
|
|
340
|
-
content +=
|
|
342
|
+
content += `from an5_adapter import An5Adapter, AdapterTableClient${hasViews ? ', ViewClient' : ''}, create_an5_adapter, set_adapter_metadata\n\n`;
|
|
341
343
|
|
|
342
344
|
const metadataModule = this.metadataModule();
|
|
343
345
|
content += 'try:\n';
|
|
@@ -376,6 +378,17 @@ export class PythonGenerator {
|
|
|
376
378
|
for (const model of models) {
|
|
377
379
|
const propName = this.toSnakeCase(model.name) + 's';
|
|
378
380
|
const singleName = this.toSnakeCase(model.name);
|
|
381
|
+
if (model.isView) {
|
|
382
|
+
// The adapter's ViewClient refuses writes; assigning it here is what makes
|
|
383
|
+
// `db.activeUsers.update(...)` fail with the read-only message instead of
|
|
384
|
+
// reaching the database as a table write.
|
|
385
|
+
content += ` client = self.adapter.view("${model.name}")\n`;
|
|
386
|
+
content += ` self.${model.name}: ViewClient = client\n`;
|
|
387
|
+
content += ` self.${model.name}s: ViewClient = client\n`;
|
|
388
|
+
content += ` self.${singleName}: ViewClient = client\n`;
|
|
389
|
+
content += ` self.${propName}: ViewClient = client\n`;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
379
392
|
content += ` client = AdapterTableClient(self.adapter, "${model.name}")\n`;
|
|
380
393
|
content += ` self.${model.name}: AdapterTableClient["${model.name}Row"] = client\n`;
|
|
381
394
|
content += ` self.${model.name}s: AdapterTableClient["${model.name}Row"] = client\n`;
|
|
@@ -441,34 +441,37 @@ pub struct DateTimeFilter {
|
|
|
441
441
|
}
|
|
442
442
|
s.push(`}\n`);
|
|
443
443
|
|
|
444
|
-
// CreateInput
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
444
|
+
// CreateInput / UpdateInput describe writes; a view has none.
|
|
445
|
+
if (!model.isView) {
|
|
446
|
+
// CreateInput (skip @id auto fields from required set via hasDefault)
|
|
447
|
+
s.push(`/// Typed data for creating a new ${name} record.`);
|
|
448
|
+
s.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`);
|
|
449
|
+
s.push(`pub struct ${name}CreateInput {`);
|
|
450
|
+
for (const f of model.fields) {
|
|
451
|
+
if (f.isId && f.hasDefault) continue;
|
|
452
|
+
const snake = this.toSnakeCase(f.name);
|
|
453
|
+
const isOpt = f.isOptional || f.hasDefault;
|
|
454
|
+
const rsType = this.mapRustType(f, isOpt);
|
|
455
|
+
if (isOpt) {
|
|
456
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
457
|
+
}
|
|
458
|
+
s.push(` pub ${snake}: ${rsType},`);
|
|
459
|
+
}
|
|
460
|
+
s.push(`}\n`);
|
|
461
|
+
|
|
462
|
+
// UpdateInput (all non-id optional)
|
|
463
|
+
s.push(`/// Typed data for updating an existing ${name} record.`);
|
|
464
|
+
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
465
|
+
s.push(`pub struct ${name}UpdateInput {`);
|
|
466
|
+
for (const f of model.fields) {
|
|
467
|
+
if (f.isId) continue;
|
|
468
|
+
const snake = this.toSnakeCase(f.name);
|
|
469
|
+
const rsType = this.mapRustType(f, true);
|
|
454
470
|
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
471
|
+
s.push(` pub ${snake}: ${rsType},`);
|
|
455
472
|
}
|
|
456
|
-
s.push(`
|
|
457
|
-
}
|
|
458
|
-
s.push(`}\n`);
|
|
459
|
-
|
|
460
|
-
// UpdateInput (all non-id optional)
|
|
461
|
-
s.push(`/// Typed data for updating an existing ${name} record.`);
|
|
462
|
-
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
463
|
-
s.push(`pub struct ${name}UpdateInput {`);
|
|
464
|
-
for (const f of model.fields) {
|
|
465
|
-
if (f.isId) continue;
|
|
466
|
-
const snake = this.toSnakeCase(f.name);
|
|
467
|
-
const rsType = this.mapRustType(f, true);
|
|
468
|
-
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
469
|
-
s.push(` pub ${snake}: ${rsType},`);
|
|
473
|
+
s.push(`}\n`);
|
|
470
474
|
}
|
|
471
|
-
s.push(`}\n`);
|
|
472
475
|
|
|
473
476
|
// FindManyArgs / FindFirstArgs / FindUniqueArgs / CountArgs
|
|
474
477
|
s.push(`/// ORM-style args for ${name}.find_many().`);
|
|
@@ -518,14 +521,16 @@ pub struct DateTimeFilter {
|
|
|
518
521
|
s.push(`}\n`);
|
|
519
522
|
|
|
520
523
|
// UpdateArgs carries the typed input alongside the where clause.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
524
|
+
if (!model.isView) {
|
|
525
|
+
s.push(`/// ORM-style args for ${name}.update().`);
|
|
526
|
+
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
527
|
+
s.push(`pub struct ${name}UpdateArgs {`);
|
|
528
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
529
|
+
s.push(` pub where_: Option<${name}WhereInput>,`);
|
|
530
|
+
s.push(` #[serde(default)]`);
|
|
531
|
+
s.push(` pub data: ${name}UpdateInput,`);
|
|
532
|
+
s.push(`}\n`);
|
|
533
|
+
}
|
|
529
534
|
}
|
|
530
535
|
return s.join('\n') + '\n';
|
|
531
536
|
}
|
|
@@ -617,7 +622,7 @@ pub struct DateTimeFilter {
|
|
|
617
622
|
|
|
618
623
|
use an5_adapters::{
|
|
619
624
|
AdapterMetadata, An5Adapter, CountArgs, DeleteManyArgs, FindManyArgs, RowMap, TableClient,
|
|
620
|
-
UpdateArgs, VectorSearchArgs,
|
|
625
|
+
UpdateArgs, VectorSearchArgs${models.some((model) => model.isView) ? ', ViewClient' : ''},
|
|
621
626
|
};
|
|
622
627
|
use serde::de::DeserializeOwned;
|
|
623
628
|
|
|
@@ -805,6 +810,11 @@ impl SqlBuilder {
|
|
|
805
810
|
private buildModelRuntimeHandle(model: Model): string {
|
|
806
811
|
const name = this.capitalize(model.name);
|
|
807
812
|
const snake = this.toSnakeCase(model.name);
|
|
813
|
+
// A view is read through the runtime's read-only ViewClient, so the handle
|
|
814
|
+
// for a view cannot expose write methods at all.
|
|
815
|
+
const reader = model.isView
|
|
816
|
+
? `self.db.adapter().view("${model.name}")`
|
|
817
|
+
: `self.db.table("${model.name}")`;
|
|
808
818
|
const lines: string[] = [];
|
|
809
819
|
|
|
810
820
|
lines.push(`/// Typed queries for ${name}. Obtained via \`An5Client::${snake}()\`.`);
|
|
@@ -825,9 +835,7 @@ impl SqlBuilder {
|
|
|
825
835
|
lines.push(` pub fn adapter(&self) -> &An5Adapter { self.db.adapter() }`);
|
|
826
836
|
lines.push(``);
|
|
827
837
|
lines.push(` pub async fn find_many(&self, args: &${name}FindManyArgs) -> Result<Vec<${name}>> {`);
|
|
828
|
-
lines.push(` let rows =
|
|
829
|
-
lines.push(` .db`);
|
|
830
|
-
lines.push(` .table("${model.name}")`);
|
|
838
|
+
lines.push(` let rows = ${reader}`);
|
|
831
839
|
lines.push(` .find_many(&FindManyArgs {`);
|
|
832
840
|
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
833
841
|
lines.push(` order_by: to_value(&args.order_by)?,`);
|
|
@@ -842,9 +850,7 @@ impl SqlBuilder {
|
|
|
842
850
|
lines.push(``);
|
|
843
851
|
lines.push(` /// The rows nearest \`vector\`, each carrying a \`distance\`.`);
|
|
844
852
|
lines.push(` pub async fn vector_search(&self, args: &${name}VectorSearchArgs) -> Result<Vec<(${name}, f64)>> {`);
|
|
845
|
-
lines.push(` let rows =
|
|
846
|
-
lines.push(` .db`);
|
|
847
|
-
lines.push(` .table("${model.name}")`);
|
|
853
|
+
lines.push(` let rows = ${reader}`);
|
|
848
854
|
lines.push(` .vector_search(&VectorSearchArgs {`);
|
|
849
855
|
lines.push(` vector: args.vector.iter().map(|v| *v as f64).collect(),`);
|
|
850
856
|
lines.push(` take: args.take.unwrap_or(0),`);
|
|
@@ -862,9 +868,7 @@ impl SqlBuilder {
|
|
|
862
868
|
lines.push(` }`);
|
|
863
869
|
lines.push(``);
|
|
864
870
|
lines.push(` pub async fn find_first(&self, args: &${name}FindFirstArgs) -> Result<Option<${name}>> {`);
|
|
865
|
-
lines.push(` let row =
|
|
866
|
-
lines.push(` .db`);
|
|
867
|
-
lines.push(` .table("${model.name}")`);
|
|
871
|
+
lines.push(` let row = ${reader}`);
|
|
868
872
|
lines.push(` .find_first(&FindManyArgs {`);
|
|
869
873
|
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
870
874
|
lines.push(` order_by: to_value(&args.order_by)?,`);
|
|
@@ -878,9 +882,7 @@ impl SqlBuilder {
|
|
|
878
882
|
lines.push(` }`);
|
|
879
883
|
lines.push(``);
|
|
880
884
|
lines.push(` pub async fn find_unique(&self, args: &${name}FindUniqueArgs) -> Result<Option<${name}>> {`);
|
|
881
|
-
lines.push(` let row =
|
|
882
|
-
lines.push(` .db`);
|
|
883
|
-
lines.push(` .table("${model.name}")`);
|
|
885
|
+
lines.push(` let row = ${reader}`);
|
|
884
886
|
lines.push(` .find_unique(&FindManyArgs {`);
|
|
885
887
|
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
886
888
|
lines.push(` ..Default::default()`);
|
|
@@ -893,37 +895,41 @@ impl SqlBuilder {
|
|
|
893
895
|
lines.push(` }`);
|
|
894
896
|
lines.push(``);
|
|
895
897
|
lines.push(` pub async fn count(&self, args: &${name}FindUniqueArgs) -> Result<i64> {`);
|
|
896
|
-
lines.push(`
|
|
897
|
-
lines.push(` .table("${model.name}")`);
|
|
898
|
+
lines.push(` ${reader}`);
|
|
898
899
|
lines.push(` .count(&CountArgs { r#where: to_value(&args.where_)? })`);
|
|
899
900
|
lines.push(` .await`);
|
|
900
901
|
lines.push(` }`);
|
|
901
902
|
lines.push(``);
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
903
|
+
|
|
904
|
+
// Writes are generated for tables only: a view has no CreateInput or
|
|
905
|
+
// UpdateInput, so these methods could not be called even if emitted.
|
|
906
|
+
if (!model.isView) {
|
|
907
|
+
lines.push(` pub async fn create(&self, data: &${name}CreateInput) -> Result<${name}> {`);
|
|
908
|
+
lines.push(` let row = self`);
|
|
909
|
+
lines.push(` .db`);
|
|
910
|
+
lines.push(` .table("${model.name}")`);
|
|
911
|
+
lines.push(` .create(&to_map(data)?)`);
|
|
912
|
+
lines.push(` .await?;`);
|
|
913
|
+
lines.push(` deserialize_row(row)`);
|
|
914
|
+
lines.push(` }`);
|
|
915
|
+
lines.push(``);
|
|
916
|
+
lines.push(` pub async fn update(&self, args: &${name}UpdateArgs) -> Result<u64> {`);
|
|
917
|
+
lines.push(` self.db`);
|
|
918
|
+
lines.push(` .table("${model.name}")`);
|
|
919
|
+
lines.push(` .update(&UpdateArgs {`);
|
|
920
|
+
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
921
|
+
lines.push(` data: serde_json::to_value(&args.data)?,`);
|
|
922
|
+
lines.push(` })`);
|
|
923
|
+
lines.push(` .await`);
|
|
924
|
+
lines.push(` }`);
|
|
925
|
+
lines.push(``);
|
|
926
|
+
lines.push(` pub async fn delete(&self, args: &${name}FindUniqueArgs) -> Result<u64> {`);
|
|
927
|
+
lines.push(` self.db`);
|
|
928
|
+
lines.push(` .table("${model.name}")`);
|
|
929
|
+
lines.push(` .delete_many(&DeleteManyArgs { r#where: to_value(&args.where_)? })`);
|
|
930
|
+
lines.push(` .await`);
|
|
931
|
+
lines.push(` }`);
|
|
932
|
+
}
|
|
927
933
|
lines.push(`}`);
|
|
928
934
|
lines.push(``);
|
|
929
935
|
return lines.join('\n') + '\n';
|