@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
@@ -0,0 +1,369 @@
1
+ // src/builtins/sqlite-storage/index.ts
2
+ import { FilesystemStorageProvider } from "@camstack/types/node";
3
+
4
+ // src/builtins/sqlite-storage/settings-store.ts
5
+ import Database from "better-sqlite3";
6
+
7
+ // src/builtins/sqlite-storage/sql-schema.ts
8
+ var CORE_TABLE_DDL = [
9
+ // Settings tables
10
+ `CREATE TABLE IF NOT EXISTS system_settings (
11
+ key TEXT PRIMARY KEY,
12
+ value JSON NOT NULL,
13
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
14
+ )`,
15
+ `CREATE TABLE IF NOT EXISTS addon_settings (
16
+ addon_id TEXT NOT NULL,
17
+ key TEXT NOT NULL,
18
+ value JSON NOT NULL,
19
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
20
+ PRIMARY KEY (addon_id, key)
21
+ )`,
22
+ `CREATE TABLE IF NOT EXISTS provider_settings (
23
+ provider_id TEXT NOT NULL,
24
+ key TEXT NOT NULL,
25
+ value JSON NOT NULL,
26
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
27
+ PRIMARY KEY (provider_id, key)
28
+ )`,
29
+ `CREATE TABLE IF NOT EXISTS device_settings (
30
+ device_id TEXT NOT NULL,
31
+ key TEXT NOT NULL,
32
+ value JSON NOT NULL,
33
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
34
+ PRIMARY KEY (device_id, key)
35
+ )`,
36
+ /**
37
+ * Per-device overrides of an addon's settings (scope: 'device' fields).
38
+ * Resolution chain: schema default -> addon_settings (global) -> this.
39
+ */
40
+ `CREATE TABLE IF NOT EXISTS addon_device_settings (
41
+ addon_id TEXT NOT NULL,
42
+ device_id TEXT NOT NULL,
43
+ key TEXT NOT NULL,
44
+ value JSON NOT NULL,
45
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
46
+ PRIMARY KEY (addon_id, device_id, key)
47
+ )`,
48
+ // P12b sweep: removed static DDL for `detection_events`, `audio_levels`,
49
+ // `track_trails` — these were consumed exclusively by the old
50
+ // analytics sub-addon (deleted) and its analysis-data-persistence cap.
51
+ // addon-pipeline-analytics now owns all detection/audio/track tables
52
+ // via `declareCollection` (pipeline-analytics:motion-events,
53
+ // :object-events, :audio-events, :tracks, :media).
54
+ // Device registry
55
+ `CREATE TABLE IF NOT EXISTS devices (
56
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ addon_id TEXT NOT NULL,
58
+ stable_id TEXT NOT NULL,
59
+ type TEXT NOT NULL,
60
+ name TEXT NOT NULL,
61
+ parent_stable_id TEXT,
62
+ role TEXT,
63
+ enabled INTEGER DEFAULT 1,
64
+ created_at TEXT DEFAULT (datetime('now')),
65
+ UNIQUE(addon_id, stable_id)
66
+ )`,
67
+ `CREATE INDEX IF NOT EXISTS idx_devices_addon ON devices(addon_id)`,
68
+ `CREATE INDEX IF NOT EXISTS idx_devices_parent ON devices(addon_id, parent_stable_id)`,
69
+ // Addon / device config store
70
+ // stable_id IS NULL -> addon-global row (one per addon_id)
71
+ // stable_id IS NOT NULL -> per-device row (one per addon_id + stable_id pair)
72
+ // Two partial unique indexes enforce the constraint because SQLite does not
73
+ // support expressions inside UNIQUE(...) declarations.
74
+ `CREATE TABLE IF NOT EXISTS addon_config (
75
+ addon_id TEXT NOT NULL,
76
+ stable_id TEXT,
77
+ data TEXT NOT NULL DEFAULT '{}',
78
+ updated_at TEXT DEFAULT (datetime('now'))
79
+ )`,
80
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_addon_config_global ON addon_config(addon_id) WHERE stable_id IS NULL`,
81
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_addon_config_device ON addon_config(addon_id, stable_id) WHERE stable_id IS NOT NULL`,
82
+ `CREATE INDEX IF NOT EXISTS idx_addon_config_addon ON addon_config(addon_id)`
83
+ ];
84
+ var CORE_TABLE_MIGRATIONS = [
85
+ // 2026-04 — DeviceRole support: optional role hint per device, used by
86
+ // admin UI to pick icons/widgets for accessory children (siren,
87
+ // floodlight, PIR, chime, doorbell, …). Nullable; existing rows stay.
88
+ `ALTER TABLE devices ADD COLUMN role TEXT`
89
+ ];
90
+ function addonTableToDdl(schema) {
91
+ const pks = schema.columns.filter((c) => c.primaryKey).map((c) => c.name);
92
+ const colDefs = schema.columns.map((c) => {
93
+ const parts = [c.name, c.type];
94
+ if (c.notNull) parts.push("NOT NULL");
95
+ return parts.join(" ");
96
+ });
97
+ let ddl = `CREATE TABLE IF NOT EXISTS ${schema.name} (
98
+ ${colDefs.join(",\n ")}`;
99
+ if (pks.length > 0) {
100
+ ddl += `,
101
+ PRIMARY KEY (${pks.join(", ")})`;
102
+ }
103
+ ddl += "\n)";
104
+ const stmts = [ddl];
105
+ for (const idx of schema.indexes ?? []) {
106
+ const unique = idx.unique ? "UNIQUE " : "";
107
+ stmts.push(`CREATE ${unique}INDEX IF NOT EXISTS ${idx.name} ON ${schema.name}(${idx.columns.join(", ")})`);
108
+ }
109
+ return stmts;
110
+ }
111
+
112
+ // src/builtins/sqlite-storage/settings-store.ts
113
+ import { RUNTIME_DEFAULTS } from "@camstack/types";
114
+ var SettingsStore = class {
115
+ db;
116
+ constructor(dbPath) {
117
+ this.db = new Database(dbPath);
118
+ this.db.pragma("journal_mode = WAL");
119
+ this.db.pragma("foreign_keys = ON");
120
+ this.initTables();
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // System settings
124
+ // ---------------------------------------------------------------------------
125
+ getSystem(key) {
126
+ const row = this.db.prepare("SELECT value FROM system_settings WHERE key = ?").get(key);
127
+ if (row === void 0) return void 0;
128
+ return JSON.parse(row.value);
129
+ }
130
+ setSystem(key, value) {
131
+ this.db.prepare(
132
+ `INSERT INTO system_settings (key, value, updated_at) VALUES (?, json(?), unixepoch())
133
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
134
+ ).run(key, JSON.stringify(value));
135
+ }
136
+ getAllSystem() {
137
+ const rows = this.db.prepare("SELECT key, value FROM system_settings").all();
138
+ return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // Addon settings
142
+ // ---------------------------------------------------------------------------
143
+ getAddon(addonId, key) {
144
+ const row = this.db.prepare(
145
+ "SELECT value FROM addon_settings WHERE addon_id = ? AND key = ?"
146
+ ).get(addonId, key);
147
+ if (row === void 0) return void 0;
148
+ return JSON.parse(row.value);
149
+ }
150
+ setAddon(addonId, key, value) {
151
+ this.db.prepare(
152
+ `INSERT INTO addon_settings (addon_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())
153
+ ON CONFLICT(addon_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
154
+ ).run(addonId, key, JSON.stringify(value));
155
+ }
156
+ getAllAddon(addonId) {
157
+ const rows = this.db.prepare(
158
+ "SELECT key, value FROM addon_settings WHERE addon_id = ?"
159
+ ).all(addonId);
160
+ return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
161
+ }
162
+ /** Bulk-replace all keys for an addon (within a transaction). */
163
+ setAllAddon(addonId, config) {
164
+ const deleteStmt = this.db.prepare("DELETE FROM addon_settings WHERE addon_id = ?");
165
+ const insertStmt = this.db.prepare(
166
+ `INSERT INTO addon_settings (addon_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())`
167
+ );
168
+ this.db.transaction(() => {
169
+ deleteStmt.run(addonId);
170
+ for (const [key, value] of Object.entries(config)) {
171
+ insertStmt.run(addonId, key, JSON.stringify(value));
172
+ }
173
+ })();
174
+ }
175
+ // ---------------------------------------------------------------------------
176
+ // Provider settings
177
+ // ---------------------------------------------------------------------------
178
+ getProvider(providerId, key) {
179
+ const row = this.db.prepare(
180
+ "SELECT value FROM provider_settings WHERE provider_id = ? AND key = ?"
181
+ ).get(providerId, key);
182
+ if (row === void 0) return void 0;
183
+ return JSON.parse(row.value);
184
+ }
185
+ setProvider(providerId, key, value) {
186
+ this.db.prepare(
187
+ `INSERT INTO provider_settings (provider_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())
188
+ ON CONFLICT(provider_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
189
+ ).run(providerId, key, JSON.stringify(value));
190
+ }
191
+ getAllProvider(providerId) {
192
+ const rows = this.db.prepare(
193
+ "SELECT key, value FROM provider_settings WHERE provider_id = ?"
194
+ ).all(providerId);
195
+ return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
196
+ }
197
+ // ---------------------------------------------------------------------------
198
+ // Device settings
199
+ // ---------------------------------------------------------------------------
200
+ getDevice(deviceId, key) {
201
+ const row = this.db.prepare(
202
+ "SELECT value FROM device_settings WHERE device_id = ? AND key = ?"
203
+ ).get(deviceId, key);
204
+ if (row === void 0) return void 0;
205
+ return JSON.parse(row.value);
206
+ }
207
+ setDevice(deviceId, key, value) {
208
+ this.db.prepare(
209
+ `INSERT INTO device_settings (device_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())
210
+ ON CONFLICT(device_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
211
+ ).run(deviceId, key, JSON.stringify(value));
212
+ }
213
+ getAllDevice(deviceId) {
214
+ const rows = this.db.prepare(
215
+ "SELECT key, value FROM device_settings WHERE device_id = ?"
216
+ ).all(deviceId);
217
+ return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
218
+ }
219
+ // ---------------------------------------------------------------------------
220
+ // Addon-device settings (per-device overrides of an addon's config)
221
+ //
222
+ // Implements the third level of the resolution chain used by the
223
+ // multi-level settings system:
224
+ // schema default -> addon_settings (global) -> addon_device_settings
225
+ // ---------------------------------------------------------------------------
226
+ getAddonDevice(addonId, deviceId) {
227
+ const rows = this.db.prepare(
228
+ "SELECT key, value FROM addon_device_settings WHERE addon_id = ? AND device_id = ?"
229
+ ).all(addonId, deviceId);
230
+ return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
231
+ }
232
+ /**
233
+ * Bulk-replace all per-device overrides for (addonId, deviceId) in a
234
+ * single transaction. Empty input clears the override set — caller
235
+ * should use `clearAddonDevice` for explicit resets.
236
+ */
237
+ setAddonDevice(addonId, deviceId, values) {
238
+ const deleteStmt = this.db.prepare(
239
+ "DELETE FROM addon_device_settings WHERE addon_id = ? AND device_id = ?"
240
+ );
241
+ const insertStmt = this.db.prepare(
242
+ `INSERT INTO addon_device_settings (addon_id, device_id, key, value, updated_at)
243
+ VALUES (?, ?, ?, json(?), unixepoch())`
244
+ );
245
+ this.db.transaction(() => {
246
+ deleteStmt.run(addonId, deviceId);
247
+ for (const [key, value] of Object.entries(values)) {
248
+ insertStmt.run(addonId, deviceId, key, JSON.stringify(value));
249
+ }
250
+ })();
251
+ }
252
+ clearAddonDevice(addonId, deviceId) {
253
+ this.db.prepare("DELETE FROM addon_device_settings WHERE addon_id = ? AND device_id = ?").run(addonId, deviceId);
254
+ }
255
+ // ---------------------------------------------------------------------------
256
+ // Lifecycle
257
+ // ---------------------------------------------------------------------------
258
+ /** Close the SQLite connection (call on shutdown). */
259
+ close() {
260
+ this.db.close();
261
+ }
262
+ /** Check if system_settings is empty (used for first-boot seeding). */
263
+ isSystemSettingsEmpty() {
264
+ const row = this.db.prepare("SELECT COUNT(*) AS cnt FROM system_settings").get();
265
+ return (row?.cnt ?? 0) === 0;
266
+ }
267
+ /** Seed system_settings with RUNTIME_DEFAULTS (only on first boot). */
268
+ seedDefaults() {
269
+ const insert = this.db.prepare(
270
+ `INSERT OR IGNORE INTO system_settings (key, value, updated_at) VALUES (?, json(?), unixepoch())`
271
+ );
272
+ this.db.transaction(() => {
273
+ for (const [key, value] of Object.entries(RUNTIME_DEFAULTS)) {
274
+ insert.run(key, JSON.stringify(value));
275
+ }
276
+ })();
277
+ }
278
+ // ---------------------------------------------------------------------------
279
+ // Private helpers
280
+ // ---------------------------------------------------------------------------
281
+ initTables() {
282
+ this.db.transaction(() => {
283
+ for (const stmt of CORE_TABLE_DDL) {
284
+ this.db.prepare(stmt).run();
285
+ }
286
+ for (const stmt of CORE_TABLE_MIGRATIONS) {
287
+ try {
288
+ this.db.prepare(stmt).run();
289
+ } catch (err) {
290
+ const msg = err instanceof Error ? err.message : String(err);
291
+ if (msg.includes("duplicate column name") || msg.includes("no such table")) continue;
292
+ throw err;
293
+ }
294
+ }
295
+ })();
296
+ }
297
+ };
298
+
299
+ // src/builtins/sqlite-storage/device-store.ts
300
+ var DeviceStore = class {
301
+ constructor(db) {
302
+ this.db = db;
303
+ }
304
+ db;
305
+ insert(addonId, device) {
306
+ this.db.prepare(
307
+ `INSERT INTO devices (addon_id, stable_id, type, name, parent_stable_id) VALUES (?, ?, ?, ?, ?)`
308
+ ).run(addonId, device.stableId, device.type, device.name, device.parentStableId);
309
+ }
310
+ listByAddon(addonId) {
311
+ return this.db.prepare(
312
+ `SELECT stable_id as stableId, type, name, parent_stable_id as parentStableId, enabled FROM devices WHERE addon_id = ?`
313
+ ).all(addonId);
314
+ }
315
+ listChildren(addonId, parentStableId) {
316
+ return this.db.prepare(
317
+ `SELECT stable_id as stableId, type, name, parent_stable_id as parentStableId, enabled FROM devices WHERE addon_id = ? AND parent_stable_id = ?`
318
+ ).all(addonId, parentStableId);
319
+ }
320
+ remove(addonId, stableId) {
321
+ this.db.prepare(`DELETE FROM devices WHERE addon_id = ? AND stable_id = ?`).run(addonId, stableId);
322
+ }
323
+ };
324
+
325
+ // src/builtins/sqlite-storage/config-store.ts
326
+ var ConfigStore = class {
327
+ constructor(db) {
328
+ this.db = db;
329
+ }
330
+ db;
331
+ save(addonId, stableId, data) {
332
+ if (stableId === null) {
333
+ this.db.prepare(
334
+ `INSERT INTO addon_config (addon_id, stable_id, data, updated_at)
335
+ VALUES (?, NULL, ?, datetime('now'))
336
+ ON CONFLICT(addon_id) WHERE stable_id IS NULL
337
+ DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
338
+ ).run(addonId, JSON.stringify(data));
339
+ } else {
340
+ this.db.prepare(
341
+ `INSERT INTO addon_config (addon_id, stable_id, data, updated_at)
342
+ VALUES (?, ?, ?, datetime('now'))
343
+ ON CONFLICT(addon_id, stable_id) WHERE stable_id IS NOT NULL
344
+ DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
345
+ ).run(addonId, stableId, JSON.stringify(data));
346
+ }
347
+ }
348
+ load(addonId, stableId) {
349
+ const row = stableId === null ? this.db.prepare(`SELECT data FROM addon_config WHERE addon_id = ? AND stable_id IS NULL`).get(addonId) : this.db.prepare(`SELECT data FROM addon_config WHERE addon_id = ? AND stable_id = ?`).get(addonId, stableId);
350
+ return row ? JSON.parse(row.data) : {};
351
+ }
352
+ remove(addonId, stableId) {
353
+ if (stableId === null) {
354
+ this.db.prepare(`DELETE FROM addon_config WHERE addon_id = ? AND stable_id IS NULL`).run(addonId);
355
+ } else {
356
+ this.db.prepare(`DELETE FROM addon_config WHERE addon_id = ? AND stable_id = ?`).run(addonId, stableId);
357
+ }
358
+ }
359
+ };
360
+
361
+ export {
362
+ CORE_TABLE_DDL,
363
+ addonTableToDdl,
364
+ SettingsStore,
365
+ DeviceStore,
366
+ ConfigStore,
367
+ FilesystemStorageProvider
368
+ };
369
+ //# sourceMappingURL=chunk-ZELBCPDC.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/builtins/sqlite-storage/index.ts","../src/builtins/sqlite-storage/settings-store.ts","../src/builtins/sqlite-storage/sql-schema.ts","../src/builtins/sqlite-storage/device-store.ts","../src/builtins/sqlite-storage/config-store.ts"],"sourcesContent":["// FilesystemStorageProvider lives in `@camstack/types/node` since 2026-05\n// (kernel ↔ core cycle break). Re-export for backward compat.\nexport { FilesystemStorageProvider } from '@camstack/types/node'\nexport { FilesystemStorageAddon } from './filesystem-storage.addon.js'\nexport { SqliteSettingsBackend } from './sqlite-settings-backend.js'\nexport { SqliteSettingsAddon } from './sqlite-settings.addon.js'\nexport { SettingsStore } from './settings-store.js'\nexport { CORE_TABLE_DDL, addonTableToDdl } from './sql-schema.js'\nexport type { AddonTableSchema } from './sql-schema.js'\nexport { DeviceStore } from './device-store.js'\nexport type { DeviceRow } from './device-store.js'\nexport { ConfigStore } from './config-store.js'\n\n// Default export for AddonLoader\nexport { FilesystemStorageAddon as default } from './filesystem-storage.addon.js'\n","import Database from 'better-sqlite3'\nimport { CORE_TABLE_DDL, CORE_TABLE_MIGRATIONS } from './sql-schema.js'\nimport { RUNTIME_DEFAULTS } from '@camstack/types'\n\n/**\n * Thin wrapper over better-sqlite3 that manages the four settings tables:\n * system_settings, addon_settings, provider_settings, device_settings.\n *\n * All values are stored as JSON text and deserialized on read.\n */\nexport class SettingsStore {\n private readonly db: Database.Database\n\n constructor(dbPath: string) {\n this.db = new Database(dbPath)\n this.db.pragma('journal_mode = WAL')\n this.db.pragma('foreign_keys = ON')\n this.initTables()\n }\n\n // ---------------------------------------------------------------------------\n // System settings\n // ---------------------------------------------------------------------------\n\n getSystem(key: string): unknown {\n const row = this.db\n .prepare<[string], { value: string }>('SELECT value FROM system_settings WHERE key = ?')\n .get(key)\n if (row === undefined) return undefined\n return JSON.parse(row.value)\n }\n\n setSystem(key: string, value: unknown): void {\n this.db\n .prepare(\n `INSERT INTO system_settings (key, value, updated_at) VALUES (?, json(?), unixepoch())\n ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n )\n .run(key, JSON.stringify(value))\n }\n\n getAllSystem(): Record<string, unknown> {\n const rows = this.db\n .prepare<[], { key: string; value: string }>('SELECT key, value FROM system_settings')\n .all()\n return Object.fromEntries(rows.map(r => [r.key, JSON.parse(r.value)]))\n }\n\n // ---------------------------------------------------------------------------\n // Addon settings\n // ---------------------------------------------------------------------------\n\n getAddon(addonId: string, key: string): unknown {\n const row = this.db\n .prepare<[string, string], { value: string }>(\n 'SELECT value FROM addon_settings WHERE addon_id = ? AND key = ?',\n )\n .get(addonId, key)\n if (row === undefined) return undefined\n return JSON.parse(row.value)\n }\n\n setAddon(addonId: string, key: string, value: unknown): void {\n this.db\n .prepare(\n `INSERT INTO addon_settings (addon_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())\n ON CONFLICT(addon_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n )\n .run(addonId, key, JSON.stringify(value))\n }\n\n getAllAddon(addonId: string): Record<string, unknown> {\n const rows = this.db\n .prepare<[string], { key: string; value: string }>(\n 'SELECT key, value FROM addon_settings WHERE addon_id = ?',\n )\n .all(addonId)\n return Object.fromEntries(rows.map(r => [r.key, JSON.parse(r.value)]))\n }\n\n /** Bulk-replace all keys for an addon (within a transaction). */\n setAllAddon(addonId: string, config: Record<string, unknown>): void {\n const deleteStmt = this.db.prepare('DELETE FROM addon_settings WHERE addon_id = ?')\n const insertStmt = this.db.prepare(\n `INSERT INTO addon_settings (addon_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())`,\n )\n this.db.transaction(() => {\n deleteStmt.run(addonId)\n for (const [key, value] of Object.entries(config)) {\n insertStmt.run(addonId, key, JSON.stringify(value))\n }\n })()\n }\n\n // ---------------------------------------------------------------------------\n // Provider settings\n // ---------------------------------------------------------------------------\n\n getProvider(providerId: string, key: string): unknown {\n const row = this.db\n .prepare<[string, string], { value: string }>(\n 'SELECT value FROM provider_settings WHERE provider_id = ? AND key = ?',\n )\n .get(providerId, key)\n if (row === undefined) return undefined\n return JSON.parse(row.value)\n }\n\n setProvider(providerId: string, key: string, value: unknown): void {\n this.db\n .prepare(\n `INSERT INTO provider_settings (provider_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())\n ON CONFLICT(provider_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n )\n .run(providerId, key, JSON.stringify(value))\n }\n\n getAllProvider(providerId: string): Record<string, unknown> {\n const rows = this.db\n .prepare<[string], { key: string; value: string }>(\n 'SELECT key, value FROM provider_settings WHERE provider_id = ?',\n )\n .all(providerId)\n return Object.fromEntries(rows.map(r => [r.key, JSON.parse(r.value)]))\n }\n\n // ---------------------------------------------------------------------------\n // Device settings\n // ---------------------------------------------------------------------------\n\n getDevice(deviceId: string, key: string): unknown {\n const row = this.db\n .prepare<[string, string], { value: string }>(\n 'SELECT value FROM device_settings WHERE device_id = ? AND key = ?',\n )\n .get(deviceId, key)\n if (row === undefined) return undefined\n return JSON.parse(row.value)\n }\n\n setDevice(deviceId: string, key: string, value: unknown): void {\n this.db\n .prepare(\n `INSERT INTO device_settings (device_id, key, value, updated_at) VALUES (?, ?, json(?), unixepoch())\n ON CONFLICT(device_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,\n )\n .run(deviceId, key, JSON.stringify(value))\n }\n\n getAllDevice(deviceId: string): Record<string, unknown> {\n const rows = this.db\n .prepare<[string], { key: string; value: string }>(\n 'SELECT key, value FROM device_settings WHERE device_id = ?',\n )\n .all(deviceId)\n return Object.fromEntries(rows.map(r => [r.key, JSON.parse(r.value)]))\n }\n\n // ---------------------------------------------------------------------------\n // Addon-device settings (per-device overrides of an addon's config)\n //\n // Implements the third level of the resolution chain used by the\n // multi-level settings system:\n // schema default -> addon_settings (global) -> addon_device_settings\n // ---------------------------------------------------------------------------\n\n getAddonDevice(addonId: string, deviceId: string): Record<string, unknown> {\n const rows = this.db\n .prepare<[string, string], { key: string; value: string }>(\n 'SELECT key, value FROM addon_device_settings WHERE addon_id = ? AND device_id = ?',\n )\n .all(addonId, deviceId)\n return Object.fromEntries(rows.map(r => [r.key, JSON.parse(r.value)]))\n }\n\n /**\n * Bulk-replace all per-device overrides for (addonId, deviceId) in a\n * single transaction. Empty input clears the override set — caller\n * should use `clearAddonDevice` for explicit resets.\n */\n setAddonDevice(addonId: string, deviceId: string, values: Record<string, unknown>): void {\n const deleteStmt = this.db.prepare(\n 'DELETE FROM addon_device_settings WHERE addon_id = ? AND device_id = ?',\n )\n const insertStmt = this.db.prepare(\n `INSERT INTO addon_device_settings (addon_id, device_id, key, value, updated_at)\n VALUES (?, ?, ?, json(?), unixepoch())`,\n )\n this.db.transaction(() => {\n deleteStmt.run(addonId, deviceId)\n for (const [key, value] of Object.entries(values)) {\n insertStmt.run(addonId, deviceId, key, JSON.stringify(value))\n }\n })()\n }\n\n clearAddonDevice(addonId: string, deviceId: string): void {\n this.db\n .prepare('DELETE FROM addon_device_settings WHERE addon_id = ? AND device_id = ?')\n .run(addonId, deviceId)\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n /** Close the SQLite connection (call on shutdown). */\n close(): void {\n this.db.close()\n }\n\n /** Check if system_settings is empty (used for first-boot seeding). */\n isSystemSettingsEmpty(): boolean {\n const row = this.db\n .prepare<[], { cnt: number }>('SELECT COUNT(*) AS cnt FROM system_settings')\n .get()\n return (row?.cnt ?? 0) === 0\n }\n\n /** Seed system_settings with RUNTIME_DEFAULTS (only on first boot). */\n seedDefaults(): void {\n const insert = this.db.prepare(\n `INSERT OR IGNORE INTO system_settings (key, value, updated_at) VALUES (?, json(?), unixepoch())`,\n )\n this.db.transaction(() => {\n for (const [key, value] of Object.entries(RUNTIME_DEFAULTS)) {\n insert.run(key, JSON.stringify(value))\n }\n })()\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private initTables(): void {\n this.db.transaction(() => {\n for (const stmt of CORE_TABLE_DDL) {\n this.db.prepare(stmt).run()\n }\n // Idempotent additive column migrations. Each ALTER statement may\n // fail on installs where the column already exists (fresh\n // installs, or a previous boot ran the same migration). We\n // swallow those specific errors and re-throw anything else —\n // a genuine schema failure should still crash startup.\n for (const stmt of CORE_TABLE_MIGRATIONS) {\n try {\n this.db.prepare(stmt).run()\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err)\n if (msg.includes('duplicate column name') || msg.includes('no such table')) continue\n throw err\n }\n }\n })()\n }\n}\n","/** Core table DDL statements -- executed on first boot */\nexport const CORE_TABLE_DDL: readonly string[] = [\n // Settings tables\n `CREATE TABLE IF NOT EXISTS system_settings (\n key TEXT PRIMARY KEY,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch())\n )`,\n `CREATE TABLE IF NOT EXISTS addon_settings (\n addon_id TEXT NOT NULL,\n key TEXT NOT NULL,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch()),\n PRIMARY KEY (addon_id, key)\n )`,\n `CREATE TABLE IF NOT EXISTS provider_settings (\n provider_id TEXT NOT NULL,\n key TEXT NOT NULL,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch()),\n PRIMARY KEY (provider_id, key)\n )`,\n `CREATE TABLE IF NOT EXISTS device_settings (\n device_id TEXT NOT NULL,\n key TEXT NOT NULL,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch()),\n PRIMARY KEY (device_id, key)\n )`,\n /**\n * Per-device overrides of an addon's settings (scope: 'device' fields).\n * Resolution chain: schema default -> addon_settings (global) -> this.\n */\n `CREATE TABLE IF NOT EXISTS addon_device_settings (\n addon_id TEXT NOT NULL,\n device_id TEXT NOT NULL,\n key TEXT NOT NULL,\n value JSON NOT NULL,\n updated_at INTEGER NOT NULL DEFAULT (unixepoch()),\n PRIMARY KEY (addon_id, device_id, key)\n )`,\n\n // P12b sweep: removed static DDL for `detection_events`, `audio_levels`,\n // `track_trails` — these were consumed exclusively by the old\n // analytics sub-addon (deleted) and its analysis-data-persistence cap.\n // addon-pipeline-analytics now owns all detection/audio/track tables\n // via `declareCollection` (pipeline-analytics:motion-events,\n // :object-events, :audio-events, :tracks, :media).\n\n // Device registry\n `CREATE TABLE IF NOT EXISTS devices (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n addon_id TEXT NOT NULL,\n stable_id TEXT NOT NULL,\n type TEXT NOT NULL,\n name TEXT NOT NULL,\n parent_stable_id TEXT,\n role TEXT,\n enabled INTEGER DEFAULT 1,\n created_at TEXT DEFAULT (datetime('now')),\n UNIQUE(addon_id, stable_id)\n)`,\n `CREATE INDEX IF NOT EXISTS idx_devices_addon ON devices(addon_id)`,\n `CREATE INDEX IF NOT EXISTS idx_devices_parent ON devices(addon_id, parent_stable_id)`,\n\n // Addon / device config store\n // stable_id IS NULL -> addon-global row (one per addon_id)\n // stable_id IS NOT NULL -> per-device row (one per addon_id + stable_id pair)\n // Two partial unique indexes enforce the constraint because SQLite does not\n // support expressions inside UNIQUE(...) declarations.\n `CREATE TABLE IF NOT EXISTS addon_config (\n addon_id TEXT NOT NULL,\n stable_id TEXT,\n data TEXT NOT NULL DEFAULT '{}',\n updated_at TEXT DEFAULT (datetime('now'))\n)`,\n `CREATE UNIQUE INDEX IF NOT EXISTS idx_addon_config_global ON addon_config(addon_id) WHERE stable_id IS NULL`,\n `CREATE UNIQUE INDEX IF NOT EXISTS idx_addon_config_device ON addon_config(addon_id, stable_id) WHERE stable_id IS NOT NULL`,\n `CREATE INDEX IF NOT EXISTS idx_addon_config_addon ON addon_config(addon_id)`,\n]\n\n/**\n * Idempotent ALTER statements run after `CORE_TABLE_DDL`. Each entry\n * may fail on installs where the column already exists — the runner\n * catches \"duplicate column name\" / \"no such table\" errors and moves\n * on. Order matters: migrations stack in chronological order.\n *\n * Pattern when adding a new column:\n * 1. Add the column to the matching `CREATE TABLE` above so fresh\n * installs get it by default.\n * 2. Append an `ALTER TABLE ... ADD COLUMN ...` here so existing\n * installs get it on next boot.\n */\nexport const CORE_TABLE_MIGRATIONS: readonly string[] = [\n // 2026-04 — DeviceRole support: optional role hint per device, used by\n // admin UI to pick icons/widgets for accessory children (siren,\n // floodlight, PIR, chime, doorbell, …). Nullable; existing rows stay.\n `ALTER TABLE devices ADD COLUMN role TEXT`,\n]\n\n/** Addon table schema declaration */\nexport interface AddonTableSchema {\n readonly name: string\n readonly columns: ReadonlyArray<{\n readonly name: string\n readonly type: 'TEXT' | 'INTEGER' | 'REAL' | 'JSON'\n readonly primaryKey?: boolean\n readonly notNull?: boolean\n }>\n readonly indexes?: ReadonlyArray<{\n readonly name: string\n readonly columns: readonly string[]\n readonly unique?: boolean\n }>\n}\n\n/** Generate CREATE TABLE DDL from addon schema */\nexport function addonTableToDdl(schema: AddonTableSchema): string[] {\n const pks = schema.columns.filter(c => c.primaryKey).map(c => c.name)\n const colDefs = schema.columns.map(c => {\n const parts = [c.name, c.type]\n if (c.notNull) parts.push('NOT NULL')\n return parts.join(' ')\n })\n\n let ddl = `CREATE TABLE IF NOT EXISTS ${schema.name} (\\n ${colDefs.join(',\\n ')}`\n if (pks.length > 0) {\n ddl += `,\\n PRIMARY KEY (${pks.join(', ')})`\n }\n ddl += '\\n)'\n\n const stmts = [ddl]\n for (const idx of schema.indexes ?? []) {\n const unique = idx.unique ? 'UNIQUE ' : ''\n stmts.push(`CREATE ${unique}INDEX IF NOT EXISTS ${idx.name} ON ${schema.name}(${idx.columns.join(', ')})`)\n }\n return stmts\n}\n","import type Database from 'better-sqlite3'\n\nexport interface DeviceRow {\n readonly stableId: string\n readonly type: string\n readonly name: string\n readonly parentStableId: string | null\n readonly enabled: boolean\n}\n\ninterface InsertInput {\n readonly stableId: string\n readonly type: string\n readonly name: string\n readonly parentStableId: string | null\n}\n\nexport class DeviceStore {\n constructor(private readonly db: Database.Database) {}\n\n insert(addonId: string, device: InsertInput): void {\n this.db\n .prepare(\n `INSERT INTO devices (addon_id, stable_id, type, name, parent_stable_id) VALUES (?, ?, ?, ?, ?)`,\n )\n .run(addonId, device.stableId, device.type, device.name, device.parentStableId)\n }\n\n listByAddon(addonId: string): readonly DeviceRow[] {\n return this.db\n .prepare(\n `SELECT stable_id as stableId, type, name, parent_stable_id as parentStableId, enabled FROM devices WHERE addon_id = ?`,\n )\n .all(addonId) as DeviceRow[]\n }\n\n listChildren(addonId: string, parentStableId: string): readonly DeviceRow[] {\n return this.db\n .prepare(\n `SELECT stable_id as stableId, type, name, parent_stable_id as parentStableId, enabled FROM devices WHERE addon_id = ? AND parent_stable_id = ?`,\n )\n .all(addonId, parentStableId) as DeviceRow[]\n }\n\n remove(addonId: string, stableId: string): void {\n this.db.prepare(`DELETE FROM devices WHERE addon_id = ? AND stable_id = ?`).run(addonId, stableId)\n }\n}\n","import type Database from 'better-sqlite3'\n\nexport class ConfigStore {\n constructor(private readonly db: Database.Database) {}\n\n save(addonId: string, stableId: string | null, data: Record<string, unknown>): void {\n // Two partial unique indexes enforce uniqueness (one for null, one for non-null stable_id).\n // SQLite resolves ON CONFLICT to the matching partial index automatically.\n if (stableId === null) {\n this.db\n .prepare(\n `INSERT INTO addon_config (addon_id, stable_id, data, updated_at)\n VALUES (?, NULL, ?, datetime('now'))\n ON CONFLICT(addon_id) WHERE stable_id IS NULL\n DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`,\n )\n .run(addonId, JSON.stringify(data))\n } else {\n this.db\n .prepare(\n `INSERT INTO addon_config (addon_id, stable_id, data, updated_at)\n VALUES (?, ?, ?, datetime('now'))\n ON CONFLICT(addon_id, stable_id) WHERE stable_id IS NOT NULL\n DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`,\n )\n .run(addonId, stableId, JSON.stringify(data))\n }\n }\n\n load(addonId: string, stableId: string | null): Record<string, unknown> {\n const row = stableId === null\n ? (this.db\n .prepare(`SELECT data FROM addon_config WHERE addon_id = ? AND stable_id IS NULL`)\n .get(addonId) as { data: string } | undefined)\n : (this.db\n .prepare(`SELECT data FROM addon_config WHERE addon_id = ? AND stable_id = ?`)\n .get(addonId, stableId) as { data: string } | undefined)\n\n return row ? (JSON.parse(row.data) as Record<string, unknown>) : {}\n }\n\n remove(addonId: string, stableId: string | null): void {\n if (stableId === null) {\n this.db.prepare(`DELETE FROM addon_config WHERE addon_id = ? AND stable_id IS NULL`).run(addonId)\n } else {\n this.db.prepare(`DELETE FROM addon_config WHERE addon_id = ? AND stable_id = ?`).run(addonId, stableId)\n }\n }\n}\n"],"mappings":"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","names":[]}