@camstack/core 0.1.14 → 0.1.15

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.
Files changed (161) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +220 -0
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js.map +1 -0
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +9 -0
  4. package/dist/builtins/addon-pages-aggregator/index.js +222 -0
  5. package/dist/builtins/addon-pages-aggregator/index.js.map +1 -0
  6. package/dist/builtins/addon-pages-aggregator/index.mjs +9 -0
  7. package/dist/builtins/addon-pages-aggregator/index.mjs.map +1 -0
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +200 -0
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js.map +1 -0
  10. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +9 -0
  11. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs.map +1 -0
  12. package/dist/builtins/addon-widgets-aggregator/index.js +202 -0
  13. package/dist/builtins/addon-widgets-aggregator/index.js.map +1 -0
  14. package/dist/builtins/addon-widgets-aggregator/index.mjs +9 -0
  15. package/dist/builtins/addon-widgets-aggregator/index.mjs.map +1 -0
  16. package/dist/builtins/alerts/alerts.addon.js +443 -0
  17. package/dist/builtins/alerts/alerts.addon.js.map +1 -0
  18. package/dist/builtins/alerts/alerts.addon.mjs +9 -0
  19. package/dist/builtins/alerts/alerts.addon.mjs.map +1 -0
  20. package/dist/builtins/alerts/index.js +443 -0
  21. package/dist/builtins/alerts/index.js.map +1 -0
  22. package/dist/builtins/alerts/index.mjs +8 -0
  23. package/dist/builtins/alerts/index.mjs.map +1 -0
  24. package/dist/builtins/console-logging/index.js +242 -0
  25. package/dist/builtins/console-logging/index.js.map +1 -0
  26. package/dist/builtins/console-logging/index.mjs +11 -0
  27. package/dist/builtins/console-logging/index.mjs.map +1 -0
  28. package/dist/builtins/device-manager/device-manager.addon.js +2155 -0
  29. package/dist/builtins/device-manager/device-manager.addon.js.map +1 -0
  30. package/dist/builtins/device-manager/device-manager.addon.mjs +9 -0
  31. package/dist/builtins/device-manager/device-manager.addon.mjs.map +1 -0
  32. package/dist/builtins/device-manager/index.js +2157 -0
  33. package/dist/builtins/device-manager/index.js.map +1 -0
  34. package/dist/builtins/device-manager/index.mjs +10 -0
  35. package/dist/builtins/device-manager/index.mjs.map +1 -0
  36. package/dist/builtins/hub-forwarder/index.js +297 -0
  37. package/dist/builtins/hub-forwarder/index.js.map +1 -0
  38. package/dist/builtins/hub-forwarder/index.mjs +11 -0
  39. package/dist/builtins/hub-forwarder/index.mjs.map +1 -0
  40. package/dist/builtins/local-auth/index.js +623 -0
  41. package/dist/builtins/local-auth/index.js.map +1 -0
  42. package/dist/builtins/local-auth/index.mjs +8 -0
  43. package/dist/builtins/local-auth/index.mjs.map +1 -0
  44. package/dist/builtins/local-auth/local-auth.addon.js +623 -0
  45. package/dist/builtins/local-auth/local-auth.addon.js.map +1 -0
  46. package/dist/builtins/local-auth/local-auth.addon.mjs +9 -0
  47. package/dist/builtins/local-auth/local-auth.addon.mjs.map +1 -0
  48. package/dist/builtins/local-backup/index.js +53 -68
  49. package/dist/builtins/local-backup/index.js.map +1 -1
  50. package/dist/builtins/local-backup/index.mjs +1 -1
  51. package/dist/builtins/native-metrics/native-metrics.addon.js +898 -0
  52. package/dist/builtins/native-metrics/native-metrics.addon.js.map +1 -0
  53. package/dist/builtins/native-metrics/native-metrics.addon.mjs +7 -0
  54. package/dist/builtins/native-metrics/native-metrics.addon.mjs.map +1 -0
  55. package/dist/builtins/snapshot/index.js +504 -0
  56. package/dist/builtins/snapshot/index.js.map +1 -0
  57. package/dist/builtins/snapshot/index.mjs +477 -0
  58. package/dist/builtins/snapshot/index.mjs.map +1 -0
  59. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +16 -166
  60. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js.map +1 -1
  61. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  62. package/dist/builtins/sqlite-storage/index.js +554 -621
  63. package/dist/builtins/sqlite-storage/index.js.map +1 -1
  64. package/dist/builtins/sqlite-storage/index.mjs +9 -11
  65. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +368 -130
  66. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js.map +1 -1
  67. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  68. package/dist/builtins/system-config/index.js +189 -0
  69. package/dist/builtins/system-config/index.js.map +1 -0
  70. package/dist/builtins/system-config/index.mjs +10 -0
  71. package/dist/builtins/system-config/index.mjs.map +1 -0
  72. package/dist/builtins/system-config/system-config.addon.js +187 -0
  73. package/dist/builtins/system-config/system-config.addon.js.map +1 -0
  74. package/dist/builtins/system-config/system-config.addon.mjs +9 -0
  75. package/dist/builtins/system-config/system-config.addon.mjs.map +1 -0
  76. package/dist/builtins/winston-logging/index.js +185 -65
  77. package/dist/builtins/winston-logging/index.js.map +1 -1
  78. package/dist/builtins/winston-logging/index.mjs +2 -1
  79. package/dist/chunk-2CIYKDRN.mjs +1 -0
  80. package/dist/chunk-2CIYKDRN.mjs.map +1 -0
  81. package/dist/chunk-2F76X6NL.mjs +136 -0
  82. package/dist/chunk-2F76X6NL.mjs.map +1 -0
  83. package/dist/chunk-2QUFBZ7M.mjs +1 -0
  84. package/dist/chunk-2QUFBZ7M.mjs.map +1 -0
  85. package/dist/chunk-3BK2Y7GY.mjs +593 -0
  86. package/dist/chunk-3BK2Y7GY.mjs.map +1 -0
  87. package/dist/chunk-4OOHFJHT.mjs +421 -0
  88. package/dist/chunk-4OOHFJHT.mjs.map +1 -0
  89. package/dist/chunk-4XHB7IHT.mjs +809 -0
  90. package/dist/chunk-4XHB7IHT.mjs.map +1 -0
  91. package/dist/{chunk-2F3XZYRW.mjs → chunk-6M2HSSTQ.mjs} +16 -7
  92. package/dist/chunk-6M2HSSTQ.mjs.map +1 -0
  93. package/dist/{chunk-SO4LROOT.mjs → chunk-7FI7SQS7.mjs} +54 -69
  94. package/dist/chunk-7FI7SQS7.mjs.map +1 -0
  95. package/dist/chunk-ED57RCQE.mjs +171 -0
  96. package/dist/chunk-ED57RCQE.mjs.map +1 -0
  97. package/dist/chunk-FZN56HGQ.mjs +626 -0
  98. package/dist/chunk-FZN56HGQ.mjs.map +1 -0
  99. package/dist/chunk-GL4OOB25.mjs +51 -0
  100. package/dist/chunk-GL4OOB25.mjs.map +1 -0
  101. package/dist/chunk-KDG2NTDB.mjs +137 -0
  102. package/dist/chunk-KDG2NTDB.mjs.map +1 -0
  103. package/dist/chunk-NRBQWBDM.mjs +191 -0
  104. package/dist/chunk-NRBQWBDM.mjs.map +1 -0
  105. package/dist/chunk-O4V246GG.mjs +2137 -0
  106. package/dist/chunk-O4V246GG.mjs.map +1 -0
  107. package/dist/chunk-QT57H266.mjs +163 -0
  108. package/dist/chunk-QT57H266.mjs.map +1 -0
  109. package/dist/chunk-QX4RH25I.mjs +141 -0
  110. package/dist/chunk-QX4RH25I.mjs.map +1 -0
  111. package/dist/chunk-TB562PZX.mjs +86 -0
  112. package/dist/chunk-TB562PZX.mjs.map +1 -0
  113. package/dist/chunk-TDYPZXK5.mjs +1 -0
  114. package/dist/chunk-TDYPZXK5.mjs.map +1 -0
  115. package/dist/chunk-UJI4LN5P.mjs +36 -0
  116. package/dist/chunk-UJI4LN5P.mjs.map +1 -0
  117. package/dist/chunk-W6RTHQGP.mjs +1 -0
  118. package/dist/chunk-W6RTHQGP.mjs.map +1 -0
  119. package/dist/chunk-ZELBCPDC.mjs +369 -0
  120. package/dist/chunk-ZELBCPDC.mjs.map +1 -0
  121. package/dist/index.d.mts +1103 -544
  122. package/dist/index.d.ts +1103 -544
  123. package/dist/index.js +7032 -6033
  124. package/dist/index.js.map +1 -1
  125. package/dist/index.mjs +568 -2226
  126. package/dist/index.mjs.map +1 -1
  127. package/dist/resource-monitor-UZUGPIAU.mjs +9 -0
  128. package/dist/resource-monitor-UZUGPIAU.mjs.map +1 -0
  129. package/dist/storage-location-manager-HFNB3PCS.mjs +7 -0
  130. package/dist/storage-location-manager-HFNB3PCS.mjs.map +1 -0
  131. package/package.json +123 -2
  132. package/dist/builtins/local-backup/index.d.mts +0 -42
  133. package/dist/builtins/local-backup/index.d.ts +0 -42
  134. package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.mts +0 -2
  135. package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts +0 -2
  136. package/dist/builtins/sqlite-storage/index.d.mts +0 -4
  137. package/dist/builtins/sqlite-storage/index.d.ts +0 -4
  138. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.mts +0 -2
  139. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +0 -2
  140. package/dist/builtins/winston-logging/index.d.mts +0 -30
  141. package/dist/builtins/winston-logging/index.d.ts +0 -30
  142. package/dist/chunk-2F3XZYRW.mjs.map +0 -1
  143. package/dist/chunk-LQFPAEQF.mjs +0 -147
  144. package/dist/chunk-LQFPAEQF.mjs.map +0 -1
  145. package/dist/chunk-R3DIIBBX.mjs +0 -532
  146. package/dist/chunk-R3DIIBBX.mjs.map +0 -1
  147. package/dist/chunk-SMNR44VG.mjs +0 -386
  148. package/dist/chunk-SMNR44VG.mjs.map +0 -1
  149. package/dist/chunk-SO4LROOT.mjs.map +0 -1
  150. package/dist/chunk-SPA4JBKN.mjs +0 -175
  151. package/dist/chunk-SPA4JBKN.mjs.map +0 -1
  152. package/dist/dist-3BY63UQ5.mjs +0 -2151
  153. package/dist/dist-3BY63UQ5.mjs.map +0 -1
  154. package/dist/filesystem-storage.addon-C42r589X.d.mts +0 -57
  155. package/dist/filesystem-storage.addon-C42r589X.d.ts +0 -57
  156. package/dist/sql-schema-CKz78rId.d.mts +0 -97
  157. package/dist/sql-schema-CKz78rId.d.ts +0 -97
  158. package/dist/sqlite-settings.addon-KwG-uKMP.d.mts +0 -79
  159. package/dist/sqlite-settings.addon-KwG-uKMP.d.ts +0 -79
  160. package/dist/storage-location-manager-KKDQNAKA.mjs +0 -7
  161. /package/dist/{storage-location-manager-KKDQNAKA.mjs.map → builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs.map} +0 -0
@@ -34,19 +34,50 @@ __export(sqlite_settings_addon_exports, {
34
34
  default: () => sqlite_settings_addon_default
35
35
  });
36
36
  module.exports = __toCommonJS(sqlite_settings_addon_exports);
37
+ var import_types2 = require("@camstack/types");
37
38
 
38
39
  // src/builtins/sqlite-storage/sqlite-settings-backend.ts
39
40
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
40
41
  var import_node_crypto = require("crypto");
41
- var SqliteSettingsBackend = class {
42
+ var import_types = require("@camstack/types");
43
+ function parseRowData(raw) {
44
+ return (0, import_types.asJsonObject)((0, import_types.parseJsonUnknown)(raw)) ?? {};
45
+ }
46
+ var SqliteSettingsBackend = class _SqliteSettingsBackend {
47
+ // Domain schemas (auth, analytics events, …) live in the addons that
48
+ // own them — same pattern as `pipeline-analytics` (`event-store.ts`,
49
+ // `track-store.ts`, `media-store.ts`) and `local-auth` (`auth-schema.ts`).
50
+ // The backend exposes `declareCollection` and stays out of domain
51
+ // knowledge.
42
52
  constructor(dbPath, runtimeDefaults) {
43
53
  this.dbPath = dbPath;
44
54
  this.runtimeDefaults = runtimeDefaults ?? {};
45
55
  }
56
+ dbPath;
46
57
  db = null;
47
- ensuredTables = /* @__PURE__ */ new Set();
48
58
  structuredTables = /* @__PURE__ */ new Set();
59
+ /** Map from scoped collection name → set of column names (non-id) that
60
+ * the structured schema owns. Routes set/get/insert/update/query to
61
+ * typed columns. Every collection MUST be declared here before use. */
62
+ declaredCollections = /* @__PURE__ */ new Map();
49
63
  runtimeDefaults;
64
+ /**
65
+ * Canonical key/value collections — declared with a `(id TEXT PK,
66
+ * data TEXT NOT NULL)` schema at boot so existing JSON-blob rows
67
+ * keep working through the structured path. Generates SQL identical
68
+ * to the previous legacy path; only the routing is unified.
69
+ */
70
+ static CANONICAL_KV_COLLECTIONS = [
71
+ "system-settings",
72
+ "addon-settings",
73
+ "addon-device-settings",
74
+ "addon-devices",
75
+ "device-runtime-state",
76
+ "sections",
77
+ "provider-settings",
78
+ "device-settings",
79
+ "alerts"
80
+ ];
50
81
  async initialize() {
51
82
  const dir = this.dbPath.substring(0, this.dbPath.lastIndexOf("/"));
52
83
  if (dir) {
@@ -56,11 +87,30 @@ var SqliteSettingsBackend = class {
56
87
  this.db = new import_better_sqlite3.default(this.dbPath);
57
88
  this.db.pragma("journal_mode = WAL");
58
89
  this.db.pragma("foreign_keys = ON");
59
- const isEmpty = await this.isEmpty("system-settings");
90
+ for (const collection of _SqliteSettingsBackend.CANONICAL_KV_COLLECTIONS) {
91
+ await this.ensureTable(collection, {
92
+ columns: [
93
+ { name: "id", type: "TEXT", primaryKey: true, notNull: true },
94
+ { name: "data", type: "TEXT", notNull: true }
95
+ ]
96
+ });
97
+ this.declaredCollections.set(collection, {
98
+ primaryKey: "id",
99
+ columns: /* @__PURE__ */ new Set(["data"])
100
+ });
101
+ }
102
+ const isEmpty = await this.isEmpty({ collection: "system-settings" });
60
103
  if (isEmpty) {
61
104
  await this.seedDefaults();
62
105
  }
63
106
  }
107
+ requireDeclared(scoped) {
108
+ const decl = this.declaredCollections.get(scoped);
109
+ if (!decl) {
110
+ throw new Error(`SqliteSettingsBackend: collection "${scoped}" is not declared. Call declareCollection() first or add it to CANONICAL_KV_COLLECTIONS.`);
111
+ }
112
+ return decl;
113
+ }
64
114
  async shutdown() {
65
115
  this.db?.close();
66
116
  this.db = null;
@@ -68,148 +118,255 @@ var SqliteSettingsBackend = class {
68
118
  // ---------------------------------------------------------------------------
69
119
  // ISettingsBackend implementation
70
120
  // ---------------------------------------------------------------------------
71
- async get(collection, key) {
72
- this.ensureCollectionTable(collection);
73
- const row = this.getDb().prepare(`SELECT data FROM "${collection}" WHERE id = ?`).get(key);
121
+ async get({ namespace, collection, key }) {
122
+ const scoped = this.scopedName(namespace, collection);
123
+ const decl = this.requireDeclared(scoped);
124
+ const cols = [`"${decl.primaryKey}"`, ...[...decl.columns].map((c) => `"${c}"`)].join(", ");
125
+ const row = this.getDb().prepare(`SELECT ${cols} FROM "${scoped}" WHERE "${decl.primaryKey}" = ?`).get(key);
74
126
  if (!row) return void 0;
75
- return JSON.parse(row.data);
76
- }
77
- async set(collection, key, value) {
78
- this.ensureCollectionTable(collection);
79
- this.getDb().prepare(`INSERT INTO "${collection}" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`).run(key, JSON.stringify(value));
127
+ if (decl.columns.size === 1 && decl.columns.has("data")) {
128
+ const raw = row["data"];
129
+ return typeof raw === "string" ? JSON.parse(raw) : raw;
130
+ }
131
+ const data = {};
132
+ for (const c of decl.columns) {
133
+ const v = row[c];
134
+ if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
135
+ try {
136
+ data[c] = JSON.parse(v);
137
+ } catch {
138
+ data[c] = v;
139
+ }
140
+ } else {
141
+ data[c] = v ?? null;
142
+ }
143
+ }
144
+ return data;
145
+ }
146
+ async set({ namespace, collection, key, value }) {
147
+ const scoped = this.scopedName(namespace, collection);
148
+ const decl = this.requireDeclared(scoped);
149
+ const row = { [decl.primaryKey]: key };
150
+ if (decl.columns.size === 1 && decl.columns.has("data")) {
151
+ row["data"] = JSON.stringify(value);
152
+ } else {
153
+ const valueObj = value !== null && typeof value === "object" ? value : {};
154
+ for (const [k, v] of Object.entries(valueObj)) {
155
+ if (decl.columns.has(k)) row[k] = this.serializeColumnValue(v);
156
+ }
157
+ }
158
+ const keys = Object.keys(row);
159
+ const cols = keys.map((k) => `"${k}"`).join(", ");
160
+ const placeholders = keys.map(() => "?").join(", ");
161
+ const updates = keys.filter((k) => k !== decl.primaryKey).map((k) => `"${k}" = excluded."${k}"`).join(", ");
162
+ const values = keys.map((k) => row[k]);
163
+ const sql = updates.length > 0 ? `INSERT INTO "${scoped}" (${cols}) VALUES (${placeholders}) ON CONFLICT("${decl.primaryKey}") DO UPDATE SET ${updates}` : `INSERT INTO "${scoped}" (${cols}) VALUES (${placeholders}) ON CONFLICT("${decl.primaryKey}") DO NOTHING`;
164
+ this.getDb().prepare(sql).run(...values);
165
+ }
166
+ async query({ namespace, collection, filter }) {
167
+ const scoped = this.scopedName(namespace, collection);
168
+ const decl = this.requireDeclared(scoped);
169
+ return this.queryDeclared(scoped, decl, filter);
170
+ }
171
+ async insert({ namespace, collection, record }) {
172
+ const scoped = this.scopedName(namespace, collection);
173
+ const decl = this.requireDeclared(scoped);
174
+ const id = record.id || (0, import_node_crypto.randomUUID)();
175
+ const row = { [decl.primaryKey]: id };
176
+ if (decl.columns.size === 1 && decl.columns.has("data")) {
177
+ row["data"] = JSON.stringify(record.data);
178
+ } else {
179
+ for (const [k, v] of Object.entries(record.data)) {
180
+ if (decl.columns.has(k)) row[k] = this.serializeColumnValue(v);
181
+ }
182
+ }
183
+ await this.tableInsert(scoped, row);
184
+ }
185
+ async update({ namespace, collection, id, data }) {
186
+ const scoped = this.scopedName(namespace, collection);
187
+ const decl = this.requireDeclared(scoped);
188
+ const updates = {};
189
+ if (decl.columns.size === 1 && decl.columns.has("data")) {
190
+ updates["data"] = JSON.stringify(data);
191
+ } else {
192
+ for (const [k, v] of Object.entries(data)) {
193
+ if (decl.columns.has(k)) updates[k] = this.serializeColumnValue(v);
194
+ }
195
+ }
196
+ if (Object.keys(updates).length > 0) {
197
+ await this.tableUpdate(scoped, { [decl.primaryKey]: id }, updates);
198
+ }
80
199
  }
81
- async query(collection, filter) {
82
- this.ensureCollectionTable(collection);
83
- let sql = `SELECT id, data FROM "${collection}"`;
200
+ async delete({ namespace, collection, key }) {
201
+ const scoped = this.scopedName(namespace, collection);
202
+ const decl = this.requireDeclared(scoped);
203
+ await this.tableDelete(scoped, { [decl.primaryKey]: key });
204
+ }
205
+ async count({ namespace, collection, filter }) {
206
+ const scoped = this.scopedName(namespace, collection);
207
+ const decl = this.requireDeclared(scoped);
208
+ const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
209
+ const isColumn = (f) => f === decl.primaryKey || decl.columns.has(f);
210
+ const fieldExpr = (f) => {
211
+ if (isColumn(f)) return `"${f}"`;
212
+ if (isKvShape) return `json_extract("data", '$.${f}')`;
213
+ return "";
214
+ };
215
+ let sql = `SELECT COUNT(*) AS cnt FROM "${scoped}"`;
216
+ const params = [];
217
+ if (filter?.where) {
218
+ const clauses = [];
219
+ for (const [field, value] of Object.entries(filter.where)) {
220
+ const expr = fieldExpr(field);
221
+ if (!expr) continue;
222
+ clauses.push(`${expr} = ?`);
223
+ params.push(this.serializeColumnValue(value));
224
+ }
225
+ if (clauses.length > 0) sql += ` WHERE ${clauses.join(" AND ")}`;
226
+ }
227
+ const row = this.getDb().prepare(sql).get(...params);
228
+ return row?.cnt ?? 0;
229
+ }
230
+ async isEmpty({ namespace, collection }) {
231
+ const scoped = this.scopedName(namespace, collection);
232
+ this.requireDeclared(scoped);
233
+ return await this.tableCount(scoped) === 0;
234
+ }
235
+ async queryDeclared(table, decl, filter) {
236
+ const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
237
+ const cols = [`"${decl.primaryKey}"`, ...[...decl.columns].map((c) => `"${c}"`)].join(", ");
238
+ let sql = `SELECT ${cols} FROM "${table}"`;
84
239
  const params = [];
85
240
  const whereClauses = [];
241
+ const isColumn = (f) => f === decl.primaryKey || decl.columns.has(f);
242
+ const fieldExpr = (f) => {
243
+ if (isColumn(f)) return `"${f}"`;
244
+ if (isKvShape) return `json_extract("data", '$.${f}')`;
245
+ return "";
246
+ };
86
247
  if (filter?.where) {
87
248
  for (const [field, value] of Object.entries(filter.where)) {
88
- if (field === "id") {
89
- whereClauses.push("id = ?");
90
- params.push(value);
91
- } else {
92
- whereClauses.push(`json_extract(data, '$.${field}') = ?`);
93
- params.push(typeof value === "object" ? JSON.stringify(value) : value);
94
- }
249
+ const expr = fieldExpr(field);
250
+ if (!expr) continue;
251
+ whereClauses.push(`${expr} = ?`);
252
+ params.push(this.serializeColumnValue(value));
95
253
  }
96
254
  }
97
255
  if (filter?.whereIn) {
98
256
  for (const [field, values] of Object.entries(filter.whereIn)) {
257
+ const expr = fieldExpr(field);
258
+ if (!expr) continue;
99
259
  const placeholders = values.map(() => "?").join(", ");
100
- if (field === "id") {
101
- whereClauses.push(`id IN (${placeholders})`);
102
- } else {
103
- whereClauses.push(`json_extract(data, '$.${field}') IN (${placeholders})`);
104
- }
105
- params.push(...values);
260
+ whereClauses.push(`${expr} IN (${placeholders})`);
261
+ for (const v of values) params.push(this.serializeColumnValue(v));
106
262
  }
107
263
  }
108
264
  if (filter?.whereBetween) {
109
265
  for (const [field, [low, high]] of Object.entries(filter.whereBetween)) {
110
- if (field === "id") {
111
- whereClauses.push("id BETWEEN ? AND ?");
112
- } else {
113
- whereClauses.push(`json_extract(data, '$.${field}') BETWEEN ? AND ?`);
114
- }
115
- params.push(low, high);
266
+ const expr = fieldExpr(field);
267
+ if (!expr) continue;
268
+ whereClauses.push(`${expr} BETWEEN ? AND ?`);
269
+ params.push(this.serializeColumnValue(low), this.serializeColumnValue(high));
116
270
  }
117
271
  }
118
- if (whereClauses.length > 0) {
119
- sql += ` WHERE ${whereClauses.join(" AND ")}`;
120
- }
272
+ if (whereClauses.length > 0) sql += ` WHERE ${whereClauses.join(" AND ")}`;
121
273
  if (filter?.orderBy) {
122
- const dir = filter.orderBy.direction === "desc" ? "DESC" : "ASC";
123
- if (filter.orderBy.field === "id") {
124
- sql += ` ORDER BY id ${dir}`;
125
- } else {
126
- sql += ` ORDER BY json_extract(data, '$.${filter.orderBy.field}') ${dir}`;
274
+ const expr = fieldExpr(filter.orderBy.field);
275
+ if (expr) {
276
+ const dir = filter.orderBy.direction === "desc" ? "DESC" : "ASC";
277
+ sql += ` ORDER BY ${expr} ${dir}`;
127
278
  }
128
279
  }
129
- if (filter?.limit) {
280
+ if (filter?.limit !== void 0) {
130
281
  sql += ` LIMIT ?`;
131
282
  params.push(filter.limit);
132
283
  }
133
- if (filter?.offset) {
284
+ if (filter?.offset !== void 0) {
134
285
  sql += ` OFFSET ?`;
135
286
  params.push(filter.offset);
136
287
  }
137
288
  const rows = this.getDb().prepare(sql).all(...params);
138
- return rows.map((row) => ({
139
- id: row.id,
140
- data: JSON.parse(row.data)
141
- }));
142
- }
143
- async insert(collection, record) {
144
- this.ensureCollectionTable(collection);
145
- const id = record.id || (0, import_node_crypto.randomUUID)();
146
- this.getDb().prepare(`INSERT INTO "${collection}" (id, data) VALUES (?, ?)`).run(id, JSON.stringify(record.data));
147
- }
148
- async update(collection, id, data) {
149
- this.ensureCollectionTable(collection);
150
- this.getDb().prepare(`UPDATE "${collection}" SET data = ? WHERE id = ?`).run(JSON.stringify(data), id);
151
- }
152
- async delete(collection, key) {
153
- this.ensureCollectionTable(collection);
154
- this.getDb().prepare(`DELETE FROM "${collection}" WHERE id = ?`).run(key);
155
- }
156
- async count(collection, filter) {
157
- this.ensureCollectionTable(collection);
158
- if (!filter) {
159
- const row = this.getDb().prepare(`SELECT COUNT(*) AS cnt FROM "${collection}"`).get();
160
- return row?.cnt ?? 0;
161
- }
162
- const rows = await this.query(collection, { ...filter, limit: void 0, offset: void 0 });
163
- return rows.length;
164
- }
165
- async isEmpty(collection) {
166
- this.ensureCollectionTable(collection);
167
- const row = this.getDb().prepare(`SELECT COUNT(*) AS cnt FROM "${collection}"`).get();
168
- return (row?.cnt ?? 0) === 0;
289
+ return rows.map((r) => {
290
+ const id = String(r[decl.primaryKey] ?? "");
291
+ if (isKvShape) {
292
+ const v = r["data"];
293
+ if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
294
+ try {
295
+ return { id, data: JSON.parse(v) };
296
+ } catch {
297
+ return { id, data: { value: v } };
298
+ }
299
+ }
300
+ return { id, data: v == null ? {} : { value: v } };
301
+ }
302
+ const data = { [decl.primaryKey]: r[decl.primaryKey] ?? null };
303
+ for (const c of decl.columns) {
304
+ const v = r[c];
305
+ if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
306
+ try {
307
+ data[c] = JSON.parse(v);
308
+ } catch {
309
+ data[c] = v;
310
+ }
311
+ } else {
312
+ data[c] = v ?? null;
313
+ }
314
+ }
315
+ return { id, data };
316
+ });
169
317
  }
170
318
  // ---------------------------------------------------------------------------
171
319
  // Legacy SettingsStore compatibility (used by ConfigManager.setSettingsStore)
172
320
  // ---------------------------------------------------------------------------
173
321
  /** Get a system setting by dot-notation key */
174
322
  getSystem(key) {
175
- this.ensureCollectionTable("system-settings");
323
+ this.requireDeclared("system-settings");
176
324
  const row = this.getDb().prepare('SELECT data FROM "system-settings" WHERE id = ?').get(key);
177
325
  if (!row) return void 0;
178
326
  return JSON.parse(row.data);
179
327
  }
180
328
  /** Set a system setting */
181
329
  setSystem(key, value) {
182
- this.ensureCollectionTable("system-settings");
330
+ this.requireDeclared("system-settings");
183
331
  this.getDb().prepare('INSERT INTO "system-settings" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data').run(key, JSON.stringify(value));
184
332
  }
185
333
  /** Get all system settings as flat key-value */
186
334
  getAllSystem() {
187
- this.ensureCollectionTable("system-settings");
335
+ this.requireDeclared("system-settings");
188
336
  const rows = this.getDb().prepare('SELECT id, data FROM "system-settings"').all();
189
337
  return Object.fromEntries(rows.map((r) => [r.id, JSON.parse(r.data)]));
190
338
  }
191
339
  /** Get all settings for an addon */
192
340
  getAllAddon(addonId) {
193
- this.ensureCollectionTable("addon-settings");
341
+ this.requireDeclared("addon-settings");
194
342
  const rows = this.getDb().prepare(`SELECT id, data FROM "addon-settings" WHERE json_extract(data, '$.addonId') = ?`).all(addonId);
195
343
  if (rows.length === 0) return {};
196
344
  const result = {};
197
345
  for (const row of rows) {
198
- const parsed = JSON.parse(row.data);
346
+ const parsed = parseRowData(row.data);
199
347
  const key = row.id.startsWith(`${addonId}.`) ? row.id.slice(addonId.length + 1) : row.id;
200
- result[key] = parsed.value ?? parsed;
348
+ const isWrapper = parsed !== null && typeof parsed === "object" && "addonId" in parsed && "key" in parsed;
349
+ if (isWrapper) {
350
+ const wrapper = parsed;
351
+ if ("value" in wrapper) {
352
+ result[key] = wrapper.value;
353
+ }
354
+ } else {
355
+ result[key] = parsed;
356
+ }
201
357
  }
202
358
  return result;
203
359
  }
204
360
  /** Bulk-set all settings for an addon */
205
361
  setAllAddon(addonId, config) {
206
- this.ensureCollectionTable("addon-settings");
362
+ this.requireDeclared("addon-settings");
207
363
  const db = this.getDb();
208
364
  const deleteStmt = db.prepare(`DELETE FROM "addon-settings" WHERE id LIKE ? || '%'`);
209
365
  const insertStmt = db.prepare('INSERT INTO "addon-settings" (id, data) VALUES (?, ?)');
210
366
  db.transaction(() => {
211
367
  deleteStmt.run(`${addonId}.`);
212
368
  for (const [key, value] of Object.entries(config)) {
369
+ if (value === void 0) continue;
213
370
  insertStmt.run(`${addonId}.${key}`, JSON.stringify({ addonId, key, value }));
214
371
  }
215
372
  })();
@@ -230,9 +387,41 @@ var SqliteSettingsBackend = class {
230
387
  setDevice(deviceId, key, value) {
231
388
  this.setScopedKey("device-settings", deviceId, key, value);
232
389
  }
390
+ // ── Addon-device settings (per-device overrides of an addon's config) ──
391
+ //
392
+ // Storage key format: "<addonId>:<deviceId>.<field>" inside the
393
+ // "addon-device-settings" collection. Uses the same JSON-blob layout
394
+ // as the other scoped collections so no schema change is required.
395
+ getAddonDevice(addonId, deviceId) {
396
+ return this.getAllScoped("addon-device-settings", `${addonId}:${deviceId}`);
397
+ }
398
+ setAddonDevice(addonId, deviceId, values) {
399
+ this.requireDeclared("addon-device-settings");
400
+ const db = this.getDb();
401
+ const prefix = `${addonId}:${deviceId}.`;
402
+ const deleteStmt = db.prepare(`DELETE FROM "addon-device-settings" WHERE id LIKE ? || '%'`);
403
+ const insertStmt = db.prepare(
404
+ `INSERT INTO "addon-device-settings" (id, data) VALUES (?, ?)
405
+ ON CONFLICT(id) DO UPDATE SET data = excluded.data`
406
+ );
407
+ db.transaction(() => {
408
+ deleteStmt.run(prefix);
409
+ for (const [key, value] of Object.entries(values)) {
410
+ insertStmt.run(
411
+ `${prefix}${key}`,
412
+ JSON.stringify({ addonId, deviceId, key, value })
413
+ );
414
+ }
415
+ })();
416
+ }
417
+ clearAddonDevice(addonId, deviceId) {
418
+ this.requireDeclared("addon-device-settings");
419
+ const prefix = `${addonId}:${deviceId}.`;
420
+ this.getDb().prepare(`DELETE FROM "addon-device-settings" WHERE id LIKE ? || '%'`).run(prefix);
421
+ }
233
422
  /** Seed system-settings with runtime defaults (first boot) */
234
423
  async seedDefaults() {
235
- this.ensureCollectionTable("system-settings");
424
+ this.requireDeclared("system-settings");
236
425
  const insert = this.getDb().prepare(
237
426
  'INSERT OR IGNORE INTO "system-settings" (id, data) VALUES (?, ?)'
238
427
  );
@@ -245,41 +434,73 @@ var SqliteSettingsBackend = class {
245
434
  // ---------------------------------------------------------------------------
246
435
  // Private helpers
247
436
  // ---------------------------------------------------------------------------
437
+ /**
438
+ * Expose the raw better-sqlite3 Database instance for components that
439
+ * need direct SQL access (e.g. DeviceStore, ConfigStore).
440
+ * Returns null if the backend has not been initialized yet.
441
+ */
442
+ getDatabase() {
443
+ return this.db;
444
+ }
248
445
  getDb() {
249
446
  if (!this.db) throw new Error("SqliteSettingsBackend not initialized \u2014 call initialize() first");
250
447
  return this.db;
251
448
  }
252
- ensureCollectionTable(collection) {
253
- if (this.ensuredTables.has(collection)) return;
254
- this.getDb().exec(
255
- `CREATE TABLE IF NOT EXISTS "${collection}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`
256
- );
257
- this.ensuredTables.add(collection);
258
- }
259
449
  getAllScoped(collection, scopeId) {
260
- this.ensureCollectionTable(collection);
450
+ this.requireDeclared(collection);
261
451
  const rows = this.getDb().prepare(`SELECT id, data FROM "${collection}" WHERE id LIKE ? || '.%'`).all(scopeId);
262
452
  const result = {};
263
453
  for (const row of rows) {
264
454
  const key = row.id.slice(scopeId.length + 1);
265
- const parsed = JSON.parse(row.data);
455
+ const parsed = parseRowData(row.data);
266
456
  result[key] = parsed.value ?? parsed;
267
457
  }
268
458
  return result;
269
459
  }
270
460
  setScopedKey(collection, scopeId, key, value) {
271
- this.ensureCollectionTable(collection);
461
+ this.requireDeclared(collection);
272
462
  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 }));
273
463
  }
464
+ // ── Declared collections (typed SQL tables behind the generic cap) ──
465
+ scopedName(namespace, collection) {
466
+ return namespace ? `${namespace}:${collection}` : collection;
467
+ }
468
+ async declareCollection(input) {
469
+ const table = this.scopedName(input.namespace, input.collection);
470
+ if (this.declaredCollections.has(table)) return;
471
+ const hasId = input.columns.some((c) => c.primaryKey === true);
472
+ const columns = hasId ? input.columns : [{ name: "id", type: "TEXT", primaryKey: true, notNull: true }, ...input.columns];
473
+ const schema = {
474
+ columns: columns.map((c) => ({
475
+ name: c.name,
476
+ // SQLite treats JSON as TEXT; we serialize object values on write
477
+ // and parse on read.
478
+ type: c.type === "JSON" ? "TEXT" : c.type,
479
+ ...c.primaryKey !== void 0 ? { primaryKey: c.primaryKey } : {},
480
+ ...c.notNull !== void 0 ? { notNull: c.notNull } : {},
481
+ ...c.unique !== void 0 ? { unique: c.unique } : {}
482
+ })),
483
+ ...input.indexes ? { indexes: input.indexes.map((i) => ({
484
+ name: i.name,
485
+ columns: i.columns,
486
+ ...i.unique !== void 0 ? { unique: i.unique } : {}
487
+ })) } : {}
488
+ };
489
+ await this.ensureTable(table, schema);
490
+ const pkCol = columns.find((c) => c.primaryKey === true);
491
+ const primaryKey = pkCol ? pkCol.name : "id";
492
+ const columnNames = new Set(columns.filter((c) => c.name !== primaryKey).map((c) => c.name));
493
+ this.declaredCollections.set(table, { primaryKey, columns: columnNames });
494
+ }
495
+ /** Serialise per-column values for SQL binding: objects → JSON, booleans → 0/1. */
496
+ serializeColumnValue(v) {
497
+ if (v === null || v === void 0) return v ?? null;
498
+ if (typeof v === "boolean") return v ? 1 : 0;
499
+ if (typeof v === "object") return JSON.stringify(v);
500
+ return v;
501
+ }
274
502
  // ── Structured table operations ────────────────────────────────────
275
503
  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
504
  if (this.structuredTables.has(table)) return;
284
505
  const colDefs = schema.columns.map((col) => {
285
506
  const parts = [`"${col.name}" ${col.type}`];
@@ -291,6 +512,19 @@ var SqliteSettingsBackend = class {
291
512
  }
292
513
  return parts.join(" ");
293
514
  });
515
+ const existingCols = this.getDb().prepare(`PRAGMA table_info("${table}")`).all();
516
+ if (existingCols.length > 0) {
517
+ const existingNames = new Set(existingCols.map((c) => c.name));
518
+ const declaredNames = new Set(schema.columns.map((c) => c.name));
519
+ const missingDeclared = schema.columns.some((c) => !existingNames.has(c.name));
520
+ const isLegacyKv = existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data");
521
+ if (isLegacyKv || missingDeclared) {
522
+ try {
523
+ this.getDb().exec(`DROP TABLE "${table}"`);
524
+ } catch {
525
+ }
526
+ }
527
+ }
294
528
  this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`);
295
529
  if (schema.indexes) {
296
530
  for (const idx of schema.indexes) {
@@ -346,12 +580,13 @@ var SqliteSettingsBackend = class {
346
580
  sql += ` OFFSET ?`;
347
581
  values.push(options.offset);
348
582
  }
349
- return this.getDb().prepare(sql).all(...values);
583
+ const rows = this.getDb().prepare(sql).all(...values);
584
+ return rows.flatMap((r) => (0, import_types.asJsonObject)(r) ? [(0, import_types.asJsonObject)(r)] : []);
350
585
  }
351
586
  async tableGet(table, filter) {
352
587
  const { whereSql, whereValues } = this.buildWhere(filter);
353
588
  const row = this.getDb().prepare(`SELECT * FROM "${table}"${whereSql} LIMIT 1`).get(...whereValues);
354
- return row ?? null;
589
+ return (0, import_types.asJsonObject)(row);
355
590
  }
356
591
  async tableCount(table, filter) {
357
592
  let sql = `SELECT COUNT(*) as count FROM "${table}"`;
@@ -361,8 +596,8 @@ var SqliteSettingsBackend = class {
361
596
  sql += whereSql;
362
597
  values.push(...whereValues);
363
598
  }
364
- const row = this.getDb().prepare(sql).get(...values);
365
- return row.count;
599
+ const row = (0, import_types.asJsonObject)(this.getDb().prepare(sql).get(...values));
600
+ return typeof row?.count === "number" ? row.count : 0;
366
601
  }
367
602
  buildWhere(filter) {
368
603
  const conditions = [];
@@ -377,41 +612,44 @@ var SqliteSettingsBackend = class {
377
612
  };
378
613
 
379
614
  // src/builtins/sqlite-storage/sqlite-settings.addon.ts
380
- var SqliteSettingsAddon = class {
381
- manifest = {
382
- id: "sqlite-settings",
383
- name: "SQLite Settings",
384
- version: "1.0.0",
385
- capabilities: [{ name: "settings-store", mode: "singleton" }]
386
- };
615
+ var SqliteSettingsAddon = class extends import_types2.BaseAddon {
387
616
  backend = null;
388
- async initialize(context) {
389
- const dbPath = context.storageProvider ? context.storageProvider.resolve("addons-data", `${context.id.replace("addon:", "")}/camstack.db`) : context.dataDir ? `${context.dataDir}/camstack.db` : "camstack-data/addons-data/sqlite-settings/camstack.db";
390
- const runtimeDefaults = context.addonConfig._runtimeDefaults ?? {};
391
- this.backend = new SqliteSettingsBackend(dbPath, runtimeDefaults);
617
+ constructor() {
618
+ super({});
619
+ }
620
+ async onInitialize() {
621
+ const addonId = this.ctx.id.replace("addon:", "");
622
+ let pathSource = "fallback (hardcoded)";
623
+ const dbPath = await this.ctx.api.storage.resolve.query({ location: "addons-data", relativePath: `${addonId}/camstack.db` }).then((p) => {
624
+ pathSource = "storage capability (addons-data)";
625
+ return p;
626
+ }).catch(() => {
627
+ if (this.ctx.dataDir) {
628
+ pathSource = "dataDir";
629
+ return `${this.ctx.dataDir}/camstack.db`;
630
+ }
631
+ return "camstack-data/addons-data/sqlite-settings/camstack.db";
632
+ });
633
+ let dbExists = false;
634
+ try {
635
+ const fs = await import("fs");
636
+ dbExists = fs.existsSync(dbPath);
637
+ } catch (err) {
638
+ this.ctx.logger.warn("Failed to check DB file existence", { meta: { error: (0, import_types2.errMsg)(err) } });
639
+ }
640
+ this.ctx.logger.info("DB path resolved", { meta: { pathSource, dbPath } });
641
+ this.ctx.logger.info("DB file status", { meta: { dbExists } });
642
+ this.backend = new SqliteSettingsBackend(dbPath, { ...import_types2.RUNTIME_DEFAULTS });
392
643
  await this.backend.initialize();
393
- context.logger.info(`SQLite settings initialized at ${dbPath}`);
644
+ this.ctx.logger.info("Initialized successfully");
645
+ return [{ capability: import_types2.settingsStoreCapability, provider: this.backend }];
394
646
  }
395
- async shutdown() {
647
+ async onShutdown() {
396
648
  await this.backend?.shutdown();
397
649
  }
398
650
  getBackend() {
399
651
  return this.backend;
400
652
  }
401
- getCapabilityProvider(name) {
402
- if (name === "settings-store" && this.backend) {
403
- return this.backend;
404
- }
405
- return null;
406
- }
407
- getConfigSchema() {
408
- return { sections: [] };
409
- }
410
- getConfig() {
411
- return {};
412
- }
413
- async onConfigChange(_config) {
414
- }
415
653
  };
416
654
  var sqlite_settings_addon_default = SqliteSettingsAddon;
417
655
  // Annotate the CommonJS export names for ESM import in node: