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 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 ${table} (${cols.join(",")}) VALUES (${qs})`,
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 = cols.map(c => `${c}=excluded.${c}`).join(",");
176
+ const updates = safeCols.map(c => `${c}=excluded.${c}`).join(",");
157
177
  await self.execute(
158
- `INSERT INTO ${table} (${cols.join(",")}) VALUES (${qs})
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 ${table} SET ${assigns} WHERE id=?`,
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 ${table} SET deletedAt=? WHERE id=?`, [ts, id]);
194
+ await self.execute(`UPDATE ${t} SET "deletedAt"=? WHERE "id"=?`, [ts, id]);
176
195
  } catch {
177
- await self.execute(`UPDATE ${table} SET deleted_at=? WHERE id=?`, [ts, id]);
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 ${table} WHERE id=?`, [id]);
200
+ await self.execute(`DELETE FROM ${t} WHERE "id"=?`, [id]);
182
201
  },
183
202
  async findById(id) {
184
- return await self.get(`SELECT * FROM ${table} WHERE id=?`, [id]);
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 => `${table}.${c} LIKE ?`).join(" OR ");
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 ${table} WHERE (${where}) LIMIT ?`,
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.15",
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
- "prepublishOnly": "node tests/test-cli.mjs && node tools/ddl-audit.mjs"
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",