@an5/orm 1.3.2 → 1.3.4
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
|
@@ -5,6 +5,7 @@ exports.directiveName = directiveName;
|
|
|
5
5
|
exports.parsePushSchema = parsePushSchema;
|
|
6
6
|
exports.safeIdentifierName = safeIdentifierName;
|
|
7
7
|
exports.qualifiedTableName = qualifiedTableName;
|
|
8
|
+
exports.qualifiedViewName = qualifiedViewName;
|
|
8
9
|
/**
|
|
9
10
|
* Reading `.an5` files for `db:push`.
|
|
10
11
|
*
|
|
@@ -14,6 +15,7 @@ exports.qualifiedTableName = qualifiedTableName;
|
|
|
14
15
|
* lands in. It is kept here rather than inline in `push.ts` so the mapping from
|
|
15
16
|
* schema text to a column can be tested without a database.
|
|
16
17
|
*/
|
|
18
|
+
const attributes_1 = require("./attributes");
|
|
17
19
|
const field_types_1 = require("./field-types");
|
|
18
20
|
/**
|
|
19
21
|
* The schema a model lands in when the schema file does not say: none, for every
|
|
@@ -27,20 +29,17 @@ const field_types_1 = require("./field-types");
|
|
|
27
29
|
function defaultSchemaFor(_provider) {
|
|
28
30
|
return '';
|
|
29
31
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const include = options.match(/\binclude\s*:\s*\[([\w,\s]+)\]/)?.[1];
|
|
36
|
-
const filter = options.match(/\bfilter\s*:\s*"([^"]+)"/)?.[1];
|
|
37
|
-
const opts = options.match(/\boptions\s*:\s*"([^"]+)"/)?.[1];
|
|
32
|
+
/** Reads `@@unique(…)`/`@@index(…)` into the shape push and migrate share. */
|
|
33
|
+
function parseDirective(line, name) {
|
|
34
|
+
const parsed = (0, attributes_1.parseIndexDirective)((0, attributes_1.attributeArgs)(line, name));
|
|
35
|
+
if (!parsed)
|
|
36
|
+
return null;
|
|
38
37
|
return {
|
|
39
|
-
fields: fields
|
|
40
|
-
...(name ? { name } : {}),
|
|
41
|
-
...(
|
|
42
|
-
...(filter ? { filter } : {}),
|
|
43
|
-
...(
|
|
38
|
+
fields: parsed.fields,
|
|
39
|
+
...(parsed.name ? { name: parsed.name } : {}),
|
|
40
|
+
...(parsed.includeFields ? { includeFields: parsed.includeFields } : {}),
|
|
41
|
+
...(parsed.filter !== undefined ? { filter: parsed.filter } : {}),
|
|
42
|
+
...(parsed.options !== undefined ? { options: parsed.options } : {}),
|
|
44
43
|
};
|
|
45
44
|
}
|
|
46
45
|
/**
|
|
@@ -67,10 +66,13 @@ function directiveName(directive, table, kind, position) {
|
|
|
67
66
|
*/
|
|
68
67
|
function parsePushSchema(text, provider) {
|
|
69
68
|
const models = [];
|
|
69
|
+
const views = [];
|
|
70
70
|
const issues = [];
|
|
71
71
|
const modelRefs = [];
|
|
72
|
+
const viewRefs = [];
|
|
72
73
|
let current = null;
|
|
73
|
-
|
|
74
|
+
let currentView = null;
|
|
75
|
+
for (const raw of (0, attributes_1.splitSchemaLines)(text)) {
|
|
74
76
|
const line = raw.trim();
|
|
75
77
|
if (!line || line.startsWith('//'))
|
|
76
78
|
continue;
|
|
@@ -84,37 +86,92 @@ function parsePushSchema(text, provider) {
|
|
|
84
86
|
compoundUniques: [],
|
|
85
87
|
indexes: [],
|
|
86
88
|
};
|
|
89
|
+
currentView = null;
|
|
87
90
|
models.push(current);
|
|
88
91
|
continue;
|
|
89
92
|
}
|
|
93
|
+
const viewHeader = line.match(/^view\s+(\w+)\s*\{/);
|
|
94
|
+
if (viewHeader) {
|
|
95
|
+
currentView = {
|
|
96
|
+
name: viewHeader[1],
|
|
97
|
+
viewName: viewHeader[1].toLowerCase() + 's',
|
|
98
|
+
schema: defaultSchemaFor(provider),
|
|
99
|
+
fields: [],
|
|
100
|
+
sql: '',
|
|
101
|
+
};
|
|
102
|
+
current = null;
|
|
103
|
+
views.push(currentView);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
90
106
|
if (line === '}') {
|
|
91
107
|
current = null;
|
|
108
|
+
currentView = null;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (currentView) {
|
|
112
|
+
if (line.startsWith('@@sql') || line.startsWith('@@definition')) {
|
|
113
|
+
const attribute = line.startsWith('@@sql') ? '@@sql' : '@@definition';
|
|
114
|
+
const sql = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, attribute));
|
|
115
|
+
if (sql !== undefined)
|
|
116
|
+
currentView.sql = sql.trim();
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (line.startsWith('@@map')) {
|
|
120
|
+
const mapped = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, '@@map'));
|
|
121
|
+
if (mapped)
|
|
122
|
+
currentView.viewName = mapped;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (line.startsWith('@@schema')) {
|
|
126
|
+
const declared = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, '@@schema'));
|
|
127
|
+
if (declared !== undefined) {
|
|
128
|
+
currentView.schema = declared.trim();
|
|
129
|
+
currentView.declaredSchema = currentView.schema;
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (line.startsWith('@@'))
|
|
134
|
+
continue;
|
|
135
|
+
const head = (0, field_types_1.readFieldLineHead)(line);
|
|
136
|
+
if (!head)
|
|
137
|
+
continue;
|
|
138
|
+
if (!(0, field_types_1.resolveFieldType)(head.type, provider)) {
|
|
139
|
+
// A view's fields are columns of a result set, never relations.
|
|
140
|
+
viewRefs.push({ view: currentView.name, field: head.name, type: head.type });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
currentView.fields.push({
|
|
144
|
+
name: head.name,
|
|
145
|
+
sqlType: head.type.toUpperCase(),
|
|
146
|
+
isOptional: head.isOptional,
|
|
147
|
+
isId: false,
|
|
148
|
+
isUnique: false,
|
|
149
|
+
});
|
|
92
150
|
continue;
|
|
93
151
|
}
|
|
94
152
|
if (!current)
|
|
95
153
|
continue;
|
|
96
154
|
if (line.startsWith('@@map')) {
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
current.tableName =
|
|
155
|
+
const mapped = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, '@@map'));
|
|
156
|
+
if (mapped)
|
|
157
|
+
current.tableName = mapped;
|
|
100
158
|
continue;
|
|
101
159
|
}
|
|
102
160
|
if (line.startsWith('@@schema')) {
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
current.schema = match[1].trim();
|
|
161
|
+
// An empty string is meaningful — `@@schema("")` says "no schema" for databases
|
|
162
|
+
// that have none — so it has to survive as `''` rather than being dropped.
|
|
163
|
+
const declared = (0, attributes_1.unquote)((0, attributes_1.attributeArgs)(line, '@@schema'));
|
|
164
|
+
if (declared !== undefined) {
|
|
165
|
+
current.schema = declared.trim();
|
|
109
166
|
current.declaredSchema = current.schema;
|
|
110
167
|
}
|
|
111
168
|
continue;
|
|
112
169
|
}
|
|
113
170
|
if (line.startsWith('@@unique') || line.startsWith('@@index')) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
171
|
+
const isUnique = line.startsWith('@@unique');
|
|
172
|
+
const definition = parseDirective(line, isUnique ? '@@unique' : '@@index');
|
|
173
|
+
if (definition) {
|
|
174
|
+
if (isUnique)
|
|
118
175
|
current.compoundUniques.push(definition);
|
|
119
176
|
else
|
|
120
177
|
current.indexes.push(definition);
|
|
@@ -132,17 +189,19 @@ function parsePushSchema(text, provider) {
|
|
|
132
189
|
}
|
|
133
190
|
// The type is kept as written (upper-cased) because that is what the database
|
|
134
191
|
// is told; which types exist is what `resolveFieldType` just checked.
|
|
192
|
+
const uniqueArgs = (0, attributes_1.attributeArgs)(line, '@unique');
|
|
135
193
|
current.fields.push({
|
|
136
194
|
name: head.name,
|
|
137
195
|
sqlType: head.type.toUpperCase(),
|
|
138
196
|
isOptional: head.isOptional,
|
|
139
|
-
isId:
|
|
140
|
-
isUnique:
|
|
197
|
+
isId: (0, attributes_1.hasAttribute)(line, '@id'),
|
|
198
|
+
isUnique: (0, attributes_1.hasAttribute)(line, '@unique'),
|
|
199
|
+
...(uniqueArgs ? { uniqueName: (0, attributes_1.stringOption)(uniqueArgs, 'map') } : {}),
|
|
141
200
|
// The expression rather than SQL: `uuid()` and `autoincrement()` are spelled
|
|
142
201
|
// differently per provider, and only the dialect knows how. `@updatedAt` is
|
|
143
202
|
// `now()` under another name.
|
|
144
|
-
defaultExpr:
|
|
145
|
-
?? (
|
|
203
|
+
defaultExpr: (0, attributes_1.attributeArgs)(line, '@default')
|
|
204
|
+
?? ((0, attributes_1.hasAttribute)(line, '@updatedAt') ? 'now()' : undefined),
|
|
146
205
|
});
|
|
147
206
|
}
|
|
148
207
|
// A declared `dbo` on a provider without schemas would push to a database that
|
|
@@ -158,8 +217,39 @@ function parsePushSchema(text, provider) {
|
|
|
158
217
|
});
|
|
159
218
|
}
|
|
160
219
|
}
|
|
220
|
+
for (const view of views) {
|
|
221
|
+
if (view.declaredSchema === 'dbo' && provider !== 'mssql') {
|
|
222
|
+
issues.push({
|
|
223
|
+
path: view.name,
|
|
224
|
+
message: `schema "dbo" does not exist in ${provider}; remove the @@schema directive or set it ` +
|
|
225
|
+
'to the schema this database uses',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (!view.sql.trim()) {
|
|
229
|
+
issues.push({ path: view.name, message: 'a view needs @@sql("SELECT ...")' });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// A `@unique` without `map:` must still get the deterministic name `db:migrate`
|
|
233
|
+
// expects, or push leaves a database-named constraint that the next diff cannot
|
|
234
|
+
// recognise and adds a second one for the same column.
|
|
235
|
+
for (const model of models) {
|
|
236
|
+
const table = safeIdentifierName(model.tableName);
|
|
237
|
+
for (const field of model.fields) {
|
|
238
|
+
if (field.isUnique && !field.isId && !field.uniqueName) {
|
|
239
|
+
field.uniqueName = `UQ_${table}_${safeIdentifierName(field.name)}`;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
161
243
|
const modelNames = models.map((model) => model.name);
|
|
244
|
+
const viewNames = new Set(views.map((view) => view.name));
|
|
162
245
|
for (const ref of modelRefs) {
|
|
246
|
+
if (viewNames.has(ref.type)) {
|
|
247
|
+
issues.push({
|
|
248
|
+
path: `${ref.model}.${ref.field}`,
|
|
249
|
+
message: `"${ref.type}" is a view and cannot be the target of a relation; select from it directly instead`,
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
163
253
|
if (!modelNames.includes(ref.type)) {
|
|
164
254
|
issues.push({
|
|
165
255
|
path: `${ref.model}.${ref.field}`,
|
|
@@ -167,7 +257,13 @@ function parsePushSchema(text, provider) {
|
|
|
167
257
|
});
|
|
168
258
|
}
|
|
169
259
|
}
|
|
170
|
-
|
|
260
|
+
for (const ref of viewRefs) {
|
|
261
|
+
issues.push({
|
|
262
|
+
path: `${ref.view}.${ref.field}`,
|
|
263
|
+
message: (0, field_types_1.unknownFieldTypeMessage)(ref.type, provider, modelNames),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return { models, views, issues };
|
|
171
267
|
}
|
|
172
268
|
/**
|
|
173
269
|
* The table name to create, `schema.table` unless the model cleared the schema.
|
|
@@ -187,3 +283,7 @@ function safeIdentifierName(raw) {
|
|
|
187
283
|
function qualifiedTableName(model) {
|
|
188
284
|
return model.schema ? `${model.schema}.${model.tableName}` : model.tableName;
|
|
189
285
|
}
|
|
286
|
+
/** The view name to create, qualified only when the view declared a schema. */
|
|
287
|
+
function qualifiedViewName(view) {
|
|
288
|
+
return view.schema ? `${view.schema}.${view.viewName}` : view.viewName;
|
|
289
|
+
}
|
|
@@ -88,28 +88,29 @@ class PythonGenerator {
|
|
|
88
88
|
content += ` ${this.toSnakeCase(f.name)}: Optional[str] = None\n`;
|
|
89
89
|
}
|
|
90
90
|
content += '\n';
|
|
91
|
-
// CreateInput
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
91
|
+
// CreateInput / UpdateInput describe writes; a view has none.
|
|
92
|
+
if (!model.isView) {
|
|
93
|
+
const requiredFields = model.fields.filter(f => !f.isId && !f.isOptional && !f.hasDefault);
|
|
94
|
+
const optionalFields = model.fields.filter(f => !f.isId && (f.isOptional || f.hasDefault));
|
|
95
|
+
content += `@dataclass\nclass ${name}CreateInput:\n """Typed data for creating a new ${name} record."""\n`;
|
|
96
|
+
for (const f of requiredFields) {
|
|
97
|
+
content += ` ${this.toSnakeCase(f.name)}: ${this.mapPyType(f)}\n`;
|
|
98
|
+
}
|
|
99
|
+
for (const f of optionalFields) {
|
|
100
|
+
content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
|
|
101
|
+
}
|
|
102
|
+
if (requiredFields.length === 0 && optionalFields.length === 0)
|
|
103
|
+
content += ' pass\n';
|
|
104
|
+
content += '\n';
|
|
105
|
+
const updateFields = model.fields.filter(f => !f.isId);
|
|
106
|
+
content += `@dataclass\nclass ${name}UpdateInput:\n """Typed data for updating an existing ${name} record."""\n`;
|
|
107
|
+
for (const f of updateFields) {
|
|
108
|
+
content += ` ${this.toSnakeCase(f.name)}: Optional[${this.mapPyType(f)}] = None\n`;
|
|
109
|
+
}
|
|
110
|
+
if (updateFields.length === 0)
|
|
111
|
+
content += ' pass\n';
|
|
112
|
+
content += '\n';
|
|
109
113
|
}
|
|
110
|
-
if (updateFields.length === 0)
|
|
111
|
-
content += ' pass\n';
|
|
112
|
-
content += '\n';
|
|
113
114
|
// FindManyArgs
|
|
114
115
|
content += `@dataclass\nclass ${name}FindManyArgs:\n """ORM-style args for ${name}.find_many()."""\n`;
|
|
115
116
|
content += ` where: Optional[${name}WhereInput] = None\n`;
|
|
@@ -292,6 +293,7 @@ class PythonGenerator {
|
|
|
292
293
|
fs_1.default.writeFileSync(path_1.default.join(outputDir, 'an5_models.py'), content);
|
|
293
294
|
}
|
|
294
295
|
generateClient(models, outputDir) {
|
|
296
|
+
const hasViews = models.some((model) => model.isView);
|
|
295
297
|
let content = '# This file is auto-generated. Do not edit directly.\n';
|
|
296
298
|
content += 'import os\n';
|
|
297
299
|
content += 'from typing import Dict, List, Optional, Any, Callable, TYPE_CHECKING\n\n';
|
|
@@ -300,7 +302,7 @@ class PythonGenerator {
|
|
|
300
302
|
// generated output dir, so no relative-import fallback here — a
|
|
301
303
|
// relative fallback would be unresolvable and trip Pylance
|
|
302
304
|
// `reportMissingImports` on every generated client.
|
|
303
|
-
content +=
|
|
305
|
+
content += `from an5_adapter import An5Adapter, AdapterTableClient${hasViews ? ', ViewClient' : ''}, create_an5_adapter, set_adapter_metadata\n\n`;
|
|
304
306
|
const metadataModule = this.metadataModule();
|
|
305
307
|
content += 'try:\n';
|
|
306
308
|
content += ` from .${metadataModule} import MODEL_TO_TABLE, MODEL_FIELDS\n`;
|
|
@@ -335,6 +337,17 @@ class PythonGenerator {
|
|
|
335
337
|
for (const model of models) {
|
|
336
338
|
const propName = this.toSnakeCase(model.name) + 's';
|
|
337
339
|
const singleName = this.toSnakeCase(model.name);
|
|
340
|
+
if (model.isView) {
|
|
341
|
+
// The adapter's ViewClient refuses writes; assigning it here is what makes
|
|
342
|
+
// `db.activeUsers.update(...)` fail with the read-only message instead of
|
|
343
|
+
// reaching the database as a table write.
|
|
344
|
+
content += ` client = self.adapter.view("${model.name}")\n`;
|
|
345
|
+
content += ` self.${model.name}: ViewClient = client\n`;
|
|
346
|
+
content += ` self.${model.name}s: ViewClient = client\n`;
|
|
347
|
+
content += ` self.${singleName}: ViewClient = client\n`;
|
|
348
|
+
content += ` self.${propName}: ViewClient = client\n`;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
338
351
|
content += ` client = AdapterTableClient(self.adapter, "${model.name}")\n`;
|
|
339
352
|
content += ` self.${model.name}: AdapterTableClient["${model.name}Row"] = client\n`;
|
|
340
353
|
content += ` self.${model.name}s: AdapterTableClient["${model.name}Row"] = client\n`;
|
|
@@ -432,35 +432,38 @@ pub struct DateTimeFilter {
|
|
|
432
432
|
s.push(` pub ${snake}: Option<SortOrder>,`);
|
|
433
433
|
}
|
|
434
434
|
s.push(`}\n`);
|
|
435
|
-
// CreateInput
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
435
|
+
// CreateInput / UpdateInput describe writes; a view has none.
|
|
436
|
+
if (!model.isView) {
|
|
437
|
+
// CreateInput (skip @id auto fields from required set via hasDefault)
|
|
438
|
+
s.push(`/// Typed data for creating a new ${name} record.`);
|
|
439
|
+
s.push(`#[derive(Debug, Clone, Serialize, Deserialize)]`);
|
|
440
|
+
s.push(`pub struct ${name}CreateInput {`);
|
|
441
|
+
for (const f of model.fields) {
|
|
442
|
+
if (f.isId && f.hasDefault)
|
|
443
|
+
continue;
|
|
444
|
+
const snake = this.toSnakeCase(f.name);
|
|
445
|
+
const isOpt = f.isOptional || f.hasDefault;
|
|
446
|
+
const rsType = this.mapRustType(f, isOpt);
|
|
447
|
+
if (isOpt) {
|
|
448
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
449
|
+
}
|
|
450
|
+
s.push(` pub ${snake}: ${rsType},`);
|
|
451
|
+
}
|
|
452
|
+
s.push(`}\n`);
|
|
453
|
+
// UpdateInput (all non-id optional)
|
|
454
|
+
s.push(`/// Typed data for updating an existing ${name} record.`);
|
|
455
|
+
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
456
|
+
s.push(`pub struct ${name}UpdateInput {`);
|
|
457
|
+
for (const f of model.fields) {
|
|
458
|
+
if (f.isId)
|
|
459
|
+
continue;
|
|
460
|
+
const snake = this.toSnakeCase(f.name);
|
|
461
|
+
const rsType = this.mapRustType(f, true);
|
|
446
462
|
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
463
|
+
s.push(` pub ${snake}: ${rsType},`);
|
|
447
464
|
}
|
|
448
|
-
s.push(`
|
|
449
|
-
}
|
|
450
|
-
s.push(`}\n`);
|
|
451
|
-
// UpdateInput (all non-id optional)
|
|
452
|
-
s.push(`/// Typed data for updating an existing ${name} record.`);
|
|
453
|
-
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
454
|
-
s.push(`pub struct ${name}UpdateInput {`);
|
|
455
|
-
for (const f of model.fields) {
|
|
456
|
-
if (f.isId)
|
|
457
|
-
continue;
|
|
458
|
-
const snake = this.toSnakeCase(f.name);
|
|
459
|
-
const rsType = this.mapRustType(f, true);
|
|
460
|
-
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
461
|
-
s.push(` pub ${snake}: ${rsType},`);
|
|
465
|
+
s.push(`}\n`);
|
|
462
466
|
}
|
|
463
|
-
s.push(`}\n`);
|
|
464
467
|
// FindManyArgs / FindFirstArgs / FindUniqueArgs / CountArgs
|
|
465
468
|
s.push(`/// ORM-style args for ${name}.find_many().`);
|
|
466
469
|
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
@@ -505,14 +508,16 @@ pub struct DateTimeFilter {
|
|
|
505
508
|
s.push(` pub distance_metric: String,`);
|
|
506
509
|
s.push(`}\n`);
|
|
507
510
|
// UpdateArgs carries the typed input alongside the where clause.
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
511
|
+
if (!model.isView) {
|
|
512
|
+
s.push(`/// ORM-style args for ${name}.update().`);
|
|
513
|
+
s.push(`#[derive(Debug, Clone, Default, Serialize, Deserialize)]`);
|
|
514
|
+
s.push(`pub struct ${name}UpdateArgs {`);
|
|
515
|
+
s.push(` #[serde(default, skip_serializing_if = "Option::is_none")]`);
|
|
516
|
+
s.push(` pub where_: Option<${name}WhereInput>,`);
|
|
517
|
+
s.push(` #[serde(default)]`);
|
|
518
|
+
s.push(` pub data: ${name}UpdateInput,`);
|
|
519
|
+
s.push(`}\n`);
|
|
520
|
+
}
|
|
516
521
|
}
|
|
517
522
|
return s.join('\n') + '\n';
|
|
518
523
|
}
|
|
@@ -601,7 +606,7 @@ pub struct DateTimeFilter {
|
|
|
601
606
|
|
|
602
607
|
use an5_adapters::{
|
|
603
608
|
AdapterMetadata, An5Adapter, CountArgs, DeleteManyArgs, FindManyArgs, RowMap, TableClient,
|
|
604
|
-
UpdateArgs, VectorSearchArgs,
|
|
609
|
+
UpdateArgs, VectorSearchArgs${models.some((model) => model.isView) ? ', ViewClient' : ''},
|
|
605
610
|
};
|
|
606
611
|
use serde::de::DeserializeOwned;
|
|
607
612
|
|
|
@@ -788,6 +793,11 @@ impl SqlBuilder {
|
|
|
788
793
|
buildModelRuntimeHandle(model) {
|
|
789
794
|
const name = this.capitalize(model.name);
|
|
790
795
|
const snake = this.toSnakeCase(model.name);
|
|
796
|
+
// A view is read through the runtime's read-only ViewClient, so the handle
|
|
797
|
+
// for a view cannot expose write methods at all.
|
|
798
|
+
const reader = model.isView
|
|
799
|
+
? `self.db.adapter().view("${model.name}")`
|
|
800
|
+
: `self.db.table("${model.name}")`;
|
|
791
801
|
const lines = [];
|
|
792
802
|
lines.push(`/// Typed queries for ${name}. Obtained via \`An5Client::${snake}()\`.`);
|
|
793
803
|
lines.push(`#[derive(Clone)]`);
|
|
@@ -807,9 +817,7 @@ impl SqlBuilder {
|
|
|
807
817
|
lines.push(` pub fn adapter(&self) -> &An5Adapter { self.db.adapter() }`);
|
|
808
818
|
lines.push(``);
|
|
809
819
|
lines.push(` pub async fn find_many(&self, args: &${name}FindManyArgs) -> Result<Vec<${name}>> {`);
|
|
810
|
-
lines.push(` let rows =
|
|
811
|
-
lines.push(` .db`);
|
|
812
|
-
lines.push(` .table("${model.name}")`);
|
|
820
|
+
lines.push(` let rows = ${reader}`);
|
|
813
821
|
lines.push(` .find_many(&FindManyArgs {`);
|
|
814
822
|
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
815
823
|
lines.push(` order_by: to_value(&args.order_by)?,`);
|
|
@@ -824,9 +832,7 @@ impl SqlBuilder {
|
|
|
824
832
|
lines.push(``);
|
|
825
833
|
lines.push(` /// The rows nearest \`vector\`, each carrying a \`distance\`.`);
|
|
826
834
|
lines.push(` pub async fn vector_search(&self, args: &${name}VectorSearchArgs) -> Result<Vec<(${name}, f64)>> {`);
|
|
827
|
-
lines.push(` let rows =
|
|
828
|
-
lines.push(` .db`);
|
|
829
|
-
lines.push(` .table("${model.name}")`);
|
|
835
|
+
lines.push(` let rows = ${reader}`);
|
|
830
836
|
lines.push(` .vector_search(&VectorSearchArgs {`);
|
|
831
837
|
lines.push(` vector: args.vector.iter().map(|v| *v as f64).collect(),`);
|
|
832
838
|
lines.push(` take: args.take.unwrap_or(0),`);
|
|
@@ -844,9 +850,7 @@ impl SqlBuilder {
|
|
|
844
850
|
lines.push(` }`);
|
|
845
851
|
lines.push(``);
|
|
846
852
|
lines.push(` pub async fn find_first(&self, args: &${name}FindFirstArgs) -> Result<Option<${name}>> {`);
|
|
847
|
-
lines.push(` let row =
|
|
848
|
-
lines.push(` .db`);
|
|
849
|
-
lines.push(` .table("${model.name}")`);
|
|
853
|
+
lines.push(` let row = ${reader}`);
|
|
850
854
|
lines.push(` .find_first(&FindManyArgs {`);
|
|
851
855
|
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
852
856
|
lines.push(` order_by: to_value(&args.order_by)?,`);
|
|
@@ -860,9 +864,7 @@ impl SqlBuilder {
|
|
|
860
864
|
lines.push(` }`);
|
|
861
865
|
lines.push(``);
|
|
862
866
|
lines.push(` pub async fn find_unique(&self, args: &${name}FindUniqueArgs) -> Result<Option<${name}>> {`);
|
|
863
|
-
lines.push(` let row =
|
|
864
|
-
lines.push(` .db`);
|
|
865
|
-
lines.push(` .table("${model.name}")`);
|
|
867
|
+
lines.push(` let row = ${reader}`);
|
|
866
868
|
lines.push(` .find_unique(&FindManyArgs {`);
|
|
867
869
|
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
868
870
|
lines.push(` ..Default::default()`);
|
|
@@ -875,37 +877,40 @@ impl SqlBuilder {
|
|
|
875
877
|
lines.push(` }`);
|
|
876
878
|
lines.push(``);
|
|
877
879
|
lines.push(` pub async fn count(&self, args: &${name}FindUniqueArgs) -> Result<i64> {`);
|
|
878
|
-
lines.push(`
|
|
879
|
-
lines.push(` .table("${model.name}")`);
|
|
880
|
+
lines.push(` ${reader}`);
|
|
880
881
|
lines.push(` .count(&CountArgs { r#where: to_value(&args.where_)? })`);
|
|
881
882
|
lines.push(` .await`);
|
|
882
883
|
lines.push(` }`);
|
|
883
884
|
lines.push(``);
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
885
|
+
// Writes are generated for tables only: a view has no CreateInput or
|
|
886
|
+
// UpdateInput, so these methods could not be called even if emitted.
|
|
887
|
+
if (!model.isView) {
|
|
888
|
+
lines.push(` pub async fn create(&self, data: &${name}CreateInput) -> Result<${name}> {`);
|
|
889
|
+
lines.push(` let row = self`);
|
|
890
|
+
lines.push(` .db`);
|
|
891
|
+
lines.push(` .table("${model.name}")`);
|
|
892
|
+
lines.push(` .create(&to_map(data)?)`);
|
|
893
|
+
lines.push(` .await?;`);
|
|
894
|
+
lines.push(` deserialize_row(row)`);
|
|
895
|
+
lines.push(` }`);
|
|
896
|
+
lines.push(``);
|
|
897
|
+
lines.push(` pub async fn update(&self, args: &${name}UpdateArgs) -> Result<u64> {`);
|
|
898
|
+
lines.push(` self.db`);
|
|
899
|
+
lines.push(` .table("${model.name}")`);
|
|
900
|
+
lines.push(` .update(&UpdateArgs {`);
|
|
901
|
+
lines.push(` r#where: to_value(&args.where_)?,`);
|
|
902
|
+
lines.push(` data: serde_json::to_value(&args.data)?,`);
|
|
903
|
+
lines.push(` })`);
|
|
904
|
+
lines.push(` .await`);
|
|
905
|
+
lines.push(` }`);
|
|
906
|
+
lines.push(``);
|
|
907
|
+
lines.push(` pub async fn delete(&self, args: &${name}FindUniqueArgs) -> Result<u64> {`);
|
|
908
|
+
lines.push(` self.db`);
|
|
909
|
+
lines.push(` .table("${model.name}")`);
|
|
910
|
+
lines.push(` .delete_many(&DeleteManyArgs { r#where: to_value(&args.where_)? })`);
|
|
911
|
+
lines.push(` .await`);
|
|
912
|
+
lines.push(` }`);
|
|
913
|
+
}
|
|
909
914
|
lines.push(`}`);
|
|
910
915
|
lines.push(``);
|
|
911
916
|
return lines.join('\n') + '\n';
|