@camstack/core 0.1.12 → 0.1.13

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.
@@ -45,6 +45,7 @@ var SqliteSettingsBackend = class {
45
45
  }
46
46
  db = null;
47
47
  ensuredTables = /* @__PURE__ */ new Set();
48
+ structuredTables = /* @__PURE__ */ new Set();
48
49
  runtimeDefaults;
49
50
  async initialize() {
50
51
  const dir = this.dbPath.substring(0, this.dbPath.lastIndexOf("/"));
@@ -68,17 +69,17 @@ var SqliteSettingsBackend = class {
68
69
  // ISettingsBackend implementation
69
70
  // ---------------------------------------------------------------------------
70
71
  async get(collection, key) {
71
- this.ensureTable(collection);
72
+ this.ensureCollectionTable(collection);
72
73
  const row = this.getDb().prepare(`SELECT data FROM "${collection}" WHERE id = ?`).get(key);
73
74
  if (!row) return void 0;
74
75
  return JSON.parse(row.data);
75
76
  }
76
77
  async set(collection, key, value) {
77
- this.ensureTable(collection);
78
+ this.ensureCollectionTable(collection);
78
79
  this.getDb().prepare(`INSERT INTO "${collection}" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`).run(key, JSON.stringify(value));
79
80
  }
80
81
  async query(collection, filter) {
81
- this.ensureTable(collection);
82
+ this.ensureCollectionTable(collection);
82
83
  let sql = `SELECT id, data FROM "${collection}"`;
83
84
  const params = [];
84
85
  const whereClauses = [];
@@ -140,20 +141,20 @@ var SqliteSettingsBackend = class {
140
141
  }));
141
142
  }
142
143
  async insert(collection, record) {
143
- this.ensureTable(collection);
144
+ this.ensureCollectionTable(collection);
144
145
  const id = record.id || (0, import_node_crypto.randomUUID)();
145
146
  this.getDb().prepare(`INSERT INTO "${collection}" (id, data) VALUES (?, ?)`).run(id, JSON.stringify(record.data));
146
147
  }
147
148
  async update(collection, id, data) {
148
- this.ensureTable(collection);
149
+ this.ensureCollectionTable(collection);
149
150
  this.getDb().prepare(`UPDATE "${collection}" SET data = ? WHERE id = ?`).run(JSON.stringify(data), id);
150
151
  }
151
152
  async delete(collection, key) {
152
- this.ensureTable(collection);
153
+ this.ensureCollectionTable(collection);
153
154
  this.getDb().prepare(`DELETE FROM "${collection}" WHERE id = ?`).run(key);
154
155
  }
155
156
  async count(collection, filter) {
156
- this.ensureTable(collection);
157
+ this.ensureCollectionTable(collection);
157
158
  if (!filter) {
158
159
  const row = this.getDb().prepare(`SELECT COUNT(*) AS cnt FROM "${collection}"`).get();
159
160
  return row?.cnt ?? 0;
@@ -162,7 +163,7 @@ var SqliteSettingsBackend = class {
162
163
  return rows.length;
163
164
  }
164
165
  async isEmpty(collection) {
165
- this.ensureTable(collection);
166
+ this.ensureCollectionTable(collection);
166
167
  const row = this.getDb().prepare(`SELECT COUNT(*) AS cnt FROM "${collection}"`).get();
167
168
  return (row?.cnt ?? 0) === 0;
168
169
  }
@@ -171,25 +172,25 @@ var SqliteSettingsBackend = class {
171
172
  // ---------------------------------------------------------------------------
172
173
  /** Get a system setting by dot-notation key */
173
174
  getSystem(key) {
174
- this.ensureTable("system-settings");
175
+ this.ensureCollectionTable("system-settings");
175
176
  const row = this.getDb().prepare('SELECT data FROM "system-settings" WHERE id = ?').get(key);
176
177
  if (!row) return void 0;
177
178
  return JSON.parse(row.data);
178
179
  }
179
180
  /** Set a system setting */
180
181
  setSystem(key, value) {
181
- this.ensureTable("system-settings");
182
+ this.ensureCollectionTable("system-settings");
182
183
  this.getDb().prepare('INSERT INTO "system-settings" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data').run(key, JSON.stringify(value));
183
184
  }
184
185
  /** Get all system settings as flat key-value */
185
186
  getAllSystem() {
186
- this.ensureTable("system-settings");
187
+ this.ensureCollectionTable("system-settings");
187
188
  const rows = this.getDb().prepare('SELECT id, data FROM "system-settings"').all();
188
189
  return Object.fromEntries(rows.map((r) => [r.id, JSON.parse(r.data)]));
189
190
  }
190
191
  /** Get all settings for an addon */
191
192
  getAllAddon(addonId) {
192
- this.ensureTable("addon-settings");
193
+ this.ensureCollectionTable("addon-settings");
193
194
  const rows = this.getDb().prepare(`SELECT id, data FROM "addon-settings" WHERE json_extract(data, '$.addonId') = ?`).all(addonId);
194
195
  if (rows.length === 0) return {};
195
196
  const result = {};
@@ -202,7 +203,7 @@ var SqliteSettingsBackend = class {
202
203
  }
203
204
  /** Bulk-set all settings for an addon */
204
205
  setAllAddon(addonId, config) {
205
- this.ensureTable("addon-settings");
206
+ this.ensureCollectionTable("addon-settings");
206
207
  const db = this.getDb();
207
208
  const deleteStmt = db.prepare(`DELETE FROM "addon-settings" WHERE id LIKE ? || '%'`);
208
209
  const insertStmt = db.prepare('INSERT INTO "addon-settings" (id, data) VALUES (?, ?)');
@@ -231,7 +232,7 @@ var SqliteSettingsBackend = class {
231
232
  }
232
233
  /** Seed system-settings with runtime defaults (first boot) */
233
234
  async seedDefaults() {
234
- this.ensureTable("system-settings");
235
+ this.ensureCollectionTable("system-settings");
235
236
  const insert = this.getDb().prepare(
236
237
  'INSERT OR IGNORE INTO "system-settings" (id, data) VALUES (?, ?)'
237
238
  );
@@ -248,7 +249,7 @@ var SqliteSettingsBackend = class {
248
249
  if (!this.db) throw new Error("SqliteSettingsBackend not initialized \u2014 call initialize() first");
249
250
  return this.db;
250
251
  }
251
- ensureTable(collection) {
252
+ ensureCollectionTable(collection) {
252
253
  if (this.ensuredTables.has(collection)) return;
253
254
  this.getDb().exec(
254
255
  `CREATE TABLE IF NOT EXISTS "${collection}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`
@@ -256,7 +257,7 @@ var SqliteSettingsBackend = class {
256
257
  this.ensuredTables.add(collection);
257
258
  }
258
259
  getAllScoped(collection, scopeId) {
259
- this.ensureTable(collection);
260
+ this.ensureCollectionTable(collection);
260
261
  const rows = this.getDb().prepare(`SELECT id, data FROM "${collection}" WHERE id LIKE ? || '.%'`).all(scopeId);
261
262
  const result = {};
262
263
  for (const row of rows) {
@@ -267,9 +268,112 @@ var SqliteSettingsBackend = class {
267
268
  return result;
268
269
  }
269
270
  setScopedKey(collection, scopeId, key, value) {
270
- this.ensureTable(collection);
271
+ this.ensureCollectionTable(collection);
271
272
  this.getDb().prepare(`INSERT INTO "${collection}" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`).run(`${scopeId}.${key}`, JSON.stringify({ scopeId, key, value }));
272
273
  }
274
+ // ── Structured table operations ────────────────────────────────────
275
+ async ensureTable(table, schema) {
276
+ if (!schema) {
277
+ if (!this.ensuredTables.has(table)) {
278
+ this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`);
279
+ this.ensuredTables.add(table);
280
+ }
281
+ return;
282
+ }
283
+ if (this.structuredTables.has(table)) return;
284
+ const colDefs = schema.columns.map((col) => {
285
+ const parts = [`"${col.name}" ${col.type}`];
286
+ if (col.primaryKey) parts.push("PRIMARY KEY");
287
+ if (col.notNull) parts.push("NOT NULL");
288
+ if (col.unique) parts.push("UNIQUE");
289
+ if (col.defaultValue !== void 0) {
290
+ parts.push(`DEFAULT ${typeof col.defaultValue === "string" ? `'${col.defaultValue}'` : col.defaultValue === null ? "NULL" : String(col.defaultValue)}`);
291
+ }
292
+ return parts.join(" ");
293
+ });
294
+ this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`);
295
+ if (schema.indexes) {
296
+ for (const idx of schema.indexes) {
297
+ const unique = idx.unique ? "UNIQUE " : "";
298
+ const cols = idx.columns.map((c) => `"${c}"`).join(", ");
299
+ this.getDb().exec(`CREATE ${unique}INDEX IF NOT EXISTS "${idx.name}" ON "${table}" (${cols})`);
300
+ }
301
+ }
302
+ this.structuredTables.add(table);
303
+ }
304
+ async tableInsert(table, row) {
305
+ const keys = Object.keys(row);
306
+ const cols = keys.map((k) => `"${k}"`).join(", ");
307
+ const placeholders = keys.map(() => "?").join(", ");
308
+ const values = keys.map((k) => {
309
+ const v = row[k];
310
+ return typeof v === "object" && v !== null ? JSON.stringify(v) : v;
311
+ });
312
+ this.getDb().prepare(`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`).run(...values);
313
+ }
314
+ async tableUpdate(table, filter, updates) {
315
+ const setClauses = [];
316
+ const setValues = [];
317
+ for (const [k, v] of Object.entries(updates)) {
318
+ setClauses.push(`"${k}" = ?`);
319
+ setValues.push(typeof v === "object" && v !== null ? JSON.stringify(v) : v);
320
+ }
321
+ const { whereSql, whereValues } = this.buildWhere(filter);
322
+ const result = this.getDb().prepare(`UPDATE "${table}" SET ${setClauses.join(", ")}${whereSql}`).run(...setValues, ...whereValues);
323
+ return result.changes;
324
+ }
325
+ async tableDelete(table, filter) {
326
+ const { whereSql, whereValues } = this.buildWhere(filter);
327
+ const result = this.getDb().prepare(`DELETE FROM "${table}"${whereSql}`).run(...whereValues);
328
+ return result.changes;
329
+ }
330
+ async tableQuery(table, options) {
331
+ let sql = `SELECT * FROM "${table}"`;
332
+ const values = [];
333
+ if (options?.where) {
334
+ const { whereSql, whereValues } = this.buildWhere(options.where);
335
+ sql += whereSql;
336
+ values.push(...whereValues);
337
+ }
338
+ if (options?.orderBy) {
339
+ sql += ` ORDER BY "${options.orderBy.field}" ${options.orderBy.direction === "desc" ? "DESC" : "ASC"}`;
340
+ }
341
+ if (options?.limit !== void 0) {
342
+ sql += ` LIMIT ?`;
343
+ values.push(options.limit);
344
+ }
345
+ if (options?.offset !== void 0) {
346
+ sql += ` OFFSET ?`;
347
+ values.push(options.offset);
348
+ }
349
+ return this.getDb().prepare(sql).all(...values);
350
+ }
351
+ async tableGet(table, filter) {
352
+ const { whereSql, whereValues } = this.buildWhere(filter);
353
+ const row = this.getDb().prepare(`SELECT * FROM "${table}"${whereSql} LIMIT 1`).get(...whereValues);
354
+ return row ?? null;
355
+ }
356
+ async tableCount(table, filter) {
357
+ let sql = `SELECT COUNT(*) as count FROM "${table}"`;
358
+ const values = [];
359
+ if (filter) {
360
+ const { whereSql, whereValues } = this.buildWhere(filter);
361
+ sql += whereSql;
362
+ values.push(...whereValues);
363
+ }
364
+ const row = this.getDb().prepare(sql).get(...values);
365
+ return row.count;
366
+ }
367
+ buildWhere(filter) {
368
+ const conditions = [];
369
+ const values = [];
370
+ for (const [k, v] of Object.entries(filter)) {
371
+ conditions.push(`"${k}" = ?`);
372
+ values.push(typeof v === "object" && v !== null ? JSON.stringify(v) : v);
373
+ }
374
+ const whereSql = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
375
+ return { whereSql, whereValues: values };
376
+ }
273
377
  };
274
378
 
275
379
  // src/builtins/sqlite-storage/sqlite-settings.addon.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/builtins/sqlite-storage/sqlite-settings.addon.ts","../../../src/builtins/sqlite-storage/sqlite-settings-backend.ts"],"sourcesContent":["import type {\n ICamstackAddon,\n AddonManifest,\n AddonContext,\n IConfigurable,\n ConfigUISchema,\n CapabilityProviderMap,\n} from '@camstack/types'\nimport { SqliteSettingsBackend } from './sqlite-settings-backend'\n\n/**\n * SQLite Settings addon — provides persistent settings storage.\n * Capability: 'settings-store' (singleton)\n *\n * Depends on 'storage' capability for resolving the DB file path.\n */\nexport class SqliteSettingsAddon implements ICamstackAddon, IConfigurable {\n readonly manifest: AddonManifest = {\n id: 'sqlite-settings',\n name: 'SQLite Settings',\n version: '1.0.0',\n capabilities: [{ name: 'settings-store', mode: 'singleton' }],\n }\n\n private backend: SqliteSettingsBackend | null = null\n\n async initialize(context: AddonContext): Promise<void> {\n // Resolve DB path from storage provider (addons-data location)\n const dbPath = context.storageProvider\n ? context.storageProvider.resolve('addons-data', `${context.id.replace('addon:', '')}/camstack.db`)\n : context.dataDir\n ? `${context.dataDir}/camstack.db`\n : 'camstack-data/addons-data/sqlite-settings/camstack.db'\n\n const runtimeDefaults = (context.addonConfig._runtimeDefaults as Record<string, unknown>) ?? {}\n this.backend = new SqliteSettingsBackend(dbPath, runtimeDefaults)\n await this.backend.initialize()\n context.logger.info(`SQLite settings initialized at ${dbPath}`)\n }\n\n async shutdown(): Promise<void> {\n await this.backend?.shutdown()\n }\n\n getBackend(): SqliteSettingsBackend | null {\n return this.backend\n }\n\n getCapabilityProvider<K extends keyof CapabilityProviderMap>(\n name: K,\n ): CapabilityProviderMap[K] | null {\n if (name === ('settings-store' as string) && this.backend) {\n return this.backend as unknown as CapabilityProviderMap[K]\n }\n return null\n }\n\n getConfigSchema(): ConfigUISchema {\n return { sections: [] }\n }\n\n getConfig(): Record<string, unknown> {\n return {}\n }\n\n async onConfigChange(_config: Record<string, unknown>): Promise<void> {\n // DB path changes require restart\n }\n}\n\nexport default SqliteSettingsAddon\n","import Database from 'better-sqlite3'\nimport { randomUUID } from 'node:crypto'\nimport type {\n ISettingsBackend,\n SettingsCollection,\n SettingsRecord,\n QueryFilter,\n} from '@camstack/types'\n\n/**\n * SQLite implementation of ISettingsBackend.\n *\n * One table per collection, auto-created on first access.\n * All data stored as JSON in a TEXT column.\n * WAL mode for concurrent reads.\n */\nexport class SqliteSettingsBackend implements ISettingsBackend {\n private db: Database.Database | null = null\n private readonly ensuredTables = new Set<string>()\n private readonly runtimeDefaults: Record<string, unknown>\n\n constructor(private readonly dbPath: string, runtimeDefaults?: Record<string, unknown>) {\n this.runtimeDefaults = runtimeDefaults ?? {}\n }\n\n async initialize(): Promise<void> {\n // Ensure parent directory exists\n const dir = this.dbPath.substring(0, this.dbPath.lastIndexOf('/'))\n if (dir) {\n const fs = await import('node:fs')\n fs.mkdirSync(dir, { recursive: true })\n }\n this.db = new Database(this.dbPath)\n this.db.pragma('journal_mode = WAL')\n this.db.pragma('foreign_keys = ON')\n\n // Seed system-settings on first boot\n const isEmpty = await this.isEmpty('system-settings')\n if (isEmpty) {\n await this.seedDefaults()\n }\n }\n\n async shutdown(): Promise<void> {\n this.db?.close()\n this.db = null\n }\n\n // ---------------------------------------------------------------------------\n // ISettingsBackend implementation\n // ---------------------------------------------------------------------------\n\n async get(collection: SettingsCollection, key: string): Promise<unknown> {\n this.ensureTable(collection)\n const row = this.getDb()\n .prepare<[string], { data: string }>(`SELECT data FROM \"${collection}\" WHERE id = ?`)\n .get(key)\n if (!row) return undefined\n return JSON.parse(row.data)\n }\n\n async set(collection: SettingsCollection, key: string, value: unknown): Promise<void> {\n this.ensureTable(collection)\n this.getDb()\n .prepare(`INSERT INTO \"${collection}\" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`)\n .run(key, JSON.stringify(value))\n }\n\n async query(collection: SettingsCollection, filter?: QueryFilter): Promise<readonly SettingsRecord[]> {\n this.ensureTable(collection)\n let sql = `SELECT id, data FROM \"${collection}\"`\n const params: unknown[] = []\n\n const whereClauses: string[] = []\n\n if (filter?.where) {\n for (const [field, value] of Object.entries(filter.where)) {\n if (field === 'id') {\n whereClauses.push('id = ?')\n params.push(value)\n } else {\n whereClauses.push(`json_extract(data, '$.${field}') = ?`)\n params.push(typeof value === 'object' ? JSON.stringify(value) : value)\n }\n }\n }\n\n if (filter?.whereIn) {\n for (const [field, values] of Object.entries(filter.whereIn)) {\n const placeholders = values.map(() => '?').join(', ')\n if (field === 'id') {\n whereClauses.push(`id IN (${placeholders})`)\n } else {\n whereClauses.push(`json_extract(data, '$.${field}') IN (${placeholders})`)\n }\n params.push(...values)\n }\n }\n\n if (filter?.whereBetween) {\n for (const [field, [low, high]] of Object.entries(filter.whereBetween)) {\n if (field === 'id') {\n whereClauses.push('id BETWEEN ? AND ?')\n } else {\n whereClauses.push(`json_extract(data, '$.${field}') BETWEEN ? AND ?`)\n }\n params.push(low, high)\n }\n }\n\n if (whereClauses.length > 0) {\n sql += ` WHERE ${whereClauses.join(' AND ')}`\n }\n\n if (filter?.orderBy) {\n const dir = filter.orderBy.direction === 'desc' ? 'DESC' : 'ASC'\n if (filter.orderBy.field === 'id') {\n sql += ` ORDER BY id ${dir}`\n } else {\n sql += ` ORDER BY json_extract(data, '$.${filter.orderBy.field}') ${dir}`\n }\n }\n\n if (filter?.limit) {\n sql += ` LIMIT ?`\n params.push(filter.limit)\n }\n\n if (filter?.offset) {\n sql += ` OFFSET ?`\n params.push(filter.offset)\n }\n\n const rows = this.getDb()\n .prepare<unknown[], { id: string; data: string }>(sql)\n .all(...params)\n\n return rows.map(row => ({\n id: row.id,\n data: JSON.parse(row.data),\n }))\n }\n\n async insert(collection: SettingsCollection, record: SettingsRecord): Promise<void> {\n this.ensureTable(collection)\n const id = record.id || randomUUID()\n this.getDb()\n .prepare(`INSERT INTO \"${collection}\" (id, data) VALUES (?, ?)`)\n .run(id, JSON.stringify(record.data))\n }\n\n async update(collection: SettingsCollection, id: string, data: Record<string, unknown>): Promise<void> {\n this.ensureTable(collection)\n this.getDb()\n .prepare(`UPDATE \"${collection}\" SET data = ? WHERE id = ?`)\n .run(JSON.stringify(data), id)\n }\n\n async delete(collection: SettingsCollection, key: string): Promise<void> {\n this.ensureTable(collection)\n this.getDb()\n .prepare(`DELETE FROM \"${collection}\" WHERE id = ?`)\n .run(key)\n }\n\n async count(collection: SettingsCollection, filter?: QueryFilter): Promise<number> {\n this.ensureTable(collection)\n if (!filter) {\n const row = this.getDb()\n .prepare<[], { cnt: number }>(`SELECT COUNT(*) AS cnt FROM \"${collection}\"`)\n .get()\n return row?.cnt ?? 0\n }\n const rows = await this.query(collection, { ...filter, limit: undefined, offset: undefined })\n return rows.length\n }\n\n async isEmpty(collection: SettingsCollection): Promise<boolean> {\n this.ensureTable(collection)\n const row = this.getDb()\n .prepare<[], { cnt: number }>(`SELECT COUNT(*) AS cnt FROM \"${collection}\"`)\n .get()\n return (row?.cnt ?? 0) === 0\n }\n\n // ---------------------------------------------------------------------------\n // Legacy SettingsStore compatibility (used by ConfigManager.setSettingsStore)\n // ---------------------------------------------------------------------------\n\n /** Get a system setting by dot-notation key */\n getSystem(key: string): unknown {\n this.ensureTable('system-settings')\n const row = this.getDb()\n .prepare<[string], { data: string }>('SELECT data FROM \"system-settings\" WHERE id = ?')\n .get(key)\n if (!row) return undefined\n return JSON.parse(row.data)\n }\n\n /** Set a system setting */\n setSystem(key: string, value: unknown): void {\n this.ensureTable('system-settings')\n this.getDb()\n .prepare('INSERT INTO \"system-settings\" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data')\n .run(key, JSON.stringify(value))\n }\n\n /** Get all system settings as flat key-value */\n getAllSystem(): Record<string, unknown> {\n this.ensureTable('system-settings')\n const rows = this.getDb()\n .prepare<[], { id: string; data: string }>('SELECT id, data FROM \"system-settings\"')\n .all()\n return Object.fromEntries(rows.map(r => [r.id, JSON.parse(r.data)]))\n }\n\n /** Get all settings for an addon */\n getAllAddon(addonId: string): Record<string, unknown> {\n this.ensureTable('addon-settings')\n const rows = this.getDb()\n .prepare<[string], { id: string; data: string }>('SELECT id, data FROM \"addon-settings\" WHERE json_extract(data, \\'$.addonId\\') = ?')\n .all(addonId)\n if (rows.length === 0) return {}\n // addon-settings stores all keys for an addon as separate rows with id = \"addonId.key\"\n const result: Record<string, unknown> = {}\n for (const row of rows) {\n const parsed = JSON.parse(row.data)\n const key = row.id.startsWith(`${addonId}.`) ? row.id.slice(addonId.length + 1) : row.id\n result[key] = parsed.value ?? parsed\n }\n return result\n }\n\n /** Bulk-set all settings for an addon */\n setAllAddon(addonId: string, config: Record<string, unknown>): void {\n this.ensureTable('addon-settings')\n const db = this.getDb()\n const deleteStmt = db.prepare('DELETE FROM \"addon-settings\" WHERE id LIKE ? || \\'%\\'')\n const insertStmt = db.prepare('INSERT INTO \"addon-settings\" (id, data) VALUES (?, ?)')\n db.transaction(() => {\n deleteStmt.run(`${addonId}.`)\n for (const [key, value] of Object.entries(config)) {\n insertStmt.run(`${addonId}.${key}`, JSON.stringify({ addonId, key, value }))\n }\n })()\n }\n\n /** Get all settings for a provider */\n getAllProvider(providerId: string): Record<string, unknown> {\n return this.getAllScoped('provider-settings', providerId)\n }\n\n /** Set a provider setting */\n setProvider(providerId: string, key: string, value: unknown): void {\n this.setScopedKey('provider-settings', providerId, key, value)\n }\n\n /** Get all settings for a device */\n getAllDevice(deviceId: string): Record<string, unknown> {\n return this.getAllScoped('device-settings', deviceId)\n }\n\n /** Set a device setting */\n setDevice(deviceId: string, key: string, value: unknown): void {\n this.setScopedKey('device-settings', deviceId, key, value)\n }\n\n /** Seed system-settings with runtime defaults (first boot) */\n private async seedDefaults(): Promise<void> {\n this.ensureTable('system-settings')\n const insert = this.getDb().prepare(\n 'INSERT OR IGNORE INTO \"system-settings\" (id, data) VALUES (?, ?)',\n )\n this.getDb().transaction(() => {\n for (const [key, value] of Object.entries(this.runtimeDefaults)) {\n insert.run(key, JSON.stringify(value))\n }\n })()\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private getDb(): Database.Database {\n if (!this.db) throw new Error('SqliteSettingsBackend not initialized — call initialize() first')\n return this.db\n }\n\n private ensureTable(collection: string): void {\n if (this.ensuredTables.has(collection)) return\n this.getDb().exec(\n `CREATE TABLE IF NOT EXISTS \"${collection}\" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`,\n )\n this.ensuredTables.add(collection)\n }\n\n private getAllScoped(collection: string, scopeId: string): Record<string, unknown> {\n this.ensureTable(collection)\n const rows = this.getDb()\n .prepare<[string], { id: string; data: string }>(`SELECT id, data FROM \"${collection}\" WHERE id LIKE ? || '.%'`)\n .all(scopeId)\n const result: Record<string, unknown> = {}\n for (const row of rows) {\n const key = row.id.slice(scopeId.length + 1)\n const parsed = JSON.parse(row.data)\n result[key] = parsed.value ?? parsed\n }\n return result\n }\n\n private setScopedKey(collection: string, scopeId: string, key: string, value: unknown): void {\n this.ensureTable(collection)\n this.getDb()\n .prepare(`INSERT INTO \"${collection}\" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`)\n .run(`${scopeId}.${key}`, JSON.stringify({ scopeId, key, value }))\n }\n}\n\nexport default SqliteSettingsBackend\n"],"mappings":"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","names":["Database"]}
1
+ {"version":3,"sources":["../../../src/builtins/sqlite-storage/sqlite-settings.addon.ts","../../../src/builtins/sqlite-storage/sqlite-settings-backend.ts"],"sourcesContent":["import type {\n ICamstackAddon,\n AddonManifest,\n AddonContext,\n IConfigurable,\n ConfigUISchema,\n CapabilityProviderMap,\n} from '@camstack/types'\nimport { SqliteSettingsBackend } from './sqlite-settings-backend'\n\n/**\n * SQLite Settings addon — provides persistent settings storage.\n * Capability: 'settings-store' (singleton)\n *\n * Depends on 'storage' capability for resolving the DB file path.\n */\nexport class SqliteSettingsAddon implements ICamstackAddon, IConfigurable {\n readonly manifest: AddonManifest = {\n id: 'sqlite-settings',\n name: 'SQLite Settings',\n version: '1.0.0',\n capabilities: [{ name: 'settings-store', mode: 'singleton' }],\n }\n\n private backend: SqliteSettingsBackend | null = null\n\n async initialize(context: AddonContext): Promise<void> {\n // Resolve DB path from storage provider (addons-data location)\n const dbPath = context.storageProvider\n ? context.storageProvider.resolve('addons-data', `${context.id.replace('addon:', '')}/camstack.db`)\n : context.dataDir\n ? `${context.dataDir}/camstack.db`\n : 'camstack-data/addons-data/sqlite-settings/camstack.db'\n\n const runtimeDefaults = (context.addonConfig._runtimeDefaults as Record<string, unknown>) ?? {}\n this.backend = new SqliteSettingsBackend(dbPath, runtimeDefaults)\n await this.backend.initialize()\n context.logger.info(`SQLite settings initialized at ${dbPath}`)\n }\n\n async shutdown(): Promise<void> {\n await this.backend?.shutdown()\n }\n\n getBackend(): SqliteSettingsBackend | null {\n return this.backend\n }\n\n getCapabilityProvider<K extends keyof CapabilityProviderMap>(\n name: K,\n ): CapabilityProviderMap[K] | null {\n if (name === ('settings-store' as string) && this.backend) {\n return this.backend as unknown as CapabilityProviderMap[K]\n }\n return null\n }\n\n getConfigSchema(): ConfigUISchema {\n return { sections: [] }\n }\n\n getConfig(): Record<string, unknown> {\n return {}\n }\n\n async onConfigChange(_config: Record<string, unknown>): Promise<void> {\n // DB path changes require restart\n }\n}\n\nexport default SqliteSettingsAddon\n","import Database from 'better-sqlite3'\nimport { randomUUID } from 'node:crypto'\nimport type {\n ISettingsBackend,\n SettingsCollection,\n SettingsRecord,\n QueryFilter,\n TableSchema,\n TableQueryOptions,\n} from '@camstack/types'\n\n/**\n * SQLite implementation of ISettingsBackend.\n *\n * One table per collection, auto-created on first access.\n * All data stored as JSON in a TEXT column.\n * WAL mode for concurrent reads.\n */\nexport class SqliteSettingsBackend implements ISettingsBackend {\n private db: Database.Database | null = null\n private readonly ensuredTables = new Set<string>()\n private readonly structuredTables = new Set<string>()\n private readonly runtimeDefaults: Record<string, unknown>\n\n constructor(private readonly dbPath: string, runtimeDefaults?: Record<string, unknown>) {\n this.runtimeDefaults = runtimeDefaults ?? {}\n }\n\n async initialize(): Promise<void> {\n // Ensure parent directory exists\n const dir = this.dbPath.substring(0, this.dbPath.lastIndexOf('/'))\n if (dir) {\n const fs = await import('node:fs')\n fs.mkdirSync(dir, { recursive: true })\n }\n this.db = new Database(this.dbPath)\n this.db.pragma('journal_mode = WAL')\n this.db.pragma('foreign_keys = ON')\n\n // Seed system-settings on first boot\n const isEmpty = await this.isEmpty('system-settings')\n if (isEmpty) {\n await this.seedDefaults()\n }\n }\n\n async shutdown(): Promise<void> {\n this.db?.close()\n this.db = null\n }\n\n // ---------------------------------------------------------------------------\n // ISettingsBackend implementation\n // ---------------------------------------------------------------------------\n\n async get(collection: SettingsCollection, key: string): Promise<unknown> {\n this.ensureCollectionTable(collection)\n const row = this.getDb()\n .prepare<[string], { data: string }>(`SELECT data FROM \"${collection}\" WHERE id = ?`)\n .get(key)\n if (!row) return undefined\n return JSON.parse(row.data)\n }\n\n async set(collection: SettingsCollection, key: string, value: unknown): Promise<void> {\n this.ensureCollectionTable(collection)\n this.getDb()\n .prepare(`INSERT INTO \"${collection}\" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`)\n .run(key, JSON.stringify(value))\n }\n\n async query(collection: SettingsCollection, filter?: QueryFilter): Promise<readonly SettingsRecord[]> {\n this.ensureCollectionTable(collection)\n let sql = `SELECT id, data FROM \"${collection}\"`\n const params: unknown[] = []\n\n const whereClauses: string[] = []\n\n if (filter?.where) {\n for (const [field, value] of Object.entries(filter.where)) {\n if (field === 'id') {\n whereClauses.push('id = ?')\n params.push(value)\n } else {\n whereClauses.push(`json_extract(data, '$.${field}') = ?`)\n params.push(typeof value === 'object' ? JSON.stringify(value) : value)\n }\n }\n }\n\n if (filter?.whereIn) {\n for (const [field, values] of Object.entries(filter.whereIn)) {\n const placeholders = values.map(() => '?').join(', ')\n if (field === 'id') {\n whereClauses.push(`id IN (${placeholders})`)\n } else {\n whereClauses.push(`json_extract(data, '$.${field}') IN (${placeholders})`)\n }\n params.push(...values)\n }\n }\n\n if (filter?.whereBetween) {\n for (const [field, [low, high]] of Object.entries(filter.whereBetween)) {\n if (field === 'id') {\n whereClauses.push('id BETWEEN ? AND ?')\n } else {\n whereClauses.push(`json_extract(data, '$.${field}') BETWEEN ? AND ?`)\n }\n params.push(low, high)\n }\n }\n\n if (whereClauses.length > 0) {\n sql += ` WHERE ${whereClauses.join(' AND ')}`\n }\n\n if (filter?.orderBy) {\n const dir = filter.orderBy.direction === 'desc' ? 'DESC' : 'ASC'\n if (filter.orderBy.field === 'id') {\n sql += ` ORDER BY id ${dir}`\n } else {\n sql += ` ORDER BY json_extract(data, '$.${filter.orderBy.field}') ${dir}`\n }\n }\n\n if (filter?.limit) {\n sql += ` LIMIT ?`\n params.push(filter.limit)\n }\n\n if (filter?.offset) {\n sql += ` OFFSET ?`\n params.push(filter.offset)\n }\n\n const rows = this.getDb()\n .prepare<unknown[], { id: string; data: string }>(sql)\n .all(...params)\n\n return rows.map(row => ({\n id: row.id,\n data: JSON.parse(row.data),\n }))\n }\n\n async insert(collection: SettingsCollection, record: SettingsRecord): Promise<void> {\n this.ensureCollectionTable(collection)\n const id = record.id || randomUUID()\n this.getDb()\n .prepare(`INSERT INTO \"${collection}\" (id, data) VALUES (?, ?)`)\n .run(id, JSON.stringify(record.data))\n }\n\n async update(collection: SettingsCollection, id: string, data: Record<string, unknown>): Promise<void> {\n this.ensureCollectionTable(collection)\n this.getDb()\n .prepare(`UPDATE \"${collection}\" SET data = ? WHERE id = ?`)\n .run(JSON.stringify(data), id)\n }\n\n async delete(collection: SettingsCollection, key: string): Promise<void> {\n this.ensureCollectionTable(collection)\n this.getDb()\n .prepare(`DELETE FROM \"${collection}\" WHERE id = ?`)\n .run(key)\n }\n\n async count(collection: SettingsCollection, filter?: QueryFilter): Promise<number> {\n this.ensureCollectionTable(collection)\n if (!filter) {\n const row = this.getDb()\n .prepare<[], { cnt: number }>(`SELECT COUNT(*) AS cnt FROM \"${collection}\"`)\n .get()\n return row?.cnt ?? 0\n }\n const rows = await this.query(collection, { ...filter, limit: undefined, offset: undefined })\n return rows.length\n }\n\n async isEmpty(collection: SettingsCollection): Promise<boolean> {\n this.ensureCollectionTable(collection)\n const row = this.getDb()\n .prepare<[], { cnt: number }>(`SELECT COUNT(*) AS cnt FROM \"${collection}\"`)\n .get()\n return (row?.cnt ?? 0) === 0\n }\n\n // ---------------------------------------------------------------------------\n // Legacy SettingsStore compatibility (used by ConfigManager.setSettingsStore)\n // ---------------------------------------------------------------------------\n\n /** Get a system setting by dot-notation key */\n getSystem(key: string): unknown {\n this.ensureCollectionTable('system-settings')\n const row = this.getDb()\n .prepare<[string], { data: string }>('SELECT data FROM \"system-settings\" WHERE id = ?')\n .get(key)\n if (!row) return undefined\n return JSON.parse(row.data)\n }\n\n /** Set a system setting */\n setSystem(key: string, value: unknown): void {\n this.ensureCollectionTable('system-settings')\n this.getDb()\n .prepare('INSERT INTO \"system-settings\" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data')\n .run(key, JSON.stringify(value))\n }\n\n /** Get all system settings as flat key-value */\n getAllSystem(): Record<string, unknown> {\n this.ensureCollectionTable('system-settings')\n const rows = this.getDb()\n .prepare<[], { id: string; data: string }>('SELECT id, data FROM \"system-settings\"')\n .all()\n return Object.fromEntries(rows.map(r => [r.id, JSON.parse(r.data)]))\n }\n\n /** Get all settings for an addon */\n getAllAddon(addonId: string): Record<string, unknown> {\n this.ensureCollectionTable('addon-settings')\n const rows = this.getDb()\n .prepare<[string], { id: string; data: string }>('SELECT id, data FROM \"addon-settings\" WHERE json_extract(data, \\'$.addonId\\') = ?')\n .all(addonId)\n if (rows.length === 0) return {}\n // addon-settings stores all keys for an addon as separate rows with id = \"addonId.key\"\n const result: Record<string, unknown> = {}\n for (const row of rows) {\n const parsed = JSON.parse(row.data)\n const key = row.id.startsWith(`${addonId}.`) ? row.id.slice(addonId.length + 1) : row.id\n result[key] = parsed.value ?? parsed\n }\n return result\n }\n\n /** Bulk-set all settings for an addon */\n setAllAddon(addonId: string, config: Record<string, unknown>): void {\n this.ensureCollectionTable('addon-settings')\n const db = this.getDb()\n const deleteStmt = db.prepare('DELETE FROM \"addon-settings\" WHERE id LIKE ? || \\'%\\'')\n const insertStmt = db.prepare('INSERT INTO \"addon-settings\" (id, data) VALUES (?, ?)')\n db.transaction(() => {\n deleteStmt.run(`${addonId}.`)\n for (const [key, value] of Object.entries(config)) {\n insertStmt.run(`${addonId}.${key}`, JSON.stringify({ addonId, key, value }))\n }\n })()\n }\n\n /** Get all settings for a provider */\n getAllProvider(providerId: string): Record<string, unknown> {\n return this.getAllScoped('provider-settings', providerId)\n }\n\n /** Set a provider setting */\n setProvider(providerId: string, key: string, value: unknown): void {\n this.setScopedKey('provider-settings', providerId, key, value)\n }\n\n /** Get all settings for a device */\n getAllDevice(deviceId: string): Record<string, unknown> {\n return this.getAllScoped('device-settings', deviceId)\n }\n\n /** Set a device setting */\n setDevice(deviceId: string, key: string, value: unknown): void {\n this.setScopedKey('device-settings', deviceId, key, value)\n }\n\n /** Seed system-settings with runtime defaults (first boot) */\n private async seedDefaults(): Promise<void> {\n this.ensureCollectionTable('system-settings')\n const insert = this.getDb().prepare(\n 'INSERT OR IGNORE INTO \"system-settings\" (id, data) VALUES (?, ?)',\n )\n this.getDb().transaction(() => {\n for (const [key, value] of Object.entries(this.runtimeDefaults)) {\n insert.run(key, JSON.stringify(value))\n }\n })()\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private getDb(): Database.Database {\n if (!this.db) throw new Error('SqliteSettingsBackend not initialized — call initialize() first')\n return this.db\n }\n\n private ensureCollectionTable(collection: string): void {\n if (this.ensuredTables.has(collection)) return\n this.getDb().exec(\n `CREATE TABLE IF NOT EXISTS \"${collection}\" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`,\n )\n this.ensuredTables.add(collection)\n }\n\n private getAllScoped(collection: string, scopeId: string): Record<string, unknown> {\n this.ensureCollectionTable(collection)\n const rows = this.getDb()\n .prepare<[string], { id: string; data: string }>(`SELECT id, data FROM \"${collection}\" WHERE id LIKE ? || '.%'`)\n .all(scopeId)\n const result: Record<string, unknown> = {}\n for (const row of rows) {\n const key = row.id.slice(scopeId.length + 1)\n const parsed = JSON.parse(row.data)\n result[key] = parsed.value ?? parsed\n }\n return result\n }\n\n private setScopedKey(collection: string, scopeId: string, key: string, value: unknown): void {\n this.ensureCollectionTable(collection)\n this.getDb()\n .prepare(`INSERT INTO \"${collection}\" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`)\n .run(`${scopeId}.${key}`, JSON.stringify({ scopeId, key, value }))\n }\n\n // ── Structured table operations ────────────────────────────────────\n\n async ensureTable(table: string, schema?: TableSchema): Promise<void> {\n if (!schema) {\n // Legacy: ensure JSON-blob collection table\n if (!this.ensuredTables.has(table)) {\n this.getDb().exec(`CREATE TABLE IF NOT EXISTS \"${table}\" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`)\n this.ensuredTables.add(table)\n }\n return\n }\n if (this.structuredTables.has(table)) return\n\n const colDefs = schema.columns.map(col => {\n const parts = [`\"${col.name}\" ${col.type}`]\n if (col.primaryKey) parts.push('PRIMARY KEY')\n if (col.notNull) parts.push('NOT NULL')\n if (col.unique) parts.push('UNIQUE')\n if (col.defaultValue !== undefined) {\n parts.push(`DEFAULT ${typeof col.defaultValue === 'string' ? `'${col.defaultValue}'` : col.defaultValue === null ? 'NULL' : String(col.defaultValue)}`)\n }\n return parts.join(' ')\n })\n\n this.getDb().exec(`CREATE TABLE IF NOT EXISTS \"${table}\" (${colDefs.join(', ')})`)\n\n if (schema.indexes) {\n for (const idx of schema.indexes) {\n const unique = idx.unique ? 'UNIQUE ' : ''\n const cols = idx.columns.map(c => `\"${c}\"`).join(', ')\n this.getDb().exec(`CREATE ${unique}INDEX IF NOT EXISTS \"${idx.name}\" ON \"${table}\" (${cols})`)\n }\n }\n\n this.structuredTables.add(table)\n }\n\n async tableInsert(table: string, row: Record<string, unknown>): Promise<void> {\n const keys = Object.keys(row)\n const cols = keys.map(k => `\"${k}\"`).join(', ')\n const placeholders = keys.map(() => '?').join(', ')\n const values = keys.map(k => {\n const v = row[k]\n return typeof v === 'object' && v !== null ? JSON.stringify(v) : v\n })\n this.getDb().prepare(`INSERT INTO \"${table}\" (${cols}) VALUES (${placeholders})`).run(...values)\n }\n\n async tableUpdate(table: string, filter: Record<string, unknown>, updates: Record<string, unknown>): Promise<number> {\n const setClauses: string[] = []\n const setValues: unknown[] = []\n for (const [k, v] of Object.entries(updates)) {\n setClauses.push(`\"${k}\" = ?`)\n setValues.push(typeof v === 'object' && v !== null ? JSON.stringify(v) : v)\n }\n\n const { whereSql, whereValues } = this.buildWhere(filter)\n const result = this.getDb()\n .prepare(`UPDATE \"${table}\" SET ${setClauses.join(', ')}${whereSql}`)\n .run(...setValues, ...whereValues)\n return result.changes\n }\n\n async tableDelete(table: string, filter: Record<string, unknown>): Promise<number> {\n const { whereSql, whereValues } = this.buildWhere(filter)\n const result = this.getDb().prepare(`DELETE FROM \"${table}\"${whereSql}`).run(...whereValues)\n return result.changes\n }\n\n async tableQuery(table: string, options?: TableQueryOptions): Promise<readonly Record<string, unknown>[]> {\n let sql = `SELECT * FROM \"${table}\"`\n const values: unknown[] = []\n\n if (options?.where) {\n const { whereSql, whereValues } = this.buildWhere(options.where)\n sql += whereSql\n values.push(...whereValues)\n }\n\n if (options?.orderBy) {\n sql += ` ORDER BY \"${options.orderBy.field}\" ${options.orderBy.direction === 'desc' ? 'DESC' : 'ASC'}`\n }\n\n if (options?.limit !== undefined) {\n sql += ` LIMIT ?`\n values.push(options.limit)\n }\n\n if (options?.offset !== undefined) {\n sql += ` OFFSET ?`\n values.push(options.offset)\n }\n\n return this.getDb().prepare(sql).all(...values) as Record<string, unknown>[]\n }\n\n async tableGet(table: string, filter: Record<string, unknown>): Promise<Record<string, unknown> | null> {\n const { whereSql, whereValues } = this.buildWhere(filter)\n const row = this.getDb().prepare(`SELECT * FROM \"${table}\"${whereSql} LIMIT 1`).get(...whereValues)\n return (row as Record<string, unknown>) ?? null\n }\n\n async tableCount(table: string, filter?: Record<string, unknown>): Promise<number> {\n let sql = `SELECT COUNT(*) as count FROM \"${table}\"`\n const values: unknown[] = []\n\n if (filter) {\n const { whereSql, whereValues } = this.buildWhere(filter)\n sql += whereSql\n values.push(...whereValues)\n }\n\n const row = this.getDb().prepare(sql).get(...values) as { count: number }\n return row.count\n }\n\n private buildWhere(filter: Record<string, unknown>): { whereSql: string; whereValues: unknown[] } {\n const conditions: string[] = []\n const values: unknown[] = []\n for (const [k, v] of Object.entries(filter)) {\n conditions.push(`\"${k}\" = ?`)\n values.push(typeof v === 'object' && v !== null ? JSON.stringify(v) : v)\n }\n const whereSql = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : ''\n return { whereSql, whereValues: values }\n }\n}\n\nexport default SqliteSettingsBackend\n"],"mappings":"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","names":["Database"]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  SqliteSettingsAddon,
3
3
  sqlite_settings_addon_default
4
- } from "../../chunk-YXNXYYHL.mjs";
4
+ } from "../../chunk-SMNR44VG.mjs";
5
5
  export {
6
6
  SqliteSettingsAddon,
7
7
  sqlite_settings_addon_default as default
@@ -8,6 +8,7 @@ var SqliteSettingsBackend = class {
8
8
  }
9
9
  db = null;
10
10
  ensuredTables = /* @__PURE__ */ new Set();
11
+ structuredTables = /* @__PURE__ */ new Set();
11
12
  runtimeDefaults;
12
13
  async initialize() {
13
14
  const dir = this.dbPath.substring(0, this.dbPath.lastIndexOf("/"));
@@ -31,17 +32,17 @@ var SqliteSettingsBackend = class {
31
32
  // ISettingsBackend implementation
32
33
  // ---------------------------------------------------------------------------
33
34
  async get(collection, key) {
34
- this.ensureTable(collection);
35
+ this.ensureCollectionTable(collection);
35
36
  const row = this.getDb().prepare(`SELECT data FROM "${collection}" WHERE id = ?`).get(key);
36
37
  if (!row) return void 0;
37
38
  return JSON.parse(row.data);
38
39
  }
39
40
  async set(collection, key, value) {
40
- this.ensureTable(collection);
41
+ this.ensureCollectionTable(collection);
41
42
  this.getDb().prepare(`INSERT INTO "${collection}" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`).run(key, JSON.stringify(value));
42
43
  }
43
44
  async query(collection, filter) {
44
- this.ensureTable(collection);
45
+ this.ensureCollectionTable(collection);
45
46
  let sql = `SELECT id, data FROM "${collection}"`;
46
47
  const params = [];
47
48
  const whereClauses = [];
@@ -103,20 +104,20 @@ var SqliteSettingsBackend = class {
103
104
  }));
104
105
  }
105
106
  async insert(collection, record) {
106
- this.ensureTable(collection);
107
+ this.ensureCollectionTable(collection);
107
108
  const id = record.id || randomUUID();
108
109
  this.getDb().prepare(`INSERT INTO "${collection}" (id, data) VALUES (?, ?)`).run(id, JSON.stringify(record.data));
109
110
  }
110
111
  async update(collection, id, data) {
111
- this.ensureTable(collection);
112
+ this.ensureCollectionTable(collection);
112
113
  this.getDb().prepare(`UPDATE "${collection}" SET data = ? WHERE id = ?`).run(JSON.stringify(data), id);
113
114
  }
114
115
  async delete(collection, key) {
115
- this.ensureTable(collection);
116
+ this.ensureCollectionTable(collection);
116
117
  this.getDb().prepare(`DELETE FROM "${collection}" WHERE id = ?`).run(key);
117
118
  }
118
119
  async count(collection, filter) {
119
- this.ensureTable(collection);
120
+ this.ensureCollectionTable(collection);
120
121
  if (!filter) {
121
122
  const row = this.getDb().prepare(`SELECT COUNT(*) AS cnt FROM "${collection}"`).get();
122
123
  return row?.cnt ?? 0;
@@ -125,7 +126,7 @@ var SqliteSettingsBackend = class {
125
126
  return rows.length;
126
127
  }
127
128
  async isEmpty(collection) {
128
- this.ensureTable(collection);
129
+ this.ensureCollectionTable(collection);
129
130
  const row = this.getDb().prepare(`SELECT COUNT(*) AS cnt FROM "${collection}"`).get();
130
131
  return (row?.cnt ?? 0) === 0;
131
132
  }
@@ -134,25 +135,25 @@ var SqliteSettingsBackend = class {
134
135
  // ---------------------------------------------------------------------------
135
136
  /** Get a system setting by dot-notation key */
136
137
  getSystem(key) {
137
- this.ensureTable("system-settings");
138
+ this.ensureCollectionTable("system-settings");
138
139
  const row = this.getDb().prepare('SELECT data FROM "system-settings" WHERE id = ?').get(key);
139
140
  if (!row) return void 0;
140
141
  return JSON.parse(row.data);
141
142
  }
142
143
  /** Set a system setting */
143
144
  setSystem(key, value) {
144
- this.ensureTable("system-settings");
145
+ this.ensureCollectionTable("system-settings");
145
146
  this.getDb().prepare('INSERT INTO "system-settings" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data').run(key, JSON.stringify(value));
146
147
  }
147
148
  /** Get all system settings as flat key-value */
148
149
  getAllSystem() {
149
- this.ensureTable("system-settings");
150
+ this.ensureCollectionTable("system-settings");
150
151
  const rows = this.getDb().prepare('SELECT id, data FROM "system-settings"').all();
151
152
  return Object.fromEntries(rows.map((r) => [r.id, JSON.parse(r.data)]));
152
153
  }
153
154
  /** Get all settings for an addon */
154
155
  getAllAddon(addonId) {
155
- this.ensureTable("addon-settings");
156
+ this.ensureCollectionTable("addon-settings");
156
157
  const rows = this.getDb().prepare(`SELECT id, data FROM "addon-settings" WHERE json_extract(data, '$.addonId') = ?`).all(addonId);
157
158
  if (rows.length === 0) return {};
158
159
  const result = {};
@@ -165,7 +166,7 @@ var SqliteSettingsBackend = class {
165
166
  }
166
167
  /** Bulk-set all settings for an addon */
167
168
  setAllAddon(addonId, config) {
168
- this.ensureTable("addon-settings");
169
+ this.ensureCollectionTable("addon-settings");
169
170
  const db = this.getDb();
170
171
  const deleteStmt = db.prepare(`DELETE FROM "addon-settings" WHERE id LIKE ? || '%'`);
171
172
  const insertStmt = db.prepare('INSERT INTO "addon-settings" (id, data) VALUES (?, ?)');
@@ -194,7 +195,7 @@ var SqliteSettingsBackend = class {
194
195
  }
195
196
  /** Seed system-settings with runtime defaults (first boot) */
196
197
  async seedDefaults() {
197
- this.ensureTable("system-settings");
198
+ this.ensureCollectionTable("system-settings");
198
199
  const insert = this.getDb().prepare(
199
200
  'INSERT OR IGNORE INTO "system-settings" (id, data) VALUES (?, ?)'
200
201
  );
@@ -211,7 +212,7 @@ var SqliteSettingsBackend = class {
211
212
  if (!this.db) throw new Error("SqliteSettingsBackend not initialized \u2014 call initialize() first");
212
213
  return this.db;
213
214
  }
214
- ensureTable(collection) {
215
+ ensureCollectionTable(collection) {
215
216
  if (this.ensuredTables.has(collection)) return;
216
217
  this.getDb().exec(
217
218
  `CREATE TABLE IF NOT EXISTS "${collection}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`
@@ -219,7 +220,7 @@ var SqliteSettingsBackend = class {
219
220
  this.ensuredTables.add(collection);
220
221
  }
221
222
  getAllScoped(collection, scopeId) {
222
- this.ensureTable(collection);
223
+ this.ensureCollectionTable(collection);
223
224
  const rows = this.getDb().prepare(`SELECT id, data FROM "${collection}" WHERE id LIKE ? || '.%'`).all(scopeId);
224
225
  const result = {};
225
226
  for (const row of rows) {
@@ -230,9 +231,112 @@ var SqliteSettingsBackend = class {
230
231
  return result;
231
232
  }
232
233
  setScopedKey(collection, scopeId, key, value) {
233
- this.ensureTable(collection);
234
+ this.ensureCollectionTable(collection);
234
235
  this.getDb().prepare(`INSERT INTO "${collection}" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`).run(`${scopeId}.${key}`, JSON.stringify({ scopeId, key, value }));
235
236
  }
237
+ // ── Structured table operations ────────────────────────────────────
238
+ async ensureTable(table, schema) {
239
+ if (!schema) {
240
+ if (!this.ensuredTables.has(table)) {
241
+ this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`);
242
+ this.ensuredTables.add(table);
243
+ }
244
+ return;
245
+ }
246
+ if (this.structuredTables.has(table)) return;
247
+ const colDefs = schema.columns.map((col) => {
248
+ const parts = [`"${col.name}" ${col.type}`];
249
+ if (col.primaryKey) parts.push("PRIMARY KEY");
250
+ if (col.notNull) parts.push("NOT NULL");
251
+ if (col.unique) parts.push("UNIQUE");
252
+ if (col.defaultValue !== void 0) {
253
+ parts.push(`DEFAULT ${typeof col.defaultValue === "string" ? `'${col.defaultValue}'` : col.defaultValue === null ? "NULL" : String(col.defaultValue)}`);
254
+ }
255
+ return parts.join(" ");
256
+ });
257
+ this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`);
258
+ if (schema.indexes) {
259
+ for (const idx of schema.indexes) {
260
+ const unique = idx.unique ? "UNIQUE " : "";
261
+ const cols = idx.columns.map((c) => `"${c}"`).join(", ");
262
+ this.getDb().exec(`CREATE ${unique}INDEX IF NOT EXISTS "${idx.name}" ON "${table}" (${cols})`);
263
+ }
264
+ }
265
+ this.structuredTables.add(table);
266
+ }
267
+ async tableInsert(table, row) {
268
+ const keys = Object.keys(row);
269
+ const cols = keys.map((k) => `"${k}"`).join(", ");
270
+ const placeholders = keys.map(() => "?").join(", ");
271
+ const values = keys.map((k) => {
272
+ const v = row[k];
273
+ return typeof v === "object" && v !== null ? JSON.stringify(v) : v;
274
+ });
275
+ this.getDb().prepare(`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`).run(...values);
276
+ }
277
+ async tableUpdate(table, filter, updates) {
278
+ const setClauses = [];
279
+ const setValues = [];
280
+ for (const [k, v] of Object.entries(updates)) {
281
+ setClauses.push(`"${k}" = ?`);
282
+ setValues.push(typeof v === "object" && v !== null ? JSON.stringify(v) : v);
283
+ }
284
+ const { whereSql, whereValues } = this.buildWhere(filter);
285
+ const result = this.getDb().prepare(`UPDATE "${table}" SET ${setClauses.join(", ")}${whereSql}`).run(...setValues, ...whereValues);
286
+ return result.changes;
287
+ }
288
+ async tableDelete(table, filter) {
289
+ const { whereSql, whereValues } = this.buildWhere(filter);
290
+ const result = this.getDb().prepare(`DELETE FROM "${table}"${whereSql}`).run(...whereValues);
291
+ return result.changes;
292
+ }
293
+ async tableQuery(table, options) {
294
+ let sql = `SELECT * FROM "${table}"`;
295
+ const values = [];
296
+ if (options?.where) {
297
+ const { whereSql, whereValues } = this.buildWhere(options.where);
298
+ sql += whereSql;
299
+ values.push(...whereValues);
300
+ }
301
+ if (options?.orderBy) {
302
+ sql += ` ORDER BY "${options.orderBy.field}" ${options.orderBy.direction === "desc" ? "DESC" : "ASC"}`;
303
+ }
304
+ if (options?.limit !== void 0) {
305
+ sql += ` LIMIT ?`;
306
+ values.push(options.limit);
307
+ }
308
+ if (options?.offset !== void 0) {
309
+ sql += ` OFFSET ?`;
310
+ values.push(options.offset);
311
+ }
312
+ return this.getDb().prepare(sql).all(...values);
313
+ }
314
+ async tableGet(table, filter) {
315
+ const { whereSql, whereValues } = this.buildWhere(filter);
316
+ const row = this.getDb().prepare(`SELECT * FROM "${table}"${whereSql} LIMIT 1`).get(...whereValues);
317
+ return row ?? null;
318
+ }
319
+ async tableCount(table, filter) {
320
+ let sql = `SELECT COUNT(*) as count FROM "${table}"`;
321
+ const values = [];
322
+ if (filter) {
323
+ const { whereSql, whereValues } = this.buildWhere(filter);
324
+ sql += whereSql;
325
+ values.push(...whereValues);
326
+ }
327
+ const row = this.getDb().prepare(sql).get(...values);
328
+ return row.count;
329
+ }
330
+ buildWhere(filter) {
331
+ const conditions = [];
332
+ const values = [];
333
+ for (const [k, v] of Object.entries(filter)) {
334
+ conditions.push(`"${k}" = ?`);
335
+ values.push(typeof v === "object" && v !== null ? JSON.stringify(v) : v);
336
+ }
337
+ const whereSql = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
338
+ return { whereSql, whereValues: values };
339
+ }
236
340
  };
237
341
 
238
342
  // src/builtins/sqlite-storage/sqlite-settings.addon.ts
@@ -279,4 +383,4 @@ export {
279
383
  SqliteSettingsAddon,
280
384
  sqlite_settings_addon_default
281
385
  };
282
- //# sourceMappingURL=chunk-YXNXYYHL.mjs.map
386
+ //# sourceMappingURL=chunk-SMNR44VG.mjs.map