allez-orm 1.0.13 → 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 +107 -103
package/package.json
CHANGED
package/tools/allez-orm.mjs
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
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:
|
|
@@ -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" },
|
|
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
|
+
}
|
|
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
|
+
}
|
|
293
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,7 +366,7 @@ function parseFieldToken(tok) {
|
|
|
351
366
|
|
|
352
367
|
function camel(s){return s.replace(/[-_](.)/g,(_,c)=>c.toUpperCase());}
|
|
353
368
|
|
|
354
|
-
// ---------------- from-json implementation (resilient) ----------------
|
|
369
|
+
// ---------------- from-json implementation (resilient & fast) ----------------
|
|
355
370
|
|
|
356
371
|
function pick(obj, ...keys) {
|
|
357
372
|
for (const k of keys) { if (obj && obj[k] !== undefined) return obj[k]; }
|
|
@@ -362,61 +377,50 @@ async function runFromJson(cliOpts) {
|
|
|
362
377
|
const file = path.resolve(cliOpts.jsonFile);
|
|
363
378
|
if (!fs.existsSync(file)) die(`Config not found: ${file}`);
|
|
364
379
|
|
|
365
|
-
const raw = fs.readFileSync(file, "utf8");
|
|
366
380
|
let cfg;
|
|
367
|
-
try {
|
|
381
|
+
try {
|
|
382
|
+
cfg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
383
|
+
} catch (e) {
|
|
384
|
+
die(`Invalid JSON: ${e.message}`);
|
|
385
|
+
}
|
|
368
386
|
|
|
369
387
|
// allow OutDir/DefaultOnDelete/Tables
|
|
370
388
|
const outDir = cliOpts.dir || pick(cfg, "outDir", "OutDir") || "schemas_cli";
|
|
371
389
|
const defaultOnDelete = pick(cfg, "defaultOnDelete", "DefaultOnDelete") ?? null;
|
|
372
390
|
|
|
373
391
|
const tables = pick(cfg, "tables", "Tables");
|
|
374
|
-
if (!Array.isArray(tables))
|
|
375
|
-
die(`Config must have an array at "tables" (or "Tables").`);
|
|
376
|
-
}
|
|
392
|
+
if (!Array.isArray(tables)) die(`Config must have an array at "tables" (or "Tables").`);
|
|
377
393
|
|
|
378
394
|
fs.mkdirSync(outDir, { recursive: true });
|
|
379
395
|
|
|
380
|
-
for (let
|
|
381
|
-
const
|
|
382
|
-
const tName = pick(
|
|
383
|
-
if (!tName || typeof tName !== "string") {
|
|
384
|
-
die(`Table at index ${ti} is missing "name".`);
|
|
385
|
-
}
|
|
386
|
-
const tStamps = !!pick(tRaw, "stamps", "Stamps");
|
|
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".`);
|
|
387
400
|
|
|
388
|
-
|
|
389
|
-
let fieldsList = pick(tRaw, "fields", "Fields", "columns", "Columns");
|
|
390
|
-
if (!Array.isArray(fieldsList)) {
|
|
391
|
-
die(`Table "${tName}" must have "fields" (or "Fields"/"columns"/"Columns") array.`);
|
|
392
|
-
}
|
|
401
|
+
const tStamps = !!pick(t, "stamps", "Stamps");
|
|
393
402
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const name = pick(f, "name", "Name");
|
|
398
|
-
if (!name || typeof name !== "string") {
|
|
399
|
-
die(`Table "${tName}" field #${fi} is missing "name".`);
|
|
400
|
-
}
|
|
401
|
-
const typeRaw = pick(f, "type", "Type");
|
|
402
|
-
const type = (typeRaw ? String(typeRaw) : "TEXT").toLowerCase();
|
|
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.`);
|
|
403
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();
|
|
404
413
|
const unique = !!pick(f, "unique", "Unique");
|
|
405
|
-
// support notnull, notNull, NotNull
|
|
406
414
|
const notnull = !!pick(f, "notnull", "notNull", "NotNull");
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (fkTable) token += `->` + fkTable;
|
|
417
|
-
|
|
418
|
-
tokens.push(token);
|
|
419
|
-
}
|
|
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
|
+
});
|
|
420
424
|
|
|
421
425
|
await generateOne({
|
|
422
426
|
outDir,
|