@mateusseiboth/ember-orm 0.1.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/LICENSE +21 -0
- package/README.md +125 -0
- package/dist/cli/bin.cjs +3265 -0
- package/dist/cli/bin.cjs.map +1 -0
- package/dist/cli/bin.d.cts +1 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +3241 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/client/index.cjs +2434 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +2 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2396 -0
- package/dist/client/index.js.map +1 -0
- package/dist/editor.cjs +1113 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +22 -0
- package/dist/editor.d.ts +22 -0
- package/dist/editor.js +1077 -0
- package/dist/editor.js.map +1 -0
- package/dist/index-0lWi8TMM.d.ts +72 -0
- package/dist/index-BSXZjDUd.d.ts +459 -0
- package/dist/index-CKqkQhZx.d.cts +459 -0
- package/dist/index-CMeqhmVc.d.cts +72 -0
- package/dist/index-D0xIdtCl.d.cts +145 -0
- package/dist/index-D0xIdtCl.d.ts +145 -0
- package/dist/index.cjs +5013 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +288 -0
- package/dist/index.d.ts +288 -0
- package/dist/index.js +4935 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
package/dist/cli/bin.js
ADDED
|
@@ -0,0 +1,3241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/bin.ts
|
|
4
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
// src/errors/index.ts
|
|
8
|
+
var EmberError = class extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = new.target.name;
|
|
12
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var SchemaParseError = class extends EmberError {
|
|
16
|
+
constructor(message, line, column, file) {
|
|
17
|
+
super(
|
|
18
|
+
`${message} (at ${file ? `${file}:` : ""}${line}:${column})`
|
|
19
|
+
);
|
|
20
|
+
this.line = line;
|
|
21
|
+
this.column = column;
|
|
22
|
+
this.file = file;
|
|
23
|
+
}
|
|
24
|
+
line;
|
|
25
|
+
column;
|
|
26
|
+
file;
|
|
27
|
+
};
|
|
28
|
+
var SchemaValidationError = class extends EmberError {
|
|
29
|
+
constructor(message, details = []) {
|
|
30
|
+
super(
|
|
31
|
+
details.length > 0 ? `${message}
|
|
32
|
+
- ${details.join("\n - ")}` : message
|
|
33
|
+
);
|
|
34
|
+
this.details = details;
|
|
35
|
+
}
|
|
36
|
+
details;
|
|
37
|
+
};
|
|
38
|
+
var DatabaseError = class extends EmberError {
|
|
39
|
+
constructor(message, cause, sql) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.cause = cause;
|
|
42
|
+
this.sql = sql;
|
|
43
|
+
}
|
|
44
|
+
cause;
|
|
45
|
+
sql;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/cli/commands.ts
|
|
49
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
50
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
51
|
+
|
|
52
|
+
// src/schema/index.ts
|
|
53
|
+
import { readFileSync, existsSync } from "fs";
|
|
54
|
+
import { resolve, dirname } from "path";
|
|
55
|
+
|
|
56
|
+
// src/schema/lexer.ts
|
|
57
|
+
var SINGLE_CHAR_TOKENS = {
|
|
58
|
+
"{": "lbrace",
|
|
59
|
+
"}": "rbrace",
|
|
60
|
+
"(": "lparen",
|
|
61
|
+
")": "rparen",
|
|
62
|
+
"[": "lbracket",
|
|
63
|
+
"]": "rbracket",
|
|
64
|
+
"=": "equals",
|
|
65
|
+
",": "comma",
|
|
66
|
+
":": "colon",
|
|
67
|
+
"?": "question",
|
|
68
|
+
".": "dot"
|
|
69
|
+
};
|
|
70
|
+
var IDENT_START = /[A-Za-z_]/;
|
|
71
|
+
var IDENT_PART = /[A-Za-z0-9_]/;
|
|
72
|
+
var Lexer = class {
|
|
73
|
+
constructor(source, file) {
|
|
74
|
+
this.source = source;
|
|
75
|
+
this.file = file;
|
|
76
|
+
}
|
|
77
|
+
source;
|
|
78
|
+
file;
|
|
79
|
+
pos = 0;
|
|
80
|
+
line = 1;
|
|
81
|
+
column = 1;
|
|
82
|
+
tokenize() {
|
|
83
|
+
const tokens = [];
|
|
84
|
+
let token = this.next();
|
|
85
|
+
while (token.type !== "eof") {
|
|
86
|
+
tokens.push(token);
|
|
87
|
+
token = this.next();
|
|
88
|
+
}
|
|
89
|
+
tokens.push(token);
|
|
90
|
+
return tokens;
|
|
91
|
+
}
|
|
92
|
+
next() {
|
|
93
|
+
this.skipWhitespaceAndComments();
|
|
94
|
+
if (this.pos >= this.source.length) {
|
|
95
|
+
return this.make("eof", "");
|
|
96
|
+
}
|
|
97
|
+
const startLine = this.line;
|
|
98
|
+
const startColumn = this.column;
|
|
99
|
+
const ch = this.source[this.pos];
|
|
100
|
+
if (ch === "/" && this.peek(1) === "/" && this.peek(2) === "/") {
|
|
101
|
+
return this.readDocComment(startLine, startColumn);
|
|
102
|
+
}
|
|
103
|
+
if (ch === '"') {
|
|
104
|
+
return this.readString(startLine, startColumn);
|
|
105
|
+
}
|
|
106
|
+
if (ch === "@") {
|
|
107
|
+
this.advance();
|
|
108
|
+
if (this.source[this.pos] === "@") {
|
|
109
|
+
this.advance();
|
|
110
|
+
return { type: "double_at", value: "@@", line: startLine, column: startColumn };
|
|
111
|
+
}
|
|
112
|
+
return { type: "at", value: "@", line: startLine, column: startColumn };
|
|
113
|
+
}
|
|
114
|
+
const single = SINGLE_CHAR_TOKENS[ch];
|
|
115
|
+
if (single) {
|
|
116
|
+
this.advance();
|
|
117
|
+
return { type: single, value: ch, line: startLine, column: startColumn };
|
|
118
|
+
}
|
|
119
|
+
if (ch === "-" || /[0-9]/.test(ch)) {
|
|
120
|
+
return this.readNumber(startLine, startColumn);
|
|
121
|
+
}
|
|
122
|
+
if (IDENT_START.test(ch)) {
|
|
123
|
+
return this.readIdentifier(startLine, startColumn);
|
|
124
|
+
}
|
|
125
|
+
throw new SchemaParseError(
|
|
126
|
+
`Unexpected character '${ch}'`,
|
|
127
|
+
startLine,
|
|
128
|
+
startColumn,
|
|
129
|
+
this.file
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
readDocComment(line, column) {
|
|
133
|
+
this.advance();
|
|
134
|
+
this.advance();
|
|
135
|
+
this.advance();
|
|
136
|
+
let value = "";
|
|
137
|
+
while (this.pos < this.source.length && this.source[this.pos] !== "\n") {
|
|
138
|
+
value += this.source[this.pos];
|
|
139
|
+
this.advance();
|
|
140
|
+
}
|
|
141
|
+
return { type: "doc_comment", value: value.trim(), line, column };
|
|
142
|
+
}
|
|
143
|
+
readString(line, column) {
|
|
144
|
+
this.advance();
|
|
145
|
+
let value = "";
|
|
146
|
+
while (this.pos < this.source.length && this.source[this.pos] !== '"') {
|
|
147
|
+
const c = this.source[this.pos];
|
|
148
|
+
if (c === "\\") {
|
|
149
|
+
this.advance();
|
|
150
|
+
const escaped = this.source[this.pos];
|
|
151
|
+
if (escaped === void 0) break;
|
|
152
|
+
value += escapeChar(escaped);
|
|
153
|
+
this.advance();
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (c === "\n") {
|
|
157
|
+
throw new SchemaParseError(
|
|
158
|
+
"Unterminated string literal",
|
|
159
|
+
line,
|
|
160
|
+
column,
|
|
161
|
+
this.file
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
value += c;
|
|
165
|
+
this.advance();
|
|
166
|
+
}
|
|
167
|
+
if (this.source[this.pos] !== '"') {
|
|
168
|
+
throw new SchemaParseError(
|
|
169
|
+
"Unterminated string literal",
|
|
170
|
+
line,
|
|
171
|
+
column,
|
|
172
|
+
this.file
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
this.advance();
|
|
176
|
+
return { type: "string", value, line, column };
|
|
177
|
+
}
|
|
178
|
+
readNumber(line, column) {
|
|
179
|
+
let value = "";
|
|
180
|
+
if (this.source[this.pos] === "-") {
|
|
181
|
+
value += "-";
|
|
182
|
+
this.advance();
|
|
183
|
+
}
|
|
184
|
+
while (this.pos < this.source.length && /[0-9.]/.test(this.source[this.pos])) {
|
|
185
|
+
value += this.source[this.pos];
|
|
186
|
+
this.advance();
|
|
187
|
+
}
|
|
188
|
+
return { type: "number", value, line, column };
|
|
189
|
+
}
|
|
190
|
+
readIdentifier(line, column) {
|
|
191
|
+
let value = "";
|
|
192
|
+
while (this.pos < this.source.length && IDENT_PART.test(this.source[this.pos])) {
|
|
193
|
+
value += this.source[this.pos];
|
|
194
|
+
this.advance();
|
|
195
|
+
}
|
|
196
|
+
return { type: "identifier", value, line, column };
|
|
197
|
+
}
|
|
198
|
+
skipWhitespaceAndComments() {
|
|
199
|
+
while (this.pos < this.source.length) {
|
|
200
|
+
const ch = this.source[this.pos];
|
|
201
|
+
if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
|
|
202
|
+
this.advance();
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (ch === "/" && this.peek(1) === "/" && this.peek(2) !== "/") {
|
|
206
|
+
while (this.pos < this.source.length && this.source[this.pos] !== "\n") {
|
|
207
|
+
this.advance();
|
|
208
|
+
}
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (ch === "/" && this.peek(1) === "*") {
|
|
212
|
+
this.advance();
|
|
213
|
+
this.advance();
|
|
214
|
+
while (this.pos < this.source.length && !(this.source[this.pos] === "*" && this.peek(1) === "/")) {
|
|
215
|
+
this.advance();
|
|
216
|
+
}
|
|
217
|
+
this.advance();
|
|
218
|
+
this.advance();
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
peek(offset) {
|
|
225
|
+
return this.source[this.pos + offset];
|
|
226
|
+
}
|
|
227
|
+
advance() {
|
|
228
|
+
if (this.source[this.pos] === "\n") {
|
|
229
|
+
this.line++;
|
|
230
|
+
this.column = 1;
|
|
231
|
+
} else {
|
|
232
|
+
this.column++;
|
|
233
|
+
}
|
|
234
|
+
this.pos++;
|
|
235
|
+
}
|
|
236
|
+
make(type, value) {
|
|
237
|
+
return { type, value, line: this.line, column: this.column };
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
function escapeChar(c) {
|
|
241
|
+
switch (c) {
|
|
242
|
+
case "n":
|
|
243
|
+
return "\n";
|
|
244
|
+
case "t":
|
|
245
|
+
return " ";
|
|
246
|
+
case "r":
|
|
247
|
+
return "\r";
|
|
248
|
+
case '"':
|
|
249
|
+
return '"';
|
|
250
|
+
case "\\":
|
|
251
|
+
return "\\";
|
|
252
|
+
default:
|
|
253
|
+
return c;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/schema/parser.ts
|
|
258
|
+
var Parser = class {
|
|
259
|
+
constructor(source, file) {
|
|
260
|
+
this.file = file;
|
|
261
|
+
this.tokens = new Lexer(source, file).tokenize();
|
|
262
|
+
}
|
|
263
|
+
file;
|
|
264
|
+
tokens;
|
|
265
|
+
pos = 0;
|
|
266
|
+
parse() {
|
|
267
|
+
const doc = { generators: [], models: [], enums: [] };
|
|
268
|
+
let pendingDoc = [];
|
|
269
|
+
while (!this.isEof()) {
|
|
270
|
+
const tok = this.peek();
|
|
271
|
+
if (tok.type === "doc_comment") {
|
|
272
|
+
pendingDoc.push(tok.value);
|
|
273
|
+
this.advance();
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (tok.type !== "identifier") {
|
|
277
|
+
throw this.error(`Unexpected token '${tok.value}'`, tok);
|
|
278
|
+
}
|
|
279
|
+
const documentation = pendingDoc.length ? pendingDoc.join("\n") : void 0;
|
|
280
|
+
pendingDoc = [];
|
|
281
|
+
switch (tok.value) {
|
|
282
|
+
case "datasource":
|
|
283
|
+
doc.datasource = this.parseDatasource();
|
|
284
|
+
break;
|
|
285
|
+
case "generator":
|
|
286
|
+
doc.generators.push(this.parseGenerator());
|
|
287
|
+
break;
|
|
288
|
+
case "model":
|
|
289
|
+
doc.models.push(this.parseModel(documentation));
|
|
290
|
+
break;
|
|
291
|
+
case "enum":
|
|
292
|
+
doc.enums.push(this.parseEnum(documentation));
|
|
293
|
+
break;
|
|
294
|
+
default:
|
|
295
|
+
throw this.error(`Unknown top-level keyword '${tok.value}'`, tok);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
resolveKinds(doc);
|
|
299
|
+
return doc;
|
|
300
|
+
}
|
|
301
|
+
// ---- Top-level blocks -------------------------------------------------
|
|
302
|
+
parseDatasource() {
|
|
303
|
+
this.expectKeyword("datasource");
|
|
304
|
+
const name = this.expect("identifier").value;
|
|
305
|
+
this.expect("lbrace");
|
|
306
|
+
const assignments = this.parseAssignments();
|
|
307
|
+
this.expect("rbrace");
|
|
308
|
+
const provider = literalString(assignments["provider"]);
|
|
309
|
+
const urlAssign = assignments["url"];
|
|
310
|
+
let url = { kind: "literal", value: "" };
|
|
311
|
+
if (urlAssign) {
|
|
312
|
+
if (urlAssign.kind === "function" && urlAssign.name === "env") {
|
|
313
|
+
url = { kind: "env", value: literalString(urlAssign.args[0]) };
|
|
314
|
+
} else if (urlAssign.kind === "string") {
|
|
315
|
+
url = { kind: "literal", value: urlAssign.value };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { name, provider: provider ?? "firebird", url };
|
|
319
|
+
}
|
|
320
|
+
parseGenerator() {
|
|
321
|
+
this.expectKeyword("generator");
|
|
322
|
+
const name = this.expect("identifier").value;
|
|
323
|
+
this.expect("lbrace");
|
|
324
|
+
const assignments = this.parseAssignments();
|
|
325
|
+
this.expect("rbrace");
|
|
326
|
+
const config = {};
|
|
327
|
+
for (const [key, value] of Object.entries(assignments)) {
|
|
328
|
+
if (value.kind === "string") config[key] = value.value;
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
name,
|
|
332
|
+
provider: literalString(assignments["provider"]) ?? "ember-client-js",
|
|
333
|
+
output: assignments["output"] ? literalString(assignments["output"]) : void 0,
|
|
334
|
+
config
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
parseAssignments() {
|
|
338
|
+
const out = {};
|
|
339
|
+
while (!this.check("rbrace") && !this.isEof()) {
|
|
340
|
+
if (this.check("doc_comment")) {
|
|
341
|
+
this.advance();
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const key = this.expect("identifier").value;
|
|
345
|
+
this.expect("equals");
|
|
346
|
+
out[key] = this.parseValue();
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
parseModel(documentation) {
|
|
351
|
+
this.expectKeyword("model");
|
|
352
|
+
const name = this.expect("identifier").value;
|
|
353
|
+
this.expect("lbrace");
|
|
354
|
+
const model = {
|
|
355
|
+
name,
|
|
356
|
+
fields: [],
|
|
357
|
+
primaryKey: [],
|
|
358
|
+
uniqueIndexes: [],
|
|
359
|
+
indexes: [],
|
|
360
|
+
documentation
|
|
361
|
+
};
|
|
362
|
+
let pendingDoc = [];
|
|
363
|
+
while (!this.check("rbrace") && !this.isEof()) {
|
|
364
|
+
const tok = this.peek();
|
|
365
|
+
if (tok.type === "doc_comment") {
|
|
366
|
+
pendingDoc.push(tok.value);
|
|
367
|
+
this.advance();
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (tok.type === "double_at") {
|
|
371
|
+
this.parseBlockAttribute(model);
|
|
372
|
+
pendingDoc = [];
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const field = this.parseField(
|
|
376
|
+
pendingDoc.length ? pendingDoc.join("\n") : void 0
|
|
377
|
+
);
|
|
378
|
+
pendingDoc = [];
|
|
379
|
+
model.fields.push(field);
|
|
380
|
+
if (field.isId && !model.primaryKey.includes(field.name)) {
|
|
381
|
+
model.primaryKey.push(field.name);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
this.expect("rbrace");
|
|
385
|
+
return model;
|
|
386
|
+
}
|
|
387
|
+
parseField(documentation) {
|
|
388
|
+
const name = this.expect("identifier").value;
|
|
389
|
+
const typeName = this.expect("identifier").value;
|
|
390
|
+
let isList = false;
|
|
391
|
+
let isRequired = true;
|
|
392
|
+
while (this.check("lbracket") || this.check("question")) {
|
|
393
|
+
if (this.check("lbracket")) {
|
|
394
|
+
this.advance();
|
|
395
|
+
this.expect("rbracket");
|
|
396
|
+
isList = true;
|
|
397
|
+
} else {
|
|
398
|
+
this.advance();
|
|
399
|
+
isRequired = false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const field = {
|
|
403
|
+
name,
|
|
404
|
+
type: typeName,
|
|
405
|
+
kind: "scalar",
|
|
406
|
+
// resolved later
|
|
407
|
+
isList,
|
|
408
|
+
isRequired,
|
|
409
|
+
isId: false,
|
|
410
|
+
isUnique: false,
|
|
411
|
+
isUpdatedAt: false,
|
|
412
|
+
documentation
|
|
413
|
+
};
|
|
414
|
+
while (this.check("at")) {
|
|
415
|
+
const attr = this.parseAttribute();
|
|
416
|
+
this.applyFieldAttribute(field, attr);
|
|
417
|
+
}
|
|
418
|
+
return field;
|
|
419
|
+
}
|
|
420
|
+
applyFieldAttribute(field, attr) {
|
|
421
|
+
if (attr.name.startsWith("db.")) {
|
|
422
|
+
field.nativeType = toNativeType(attr);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
switch (attr.name) {
|
|
426
|
+
case "id":
|
|
427
|
+
field.isId = true;
|
|
428
|
+
break;
|
|
429
|
+
case "unique":
|
|
430
|
+
field.isUnique = true;
|
|
431
|
+
break;
|
|
432
|
+
case "updatedAt":
|
|
433
|
+
field.isUpdatedAt = true;
|
|
434
|
+
break;
|
|
435
|
+
case "default":
|
|
436
|
+
field.default = toDefaultValue(attr.args[0]);
|
|
437
|
+
break;
|
|
438
|
+
case "map":
|
|
439
|
+
field.dbName = literalString(attr.args[0]);
|
|
440
|
+
break;
|
|
441
|
+
case "relation":
|
|
442
|
+
field.relation = toRelationInfo(attr.args);
|
|
443
|
+
break;
|
|
444
|
+
default:
|
|
445
|
+
throw this.error(`Unknown field attribute '@${attr.name}'`, attr);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
parseBlockAttribute(model) {
|
|
449
|
+
const start = this.expect("double_at");
|
|
450
|
+
const name = this.expect("identifier").value;
|
|
451
|
+
const args = this.check("lparen") ? this.parseArgList() : [];
|
|
452
|
+
switch (name) {
|
|
453
|
+
case "id":
|
|
454
|
+
model.primaryKey = fieldNameList(args);
|
|
455
|
+
break;
|
|
456
|
+
case "unique":
|
|
457
|
+
model.uniqueIndexes.push({
|
|
458
|
+
fields: fieldNameList(args),
|
|
459
|
+
name: namedArg(args, "map")
|
|
460
|
+
});
|
|
461
|
+
break;
|
|
462
|
+
case "index":
|
|
463
|
+
model.indexes.push({
|
|
464
|
+
fields: fieldNameList(args),
|
|
465
|
+
name: namedArg(args, "map"),
|
|
466
|
+
unique: false
|
|
467
|
+
});
|
|
468
|
+
break;
|
|
469
|
+
case "map":
|
|
470
|
+
model.dbName = literalString(args[0]);
|
|
471
|
+
break;
|
|
472
|
+
default:
|
|
473
|
+
throw this.error(`Unknown block attribute '@@${name}'`, start);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
parseEnum(documentation) {
|
|
477
|
+
this.expectKeyword("enum");
|
|
478
|
+
const name = this.expect("identifier").value;
|
|
479
|
+
this.expect("lbrace");
|
|
480
|
+
const node = { name, values: [], documentation };
|
|
481
|
+
while (!this.check("rbrace") && !this.isEof()) {
|
|
482
|
+
if (this.check("doc_comment")) {
|
|
483
|
+
this.advance();
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (this.check("double_at")) {
|
|
487
|
+
this.advance();
|
|
488
|
+
const attrName = this.expect("identifier").value;
|
|
489
|
+
const args = this.check("lparen") ? this.parseArgList() : [];
|
|
490
|
+
if (attrName === "map") node.dbName = literalString(args[0]);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const valueName = this.expect("identifier").value;
|
|
494
|
+
let dbName;
|
|
495
|
+
while (this.check("at")) {
|
|
496
|
+
const attr = this.parseAttribute();
|
|
497
|
+
if (attr.name === "map") dbName = literalString(attr.args[0]);
|
|
498
|
+
}
|
|
499
|
+
node.values.push({ name: valueName, dbName });
|
|
500
|
+
}
|
|
501
|
+
this.expect("rbrace");
|
|
502
|
+
return node;
|
|
503
|
+
}
|
|
504
|
+
// ---- Attributes & values ---------------------------------------------
|
|
505
|
+
parseAttribute() {
|
|
506
|
+
const at = this.expect("at");
|
|
507
|
+
let name = this.expect("identifier").value;
|
|
508
|
+
if (this.check("dot")) {
|
|
509
|
+
this.advance();
|
|
510
|
+
name += "." + this.expect("identifier").value;
|
|
511
|
+
}
|
|
512
|
+
const args = this.check("lparen") ? this.parseArgList() : [];
|
|
513
|
+
return { name, args, line: at.line, column: at.column };
|
|
514
|
+
}
|
|
515
|
+
parseArgList() {
|
|
516
|
+
this.expect("lparen");
|
|
517
|
+
const args = [];
|
|
518
|
+
while (!this.check("rparen") && !this.isEof()) {
|
|
519
|
+
args.push(this.parseArg());
|
|
520
|
+
if (this.check("comma")) this.advance();
|
|
521
|
+
}
|
|
522
|
+
this.expect("rparen");
|
|
523
|
+
return args;
|
|
524
|
+
}
|
|
525
|
+
/** Handles both positional values and `name: value` named arguments. */
|
|
526
|
+
parseArg() {
|
|
527
|
+
if (this.check("identifier") && this.peek(1)?.type === "colon") {
|
|
528
|
+
const key = this.expect("identifier").value;
|
|
529
|
+
this.expect("colon");
|
|
530
|
+
const value = this.parseValue();
|
|
531
|
+
return { kind: "function", name: `__named:${key}`, args: [value] };
|
|
532
|
+
}
|
|
533
|
+
return this.parseValue();
|
|
534
|
+
}
|
|
535
|
+
parseValue() {
|
|
536
|
+
const tok = this.peek();
|
|
537
|
+
switch (tok.type) {
|
|
538
|
+
case "string":
|
|
539
|
+
this.advance();
|
|
540
|
+
return { kind: "string", value: tok.value };
|
|
541
|
+
case "number":
|
|
542
|
+
this.advance();
|
|
543
|
+
return { kind: "number", value: Number(tok.value) };
|
|
544
|
+
case "lbracket": {
|
|
545
|
+
this.advance();
|
|
546
|
+
const items = [];
|
|
547
|
+
while (!this.check("rbracket") && !this.isEof()) {
|
|
548
|
+
items.push(this.parseValue());
|
|
549
|
+
if (this.check("comma")) this.advance();
|
|
550
|
+
}
|
|
551
|
+
this.expect("rbracket");
|
|
552
|
+
return { kind: "array", items };
|
|
553
|
+
}
|
|
554
|
+
case "identifier": {
|
|
555
|
+
this.advance();
|
|
556
|
+
if (tok.value === "true" || tok.value === "false") {
|
|
557
|
+
return { kind: "boolean", value: tok.value === "true" };
|
|
558
|
+
}
|
|
559
|
+
if (this.check("lparen")) {
|
|
560
|
+
const args = this.parseArgList();
|
|
561
|
+
return { kind: "function", name: tok.value, args };
|
|
562
|
+
}
|
|
563
|
+
return { kind: "ref", value: tok.value };
|
|
564
|
+
}
|
|
565
|
+
default:
|
|
566
|
+
throw this.error(`Unexpected value token '${tok.value}'`, tok);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// ---- Token helpers ----------------------------------------------------
|
|
570
|
+
peek(offset = 0) {
|
|
571
|
+
return this.tokens[this.pos + offset] ?? this.tokens[this.tokens.length - 1];
|
|
572
|
+
}
|
|
573
|
+
advance() {
|
|
574
|
+
const tok = this.tokens[this.pos];
|
|
575
|
+
if (this.pos < this.tokens.length - 1) this.pos++;
|
|
576
|
+
return tok;
|
|
577
|
+
}
|
|
578
|
+
check(type) {
|
|
579
|
+
return this.peek().type === type;
|
|
580
|
+
}
|
|
581
|
+
isEof() {
|
|
582
|
+
return this.peek().type === "eof";
|
|
583
|
+
}
|
|
584
|
+
expect(type) {
|
|
585
|
+
const tok = this.peek();
|
|
586
|
+
if (tok.type !== type) {
|
|
587
|
+
throw this.error(`Expected ${type} but found '${tok.value}' (${tok.type})`, tok);
|
|
588
|
+
}
|
|
589
|
+
return this.advance();
|
|
590
|
+
}
|
|
591
|
+
expectKeyword(keyword) {
|
|
592
|
+
const tok = this.peek();
|
|
593
|
+
if (tok.type !== "identifier" || tok.value !== keyword) {
|
|
594
|
+
throw this.error(`Expected keyword '${keyword}'`, tok);
|
|
595
|
+
}
|
|
596
|
+
return this.advance();
|
|
597
|
+
}
|
|
598
|
+
error(message, at) {
|
|
599
|
+
return new SchemaParseError(message, at.line, at.column, this.file);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
function literalString(v) {
|
|
603
|
+
if (!v) return "";
|
|
604
|
+
if (v.kind === "string") return v.value;
|
|
605
|
+
if (v.kind === "ref") return v.value;
|
|
606
|
+
return String("value" in v ? v.value : "");
|
|
607
|
+
}
|
|
608
|
+
function toNativeType(attr) {
|
|
609
|
+
const name = attr.name.slice("db.".length);
|
|
610
|
+
const args = attr.args.filter((a) => a.kind === "number").map((a) => a.value);
|
|
611
|
+
return { name, args };
|
|
612
|
+
}
|
|
613
|
+
function toDefaultValue(v) {
|
|
614
|
+
if (!v) return {};
|
|
615
|
+
switch (v.kind) {
|
|
616
|
+
case "function":
|
|
617
|
+
return { function: { name: v.name, args: v.args } };
|
|
618
|
+
case "string":
|
|
619
|
+
return { literal: v.value };
|
|
620
|
+
case "number":
|
|
621
|
+
return { literal: v.value };
|
|
622
|
+
case "boolean":
|
|
623
|
+
return { literal: v.value };
|
|
624
|
+
case "ref":
|
|
625
|
+
return { literal: v.value };
|
|
626
|
+
default:
|
|
627
|
+
return {};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
function toRelationInfo(args) {
|
|
631
|
+
const info = {};
|
|
632
|
+
for (const arg of args) {
|
|
633
|
+
if (arg.kind === "string") {
|
|
634
|
+
info.name = arg.value;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const named = asNamed(arg);
|
|
638
|
+
if (!named) continue;
|
|
639
|
+
const [key, value] = named;
|
|
640
|
+
if (key === "name" && value.kind === "string") info.name = value.value;
|
|
641
|
+
if (key === "fields") info.fields = refArray(value);
|
|
642
|
+
if (key === "references") info.references = refArray(value);
|
|
643
|
+
if (key === "onDelete") info.onDelete = refName(value);
|
|
644
|
+
if (key === "onUpdate") info.onUpdate = refName(value);
|
|
645
|
+
}
|
|
646
|
+
return info;
|
|
647
|
+
}
|
|
648
|
+
function asNamed(arg) {
|
|
649
|
+
if (arg.kind === "function" && arg.name.startsWith("__named:")) {
|
|
650
|
+
return [arg.name.slice("__named:".length), arg.args[0]];
|
|
651
|
+
}
|
|
652
|
+
return void 0;
|
|
653
|
+
}
|
|
654
|
+
function namedArg(args, key) {
|
|
655
|
+
for (const arg of args) {
|
|
656
|
+
const named = asNamed(arg);
|
|
657
|
+
if (named && named[0] === key && named[1].kind === "string") {
|
|
658
|
+
return named[1].value;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return void 0;
|
|
662
|
+
}
|
|
663
|
+
function refArray(v) {
|
|
664
|
+
if (v.kind === "array") {
|
|
665
|
+
return v.items.map((i) => refName(i)).filter((s) => !!s);
|
|
666
|
+
}
|
|
667
|
+
const single = refName(v);
|
|
668
|
+
return single ? [single] : [];
|
|
669
|
+
}
|
|
670
|
+
function refName(v) {
|
|
671
|
+
if (v.kind === "ref") return v.value;
|
|
672
|
+
if (v.kind === "string") return v.value;
|
|
673
|
+
return void 0;
|
|
674
|
+
}
|
|
675
|
+
function fieldNameList(args) {
|
|
676
|
+
for (const arg of args) {
|
|
677
|
+
if (arg.kind === "array") return refArray(arg);
|
|
678
|
+
const named = asNamed(arg);
|
|
679
|
+
if (named && named[0] === "fields") return refArray(named[1]);
|
|
680
|
+
}
|
|
681
|
+
return args.map((a) => refName(a)).filter((s) => !!s);
|
|
682
|
+
}
|
|
683
|
+
function resolveKinds(doc) {
|
|
684
|
+
const modelNames = new Set(doc.models.map((m) => m.name));
|
|
685
|
+
const enumNames = new Set(doc.enums.map((e) => e.name));
|
|
686
|
+
for (const model of doc.models) {
|
|
687
|
+
for (const field of model.fields) {
|
|
688
|
+
if (modelNames.has(field.type)) field.kind = "object";
|
|
689
|
+
else if (enumNames.has(field.type)) field.kind = "enum";
|
|
690
|
+
else field.kind = "scalar";
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/ast/index.ts
|
|
696
|
+
var SCALAR_TYPES = [
|
|
697
|
+
"String",
|
|
698
|
+
"Boolean",
|
|
699
|
+
"Int",
|
|
700
|
+
"BigInt",
|
|
701
|
+
"Float",
|
|
702
|
+
"Decimal",
|
|
703
|
+
"DateTime",
|
|
704
|
+
"Bytes",
|
|
705
|
+
"Json"
|
|
706
|
+
];
|
|
707
|
+
function findModel(schema, name) {
|
|
708
|
+
return schema.models.find((m) => m.name === name);
|
|
709
|
+
}
|
|
710
|
+
function fieldColumn(field) {
|
|
711
|
+
return field.dbName ?? field.name.toUpperCase();
|
|
712
|
+
}
|
|
713
|
+
function modelTable(model) {
|
|
714
|
+
return model.dbName ?? model.name.toUpperCase();
|
|
715
|
+
}
|
|
716
|
+
function scalarFields(model) {
|
|
717
|
+
return model.fields.filter((f) => f.kind !== "object");
|
|
718
|
+
}
|
|
719
|
+
function relationFields(model) {
|
|
720
|
+
return model.fields.filter((f) => f.kind === "object");
|
|
721
|
+
}
|
|
722
|
+
function idFields(model) {
|
|
723
|
+
if (model.primaryKey.length > 0) {
|
|
724
|
+
return model.primaryKey.map((n) => model.fields.find((f) => f.name === n)).filter((f) => !!f);
|
|
725
|
+
}
|
|
726
|
+
return model.fields.filter((f) => f.isId);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/schema/validator.ts
|
|
730
|
+
function validateSchema(doc) {
|
|
731
|
+
const errors = [];
|
|
732
|
+
const scalar = new Set(SCALAR_TYPES);
|
|
733
|
+
const enumNames = new Set(doc.enums.map((e) => e.name));
|
|
734
|
+
const modelNames = new Set(doc.models.map((m) => m.name));
|
|
735
|
+
const duplicateModels = findDuplicates(doc.models.map((m) => m.name));
|
|
736
|
+
for (const name of duplicateModels) {
|
|
737
|
+
errors.push(`Duplicate model '${name}'.`);
|
|
738
|
+
}
|
|
739
|
+
for (const model of doc.models) {
|
|
740
|
+
const fieldNames = /* @__PURE__ */ new Set();
|
|
741
|
+
for (const field of model.fields) {
|
|
742
|
+
if (fieldNames.has(field.name)) {
|
|
743
|
+
errors.push(`Duplicate field '${model.name}.${field.name}'.`);
|
|
744
|
+
}
|
|
745
|
+
fieldNames.add(field.name);
|
|
746
|
+
const known = scalar.has(field.type) || enumNames.has(field.type) || modelNames.has(field.type);
|
|
747
|
+
if (!known) {
|
|
748
|
+
errors.push(
|
|
749
|
+
`Field '${model.name}.${field.name}' has unknown type '${field.type}'.`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
if (field.kind === "object" && field.relation) {
|
|
753
|
+
validateRelation(doc, model.name, field.name, field.relation, errors);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
for (const pk of model.primaryKey) {
|
|
757
|
+
if (!fieldNames.has(pk)) {
|
|
758
|
+
errors.push(
|
|
759
|
+
`Primary key field '${pk}' does not exist on model '${model.name}'.`
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const hasId = model.primaryKey.length > 0 || model.fields.some((f) => f.isId);
|
|
764
|
+
if (!hasId) {
|
|
765
|
+
errors.push(
|
|
766
|
+
`Model '${model.name}' has no @id / @@id. Every model needs a primary key.`
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
for (const enumNode of doc.enums) {
|
|
771
|
+
if (enumNode.values.length === 0) {
|
|
772
|
+
errors.push(`Enum '${enumNode.name}' has no values.`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (errors.length > 0) {
|
|
776
|
+
throw new SchemaValidationError("Schema validation failed", errors);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function validateRelation(doc, modelName, fieldName, relation, errors) {
|
|
780
|
+
const model = findModel(doc, modelName);
|
|
781
|
+
if (!model) return;
|
|
782
|
+
for (const f of relation.fields ?? []) {
|
|
783
|
+
if (!model.fields.some((mf) => mf.name === f)) {
|
|
784
|
+
errors.push(
|
|
785
|
+
`Relation '${modelName}.${fieldName}' references local field '${f}' which does not exist.`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function findDuplicates(values) {
|
|
791
|
+
const seen = /* @__PURE__ */ new Set();
|
|
792
|
+
const dups = /* @__PURE__ */ new Set();
|
|
793
|
+
for (const v of values) {
|
|
794
|
+
if (seen.has(v)) dups.add(v);
|
|
795
|
+
seen.add(v);
|
|
796
|
+
}
|
|
797
|
+
return [...dups];
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/schema/printer.ts
|
|
801
|
+
function printSchema(doc) {
|
|
802
|
+
const blocks = [];
|
|
803
|
+
if (doc.datasource) {
|
|
804
|
+
const ds = doc.datasource;
|
|
805
|
+
const url = ds.url.kind === "env" ? `env("${ds.url.value}")` : `"${ds.url.value}"`;
|
|
806
|
+
blocks.push(
|
|
807
|
+
`datasource ${ds.name} {
|
|
808
|
+
provider = "${ds.provider}"
|
|
809
|
+
url = ${url}
|
|
810
|
+
}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
for (const gen of doc.generators) {
|
|
814
|
+
const lines = [` provider = "${gen.provider}"`];
|
|
815
|
+
if (gen.output) lines.push(` output = "${gen.output}"`);
|
|
816
|
+
for (const [k, v] of Object.entries(gen.config)) {
|
|
817
|
+
if (k === "provider" || k === "output") continue;
|
|
818
|
+
lines.push(` ${k} = "${v}"`);
|
|
819
|
+
}
|
|
820
|
+
blocks.push(`generator ${gen.name} {
|
|
821
|
+
${lines.join("\n")}
|
|
822
|
+
}`);
|
|
823
|
+
}
|
|
824
|
+
for (const enumNode of doc.enums) {
|
|
825
|
+
blocks.push(printEnum(enumNode));
|
|
826
|
+
}
|
|
827
|
+
for (const model of doc.models) {
|
|
828
|
+
blocks.push(printModel(model));
|
|
829
|
+
}
|
|
830
|
+
return blocks.join("\n\n") + "\n";
|
|
831
|
+
}
|
|
832
|
+
function printEnum(node) {
|
|
833
|
+
const lines = [];
|
|
834
|
+
if (node.documentation) lines.push(...docLines(node.documentation));
|
|
835
|
+
lines.push(`enum ${node.name} {`);
|
|
836
|
+
for (const v of node.values) {
|
|
837
|
+
lines.push(` ${v.name}${v.dbName ? ` @map("${v.dbName}")` : ""}`);
|
|
838
|
+
}
|
|
839
|
+
if (node.dbName) lines.push(`
|
|
840
|
+
@@map("${node.dbName}")`);
|
|
841
|
+
lines.push(`}`);
|
|
842
|
+
return lines.join("\n");
|
|
843
|
+
}
|
|
844
|
+
function printModel(model) {
|
|
845
|
+
const lines = [];
|
|
846
|
+
if (model.documentation) lines.push(...docLines(model.documentation));
|
|
847
|
+
lines.push(`model ${model.name} {`);
|
|
848
|
+
const nameWidth = Math.max(...model.fields.map((f) => f.name.length), 0);
|
|
849
|
+
const typeWidth = Math.max(...model.fields.map((f) => fieldType(f).length), 0);
|
|
850
|
+
for (const field of model.fields) {
|
|
851
|
+
if (field.documentation) {
|
|
852
|
+
lines.push(...docLines(field.documentation).map((l) => ` ${l}`));
|
|
853
|
+
}
|
|
854
|
+
const attrs = fieldAttributes(field);
|
|
855
|
+
const name = field.name.padEnd(nameWidth);
|
|
856
|
+
const type = fieldType(field).padEnd(typeWidth);
|
|
857
|
+
lines.push(` ${name} ${type}${attrs ? ` ${attrs}` : ""}`.trimEnd());
|
|
858
|
+
}
|
|
859
|
+
const blockAttrs = modelBlockAttributes(model);
|
|
860
|
+
if (blockAttrs.length > 0) {
|
|
861
|
+
lines.push("");
|
|
862
|
+
for (const a of blockAttrs) lines.push(` ${a}`);
|
|
863
|
+
}
|
|
864
|
+
lines.push(`}`);
|
|
865
|
+
return lines.join("\n");
|
|
866
|
+
}
|
|
867
|
+
function fieldType(field) {
|
|
868
|
+
let t = field.type;
|
|
869
|
+
if (field.isList) t += "[]";
|
|
870
|
+
else if (!field.isRequired) t += "?";
|
|
871
|
+
return t;
|
|
872
|
+
}
|
|
873
|
+
function fieldAttributes(field) {
|
|
874
|
+
const parts = [];
|
|
875
|
+
if (field.isId) parts.push("@id");
|
|
876
|
+
if (field.isUnique) parts.push("@unique");
|
|
877
|
+
if (field.default) parts.push(`@default(${printDefault(field.default)})`);
|
|
878
|
+
if (field.isUpdatedAt) parts.push("@updatedAt");
|
|
879
|
+
if (field.relation) {
|
|
880
|
+
const rel2 = field.relation;
|
|
881
|
+
const args = [];
|
|
882
|
+
if (rel2.name) args.push(`"${rel2.name}"`);
|
|
883
|
+
if (rel2.fields?.length) args.push(`fields: [${rel2.fields.join(", ")}]`);
|
|
884
|
+
if (rel2.references?.length)
|
|
885
|
+
args.push(`references: [${rel2.references.join(", ")}]`);
|
|
886
|
+
if (rel2.onDelete) args.push(`onDelete: ${rel2.onDelete}`);
|
|
887
|
+
if (rel2.onUpdate) args.push(`onUpdate: ${rel2.onUpdate}`);
|
|
888
|
+
parts.push(args.length ? `@relation(${args.join(", ")})` : "@relation");
|
|
889
|
+
}
|
|
890
|
+
if (field.nativeType) {
|
|
891
|
+
const a = field.nativeType.args.length ? `(${field.nativeType.args.join(", ")})` : "";
|
|
892
|
+
parts.push(`@db.${field.nativeType.name}${a}`);
|
|
893
|
+
}
|
|
894
|
+
if (field.dbName) parts.push(`@map("${field.dbName}")`);
|
|
895
|
+
return parts.join(" ");
|
|
896
|
+
}
|
|
897
|
+
function printDefault(def) {
|
|
898
|
+
if (def.function) {
|
|
899
|
+
return `${def.function.name}(${def.function.args.map(printArgValue).join(", ")})`;
|
|
900
|
+
}
|
|
901
|
+
if (typeof def.literal === "string") {
|
|
902
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(def.literal) ? def.literal : `"${def.literal}"`;
|
|
903
|
+
}
|
|
904
|
+
return String(def.literal);
|
|
905
|
+
}
|
|
906
|
+
function printArgValue(v) {
|
|
907
|
+
switch (v.kind) {
|
|
908
|
+
case "string":
|
|
909
|
+
return `"${v.value}"`;
|
|
910
|
+
case "number":
|
|
911
|
+
return String(v.value);
|
|
912
|
+
case "boolean":
|
|
913
|
+
return String(v.value);
|
|
914
|
+
case "ref":
|
|
915
|
+
return v.value;
|
|
916
|
+
case "array":
|
|
917
|
+
return `[${v.items.map(printArgValue).join(", ")}]`;
|
|
918
|
+
case "function":
|
|
919
|
+
return `${v.name}(${v.args.map(printArgValue).join(", ")})`;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function modelBlockAttributes(model) {
|
|
923
|
+
const out = [];
|
|
924
|
+
const inlineId = model.primaryKey.length === 1 && model.fields.find((f) => f.name === model.primaryKey[0])?.isId;
|
|
925
|
+
if (model.primaryKey.length > 0 && !inlineId) {
|
|
926
|
+
out.push(`@@id([${model.primaryKey.join(", ")}])`);
|
|
927
|
+
}
|
|
928
|
+
for (const u of model.uniqueIndexes) {
|
|
929
|
+
out.push(
|
|
930
|
+
`@@unique([${u.fields.join(", ")}]${u.name ? `, map: "${u.name}"` : ""})`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
for (const i of model.indexes) {
|
|
934
|
+
out.push(
|
|
935
|
+
`@@index([${i.fields.join(", ")}]${i.name ? `, map: "${i.name}"` : ""})`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
if (model.dbName) out.push(`@@map("${model.dbName}")`);
|
|
939
|
+
return out;
|
|
940
|
+
}
|
|
941
|
+
function docLines(documentation) {
|
|
942
|
+
return documentation.split("\n").map((l) => `/// ${l}`);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/utils/index.ts
|
|
946
|
+
function uniq(items) {
|
|
947
|
+
return [...new Set(items)];
|
|
948
|
+
}
|
|
949
|
+
function pascalCase(input) {
|
|
950
|
+
return input.toLowerCase().replace(
|
|
951
|
+
/[^a-zA-Z0-9]+(.)?/g,
|
|
952
|
+
(_, c) => c ? c.toUpperCase() : ""
|
|
953
|
+
).replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
954
|
+
}
|
|
955
|
+
function camelCase(input) {
|
|
956
|
+
const pascal = pascalCase(input);
|
|
957
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
958
|
+
}
|
|
959
|
+
function pluralize(word) {
|
|
960
|
+
if (/[^aeiou]y$/i.test(word)) return word.replace(/y$/i, "ies");
|
|
961
|
+
if (/(s|x|z|ch|sh)$/i.test(word)) return `${word}es`;
|
|
962
|
+
return `${word}s`;
|
|
963
|
+
}
|
|
964
|
+
function lowerFirst(input) {
|
|
965
|
+
return input.charAt(0).toLowerCase() + input.slice(1);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// src/schema/relations-complete.ts
|
|
969
|
+
function completeRelations(doc) {
|
|
970
|
+
const original = [];
|
|
971
|
+
for (const model of doc.models) {
|
|
972
|
+
for (const field of model.fields) {
|
|
973
|
+
if (field.kind === "object") original.push({ model, field });
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
for (const { model, field } of original) {
|
|
977
|
+
const related = findModel(doc, field.type);
|
|
978
|
+
if (!related) continue;
|
|
979
|
+
if (hasPartner(related, model, field)) continue;
|
|
980
|
+
if (field.isList) {
|
|
981
|
+
addOwningSide(related, model, field);
|
|
982
|
+
} else if (field.relation?.fields?.length) {
|
|
983
|
+
addBackRelation(related, model, field);
|
|
984
|
+
} else {
|
|
985
|
+
upgradeBareToOne(doc, model, field);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return doc;
|
|
989
|
+
}
|
|
990
|
+
function hasPartner(related, model, field) {
|
|
991
|
+
return related.fields.some(
|
|
992
|
+
(f) => f !== field && f.kind === "object" && f.type === model.name && relationNamesMatch(f, field)
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
function relationNamesMatch(a, b) {
|
|
996
|
+
const an = a.relation?.name;
|
|
997
|
+
const bn = b.relation?.name;
|
|
998
|
+
if (an && bn) return an === bn;
|
|
999
|
+
if (!an && !bn) return true;
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
function addBackRelation(related, owner, owning) {
|
|
1003
|
+
const fkFields = (owning.relation?.fields ?? []).map((n) => owner.fields.find((f) => f.name === n)).filter((f) => !!f);
|
|
1004
|
+
const isOneToOne = fkFields.length > 0 && fkFields.every((f) => f.isUnique);
|
|
1005
|
+
const baseName = isOneToOne ? camelCase(owner.name) : camelCase(pluralize(owner.name));
|
|
1006
|
+
const name = uniqueFieldName(related, baseName);
|
|
1007
|
+
related.fields.push({
|
|
1008
|
+
name,
|
|
1009
|
+
type: owner.name,
|
|
1010
|
+
kind: "object",
|
|
1011
|
+
isList: !isOneToOne,
|
|
1012
|
+
isRequired: false,
|
|
1013
|
+
isId: false,
|
|
1014
|
+
isUnique: false,
|
|
1015
|
+
isUpdatedAt: false,
|
|
1016
|
+
// Only emit @relation when the relation is named; an unnamed back-relation
|
|
1017
|
+
// carries no attribute (matches Prisma's formatter output).
|
|
1018
|
+
...owning.relation?.name ? { relation: { name: owning.relation.name } } : {}
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
function addOwningSide(related, parent, listField) {
|
|
1022
|
+
const ref = idFields(parent)[0];
|
|
1023
|
+
if (!ref) return;
|
|
1024
|
+
const relationField = uniqueFieldName(related, camelCase(parent.name));
|
|
1025
|
+
const fkName = uniqueFieldName(
|
|
1026
|
+
related,
|
|
1027
|
+
`${camelCase(parent.name)}${pascalCase(ref.name)}`
|
|
1028
|
+
);
|
|
1029
|
+
related.fields.push({
|
|
1030
|
+
name: fkName,
|
|
1031
|
+
type: ref.type,
|
|
1032
|
+
kind: "scalar",
|
|
1033
|
+
isList: false,
|
|
1034
|
+
isRequired: true,
|
|
1035
|
+
isId: false,
|
|
1036
|
+
isUnique: false,
|
|
1037
|
+
isUpdatedAt: false,
|
|
1038
|
+
...ref.nativeType ? { nativeType: ref.nativeType } : {}
|
|
1039
|
+
});
|
|
1040
|
+
related.fields.push({
|
|
1041
|
+
name: relationField,
|
|
1042
|
+
type: parent.name,
|
|
1043
|
+
kind: "object",
|
|
1044
|
+
isList: false,
|
|
1045
|
+
isRequired: true,
|
|
1046
|
+
isId: false,
|
|
1047
|
+
isUnique: false,
|
|
1048
|
+
isUpdatedAt: false,
|
|
1049
|
+
relation: {
|
|
1050
|
+
...listField.relation?.name ? { name: listField.relation.name } : {},
|
|
1051
|
+
fields: [fkName],
|
|
1052
|
+
references: [ref.name]
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
function upgradeBareToOne(doc, model, field) {
|
|
1057
|
+
const related = findModel(doc, field.type);
|
|
1058
|
+
if (!related) return;
|
|
1059
|
+
const ref = idFields(related)[0];
|
|
1060
|
+
if (!ref) return;
|
|
1061
|
+
const fkName = uniqueFieldName(model, `${field.name}${pascalCase(ref.name)}`);
|
|
1062
|
+
const idx = model.fields.indexOf(field);
|
|
1063
|
+
model.fields.splice(idx, 0, {
|
|
1064
|
+
name: fkName,
|
|
1065
|
+
type: ref.type,
|
|
1066
|
+
kind: "scalar",
|
|
1067
|
+
isList: false,
|
|
1068
|
+
isRequired: field.isRequired,
|
|
1069
|
+
isId: false,
|
|
1070
|
+
isUnique: false,
|
|
1071
|
+
isUpdatedAt: false,
|
|
1072
|
+
...ref.nativeType ? { nativeType: ref.nativeType } : {}
|
|
1073
|
+
});
|
|
1074
|
+
field.relation = {
|
|
1075
|
+
...field.relation ?? {},
|
|
1076
|
+
fields: [fkName],
|
|
1077
|
+
references: [ref.name]
|
|
1078
|
+
};
|
|
1079
|
+
addBackRelation(related, model, field);
|
|
1080
|
+
}
|
|
1081
|
+
function uniqueFieldName(model, base) {
|
|
1082
|
+
let name = base || "relation";
|
|
1083
|
+
let i = 1;
|
|
1084
|
+
while (model.fields.some((f) => f.name === name)) name = `${base}_${++i}`;
|
|
1085
|
+
return name;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/schema/index.ts
|
|
1089
|
+
function formatSchema(source, file) {
|
|
1090
|
+
const doc = completeRelations(parseSchema(source, file));
|
|
1091
|
+
validateSchema(doc);
|
|
1092
|
+
return printSchema(doc);
|
|
1093
|
+
}
|
|
1094
|
+
function parseSchema(source, file) {
|
|
1095
|
+
return new Parser(source, file).parse();
|
|
1096
|
+
}
|
|
1097
|
+
function parseAndValidate(source, file) {
|
|
1098
|
+
const doc = parseSchema(source, file);
|
|
1099
|
+
validateSchema(doc);
|
|
1100
|
+
return doc;
|
|
1101
|
+
}
|
|
1102
|
+
var DEFAULT_SCHEMA_PATHS = [
|
|
1103
|
+
"ember/schema.ember",
|
|
1104
|
+
"schema.ember",
|
|
1105
|
+
"prisma/schema.ember"
|
|
1106
|
+
];
|
|
1107
|
+
function findSchemaPath(base = process.cwd(), explicit) {
|
|
1108
|
+
if (explicit) {
|
|
1109
|
+
const p = resolve(base, explicit);
|
|
1110
|
+
return existsSync(p) ? p : void 0;
|
|
1111
|
+
}
|
|
1112
|
+
for (const candidate of DEFAULT_SCHEMA_PATHS) {
|
|
1113
|
+
const p = resolve(base, candidate);
|
|
1114
|
+
if (existsSync(p)) return p;
|
|
1115
|
+
}
|
|
1116
|
+
return void 0;
|
|
1117
|
+
}
|
|
1118
|
+
function loadSchema(path) {
|
|
1119
|
+
const source = readFileSync(path, "utf8");
|
|
1120
|
+
const document = parseAndValidate(source, path);
|
|
1121
|
+
return {
|
|
1122
|
+
document,
|
|
1123
|
+
path,
|
|
1124
|
+
databaseUrl: resolveDatasourceUrl(document, dirname(path))
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function resolveDatasourceUrl(doc, _base) {
|
|
1128
|
+
const ds = doc.datasource;
|
|
1129
|
+
if (!ds) return void 0;
|
|
1130
|
+
if (ds.url.kind === "literal") return ds.url.value;
|
|
1131
|
+
return process.env[ds.url.value];
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// src/driver/firebird-driver.ts
|
|
1135
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
1136
|
+
import Firebird from "node-firebird";
|
|
1137
|
+
var fb = Firebird;
|
|
1138
|
+
function isolationConstant(level) {
|
|
1139
|
+
switch (level) {
|
|
1140
|
+
case "READ_COMMITTED_READ_ONLY":
|
|
1141
|
+
return fb.ISOLATION_READ_COMMITTED_READ_ONLY;
|
|
1142
|
+
case "REPEATABLE_READ":
|
|
1143
|
+
return fb.ISOLATION_REPEATABLE_READ;
|
|
1144
|
+
case "SERIALIZABLE":
|
|
1145
|
+
return fb.ISOLATION_SERIALIZABLE;
|
|
1146
|
+
case "READ_COMMITTED":
|
|
1147
|
+
default:
|
|
1148
|
+
return fb.ISOLATION_READ_COMMITTED;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
var FirebirdDriver = class {
|
|
1152
|
+
pool = null;
|
|
1153
|
+
options;
|
|
1154
|
+
poolMax;
|
|
1155
|
+
onQuery;
|
|
1156
|
+
activeTx = new AsyncLocalStorage();
|
|
1157
|
+
constructor(config, driverOptions) {
|
|
1158
|
+
this.poolMax = config.poolMax ?? 5;
|
|
1159
|
+
this.onQuery = driverOptions?.onQuery;
|
|
1160
|
+
this.options = {
|
|
1161
|
+
host: config.host,
|
|
1162
|
+
port: config.port,
|
|
1163
|
+
database: config.database,
|
|
1164
|
+
user: config.user,
|
|
1165
|
+
password: config.password,
|
|
1166
|
+
role: config.role ?? "",
|
|
1167
|
+
pageSize: config.pageSize ?? 4096,
|
|
1168
|
+
encoding: config.encoding ?? "UTF8",
|
|
1169
|
+
blobAsText: config.blobAsText ?? true,
|
|
1170
|
+
lowercase_keys: config.lowercaseKeys ?? false,
|
|
1171
|
+
retryConnectionInterval: 1e3,
|
|
1172
|
+
// FB3+ secure auth (Srp) is negotiated by default; set explicitly to force
|
|
1173
|
+
// a plugin, or "Legacy_Auth" for Firebird 2.1/2.5 servers.
|
|
1174
|
+
...config.authPlugin ? { pluginName: config.authPlugin } : {},
|
|
1175
|
+
...config.wireCompression != null ? { wireCompression: config.wireCompression } : {}
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
async connect() {
|
|
1179
|
+
if (this.pool) return;
|
|
1180
|
+
this.pool = fb.pool(this.poolMax, this.options);
|
|
1181
|
+
}
|
|
1182
|
+
async disconnect() {
|
|
1183
|
+
if (!this.pool) return;
|
|
1184
|
+
this.pool.destroy();
|
|
1185
|
+
this.pool = null;
|
|
1186
|
+
}
|
|
1187
|
+
async transaction(fn, options) {
|
|
1188
|
+
const existing = this.activeTx.getStore();
|
|
1189
|
+
if (existing) {
|
|
1190
|
+
return fn(existing);
|
|
1191
|
+
}
|
|
1192
|
+
await this.connect();
|
|
1193
|
+
const db = await this.acquire();
|
|
1194
|
+
const tr = await this.begin(db, options?.isolation);
|
|
1195
|
+
const ctx = {
|
|
1196
|
+
query: (sql, params) => this.runOnTransaction(tr, sql, params)
|
|
1197
|
+
};
|
|
1198
|
+
try {
|
|
1199
|
+
const result = await this.activeTx.run(ctx, () => fn(ctx));
|
|
1200
|
+
await this.commit(tr);
|
|
1201
|
+
return result;
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
await this.safeRollback(tr);
|
|
1204
|
+
throw err;
|
|
1205
|
+
} finally {
|
|
1206
|
+
db.detach();
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
// ---- promise wrappers over node-firebird -------------------------------
|
|
1210
|
+
acquire() {
|
|
1211
|
+
return new Promise((resolve4, reject) => {
|
|
1212
|
+
this.pool.get((err, db) => {
|
|
1213
|
+
if (err) return reject(wrap(err, "Failed to acquire connection"));
|
|
1214
|
+
resolve4(db);
|
|
1215
|
+
});
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
begin(db, isolation) {
|
|
1219
|
+
return new Promise((resolve4, reject) => {
|
|
1220
|
+
db.transaction(isolationConstant(isolation), (err, tr) => {
|
|
1221
|
+
if (err) return reject(wrap(err, "Failed to start transaction"));
|
|
1222
|
+
resolve4(tr);
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
runOnTransaction(tr, sql, params = []) {
|
|
1227
|
+
const start = this.onQuery ? performance.now() : 0;
|
|
1228
|
+
return new Promise((resolve4, reject) => {
|
|
1229
|
+
tr.query(sql, [...params], (err, result) => {
|
|
1230
|
+
if (err) return reject(wrap(err, "Query failed", sql));
|
|
1231
|
+
const rows = normalizeRows(result);
|
|
1232
|
+
if (this.onQuery) {
|
|
1233
|
+
this.onQuery({
|
|
1234
|
+
sql,
|
|
1235
|
+
params,
|
|
1236
|
+
durationMs: performance.now() - start,
|
|
1237
|
+
rowCount: rows.length
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
resolve4(rows);
|
|
1241
|
+
});
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
commit(tr) {
|
|
1245
|
+
return new Promise((resolve4, reject) => {
|
|
1246
|
+
tr.commit((err) => {
|
|
1247
|
+
if (err) return reject(wrap(err, "Failed to commit transaction"));
|
|
1248
|
+
resolve4();
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
safeRollback(tr) {
|
|
1253
|
+
return new Promise((resolve4) => {
|
|
1254
|
+
tr.rollback(() => resolve4());
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
function normalizeRows(result) {
|
|
1259
|
+
if (Array.isArray(result)) return result;
|
|
1260
|
+
if (result === void 0 || result === null) return [];
|
|
1261
|
+
return [result];
|
|
1262
|
+
}
|
|
1263
|
+
function wrap(err, message, sql) {
|
|
1264
|
+
const detail = err && typeof err === "object" && "message" in err ? String(err.message) : String(err);
|
|
1265
|
+
return new DatabaseError(`${message}: ${detail}`, err, sql);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/driver/url.ts
|
|
1269
|
+
function parseConnectionUrl(url) {
|
|
1270
|
+
let parsed;
|
|
1271
|
+
try {
|
|
1272
|
+
parsed = new URL(url);
|
|
1273
|
+
} catch {
|
|
1274
|
+
throw new EmberError(`Invalid Firebird connection URL: ${url}`);
|
|
1275
|
+
}
|
|
1276
|
+
if (!/^firebird:?$/.test(parsed.protocol.replace(":", "") + ":")) {
|
|
1277
|
+
if (parsed.protocol !== "firebird:") {
|
|
1278
|
+
throw new EmberError(
|
|
1279
|
+
`Unsupported protocol '${parsed.protocol}'. Expected 'firebird:'.`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
const database = normalizeDatabasePath(parsed.pathname);
|
|
1284
|
+
if (!database) {
|
|
1285
|
+
throw new EmberError(`Connection URL is missing a database path: ${url}`);
|
|
1286
|
+
}
|
|
1287
|
+
const params = parsed.searchParams;
|
|
1288
|
+
const config = {
|
|
1289
|
+
host: parsed.hostname || "127.0.0.1",
|
|
1290
|
+
port: parsed.port ? Number(parsed.port) : 3050,
|
|
1291
|
+
database,
|
|
1292
|
+
user: decodeURIComponent(parsed.username || "SYSDBA"),
|
|
1293
|
+
password: decodeURIComponent(parsed.password || "masterkey"),
|
|
1294
|
+
encoding: params.get("encoding") ?? "UTF8"
|
|
1295
|
+
};
|
|
1296
|
+
const role = params.get("role");
|
|
1297
|
+
if (role) config.role = role;
|
|
1298
|
+
const poolMax = params.get("poolMax") ?? params.get("connection_limit");
|
|
1299
|
+
if (poolMax) config.poolMax = Number(poolMax);
|
|
1300
|
+
const pageSize = params.get("pageSize");
|
|
1301
|
+
if (pageSize) config.pageSize = Number(pageSize);
|
|
1302
|
+
const auth = (params.get("authPlugin") ?? params.get("auth"))?.toLowerCase();
|
|
1303
|
+
if (auth === "legacy" || auth === "legacy_auth") config.authPlugin = "Legacy_Auth";
|
|
1304
|
+
else if (auth === "srp") config.authPlugin = "Srp";
|
|
1305
|
+
const wireCompression = params.get("wireCompression");
|
|
1306
|
+
if (wireCompression != null) {
|
|
1307
|
+
config.wireCompression = wireCompression !== "false" && wireCompression !== "0";
|
|
1308
|
+
}
|
|
1309
|
+
const version = params.get("version");
|
|
1310
|
+
if (version) config.version = normalizeVersion(version);
|
|
1311
|
+
return config;
|
|
1312
|
+
}
|
|
1313
|
+
var VERSIONS = /* @__PURE__ */ new Set(["2.1", "2.5", "3", "4", "5"]);
|
|
1314
|
+
function normalizeVersion(raw) {
|
|
1315
|
+
const trimmed = raw.trim();
|
|
1316
|
+
if (VERSIONS.has(trimmed)) return trimmed;
|
|
1317
|
+
if (/^2\.1/.test(trimmed)) return "2.1";
|
|
1318
|
+
if (/^2\.5/.test(trimmed)) return "2.5";
|
|
1319
|
+
const major = trimmed.split(".")[0];
|
|
1320
|
+
if (major && VERSIONS.has(major)) return major;
|
|
1321
|
+
return void 0;
|
|
1322
|
+
}
|
|
1323
|
+
function normalizeDatabasePath(pathname) {
|
|
1324
|
+
let p = decodeURIComponent(pathname);
|
|
1325
|
+
if (p.startsWith("//")) p = p.slice(1);
|
|
1326
|
+
if (/^\/[A-Za-z]:\//.test(p)) p = p.slice(1);
|
|
1327
|
+
return p;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// src/driver/index.ts
|
|
1331
|
+
function createDriver(source, options) {
|
|
1332
|
+
const config = typeof source === "string" ? parseConnectionUrl(source) : source;
|
|
1333
|
+
return new FirebirdDriver(config, options);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/sql/dialect.ts
|
|
1337
|
+
var FirebirdDialect = class {
|
|
1338
|
+
supportsReturning = true;
|
|
1339
|
+
version;
|
|
1340
|
+
supportsBooleanType;
|
|
1341
|
+
supportsIdentity;
|
|
1342
|
+
supportsWindowFunctions;
|
|
1343
|
+
constructor(options = {}) {
|
|
1344
|
+
this.version = options.version ?? "3";
|
|
1345
|
+
const rank = versionRank(this.version);
|
|
1346
|
+
this.supportsBooleanType = rank >= 30;
|
|
1347
|
+
this.supportsIdentity = rank >= 30;
|
|
1348
|
+
this.supportsWindowFunctions = rank >= 30;
|
|
1349
|
+
}
|
|
1350
|
+
booleanColumnType() {
|
|
1351
|
+
return this.supportsBooleanType ? "BOOLEAN" : "SMALLINT";
|
|
1352
|
+
}
|
|
1353
|
+
quoteId(name) {
|
|
1354
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
1355
|
+
}
|
|
1356
|
+
quoteRef(table, column) {
|
|
1357
|
+
return `${this.quoteId(table)}.${this.quoteId(column)}`;
|
|
1358
|
+
}
|
|
1359
|
+
paginationClause(take, skip) {
|
|
1360
|
+
const parts = [];
|
|
1361
|
+
if (typeof take === "number" && take >= 0) {
|
|
1362
|
+
parts.push(`FIRST ${Math.trunc(take)}`);
|
|
1363
|
+
}
|
|
1364
|
+
if (typeof skip === "number" && skip > 0) {
|
|
1365
|
+
parts.push(`SKIP ${Math.trunc(skip)}`);
|
|
1366
|
+
}
|
|
1367
|
+
return parts.join(" ");
|
|
1368
|
+
}
|
|
1369
|
+
caseInsensitive(expr) {
|
|
1370
|
+
return `UPPER(${expr})`;
|
|
1371
|
+
}
|
|
1372
|
+
defaultFunctionSql(name) {
|
|
1373
|
+
switch (name) {
|
|
1374
|
+
case "now":
|
|
1375
|
+
return "CURRENT_TIMESTAMP";
|
|
1376
|
+
case "uuid":
|
|
1377
|
+
case "cuid":
|
|
1378
|
+
return "UUID_TO_CHAR(GEN_UUID())";
|
|
1379
|
+
case "autoincrement":
|
|
1380
|
+
return null;
|
|
1381
|
+
// handled via generators/identity, not an inline default
|
|
1382
|
+
default:
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
coerceValue(value) {
|
|
1387
|
+
if (value === null || value === void 0) return null;
|
|
1388
|
+
if (value instanceof Date) return value;
|
|
1389
|
+
if (Buffer.isBuffer(value)) return value;
|
|
1390
|
+
if (typeof value === "boolean") {
|
|
1391
|
+
return this.supportsBooleanType ? value : value ? 1 : 0;
|
|
1392
|
+
}
|
|
1393
|
+
if (typeof value === "bigint") return value;
|
|
1394
|
+
if (typeof value === "number") return value;
|
|
1395
|
+
if (typeof value === "string") return value;
|
|
1396
|
+
return JSON.stringify(value);
|
|
1397
|
+
}
|
|
1398
|
+
};
|
|
1399
|
+
function versionRank(version) {
|
|
1400
|
+
switch (version) {
|
|
1401
|
+
case "2.1":
|
|
1402
|
+
return 21;
|
|
1403
|
+
case "2.5":
|
|
1404
|
+
return 25;
|
|
1405
|
+
case "3":
|
|
1406
|
+
return 30;
|
|
1407
|
+
case "4":
|
|
1408
|
+
return 40;
|
|
1409
|
+
case "5":
|
|
1410
|
+
return 50;
|
|
1411
|
+
default:
|
|
1412
|
+
return 30;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/introspect/firebird-meta.ts
|
|
1417
|
+
var trim = (v) => v == null ? "" : String(v).trim();
|
|
1418
|
+
var numOrNull = (v) => v == null ? null : Number(v);
|
|
1419
|
+
var FirebirdMetadataReader = class {
|
|
1420
|
+
constructor(tx) {
|
|
1421
|
+
this.tx = tx;
|
|
1422
|
+
}
|
|
1423
|
+
tx;
|
|
1424
|
+
async tables() {
|
|
1425
|
+
const rows = await this.tx.query(
|
|
1426
|
+
`SELECT RDB$RELATION_NAME
|
|
1427
|
+
FROM RDB$RELATIONS
|
|
1428
|
+
WHERE RDB$VIEW_BLR IS NULL
|
|
1429
|
+
AND (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG = 0)
|
|
1430
|
+
ORDER BY RDB$RELATION_NAME`
|
|
1431
|
+
);
|
|
1432
|
+
return rows.map((r) => trim(r["RDB$RELATION_NAME"]));
|
|
1433
|
+
}
|
|
1434
|
+
async columns() {
|
|
1435
|
+
const rows = await this.tx.query(
|
|
1436
|
+
`SELECT
|
|
1437
|
+
rf.RDB$RELATION_NAME AS TABLE_NAME,
|
|
1438
|
+
rf.RDB$FIELD_NAME AS FIELD_NAME,
|
|
1439
|
+
rf.RDB$FIELD_POSITION AS FIELD_POSITION,
|
|
1440
|
+
rf.RDB$NULL_FLAG AS NULL_FLAG,
|
|
1441
|
+
rf.RDB$DEFAULT_SOURCE AS DEFAULT_SOURCE,
|
|
1442
|
+
rf.RDB$IDENTITY_TYPE AS IDENTITY_TYPE,
|
|
1443
|
+
f.RDB$FIELD_TYPE AS FIELD_TYPE,
|
|
1444
|
+
f.RDB$FIELD_SUB_TYPE AS FIELD_SUB_TYPE,
|
|
1445
|
+
f.RDB$FIELD_LENGTH AS FIELD_LENGTH,
|
|
1446
|
+
f.RDB$CHARACTER_LENGTH AS CHAR_LENGTH,
|
|
1447
|
+
f.RDB$FIELD_PRECISION AS FIELD_PRECISION,
|
|
1448
|
+
f.RDB$FIELD_SCALE AS FIELD_SCALE
|
|
1449
|
+
FROM RDB$RELATION_FIELDS rf
|
|
1450
|
+
JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE
|
|
1451
|
+
JOIN RDB$RELATIONS r ON r.RDB$RELATION_NAME = rf.RDB$RELATION_NAME
|
|
1452
|
+
WHERE r.RDB$VIEW_BLR IS NULL
|
|
1453
|
+
AND (r.RDB$SYSTEM_FLAG IS NULL OR r.RDB$SYSTEM_FLAG = 0)
|
|
1454
|
+
ORDER BY rf.RDB$RELATION_NAME, rf.RDB$FIELD_POSITION`
|
|
1455
|
+
);
|
|
1456
|
+
return rows.map((r) => ({
|
|
1457
|
+
table: trim(r.TABLE_NAME),
|
|
1458
|
+
name: trim(r.FIELD_NAME),
|
|
1459
|
+
position: Number(r.FIELD_POSITION ?? 0),
|
|
1460
|
+
fieldType: Number(r.FIELD_TYPE ?? 0),
|
|
1461
|
+
fieldSubType: numOrNull(r.FIELD_SUB_TYPE),
|
|
1462
|
+
length: numOrNull(r.FIELD_LENGTH),
|
|
1463
|
+
charLength: numOrNull(r.CHAR_LENGTH),
|
|
1464
|
+
precision: numOrNull(r.FIELD_PRECISION),
|
|
1465
|
+
scale: numOrNull(r.FIELD_SCALE),
|
|
1466
|
+
notNull: r.NULL_FLAG != null && Number(r.NULL_FLAG) === 1,
|
|
1467
|
+
defaultSource: r.DEFAULT_SOURCE == null ? null : trim(r.DEFAULT_SOURCE),
|
|
1468
|
+
isIdentity: r.IDENTITY_TYPE != null
|
|
1469
|
+
}));
|
|
1470
|
+
}
|
|
1471
|
+
async constraints() {
|
|
1472
|
+
const rows = await this.tx.query(
|
|
1473
|
+
`SELECT
|
|
1474
|
+
rc.RDB$CONSTRAINT_NAME AS CONSTRAINT_NAME,
|
|
1475
|
+
rc.RDB$CONSTRAINT_TYPE AS CONSTRAINT_TYPE,
|
|
1476
|
+
rc.RDB$RELATION_NAME AS TABLE_NAME,
|
|
1477
|
+
rc.RDB$INDEX_NAME AS INDEX_NAME,
|
|
1478
|
+
seg.RDB$FIELD_NAME AS FIELD_NAME,
|
|
1479
|
+
seg.RDB$FIELD_POSITION AS SEG_POSITION,
|
|
1480
|
+
refc.RDB$UPDATE_RULE AS UPDATE_RULE,
|
|
1481
|
+
refc.RDB$DELETE_RULE AS DELETE_RULE,
|
|
1482
|
+
refc.RDB$CONST_NAME_UQ AS UQ_NAME
|
|
1483
|
+
FROM RDB$RELATION_CONSTRAINTS rc
|
|
1484
|
+
JOIN RDB$INDEX_SEGMENTS seg ON seg.RDB$INDEX_NAME = rc.RDB$INDEX_NAME
|
|
1485
|
+
LEFT JOIN RDB$REF_CONSTRAINTS refc ON refc.RDB$CONSTRAINT_NAME = rc.RDB$CONSTRAINT_NAME
|
|
1486
|
+
WHERE rc.RDB$CONSTRAINT_TYPE IN ('PRIMARY KEY', 'UNIQUE', 'FOREIGN KEY')
|
|
1487
|
+
ORDER BY rc.RDB$CONSTRAINT_NAME, seg.RDB$FIELD_POSITION`
|
|
1488
|
+
);
|
|
1489
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1490
|
+
const uqByName = /* @__PURE__ */ new Map();
|
|
1491
|
+
for (const r of rows) {
|
|
1492
|
+
const name = trim(r.CONSTRAINT_NAME);
|
|
1493
|
+
let c = byName.get(name);
|
|
1494
|
+
if (!c) {
|
|
1495
|
+
c = {
|
|
1496
|
+
table: trim(r.TABLE_NAME),
|
|
1497
|
+
type: trim(r.CONSTRAINT_TYPE),
|
|
1498
|
+
name,
|
|
1499
|
+
indexName: r.INDEX_NAME == null ? null : trim(r.INDEX_NAME),
|
|
1500
|
+
columns: []
|
|
1501
|
+
};
|
|
1502
|
+
if (c.type === "FOREIGN KEY") {
|
|
1503
|
+
c.updateRule = trim(r.UPDATE_RULE) || void 0;
|
|
1504
|
+
c.deleteRule = trim(r.DELETE_RULE) || void 0;
|
|
1505
|
+
uqByName.set(name, trim(r.UQ_NAME));
|
|
1506
|
+
}
|
|
1507
|
+
byName.set(name, c);
|
|
1508
|
+
}
|
|
1509
|
+
const col = trim(r.FIELD_NAME);
|
|
1510
|
+
if (col && !c.columns.includes(col)) c.columns.push(col);
|
|
1511
|
+
}
|
|
1512
|
+
for (const c of byName.values()) {
|
|
1513
|
+
if (c.type !== "FOREIGN KEY") continue;
|
|
1514
|
+
const uqName = uqByName.get(c.name);
|
|
1515
|
+
if (!uqName) continue;
|
|
1516
|
+
const target = byName.get(uqName);
|
|
1517
|
+
if (target) {
|
|
1518
|
+
c.references = { table: target.table, columns: [...target.columns] };
|
|
1519
|
+
} else {
|
|
1520
|
+
const resolved = await this.resolveConstraintColumns(uqName);
|
|
1521
|
+
if (resolved) c.references = resolved;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
return [...byName.values()];
|
|
1525
|
+
}
|
|
1526
|
+
async resolveConstraintColumns(constraintName2) {
|
|
1527
|
+
const rows = await this.tx.query(
|
|
1528
|
+
`SELECT rc.RDB$RELATION_NAME AS TABLE_NAME, seg.RDB$FIELD_NAME AS FIELD_NAME
|
|
1529
|
+
FROM RDB$RELATION_CONSTRAINTS rc
|
|
1530
|
+
JOIN RDB$INDEX_SEGMENTS seg ON seg.RDB$INDEX_NAME = rc.RDB$INDEX_NAME
|
|
1531
|
+
WHERE rc.RDB$CONSTRAINT_NAME = ?
|
|
1532
|
+
ORDER BY seg.RDB$FIELD_POSITION`,
|
|
1533
|
+
[constraintName2]
|
|
1534
|
+
);
|
|
1535
|
+
if (rows.length === 0) return null;
|
|
1536
|
+
return {
|
|
1537
|
+
table: trim(rows[0].TABLE_NAME),
|
|
1538
|
+
columns: rows.map((r) => trim(r.FIELD_NAME))
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
// src/introspect/type-map.ts
|
|
1544
|
+
var FB = {
|
|
1545
|
+
SMALLINT: 7,
|
|
1546
|
+
INTEGER: 8,
|
|
1547
|
+
QUAD: 9,
|
|
1548
|
+
FLOAT: 10,
|
|
1549
|
+
DATE_LEGACY: 11,
|
|
1550
|
+
DATE: 12,
|
|
1551
|
+
TIME: 13,
|
|
1552
|
+
CHAR: 14,
|
|
1553
|
+
INT64: 16,
|
|
1554
|
+
// BIGINT / numeric
|
|
1555
|
+
BOOLEAN: 23,
|
|
1556
|
+
DOUBLE: 27,
|
|
1557
|
+
TIMESTAMP: 35,
|
|
1558
|
+
VARCHAR: 37,
|
|
1559
|
+
BLOB: 261
|
|
1560
|
+
};
|
|
1561
|
+
function mapColumnType(col) {
|
|
1562
|
+
const scale = col.scale ?? 0;
|
|
1563
|
+
switch (col.fieldType) {
|
|
1564
|
+
case FB.SMALLINT:
|
|
1565
|
+
return scale < 0 ? decimal(col) : { scalar: "Int", native: { name: "SmallInt", args: [] } };
|
|
1566
|
+
case FB.INTEGER:
|
|
1567
|
+
return scale < 0 ? decimal(col) : { scalar: "Int", native: { name: "Integer", args: [] } };
|
|
1568
|
+
case FB.INT64:
|
|
1569
|
+
return scale < 0 ? decimal(col) : { scalar: "BigInt", native: { name: "BigInt", args: [] } };
|
|
1570
|
+
case FB.FLOAT:
|
|
1571
|
+
return { scalar: "Float", native: { name: "Float", args: [] } };
|
|
1572
|
+
case FB.DOUBLE:
|
|
1573
|
+
return { scalar: "Float", native: { name: "DoublePrecision", args: [] } };
|
|
1574
|
+
case FB.BOOLEAN:
|
|
1575
|
+
return { scalar: "Boolean", native: { name: "Boolean", args: [] } };
|
|
1576
|
+
case FB.CHAR:
|
|
1577
|
+
return {
|
|
1578
|
+
scalar: "String",
|
|
1579
|
+
native: { name: "Char", args: col.charLength ? [col.charLength] : [] }
|
|
1580
|
+
};
|
|
1581
|
+
case FB.VARCHAR:
|
|
1582
|
+
return {
|
|
1583
|
+
scalar: "String",
|
|
1584
|
+
native: { name: "VarChar", args: col.charLength ? [col.charLength] : [] }
|
|
1585
|
+
};
|
|
1586
|
+
case FB.DATE:
|
|
1587
|
+
case FB.DATE_LEGACY:
|
|
1588
|
+
return { scalar: "DateTime", native: { name: "Date", args: [] } };
|
|
1589
|
+
case FB.TIME:
|
|
1590
|
+
return { scalar: "DateTime", native: { name: "Time", args: [] } };
|
|
1591
|
+
case FB.TIMESTAMP:
|
|
1592
|
+
return { scalar: "DateTime", native: { name: "Timestamp", args: [] } };
|
|
1593
|
+
case FB.BLOB:
|
|
1594
|
+
return col.fieldSubType === 1 ? { scalar: "String", native: { name: "Text", args: [] } } : { scalar: "Bytes", native: { name: "Blob", args: [] } };
|
|
1595
|
+
default:
|
|
1596
|
+
return { scalar: "String" };
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
function decimal(col) {
|
|
1600
|
+
const precision = col.precision ?? 18;
|
|
1601
|
+
const scaleAbs = Math.abs(col.scale ?? 0);
|
|
1602
|
+
return {
|
|
1603
|
+
scalar: "Decimal",
|
|
1604
|
+
native: { name: "Decimal", args: [precision, scaleAbs] }
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// src/introspect/index.ts
|
|
1609
|
+
var Introspector = class {
|
|
1610
|
+
constructor(driver) {
|
|
1611
|
+
this.driver = driver;
|
|
1612
|
+
}
|
|
1613
|
+
driver;
|
|
1614
|
+
async introspect(options = {}) {
|
|
1615
|
+
const { tables, columns, constraints } = await this.driver.transaction(
|
|
1616
|
+
async (tx) => {
|
|
1617
|
+
const reader = new FirebirdMetadataReader(tx);
|
|
1618
|
+
return {
|
|
1619
|
+
tables: await reader.tables(),
|
|
1620
|
+
columns: await reader.columns(),
|
|
1621
|
+
constraints: await reader.constraints()
|
|
1622
|
+
};
|
|
1623
|
+
},
|
|
1624
|
+
{ isolation: "READ_COMMITTED_READ_ONLY" }
|
|
1625
|
+
);
|
|
1626
|
+
const columnsByTable = groupBy(columns, (c) => c.table);
|
|
1627
|
+
const constraintsByTable = groupBy(constraints, (c) => c.table);
|
|
1628
|
+
const modelNames = buildNameMap(tables);
|
|
1629
|
+
const models = tables.map(
|
|
1630
|
+
(table) => this.buildModel(
|
|
1631
|
+
table,
|
|
1632
|
+
modelNames,
|
|
1633
|
+
columnsByTable.get(table) ?? [],
|
|
1634
|
+
constraintsByTable.get(table) ?? []
|
|
1635
|
+
)
|
|
1636
|
+
);
|
|
1637
|
+
addRelations(models, modelNames, constraints);
|
|
1638
|
+
const enums = [];
|
|
1639
|
+
const doc = {
|
|
1640
|
+
generators: [
|
|
1641
|
+
{ name: "client", provider: "ember-client-js", output: "../generated", config: {} }
|
|
1642
|
+
],
|
|
1643
|
+
models,
|
|
1644
|
+
enums
|
|
1645
|
+
};
|
|
1646
|
+
if (options.datasource) {
|
|
1647
|
+
doc.datasource = {
|
|
1648
|
+
name: options.datasource.name,
|
|
1649
|
+
provider: options.datasource.provider,
|
|
1650
|
+
url: options.datasource.envVar ? { kind: "env", value: options.datasource.envVar } : { kind: "literal", value: options.datasource.url ?? "" }
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
return doc;
|
|
1654
|
+
}
|
|
1655
|
+
buildModel(table, modelNames, columns, constraints) {
|
|
1656
|
+
const modelName = modelNames.get(table);
|
|
1657
|
+
const pk = constraints.find((c) => c.type === "PRIMARY KEY");
|
|
1658
|
+
const uniques = constraints.filter((c) => c.type === "UNIQUE");
|
|
1659
|
+
const fieldNameMap = buildNameMap(columns.map((c) => c.name), camelCase);
|
|
1660
|
+
const fields = columns.map(
|
|
1661
|
+
(col) => buildField(col, fieldNameMap, pk, uniques)
|
|
1662
|
+
);
|
|
1663
|
+
const model = {
|
|
1664
|
+
name: modelName,
|
|
1665
|
+
dbName: modelName !== table ? table : void 0,
|
|
1666
|
+
fields,
|
|
1667
|
+
primaryKey: pk && pk.columns.length > 1 ? pk.columns.map((c) => fieldNameMap.get(c)) : [],
|
|
1668
|
+
uniqueIndexes: uniques.filter((u) => u.columns.length > 1).map((u) => ({ fields: u.columns.map((c) => fieldNameMap.get(c)) })),
|
|
1669
|
+
indexes: []
|
|
1670
|
+
};
|
|
1671
|
+
return model;
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
function buildField(col, fieldNameMap, pk, uniques) {
|
|
1675
|
+
const fieldName = fieldNameMap.get(col.name);
|
|
1676
|
+
const mapped = mapColumnType(col);
|
|
1677
|
+
const isSingleIdCol = !!pk && pk.columns.length === 1 && pk.columns[0] === col.name;
|
|
1678
|
+
const isSingleUnique = uniques.some(
|
|
1679
|
+
(u) => u.columns.length === 1 && u.columns[0] === col.name
|
|
1680
|
+
);
|
|
1681
|
+
const field = {
|
|
1682
|
+
name: fieldName,
|
|
1683
|
+
type: mapped.scalar,
|
|
1684
|
+
kind: "scalar",
|
|
1685
|
+
isList: false,
|
|
1686
|
+
isRequired: col.notNull || isSingleIdCol,
|
|
1687
|
+
isId: isSingleIdCol,
|
|
1688
|
+
isUnique: isSingleUnique,
|
|
1689
|
+
isUpdatedAt: false,
|
|
1690
|
+
dbName: fieldName !== col.name ? col.name : void 0,
|
|
1691
|
+
nativeType: mapped.native,
|
|
1692
|
+
default: parseDefault(col)
|
|
1693
|
+
};
|
|
1694
|
+
return field;
|
|
1695
|
+
}
|
|
1696
|
+
function parseDefault(col) {
|
|
1697
|
+
if (col.isIdentity) return { function: { name: "autoincrement", args: [] } };
|
|
1698
|
+
if (!col.defaultSource) return void 0;
|
|
1699
|
+
const src = col.defaultSource.replace(/^DEFAULT\s+/i, "").trim();
|
|
1700
|
+
if (/^CURRENT_TIMESTAMP/i.test(src)) return { function: { name: "now", args: [] } };
|
|
1701
|
+
if (/^(TRUE|FALSE)$/i.test(src)) return { literal: /^TRUE$/i.test(src) };
|
|
1702
|
+
if (/^-?\d+(\.\d+)?$/.test(src)) return { literal: Number(src) };
|
|
1703
|
+
const str = /^'(.*)'$/.exec(src);
|
|
1704
|
+
if (str) return { literal: str[1] };
|
|
1705
|
+
return void 0;
|
|
1706
|
+
}
|
|
1707
|
+
function addRelations(models, modelNames, constraints) {
|
|
1708
|
+
const byName = new Map(models.map((m) => [m.name, m]));
|
|
1709
|
+
for (const fk of constraints) {
|
|
1710
|
+
if (fk.type !== "FOREIGN KEY" || !fk.references) continue;
|
|
1711
|
+
const childModel = byName.get(modelNames.get(fk.table));
|
|
1712
|
+
const parentModel = byName.get(modelNames.get(fk.references.table));
|
|
1713
|
+
if (!childModel || !parentModel) continue;
|
|
1714
|
+
const localFieldNames = fk.columns.map((c) => columnToField(childModel, c));
|
|
1715
|
+
const refFieldNames = fk.references.columns.map(
|
|
1716
|
+
(c) => columnToField(parentModel, c)
|
|
1717
|
+
);
|
|
1718
|
+
const childFieldName = uniqueFieldName2(childModel, lowerFirst(parentModel.name));
|
|
1719
|
+
childModel.fields.push({
|
|
1720
|
+
name: childFieldName,
|
|
1721
|
+
type: parentModel.name,
|
|
1722
|
+
kind: "object",
|
|
1723
|
+
isList: false,
|
|
1724
|
+
isRequired: localFieldNames.every(
|
|
1725
|
+
(n) => childModel.fields.find((f) => f.name === n)?.isRequired
|
|
1726
|
+
),
|
|
1727
|
+
isId: false,
|
|
1728
|
+
isUnique: false,
|
|
1729
|
+
isUpdatedAt: false,
|
|
1730
|
+
relation: {
|
|
1731
|
+
fields: localFieldNames,
|
|
1732
|
+
references: refFieldNames,
|
|
1733
|
+
onDelete: mapRule(fk.deleteRule),
|
|
1734
|
+
onUpdate: mapRule(fk.updateRule)
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
const backName = uniqueFieldName2(
|
|
1738
|
+
parentModel,
|
|
1739
|
+
lowerFirst(pluralize(childModel.name))
|
|
1740
|
+
);
|
|
1741
|
+
parentModel.fields.push({
|
|
1742
|
+
name: backName,
|
|
1743
|
+
type: childModel.name,
|
|
1744
|
+
kind: "object",
|
|
1745
|
+
isList: true,
|
|
1746
|
+
isRequired: false,
|
|
1747
|
+
isId: false,
|
|
1748
|
+
isUnique: false,
|
|
1749
|
+
isUpdatedAt: false
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
function columnToField(model, column) {
|
|
1754
|
+
const f = model.fields.find((x) => (x.dbName ?? x.name) === column);
|
|
1755
|
+
return f ? f.name : camelCase(column);
|
|
1756
|
+
}
|
|
1757
|
+
function uniqueFieldName2(model, base) {
|
|
1758
|
+
let name = base;
|
|
1759
|
+
let i = 1;
|
|
1760
|
+
while (model.fields.some((f) => f.name === name)) name = `${base}_${++i}`;
|
|
1761
|
+
return name;
|
|
1762
|
+
}
|
|
1763
|
+
function mapRule(rule) {
|
|
1764
|
+
switch ((rule ?? "").toUpperCase()) {
|
|
1765
|
+
case "CASCADE":
|
|
1766
|
+
return "Cascade";
|
|
1767
|
+
case "SET NULL":
|
|
1768
|
+
return "SetNull";
|
|
1769
|
+
case "SET DEFAULT":
|
|
1770
|
+
return "SetDefault";
|
|
1771
|
+
case "NO ACTION":
|
|
1772
|
+
return "NoAction";
|
|
1773
|
+
case "RESTRICT":
|
|
1774
|
+
return "Restrict";
|
|
1775
|
+
default:
|
|
1776
|
+
return void 0;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
function buildNameMap(dbNames, transform = pascalCase) {
|
|
1780
|
+
const map = /* @__PURE__ */ new Map();
|
|
1781
|
+
const used = /* @__PURE__ */ new Set();
|
|
1782
|
+
for (const dbName of uniq(dbNames)) {
|
|
1783
|
+
let name = transform(dbName);
|
|
1784
|
+
if (!name) name = dbName;
|
|
1785
|
+
let candidate = name;
|
|
1786
|
+
let i = 1;
|
|
1787
|
+
while (used.has(candidate)) candidate = `${name}_${++i}`;
|
|
1788
|
+
used.add(candidate);
|
|
1789
|
+
map.set(dbName, candidate);
|
|
1790
|
+
}
|
|
1791
|
+
return map;
|
|
1792
|
+
}
|
|
1793
|
+
function groupBy(items, key) {
|
|
1794
|
+
const map = /* @__PURE__ */ new Map();
|
|
1795
|
+
for (const item of items) {
|
|
1796
|
+
const k = key(item);
|
|
1797
|
+
const list = map.get(k) ?? [];
|
|
1798
|
+
list.push(item);
|
|
1799
|
+
map.set(k, list);
|
|
1800
|
+
}
|
|
1801
|
+
return map;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// src/generator/index.ts
|
|
1805
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
1806
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
1807
|
+
|
|
1808
|
+
// src/generator/ts-types.ts
|
|
1809
|
+
var SCALAR_TS = {
|
|
1810
|
+
String: "string",
|
|
1811
|
+
Boolean: "boolean",
|
|
1812
|
+
Int: "number",
|
|
1813
|
+
BigInt: "bigint",
|
|
1814
|
+
Float: "number",
|
|
1815
|
+
Decimal: "number",
|
|
1816
|
+
DateTime: "Date",
|
|
1817
|
+
Bytes: "Buffer",
|
|
1818
|
+
Json: "JsonValue"
|
|
1819
|
+
};
|
|
1820
|
+
function baseTsType(field, schema) {
|
|
1821
|
+
if (field.kind === "enum") return field.type;
|
|
1822
|
+
if (field.kind === "object") return field.type;
|
|
1823
|
+
return SCALAR_TS[field.type] ?? "unknown";
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/generator/index.ts
|
|
1827
|
+
var ClientGenerator = class {
|
|
1828
|
+
constructor(schema) {
|
|
1829
|
+
this.schema = schema;
|
|
1830
|
+
}
|
|
1831
|
+
schema;
|
|
1832
|
+
generate() {
|
|
1833
|
+
const parts = [];
|
|
1834
|
+
parts.push(this.header());
|
|
1835
|
+
parts.push(this.enums());
|
|
1836
|
+
parts.push(this.modelTypes());
|
|
1837
|
+
parts.push(this.registry());
|
|
1838
|
+
parts.push(PAYLOAD_RESOLVER);
|
|
1839
|
+
parts.push(this.namespace());
|
|
1840
|
+
parts.push(this.clientClass());
|
|
1841
|
+
return parts.filter(Boolean).join("\n\n") + "\n";
|
|
1842
|
+
}
|
|
1843
|
+
// ---- header & shared ----------------------------------------------------
|
|
1844
|
+
header() {
|
|
1845
|
+
return `// AUTO-GENERATED by EmberORM. Do not edit by hand.
|
|
1846
|
+
/* eslint-disable */
|
|
1847
|
+
import { EmberClientBase } from "ember-orm/client";
|
|
1848
|
+
import type { ClientOptions } from "ember-orm/client";
|
|
1849
|
+
|
|
1850
|
+
export type JsonValue =
|
|
1851
|
+
| string
|
|
1852
|
+
| number
|
|
1853
|
+
| boolean
|
|
1854
|
+
| null
|
|
1855
|
+
| { [key: string]: JsonValue }
|
|
1856
|
+
| JsonValue[];
|
|
1857
|
+
|
|
1858
|
+
export type SortOrder = "asc" | "desc";
|
|
1859
|
+
export type QueryMode = "default" | "insensitive";
|
|
1860
|
+
|
|
1861
|
+
const schemaDocument = ${JSON.stringify(this.schema)} as unknown as ClientOptions["schema"];`;
|
|
1862
|
+
}
|
|
1863
|
+
enums() {
|
|
1864
|
+
return this.schema.enums.map((e) => this.enumType(e)).join("\n\n");
|
|
1865
|
+
}
|
|
1866
|
+
enumType(node) {
|
|
1867
|
+
const values = node.values.map((v) => ` ${v.name}: "${v.name}"`).join(",\n");
|
|
1868
|
+
return `export const ${node.name} = {
|
|
1869
|
+
${values}
|
|
1870
|
+
} as const;
|
|
1871
|
+
export type ${node.name} = (typeof ${node.name})[keyof typeof ${node.name}];`;
|
|
1872
|
+
}
|
|
1873
|
+
modelTypes() {
|
|
1874
|
+
return this.schema.models.map((m) => this.modelType(m)).join("\n\n");
|
|
1875
|
+
}
|
|
1876
|
+
modelType(model) {
|
|
1877
|
+
const scalars = scalarFields(model).map((f) => ` ${f.name}: ${this.scalarType(f)};`).join("\n");
|
|
1878
|
+
const lines = [];
|
|
1879
|
+
lines.push(`export type ${model.name} = {
|
|
1880
|
+
${scalars}
|
|
1881
|
+
};`);
|
|
1882
|
+
lines.push(`export type ${model.name}GetPayload<A> = $Payload<"${model.name}", A>;`);
|
|
1883
|
+
lines.push(this.fluentType(model));
|
|
1884
|
+
return lines.join("\n");
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Fluent-API return type for single-record reads: a Promise of the payload
|
|
1888
|
+
* that also exposes a method per relation (to-many -> array; to-one ->
|
|
1889
|
+
* chainable fluent).
|
|
1890
|
+
*/
|
|
1891
|
+
fluentType(model) {
|
|
1892
|
+
const n = model.name;
|
|
1893
|
+
const methods = relationFields(model).map((f) => {
|
|
1894
|
+
if (f.isList) {
|
|
1895
|
+
return ` ${f.name}<S extends Prisma.${f.type}FindManyArgs = {}>(args?: S): Promise<${f.type}GetPayload<S>[]>;`;
|
|
1896
|
+
}
|
|
1897
|
+
return ` ${f.name}<S extends Prisma.${f.type}FindFirstArgs = {}>(args?: S): ${f.type}Fluent<S, null>;`;
|
|
1898
|
+
});
|
|
1899
|
+
return `export type ${n}Fluent<A, Null = null> = Promise<${n}GetPayload<A> | Null> & {
|
|
1900
|
+
${methods.join("\n")}
|
|
1901
|
+
};`;
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Type-level registry powering recursive payload resolution: a model-name
|
|
1905
|
+
* union, a scalar-payload map, and a relation map carrying each relation's
|
|
1906
|
+
* target model, list-ness and nullability.
|
|
1907
|
+
*/
|
|
1908
|
+
registry() {
|
|
1909
|
+
const names = this.schema.models.map((m) => `"${m.name}"`).join(" | ");
|
|
1910
|
+
const scalarEntries = this.schema.models.map((m) => ` ${m.name}: ${m.name};`).join("\n");
|
|
1911
|
+
const relationEntries = this.schema.models.map((m) => {
|
|
1912
|
+
const rels = relationFields(m).map((f) => {
|
|
1913
|
+
const nullable = !f.isList && !f.isRequired ? "; isNullable: true" : "";
|
|
1914
|
+
return ` ${f.name}: { model: "${f.type}"; isList: ${f.isList}${nullable} }`;
|
|
1915
|
+
}).join(";\n");
|
|
1916
|
+
return ` ${m.name}: {${rels ? `
|
|
1917
|
+
${rels}
|
|
1918
|
+
` : ""}};`;
|
|
1919
|
+
}).join("\n");
|
|
1920
|
+
return `export type $ModelName = ${names || "never"};
|
|
1921
|
+
|
|
1922
|
+
export interface $ScalarPayload {
|
|
1923
|
+
${scalarEntries}
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
export interface $RelationMap {
|
|
1927
|
+
${relationEntries}
|
|
1928
|
+
}`;
|
|
1929
|
+
}
|
|
1930
|
+
scalarType(field) {
|
|
1931
|
+
const base = baseTsType(field, this.schema);
|
|
1932
|
+
if (field.isList) return `${base}[]`;
|
|
1933
|
+
return field.isRequired ? base : `${base} | null`;
|
|
1934
|
+
}
|
|
1935
|
+
// ---- input types (Prisma namespace) -------------------------------------
|
|
1936
|
+
namespace() {
|
|
1937
|
+
const blocks = [SHARED_FILTERS];
|
|
1938
|
+
for (const model of this.schema.models) {
|
|
1939
|
+
blocks.push(this.whereInput(model));
|
|
1940
|
+
blocks.push(this.orderByInput(model));
|
|
1941
|
+
blocks.push(this.selectInclude(model));
|
|
1942
|
+
blocks.push(this.dataInputs(model));
|
|
1943
|
+
blocks.push(this.argsTypes(model));
|
|
1944
|
+
blocks.push(this.delegateInterface(model));
|
|
1945
|
+
}
|
|
1946
|
+
return `export namespace Prisma {
|
|
1947
|
+
${indentBlock(blocks.join("\n\n"))}
|
|
1948
|
+
}`;
|
|
1949
|
+
}
|
|
1950
|
+
whereInput(model) {
|
|
1951
|
+
const fields = [];
|
|
1952
|
+
for (const f of model.fields) {
|
|
1953
|
+
if (f.kind === "object") {
|
|
1954
|
+
fields.push(` ${f.name}?: ${this.relationFilter(f)};`);
|
|
1955
|
+
} else {
|
|
1956
|
+
fields.push(` ${f.name}?: ${this.fieldFilter(f)};`);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return `export type ${model.name}WhereInput = {
|
|
1960
|
+
AND?: ${model.name}WhereInput | ${model.name}WhereInput[];
|
|
1961
|
+
OR?: ${model.name}WhereInput[];
|
|
1962
|
+
NOT?: ${model.name}WhereInput | ${model.name}WhereInput[];
|
|
1963
|
+
${fields.join("\n")}
|
|
1964
|
+
};
|
|
1965
|
+
|
|
1966
|
+
export type ${model.name}WhereUniqueInput = ${this.whereUnique(model)};`;
|
|
1967
|
+
}
|
|
1968
|
+
whereUnique(model) {
|
|
1969
|
+
const uniques = /* @__PURE__ */ new Set();
|
|
1970
|
+
for (const f of model.fields) if (f.isId || f.isUnique) uniques.add(f.name);
|
|
1971
|
+
for (const f of model.primaryKey) uniques.add(f);
|
|
1972
|
+
const parts = [...uniques].map((name) => {
|
|
1973
|
+
const field = model.fields.find((x) => x.name === name);
|
|
1974
|
+
return `{ ${name}: ${baseTsType(field, this.schema)} }`;
|
|
1975
|
+
});
|
|
1976
|
+
const base = `Partial<${model.name}WhereInput>`;
|
|
1977
|
+
return parts.length ? `(${parts.join(" | ")}) & ${base}` : base;
|
|
1978
|
+
}
|
|
1979
|
+
fieldFilter(field) {
|
|
1980
|
+
const base = baseTsType(field, this.schema);
|
|
1981
|
+
const filterName = FILTER_FOR_SCALAR[field.type] ?? null;
|
|
1982
|
+
if (field.kind === "enum") {
|
|
1983
|
+
return `${base} | { equals?: ${base}; in?: ${base}[]; notIn?: ${base}[]; not?: ${base} }`;
|
|
1984
|
+
}
|
|
1985
|
+
if (filterName) return `${base} | ${filterName}`;
|
|
1986
|
+
return `${base}`;
|
|
1987
|
+
}
|
|
1988
|
+
relationFilter(field) {
|
|
1989
|
+
if (field.isList) {
|
|
1990
|
+
return `{ some?: ${field.type}WhereInput; every?: ${field.type}WhereInput; none?: ${field.type}WhereInput }`;
|
|
1991
|
+
}
|
|
1992
|
+
return `${field.type}WhereInput | { is?: ${field.type}WhereInput | null; isNot?: ${field.type}WhereInput | null } | null`;
|
|
1993
|
+
}
|
|
1994
|
+
orderByInput(model) {
|
|
1995
|
+
const fields = scalarFields(model).map((f) => ` ${f.name}?: SortOrder;`).join("\n");
|
|
1996
|
+
return `export type ${model.name}OrderByInput = {
|
|
1997
|
+
${fields}
|
|
1998
|
+
};`;
|
|
1999
|
+
}
|
|
2000
|
+
selectInclude(model) {
|
|
2001
|
+
const scalarSel = scalarFields(model).map((f) => ` ${f.name}?: boolean;`).join("\n");
|
|
2002
|
+
const relSel = relationFields(model).map((f) => ` ${f.name}?: boolean | ${f.type}FindManyArgs;`).join("\n");
|
|
2003
|
+
const include = relationFields(model).map((f) => ` ${f.name}?: boolean | ${f.type}FindManyArgs;`).join("\n");
|
|
2004
|
+
const listRelations = relationFields(model).filter((f) => f.isList);
|
|
2005
|
+
const countLine = listRelations.length ? `
|
|
2006
|
+
_count?: boolean | { select?: { ${listRelations.map((f) => `${f.name}?: boolean`).join("; ")} } };` : "";
|
|
2007
|
+
return `export type ${model.name}Select = {
|
|
2008
|
+
${scalarSel}${relSel ? "\n" + relSel : ""}${countLine}
|
|
2009
|
+
};
|
|
2010
|
+
|
|
2011
|
+
export type ${model.name}Include = {
|
|
2012
|
+
${include || " [k: string]: never;"}${countLine}
|
|
2013
|
+
};`;
|
|
2014
|
+
}
|
|
2015
|
+
dataInputs(model) {
|
|
2016
|
+
const foreignKeys2 = /* @__PURE__ */ new Set();
|
|
2017
|
+
for (const rel2 of relationFields(model)) {
|
|
2018
|
+
for (const fk of rel2.relation?.fields ?? []) foreignKeys2.add(fk);
|
|
2019
|
+
}
|
|
2020
|
+
const createScalars = scalarFields(model).filter((f) => !isGeneratedOnCreate(f)).map((f) => {
|
|
2021
|
+
const optional = isOptionalOnCreate(f) || foreignKeys2.has(f.name) ? "?" : "";
|
|
2022
|
+
return ` ${f.name}${optional}: ${this.inputScalarType(f)};`;
|
|
2023
|
+
});
|
|
2024
|
+
const createRelations = relationFields(model).map(
|
|
2025
|
+
(f) => ` ${f.name}?: ${this.nestedCreate(f)};`
|
|
2026
|
+
);
|
|
2027
|
+
const updateScalars = scalarFields(model).map(
|
|
2028
|
+
(f) => ` ${f.name}?: ${this.updateScalarType(f)};`
|
|
2029
|
+
);
|
|
2030
|
+
const updateRelations = relationFields(model).map(
|
|
2031
|
+
(f) => ` ${f.name}?: ${this.nestedUpdate(f)};`
|
|
2032
|
+
);
|
|
2033
|
+
return `export type ${model.name}CreateInput = {
|
|
2034
|
+
${[...createScalars, ...createRelations].join("\n")}
|
|
2035
|
+
};
|
|
2036
|
+
|
|
2037
|
+
export type ${model.name}UpdateInput = {
|
|
2038
|
+
${[...updateScalars, ...updateRelations].join("\n")}
|
|
2039
|
+
};`;
|
|
2040
|
+
}
|
|
2041
|
+
inputScalarType(field) {
|
|
2042
|
+
const base = baseTsType(field, this.schema);
|
|
2043
|
+
if (field.isList) return `${base}[]`;
|
|
2044
|
+
return field.isRequired ? base : `${base} | null`;
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Update value for a scalar field: a bare value, `{ set }`, and — for numeric
|
|
2048
|
+
* fields — the atomic operators increment/decrement/multiply/divide.
|
|
2049
|
+
*/
|
|
2050
|
+
updateScalarType(field) {
|
|
2051
|
+
const value = this.inputScalarType(field);
|
|
2052
|
+
if (field.isList) return value;
|
|
2053
|
+
const base = baseTsType(field, this.schema);
|
|
2054
|
+
if (NUMERIC_SCALARS.has(field.type)) {
|
|
2055
|
+
const nullable = field.isRequired ? "" : " | null";
|
|
2056
|
+
return `${value} | { set?: ${base}${nullable}; increment?: ${base}; decrement?: ${base}; multiply?: ${base}; divide?: ${base} }`;
|
|
2057
|
+
}
|
|
2058
|
+
return `${value} | { set?: ${value} }`;
|
|
2059
|
+
}
|
|
2060
|
+
nestedCreate(field) {
|
|
2061
|
+
const t = field.type;
|
|
2062
|
+
if (field.isList) {
|
|
2063
|
+
return `{ create?: ${t}CreateInput | ${t}CreateInput[]; connect?: ${t}WhereUniqueInput | ${t}WhereUniqueInput[]; connectOrCreate?: { where: ${t}WhereUniqueInput; create: ${t}CreateInput } | { where: ${t}WhereUniqueInput; create: ${t}CreateInput }[] }`;
|
|
2064
|
+
}
|
|
2065
|
+
return `{ create?: ${t}CreateInput; connect?: ${t}WhereUniqueInput; connectOrCreate?: { where: ${t}WhereUniqueInput; create: ${t}CreateInput } }`;
|
|
2066
|
+
}
|
|
2067
|
+
nestedUpdate(field) {
|
|
2068
|
+
const t = field.type;
|
|
2069
|
+
if (field.isList) {
|
|
2070
|
+
return `{ create?: ${t}CreateInput | ${t}CreateInput[]; connect?: ${t}WhereUniqueInput | ${t}WhereUniqueInput[]; disconnect?: ${t}WhereUniqueInput | ${t}WhereUniqueInput[]; set?: ${t}WhereUniqueInput | ${t}WhereUniqueInput[]; delete?: ${t}WhereUniqueInput | ${t}WhereUniqueInput[] }`;
|
|
2071
|
+
}
|
|
2072
|
+
return `{ create?: ${t}CreateInput; connect?: ${t}WhereUniqueInput; disconnect?: boolean }`;
|
|
2073
|
+
}
|
|
2074
|
+
argsTypes(model) {
|
|
2075
|
+
const n = model.name;
|
|
2076
|
+
return `export type ${n}Omit = Partial<Record<keyof ${n}, boolean>>;
|
|
2077
|
+
|
|
2078
|
+
export type ${n}FindManyArgs = {
|
|
2079
|
+
where?: ${n}WhereInput;
|
|
2080
|
+
orderBy?: ${n}OrderByInput | ${n}OrderByInput[];
|
|
2081
|
+
select?: ${n}Select;
|
|
2082
|
+
include?: ${n}Include;
|
|
2083
|
+
omit?: ${n}Omit;
|
|
2084
|
+
take?: number;
|
|
2085
|
+
skip?: number;
|
|
2086
|
+
cursor?: ${n}WhereUniqueInput;
|
|
2087
|
+
distinct?: (keyof ${n})[];
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
export type ${n}FindFirstArgs = ${n}FindManyArgs;
|
|
2091
|
+
|
|
2092
|
+
export type ${n}FindUniqueArgs = {
|
|
2093
|
+
where: ${n}WhereUniqueInput;
|
|
2094
|
+
select?: ${n}Select;
|
|
2095
|
+
include?: ${n}Include;
|
|
2096
|
+
omit?: ${n}Omit;
|
|
2097
|
+
};
|
|
2098
|
+
|
|
2099
|
+
export type ${n}CreateArgs = {
|
|
2100
|
+
data: ${n}CreateInput;
|
|
2101
|
+
select?: ${n}Select;
|
|
2102
|
+
include?: ${n}Include;
|
|
2103
|
+
omit?: ${n}Omit;
|
|
2104
|
+
};
|
|
2105
|
+
|
|
2106
|
+
export type ${n}CreateManyArgs = { data: ${n}CreateInput[]; skipDuplicates?: boolean };
|
|
2107
|
+
|
|
2108
|
+
export type ${n}CreateManyAndReturnArgs = {
|
|
2109
|
+
data: ${n}CreateInput[];
|
|
2110
|
+
select?: ${n}Select;
|
|
2111
|
+
omit?: ${n}Omit;
|
|
2112
|
+
skipDuplicates?: boolean;
|
|
2113
|
+
};
|
|
2114
|
+
|
|
2115
|
+
export type ${n}UpdateArgs = {
|
|
2116
|
+
where: ${n}WhereUniqueInput;
|
|
2117
|
+
data: ${n}UpdateInput;
|
|
2118
|
+
select?: ${n}Select;
|
|
2119
|
+
include?: ${n}Include;
|
|
2120
|
+
omit?: ${n}Omit;
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
export type ${n}UpdateManyArgs = { where?: ${n}WhereInput; data: ${n}UpdateInput };
|
|
2124
|
+
|
|
2125
|
+
export type ${n}UpsertArgs = {
|
|
2126
|
+
where: ${n}WhereUniqueInput;
|
|
2127
|
+
create: ${n}CreateInput;
|
|
2128
|
+
update: ${n}UpdateInput;
|
|
2129
|
+
select?: ${n}Select;
|
|
2130
|
+
include?: ${n}Include;
|
|
2131
|
+
omit?: ${n}Omit;
|
|
2132
|
+
};
|
|
2133
|
+
|
|
2134
|
+
export type ${n}DeleteArgs = { where: ${n}WhereUniqueInput; select?: ${n}Select; include?: ${n}Include };
|
|
2135
|
+
export type ${n}DeleteManyArgs = { where?: ${n}WhereInput };
|
|
2136
|
+
|
|
2137
|
+
export type ${n}CountArgs = { where?: ${n}WhereInput; take?: number; skip?: number };
|
|
2138
|
+
|
|
2139
|
+
export type ${n}AggregateArgs = {
|
|
2140
|
+
where?: ${n}WhereInput;
|
|
2141
|
+
_count?: true | Partial<Record<keyof ${n}, boolean>>;
|
|
2142
|
+
_avg?: Partial<Record<keyof ${n}, boolean>>;
|
|
2143
|
+
_sum?: Partial<Record<keyof ${n}, boolean>>;
|
|
2144
|
+
_min?: Partial<Record<keyof ${n}, boolean>>;
|
|
2145
|
+
_max?: Partial<Record<keyof ${n}, boolean>>;
|
|
2146
|
+
};
|
|
2147
|
+
|
|
2148
|
+
export type ${n}GroupByArgs = ${n}AggregateArgs & {
|
|
2149
|
+
by: (keyof ${n})[];
|
|
2150
|
+
having?: ${n}WhereInput;
|
|
2151
|
+
orderBy?: ${n}OrderByInput | ${n}OrderByInput[];
|
|
2152
|
+
};`;
|
|
2153
|
+
}
|
|
2154
|
+
delegateInterface(model) {
|
|
2155
|
+
const n = model.name;
|
|
2156
|
+
return `export interface ${n}Delegate {
|
|
2157
|
+
findMany<A extends ${n}FindManyArgs>(args?: A): Promise<${n}GetPayload<A>[]>;
|
|
2158
|
+
findFirst<A extends ${n}FindFirstArgs>(args?: A): ${n}Fluent<A, null>;
|
|
2159
|
+
findFirstOrThrow<A extends ${n}FindFirstArgs>(args?: A): ${n}Fluent<A, never>;
|
|
2160
|
+
findUnique<A extends ${n}FindUniqueArgs>(args: A): ${n}Fluent<A, null>;
|
|
2161
|
+
findUniqueOrThrow<A extends ${n}FindUniqueArgs>(args: A): ${n}Fluent<A, never>;
|
|
2162
|
+
create<A extends ${n}CreateArgs>(args: A): Promise<${n}GetPayload<A>>;
|
|
2163
|
+
createMany(args: ${n}CreateManyArgs): Promise<{ count: number }>;
|
|
2164
|
+
createManyAndReturn<A extends ${n}CreateManyAndReturnArgs>(args: A): Promise<${n}GetPayload<A>[]>;
|
|
2165
|
+
update<A extends ${n}UpdateArgs>(args: A): Promise<${n}GetPayload<A>>;
|
|
2166
|
+
updateMany(args: ${n}UpdateManyArgs): Promise<{ count: number }>;
|
|
2167
|
+
upsert<A extends ${n}UpsertArgs>(args: A): Promise<${n}GetPayload<A>>;
|
|
2168
|
+
delete<A extends ${n}DeleteArgs>(args: A): Promise<${n}GetPayload<A>>;
|
|
2169
|
+
deleteMany(args?: ${n}DeleteManyArgs): Promise<{ count: number }>;
|
|
2170
|
+
count(args?: ${n}CountArgs): Promise<number>;
|
|
2171
|
+
aggregate(args?: ${n}AggregateArgs): Promise<Record<string, any>>;
|
|
2172
|
+
groupBy(args: ${n}GroupByArgs): Promise<Record<string, any>[]>;
|
|
2173
|
+
}`;
|
|
2174
|
+
}
|
|
2175
|
+
// ---- client class -------------------------------------------------------
|
|
2176
|
+
clientClass() {
|
|
2177
|
+
const delegates = this.schema.models.map((m) => ` declare readonly ${lowerFirst(m.name)}: Prisma.${m.name}Delegate;`).join("\n");
|
|
2178
|
+
return `export type EmberClientOptions = {
|
|
2179
|
+
datasourceUrl?: string;
|
|
2180
|
+
datasource?: ClientOptions["datasource"];
|
|
2181
|
+
log?: ClientOptions["log"];
|
|
2182
|
+
};
|
|
2183
|
+
|
|
2184
|
+
export class EmberClient extends EmberClientBase {
|
|
2185
|
+
constructor(options: EmberClientOptions = {}) {
|
|
2186
|
+
super({ schema: schemaDocument, ...options });
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
${delegates}
|
|
2190
|
+
}`;
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
function generateClientSource(schema) {
|
|
2194
|
+
return new ClientGenerator(schema).generate();
|
|
2195
|
+
}
|
|
2196
|
+
function writeClient(schema, outDir) {
|
|
2197
|
+
const source = generateClientSource(schema);
|
|
2198
|
+
const file = resolve2(outDir, "index.ts");
|
|
2199
|
+
mkdirSync(dirname2(file), { recursive: true });
|
|
2200
|
+
writeFileSync(file, source, "utf8");
|
|
2201
|
+
return file;
|
|
2202
|
+
}
|
|
2203
|
+
var PAYLOAD_RESOLVER = `type $Scalar<M extends $ModelName> = $ScalarPayload[M];
|
|
2204
|
+
type $Relations<M extends $ModelName> = M extends keyof $RelationMap
|
|
2205
|
+
? $RelationMap[M]
|
|
2206
|
+
: {};
|
|
2207
|
+
|
|
2208
|
+
// A relation sub-arg may be \`true\` (full payload) or a nested args object.
|
|
2209
|
+
type $NormalizeArgs<Sub> = Sub extends object ? Sub : {};
|
|
2210
|
+
|
|
2211
|
+
type $RelationResult<Info, Sub> = Info extends {
|
|
2212
|
+
model: infer RM extends $ModelName;
|
|
2213
|
+
isList: infer L;
|
|
2214
|
+
}
|
|
2215
|
+
? L extends true
|
|
2216
|
+
? $Payload<RM, $NormalizeArgs<Sub>>[]
|
|
2217
|
+
: Info extends { isNullable: true }
|
|
2218
|
+
? $Payload<RM, $NormalizeArgs<Sub>> | null
|
|
2219
|
+
: $Payload<RM, $NormalizeArgs<Sub>>
|
|
2220
|
+
: never;
|
|
2221
|
+
|
|
2222
|
+
// To-many relation keys of a model (eligible for _count).
|
|
2223
|
+
type $ListRelations<M extends $ModelName> = {
|
|
2224
|
+
[K in keyof $Relations<M>]: $Relations<M>[K] extends { isList: true }
|
|
2225
|
+
? K
|
|
2226
|
+
: never;
|
|
2227
|
+
}[keyof $Relations<M>];
|
|
2228
|
+
|
|
2229
|
+
type $CountResult<M extends $ModelName, C> = C extends { select: infer SC }
|
|
2230
|
+
? { [K in keyof SC & $ListRelations<M>]: number }
|
|
2231
|
+
: { [K in $ListRelations<M> & string]: number };
|
|
2232
|
+
|
|
2233
|
+
type $CountPart<M extends $ModelName, A> = A extends { _count: infer C }
|
|
2234
|
+
? { _count: $CountResult<M, C> }
|
|
2235
|
+
: {};
|
|
2236
|
+
|
|
2237
|
+
type $SelectResult<M extends $ModelName, S> = {
|
|
2238
|
+
[K in keyof S & keyof $Scalar<M>]: $Scalar<M>[K];
|
|
2239
|
+
} & {
|
|
2240
|
+
[K in keyof S & keyof $Relations<M>]: $RelationResult<$Relations<M>[K], S[K]>;
|
|
2241
|
+
} & $CountPart<M, S>;
|
|
2242
|
+
|
|
2243
|
+
type $IncludeResult<M extends $ModelName, I> = {
|
|
2244
|
+
[K in keyof I & keyof $Relations<M>]: $RelationResult<$Relations<M>[K], I[K]>;
|
|
2245
|
+
} & $CountPart<M, I>;
|
|
2246
|
+
|
|
2247
|
+
export type $Payload<M extends $ModelName, A> = A extends { select: infer S }
|
|
2248
|
+
? $SelectResult<M, S>
|
|
2249
|
+
: A extends { include: infer I }
|
|
2250
|
+
? $Scalar<M> & $IncludeResult<M, I>
|
|
2251
|
+
: $Scalar<M>;`;
|
|
2252
|
+
var NUMERIC_SCALARS = /* @__PURE__ */ new Set(["Int", "BigInt", "Float", "Decimal"]);
|
|
2253
|
+
var FILTER_FOR_SCALAR = {
|
|
2254
|
+
String: "StringFilter",
|
|
2255
|
+
Int: "IntFilter",
|
|
2256
|
+
BigInt: "BigIntFilter",
|
|
2257
|
+
Float: "FloatFilter",
|
|
2258
|
+
Decimal: "FloatFilter",
|
|
2259
|
+
Boolean: "BoolFilter",
|
|
2260
|
+
DateTime: "DateTimeFilter",
|
|
2261
|
+
Bytes: "BytesFilter",
|
|
2262
|
+
Json: "JsonFilter"
|
|
2263
|
+
};
|
|
2264
|
+
var SHARED_FILTERS = `export type StringFilter = {
|
|
2265
|
+
equals?: string;
|
|
2266
|
+
not?: string | StringFilter;
|
|
2267
|
+
in?: string[];
|
|
2268
|
+
notIn?: string[];
|
|
2269
|
+
lt?: string;
|
|
2270
|
+
lte?: string;
|
|
2271
|
+
gt?: string;
|
|
2272
|
+
gte?: string;
|
|
2273
|
+
contains?: string;
|
|
2274
|
+
startsWith?: string;
|
|
2275
|
+
endsWith?: string;
|
|
2276
|
+
mode?: QueryMode;
|
|
2277
|
+
};
|
|
2278
|
+
|
|
2279
|
+
export type IntFilter = {
|
|
2280
|
+
equals?: number;
|
|
2281
|
+
not?: number | IntFilter;
|
|
2282
|
+
in?: number[];
|
|
2283
|
+
notIn?: number[];
|
|
2284
|
+
lt?: number;
|
|
2285
|
+
lte?: number;
|
|
2286
|
+
gt?: number;
|
|
2287
|
+
gte?: number;
|
|
2288
|
+
};
|
|
2289
|
+
|
|
2290
|
+
export type FloatFilter = IntFilter;
|
|
2291
|
+
export type BigIntFilter = {
|
|
2292
|
+
equals?: bigint;
|
|
2293
|
+
not?: bigint | BigIntFilter;
|
|
2294
|
+
in?: bigint[];
|
|
2295
|
+
notIn?: bigint[];
|
|
2296
|
+
lt?: bigint;
|
|
2297
|
+
lte?: bigint;
|
|
2298
|
+
gt?: bigint;
|
|
2299
|
+
gte?: bigint;
|
|
2300
|
+
};
|
|
2301
|
+
|
|
2302
|
+
export type BoolFilter = { equals?: boolean; not?: boolean };
|
|
2303
|
+
|
|
2304
|
+
export type DateTimeFilter = {
|
|
2305
|
+
equals?: Date;
|
|
2306
|
+
not?: Date | DateTimeFilter;
|
|
2307
|
+
in?: Date[];
|
|
2308
|
+
notIn?: Date[];
|
|
2309
|
+
lt?: Date;
|
|
2310
|
+
lte?: Date;
|
|
2311
|
+
gt?: Date;
|
|
2312
|
+
gte?: Date;
|
|
2313
|
+
};
|
|
2314
|
+
|
|
2315
|
+
export type BytesFilter = { equals?: Buffer; not?: Buffer };
|
|
2316
|
+
|
|
2317
|
+
// Firebird has no JSON SQL functions: filters operate on the serialized text.
|
|
2318
|
+
// 'path' is accepted by the type but rejected at runtime on Firebird.
|
|
2319
|
+
export type JsonFilter = {
|
|
2320
|
+
equals?: JsonValue;
|
|
2321
|
+
not?: JsonValue;
|
|
2322
|
+
string_contains?: string;
|
|
2323
|
+
string_starts_with?: string;
|
|
2324
|
+
string_ends_with?: string;
|
|
2325
|
+
path?: string[];
|
|
2326
|
+
};`;
|
|
2327
|
+
function isGeneratedOnCreate(field) {
|
|
2328
|
+
return field.default?.function?.name === "autoincrement";
|
|
2329
|
+
}
|
|
2330
|
+
function isOptionalOnCreate(field) {
|
|
2331
|
+
return !field.isRequired || !!field.default || field.isUpdatedAt || field.isList;
|
|
2332
|
+
}
|
|
2333
|
+
function indentBlock(text) {
|
|
2334
|
+
return text.split("\n").map((l) => l ? " " + l : l).join("\n");
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/migrate/index.ts
|
|
2338
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2339
|
+
import { join as join2 } from "path";
|
|
2340
|
+
|
|
2341
|
+
// src/migrate/diff.ts
|
|
2342
|
+
function diffSchemas(desired, current) {
|
|
2343
|
+
const currentByTable = new Map(
|
|
2344
|
+
current.models.map((m) => [modelTable(m), m])
|
|
2345
|
+
);
|
|
2346
|
+
const desiredTables = new Set(desired.models.map((m) => modelTable(m)));
|
|
2347
|
+
const createdModels = [];
|
|
2348
|
+
const modelChanges = [];
|
|
2349
|
+
for (const model of desired.models) {
|
|
2350
|
+
const table = modelTable(model);
|
|
2351
|
+
const currentModel = currentByTable.get(table);
|
|
2352
|
+
if (!currentModel) {
|
|
2353
|
+
createdModels.push(model);
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
const change = diffModel(desired, model, currentModel, table);
|
|
2357
|
+
if (hasModelChanges(change)) modelChanges.push(change);
|
|
2358
|
+
}
|
|
2359
|
+
const droppedTables = current.models.map((m) => modelTable(m)).filter((t) => !desiredTables.has(t));
|
|
2360
|
+
return { createdModels, droppedTables, modelChanges };
|
|
2361
|
+
}
|
|
2362
|
+
function diffModel(desired, model, current, table) {
|
|
2363
|
+
const desiredCols = new Map(
|
|
2364
|
+
scalarFields(model).map((f) => [fieldColumn(f), f])
|
|
2365
|
+
);
|
|
2366
|
+
const currentCols = new Map(
|
|
2367
|
+
scalarFields(current).map((f) => [fieldColumn(f), f])
|
|
2368
|
+
);
|
|
2369
|
+
const addedColumns = [];
|
|
2370
|
+
const changedColumns = [];
|
|
2371
|
+
for (const [col, field] of desiredCols) {
|
|
2372
|
+
const existing = currentCols.get(col);
|
|
2373
|
+
if (!existing) {
|
|
2374
|
+
addedColumns.push(field);
|
|
2375
|
+
continue;
|
|
2376
|
+
}
|
|
2377
|
+
const typeChanged = !sameColumnType(field, existing);
|
|
2378
|
+
const nullabilityChanged = field.isRequired !== existing.isRequired;
|
|
2379
|
+
if (typeChanged || nullabilityChanged) {
|
|
2380
|
+
changedColumns.push({ field, table, typeChanged, nullabilityChanged });
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
const droppedColumns = [...currentCols.keys()].filter(
|
|
2384
|
+
(c) => !desiredCols.has(c)
|
|
2385
|
+
);
|
|
2386
|
+
const currentUniqueSets = uniqueColumnSets(current).map(setKey);
|
|
2387
|
+
const addedUniques = uniqueColumnSets(model).filter((cols) => !currentUniqueSets.includes(setKey(cols))).map((cols) => ({ name: constraintName("UQ", table, cols), columns: cols }));
|
|
2388
|
+
const currentFkSets = foreignKeys(desired, current).map(fkKey);
|
|
2389
|
+
const addedForeignKeys = foreignKeys(desired, model).filter((fk) => !currentFkSets.includes(fkKey(fk))).map((fk) => ({ ...fk, name: constraintName("FK", table, fk.columns) }));
|
|
2390
|
+
return {
|
|
2391
|
+
model,
|
|
2392
|
+
table,
|
|
2393
|
+
addedColumns,
|
|
2394
|
+
droppedColumns,
|
|
2395
|
+
changedColumns,
|
|
2396
|
+
addedUniques,
|
|
2397
|
+
addedForeignKeys
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
function hasModelChanges(c) {
|
|
2401
|
+
return c.addedColumns.length > 0 || c.droppedColumns.length > 0 || c.changedColumns.length > 0 || c.addedUniques.length > 0 || c.addedForeignKeys.length > 0;
|
|
2402
|
+
}
|
|
2403
|
+
function uniqueColumnSets(model) {
|
|
2404
|
+
const sets = [];
|
|
2405
|
+
for (const f of scalarFields(model)) {
|
|
2406
|
+
if (f.isUnique) sets.push([fieldColumn(f)]);
|
|
2407
|
+
}
|
|
2408
|
+
for (const u of model.uniqueIndexes) {
|
|
2409
|
+
sets.push(u.fields.map((name) => columnFor(model, name)));
|
|
2410
|
+
}
|
|
2411
|
+
return sets;
|
|
2412
|
+
}
|
|
2413
|
+
function indexSpecs(model) {
|
|
2414
|
+
return model.indexes.map((i) => ({
|
|
2415
|
+
name: i.name ?? constraintName("IDX", modelTable(model), i.fields.map((n) => columnFor(model, n))),
|
|
2416
|
+
columns: i.fields.map((name) => columnFor(model, name)),
|
|
2417
|
+
unique: i.unique
|
|
2418
|
+
}));
|
|
2419
|
+
}
|
|
2420
|
+
function foreignKeys(schema, model) {
|
|
2421
|
+
const out = [];
|
|
2422
|
+
for (const f of relationFields(model)) {
|
|
2423
|
+
const rel2 = f.relation;
|
|
2424
|
+
if (!rel2?.fields?.length) continue;
|
|
2425
|
+
const refModel = schema.models.find((m) => m.name === f.type);
|
|
2426
|
+
if (!refModel) continue;
|
|
2427
|
+
out.push({
|
|
2428
|
+
columns: rel2.fields.map((name) => columnFor(model, name)),
|
|
2429
|
+
refTable: modelTable(refModel),
|
|
2430
|
+
refColumns: (rel2.references ?? []).map((name) => columnFor(refModel, name)),
|
|
2431
|
+
onDelete: rel2.onDelete,
|
|
2432
|
+
onUpdate: rel2.onUpdate
|
|
2433
|
+
});
|
|
2434
|
+
}
|
|
2435
|
+
return out;
|
|
2436
|
+
}
|
|
2437
|
+
function columnFor(model, fieldName) {
|
|
2438
|
+
const f = model.fields.find((x) => x.name === fieldName);
|
|
2439
|
+
return f ? fieldColumn(f) : fieldName.toUpperCase();
|
|
2440
|
+
}
|
|
2441
|
+
function sameColumnType(a, b) {
|
|
2442
|
+
if (a.type !== b.type) return false;
|
|
2443
|
+
return sameNative(a.nativeType, b.nativeType);
|
|
2444
|
+
}
|
|
2445
|
+
function sameNative(a, b) {
|
|
2446
|
+
if (!a && !b) return true;
|
|
2447
|
+
if (!a || !b) return true;
|
|
2448
|
+
return a.name === b.name && a.args.join(",") === b.args.join(",");
|
|
2449
|
+
}
|
|
2450
|
+
function setKey(cols) {
|
|
2451
|
+
return [...cols].sort().join(",");
|
|
2452
|
+
}
|
|
2453
|
+
function fkKey(fk) {
|
|
2454
|
+
return `${setKey(fk.columns)}->${fk.refTable}(${setKey(fk.refColumns)})`;
|
|
2455
|
+
}
|
|
2456
|
+
function constraintName(prefix, table, columns) {
|
|
2457
|
+
const base = `${prefix}_${table}_${columns.join("_")}`.replace(/[^A-Za-z0-9_]/g, "_");
|
|
2458
|
+
if (base.length <= 31) return base;
|
|
2459
|
+
let hash = 0;
|
|
2460
|
+
for (let i = 0; i < base.length; i++) hash = hash * 31 + base.charCodeAt(i) | 0;
|
|
2461
|
+
const suffix = Math.abs(hash).toString(36).slice(0, 6);
|
|
2462
|
+
return `${base.slice(0, 24)}_${suffix}`;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// src/migrate/ddl.ts
|
|
2466
|
+
var FirebirdDdl = class {
|
|
2467
|
+
constructor(d) {
|
|
2468
|
+
this.d = d;
|
|
2469
|
+
}
|
|
2470
|
+
d;
|
|
2471
|
+
// ---- column types -------------------------------------------------------
|
|
2472
|
+
/** Firebird column type for a field, honoring its `@db.*` native type. */
|
|
2473
|
+
columnType(field) {
|
|
2474
|
+
const native = field.nativeType;
|
|
2475
|
+
if (native) {
|
|
2476
|
+
const args = native.args.length ? `(${native.args.join(", ")})` : "";
|
|
2477
|
+
switch (native.name) {
|
|
2478
|
+
case "VarChar":
|
|
2479
|
+
return `VARCHAR${args || "(255)"}`;
|
|
2480
|
+
case "Char":
|
|
2481
|
+
return `CHAR${args || "(1)"}`;
|
|
2482
|
+
case "Text":
|
|
2483
|
+
return "BLOB SUB_TYPE TEXT";
|
|
2484
|
+
case "SmallInt":
|
|
2485
|
+
return "SMALLINT";
|
|
2486
|
+
case "Integer":
|
|
2487
|
+
return "INTEGER";
|
|
2488
|
+
case "BigInt":
|
|
2489
|
+
return "BIGINT";
|
|
2490
|
+
case "Float":
|
|
2491
|
+
return "FLOAT";
|
|
2492
|
+
case "DoublePrecision":
|
|
2493
|
+
return "DOUBLE PRECISION";
|
|
2494
|
+
case "Decimal":
|
|
2495
|
+
return `DECIMAL${args || "(18, 4)"}`;
|
|
2496
|
+
case "Boolean":
|
|
2497
|
+
return this.d.booleanColumnType();
|
|
2498
|
+
case "Date":
|
|
2499
|
+
return "DATE";
|
|
2500
|
+
case "Time":
|
|
2501
|
+
return "TIME";
|
|
2502
|
+
case "Timestamp":
|
|
2503
|
+
return "TIMESTAMP";
|
|
2504
|
+
case "Blob":
|
|
2505
|
+
return "BLOB SUB_TYPE BINARY";
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
if (field.type === "Boolean") return this.d.booleanColumnType();
|
|
2509
|
+
return DEFAULT_DDL_TYPE[field.type] ?? "VARCHAR(255)";
|
|
2510
|
+
}
|
|
2511
|
+
/** `"COL" <type> [GENERATED ...] [DEFAULT ...] [NOT NULL]`. */
|
|
2512
|
+
columnDefinition(field) {
|
|
2513
|
+
const parts = [this.d.quoteId(fieldColumn(field)), this.columnType(field)];
|
|
2514
|
+
if (isIdentity(field) && this.d.supportsIdentity) {
|
|
2515
|
+
parts.push("GENERATED BY DEFAULT AS IDENTITY");
|
|
2516
|
+
} else if (!isIdentity(field)) {
|
|
2517
|
+
const def = defaultClause(field.default);
|
|
2518
|
+
if (def) parts.push(def);
|
|
2519
|
+
}
|
|
2520
|
+
if (field.isRequired) parts.push("NOT NULL");
|
|
2521
|
+
return parts.join(" ");
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* On Firebird 2.1/2.5 (no IDENTITY), emulate autoincrement with a SEQUENCE
|
|
2525
|
+
* and a BEFORE INSERT trigger. Returns the extra DDL objects to create after
|
|
2526
|
+
* the table; an empty array when IDENTITY is supported.
|
|
2527
|
+
*/
|
|
2528
|
+
autoIncrementObjects(model) {
|
|
2529
|
+
if (this.d.supportsIdentity) return [];
|
|
2530
|
+
const table = modelTable(model);
|
|
2531
|
+
const out = [];
|
|
2532
|
+
for (const field of scalarFields(model)) {
|
|
2533
|
+
if (isIdentity(field)) {
|
|
2534
|
+
out.push(...this.autoIncrementForColumn(table, fieldColumn(field)));
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
return out;
|
|
2538
|
+
}
|
|
2539
|
+
autoIncrementForColumn(table, column) {
|
|
2540
|
+
if (this.d.supportsIdentity) return [];
|
|
2541
|
+
const seq = capName(`GEN_${table}_${column}`);
|
|
2542
|
+
const trig = capName(`${table}_BI_${column}`);
|
|
2543
|
+
const qTable = this.d.quoteId(table);
|
|
2544
|
+
const qCol = this.d.quoteId(column);
|
|
2545
|
+
const qSeq = this.d.quoteId(seq);
|
|
2546
|
+
return [
|
|
2547
|
+
`CREATE SEQUENCE ${qSeq}`,
|
|
2548
|
+
`CREATE TRIGGER ${this.d.quoteId(trig)} FOR ${qTable} ACTIVE BEFORE INSERT POSITION 0 AS
|
|
2549
|
+
BEGIN
|
|
2550
|
+
IF (NEW.${qCol} IS NULL) THEN NEW.${qCol} = GEN_ID(${qSeq}, 1);
|
|
2551
|
+
END`
|
|
2552
|
+
];
|
|
2553
|
+
}
|
|
2554
|
+
// ---- table-level --------------------------------------------------------
|
|
2555
|
+
createTable(model) {
|
|
2556
|
+
const cols = scalarFields(model).map((f) => " " + this.columnDefinition(f));
|
|
2557
|
+
const pk = idFields(model);
|
|
2558
|
+
const lines = [...cols];
|
|
2559
|
+
if (pk.length > 0) {
|
|
2560
|
+
lines.push(
|
|
2561
|
+
` PRIMARY KEY (${pk.map((f) => this.d.quoteId(fieldColumn(f))).join(", ")})`
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
return `CREATE TABLE ${this.d.quoteId(modelTable(model))} (
|
|
2565
|
+
${lines.join(",\n")}
|
|
2566
|
+
)`;
|
|
2567
|
+
}
|
|
2568
|
+
dropTable(table) {
|
|
2569
|
+
return `DROP TABLE ${this.d.quoteId(table)}`;
|
|
2570
|
+
}
|
|
2571
|
+
addColumn(table, field) {
|
|
2572
|
+
return `ALTER TABLE ${this.d.quoteId(table)} ADD ${this.columnDefinition(field)}`;
|
|
2573
|
+
}
|
|
2574
|
+
dropColumn(table, column) {
|
|
2575
|
+
return `ALTER TABLE ${this.d.quoteId(table)} DROP ${this.d.quoteId(column)}`;
|
|
2576
|
+
}
|
|
2577
|
+
/** Change a column's data type (Firebird: ALTER COLUMN ... TYPE ...). */
|
|
2578
|
+
alterColumnType(table, field) {
|
|
2579
|
+
return `ALTER TABLE ${this.d.quoteId(table)} ALTER COLUMN ${this.d.quoteId(
|
|
2580
|
+
fieldColumn(field)
|
|
2581
|
+
)} TYPE ${this.columnType(field)}`;
|
|
2582
|
+
}
|
|
2583
|
+
setNotNull(table, column, notNull) {
|
|
2584
|
+
const action = notNull ? "SET NOT NULL" : "DROP NOT NULL";
|
|
2585
|
+
return `ALTER TABLE ${this.d.quoteId(table)} ALTER COLUMN ${this.d.quoteId(
|
|
2586
|
+
column
|
|
2587
|
+
)} ${action}`;
|
|
2588
|
+
}
|
|
2589
|
+
// ---- constraints & indexes ---------------------------------------------
|
|
2590
|
+
addUnique(table, name, columns) {
|
|
2591
|
+
return `ALTER TABLE ${this.d.quoteId(table)} ADD CONSTRAINT ${this.d.quoteId(
|
|
2592
|
+
name
|
|
2593
|
+
)} UNIQUE (${columns.map((c) => this.d.quoteId(c)).join(", ")})`;
|
|
2594
|
+
}
|
|
2595
|
+
dropConstraint(table, name) {
|
|
2596
|
+
return `ALTER TABLE ${this.d.quoteId(table)} DROP CONSTRAINT ${this.d.quoteId(name)}`;
|
|
2597
|
+
}
|
|
2598
|
+
addForeignKey(table, name, columns, refTable, refColumns, onDelete, onUpdate) {
|
|
2599
|
+
let sql = `ALTER TABLE ${this.d.quoteId(table)} ADD CONSTRAINT ${this.d.quoteId(
|
|
2600
|
+
name
|
|
2601
|
+
)} FOREIGN KEY (${columns.map((c) => this.d.quoteId(c)).join(", ")}) REFERENCES ${this.d.quoteId(
|
|
2602
|
+
refTable
|
|
2603
|
+
)} (${refColumns.map((c) => this.d.quoteId(c)).join(", ")})`;
|
|
2604
|
+
if (onUpdate) sql += ` ON UPDATE ${referentialSql(onUpdate)}`;
|
|
2605
|
+
if (onDelete) sql += ` ON DELETE ${referentialSql(onDelete)}`;
|
|
2606
|
+
return sql;
|
|
2607
|
+
}
|
|
2608
|
+
createIndex(table, name, columns, unique) {
|
|
2609
|
+
return `CREATE ${unique ? "UNIQUE " : ""}INDEX ${this.d.quoteId(name)} ON ${this.d.quoteId(
|
|
2610
|
+
table
|
|
2611
|
+
)} (${columns.map((c) => this.d.quoteId(c)).join(", ")})`;
|
|
2612
|
+
}
|
|
2613
|
+
dropIndex(name) {
|
|
2614
|
+
return `DROP INDEX ${this.d.quoteId(name)}`;
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
var DEFAULT_DDL_TYPE = {
|
|
2618
|
+
String: "VARCHAR(255)",
|
|
2619
|
+
Boolean: "BOOLEAN",
|
|
2620
|
+
Int: "INTEGER",
|
|
2621
|
+
BigInt: "BIGINT",
|
|
2622
|
+
Float: "DOUBLE PRECISION",
|
|
2623
|
+
Decimal: "DECIMAL(18, 4)",
|
|
2624
|
+
DateTime: "TIMESTAMP",
|
|
2625
|
+
Bytes: "BLOB SUB_TYPE BINARY",
|
|
2626
|
+
Json: "BLOB SUB_TYPE TEXT"
|
|
2627
|
+
};
|
|
2628
|
+
function isIdentity(field) {
|
|
2629
|
+
return field.default?.function?.name === "autoincrement";
|
|
2630
|
+
}
|
|
2631
|
+
function capName(name) {
|
|
2632
|
+
const safe = name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
2633
|
+
if (safe.length <= 31) return safe;
|
|
2634
|
+
let hash = 0;
|
|
2635
|
+
for (let i = 0; i < safe.length; i++) hash = hash * 31 + safe.charCodeAt(i) | 0;
|
|
2636
|
+
return `${safe.slice(0, 24)}_${Math.abs(hash).toString(36).slice(0, 6)}`;
|
|
2637
|
+
}
|
|
2638
|
+
function defaultClause(def) {
|
|
2639
|
+
if (!def) return null;
|
|
2640
|
+
if (def.function) {
|
|
2641
|
+
switch (def.function.name) {
|
|
2642
|
+
case "now":
|
|
2643
|
+
return "DEFAULT CURRENT_TIMESTAMP";
|
|
2644
|
+
case "uuid":
|
|
2645
|
+
case "cuid":
|
|
2646
|
+
return null;
|
|
2647
|
+
// generated app-side
|
|
2648
|
+
default:
|
|
2649
|
+
return null;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
if (def.literal === void 0) return null;
|
|
2653
|
+
if (typeof def.literal === "boolean") {
|
|
2654
|
+
return `DEFAULT ${def.literal ? "TRUE" : "FALSE"}`;
|
|
2655
|
+
}
|
|
2656
|
+
if (typeof def.literal === "number") return `DEFAULT ${def.literal}`;
|
|
2657
|
+
return `DEFAULT '${String(def.literal).replace(/'/g, "''")}'`;
|
|
2658
|
+
}
|
|
2659
|
+
function referentialSql(action) {
|
|
2660
|
+
switch (action) {
|
|
2661
|
+
case "Cascade":
|
|
2662
|
+
return "CASCADE";
|
|
2663
|
+
case "SetNull":
|
|
2664
|
+
return "SET NULL";
|
|
2665
|
+
case "SetDefault":
|
|
2666
|
+
return "SET DEFAULT";
|
|
2667
|
+
case "NoAction":
|
|
2668
|
+
return "NO ACTION";
|
|
2669
|
+
case "Restrict":
|
|
2670
|
+
return "NO ACTION";
|
|
2671
|
+
// Firebird has no RESTRICT; NO ACTION is closest
|
|
2672
|
+
default:
|
|
2673
|
+
return "NO ACTION";
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
// src/migrate/planner.ts
|
|
2678
|
+
function planMigration(diff, desired, dialect) {
|
|
2679
|
+
const ddl = new FirebirdDdl(dialect);
|
|
2680
|
+
const drops = [];
|
|
2681
|
+
const creates = [];
|
|
2682
|
+
const alters = [];
|
|
2683
|
+
const constraints = [];
|
|
2684
|
+
const indexes = [];
|
|
2685
|
+
const foreignKeyStmts = [];
|
|
2686
|
+
for (const change of diff.modelChanges) {
|
|
2687
|
+
for (const col of change.droppedColumns) {
|
|
2688
|
+
drops.push(ddl.dropColumn(change.table, col));
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
for (const table of diff.droppedTables) {
|
|
2692
|
+
drops.push(ddl.dropTable(table));
|
|
2693
|
+
}
|
|
2694
|
+
for (const model of diff.createdModels) {
|
|
2695
|
+
const table = modelTable(model);
|
|
2696
|
+
creates.push(ddl.createTable(model));
|
|
2697
|
+
creates.push(...ddl.autoIncrementObjects(model));
|
|
2698
|
+
for (const cols of uniqueColumnSets(model)) {
|
|
2699
|
+
if (cols.length === 1) continue;
|
|
2700
|
+
constraints.push(
|
|
2701
|
+
ddl.addUnique(table, constraintName("UQ", table, cols), cols)
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
for (const idx of indexSpecs(model)) {
|
|
2705
|
+
indexes.push(ddl.createIndex(table, idx.name, idx.columns, idx.unique));
|
|
2706
|
+
}
|
|
2707
|
+
for (const fk of foreignKeys(desired, model)) {
|
|
2708
|
+
foreignKeyStmts.push(
|
|
2709
|
+
ddl.addForeignKey(
|
|
2710
|
+
table,
|
|
2711
|
+
constraintName("FK", table, fk.columns),
|
|
2712
|
+
fk.columns,
|
|
2713
|
+
fk.refTable,
|
|
2714
|
+
fk.refColumns,
|
|
2715
|
+
fk.onDelete,
|
|
2716
|
+
fk.onUpdate
|
|
2717
|
+
)
|
|
2718
|
+
);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
for (const change of diff.modelChanges) {
|
|
2722
|
+
for (const field of change.addedColumns) {
|
|
2723
|
+
alters.push(ddl.addColumn(change.table, field));
|
|
2724
|
+
if (isIdentity(field)) {
|
|
2725
|
+
creates.push(
|
|
2726
|
+
...ddl.autoIncrementForColumn(change.table, fieldColumn(field))
|
|
2727
|
+
);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
for (const cc of change.changedColumns) {
|
|
2731
|
+
if (cc.typeChanged) alters.push(ddl.alterColumnType(cc.table, cc.field));
|
|
2732
|
+
if (cc.nullabilityChanged) {
|
|
2733
|
+
alters.push(
|
|
2734
|
+
ddl.setNotNull(cc.table, columnName(cc), cc.field.isRequired)
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
for (const uq of change.addedUniques) {
|
|
2739
|
+
if (uq.columns.length === 1) {
|
|
2740
|
+
constraints.push(ddl.addUnique(change.table, uq.name, uq.columns));
|
|
2741
|
+
} else {
|
|
2742
|
+
constraints.push(ddl.addUnique(change.table, uq.name, uq.columns));
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
for (const fk of change.addedForeignKeys) {
|
|
2746
|
+
foreignKeyStmts.push(
|
|
2747
|
+
ddl.addForeignKey(
|
|
2748
|
+
change.table,
|
|
2749
|
+
fk.name,
|
|
2750
|
+
fk.columns,
|
|
2751
|
+
fk.refTable,
|
|
2752
|
+
fk.refColumns,
|
|
2753
|
+
fk.onDelete,
|
|
2754
|
+
fk.onUpdate
|
|
2755
|
+
)
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
return [
|
|
2760
|
+
...drops,
|
|
2761
|
+
...creates,
|
|
2762
|
+
...alters,
|
|
2763
|
+
...constraints,
|
|
2764
|
+
...indexes,
|
|
2765
|
+
...foreignKeyStmts
|
|
2766
|
+
];
|
|
2767
|
+
}
|
|
2768
|
+
var BREAKPOINT = "--> statement-breakpoint";
|
|
2769
|
+
function renderMigrationSql(statements) {
|
|
2770
|
+
if (statements.length === 0) return "-- This migration is empty.\n";
|
|
2771
|
+
return statements.join(`
|
|
2772
|
+
${BREAKPOINT}
|
|
2773
|
+
`) + "\n";
|
|
2774
|
+
}
|
|
2775
|
+
function splitStatements(sql) {
|
|
2776
|
+
return sql.split(BREAKPOINT).map((s) => stripComments(s).trim().replace(/;\s*$/, "").trim()).filter((s) => s.length > 0);
|
|
2777
|
+
}
|
|
2778
|
+
function stripComments(sql) {
|
|
2779
|
+
return sql.split("\n").filter((line) => !line.trim().startsWith("--")).join("\n");
|
|
2780
|
+
}
|
|
2781
|
+
function columnName(cc) {
|
|
2782
|
+
return cc.field.dbName ?? cc.field.name.toUpperCase();
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
// src/migrate/history.ts
|
|
2786
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
2787
|
+
import { join } from "path";
|
|
2788
|
+
var HISTORY_TABLE = "_EMBER_MIGRATIONS";
|
|
2789
|
+
async function ensureHistoryTable(tx, dialect) {
|
|
2790
|
+
const exists = await tx.query(
|
|
2791
|
+
`SELECT COUNT(*) AS N FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = ?`,
|
|
2792
|
+
[HISTORY_TABLE]
|
|
2793
|
+
);
|
|
2794
|
+
if (Number(exists[0]?.N ?? 0) > 0) return;
|
|
2795
|
+
const t = dialect.quoteId(HISTORY_TABLE);
|
|
2796
|
+
await tx.query(
|
|
2797
|
+
`CREATE TABLE ${t} (
|
|
2798
|
+
${dialect.quoteId("ID")} VARCHAR(128) NOT NULL PRIMARY KEY,
|
|
2799
|
+
${dialect.quoteId("CHECKSUM")} VARCHAR(64),
|
|
2800
|
+
${dialect.quoteId("STEPS")} INTEGER,
|
|
2801
|
+
${dialect.quoteId("APPLIED_AT")} TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
2802
|
+
)`
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
async function appliedMigrations(tx, dialect) {
|
|
2806
|
+
const t = dialect.quoteId(HISTORY_TABLE);
|
|
2807
|
+
const rows = await tx.query(
|
|
2808
|
+
`SELECT ${dialect.quoteId("ID")} AS "id", ${dialect.quoteId("CHECKSUM")} AS "checksum", ${dialect.quoteId("STEPS")} AS "steps" FROM ${t} ORDER BY ${dialect.quoteId("ID")}`
|
|
2809
|
+
);
|
|
2810
|
+
return rows.map((r) => ({
|
|
2811
|
+
id: String(r.id).trim(),
|
|
2812
|
+
checksum: r.checksum == null ? "" : String(r.checksum).trim(),
|
|
2813
|
+
steps: Number(r.steps ?? 0)
|
|
2814
|
+
}));
|
|
2815
|
+
}
|
|
2816
|
+
async function recordMigration(tx, dialect, migration) {
|
|
2817
|
+
const t = dialect.quoteId(HISTORY_TABLE);
|
|
2818
|
+
await tx.query(
|
|
2819
|
+
`INSERT INTO ${t} (${dialect.quoteId("ID")}, ${dialect.quoteId("CHECKSUM")}, ${dialect.quoteId("STEPS")}) VALUES (?, ?, ?)`,
|
|
2820
|
+
[migration.id, migration.checksum, migration.steps]
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
function listLocalMigrations(migrationsDir) {
|
|
2824
|
+
if (!existsSync2(migrationsDir)) return [];
|
|
2825
|
+
return readdirSync(migrationsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort().map((id) => {
|
|
2826
|
+
const dir = join(migrationsDir, id);
|
|
2827
|
+
const file = join(dir, "migration.sql");
|
|
2828
|
+
return {
|
|
2829
|
+
id,
|
|
2830
|
+
dir,
|
|
2831
|
+
sql: existsSync2(file) ? readFileSync2(file, "utf8") : ""
|
|
2832
|
+
};
|
|
2833
|
+
}).filter((m) => m.sql.length > 0);
|
|
2834
|
+
}
|
|
2835
|
+
function checksum(sql) {
|
|
2836
|
+
let h = 2166136261;
|
|
2837
|
+
for (let i = 0; i < sql.length; i++) {
|
|
2838
|
+
h ^= sql.charCodeAt(i);
|
|
2839
|
+
h = Math.imul(h, 16777619);
|
|
2840
|
+
}
|
|
2841
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// src/migrate/index.ts
|
|
2845
|
+
var Migrator = class {
|
|
2846
|
+
constructor(driver, desired, migrationsDir, dialect) {
|
|
2847
|
+
this.driver = driver;
|
|
2848
|
+
this.desired = desired;
|
|
2849
|
+
this.migrationsDir = migrationsDir;
|
|
2850
|
+
this.dialect = dialect ?? new FirebirdDialect();
|
|
2851
|
+
}
|
|
2852
|
+
driver;
|
|
2853
|
+
desired;
|
|
2854
|
+
migrationsDir;
|
|
2855
|
+
dialect;
|
|
2856
|
+
/** Compute the diff between the desired schema and the live database. */
|
|
2857
|
+
async diff() {
|
|
2858
|
+
const current = await this.currentSchema();
|
|
2859
|
+
return diffSchemas(this.desired, current);
|
|
2860
|
+
}
|
|
2861
|
+
/** Plan (but do not apply) the DDL needed to reach the desired schema. */
|
|
2862
|
+
async plan() {
|
|
2863
|
+
const diff = await this.diff();
|
|
2864
|
+
return planMigration(diff, this.desired, this.dialect);
|
|
2865
|
+
}
|
|
2866
|
+
/**
|
|
2867
|
+
* `migrate dev`: create a timestamped migration from the current diff, apply
|
|
2868
|
+
* it, and record it in history.
|
|
2869
|
+
*/
|
|
2870
|
+
async dev(name = "migration") {
|
|
2871
|
+
const statements = await this.plan();
|
|
2872
|
+
if (statements.length === 0) return { empty: true, statements: [] };
|
|
2873
|
+
const id = `${timestamp()}_${slug(name)}`;
|
|
2874
|
+
const dir = join2(this.migrationsDir, id);
|
|
2875
|
+
const body = renderMigrationSql(statements);
|
|
2876
|
+
mkdirSync2(dir, { recursive: true });
|
|
2877
|
+
writeFileSync2(join2(dir, "migration.sql"), body, "utf8");
|
|
2878
|
+
await this.driver.transaction(async (tx) => {
|
|
2879
|
+
await ensureHistoryTable(tx, this.dialect);
|
|
2880
|
+
for (const stmt of statements) await tx.query(stmt);
|
|
2881
|
+
await recordMigration(tx, this.dialect, {
|
|
2882
|
+
id,
|
|
2883
|
+
checksum: checksum(body),
|
|
2884
|
+
steps: statements.length
|
|
2885
|
+
});
|
|
2886
|
+
});
|
|
2887
|
+
return { empty: false, id, dir, statements };
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* `db push`: apply the diff directly to the database without writing a
|
|
2891
|
+
* migration file (prototyping flow).
|
|
2892
|
+
*/
|
|
2893
|
+
async push() {
|
|
2894
|
+
const statements = await this.plan();
|
|
2895
|
+
if (statements.length === 0) return { statements: [] };
|
|
2896
|
+
await this.driver.transaction(async (tx) => {
|
|
2897
|
+
for (const stmt of statements) await tx.query(stmt);
|
|
2898
|
+
});
|
|
2899
|
+
return { statements };
|
|
2900
|
+
}
|
|
2901
|
+
/** `migrate deploy`: apply every on-disk migration not yet recorded. */
|
|
2902
|
+
async deploy() {
|
|
2903
|
+
const local = listLocalMigrations(this.migrationsDir);
|
|
2904
|
+
const applied = [];
|
|
2905
|
+
const known = await this.driver.transaction(async (tx) => {
|
|
2906
|
+
await ensureHistoryTable(tx, this.dialect);
|
|
2907
|
+
return new Set((await appliedMigrations(tx, this.dialect)).map((m) => m.id));
|
|
2908
|
+
});
|
|
2909
|
+
for (const migration of local) {
|
|
2910
|
+
if (known.has(migration.id)) continue;
|
|
2911
|
+
const statements = splitStatements(migration.sql);
|
|
2912
|
+
await this.driver.transaction(async (tx) => {
|
|
2913
|
+
for (const stmt of statements) await tx.query(stmt);
|
|
2914
|
+
await recordMigration(tx, this.dialect, {
|
|
2915
|
+
id: migration.id,
|
|
2916
|
+
checksum: checksum(migration.sql),
|
|
2917
|
+
steps: statements.length
|
|
2918
|
+
});
|
|
2919
|
+
});
|
|
2920
|
+
applied.push({ id: migration.id, steps: statements.length });
|
|
2921
|
+
}
|
|
2922
|
+
return { applied };
|
|
2923
|
+
}
|
|
2924
|
+
/** `migrate status`: list applied vs pending migrations. */
|
|
2925
|
+
async status() {
|
|
2926
|
+
const local = listLocalMigrations(this.migrationsDir).map((m) => m.id);
|
|
2927
|
+
const applied = await this.driver.transaction(async (tx) => {
|
|
2928
|
+
await ensureHistoryTable(tx, this.dialect);
|
|
2929
|
+
return (await appliedMigrations(tx, this.dialect)).map((m) => m.id);
|
|
2930
|
+
});
|
|
2931
|
+
const appliedSet = new Set(applied);
|
|
2932
|
+
return {
|
|
2933
|
+
applied,
|
|
2934
|
+
pending: local.filter((id) => !appliedSet.has(id))
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
async currentSchema() {
|
|
2938
|
+
const introspector = new Introspector(this.driver);
|
|
2939
|
+
const current = await introspector.introspect();
|
|
2940
|
+
current.models = current.models.filter(
|
|
2941
|
+
(m) => modelTable(m) !== HISTORY_TABLE
|
|
2942
|
+
);
|
|
2943
|
+
return current;
|
|
2944
|
+
}
|
|
2945
|
+
};
|
|
2946
|
+
function timestamp() {
|
|
2947
|
+
const d = /* @__PURE__ */ new Date();
|
|
2948
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
2949
|
+
return `${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}`;
|
|
2950
|
+
}
|
|
2951
|
+
function slug(name) {
|
|
2952
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "migration";
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// src/cli/commands.ts
|
|
2956
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2957
|
+
var DEFAULT_SCHEMA = "ember/schema.ember";
|
|
2958
|
+
var STARTER_SCHEMA = `datasource db {
|
|
2959
|
+
provider = "firebird"
|
|
2960
|
+
url = env("DATABASE_URL")
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
generator client {
|
|
2964
|
+
provider = "ember-client-js"
|
|
2965
|
+
output = "../generated"
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
/// Example model \u2014 replace with your own or run \`ember db pull\`.
|
|
2969
|
+
model User {
|
|
2970
|
+
id Int @id @default(autoincrement())
|
|
2971
|
+
email String @unique
|
|
2972
|
+
name String?
|
|
2973
|
+
createdAt DateTime @default(now())
|
|
2974
|
+
}
|
|
2975
|
+
`;
|
|
2976
|
+
function init(ctx) {
|
|
2977
|
+
const target = resolve3(ctx.cwd, DEFAULT_SCHEMA);
|
|
2978
|
+
if (existsSync3(target)) {
|
|
2979
|
+
ctx.error(`Schema already exists at ${target}`);
|
|
2980
|
+
return 1;
|
|
2981
|
+
}
|
|
2982
|
+
mkdirSync3(dirname3(target), { recursive: true });
|
|
2983
|
+
writeFileSync3(target, STARTER_SCHEMA, "utf8");
|
|
2984
|
+
ctx.log(`Created ${rel(ctx.cwd, target)}`);
|
|
2985
|
+
ctx.log("Set DATABASE_URL and run `ember db pull` or `ember generate`.");
|
|
2986
|
+
return 0;
|
|
2987
|
+
}
|
|
2988
|
+
function validate(ctx, schemaPath) {
|
|
2989
|
+
const path = requireSchema(ctx, schemaPath);
|
|
2990
|
+
if (!path) return 1;
|
|
2991
|
+
loadSchema(path);
|
|
2992
|
+
ctx.log(`Schema at ${rel(ctx.cwd, path)} is valid.`);
|
|
2993
|
+
return 0;
|
|
2994
|
+
}
|
|
2995
|
+
function format(ctx, schemaPath) {
|
|
2996
|
+
const path = requireSchema(ctx, schemaPath);
|
|
2997
|
+
if (!path) return 1;
|
|
2998
|
+
const source = readFileSync3(path, "utf8");
|
|
2999
|
+
writeFileSync3(path, formatSchema(source, path), "utf8");
|
|
3000
|
+
ctx.log(`Formatted ${rel(ctx.cwd, path)}`);
|
|
3001
|
+
return 0;
|
|
3002
|
+
}
|
|
3003
|
+
function generate(ctx, schemaPath) {
|
|
3004
|
+
const path = requireSchema(ctx, schemaPath);
|
|
3005
|
+
if (!path) return 1;
|
|
3006
|
+
const { document } = loadSchema(path);
|
|
3007
|
+
const gen = document.generators[0];
|
|
3008
|
+
const out = resolve3(dirname3(path), gen?.output ?? "../generated");
|
|
3009
|
+
const file = writeClient(document, out);
|
|
3010
|
+
ctx.log(`Generated client at ${rel(ctx.cwd, file)}`);
|
|
3011
|
+
return 0;
|
|
3012
|
+
}
|
|
3013
|
+
async function dbPull(ctx, options) {
|
|
3014
|
+
const path = findSchemaPath(ctx.cwd, options.schemaPath) ?? resolve3(ctx.cwd, DEFAULT_SCHEMA);
|
|
3015
|
+
let url = options.url;
|
|
3016
|
+
let envVar = "DATABASE_URL";
|
|
3017
|
+
if (!url && existsSync3(path)) {
|
|
3018
|
+
const { document } = loadSchema(path);
|
|
3019
|
+
url = resolveDatasourceUrl(document, dirname3(path));
|
|
3020
|
+
if (document.datasource?.url.kind === "env") {
|
|
3021
|
+
envVar = document.datasource.url.value;
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
url ??= process.env.DATABASE_URL;
|
|
3025
|
+
if (!url) {
|
|
3026
|
+
ctx.error(
|
|
3027
|
+
"No database URL. Set DATABASE_URL, pass --url, or add a datasource block."
|
|
3028
|
+
);
|
|
3029
|
+
return 1;
|
|
3030
|
+
}
|
|
3031
|
+
const driver = createDriver(url);
|
|
3032
|
+
try {
|
|
3033
|
+
await driver.connect();
|
|
3034
|
+
const introspector = new Introspector(driver);
|
|
3035
|
+
const document = await introspector.introspect({
|
|
3036
|
+
datasource: { name: "db", provider: "firebird", envVar }
|
|
3037
|
+
});
|
|
3038
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
3039
|
+
writeFileSync3(path, printSchema(document), "utf8");
|
|
3040
|
+
ctx.log(
|
|
3041
|
+
`Introspected ${document.models.length} model(s) into ${rel(ctx.cwd, path)}`
|
|
3042
|
+
);
|
|
3043
|
+
return 0;
|
|
3044
|
+
} finally {
|
|
3045
|
+
await driver.disconnect();
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
async function migrateDev(ctx, options) {
|
|
3049
|
+
return withMigrator(ctx, options, async (migrator) => {
|
|
3050
|
+
const result = await migrator.dev(options.name ?? "migration");
|
|
3051
|
+
if (result.empty) {
|
|
3052
|
+
ctx.log("No schema changes \u2014 database already in sync.");
|
|
3053
|
+
return 0;
|
|
3054
|
+
}
|
|
3055
|
+
ctx.log(`Created and applied migration ${result.id} (${result.statements.length} step(s)).`);
|
|
3056
|
+
return 0;
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
async function migrateDeploy(ctx, options) {
|
|
3060
|
+
return withMigrator(ctx, options, async (migrator) => {
|
|
3061
|
+
const { applied } = await migrator.deploy();
|
|
3062
|
+
if (applied.length === 0) {
|
|
3063
|
+
ctx.log("No pending migrations.");
|
|
3064
|
+
return 0;
|
|
3065
|
+
}
|
|
3066
|
+
for (const m of applied) ctx.log(`Applied ${m.id} (${m.steps} step(s)).`);
|
|
3067
|
+
return 0;
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
async function migrateStatus(ctx, options) {
|
|
3071
|
+
return withMigrator(ctx, options, async (migrator) => {
|
|
3072
|
+
const { applied, pending } = await migrator.status();
|
|
3073
|
+
ctx.log(`Applied (${applied.length}):`);
|
|
3074
|
+
for (const id of applied) ctx.log(` \u2713 ${id}`);
|
|
3075
|
+
ctx.log(`Pending (${pending.length}):`);
|
|
3076
|
+
for (const id of pending) ctx.log(` \u2022 ${id}`);
|
|
3077
|
+
return 0;
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
async function dbPush(ctx, options) {
|
|
3081
|
+
return withMigrator(ctx, options, async (migrator) => {
|
|
3082
|
+
const { statements } = await migrator.push();
|
|
3083
|
+
if (statements.length === 0) {
|
|
3084
|
+
ctx.log("No schema changes \u2014 database already in sync.");
|
|
3085
|
+
return 0;
|
|
3086
|
+
}
|
|
3087
|
+
ctx.log(`Applied ${statements.length} statement(s) to the database.`);
|
|
3088
|
+
return 0;
|
|
3089
|
+
});
|
|
3090
|
+
}
|
|
3091
|
+
async function withMigrator(ctx, options, fn) {
|
|
3092
|
+
const path = requireSchema(ctx, options.schemaPath);
|
|
3093
|
+
if (!path) return 1;
|
|
3094
|
+
const { document } = loadSchema(path);
|
|
3095
|
+
const url = options.url ?? resolveDatasourceUrl(document, dirname3(path)) ?? process.env.DATABASE_URL;
|
|
3096
|
+
if (!url) {
|
|
3097
|
+
ctx.error(
|
|
3098
|
+
"No database URL. Set DATABASE_URL, pass --url, or add a datasource block."
|
|
3099
|
+
);
|
|
3100
|
+
return 1;
|
|
3101
|
+
}
|
|
3102
|
+
const migrationsDir = resolve3(dirname3(path), "migrations");
|
|
3103
|
+
const config = parseConnectionUrl(url);
|
|
3104
|
+
const dialect = new FirebirdDialect({ version: config.version });
|
|
3105
|
+
const driver = createDriver(config);
|
|
3106
|
+
try {
|
|
3107
|
+
await driver.connect();
|
|
3108
|
+
const migrator = new Migrator(driver, document, migrationsDir, dialect);
|
|
3109
|
+
return await fn(migrator);
|
|
3110
|
+
} finally {
|
|
3111
|
+
await driver.disconnect();
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
function requireSchema(ctx, schemaPath) {
|
|
3115
|
+
const path = findSchemaPath(ctx.cwd, schemaPath);
|
|
3116
|
+
if (!path) {
|
|
3117
|
+
ctx.error(
|
|
3118
|
+
`Schema not found. Expected ${DEFAULT_SCHEMA} (or pass --schema). Run \`ember init\` to create one.`
|
|
3119
|
+
);
|
|
3120
|
+
return null;
|
|
3121
|
+
}
|
|
3122
|
+
return path;
|
|
3123
|
+
}
|
|
3124
|
+
function rel(cwd, target) {
|
|
3125
|
+
return target.startsWith(cwd) ? target.slice(cwd.length + 1) : target;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// src/cli/bin.ts
|
|
3129
|
+
var HELP = `EmberORM \u2014 Prisma-like ORM for Firebird
|
|
3130
|
+
|
|
3131
|
+
Usage: ember <command> [options]
|
|
3132
|
+
|
|
3133
|
+
Commands:
|
|
3134
|
+
init Scaffold ember/schema.ember
|
|
3135
|
+
db pull Introspect the database into your schema
|
|
3136
|
+
db push Apply schema changes directly (no migration file)
|
|
3137
|
+
generate Generate the typed client from the schema
|
|
3138
|
+
migrate dev Diff, create a migration file, and apply it
|
|
3139
|
+
migrate deploy Apply all pending migration files
|
|
3140
|
+
migrate status Show applied vs pending migrations
|
|
3141
|
+
format Re-print the schema with canonical formatting
|
|
3142
|
+
validate Parse and validate the schema
|
|
3143
|
+
|
|
3144
|
+
Options:
|
|
3145
|
+
--schema <path> Path to the schema file
|
|
3146
|
+
--url <url> Firebird connection URL (overrides datasource/env)
|
|
3147
|
+
--name <name> Migration name (migrate dev)
|
|
3148
|
+
-h, --help Show this help
|
|
3149
|
+
-v, --version Show version
|
|
3150
|
+
`;
|
|
3151
|
+
function parseArgs(argv) {
|
|
3152
|
+
const command = [];
|
|
3153
|
+
const flags = {};
|
|
3154
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3155
|
+
const arg = argv[i];
|
|
3156
|
+
if (arg.startsWith("--")) {
|
|
3157
|
+
const key = arg.slice(2);
|
|
3158
|
+
const next = argv[i + 1];
|
|
3159
|
+
if (next && !next.startsWith("-")) {
|
|
3160
|
+
flags[key] = next;
|
|
3161
|
+
i++;
|
|
3162
|
+
} else {
|
|
3163
|
+
flags[key] = true;
|
|
3164
|
+
}
|
|
3165
|
+
} else if (arg === "-h") {
|
|
3166
|
+
flags.help = true;
|
|
3167
|
+
} else if (arg === "-v") {
|
|
3168
|
+
flags.version = true;
|
|
3169
|
+
} else {
|
|
3170
|
+
command.push(arg);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
return { command, flags };
|
|
3174
|
+
}
|
|
3175
|
+
async function main() {
|
|
3176
|
+
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
3177
|
+
const ctx = {
|
|
3178
|
+
cwd: process.cwd(),
|
|
3179
|
+
log: (m) => process.stdout.write(m + "\n"),
|
|
3180
|
+
error: (m) => process.stderr.write(`error: ${m}
|
|
3181
|
+
`)
|
|
3182
|
+
};
|
|
3183
|
+
if (flags.version) {
|
|
3184
|
+
ctx.log(readVersion());
|
|
3185
|
+
return 0;
|
|
3186
|
+
}
|
|
3187
|
+
if (flags.help || command.length === 0) {
|
|
3188
|
+
ctx.log(HELP);
|
|
3189
|
+
return command.length === 0 ? 1 : 0;
|
|
3190
|
+
}
|
|
3191
|
+
const schemaPath = typeof flags.schema === "string" ? flags.schema : void 0;
|
|
3192
|
+
const url = typeof flags.url === "string" ? flags.url : void 0;
|
|
3193
|
+
const name = typeof flags.name === "string" ? flags.name : void 0;
|
|
3194
|
+
const [first, second] = command;
|
|
3195
|
+
switch (first) {
|
|
3196
|
+
case "init":
|
|
3197
|
+
return init(ctx);
|
|
3198
|
+
case "validate":
|
|
3199
|
+
return validate(ctx, schemaPath);
|
|
3200
|
+
case "format":
|
|
3201
|
+
return format(ctx, schemaPath);
|
|
3202
|
+
case "generate":
|
|
3203
|
+
return generate(ctx, schemaPath);
|
|
3204
|
+
case "db":
|
|
3205
|
+
if (second === "pull") return dbPull(ctx, { schemaPath, url });
|
|
3206
|
+
if (second === "push") return dbPush(ctx, { schemaPath, url });
|
|
3207
|
+
ctx.error(`Unknown db subcommand '${second ?? ""}'.`);
|
|
3208
|
+
return 1;
|
|
3209
|
+
case "migrate":
|
|
3210
|
+
if (second === "dev") return migrateDev(ctx, { schemaPath, url, name });
|
|
3211
|
+
if (second === "deploy") return migrateDeploy(ctx, { schemaPath, url });
|
|
3212
|
+
if (second === "status") return migrateStatus(ctx, { schemaPath, url });
|
|
3213
|
+
ctx.error(`Unknown migrate subcommand '${second ?? ""}'.`);
|
|
3214
|
+
return 1;
|
|
3215
|
+
default:
|
|
3216
|
+
ctx.error(`Unknown command '${first}'. Run 'ember --help'.`);
|
|
3217
|
+
return 1;
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
function readVersion() {
|
|
3221
|
+
for (const candidate of ["../../package.json", "../package.json"]) {
|
|
3222
|
+
try {
|
|
3223
|
+
const url = new URL(candidate, import.meta.url);
|
|
3224
|
+
const pkg = JSON.parse(readFileSync4(fileURLToPath(url), "utf8"));
|
|
3225
|
+
if (pkg?.version) return `ember ${pkg.version}`;
|
|
3226
|
+
} catch {
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
return "ember (unknown version)";
|
|
3230
|
+
}
|
|
3231
|
+
main().then((code) => process.exit(code)).catch((err) => {
|
|
3232
|
+
if (err instanceof EmberError) {
|
|
3233
|
+
process.stderr.write(`error: ${err.message}
|
|
3234
|
+
`);
|
|
3235
|
+
} else {
|
|
3236
|
+
process.stderr.write(`unexpected error: ${err.stack ?? err}
|
|
3237
|
+
`);
|
|
3238
|
+
}
|
|
3239
|
+
process.exit(1);
|
|
3240
|
+
});
|
|
3241
|
+
//# sourceMappingURL=bin.js.map
|