allez-orm 1.0.5 → 1.0.8
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 +222 -206
- package/package.json +2 -1
package/allez-orm.mjs
CHANGED
|
@@ -20,259 +20,275 @@
|
|
|
20
20
|
* @property {number=} autoSaveMs
|
|
21
21
|
* @property {(file:string)=>string=} wasmLocateFile
|
|
22
22
|
* @property {Schema[]=} schemas
|
|
23
|
-
* @property {Record<string,{default:Schema}>=} schemaModules
|
|
23
|
+
* @property {Record<string,{default:Schema}>=} schemaModules
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
const DEFAULT_DB_NAME = "allez.db";
|
|
27
27
|
const DEFAULT_AUTOSAVE_MS = 1500;
|
|
28
28
|
|
|
29
|
+
/** Resolve/init sql.js WASM in a way that never triggers node core deps. */
|
|
30
|
+
async function loadSqlJs(opts = {}) {
|
|
31
|
+
// 1) If user included <script src="https://sql.js.org/dist/sql-wasm.js">, use it.
|
|
32
|
+
if (typeof window !== "undefined" && window.initSqlJs) {
|
|
33
|
+
return await window.initSqlJs({
|
|
34
|
+
locateFile: opts.wasmLocateFile ?? (f => `https://sql.js.org/dist/${f}`)
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2) Try a CDN ESM import that bundlers won't touch (no fs/path/crypto resolution).
|
|
39
|
+
// Dynamic URL import requires CORS; the official CDN allows it.
|
|
40
|
+
try {
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
const mod = await import(/* webpackIgnore: true */ "https://sql.js.org/dist/sql-wasm.js");
|
|
43
|
+
const initSqlJs = mod.default || mod;
|
|
44
|
+
return await initSqlJs({
|
|
45
|
+
locateFile: opts.wasmLocateFile ?? (f => `https://sql.js.org/dist/${f}`)
|
|
46
|
+
});
|
|
47
|
+
} catch (_) {
|
|
48
|
+
// continue to step 3
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3) Last resort: local dist entry. This ONLY works if the consumer configured
|
|
52
|
+
// resolve.alias OR fallbacks to disable node core modules in their bundler.
|
|
53
|
+
const mod = await import("sql.js/dist/sql-wasm.js"); // never import "sql.js"
|
|
54
|
+
const initSqlJs = mod.default || mod;
|
|
55
|
+
return await initSqlJs({
|
|
56
|
+
locateFile: opts.wasmLocateFile ?? (f => `https://sql.js.org/dist/${f}`)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
29
60
|
export class AllezORM {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
61
|
+
/** @param {any} SQL @param {any} db @param {InitOptions} opts */
|
|
62
|
+
constructor(SQL, db, opts) {
|
|
63
|
+
this.SQL = SQL;
|
|
64
|
+
this.db = db;
|
|
65
|
+
this.dbName = opts.dbName ?? DEFAULT_DB_NAME;
|
|
66
|
+
this.autoSaveMs = opts.autoSaveMs ?? DEFAULT_AUTOSAVE_MS;
|
|
67
|
+
this.saveTimer = null;
|
|
68
|
+
}
|
|
38
69
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.db.exec(sql);
|
|
52
|
-
}
|
|
53
|
-
// persist to IndexedDB if your class exposes it
|
|
54
|
-
if (typeof this.saveNow === "function") await this.saveNow();
|
|
55
|
-
return true;
|
|
70
|
+
// run arbitrary SQL (DDL/DML). Returns true on success.
|
|
71
|
+
async exec(sql, params = []) {
|
|
72
|
+
if (params && params.length) {
|
|
73
|
+
const stmt = this.db.prepare(sql);
|
|
74
|
+
try {
|
|
75
|
+
stmt.bind(params);
|
|
76
|
+
while (stmt.step()) { /* drain */ }
|
|
77
|
+
} finally {
|
|
78
|
+
stmt.free();
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
this.db.exec(sql);
|
|
56
82
|
}
|
|
83
|
+
if (typeof this.saveNow === "function") await this.saveNow();
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
57
86
|
|
|
58
|
-
|
|
59
|
-
run(sql, params) { return this.exec(sql, params); }
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
/** @param {InitOptions=} opts */
|
|
63
|
-
static async init(opts = {}) {
|
|
64
|
-
// Resolve sql.js from global <script> or dynamic import
|
|
65
|
-
let SQL;
|
|
66
|
-
if (typeof window !== "undefined" && window.initSqlJs) {
|
|
67
|
-
SQL = await window.initSqlJs({
|
|
68
|
-
locateFile: opts.wasmLocateFile ?? (f => `https://sql.js.org/dist/${f}`)
|
|
69
|
-
});
|
|
70
|
-
} else {
|
|
71
|
-
const { default: initSqlJs } = await import("sql.js/dist/sql-wasm.js");
|
|
72
|
-
SQL = await initSqlJs({
|
|
73
|
-
locateFile: opts.wasmLocateFile ?? (f => `https://sql.js.org/dist/${f}`)
|
|
74
|
-
});
|
|
75
|
-
}
|
|
87
|
+
run(sql, params) { return this.exec(sql, params); }
|
|
76
88
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const orm = new AllezORM(SQL, db, opts);
|
|
82
|
-
await orm.execute("PRAGMA foreign_keys = ON;"); // <-- add this line
|
|
83
|
-
await orm.#ensureMeta();
|
|
89
|
+
/** @param {InitOptions=} opts */
|
|
90
|
+
static async init(opts = {}) {
|
|
91
|
+
// Always use the WASM/browser build loader above.
|
|
92
|
+
const SQL = await loadSqlJs(opts);
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
// Restore DB from IndexedDB, or create fresh
|
|
95
|
+
const saved = await idbGet(opts.dbName ?? DEFAULT_DB_NAME);
|
|
96
|
+
const db = saved ? new SQL.Database(saved) : new SQL.Database();
|
|
88
97
|
|
|
98
|
+
const orm = new AllezORM(SQL, db, opts);
|
|
99
|
+
await orm.execute("PRAGMA foreign_keys = ON;");
|
|
100
|
+
await orm.#ensureMeta();
|
|
89
101
|
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
const schemas = collectSchemas(opts);
|
|
103
|
+
await orm.registerSchemas(schemas);
|
|
104
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
92
105
|
|
|
93
|
-
|
|
106
|
+
return orm;
|
|
107
|
+
}
|
|
94
108
|
|
|
95
|
-
|
|
96
|
-
const stmt = this.db.prepare(sql);
|
|
97
|
-
try {
|
|
98
|
-
stmt.bind(params);
|
|
99
|
-
while (stmt.step()) { } // drain
|
|
100
|
-
} finally {
|
|
101
|
-
stmt.free();
|
|
102
|
-
}
|
|
103
|
-
this.#scheduleSave();
|
|
104
|
-
}
|
|
109
|
+
// ---------------- core SQL helpers ----------------
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
stmt.free();
|
|
114
|
-
}
|
|
115
|
-
return out;
|
|
111
|
+
async execute(sql, params = []) {
|
|
112
|
+
const stmt = this.db.prepare(sql);
|
|
113
|
+
try {
|
|
114
|
+
stmt.bind(params);
|
|
115
|
+
while (stmt.step()) { /* drain */ }
|
|
116
|
+
} finally {
|
|
117
|
+
stmt.free();
|
|
116
118
|
}
|
|
119
|
+
this.#scheduleSave();
|
|
120
|
+
}
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
async query(sql, params = []) {
|
|
123
|
+
const stmt = this.db.prepare(sql);
|
|
124
|
+
const out = [];
|
|
125
|
+
try {
|
|
126
|
+
stmt.bind(params);
|
|
127
|
+
while (stmt.step()) out.push(stmt.getAsObject());
|
|
128
|
+
} finally {
|
|
129
|
+
stmt.free();
|
|
121
130
|
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async get(sql, params = []) {
|
|
135
|
+
const rows = await this.query(sql, params);
|
|
136
|
+
return rows[0];
|
|
137
|
+
}
|
|
122
138
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
// ---------------- table helper ----------------
|
|
140
|
+
|
|
141
|
+
table(table) {
|
|
142
|
+
const self = this;
|
|
143
|
+
return {
|
|
144
|
+
async insert(obj) {
|
|
145
|
+
const cols = Object.keys(obj);
|
|
146
|
+
const qs = cols.map(() => "?").join(",");
|
|
147
|
+
await self.execute(
|
|
148
|
+
`INSERT INTO ${table} (${cols.join(",")}) VALUES (${qs})`,
|
|
149
|
+
cols.map(c => obj[c])
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
async upsert(obj) {
|
|
153
|
+
const cols = Object.keys(obj);
|
|
154
|
+
const qs = cols.map(() => "?").join(",");
|
|
155
|
+
const updates = cols.map(c => `${c}=excluded.${c}`).join(",");
|
|
156
|
+
await self.execute(
|
|
157
|
+
`INSERT INTO ${table} (${cols.join(",")}) VALUES (${qs})
|
|
142
158
|
ON CONFLICT(id) DO UPDATE SET ${updates}`,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
159
|
+
cols.map(c => obj[c])
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
async update(id, patch) {
|
|
163
|
+
const cols = Object.keys(patch);
|
|
164
|
+
if (!cols.length) return;
|
|
165
|
+
const assigns = cols.map(c => `${c}=?`).join(",");
|
|
166
|
+
await self.execute(
|
|
167
|
+
`UPDATE ${table} SET ${assigns} WHERE id=?`,
|
|
168
|
+
[...cols.map(c => patch[c]), id]
|
|
169
|
+
);
|
|
170
|
+
},
|
|
171
|
+
async deleteSoft(id, ts = new Date().toISOString()) {
|
|
172
|
+
await self.execute(`UPDATE ${table} SET deleted_at=? WHERE id=?`, [ts, id]);
|
|
173
|
+
},
|
|
174
|
+
async remove(id) {
|
|
175
|
+
await self.execute(`DELETE FROM ${table} WHERE id=?`, [id]);
|
|
176
|
+
},
|
|
177
|
+
async findById(id) {
|
|
178
|
+
return await self.get(`SELECT * FROM ${table} WHERE id=?`, [id]);
|
|
179
|
+
},
|
|
180
|
+
async searchLike(q, columns, limit = 50) {
|
|
181
|
+
if (!columns?.length) return [];
|
|
182
|
+
const where = columns.map(c => `${table}.${c} LIKE ?`).join(" OR ");
|
|
183
|
+
const params = columns.map(() => `%${q}%`);
|
|
184
|
+
return await self.query(
|
|
185
|
+
`SELECT * FROM ${table} WHERE (${where}) LIMIT ?`,
|
|
186
|
+
[...params, limit]
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
175
191
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
}
|
|
192
|
+
// ---------------- schema registration ----------------
|
|
193
|
+
|
|
194
|
+
/** @param {Schema[]} schemas */
|
|
195
|
+
async registerSchemas(schemas) {
|
|
196
|
+
const meta = await this.#currentVersions();
|
|
197
|
+
for (const s of schemas) {
|
|
198
|
+
const exists = await this.get(
|
|
199
|
+
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
|
200
|
+
[s.table]
|
|
201
|
+
);
|
|
202
|
+
if (!exists) {
|
|
203
|
+
await this.execute(s.createSQL);
|
|
204
|
+
if (Array.isArray(s.extraSQL)) {
|
|
205
|
+
for (const x of s.extraSQL) await this.execute(x);
|
|
206
|
+
}
|
|
207
|
+
await this.execute(
|
|
208
|
+
`INSERT OR REPLACE INTO allez_meta(table_name,version) VALUES(?,?)`,
|
|
209
|
+
[s.table, s.version ?? 1]
|
|
210
|
+
);
|
|
211
|
+
} else {
|
|
212
|
+
const cur = meta.get(s.table) ?? 1;
|
|
213
|
+
const next = s.version ?? cur;
|
|
214
|
+
if (s.onUpgrade && next > cur) {
|
|
215
|
+
await s.onUpgrade(this.db, cur, next);
|
|
216
|
+
await this.execute(
|
|
217
|
+
`UPDATE allez_meta SET version=? WHERE table_name=?`,
|
|
218
|
+
[next, s.table]
|
|
219
|
+
);
|
|
206
220
|
}
|
|
207
|
-
|
|
221
|
+
}
|
|
208
222
|
}
|
|
223
|
+
this.#scheduleSave();
|
|
224
|
+
}
|
|
209
225
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
async saveNow() {
|
|
227
|
+
const data = this.db.export(); // Uint8Array
|
|
228
|
+
await idbSet(this.dbName, data);
|
|
229
|
+
}
|
|
214
230
|
|
|
215
|
-
|
|
231
|
+
// ---------------- internals ----------------
|
|
216
232
|
|
|
217
|
-
|
|
218
|
-
|
|
233
|
+
async #ensureMeta() {
|
|
234
|
+
await this.execute(`
|
|
219
235
|
CREATE TABLE IF NOT EXISTS allez_meta (
|
|
220
236
|
table_name TEXT PRIMARY KEY,
|
|
221
237
|
version INTEGER NOT NULL
|
|
222
238
|
);
|
|
223
239
|
`);
|
|
224
|
-
|
|
240
|
+
}
|
|
225
241
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
242
|
+
async #currentVersions() {
|
|
243
|
+
const rows = await this.query(
|
|
244
|
+
`SELECT table_name, version FROM allez_meta`
|
|
245
|
+
);
|
|
246
|
+
return new Map(rows.map(r => [r.table_name, r.version]));
|
|
247
|
+
}
|
|
232
248
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
249
|
+
#scheduleSave() {
|
|
250
|
+
clearTimeout(this.saveTimer);
|
|
251
|
+
this.saveTimer = setTimeout(() => { void this.saveNow(); }, this.autoSaveMs);
|
|
252
|
+
}
|
|
237
253
|
}
|
|
238
254
|
|
|
239
255
|
// ---------------- helpers: schema collection + IndexedDB ----------------
|
|
240
256
|
|
|
241
257
|
/** @param {InitOptions} opts */
|
|
242
258
|
function collectSchemas(opts) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
259
|
+
const fromModules = opts.schemaModules
|
|
260
|
+
? Object.values(opts.schemaModules).map(m => m.default)
|
|
261
|
+
: [];
|
|
262
|
+
const fromArray = opts.schemas ?? [];
|
|
263
|
+
return [...fromModules, ...fromArray];
|
|
248
264
|
}
|
|
249
265
|
|
|
250
266
|
function openIdb() {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
267
|
+
return new Promise((resolve, reject) => {
|
|
268
|
+
const req = indexedDB.open("allez-orm-store", 1);
|
|
269
|
+
req.onupgradeneeded = () => req.result.createObjectStore("dbs");
|
|
270
|
+
req.onerror = () => reject(req.error);
|
|
271
|
+
req.onsuccess = () => resolve(req.result);
|
|
272
|
+
});
|
|
257
273
|
}
|
|
258
274
|
|
|
259
275
|
async function idbGet(key) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
const db = await openIdb();
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const tx = db.transaction("dbs", "readonly");
|
|
279
|
+
const store = tx.objectStore("dbs");
|
|
280
|
+
const get = store.get(key);
|
|
281
|
+
get.onsuccess = () => resolve(get.result || null);
|
|
282
|
+
get.onerror = () => reject(get.error);
|
|
283
|
+
});
|
|
268
284
|
}
|
|
269
285
|
|
|
270
286
|
async function idbSet(key, value) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
287
|
+
const db = await openIdb();
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
const tx = db.transaction("dbs", "readwrite");
|
|
290
|
+
tx.objectStore("dbs").put(value, key);
|
|
291
|
+
tx.oncomplete = () => resolve();
|
|
292
|
+
tx.onerror = () => reject(tx.error);
|
|
293
|
+
});
|
|
278
294
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "allez-orm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "AllezORM: lightweight browser SQLite ORM (sql.js) + schema generator CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./allez-orm.mjs",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
18
|
".": {
|
|
19
|
+
"types": "./index.d.ts",
|
|
19
20
|
"import": "./allez-orm.mjs",
|
|
20
21
|
"default": "./allez-orm.mjs"
|
|
21
22
|
},
|