allez-orm 1.0.15 → 1.1.0
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 +35 -13
- package/index.d.ts +7 -0
- package/package.json +4 -2
package/allez-orm.mjs
CHANGED
|
@@ -27,6 +27,23 @@ const DEFAULT_DB_NAME = "allez.db";
|
|
|
27
27
|
const DEFAULT_AUTOSAVE_MS = 1500;
|
|
28
28
|
const isBrowser = typeof window !== "undefined";
|
|
29
29
|
|
|
30
|
+
// -------- identifier safety --------
|
|
31
|
+
const SAFE_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
32
|
+
|
|
33
|
+
/** Validate and quote a SQL identifier (table or column name). */
|
|
34
|
+
function safeIdent(name) {
|
|
35
|
+
if (typeof name !== "string" || !name) {
|
|
36
|
+
throw new Error(`Invalid SQL identifier: ${JSON.stringify(name)}`);
|
|
37
|
+
}
|
|
38
|
+
if (!SAFE_IDENT_RE.test(name)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Unsafe SQL identifier rejected: ${JSON.stringify(name)}. ` +
|
|
41
|
+
`Identifiers must match /^[A-Za-z_][A-Za-z0-9_]*$/.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return `"${name}"`;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
// -------- sql.js loader (browser-safe, no node core deps) --------
|
|
31
48
|
async function loadSqlJs(opts = {}) {
|
|
32
49
|
// 1) If user included <script src="https://sql.js.org/dist/sql-wasm.js">, use it.
|
|
@@ -141,54 +158,56 @@ export class AllezORM {
|
|
|
141
158
|
|
|
142
159
|
table(table) {
|
|
143
160
|
const self = this;
|
|
161
|
+
const t = safeIdent(table);
|
|
144
162
|
return {
|
|
145
163
|
async insert(obj) {
|
|
146
164
|
const cols = Object.keys(obj);
|
|
165
|
+
const safeCols = cols.map(c => safeIdent(c));
|
|
147
166
|
const qs = cols.map(() => "?").join(",");
|
|
148
167
|
await self.execute(
|
|
149
|
-
`INSERT INTO ${
|
|
168
|
+
`INSERT INTO ${t} (${safeCols.join(",")}) VALUES (${qs})`,
|
|
150
169
|
cols.map(c => obj[c])
|
|
151
170
|
);
|
|
152
171
|
},
|
|
153
172
|
async upsert(obj) {
|
|
154
173
|
const cols = Object.keys(obj);
|
|
174
|
+
const safeCols = cols.map(c => safeIdent(c));
|
|
155
175
|
const qs = cols.map(() => "?").join(",");
|
|
156
|
-
const updates =
|
|
176
|
+
const updates = safeCols.map(c => `${c}=excluded.${c}`).join(",");
|
|
157
177
|
await self.execute(
|
|
158
|
-
`INSERT INTO ${
|
|
159
|
-
ON CONFLICT(id) DO UPDATE SET ${updates}`,
|
|
178
|
+
`INSERT INTO ${t} (${safeCols.join(",")}) VALUES (${qs})
|
|
179
|
+
ON CONFLICT("id") DO UPDATE SET ${updates}`,
|
|
160
180
|
cols.map(c => obj[c])
|
|
161
181
|
);
|
|
162
182
|
},
|
|
163
183
|
async update(id, patch) {
|
|
164
184
|
const cols = Object.keys(patch);
|
|
165
185
|
if (!cols.length) return;
|
|
166
|
-
const assigns = cols.map(c => `${c}=?`).join(",");
|
|
186
|
+
const assigns = cols.map(c => `${safeIdent(c)}=?`).join(",");
|
|
167
187
|
await self.execute(
|
|
168
|
-
`UPDATE ${
|
|
188
|
+
`UPDATE ${t} SET ${assigns} WHERE "id"=?`,
|
|
169
189
|
[...cols.map(c => patch[c]), id]
|
|
170
190
|
);
|
|
171
191
|
},
|
|
172
192
|
async deleteSoft(id, ts = new Date().toISOString()) {
|
|
173
|
-
// keep naming consistent across projects
|
|
174
193
|
try {
|
|
175
|
-
await self.execute(`UPDATE ${
|
|
194
|
+
await self.execute(`UPDATE ${t} SET "deletedAt"=? WHERE "id"=?`, [ts, id]);
|
|
176
195
|
} catch {
|
|
177
|
-
await self.execute(`UPDATE ${
|
|
196
|
+
await self.execute(`UPDATE ${t} SET "deleted_at"=? WHERE "id"=?`, [ts, id]);
|
|
178
197
|
}
|
|
179
198
|
},
|
|
180
199
|
async remove(id) {
|
|
181
|
-
await self.execute(`DELETE FROM ${
|
|
200
|
+
await self.execute(`DELETE FROM ${t} WHERE "id"=?`, [id]);
|
|
182
201
|
},
|
|
183
202
|
async findById(id) {
|
|
184
|
-
return await self.get(`SELECT * FROM ${
|
|
203
|
+
return await self.get(`SELECT * FROM ${t} WHERE "id"=?`, [id]);
|
|
185
204
|
},
|
|
186
205
|
async searchLike(q, columns, limit = 50) {
|
|
187
206
|
if (!columns?.length) return [];
|
|
188
|
-
const where = columns.map(c => `${
|
|
207
|
+
const where = columns.map(c => `${t}.${safeIdent(c)} LIKE ?`).join(" OR ");
|
|
189
208
|
const params = columns.map(() => `%${q}%`);
|
|
190
209
|
return await self.query(
|
|
191
|
-
`SELECT * FROM ${
|
|
210
|
+
`SELECT * FROM ${t} WHERE (${where}) LIMIT ?`,
|
|
192
211
|
[...params, limit]
|
|
193
212
|
);
|
|
194
213
|
}
|
|
@@ -381,5 +400,8 @@ export async function exec(db, sql, params = []) {
|
|
|
381
400
|
await db.execute(sql, params);
|
|
382
401
|
}
|
|
383
402
|
|
|
403
|
+
// Expose safeIdent for consumers who build custom SQL.
|
|
404
|
+
export { safeIdent };
|
|
405
|
+
|
|
384
406
|
// Keep a default export for advanced consumers.
|
|
385
407
|
export default AllezORM;
|
package/index.d.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface InitOptions {
|
|
|
18
18
|
|
|
19
19
|
export type Row = Record<string, any>;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Validate and double-quote a SQL identifier (table or column name).
|
|
23
|
+
* Throws if the name contains unsafe characters.
|
|
24
|
+
* Valid pattern: /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
25
|
+
*/
|
|
26
|
+
export function safeIdent(name: string): string;
|
|
27
|
+
|
|
21
28
|
export interface TableHelper<T extends Row = Row> {
|
|
22
29
|
insert(obj: Partial<T>): Promise<void>;
|
|
23
30
|
upsert(obj: Partial<T>): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "allez-orm",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AllezORM: lightweight browser SQLite ORM (sql.js) + schema generator CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"allez": "node tools/allez-orm.mjs",
|
|
34
34
|
"test:cli": "node tests/test-cli.mjs",
|
|
35
35
|
"ddl:audit": "node tools/ddl-audit.mjs",
|
|
36
|
-
"
|
|
36
|
+
"test:security": "node tests/test-security.mjs",
|
|
37
|
+
"test": "node tests/test-cli.mjs && node tests/test-security.mjs && node tools/ddl-audit.mjs",
|
|
38
|
+
"prepublishOnly": "node tests/test-cli.mjs && node tests/test-security.mjs && node tools/ddl-audit.mjs"
|
|
37
39
|
},
|
|
38
40
|
"files": [
|
|
39
41
|
"allez-orm.mjs",
|