allez-orm 1.0.12 → 1.0.15
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/package.json +1 -1
- package/tools/allez-orm.mjs +126 -93
package/package.json
CHANGED
package/tools/allez-orm.mjs
CHANGED
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
* - Optional "stamps": created_at, updated_at, deleted_at
|
|
8
8
|
* - Optional unique / not-null markers
|
|
9
9
|
* - Optional ON DELETE behavior for *all* FKs via --onDelete=
|
|
10
|
-
* -
|
|
10
|
+
* - (No extraSQL output by default)
|
|
11
11
|
* - Auto-create stub schemas for FK target tables if missing
|
|
12
12
|
*
|
|
13
13
|
* New:
|
|
14
|
-
* - from-json <file>: bulk-generate schemas from a JSON config
|
|
14
|
+
* - from-json <file>: bulk-generate schemas from a JSON config (fields can be objects or string tokens)
|
|
15
15
|
* - --print-json-schema: output the JSON Schema used for validation
|
|
16
16
|
*
|
|
17
17
|
* Usage:
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* allez-orm create table <name> [fields...] [--dir=schemas_cli] [--stamps] [-f|--force] [--onDelete=cascade|restrict|setnull|noaction]
|
|
19
|
+
* allez-orm from-json <config.json> [--dir=schemas_cli] [-f|--force]
|
|
20
|
+
* allez-orm --print-json-schema
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import fs from "node:fs";
|
|
@@ -86,7 +86,6 @@ function parseOptions(args) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// env var ALLEZ_FORCE=1 is honored (does not break positional parsing)
|
|
90
89
|
if (process.env.ALLEZ_FORCE === "1") out.force = true;
|
|
91
90
|
|
|
92
91
|
out.cmd = positional[0] || null;
|
|
@@ -122,26 +121,32 @@ const CONFIG_JSON_SCHEMA = JSON.stringify({
|
|
|
122
121
|
properties: {
|
|
123
122
|
name: { type: "string", minLength: 1 },
|
|
124
123
|
stamps: { type: "boolean" },
|
|
124
|
+
// Accept either rich field objects or simple string tokens.
|
|
125
125
|
fields: {
|
|
126
126
|
type: "array",
|
|
127
127
|
items: {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
properties: {
|
|
132
|
-
name: { type: "string" },
|
|
133
|
-
type: { type: "string" }, // TEXT (default), INTEGER, etc
|
|
134
|
-
unique: { type: "boolean" },
|
|
135
|
-
notnull: { type: "boolean" },
|
|
136
|
-
fk: {
|
|
137
|
-
type: ["object", "null"],
|
|
128
|
+
anyOf: [
|
|
129
|
+
{
|
|
130
|
+
type: "object",
|
|
138
131
|
additionalProperties: false,
|
|
132
|
+
required: ["name"],
|
|
139
133
|
properties: {
|
|
140
|
-
|
|
141
|
-
|
|
134
|
+
name: { type: "string" },
|
|
135
|
+
type: { type: "string" },
|
|
136
|
+
unique: { type: "boolean" },
|
|
137
|
+
notnull: { type: "boolean" },
|
|
138
|
+
fk: {
|
|
139
|
+
type: ["object", "null"],
|
|
140
|
+
additionalProperties: false,
|
|
141
|
+
properties: {
|
|
142
|
+
table: { type: "string" },
|
|
143
|
+
column: { type: "string", default: "id" }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
142
146
|
}
|
|
143
|
-
}
|
|
144
|
-
|
|
147
|
+
},
|
|
148
|
+
{ type: "string" } // token form: "email:text!+->users"
|
|
149
|
+
]
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
152
|
},
|
|
@@ -167,7 +172,6 @@ if (!opts.cmd) {
|
|
|
167
172
|
if (opts.cmd === "from-json") {
|
|
168
173
|
if (!opts.jsonFile) die("from-json requires a <config.json> path");
|
|
169
174
|
runFromJson(opts).catch(e => die(e.stack || String(e)));
|
|
170
|
-
// will exit inside
|
|
171
175
|
} else if (opts.cmd === "create" && opts.sub === "table" && opts.table) {
|
|
172
176
|
fs.mkdirSync(opts.dir, { recursive: true });
|
|
173
177
|
generateOne({
|
|
@@ -188,16 +192,15 @@ if (opts.cmd === "from-json") {
|
|
|
188
192
|
|
|
189
193
|
async function generateOne({ outDir, name, stamps, onDelete, force, fieldTokens }) {
|
|
190
194
|
const outFile = path.join(outDir, `${name}.schema.js`);
|
|
191
|
-
if (fs.existsSync(outFile)
|
|
195
|
+
if (!force && fs.existsSync(outFile)) {
|
|
192
196
|
die(`Refusing to overwrite existing file: ${outFile}\n(use -f or ALLEZ_FORCE=1)`);
|
|
193
197
|
}
|
|
194
198
|
|
|
195
|
-
// Parse tokens
|
|
199
|
+
// Parse tokens -> field descriptors
|
|
196
200
|
const fields = fieldTokens.map(parseFieldToken).filter(Boolean);
|
|
197
201
|
|
|
198
202
|
// Ensure id PK
|
|
199
|
-
|
|
200
|
-
if (!hasId) {
|
|
203
|
+
if (!fields.some(f => f.name === "id")) {
|
|
201
204
|
fields.unshift({ name: "id", type: "INTEGER", notnull: true, unique: false, fk: null, pk: true });
|
|
202
205
|
}
|
|
203
206
|
|
|
@@ -210,31 +213,20 @@ async function generateOne({ outDir, name, stamps, onDelete, force, fieldTokens
|
|
|
210
213
|
);
|
|
211
214
|
}
|
|
212
215
|
|
|
213
|
-
//
|
|
214
|
-
const
|
|
216
|
+
// Fast column assembly
|
|
217
|
+
const onDel = onDelete ? ({ cascade:"CASCADE", restrict:"RESTRICT", setnull:"SET NULL", noaction:"NO ACTION" })[onDelete] : null;
|
|
218
|
+
const columnLines = fields.map(f => sqlForColumn(f, onDel));
|
|
215
219
|
|
|
216
|
-
//
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
if (f.fk) {
|
|
220
|
-
extraSQL.push(
|
|
221
|
-
`\`CREATE INDEX IF NOT EXISTS idx_${name}_${f.name}_fk ON ${name}(${f.name});\``
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// module text
|
|
227
|
-
const moduleText = `// ${name}.schema.js (generated by tools/allez-orm.mjs)
|
|
220
|
+
// Module text — no extraSQL emitted
|
|
221
|
+
const moduleText =
|
|
222
|
+
`// ${name}.schema.js (generated by tools/allez-orm.mjs)
|
|
228
223
|
const ${camel(name)}Schema = {
|
|
229
224
|
table: "${name}",
|
|
230
225
|
version: 1,
|
|
231
226
|
createSQL: \`
|
|
232
227
|
CREATE TABLE IF NOT EXISTS ${name} (
|
|
233
228
|
${columnLines.join(",\n ")}
|
|
234
|
-
)
|
|
235
|
-
extraSQL: [
|
|
236
|
-
${extraSQL.join("\n ")}
|
|
237
|
-
]
|
|
229
|
+
);\`
|
|
238
230
|
};
|
|
239
231
|
export default ${camel(name)}Schema;
|
|
240
232
|
`;
|
|
@@ -243,24 +235,21 @@ export default ${camel(name)}Schema;
|
|
|
243
235
|
fs.writeFileSync(outFile, moduleText, "utf8");
|
|
244
236
|
console.log(`Wrote ${outFile}`);
|
|
245
237
|
|
|
246
|
-
//
|
|
247
|
-
const fkTargets =
|
|
248
|
-
|
|
249
|
-
|
|
238
|
+
// Stub FK targets (only if not self & missing)
|
|
239
|
+
const fkTargets = new Set();
|
|
240
|
+
for (const f of fields) if (f.fk && f.fk.table && f.fk.table !== name) fkTargets.add(f.fk.table);
|
|
250
241
|
for (const t of fkTargets) {
|
|
251
242
|
const stubPath = path.join(outDir, `${t}.schema.js`);
|
|
252
243
|
if (!fs.existsSync(stubPath)) {
|
|
253
|
-
const stub =
|
|
244
|
+
const stub =
|
|
245
|
+
`// ${t}.schema.js (generated by tools/allez-orm.mjs - stub for FK target)
|
|
254
246
|
const ${camel(t)}Schema = {
|
|
255
247
|
table: "${t}",
|
|
256
248
|
version: 1,
|
|
257
249
|
createSQL: \`
|
|
258
250
|
CREATE TABLE IF NOT EXISTS ${t} (
|
|
259
251
|
id INTEGER PRIMARY KEY AUTOINCREMENT
|
|
260
|
-
)
|
|
261
|
-
extraSQL: [
|
|
262
|
-
|
|
263
|
-
]
|
|
252
|
+
);\`
|
|
264
253
|
};
|
|
265
254
|
export default ${camel(t)}Schema;
|
|
266
255
|
`;
|
|
@@ -270,28 +259,57 @@ export default ${camel(t)}Schema;
|
|
|
270
259
|
}
|
|
271
260
|
}
|
|
272
261
|
|
|
273
|
-
function sqlForColumn(f,
|
|
262
|
+
function sqlForColumn(f, onDelUpper) {
|
|
274
263
|
if (f.pk) return `id INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
275
264
|
let s = `${f.name} ${f.type}`;
|
|
276
|
-
//
|
|
265
|
+
// Keep order: UNIQUE then NOT NULL (matches tests/expectations)
|
|
277
266
|
if (f.unique) s += ` UNIQUE`;
|
|
278
267
|
if (f.notnull) s += ` NOT NULL`;
|
|
279
268
|
if (f.fk) {
|
|
280
269
|
s += ` REFERENCES ${f.fk.table}(${f.fk.column || "id"})`;
|
|
281
|
-
if (
|
|
282
|
-
const map = { cascade: "CASCADE", restrict: "RESTRICT", setnull: "SET NULL", noaction: "NO ACTION" };
|
|
283
|
-
s += ` ON DELETE ${map[onDelete]}`;
|
|
284
|
-
}
|
|
270
|
+
if (onDelUpper) s += ` ON DELETE ${onDelUpper}`;
|
|
285
271
|
}
|
|
286
272
|
return s;
|
|
287
273
|
}
|
|
288
274
|
|
|
289
275
|
function parseFieldToken(tok) {
|
|
290
|
-
// Accept
|
|
291
|
-
|
|
292
|
-
|
|
276
|
+
// Accept:
|
|
277
|
+
// - token string "col[:type][!][+][->target]" or "col:type,unique,notnull"
|
|
278
|
+
// - object {name,type,unique,notnull,fk:{table,column}}
|
|
279
|
+
if (tok == null) return null;
|
|
280
|
+
|
|
281
|
+
// Fast path: token is already string
|
|
282
|
+
if (typeof tok === "string") {
|
|
283
|
+
return parseTokenString(tok);
|
|
284
|
+
}
|
|
293
285
|
|
|
286
|
+
// Object form -> tokenize once, then reuse the same parser
|
|
287
|
+
if (typeof tok === "object") {
|
|
288
|
+
const name = String(tok.name ?? tok.Name ?? "").trim();
|
|
289
|
+
if (!name) return null;
|
|
290
|
+
const type = String(tok.type ?? tok.Type ?? "TEXT").trim().toLowerCase();
|
|
291
|
+
const unique = !!(tok.unique ?? tok.Unique);
|
|
292
|
+
const notnull = !!(tok.notnull ?? tok.notNull ?? tok.NotNull);
|
|
293
|
+
const fkRaw = tok.fk ?? tok.FK;
|
|
294
|
+
const fkTable = fkRaw ? (fkRaw.table ?? fkRaw.Table) : null;
|
|
295
|
+
const fkCol = fkRaw ? (fkRaw.column ?? fkRaw.Column ?? "id") : "id";
|
|
296
|
+
|
|
297
|
+
let token = `${name}:${type}`;
|
|
298
|
+
if (notnull) token += "!";
|
|
299
|
+
if (unique) token += "+";
|
|
300
|
+
if (fkTable) token += `->${fkTable}`;
|
|
301
|
+
|
|
302
|
+
const parsed = parseTokenString(token);
|
|
303
|
+
if (fkTable) parsed.fk.column = fkCol; // preserve non-id if provided
|
|
304
|
+
return parsed;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function parseTokenString(tok) {
|
|
294
311
|
// Split on "," for attribute list
|
|
312
|
+
const ret = { name: "", type: "TEXT", notnull: false, unique: false, fk: null };
|
|
295
313
|
let main = tok;
|
|
296
314
|
let flags = [];
|
|
297
315
|
if (tok.includes(",")) {
|
|
@@ -305,14 +323,12 @@ function parseFieldToken(tok) {
|
|
|
305
323
|
let type = null;
|
|
306
324
|
let fkTarget = null;
|
|
307
325
|
|
|
308
|
-
// ->target
|
|
309
326
|
const fkIdx = main.indexOf("->");
|
|
310
327
|
if (fkIdx >= 0) {
|
|
311
328
|
fkTarget = main.slice(fkIdx + 2).trim();
|
|
312
329
|
name = main.slice(0, fkIdx);
|
|
313
330
|
}
|
|
314
331
|
|
|
315
|
-
// :type
|
|
316
332
|
const typeIdx = name.indexOf(":");
|
|
317
333
|
if (typeIdx >= 0) {
|
|
318
334
|
type = name.slice(typeIdx + 1).trim(); // may contain !/+
|
|
@@ -321,7 +337,7 @@ function parseFieldToken(tok) {
|
|
|
321
337
|
type = null;
|
|
322
338
|
}
|
|
323
339
|
|
|
324
|
-
// flags
|
|
340
|
+
// flags may appear on either side
|
|
325
341
|
const nameHasBang = /!/.test(name);
|
|
326
342
|
const nameHasPlus = /\+/.test(name);
|
|
327
343
|
const typeHasBang = type ? /!/.test(type) : false;
|
|
@@ -330,7 +346,6 @@ function parseFieldToken(tok) {
|
|
|
330
346
|
if (nameHasBang || typeHasBang) ret.notnull = true;
|
|
331
347
|
if (nameHasPlus || typeHasPlus) ret.unique = true;
|
|
332
348
|
|
|
333
|
-
// Clean trailing !/+ off name and type
|
|
334
349
|
name = name.replace(/[!+]+$/,"").trim();
|
|
335
350
|
if (type) {
|
|
336
351
|
type = type.replace(/[!+]+$/,"").trim();
|
|
@@ -351,48 +366,66 @@ function parseFieldToken(tok) {
|
|
|
351
366
|
|
|
352
367
|
function camel(s){return s.replace(/[-_](.)/g,(_,c)=>c.toUpperCase());}
|
|
353
368
|
|
|
354
|
-
// ---------------- from-json implementation ----------------
|
|
369
|
+
// ---------------- from-json implementation (resilient & fast) ----------------
|
|
370
|
+
|
|
371
|
+
function pick(obj, ...keys) {
|
|
372
|
+
for (const k of keys) { if (obj && obj[k] !== undefined) return obj[k]; }
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
355
375
|
|
|
356
376
|
async function runFromJson(cliOpts) {
|
|
357
377
|
const file = path.resolve(cliOpts.jsonFile);
|
|
358
378
|
if (!fs.existsSync(file)) die(`Config not found: ${file}`);
|
|
359
379
|
|
|
360
|
-
const raw = fs.readFileSync(file, "utf8");
|
|
361
380
|
let cfg;
|
|
362
|
-
try {
|
|
381
|
+
try {
|
|
382
|
+
cfg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
383
|
+
} catch (e) {
|
|
384
|
+
die(`Invalid JSON: ${e.message}`);
|
|
385
|
+
}
|
|
363
386
|
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
387
|
+
// allow OutDir/DefaultOnDelete/Tables
|
|
388
|
+
const outDir = cliOpts.dir || pick(cfg, "outDir", "OutDir") || "schemas_cli";
|
|
389
|
+
const defaultOnDelete = pick(cfg, "defaultOnDelete", "DefaultOnDelete") ?? null;
|
|
367
390
|
|
|
368
|
-
const
|
|
369
|
-
|
|
391
|
+
const tables = pick(cfg, "tables", "Tables");
|
|
392
|
+
if (!Array.isArray(tables)) die(`Config must have an array at "tables" (or "Tables").`);
|
|
370
393
|
|
|
371
394
|
fs.mkdirSync(outDir, { recursive: true });
|
|
372
395
|
|
|
373
|
-
for (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (f
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
396
|
+
for (let i = 0; i < tables.length; i++) {
|
|
397
|
+
const t = tables[i] || {};
|
|
398
|
+
const tName = pick(t, "name", "Name");
|
|
399
|
+
if (!tName || typeof tName !== "string") die(`Table at index ${i} is missing "name".`);
|
|
400
|
+
|
|
401
|
+
const tStamps = !!pick(t, "stamps", "Stamps");
|
|
402
|
+
|
|
403
|
+
// Accept fields/Fields/columns/Columns; items can be objects or string tokens
|
|
404
|
+
let fieldsList = pick(t, "fields", "Fields", "columns", "Columns");
|
|
405
|
+
if (!Array.isArray(fieldsList)) die(`Table "${tName}" must have "fields" array.`);
|
|
406
|
+
|
|
407
|
+
// Normalize to token strings for speed (object items are converted once)
|
|
408
|
+
const tokens = fieldsList.map(f => {
|
|
409
|
+
if (typeof f === "string") return f;
|
|
410
|
+
const name = pick(f, "name", "Name");
|
|
411
|
+
if (!name) die(`Table "${tName}" has a field without "name".`);
|
|
412
|
+
const type = String(pick(f, "type", "Type") ?? "TEXT").toLowerCase();
|
|
413
|
+
const unique = !!pick(f, "unique", "Unique");
|
|
414
|
+
const notnull = !!pick(f, "notnull", "notNull", "NotNull");
|
|
415
|
+
const fk = pick(f, "fk", "FK");
|
|
416
|
+
const fkTable = fk ? pick(fk, "table", "Table") : null;
|
|
417
|
+
|
|
418
|
+
let tok = `${name}:${type}`;
|
|
419
|
+
if (notnull) tok += "!";
|
|
420
|
+
if (unique) tok += "+";
|
|
421
|
+
if (fkTable) tok += `->${fkTable}`;
|
|
422
|
+
return tok;
|
|
423
|
+
});
|
|
391
424
|
|
|
392
425
|
await generateOne({
|
|
393
426
|
outDir,
|
|
394
|
-
name:
|
|
395
|
-
stamps:
|
|
427
|
+
name: tName,
|
|
428
|
+
stamps: tStamps,
|
|
396
429
|
onDelete: defaultOnDelete || null,
|
|
397
430
|
force: cliOpts.force,
|
|
398
431
|
fieldTokens: tokens
|