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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/tools/allez-orm.mjs +107 -103
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "allez-orm",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "AllezORM: lightweight browser SQLite ORM (sql.js) + schema generator CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- * - Auto index per FK column in extraSQL (emitted as backticked strings)
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
- type: "object",
129
- additionalProperties: false,
130
- required: ["name"],
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
- table: { type: "string" },
141
- column: { type: "string", default: "id" }
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) && !force) {
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 into field descriptors
199
+ // Parse tokens -> field descriptors
196
200
  const fields = fieldTokens.map(parseFieldToken).filter(Boolean);
197
201
 
198
202
  // Ensure id PK
199
- const hasId = fields.some(f => f.name === "id");
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
- // SQL
214
- const columnLines = fields.map(f => sqlForColumn(f, onDelete));
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
- // FK indexes
217
- const extraSQL = [];
218
- for (const f of fields) {
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
- // stub FK targets
247
- const fkTargets = Array.from(new Set(fields.filter(f => f.fk).map(f => f.fk.table)))
248
- .filter(t => t && t !== name);
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 = `// ${t}.schema.js (generated by tools/allez-orm.mjs - stub for FK target)
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, onDelete) {
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
- // ordering (UNIQUE then NOT NULL) matches tests
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 (onDelete) {
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 "col[:type][!][+][->target]" OR "col:type,unique,notnull"
291
- const ret = { name: "", type: "TEXT", notnull: false, unique: false, fk: null };
292
- if (!tok) return null;
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 from both name and type segments
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 { cfg = JSON.parse(raw); } catch (e) { die(`Invalid JSON: ${e.message}`); }
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 ti = 0; ti < tables.length; ti++) {
381
- const tRaw = tables[ti] || {};
382
- const tName = pick(tRaw, "name", "Name");
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
- // accept fields/Fields OR columns/Columns
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
- const tokens = [];
395
- for (let fi = 0; fi < fieldsList.length; fi++) {
396
- const f = fieldsList[fi] || {};
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
- // FK variations: fk/FK with table/Table, column/Column
409
- const fkRaw = pick(f, "fk", "FK");
410
- const fkTable = fkRaw ? pick(fkRaw, "table", "Table") : undefined;
411
- const fkCol = fkRaw ? (pick(fkRaw, "column", "Column") || "id") : undefined;
412
-
413
- let token = name + `:${type}`;
414
- if (notnull) token += `!`;
415
- if (unique) token += `+`;
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,