allez-orm 1.0.10 → 1.0.12
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/allez-orm.mjs +33 -5
- package/index.d.ts +28 -4
- package/package.json +4 -4
- package/tools/allez-orm.mjs +344 -302
- package/tools/ddl-audit.mjs +24 -78
package/allez-orm.mjs
CHANGED
|
@@ -197,23 +197,51 @@ export class AllezORM {
|
|
|
197
197
|
|
|
198
198
|
// ---------------- schema registration ----------------
|
|
199
199
|
|
|
200
|
+
/** Soft-run an extra SQL statement; swallow known-unsupported patterns. */
|
|
201
|
+
async #tryExtra(sql) {
|
|
202
|
+
const s = (sql || "").trim();
|
|
203
|
+
if (!s) return;
|
|
204
|
+
|
|
205
|
+
// SQLite does not support adding FK constraints via ALTER TABLE.
|
|
206
|
+
const isAlterFk =
|
|
207
|
+
/^ALTER\s+TABLE\s+.+\s+ADD\s+FOREIGN\s+KEY/i.test(s);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
if (isAlterFk) {
|
|
211
|
+
console.warn(
|
|
212
|
+
"[AllezORM] Skipping unsupported statement (SQLite):",
|
|
213
|
+
s
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
await this.execute(s);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.warn("[AllezORM] extraSQL failed and was skipped:", s, err);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
200
223
|
/** @param {Schema[]} schemas */
|
|
201
224
|
async registerSchemas(schemas) {
|
|
202
225
|
const meta = await this.#currentVersions();
|
|
203
226
|
for (const s of schemas) {
|
|
204
|
-
if (!s?.table || !s?.createSQL)
|
|
205
|
-
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
227
|
+
if (!s?.table || !s?.createSQL) continue;
|
|
228
|
+
|
|
208
229
|
const exists = await this.get(
|
|
209
230
|
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
|
210
231
|
[s.table]
|
|
211
232
|
);
|
|
233
|
+
|
|
212
234
|
if (!exists) {
|
|
235
|
+
// Hard-fail for invalid CREATE (developer action required)
|
|
213
236
|
await this.execute(s.createSQL);
|
|
237
|
+
|
|
238
|
+
// Best-effort for side DDL (indexes/triggers/fts)
|
|
214
239
|
if (Array.isArray(s.extraSQL)) {
|
|
215
|
-
for (const x of s.extraSQL)
|
|
240
|
+
for (const x of s.extraSQL) {
|
|
241
|
+
await this.#tryExtra(x);
|
|
242
|
+
}
|
|
216
243
|
}
|
|
244
|
+
|
|
217
245
|
await this.execute(
|
|
218
246
|
`INSERT OR REPLACE INTO allez_meta(table_name,version) VALUES(?,?)`,
|
|
219
247
|
[s.table, s.version ?? 1]
|
package/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// index.d.ts
|
|
2
|
+
|
|
2
3
|
export interface Schema {
|
|
3
4
|
table: string;
|
|
4
5
|
createSQL: string;
|
|
@@ -21,30 +22,53 @@ export interface TableHelper<T extends Row = Row> {
|
|
|
21
22
|
insert(obj: Partial<T>): Promise<void>;
|
|
22
23
|
upsert(obj: Partial<T>): Promise<void>;
|
|
23
24
|
update(id: any, patch: Partial<T>): Promise<void>;
|
|
24
|
-
/** Soft delete; implementation will try `deletedAt` then `deleted_at`. */
|
|
25
25
|
deleteSoft(id: any, ts?: string): Promise<void>;
|
|
26
26
|
remove(id: any): Promise<void>;
|
|
27
|
-
findById(id: any): Promise<T |
|
|
27
|
+
findById(id: any): Promise<T | undefined>;
|
|
28
28
|
searchLike(q: string, columns: (keyof T | string)[], limit?: number): Promise<T[]>;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export class AllezORM {
|
|
32
32
|
constructor(SQL: any, db: any, opts: InitOptions);
|
|
33
|
+
|
|
34
|
+
/** Initialize (loads sql.js, restores from IndexedDB, applies schemas). */
|
|
33
35
|
static init(opts?: InitOptions): Promise<AllezORM>;
|
|
36
|
+
|
|
37
|
+
/** Persist the current database to IndexedDB immediately. */
|
|
34
38
|
saveNow(): Promise<void>;
|
|
39
|
+
|
|
40
|
+
/** Execute arbitrary SQL and auto-save (convenience, returns true). */
|
|
35
41
|
exec(sql: string, params?: any[]): Promise<boolean>;
|
|
42
|
+
|
|
43
|
+
/** Alias for exec. */
|
|
36
44
|
run(sql: string, params?: any[]): Promise<boolean>;
|
|
45
|
+
|
|
46
|
+
/** Low-level execute; schedules a debounced save. */
|
|
37
47
|
execute(sql: string, params?: any[]): Promise<void>;
|
|
48
|
+
|
|
49
|
+
/** SELECT helper returning plain objects. */
|
|
38
50
|
query<T = Row>(sql: string, params?: any[]): Promise<T[]>;
|
|
39
|
-
|
|
51
|
+
|
|
52
|
+
/** SELECT one row (undefined if no row). */
|
|
53
|
+
get<T = Row>(sql: string, params?: any[]): Promise<T | undefined>;
|
|
54
|
+
|
|
55
|
+
/** Table-scoped helpers. */
|
|
40
56
|
table<T extends Row = Row>(table: string): TableHelper<T>;
|
|
57
|
+
|
|
58
|
+
/** Register / upgrade schemas. */
|
|
41
59
|
registerSchemas(schemas: Schema[]): Promise<void>;
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
/**
|
|
62
|
+
/** Open (or reuse) a browser DB by name. */
|
|
45
63
|
export function openBrowserDb(name: string, opts?: InitOptions): Promise<AllezORM>;
|
|
64
|
+
|
|
65
|
+
/** Alias for openBrowserDb. */
|
|
46
66
|
export const openDb: typeof openBrowserDb;
|
|
67
|
+
|
|
68
|
+
/** Apply an array of schemas to an opened AllezORM instance. */
|
|
47
69
|
export function applySchemas(db: AllezORM, schemas?: Schema[]): Promise<void>;
|
|
70
|
+
|
|
71
|
+
/** Convenience helpers that operate on an AllezORM instance. */
|
|
48
72
|
export function query<T = Row>(db: AllezORM, sql: string, params?: any[]): Promise<T[]>;
|
|
49
73
|
export function exec(db: AllezORM, sql: string, params?: any[]): Promise<void>;
|
|
50
74
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "allez-orm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "AllezORM: lightweight browser SQLite ORM (sql.js) + schema generator CLI",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"allez-orm": "tools/allez-orm.mjs"
|
|
8
|
+
},
|
|
6
9
|
"main": "./allez-orm.mjs",
|
|
7
10
|
"module": "./allez-orm.mjs",
|
|
8
11
|
"browser": "./allez-orm.mjs",
|
|
@@ -32,9 +35,6 @@
|
|
|
32
35
|
"ddl:audit": "node tools/ddl-audit.mjs",
|
|
33
36
|
"prepublishOnly": "node tests/test-cli.mjs && node tools/ddl-audit.mjs"
|
|
34
37
|
},
|
|
35
|
-
"bin": {
|
|
36
|
-
"allez-orm": "tools/allez-orm.mjs"
|
|
37
|
-
},
|
|
38
38
|
"files": [
|
|
39
39
|
"allez-orm.mjs",
|
|
40
40
|
"index.d.ts",
|
package/tools/allez-orm.mjs
CHANGED
|
@@ -1,361 +1,403 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Allez ORM – schema generator CLI
|
|
4
|
+
*
|
|
5
|
+
* Generates a <table>.schema.js with:
|
|
6
|
+
* - CREATE TABLE with inline foreign keys: col TYPE REFERENCES target(id) [ON DELETE ...]
|
|
7
|
+
* - Optional "stamps": created_at, updated_at, deleted_at
|
|
8
|
+
* - Optional unique / not-null markers
|
|
9
|
+
* - Optional ON DELETE behavior for *all* FKs via --onDelete=
|
|
10
|
+
* - Auto index per FK column in extraSQL (emitted as backticked strings)
|
|
11
|
+
* - Auto-create stub schemas for FK target tables if missing
|
|
12
|
+
*
|
|
13
|
+
* New:
|
|
14
|
+
* - from-json <file>: bulk-generate schemas from a JSON config
|
|
15
|
+
* - --print-json-schema: output the JSON Schema used for validation
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node tools/allez-orm.mjs create table <name> [fields...] [--dir=schemas_cli] [--stamps] [-f|--force] [--onDelete=cascade|restrict|setnull|noaction]
|
|
19
|
+
* node tools/allez-orm.mjs from-json <config.json> [--dir=schemas_cli] [-f|--force]
|
|
20
|
+
* node tools/allez-orm.mjs --print-json-schema
|
|
21
|
+
*/
|
|
22
|
+
|
|
3
23
|
import fs from "node:fs";
|
|
4
24
|
import path from "node:path";
|
|
5
25
|
import process from "node:process";
|
|
6
26
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
// ---- robust force detection (works across shells) ----
|
|
10
|
-
const FORCE_FROM_ENV = String(process.env.ALLEZ_FORCE || "").trim() === "1";
|
|
11
|
-
const FORCE_FROM_ARGV = process.argv.includes("--force") || process.argv.includes("-f");
|
|
12
|
-
const FORCE_EFFECTIVE = FORCE_FROM_ENV || FORCE_FROM_ARGV;
|
|
27
|
+
const argv = process.argv.slice(2);
|
|
13
28
|
|
|
14
|
-
|
|
15
|
-
function help() {
|
|
29
|
+
const usage = () => {
|
|
16
30
|
console.log(`
|
|
17
|
-
AllezORM schema generator
|
|
18
|
-
|
|
19
31
|
Usage:
|
|
20
|
-
allez-orm create table <name> [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
name:type!+^#~ -> ! NOT NULL, + UNIQUE, ^ INDEX, # PRIMARY KEY, ~ AUTOINCREMENT
|
|
35
|
-
name:type,notnull,unique,index,pk,ai,default=<expr>
|
|
36
|
-
^col -> standalone index on col
|
|
37
|
-
fk shorthand:
|
|
38
|
-
col>table -> INTEGER REFERENCES table(id)
|
|
39
|
-
col>table(col) -> INTEGER REFERENCES table(col)
|
|
40
|
-
col:fk(table.col) -> same, leaves type as INTEGER unless you pass another
|
|
41
|
-
PowerShell-safe FK alias:
|
|
42
|
-
col->table[(col)] -> same as '>'
|
|
43
|
-
Per-field FK options via commas: ,onDelete=cascade ,onUpdate=restrict ,defer ,deferrable
|
|
32
|
+
allez-orm create table <name> [options] [fields...]
|
|
33
|
+
allez-orm from-json <config.json> [--dir=<outDir>] [-f|--force]
|
|
34
|
+
allez-orm --print-json-schema
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--dir=<outDir> Output directory (default: schemas_cli)
|
|
38
|
+
--stamps Add created_at, updated_at, deleted_at columns
|
|
39
|
+
--onDelete=<mode> ON DELETE for *all* FKs (cascade|restrict|setnull|noaction). Default: none
|
|
40
|
+
-f, --force Overwrite existing files
|
|
41
|
+
--help Show help
|
|
42
|
+
|
|
43
|
+
Field syntax:
|
|
44
|
+
col[:type][!][+][->target] or "col:type,unique,notnull"
|
|
45
|
+
Examples: email:text!+ user_id:text->users org_id:integer->orgs
|
|
44
46
|
`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const TYPE_ALIASES = {
|
|
48
|
-
int: "INTEGER", integer: "INTEGER", bool: "INTEGER",
|
|
49
|
-
text: "TEXT", string: "TEXT", datetime: "TEXT", timestamp: "TEXT",
|
|
50
|
-
real: "REAL", float: "REAL",
|
|
51
|
-
number: "NUMERIC", numeric: "NUMERIC", blob: "BLOB"
|
|
52
47
|
};
|
|
53
48
|
|
|
54
|
-
const
|
|
49
|
+
const die = (m, code = 1) => { console.error(m); process.exit(code); };
|
|
55
50
|
|
|
56
|
-
function
|
|
57
|
-
return name.split(/[_\- ]+/).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
58
|
-
}
|
|
59
|
-
function sanitizeTable(name) {
|
|
60
|
-
const ok = name.trim().toLowerCase().replace(/[^\w]/g, "_");
|
|
61
|
-
if (!ok) die("Invalid table name.");
|
|
62
|
-
return ok;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function parseArgs(argv) {
|
|
51
|
+
function parseOptions(args) {
|
|
66
52
|
const out = {
|
|
67
|
-
|
|
68
|
-
|
|
53
|
+
dir: "schemas_cli",
|
|
54
|
+
stamps: false,
|
|
55
|
+
onDelete: null,
|
|
56
|
+
force: false,
|
|
57
|
+
cmd: null,
|
|
58
|
+
sub: null,
|
|
59
|
+
table: null,
|
|
60
|
+
fields: [],
|
|
61
|
+
jsonFile: null,
|
|
62
|
+
printJsonSchema: false,
|
|
69
63
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (a === "--
|
|
73
|
-
|
|
74
|
-
else if (a.startsWith("--
|
|
75
|
-
|
|
76
|
-
else if (a === "--
|
|
77
|
-
|
|
78
|
-
else if (a
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
64
|
+
const positional = [];
|
|
65
|
+
for (const a of args) {
|
|
66
|
+
if (a === "--help" || a === "-h") {
|
|
67
|
+
usage(); process.exit(0);
|
|
68
|
+
} else if (a.startsWith("--dir=")) {
|
|
69
|
+
out.dir = a.slice(6);
|
|
70
|
+
} else if (a === "--stamps") {
|
|
71
|
+
out.stamps = true;
|
|
72
|
+
} else if (a.startsWith("--onDelete=")) {
|
|
73
|
+
const v = a.slice(11).toLowerCase();
|
|
74
|
+
if (!["cascade","restrict","setnull","noaction"].includes(v)) {
|
|
75
|
+
die(`Invalid --onDelete value: ${v}`);
|
|
76
|
+
}
|
|
77
|
+
out.onDelete = v;
|
|
78
|
+
} else if (a === "-f" || a === "--force") {
|
|
79
|
+
out.force = true;
|
|
80
|
+
} else if (a === "--print-json-schema") {
|
|
81
|
+
out.printJsonSchema = true;
|
|
82
|
+
} else if (a.startsWith("-")) {
|
|
83
|
+
die(`Unknown option: ${a}`);
|
|
84
|
+
} else {
|
|
85
|
+
positional.push(a);
|
|
86
|
+
}
|
|
83
87
|
}
|
|
84
|
-
if (FORCE_EFFECTIVE) out.force = true;
|
|
85
88
|
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
return out;
|
|
89
|
-
}
|
|
89
|
+
// env var ALLEZ_FORCE=1 is honored (does not break positional parsing)
|
|
90
|
+
if (process.env.ALLEZ_FORCE === "1") out.force = true;
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
let name, rhs = "", explicitType = false;
|
|
92
|
+
out.cmd = positional[0] || null;
|
|
93
|
+
out.sub = positional[1] || null;
|
|
94
94
|
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
} else {
|
|
99
|
-
|
|
100
|
-
rhs = ""; // default TEXT; short flags may be on name itself
|
|
95
|
+
if (out.cmd === "create" && out.sub === "table") {
|
|
96
|
+
out.table = positional[2] || null;
|
|
97
|
+
out.fields = positional.slice(3);
|
|
98
|
+
} else if (out.cmd === "from-json") {
|
|
99
|
+
out.jsonFile = positional[1] || null;
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const opts = parseOptions(argv);
|
|
106
|
+
|
|
107
|
+
// ---------------- JSON Schema (string) ----------------
|
|
108
|
+
|
|
109
|
+
const CONFIG_JSON_SCHEMA = JSON.stringify({
|
|
110
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
111
|
+
$id: "https://allez-orm.dev/allez.config.schema.json",
|
|
112
|
+
type: "object",
|
|
113
|
+
additionalProperties: false,
|
|
114
|
+
properties: {
|
|
115
|
+
outDir: { type: "string" },
|
|
116
|
+
defaultOnDelete: { enum: ["cascade","restrict","setnull","noaction",null] },
|
|
117
|
+
tables: {
|
|
118
|
+
type: "array",
|
|
119
|
+
items: {
|
|
120
|
+
type: "object",
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
properties: {
|
|
123
|
+
name: { type: "string", minLength: 1 },
|
|
124
|
+
stamps: { type: "boolean" },
|
|
125
|
+
fields: {
|
|
126
|
+
type: "array",
|
|
127
|
+
items: {
|
|
128
|
+
type: "object",
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
required: ["name"],
|
|
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"],
|
|
138
|
+
additionalProperties: false,
|
|
139
|
+
properties: {
|
|
140
|
+
table: { type: "string" },
|
|
141
|
+
column: { type: "string", default: "id" }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
required: ["name","fields"]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
required: ["tables"]
|
|
153
|
+
}, null, 2);
|
|
119
154
|
|
|
120
|
-
|
|
121
|
-
c === "!" ? "notnull" :
|
|
122
|
-
c === "+" ? "unique" :
|
|
123
|
-
c === "^" ? "index" :
|
|
124
|
-
c === "#" ? "pk" :
|
|
125
|
-
c === "~" ? "ai" :
|
|
126
|
-
""
|
|
127
|
-
).filter(Boolean));
|
|
155
|
+
// ---------------- command switchboard ----------------
|
|
128
156
|
|
|
129
|
-
|
|
130
|
-
|
|
157
|
+
if (opts.printJsonSchema) {
|
|
158
|
+
console.log(CONFIG_JSON_SCHEMA);
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!opts.cmd) {
|
|
163
|
+
usage();
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
131
166
|
|
|
132
|
-
|
|
167
|
+
if (opts.cmd === "from-json") {
|
|
168
|
+
if (!opts.jsonFile) die("from-json requires a <config.json> path");
|
|
169
|
+
runFromJson(opts).catch(e => die(e.stack || String(e)));
|
|
170
|
+
// will exit inside
|
|
171
|
+
} else if (opts.cmd === "create" && opts.sub === "table" && opts.table) {
|
|
172
|
+
fs.mkdirSync(opts.dir, { recursive: true });
|
|
173
|
+
generateOne({
|
|
174
|
+
outDir: opts.dir,
|
|
175
|
+
name: opts.table,
|
|
176
|
+
stamps: opts.stamps,
|
|
177
|
+
onDelete: opts.onDelete,
|
|
178
|
+
force: opts.force,
|
|
179
|
+
fieldTokens: opts.fields
|
|
180
|
+
}).then(() => process.exit(0))
|
|
181
|
+
.catch(e => die(e.stack || String(e)));
|
|
182
|
+
} else {
|
|
183
|
+
usage();
|
|
184
|
+
die("Expected: create table <name> … or from-json <config.json>");
|
|
133
185
|
}
|
|
134
186
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
&& !specRaw.includes(">")
|
|
142
|
-
&& !specRaw.includes("->")) {
|
|
143
|
-
return { kind: "indexOnly", name: specRaw.slice(1) };
|
|
187
|
+
// ---------------- core generator (shared) ----------------
|
|
188
|
+
|
|
189
|
+
async function generateOne({ outDir, name, stamps, onDelete, force, fieldTokens }) {
|
|
190
|
+
const outFile = path.join(outDir, `${name}.schema.js`);
|
|
191
|
+
if (fs.existsSync(outFile) && !force) {
|
|
192
|
+
die(`Refusing to overwrite existing file: ${outFile}\n(use -f or ALLEZ_FORCE=1)`);
|
|
144
193
|
}
|
|
145
194
|
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const refCol = m[2] || "id";
|
|
154
|
-
|
|
155
|
-
// default FK storage type is INTEGER unless caller annotated a type
|
|
156
|
-
const resolvedType = explicitType ? (type || "TEXT") : "INTEGER";
|
|
157
|
-
const finalType = TYPE_ALIASES[(resolvedType || "").toLowerCase()] ?? (resolvedType || "TEXT");
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
kind: "field",
|
|
161
|
-
name,
|
|
162
|
-
type: finalType,
|
|
163
|
-
flags,
|
|
164
|
-
fk: { table, col: refCol, opts: [] },
|
|
165
|
-
wantsIndex: flags.has("index")
|
|
166
|
-
};
|
|
195
|
+
// Parse tokens into field descriptors
|
|
196
|
+
const fields = fieldTokens.map(parseFieldToken).filter(Boolean);
|
|
197
|
+
|
|
198
|
+
// Ensure id PK
|
|
199
|
+
const hasId = fields.some(f => f.name === "id");
|
|
200
|
+
if (!hasId) {
|
|
201
|
+
fields.unshift({ name: "id", type: "INTEGER", notnull: true, unique: false, fk: null, pk: true });
|
|
167
202
|
}
|
|
168
203
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const typeLower = (type || "").toLowerCase();
|
|
177
|
-
if (typeLower.startsWith("fk(") && typeLower.endsWith(")")) {
|
|
178
|
-
const inside = type.slice(3, -1);
|
|
179
|
-
const [t, c = "id"] = inside.split(".");
|
|
180
|
-
fk = { table: sanitizeTable(t), col: c, opts: [] };
|
|
181
|
-
type = "INTEGER"; // storage type for fk()
|
|
204
|
+
// stamps
|
|
205
|
+
if (stamps) {
|
|
206
|
+
fields.push(
|
|
207
|
+
{ name: "created_at", type: "TEXT", notnull: true },
|
|
208
|
+
{ name: "updated_at", type: "TEXT", notnull: true },
|
|
209
|
+
{ name: "deleted_at", type: "TEXT", notnull: false }
|
|
210
|
+
);
|
|
182
211
|
}
|
|
183
212
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
else if (["pk", "primary", "#"].includes(key)) flags.add("pk");
|
|
196
|
-
else if (["ai", "autoincrement", "~"].includes(key)) flags.add("ai");
|
|
197
|
-
else if (["default", "def", "="].includes(key)) flags.add(`default=${v}`);
|
|
198
|
-
else if (["ondelete", "on_delete"].includes(key)) fk?.opts.push(`ON DELETE ${v.toUpperCase()}`);
|
|
199
|
-
else if (["onupdate", "on_update"].includes(key)) fk?.opts.push(`ON UPDATE ${v.toUpperCase()}`);
|
|
200
|
-
else if (["defer", "deferrable"].includes(key)) fk?.opts.push(`DEFERRABLE INITIALLY DEFERRED`);
|
|
201
|
-
else die(`Unknown flag '${key}' in '${specRaw}'`);
|
|
213
|
+
// SQL
|
|
214
|
+
const columnLines = fields.map(f => sqlForColumn(f, onDelete));
|
|
215
|
+
|
|
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
|
+
}
|
|
202
224
|
}
|
|
203
225
|
|
|
204
|
-
|
|
205
|
-
}
|
|
226
|
+
// module text
|
|
227
|
+
const moduleText = `// ${name}.schema.js (generated by tools/allez-orm.mjs)
|
|
228
|
+
const ${camel(name)}Schema = {
|
|
229
|
+
table: "${name}",
|
|
230
|
+
version: 1,
|
|
231
|
+
createSQL: \`
|
|
232
|
+
CREATE TABLE IF NOT EXISTS ${name} (
|
|
233
|
+
${columnLines.join(",\n ")}
|
|
234
|
+
);\`,
|
|
235
|
+
extraSQL: [
|
|
236
|
+
${extraSQL.join("\n ")}
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
export default ${camel(name)}Schema;
|
|
240
|
+
`;
|
|
206
241
|
|
|
207
|
-
|
|
208
|
-
|
|
242
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
243
|
+
fs.writeFileSync(outFile, moduleText, "utf8");
|
|
244
|
+
console.log(`Wrote ${outFile}`);
|
|
209
245
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
parts.push("AUTOINCREMENT");
|
|
214
|
-
}
|
|
215
|
-
if (f.flags?.has("unique")) parts.push("UNIQUE");
|
|
216
|
-
if (f.flags?.has("notnull")) parts.push("NOT NULL");
|
|
217
|
-
|
|
218
|
-
// FK inline constraint
|
|
219
|
-
const fk = f.fk;
|
|
220
|
-
if (fk) {
|
|
221
|
-
const segs = [`REFERENCES ${fk.table}(${fk.col})`];
|
|
222
|
-
const scoped = [];
|
|
223
|
-
const gDel = globalFK?.onDelete ? `ON DELETE ${globalFK.onDelete.toUpperCase()}` : null;
|
|
224
|
-
const gUpd = globalFK?.onUpdate ? `ON UPDATE ${globalFK.onUpdate.toUpperCase()}` : null;
|
|
225
|
-
if (fk.opts?.length) scoped.push(...fk.opts);
|
|
226
|
-
else { if (gDel) scoped.push(gDel); if (gUpd) scoped.push(gUpd); }
|
|
227
|
-
parts.push(segs.join(" "));
|
|
228
|
-
if (scoped.length) parts.push(scoped.join(" "));
|
|
229
|
-
}
|
|
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);
|
|
230
249
|
|
|
231
|
-
const
|
|
232
|
-
|
|
250
|
+
for (const t of fkTargets) {
|
|
251
|
+
const stubPath = path.join(outDir, `${t}.schema.js`);
|
|
252
|
+
if (!fs.existsSync(stubPath)) {
|
|
253
|
+
const stub = `// ${t}.schema.js (generated by tools/allez-orm.mjs - stub for FK target)
|
|
254
|
+
const ${camel(t)}Schema = {
|
|
255
|
+
table: "${t}",
|
|
256
|
+
version: 1,
|
|
257
|
+
createSQL: \`
|
|
258
|
+
CREATE TABLE IF NOT EXISTS ${t} (
|
|
259
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT
|
|
260
|
+
);\`,
|
|
261
|
+
extraSQL: [
|
|
262
|
+
|
|
263
|
+
]
|
|
264
|
+
};
|
|
265
|
+
export default ${camel(t)}Schema;
|
|
266
|
+
`;
|
|
267
|
+
fs.writeFileSync(stubPath, stub, "utf8");
|
|
268
|
+
console.log(`Wrote stub ${stubPath}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
233
272
|
|
|
234
|
-
|
|
273
|
+
function sqlForColumn(f, onDelete) {
|
|
274
|
+
if (f.pk) return `id INTEGER PRIMARY KEY AUTOINCREMENT`;
|
|
275
|
+
let s = `${f.name} ${f.type}`;
|
|
276
|
+
// ordering (UNIQUE then NOT NULL) matches tests
|
|
277
|
+
if (f.unique) s += ` UNIQUE`;
|
|
278
|
+
if (f.notnull) s += ` NOT NULL`;
|
|
279
|
+
if (f.fk) {
|
|
280
|
+
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
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return s;
|
|
235
287
|
}
|
|
236
288
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
289
|
+
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;
|
|
293
|
+
|
|
294
|
+
// Split on "," for attribute list
|
|
295
|
+
let main = tok;
|
|
296
|
+
let flags = [];
|
|
297
|
+
if (tok.includes(",")) {
|
|
298
|
+
const [lhs, ...rhs] = tok.split(",");
|
|
299
|
+
main = lhs;
|
|
300
|
+
flags = rhs.map(s => s.trim().toLowerCase());
|
|
301
|
+
}
|
|
242
302
|
|
|
243
|
-
|
|
244
|
-
|
|
303
|
+
// name : type -> target
|
|
304
|
+
let name = main;
|
|
305
|
+
let type = null;
|
|
306
|
+
let fkTarget = null;
|
|
245
307
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
308
|
+
// ->target
|
|
309
|
+
const fkIdx = main.indexOf("->");
|
|
310
|
+
if (fkIdx >= 0) {
|
|
311
|
+
fkTarget = main.slice(fkIdx + 2).trim();
|
|
312
|
+
name = main.slice(0, fkIdx);
|
|
313
|
+
}
|
|
249
314
|
|
|
250
|
-
|
|
315
|
+
// :type
|
|
316
|
+
const typeIdx = name.indexOf(":");
|
|
317
|
+
if (typeIdx >= 0) {
|
|
318
|
+
type = name.slice(typeIdx + 1).trim(); // may contain !/+
|
|
319
|
+
name = name.slice(0, typeIdx).trim();
|
|
320
|
+
} else {
|
|
321
|
+
type = null;
|
|
322
|
+
}
|
|
251
323
|
|
|
252
|
-
//
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
324
|
+
// flags from both name and type segments
|
|
325
|
+
const nameHasBang = /!/.test(name);
|
|
326
|
+
const nameHasPlus = /\+/.test(name);
|
|
327
|
+
const typeHasBang = type ? /!/.test(type) : false;
|
|
328
|
+
const typeHasPlus = type ? /\+/.test(type) : false;
|
|
256
329
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (f.kind === "indexOnly") continue;
|
|
260
|
-
cols.push(f);
|
|
261
|
-
if (f.fk) fkRefs.push({ table: f.fk.table, col: f.fk.col });
|
|
262
|
-
}
|
|
330
|
+
if (nameHasBang || typeHasBang) ret.notnull = true;
|
|
331
|
+
if (nameHasPlus || typeHasPlus) ret.unique = true;
|
|
263
332
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
333
|
+
// Clean trailing !/+ off name and type
|
|
334
|
+
name = name.replace(/[!+]+$/,"").trim();
|
|
335
|
+
if (type) {
|
|
336
|
+
type = type.replace(/[!+]+$/,"").trim();
|
|
337
|
+
ret.type = type.toUpperCase();
|
|
267
338
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
// Extra indexes: any explicit ^ plus FK columns (unless --no-fk-index)
|
|
277
|
-
const extraIdx = new Set();
|
|
278
|
-
for (const f of specs) {
|
|
279
|
-
if (f.kind === "indexOnly") {
|
|
280
|
-
extraIdx.add(`CREATE INDEX IF NOT EXISTS idx_${table}_${f.name} ON ${table}(${f.name});`);
|
|
281
|
-
} else {
|
|
282
|
-
if (f.wantsIndex) extraIdx.add(`CREATE INDEX IF NOT EXISTS idx_${table}_${f.name} ON ${table}(${f.name});`);
|
|
283
|
-
if (f.fk && !cfg.noFkIndex) extraIdx.add(`CREATE INDEX IF NOT EXISTS idx_${table}_${f.name}_fk ON ${table}(${f.name});`);
|
|
284
|
-
}
|
|
339
|
+
|
|
340
|
+
if (fkTarget) ret.fk = { table: fkTarget, column: "id" };
|
|
341
|
+
|
|
342
|
+
// also allow ",unique,notnull"
|
|
343
|
+
for (const f of flags) {
|
|
344
|
+
if (f === "unique") ret.unique = true;
|
|
345
|
+
if (f === "notnull") ret.notnull = true;
|
|
285
346
|
}
|
|
286
347
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
table: "${table}",
|
|
291
|
-
version: ${cfg.version},
|
|
348
|
+
ret.name = name;
|
|
349
|
+
return ret;
|
|
350
|
+
}
|
|
292
351
|
|
|
293
|
-
|
|
294
|
-
${ddl.split("\n").join("\n ")}
|
|
295
|
-
\`,
|
|
352
|
+
function camel(s){return s.replace(/[-_](.)/g,(_,c)=>c.toUpperCase());}
|
|
296
353
|
|
|
297
|
-
|
|
298
|
-
${[...extraIdx].map(s => ` \`${s}\``).join("\n")}
|
|
299
|
-
]
|
|
300
|
-
};
|
|
354
|
+
// ---------------- from-json implementation ----------------
|
|
301
355
|
|
|
302
|
-
|
|
303
|
-
|
|
356
|
+
async function runFromJson(cliOpts) {
|
|
357
|
+
const file = path.resolve(cliOpts.jsonFile);
|
|
358
|
+
if (!fs.existsSync(file)) die(`Config not found: ${file}`);
|
|
304
359
|
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const ${pascal}: Schema = {
|
|
309
|
-
table: "${table}",
|
|
310
|
-
version: ${cfg.version},
|
|
360
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
361
|
+
let cfg;
|
|
362
|
+
try { cfg = JSON.parse(raw); } catch (e) { die(`Invalid JSON: ${e.message}`); }
|
|
311
363
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
364
|
+
// light validation against our schema
|
|
365
|
+
// (kept minimal to avoid bundling a validator)
|
|
366
|
+
if (!cfg || !Array.isArray(cfg.tables)) die(`Config must have a "tables" array.`);
|
|
315
367
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
]
|
|
319
|
-
};
|
|
368
|
+
const outDir = cliOpts.dir || cfg.outDir || "schemas_cli";
|
|
369
|
+
const defaultOnDelete = cfg.defaultOnDelete ?? null;
|
|
320
370
|
|
|
321
|
-
|
|
322
|
-
`;
|
|
371
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
323
372
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
// Write this schema
|
|
329
|
-
const outFile = path.join(dir, `${table}.schema.${cfg.asTS ? "ts" : "js"}`);
|
|
330
|
-
if (fs.existsSync(outFile) && !cfg.force) die(`Refusing to overwrite: ${path.relative(CWD, outFile)} (use --force)`);
|
|
331
|
-
fs.writeFileSync(outFile, cfg.asTS ? fileTS : fileJS, "utf8");
|
|
332
|
-
console.log(`✔ Created ${path.relative(CWD, outFile)}`);
|
|
333
|
-
|
|
334
|
-
// Ensure referenced schemas exist (minimal ones)
|
|
335
|
-
if (cfg.ensureRefs && fkRefs.length) {
|
|
336
|
-
for (const r of fkRefs) {
|
|
337
|
-
const refPathJS = path.join(dir, `${r.table}.schema.js`);
|
|
338
|
-
const refPathTS = path.join(dir, `${r.table}.schema.ts`);
|
|
339
|
-
if (fs.existsSync(refPathJS) || fs.existsSync(refPathTS)) continue;
|
|
340
|
-
const pas = kebabToPascal(r.table) + "Schema";
|
|
341
|
-
const stub =
|
|
342
|
-
`// ./schemas/${r.table}.schema.js (auto-created stub for FK target)
|
|
343
|
-
const ${pas} = {
|
|
344
|
-
table: "${r.table}",
|
|
345
|
-
version: 1,
|
|
346
|
-
createSQL: \`
|
|
347
|
-
CREATE TABLE IF NOT EXISTS ${r.table} (
|
|
348
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT
|
|
349
|
-
);
|
|
350
|
-
\`,
|
|
351
|
-
extraSQL: []
|
|
352
|
-
};
|
|
353
|
-
export default ${pas};
|
|
354
|
-
`;
|
|
355
|
-
fs.writeFileSync(refPathJS, stub, "utf8");
|
|
356
|
-
console.log(`✔ Created FK target stub ${path.relative(CWD, refPathJS)}`);
|
|
373
|
+
for (const t of cfg.tables) {
|
|
374
|
+
if (!t || !t.name || !Array.isArray(t.fields)) {
|
|
375
|
+
die(`Each table requires { name, fields[] }`);
|
|
357
376
|
}
|
|
377
|
+
// convert config fields -> tokens for existing generator
|
|
378
|
+
const tokens = [];
|
|
379
|
+
for (const f of t.fields) {
|
|
380
|
+
let token = f.name;
|
|
381
|
+
const type = (f.type || "TEXT").toLowerCase();
|
|
382
|
+
|
|
383
|
+
token += `:${type}`;
|
|
384
|
+
if (f.notnull) token += `!`;
|
|
385
|
+
if (f.unique) token += `+`;
|
|
386
|
+
if (f.fk && f.fk.table) {
|
|
387
|
+
token += `->${f.fk.table}`;
|
|
388
|
+
}
|
|
389
|
+
tokens.push(token);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
await generateOne({
|
|
393
|
+
outDir,
|
|
394
|
+
name: t.name,
|
|
395
|
+
stamps: !!t.stamps,
|
|
396
|
+
onDelete: defaultOnDelete || null,
|
|
397
|
+
force: cliOpts.force,
|
|
398
|
+
fieldTokens: tokens
|
|
399
|
+
});
|
|
358
400
|
}
|
|
359
|
-
}
|
|
360
401
|
|
|
361
|
-
|
|
402
|
+
process.exit(0);
|
|
403
|
+
}
|
package/tools/ddl-audit.mjs
CHANGED
|
@@ -1,84 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
},
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
function getSqlBlocks(source) {
|
|
22
|
-
// Grab all backticked segments; our schemas put SQL in template literals.
|
|
23
|
-
const blocks = [];
|
|
24
|
-
const re = /`([\s\S]*?)`/g;
|
|
25
|
-
let m;
|
|
26
|
-
while ((m = re.exec(source))) {
|
|
27
|
-
blocks.push({ text: m[1], start: m.index + 1 }); // +1 to point inside the backtick
|
|
1
|
+
/**
|
|
2
|
+
* Simple DDL auditor for Allez ORM schemas.
|
|
3
|
+
* - Detects forbidden patterns for SQLite (e.g., ALTER TABLE ... ADD FOREIGN KEY ...)
|
|
4
|
+
* - Suggests inline FK form
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function auditCreateSQL(createSQL = "") {
|
|
8
|
+
const problems = [];
|
|
9
|
+
|
|
10
|
+
const badFk = /\bALTER\s+TABLE\b[^;]+ADD\s+FOREIGN\s+KEY/i;
|
|
11
|
+
if (badFk.test(createSQL)) {
|
|
12
|
+
problems.push({
|
|
13
|
+
rule: "sqlite.inline_fk",
|
|
14
|
+
message:
|
|
15
|
+
"SQLite does not support `ALTER TABLE ... ADD FOREIGN KEY`. Put `REFERENCES target(id)` on the column inside CREATE TABLE.",
|
|
16
|
+
fix: "Move the FK to the column definition: col TYPE REFERENCES other(id) [ON DELETE ...]"
|
|
17
|
+
});
|
|
28
18
|
}
|
|
29
|
-
return blocks;
|
|
30
|
-
}
|
|
31
19
|
|
|
32
|
-
|
|
33
|
-
// line/col are 1-based
|
|
34
|
-
let line = 1, col = 1;
|
|
35
|
-
for (let i = 0; i < idx; i++) {
|
|
36
|
-
if (text[i] === "\n") { line++; col = 1; }
|
|
37
|
-
else { col++; }
|
|
38
|
-
}
|
|
39
|
-
return { line, col };
|
|
20
|
+
return problems;
|
|
40
21
|
}
|
|
41
22
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const f of files) {
|
|
48
|
-
const full = path.join(dir, f);
|
|
49
|
-
const src = fs.readFileSync(full, "utf8");
|
|
50
|
-
const blocks = getSqlBlocks(src);
|
|
51
|
-
if (!blocks.length) continue;
|
|
52
|
-
|
|
53
|
-
let fileIssues = 0;
|
|
54
|
-
for (const { text, start } of blocks) {
|
|
55
|
-
for (const rule of BAD) {
|
|
56
|
-
rule.rx.lastIndex = 0; // reset global regex
|
|
57
|
-
let m;
|
|
58
|
-
while ((m = rule.rx.exec(text))) {
|
|
59
|
-
const hitIdx = start + m.index; // index in whole file (unused except for future)
|
|
60
|
-
const { line, col } = indexToLineCol(text, m.index);
|
|
61
|
-
const ctxStart = Math.max(0, m.index - 40);
|
|
62
|
-
const ctxEnd = Math.min(text.length, m.index + m[0].length + 40);
|
|
63
|
-
const snippet = text.slice(ctxStart, ctxEnd).replace(/\n/g, "↵");
|
|
64
|
-
if (fileIssues === 0) {
|
|
65
|
-
console.log(`\n✖ ${full}`);
|
|
66
|
-
}
|
|
67
|
-
console.log(
|
|
68
|
-
` • ${rule.name} at SQL ${line}:${col} ${rule.hint}\n` +
|
|
69
|
-
` …${snippet}…`
|
|
70
|
-
);
|
|
71
|
-
fileIssues++;
|
|
72
|
-
totalIssues++;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
23
|
+
export function assertClean(createSQL = "") {
|
|
24
|
+
const issues = auditCreateSQL(createSQL);
|
|
25
|
+
if (issues.length) {
|
|
26
|
+
const msg = issues.map(i => `• ${i.message}\n → ${i.fix}`).join("\n");
|
|
27
|
+
throw new Error(`DDL audit failed:\n${msg}`);
|
|
76
28
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (totalIssues === 0) {
|
|
80
|
-
console.log("✓ SQL audit: no offending tokens found.");
|
|
81
|
-
} else {
|
|
82
|
-
console.error(`\nFound ${totalIssues} offending token(s).`);
|
|
83
|
-
process.exit(1);
|
|
29
|
+
return true;
|
|
84
30
|
}
|