@camstack/core 0.1.13 → 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
@@ -31,213 +31,124 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var sqlite_storage_exports = {};
32
32
  __export(sqlite_storage_exports, {
33
33
  CORE_TABLE_DDL: () => CORE_TABLE_DDL,
34
- FileSystemStorage: () => FileSystemStorage,
34
+ ConfigStore: () => ConfigStore,
35
+ DeviceStore: () => DeviceStore,
35
36
  FilesystemStorageAddon: () => FilesystemStorageAddon,
36
- FilesystemStorageProvider: () => FilesystemStorageProvider,
37
+ FilesystemStorageProvider: () => import_node2.FilesystemStorageProvider,
37
38
  SettingsStore: () => SettingsStore,
38
39
  SqliteSettingsAddon: () => SqliteSettingsAddon,
39
40
  SqliteSettingsBackend: () => SqliteSettingsBackend,
40
- SqliteStorageAddon: () => SqliteStorageAddon,
41
- SqliteStorageProvider: () => SqliteStorageProvider,
42
41
  addonTableToDdl: () => addonTableToDdl,
43
42
  default: () => FilesystemStorageAddon
44
43
  });
45
44
  module.exports = __toCommonJS(sqlite_storage_exports);
46
-
47
- // src/builtins/sqlite-storage/filesystem-storage-provider.ts
48
- var fs = __toESM(require("fs"));
49
- var path = __toESM(require("path"));
50
- var STORAGE_LOCATION_TYPES = [
51
- "recordings-high",
52
- "recordings-low",
53
- "recordings-clips",
54
- "event-images",
55
- "models",
56
- "addons-data",
57
- "cache",
58
- "logs"
59
- ];
60
- var DEFAULT_LOCATION_SUBDIRS = {
61
- "recordings-high": "recordings-high",
62
- "recordings-low": "recordings-low",
63
- "recordings-clips": "recordings-clips",
64
- "event-images": "event-images",
65
- "models": "models",
66
- "addons-data": "addons-data",
67
- "cache": "/tmp/camstack-cache",
68
- "logs": "logs"
69
- };
70
- var FilesystemStorageProvider = class {
71
- id = "local";
72
- name = "Local Filesystem";
73
- supportedLocations = [...STORAGE_LOCATION_TYPES];
74
- rootPath;
75
- locationPaths;
76
- constructor(rootPath, overrides) {
77
- this.rootPath = path.resolve(rootPath);
78
- this.locationPaths = /* @__PURE__ */ new Map();
79
- for (const loc of STORAGE_LOCATION_TYPES) {
80
- const override = overrides?.[loc];
81
- if (override) {
82
- this.locationPaths.set(loc, path.resolve(override));
83
- } else {
84
- const subdir = DEFAULT_LOCATION_SUBDIRS[loc];
85
- this.locationPaths.set(
86
- loc,
87
- path.isAbsolute(subdir) ? subdir : path.join(this.rootPath, subdir)
88
- );
89
- }
90
- }
91
- }
92
- resolve(location, relativePath) {
93
- const base = this.locationPaths.get(location);
94
- if (!base) throw new Error(`Unknown storage location: ${location}`);
95
- return path.join(base, relativePath);
96
- }
97
- async write(location, relativePath, data) {
98
- const filePath = this.resolve(location, relativePath);
99
- await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
100
- if (Buffer.isBuffer(data)) {
101
- await fs.promises.writeFile(filePath, data);
102
- } else {
103
- const writeStream = fs.createWriteStream(filePath);
104
- await new Promise((resolve2, reject) => {
105
- data.pipe(writeStream);
106
- writeStream.on("finish", resolve2);
107
- writeStream.on("error", reject);
108
- });
109
- }
110
- }
111
- async read(location, relativePath) {
112
- return fs.promises.readFile(this.resolve(location, relativePath));
113
- }
114
- async exists(location, relativePath) {
115
- try {
116
- await fs.promises.access(this.resolve(location, relativePath));
117
- return true;
118
- } catch {
119
- return false;
120
- }
121
- }
122
- async list(location, prefix) {
123
- const base = this.locationPaths.get(location);
124
- if (!base) return [];
125
- const dir = prefix ? path.join(base, prefix) : base;
126
- try {
127
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
128
- return entries.map((e) => prefix ? `${prefix}/${e.name}` : e.name);
129
- } catch {
130
- return [];
131
- }
132
- }
133
- async delete(location, relativePath) {
134
- const filePath = this.resolve(location, relativePath);
135
- await fs.promises.rm(filePath, { force: true });
136
- }
137
- async getAvailableSpace(location) {
138
- const base = this.locationPaths.get(location);
139
- if (!base) return null;
140
- try {
141
- const stats = await fs.promises.statfs(base);
142
- return stats.bavail * stats.bsize;
143
- } catch {
144
- return null;
145
- }
146
- }
147
- async initialize() {
148
- for (const [, dirPath] of this.locationPaths) {
149
- await fs.promises.mkdir(dirPath, { recursive: true });
150
- }
151
- }
152
- async shutdown() {
153
- }
154
- /** Get the resolved path for a location type */
155
- getLocationPath(location) {
156
- const p = this.locationPaths.get(location);
157
- if (!p) throw new Error(`Unknown storage location: ${location}`);
158
- return p;
159
- }
160
- /** Get the root path */
161
- getRootPath() {
162
- return this.rootPath;
163
- }
164
- };
45
+ var import_node2 = require("@camstack/types/node");
165
46
 
166
47
  // src/builtins/sqlite-storage/filesystem-storage.addon.ts
167
- var FilesystemStorageAddon = class {
168
- manifest = {
169
- id: "filesystem-storage",
170
- name: "Local Filesystem Storage",
171
- version: "1.0.0",
172
- capabilities: [{ name: "storage", mode: "collection" }]
173
- };
48
+ var import_types = require("@camstack/types");
49
+ var import_node = require("@camstack/types/node");
50
+ var FilesystemStorageAddon = class extends import_types.BaseAddon {
174
51
  provider = null;
175
- currentConfig = {
176
- rootPath: "camstack-data"
177
- };
178
- async initialize(context) {
179
- const rootPath = context.addonConfig.rootPath ?? this.currentConfig.rootPath;
180
- this.currentConfig.rootPath = rootPath;
52
+ constructor() {
53
+ super({ rootPath: "camstack-data" });
54
+ }
55
+ async onInitialize() {
56
+ const rootPath = this.ctx.addonConfig.rootPath ?? this.config.rootPath;
181
57
  const overrides = {};
182
- for (const key of Object.keys(context.addonConfig)) {
58
+ const addonCfg = this.ctx.addonConfig;
59
+ for (const key of Object.keys(addonCfg)) {
183
60
  if (key.startsWith("override.")) {
184
61
  const loc = key.slice("override.".length);
185
- overrides[loc] = context.addonConfig[key];
62
+ overrides[loc] = addonCfg[key];
186
63
  }
187
64
  }
188
- this.provider = new FilesystemStorageProvider(rootPath, overrides);
189
- await this.provider.initialize();
190
- context.logger.info(`Filesystem storage initialized at ${this.provider.getRootPath()}`);
191
- }
192
- async shutdown() {
193
- await this.provider?.shutdown();
65
+ this.provider = new import_node.FilesystemStorageProvider(rootPath, overrides);
66
+ this.ctx.logger.info("Filesystem storage initialized", { meta: { rootPath: this.provider.getRootPath() } });
67
+ return [{ capability: import_types.storageCapability, provider: this.provider }];
194
68
  }
195
- getCapabilityProvider(name) {
196
- if (name === "storage" && this.provider) {
197
- return this.provider;
198
- }
199
- return null;
69
+ async onShutdown() {
70
+ this.provider = null;
200
71
  }
201
72
  getProvider() {
202
73
  return this.provider;
203
74
  }
204
- getConfigSchema() {
205
- return { sections: [] };
206
- }
207
- getConfig() {
208
- return { ...this.currentConfig };
209
- }
210
- async onConfigChange(config) {
211
- this.currentConfig.rootPath = config.rootPath ?? this.currentConfig.rootPath;
212
- }
213
75
  };
214
76
 
215
77
  // src/builtins/sqlite-storage/sqlite-settings-backend.ts
216
78
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
217
79
  var import_node_crypto = require("crypto");
218
- var SqliteSettingsBackend = class {
80
+ var import_types2 = require("@camstack/types");
81
+ function parseRowData(raw) {
82
+ return (0, import_types2.asJsonObject)((0, import_types2.parseJsonUnknown)(raw)) ?? {};
83
+ }
84
+ var SqliteSettingsBackend = class _SqliteSettingsBackend {
85
+ // Domain schemas (auth, analytics events, …) live in the addons that
86
+ // own them — same pattern as `pipeline-analytics` (`event-store.ts`,
87
+ // `track-store.ts`, `media-store.ts`) and `local-auth` (`auth-schema.ts`).
88
+ // The backend exposes `declareCollection` and stays out of domain
89
+ // knowledge.
219
90
  constructor(dbPath, runtimeDefaults) {
220
91
  this.dbPath = dbPath;
221
92
  this.runtimeDefaults = runtimeDefaults ?? {};
222
93
  }
94
+ dbPath;
223
95
  db = null;
224
- ensuredTables = /* @__PURE__ */ new Set();
225
96
  structuredTables = /* @__PURE__ */ new Set();
97
+ /** Map from scoped collection name → set of column names (non-id) that
98
+ * the structured schema owns. Routes set/get/insert/update/query to
99
+ * typed columns. Every collection MUST be declared here before use. */
100
+ declaredCollections = /* @__PURE__ */ new Map();
226
101
  runtimeDefaults;
102
+ /**
103
+ * Canonical key/value collections — declared with a `(id TEXT PK,
104
+ * data TEXT NOT NULL)` schema at boot so existing JSON-blob rows
105
+ * keep working through the structured path. Generates SQL identical
106
+ * to the previous legacy path; only the routing is unified.
107
+ */
108
+ static CANONICAL_KV_COLLECTIONS = [
109
+ "system-settings",
110
+ "addon-settings",
111
+ "addon-device-settings",
112
+ "addon-devices",
113
+ "device-runtime-state",
114
+ "sections",
115
+ "provider-settings",
116
+ "device-settings",
117
+ "alerts"
118
+ ];
227
119
  async initialize() {
228
120
  const dir = this.dbPath.substring(0, this.dbPath.lastIndexOf("/"));
229
121
  if (dir) {
230
- const fs3 = await import("fs");
231
- fs3.mkdirSync(dir, { recursive: true });
122
+ const fs = await import("fs");
123
+ fs.mkdirSync(dir, { recursive: true });
232
124
  }
233
125
  this.db = new import_better_sqlite3.default(this.dbPath);
234
126
  this.db.pragma("journal_mode = WAL");
235
127
  this.db.pragma("foreign_keys = ON");
236
- const isEmpty = await this.isEmpty("system-settings");
128
+ for (const collection of _SqliteSettingsBackend.CANONICAL_KV_COLLECTIONS) {
129
+ await this.ensureTable(collection, {
130
+ columns: [
131
+ { name: "id", type: "TEXT", primaryKey: true, notNull: true },
132
+ { name: "data", type: "TEXT", notNull: true }
133
+ ]
134
+ });
135
+ this.declaredCollections.set(collection, {
136
+ primaryKey: "id",
137
+ columns: /* @__PURE__ */ new Set(["data"])
138
+ });
139
+ }
140
+ const isEmpty = await this.isEmpty({ collection: "system-settings" });
237
141
  if (isEmpty) {
238
142
  await this.seedDefaults();
239
143
  }
240
144
  }
145
+ requireDeclared(scoped) {
146
+ const decl = this.declaredCollections.get(scoped);
147
+ if (!decl) {
148
+ throw new Error(`SqliteSettingsBackend: collection "${scoped}" is not declared. Call declareCollection() first or add it to CANONICAL_KV_COLLECTIONS.`);
149
+ }
150
+ return decl;
151
+ }
241
152
  async shutdown() {
242
153
  this.db?.close();
243
154
  this.db = null;
@@ -245,148 +156,255 @@ var SqliteSettingsBackend = class {
245
156
  // ---------------------------------------------------------------------------
246
157
  // ISettingsBackend implementation
247
158
  // ---------------------------------------------------------------------------
248
- async get(collection, key) {
249
- this.ensureCollectionTable(collection);
250
- const row = this.getDb().prepare(`SELECT data FROM "${collection}" WHERE id = ?`).get(key);
159
+ async get({ namespace, collection, key }) {
160
+ const scoped = this.scopedName(namespace, collection);
161
+ const decl = this.requireDeclared(scoped);
162
+ const cols = [`"${decl.primaryKey}"`, ...[...decl.columns].map((c) => `"${c}"`)].join(", ");
163
+ const row = this.getDb().prepare(`SELECT ${cols} FROM "${scoped}" WHERE "${decl.primaryKey}" = ?`).get(key);
251
164
  if (!row) return void 0;
252
- return JSON.parse(row.data);
253
- }
254
- async set(collection, key, value) {
255
- this.ensureCollectionTable(collection);
256
- this.getDb().prepare(`INSERT INTO "${collection}" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`).run(key, JSON.stringify(value));
165
+ if (decl.columns.size === 1 && decl.columns.has("data")) {
166
+ const raw = row["data"];
167
+ return typeof raw === "string" ? JSON.parse(raw) : raw;
168
+ }
169
+ const data = {};
170
+ for (const c of decl.columns) {
171
+ const v = row[c];
172
+ if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
173
+ try {
174
+ data[c] = JSON.parse(v);
175
+ } catch {
176
+ data[c] = v;
177
+ }
178
+ } else {
179
+ data[c] = v ?? null;
180
+ }
181
+ }
182
+ return data;
183
+ }
184
+ async set({ namespace, collection, key, value }) {
185
+ const scoped = this.scopedName(namespace, collection);
186
+ const decl = this.requireDeclared(scoped);
187
+ const row = { [decl.primaryKey]: key };
188
+ if (decl.columns.size === 1 && decl.columns.has("data")) {
189
+ row["data"] = JSON.stringify(value);
190
+ } else {
191
+ const valueObj = value !== null && typeof value === "object" ? value : {};
192
+ for (const [k, v] of Object.entries(valueObj)) {
193
+ if (decl.columns.has(k)) row[k] = this.serializeColumnValue(v);
194
+ }
195
+ }
196
+ const keys = Object.keys(row);
197
+ const cols = keys.map((k) => `"${k}"`).join(", ");
198
+ const placeholders = keys.map(() => "?").join(", ");
199
+ const updates = keys.filter((k) => k !== decl.primaryKey).map((k) => `"${k}" = excluded."${k}"`).join(", ");
200
+ const values = keys.map((k) => row[k]);
201
+ 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`;
202
+ this.getDb().prepare(sql).run(...values);
203
+ }
204
+ async query({ namespace, collection, filter }) {
205
+ const scoped = this.scopedName(namespace, collection);
206
+ const decl = this.requireDeclared(scoped);
207
+ return this.queryDeclared(scoped, decl, filter);
208
+ }
209
+ async insert({ namespace, collection, record }) {
210
+ const scoped = this.scopedName(namespace, collection);
211
+ const decl = this.requireDeclared(scoped);
212
+ const id = record.id || (0, import_node_crypto.randomUUID)();
213
+ const row = { [decl.primaryKey]: id };
214
+ if (decl.columns.size === 1 && decl.columns.has("data")) {
215
+ row["data"] = JSON.stringify(record.data);
216
+ } else {
217
+ for (const [k, v] of Object.entries(record.data)) {
218
+ if (decl.columns.has(k)) row[k] = this.serializeColumnValue(v);
219
+ }
220
+ }
221
+ await this.tableInsert(scoped, row);
222
+ }
223
+ async update({ namespace, collection, id, data }) {
224
+ const scoped = this.scopedName(namespace, collection);
225
+ const decl = this.requireDeclared(scoped);
226
+ const updates = {};
227
+ if (decl.columns.size === 1 && decl.columns.has("data")) {
228
+ updates["data"] = JSON.stringify(data);
229
+ } else {
230
+ for (const [k, v] of Object.entries(data)) {
231
+ if (decl.columns.has(k)) updates[k] = this.serializeColumnValue(v);
232
+ }
233
+ }
234
+ if (Object.keys(updates).length > 0) {
235
+ await this.tableUpdate(scoped, { [decl.primaryKey]: id }, updates);
236
+ }
257
237
  }
258
- async query(collection, filter) {
259
- this.ensureCollectionTable(collection);
260
- let sql = `SELECT id, data FROM "${collection}"`;
238
+ async delete({ namespace, collection, key }) {
239
+ const scoped = this.scopedName(namespace, collection);
240
+ const decl = this.requireDeclared(scoped);
241
+ await this.tableDelete(scoped, { [decl.primaryKey]: key });
242
+ }
243
+ async count({ namespace, collection, filter }) {
244
+ const scoped = this.scopedName(namespace, collection);
245
+ const decl = this.requireDeclared(scoped);
246
+ const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
247
+ const isColumn = (f) => f === decl.primaryKey || decl.columns.has(f);
248
+ const fieldExpr = (f) => {
249
+ if (isColumn(f)) return `"${f}"`;
250
+ if (isKvShape) return `json_extract("data", '$.${f}')`;
251
+ return "";
252
+ };
253
+ let sql = `SELECT COUNT(*) AS cnt FROM "${scoped}"`;
254
+ const params = [];
255
+ if (filter?.where) {
256
+ const clauses = [];
257
+ for (const [field, value] of Object.entries(filter.where)) {
258
+ const expr = fieldExpr(field);
259
+ if (!expr) continue;
260
+ clauses.push(`${expr} = ?`);
261
+ params.push(this.serializeColumnValue(value));
262
+ }
263
+ if (clauses.length > 0) sql += ` WHERE ${clauses.join(" AND ")}`;
264
+ }
265
+ const row = this.getDb().prepare(sql).get(...params);
266
+ return row?.cnt ?? 0;
267
+ }
268
+ async isEmpty({ namespace, collection }) {
269
+ const scoped = this.scopedName(namespace, collection);
270
+ this.requireDeclared(scoped);
271
+ return await this.tableCount(scoped) === 0;
272
+ }
273
+ async queryDeclared(table, decl, filter) {
274
+ const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
275
+ const cols = [`"${decl.primaryKey}"`, ...[...decl.columns].map((c) => `"${c}"`)].join(", ");
276
+ let sql = `SELECT ${cols} FROM "${table}"`;
261
277
  const params = [];
262
278
  const whereClauses = [];
279
+ const isColumn = (f) => f === decl.primaryKey || decl.columns.has(f);
280
+ const fieldExpr = (f) => {
281
+ if (isColumn(f)) return `"${f}"`;
282
+ if (isKvShape) return `json_extract("data", '$.${f}')`;
283
+ return "";
284
+ };
263
285
  if (filter?.where) {
264
286
  for (const [field, value] of Object.entries(filter.where)) {
265
- if (field === "id") {
266
- whereClauses.push("id = ?");
267
- params.push(value);
268
- } else {
269
- whereClauses.push(`json_extract(data, '$.${field}') = ?`);
270
- params.push(typeof value === "object" ? JSON.stringify(value) : value);
271
- }
287
+ const expr = fieldExpr(field);
288
+ if (!expr) continue;
289
+ whereClauses.push(`${expr} = ?`);
290
+ params.push(this.serializeColumnValue(value));
272
291
  }
273
292
  }
274
293
  if (filter?.whereIn) {
275
294
  for (const [field, values] of Object.entries(filter.whereIn)) {
295
+ const expr = fieldExpr(field);
296
+ if (!expr) continue;
276
297
  const placeholders = values.map(() => "?").join(", ");
277
- if (field === "id") {
278
- whereClauses.push(`id IN (${placeholders})`);
279
- } else {
280
- whereClauses.push(`json_extract(data, '$.${field}') IN (${placeholders})`);
281
- }
282
- params.push(...values);
298
+ whereClauses.push(`${expr} IN (${placeholders})`);
299
+ for (const v of values) params.push(this.serializeColumnValue(v));
283
300
  }
284
301
  }
285
302
  if (filter?.whereBetween) {
286
303
  for (const [field, [low, high]] of Object.entries(filter.whereBetween)) {
287
- if (field === "id") {
288
- whereClauses.push("id BETWEEN ? AND ?");
289
- } else {
290
- whereClauses.push(`json_extract(data, '$.${field}') BETWEEN ? AND ?`);
291
- }
292
- params.push(low, high);
304
+ const expr = fieldExpr(field);
305
+ if (!expr) continue;
306
+ whereClauses.push(`${expr} BETWEEN ? AND ?`);
307
+ params.push(this.serializeColumnValue(low), this.serializeColumnValue(high));
293
308
  }
294
309
  }
295
- if (whereClauses.length > 0) {
296
- sql += ` WHERE ${whereClauses.join(" AND ")}`;
297
- }
310
+ if (whereClauses.length > 0) sql += ` WHERE ${whereClauses.join(" AND ")}`;
298
311
  if (filter?.orderBy) {
299
- const dir = filter.orderBy.direction === "desc" ? "DESC" : "ASC";
300
- if (filter.orderBy.field === "id") {
301
- sql += ` ORDER BY id ${dir}`;
302
- } else {
303
- sql += ` ORDER BY json_extract(data, '$.${filter.orderBy.field}') ${dir}`;
312
+ const expr = fieldExpr(filter.orderBy.field);
313
+ if (expr) {
314
+ const dir = filter.orderBy.direction === "desc" ? "DESC" : "ASC";
315
+ sql += ` ORDER BY ${expr} ${dir}`;
304
316
  }
305
317
  }
306
- if (filter?.limit) {
318
+ if (filter?.limit !== void 0) {
307
319
  sql += ` LIMIT ?`;
308
320
  params.push(filter.limit);
309
321
  }
310
- if (filter?.offset) {
322
+ if (filter?.offset !== void 0) {
311
323
  sql += ` OFFSET ?`;
312
324
  params.push(filter.offset);
313
325
  }
314
326
  const rows = this.getDb().prepare(sql).all(...params);
315
- return rows.map((row) => ({
316
- id: row.id,
317
- data: JSON.parse(row.data)
318
- }));
319
- }
320
- async insert(collection, record) {
321
- this.ensureCollectionTable(collection);
322
- const id = record.id || (0, import_node_crypto.randomUUID)();
323
- this.getDb().prepare(`INSERT INTO "${collection}" (id, data) VALUES (?, ?)`).run(id, JSON.stringify(record.data));
324
- }
325
- async update(collection, id, data) {
326
- this.ensureCollectionTable(collection);
327
- this.getDb().prepare(`UPDATE "${collection}" SET data = ? WHERE id = ?`).run(JSON.stringify(data), id);
328
- }
329
- async delete(collection, key) {
330
- this.ensureCollectionTable(collection);
331
- this.getDb().prepare(`DELETE FROM "${collection}" WHERE id = ?`).run(key);
332
- }
333
- async count(collection, filter) {
334
- this.ensureCollectionTable(collection);
335
- if (!filter) {
336
- const row = this.getDb().prepare(`SELECT COUNT(*) AS cnt FROM "${collection}"`).get();
337
- return row?.cnt ?? 0;
338
- }
339
- const rows = await this.query(collection, { ...filter, limit: void 0, offset: void 0 });
340
- return rows.length;
341
- }
342
- async isEmpty(collection) {
343
- this.ensureCollectionTable(collection);
344
- const row = this.getDb().prepare(`SELECT COUNT(*) AS cnt FROM "${collection}"`).get();
345
- return (row?.cnt ?? 0) === 0;
327
+ return rows.map((r) => {
328
+ const id = String(r[decl.primaryKey] ?? "");
329
+ if (isKvShape) {
330
+ const v = r["data"];
331
+ if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
332
+ try {
333
+ return { id, data: JSON.parse(v) };
334
+ } catch {
335
+ return { id, data: { value: v } };
336
+ }
337
+ }
338
+ return { id, data: v == null ? {} : { value: v } };
339
+ }
340
+ const data = { [decl.primaryKey]: r[decl.primaryKey] ?? null };
341
+ for (const c of decl.columns) {
342
+ const v = r[c];
343
+ if (typeof v === "string" && (v.startsWith("{") || v.startsWith("["))) {
344
+ try {
345
+ data[c] = JSON.parse(v);
346
+ } catch {
347
+ data[c] = v;
348
+ }
349
+ } else {
350
+ data[c] = v ?? null;
351
+ }
352
+ }
353
+ return { id, data };
354
+ });
346
355
  }
347
356
  // ---------------------------------------------------------------------------
348
357
  // Legacy SettingsStore compatibility (used by ConfigManager.setSettingsStore)
349
358
  // ---------------------------------------------------------------------------
350
359
  /** Get a system setting by dot-notation key */
351
360
  getSystem(key) {
352
- this.ensureCollectionTable("system-settings");
361
+ this.requireDeclared("system-settings");
353
362
  const row = this.getDb().prepare('SELECT data FROM "system-settings" WHERE id = ?').get(key);
354
363
  if (!row) return void 0;
355
364
  return JSON.parse(row.data);
356
365
  }
357
366
  /** Set a system setting */
358
367
  setSystem(key, value) {
359
- this.ensureCollectionTable("system-settings");
368
+ this.requireDeclared("system-settings");
360
369
  this.getDb().prepare('INSERT INTO "system-settings" (id, data) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data').run(key, JSON.stringify(value));
361
370
  }
362
371
  /** Get all system settings as flat key-value */
363
372
  getAllSystem() {
364
- this.ensureCollectionTable("system-settings");
373
+ this.requireDeclared("system-settings");
365
374
  const rows = this.getDb().prepare('SELECT id, data FROM "system-settings"').all();
366
375
  return Object.fromEntries(rows.map((r) => [r.id, JSON.parse(r.data)]));
367
376
  }
368
377
  /** Get all settings for an addon */
369
378
  getAllAddon(addonId) {
370
- this.ensureCollectionTable("addon-settings");
379
+ this.requireDeclared("addon-settings");
371
380
  const rows = this.getDb().prepare(`SELECT id, data FROM "addon-settings" WHERE json_extract(data, '$.addonId') = ?`).all(addonId);
372
381
  if (rows.length === 0) return {};
373
382
  const result = {};
374
383
  for (const row of rows) {
375
- const parsed = JSON.parse(row.data);
384
+ const parsed = parseRowData(row.data);
376
385
  const key = row.id.startsWith(`${addonId}.`) ? row.id.slice(addonId.length + 1) : row.id;
377
- result[key] = parsed.value ?? parsed;
386
+ const isWrapper = parsed !== null && typeof parsed === "object" && "addonId" in parsed && "key" in parsed;
387
+ if (isWrapper) {
388
+ const wrapper = parsed;
389
+ if ("value" in wrapper) {
390
+ result[key] = wrapper.value;
391
+ }
392
+ } else {
393
+ result[key] = parsed;
394
+ }
378
395
  }
379
396
  return result;
380
397
  }
381
398
  /** Bulk-set all settings for an addon */
382
399
  setAllAddon(addonId, config) {
383
- this.ensureCollectionTable("addon-settings");
400
+ this.requireDeclared("addon-settings");
384
401
  const db = this.getDb();
385
402
  const deleteStmt = db.prepare(`DELETE FROM "addon-settings" WHERE id LIKE ? || '%'`);
386
403
  const insertStmt = db.prepare('INSERT INTO "addon-settings" (id, data) VALUES (?, ?)');
387
404
  db.transaction(() => {
388
405
  deleteStmt.run(`${addonId}.`);
389
406
  for (const [key, value] of Object.entries(config)) {
407
+ if (value === void 0) continue;
390
408
  insertStmt.run(`${addonId}.${key}`, JSON.stringify({ addonId, key, value }));
391
409
  }
392
410
  })();
@@ -407,9 +425,41 @@ var SqliteSettingsBackend = class {
407
425
  setDevice(deviceId, key, value) {
408
426
  this.setScopedKey("device-settings", deviceId, key, value);
409
427
  }
428
+ // ── Addon-device settings (per-device overrides of an addon's config) ──
429
+ //
430
+ // Storage key format: "<addonId>:<deviceId>.<field>" inside the
431
+ // "addon-device-settings" collection. Uses the same JSON-blob layout
432
+ // as the other scoped collections so no schema change is required.
433
+ getAddonDevice(addonId, deviceId) {
434
+ return this.getAllScoped("addon-device-settings", `${addonId}:${deviceId}`);
435
+ }
436
+ setAddonDevice(addonId, deviceId, values) {
437
+ this.requireDeclared("addon-device-settings");
438
+ const db = this.getDb();
439
+ const prefix = `${addonId}:${deviceId}.`;
440
+ const deleteStmt = db.prepare(`DELETE FROM "addon-device-settings" WHERE id LIKE ? || '%'`);
441
+ const insertStmt = db.prepare(
442
+ `INSERT INTO "addon-device-settings" (id, data) VALUES (?, ?)
443
+ ON CONFLICT(id) DO UPDATE SET data = excluded.data`
444
+ );
445
+ db.transaction(() => {
446
+ deleteStmt.run(prefix);
447
+ for (const [key, value] of Object.entries(values)) {
448
+ insertStmt.run(
449
+ `${prefix}${key}`,
450
+ JSON.stringify({ addonId, deviceId, key, value })
451
+ );
452
+ }
453
+ })();
454
+ }
455
+ clearAddonDevice(addonId, deviceId) {
456
+ this.requireDeclared("addon-device-settings");
457
+ const prefix = `${addonId}:${deviceId}.`;
458
+ this.getDb().prepare(`DELETE FROM "addon-device-settings" WHERE id LIKE ? || '%'`).run(prefix);
459
+ }
410
460
  /** Seed system-settings with runtime defaults (first boot) */
411
461
  async seedDefaults() {
412
- this.ensureCollectionTable("system-settings");
462
+ this.requireDeclared("system-settings");
413
463
  const insert = this.getDb().prepare(
414
464
  'INSERT OR IGNORE INTO "system-settings" (id, data) VALUES (?, ?)'
415
465
  );
@@ -422,41 +472,73 @@ var SqliteSettingsBackend = class {
422
472
  // ---------------------------------------------------------------------------
423
473
  // Private helpers
424
474
  // ---------------------------------------------------------------------------
475
+ /**
476
+ * Expose the raw better-sqlite3 Database instance for components that
477
+ * need direct SQL access (e.g. DeviceStore, ConfigStore).
478
+ * Returns null if the backend has not been initialized yet.
479
+ */
480
+ getDatabase() {
481
+ return this.db;
482
+ }
425
483
  getDb() {
426
484
  if (!this.db) throw new Error("SqliteSettingsBackend not initialized \u2014 call initialize() first");
427
485
  return this.db;
428
486
  }
429
- ensureCollectionTable(collection) {
430
- if (this.ensuredTables.has(collection)) return;
431
- this.getDb().exec(
432
- `CREATE TABLE IF NOT EXISTS "${collection}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`
433
- );
434
- this.ensuredTables.add(collection);
435
- }
436
487
  getAllScoped(collection, scopeId) {
437
- this.ensureCollectionTable(collection);
488
+ this.requireDeclared(collection);
438
489
  const rows = this.getDb().prepare(`SELECT id, data FROM "${collection}" WHERE id LIKE ? || '.%'`).all(scopeId);
439
490
  const result = {};
440
491
  for (const row of rows) {
441
492
  const key = row.id.slice(scopeId.length + 1);
442
- const parsed = JSON.parse(row.data);
493
+ const parsed = parseRowData(row.data);
443
494
  result[key] = parsed.value ?? parsed;
444
495
  }
445
496
  return result;
446
497
  }
447
498
  setScopedKey(collection, scopeId, key, value) {
448
- this.ensureCollectionTable(collection);
499
+ this.requireDeclared(collection);
449
500
  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 }));
450
501
  }
502
+ // ── Declared collections (typed SQL tables behind the generic cap) ──
503
+ scopedName(namespace, collection) {
504
+ return namespace ? `${namespace}:${collection}` : collection;
505
+ }
506
+ async declareCollection(input) {
507
+ const table = this.scopedName(input.namespace, input.collection);
508
+ if (this.declaredCollections.has(table)) return;
509
+ const hasId = input.columns.some((c) => c.primaryKey === true);
510
+ const columns = hasId ? input.columns : [{ name: "id", type: "TEXT", primaryKey: true, notNull: true }, ...input.columns];
511
+ const schema = {
512
+ columns: columns.map((c) => ({
513
+ name: c.name,
514
+ // SQLite treats JSON as TEXT; we serialize object values on write
515
+ // and parse on read.
516
+ type: c.type === "JSON" ? "TEXT" : c.type,
517
+ ...c.primaryKey !== void 0 ? { primaryKey: c.primaryKey } : {},
518
+ ...c.notNull !== void 0 ? { notNull: c.notNull } : {},
519
+ ...c.unique !== void 0 ? { unique: c.unique } : {}
520
+ })),
521
+ ...input.indexes ? { indexes: input.indexes.map((i) => ({
522
+ name: i.name,
523
+ columns: i.columns,
524
+ ...i.unique !== void 0 ? { unique: i.unique } : {}
525
+ })) } : {}
526
+ };
527
+ await this.ensureTable(table, schema);
528
+ const pkCol = columns.find((c) => c.primaryKey === true);
529
+ const primaryKey = pkCol ? pkCol.name : "id";
530
+ const columnNames = new Set(columns.filter((c) => c.name !== primaryKey).map((c) => c.name));
531
+ this.declaredCollections.set(table, { primaryKey, columns: columnNames });
532
+ }
533
+ /** Serialise per-column values for SQL binding: objects → JSON, booleans → 0/1. */
534
+ serializeColumnValue(v) {
535
+ if (v === null || v === void 0) return v ?? null;
536
+ if (typeof v === "boolean") return v ? 1 : 0;
537
+ if (typeof v === "object") return JSON.stringify(v);
538
+ return v;
539
+ }
451
540
  // ── Structured table operations ────────────────────────────────────
452
541
  async ensureTable(table, schema) {
453
- if (!schema) {
454
- if (!this.ensuredTables.has(table)) {
455
- this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`);
456
- this.ensuredTables.add(table);
457
- }
458
- return;
459
- }
460
542
  if (this.structuredTables.has(table)) return;
461
543
  const colDefs = schema.columns.map((col) => {
462
544
  const parts = [`"${col.name}" ${col.type}`];
@@ -468,6 +550,19 @@ var SqliteSettingsBackend = class {
468
550
  }
469
551
  return parts.join(" ");
470
552
  });
553
+ const existingCols = this.getDb().prepare(`PRAGMA table_info("${table}")`).all();
554
+ if (existingCols.length > 0) {
555
+ const existingNames = new Set(existingCols.map((c) => c.name));
556
+ const declaredNames = new Set(schema.columns.map((c) => c.name));
557
+ const missingDeclared = schema.columns.some((c) => !existingNames.has(c.name));
558
+ const isLegacyKv = existingCols.length <= 2 && existingNames.has("data") && !declaredNames.has("data");
559
+ if (isLegacyKv || missingDeclared) {
560
+ try {
561
+ this.getDb().exec(`DROP TABLE "${table}"`);
562
+ } catch {
563
+ }
564
+ }
565
+ }
471
566
  this.getDb().exec(`CREATE TABLE IF NOT EXISTS "${table}" (${colDefs.join(", ")})`);
472
567
  if (schema.indexes) {
473
568
  for (const idx of schema.indexes) {
@@ -523,12 +618,13 @@ var SqliteSettingsBackend = class {
523
618
  sql += ` OFFSET ?`;
524
619
  values.push(options.offset);
525
620
  }
526
- return this.getDb().prepare(sql).all(...values);
621
+ const rows = this.getDb().prepare(sql).all(...values);
622
+ return rows.flatMap((r) => (0, import_types2.asJsonObject)(r) ? [(0, import_types2.asJsonObject)(r)] : []);
527
623
  }
528
624
  async tableGet(table, filter) {
529
625
  const { whereSql, whereValues } = this.buildWhere(filter);
530
626
  const row = this.getDb().prepare(`SELECT * FROM "${table}"${whereSql} LIMIT 1`).get(...whereValues);
531
- return row ?? null;
627
+ return (0, import_types2.asJsonObject)(row);
532
628
  }
533
629
  async tableCount(table, filter) {
534
630
  let sql = `SELECT COUNT(*) as count FROM "${table}"`;
@@ -538,8 +634,8 @@ var SqliteSettingsBackend = class {
538
634
  sql += whereSql;
539
635
  values.push(...whereValues);
540
636
  }
541
- const row = this.getDb().prepare(sql).get(...values);
542
- return row.count;
637
+ const row = (0, import_types2.asJsonObject)(this.getDb().prepare(sql).get(...values));
638
+ return typeof row?.count === "number" ? row.count : 0;
543
639
  }
544
640
  buildWhere(filter) {
545
641
  const conditions = [];
@@ -554,332 +650,49 @@ var SqliteSettingsBackend = class {
554
650
  };
555
651
 
556
652
  // src/builtins/sqlite-storage/sqlite-settings.addon.ts
557
- var SqliteSettingsAddon = class {
558
- manifest = {
559
- id: "sqlite-settings",
560
- name: "SQLite Settings",
561
- version: "1.0.0",
562
- capabilities: [{ name: "settings-store", mode: "singleton" }]
563
- };
653
+ var import_types3 = require("@camstack/types");
654
+ var SqliteSettingsAddon = class extends import_types3.BaseAddon {
564
655
  backend = null;
565
- async initialize(context) {
566
- 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";
567
- const runtimeDefaults = context.addonConfig._runtimeDefaults ?? {};
568
- this.backend = new SqliteSettingsBackend(dbPath, runtimeDefaults);
656
+ constructor() {
657
+ super({});
658
+ }
659
+ async onInitialize() {
660
+ const addonId = this.ctx.id.replace("addon:", "");
661
+ let pathSource = "fallback (hardcoded)";
662
+ const dbPath = await this.ctx.api.storage.resolve.query({ location: "addons-data", relativePath: `${addonId}/camstack.db` }).then((p) => {
663
+ pathSource = "storage capability (addons-data)";
664
+ return p;
665
+ }).catch(() => {
666
+ if (this.ctx.dataDir) {
667
+ pathSource = "dataDir";
668
+ return `${this.ctx.dataDir}/camstack.db`;
669
+ }
670
+ return "camstack-data/addons-data/sqlite-settings/camstack.db";
671
+ });
672
+ let dbExists = false;
673
+ try {
674
+ const fs = await import("fs");
675
+ dbExists = fs.existsSync(dbPath);
676
+ } catch (err) {
677
+ this.ctx.logger.warn("Failed to check DB file existence", { meta: { error: (0, import_types3.errMsg)(err) } });
678
+ }
679
+ this.ctx.logger.info("DB path resolved", { meta: { pathSource, dbPath } });
680
+ this.ctx.logger.info("DB file status", { meta: { dbExists } });
681
+ this.backend = new SqliteSettingsBackend(dbPath, { ...import_types3.RUNTIME_DEFAULTS });
569
682
  await this.backend.initialize();
570
- context.logger.info(`SQLite settings initialized at ${dbPath}`);
683
+ this.ctx.logger.info("Initialized successfully");
684
+ return [{ capability: import_types3.settingsStoreCapability, provider: this.backend }];
571
685
  }
572
- async shutdown() {
686
+ async onShutdown() {
573
687
  await this.backend?.shutdown();
574
688
  }
575
689
  getBackend() {
576
690
  return this.backend;
577
691
  }
578
- getCapabilityProvider(name) {
579
- if (name === "settings-store" && this.backend) {
580
- return this.backend;
581
- }
582
- return null;
583
- }
584
- getConfigSchema() {
585
- return { sections: [] };
586
- }
587
- getConfig() {
588
- return {};
589
- }
590
- async onConfigChange(_config) {
591
- }
592
- };
593
-
594
- // src/builtins/sqlite-storage/sqlite-storage.provider.ts
595
- var import_better_sqlite32 = __toESM(require("better-sqlite3"));
596
- var fs2 = __toESM(require("fs"));
597
- var path2 = __toESM(require("path"));
598
- var import_node_crypto2 = require("crypto");
599
- var LOCATION_TYPES = {
600
- // New location names (from StorageLocationManager)
601
- data: "structured",
602
- // settings, events, trails — SQL only
603
- media: "files",
604
- // crops, snapshots, thumbnails — files only
605
- recordings: "files",
606
- // video segments — files only
607
- models: "files",
608
- // ONNX/TFLite models — files only
609
- cache: "files",
610
- // temp files — files only
611
- logs: "files",
612
- // Winston log files — files only
613
- // Legacy location names (backward compat)
614
- config: "structured",
615
- events: "structured",
616
- addon: "both"
617
- };
618
- var SqliteStructuredStorage = class {
619
- constructor(db) {
620
- this.db = db;
621
- }
622
- ensuredTables = /* @__PURE__ */ new Set();
623
- ensureTable(collection) {
624
- if (this.ensuredTables.has(collection)) return;
625
- this.db.exec(
626
- `CREATE TABLE IF NOT EXISTS "${collection}" (id TEXT PRIMARY KEY, data TEXT)`
627
- );
628
- this.ensuredTables.add(collection);
629
- }
630
- async insert(record) {
631
- this.ensureTable(record.collection);
632
- const id = record.id || (0, import_node_crypto2.randomUUID)();
633
- const newRecord = {
634
- collection: record.collection,
635
- id,
636
- data: record.data
637
- };
638
- this.db.prepare(`INSERT INTO "${record.collection}" (id, data) VALUES (?, ?)`).run(id, JSON.stringify(newRecord.data));
639
- return newRecord;
640
- }
641
- async query(collection, filter) {
642
- this.ensureTable(collection);
643
- const { sql, params } = this.buildSelect(collection, filter);
644
- const rows = this.db.prepare(sql).all(...params);
645
- return rows.map((row) => ({
646
- collection,
647
- id: row.id,
648
- data: JSON.parse(row.data)
649
- }));
650
- }
651
- async update(collection, id, data) {
652
- this.ensureTable(collection);
653
- this.db.prepare(`UPDATE "${collection}" SET data = ? WHERE id = ?`).run(JSON.stringify(data), id);
654
- return { collection, id, data };
655
- }
656
- async delete(collection, id) {
657
- this.ensureTable(collection);
658
- this.db.prepare(`DELETE FROM "${collection}" WHERE id = ?`).run(id);
659
- }
660
- async count(collection, filter) {
661
- this.ensureTable(collection);
662
- const { sql, params } = this.buildCount(collection, filter);
663
- const row = this.db.prepare(sql).get(...params);
664
- return row.cnt;
665
- }
666
- buildWhereClause(filter) {
667
- if (!filter) return { clause: "", params: [] };
668
- const conditions = [];
669
- const params = [];
670
- if (filter.where) {
671
- for (const [field, value] of Object.entries(filter.where)) {
672
- if (field === "id") {
673
- conditions.push("id = ?");
674
- params.push(value);
675
- } else {
676
- conditions.push(`json_extract(data, '$.${field}') = ?`);
677
- params.push(value);
678
- }
679
- }
680
- }
681
- if (filter.whereIn) {
682
- for (const [field, values] of Object.entries(filter.whereIn)) {
683
- const placeholders = values.map(() => "?").join(", ");
684
- if (field === "id") {
685
- conditions.push(`id IN (${placeholders})`);
686
- } else {
687
- conditions.push(`json_extract(data, '$.${field}') IN (${placeholders})`);
688
- }
689
- params.push(...values);
690
- }
691
- }
692
- if (filter.whereBetween) {
693
- for (const [field, [low, high]] of Object.entries(filter.whereBetween)) {
694
- if (field === "id") {
695
- conditions.push("id BETWEEN ? AND ?");
696
- } else {
697
- conditions.push(`json_extract(data, '$.${field}') BETWEEN ? AND ?`);
698
- }
699
- params.push(low, high);
700
- }
701
- }
702
- const clause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
703
- return { clause, params };
704
- }
705
- buildSelect(collection, filter) {
706
- const { clause, params } = this.buildWhereClause(filter);
707
- let sql = `SELECT id, data FROM "${collection}"${clause}`;
708
- if (filter?.orderBy) {
709
- const dir = filter.orderBy.direction === "desc" ? "DESC" : "ASC";
710
- if (filter.orderBy.field === "id") {
711
- sql += ` ORDER BY id ${dir}`;
712
- } else {
713
- sql += ` ORDER BY json_extract(data, '$.${filter.orderBy.field}') ${dir}`;
714
- }
715
- }
716
- if (filter?.limit !== void 0) {
717
- sql += ` LIMIT ?`;
718
- params.push(filter.limit);
719
- }
720
- if (filter?.offset !== void 0) {
721
- sql += ` OFFSET ?`;
722
- params.push(filter.offset);
723
- }
724
- return { sql, params };
725
- }
726
- buildCount(collection, filter) {
727
- const { clause, params } = this.buildWhereClause(filter);
728
- return { sql: `SELECT COUNT(*) as cnt FROM "${collection}"${clause}`, params };
729
- }
730
- };
731
- var FileSystemStorage = class {
732
- constructor(basePath) {
733
- this.basePath = basePath;
734
- }
735
- async readFile(filePath) {
736
- const fullPath = path2.join(this.basePath, filePath);
737
- return fs2.promises.readFile(fullPath);
738
- }
739
- async writeFile(filePath, data) {
740
- const fullPath = path2.join(this.basePath, filePath);
741
- fs2.mkdirSync(path2.dirname(fullPath), { recursive: true });
742
- await fs2.promises.writeFile(fullPath, data);
743
- }
744
- async deleteFile(filePath) {
745
- const fullPath = path2.join(this.basePath, filePath);
746
- await fs2.promises.unlink(fullPath);
747
- }
748
- async listFiles(prefix) {
749
- const searchDir = prefix ? path2.join(this.basePath, prefix) : this.basePath;
750
- try {
751
- const entries = await fs2.promises.readdir(searchDir, { recursive: true });
752
- const files = [];
753
- for (const entry of entries) {
754
- const entryStr = String(entry);
755
- const relative = prefix ? path2.join(prefix, entryStr) : entryStr;
756
- const fullPath = path2.join(this.basePath, relative);
757
- try {
758
- const stat = await fs2.promises.stat(fullPath);
759
- if (stat.isFile()) {
760
- files.push(relative);
761
- }
762
- } catch {
763
- }
764
- }
765
- return files;
766
- } catch {
767
- return [];
768
- }
769
- }
770
- async getFileUrl(_path) {
771
- return null;
772
- }
773
- async exists(filePath) {
774
- const fullPath = path2.join(this.basePath, filePath);
775
- try {
776
- await fs2.promises.access(fullPath, fs2.constants.F_OK);
777
- return true;
778
- } catch {
779
- return false;
780
- }
781
- }
782
- };
783
- var SqliteStorageProvider = class {
784
- mainDb = null;
785
- sharedStructured = null;
786
- locations = /* @__PURE__ */ new Map();
787
- async initialize() {
788
- }
789
- /**
790
- * Configure all storage locations.
791
- * ONE single SQLite database (camstack.db) is used for ALL structured storage.
792
- * File-based locations use the filesystem at their configured path.
793
- */
794
- async configure(config) {
795
- const dataPath = config.locations["data"] ?? config.locations["config"] ?? Object.values(config.locations)[0];
796
- if (!dataPath) throw new Error("No data path configured for SQLite storage");
797
- fs2.mkdirSync(dataPath, { recursive: true });
798
- const dbPath = path2.join(dataPath, "camstack.db");
799
- this.mainDb = new import_better_sqlite32.default(dbPath);
800
- this.mainDb.pragma("journal_mode = WAL");
801
- this.sharedStructured = new SqliteStructuredStorage(this.mainDb);
802
- for (const [name, dirPath] of Object.entries(config.locations)) {
803
- const locationName = name;
804
- const locationType = LOCATION_TYPES[name] ?? "files";
805
- fs2.mkdirSync(dirPath, { recursive: true });
806
- const location = {};
807
- if (locationType === "structured" || locationType === "both") {
808
- location.structured = this.sharedStructured;
809
- }
810
- if (locationType === "files" || locationType === "both") {
811
- location.files = new FileSystemStorage(dirPath);
812
- }
813
- this.locations.set(locationName, location);
814
- }
815
- }
816
- getLocation(name) {
817
- const location = this.locations.get(name);
818
- if (!location) {
819
- throw new Error(`Storage location "${name}" not found`);
820
- }
821
- return location;
822
- }
823
- async shutdown() {
824
- if (this.mainDb) {
825
- this.mainDb.close();
826
- this.mainDb = null;
827
- this.sharedStructured = null;
828
- }
829
- this.locations.clear();
830
- }
831
- async export(_locationName) {
832
- throw new Error("Export not yet implemented");
833
- }
834
- async import(_locationName, _data) {
835
- throw new Error("Import not yet implemented");
836
- }
837
- };
838
-
839
- // src/builtins/sqlite-storage/sqlite-storage.addon.ts
840
- var SqliteStorageAddon = class {
841
- manifest = {
842
- id: "sqlite-storage",
843
- name: "SQLite Storage",
844
- version: "1.0.0",
845
- capabilities: ["storage"]
846
- };
847
- provider = null;
848
- async initialize(context) {
849
- const storageConfig = {
850
- locations: { ...context.locationPaths }
851
- };
852
- this.provider = new SqliteStorageProvider();
853
- await this.provider.configure(storageConfig);
854
- context.logger.info("SQLite storage initialized");
855
- }
856
- async shutdown() {
857
- await this.provider?.shutdown();
858
- }
859
- getProvider() {
860
- if (!this.provider) throw new Error("SQLite storage not initialized");
861
- return this.provider;
862
- }
863
- getCapabilityProvider(name) {
864
- if (name === "storage" && this.provider) {
865
- return this.provider;
866
- }
867
- return null;
868
- }
869
- getConfigSchema() {
870
- return {
871
- sections: []
872
- };
873
- }
874
- getConfig() {
875
- return {};
876
- }
877
- async onConfigChange(_config) {
878
- }
879
692
  };
880
693
 
881
694
  // src/builtins/sqlite-storage/settings-store.ts
882
- var import_better_sqlite33 = __toESM(require("better-sqlite3"));
695
+ var import_better_sqlite32 = __toESM(require("better-sqlite3"));
883
696
 
884
697
  // src/builtins/sqlite-storage/sql-schema.ts
885
698
  var CORE_TABLE_DDL = [
@@ -910,45 +723,59 @@ var CORE_TABLE_DDL = [
910
723
  updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
911
724
  PRIMARY KEY (device_id, key)
912
725
  )`,
913
- // Detection events
914
- `CREATE TABLE IF NOT EXISTS detection_events (
915
- id TEXT PRIMARY KEY,
916
- timestamp INTEGER NOT NULL,
917
- device_id TEXT NOT NULL,
918
- class_name TEXT NOT NULL,
919
- score REAL NOT NULL,
920
- severity TEXT NOT NULL,
921
- track_id TEXT,
922
- zones JSON,
923
- recognition JSON,
924
- media_files JSON,
925
- data JSON
926
- )`,
927
- `CREATE INDEX IF NOT EXISTS idx_det_device_ts ON detection_events(device_id, timestamp)`,
928
- `CREATE INDEX IF NOT EXISTS idx_det_class_ts ON detection_events(class_name, timestamp)`,
929
- // Audio levels
930
- `CREATE TABLE IF NOT EXISTS audio_levels (
931
- id TEXT PRIMARY KEY,
932
- timestamp INTEGER NOT NULL,
933
- device_id TEXT NOT NULL,
934
- dbfs REAL NOT NULL,
935
- rms REAL NOT NULL,
936
- state TEXT NOT NULL
937
- )`,
938
- `CREATE INDEX IF NOT EXISTS idx_audio_device_ts ON audio_levels(device_id, timestamp)`,
939
- // Track trails
940
- `CREATE TABLE IF NOT EXISTS track_trails (
941
- track_id TEXT PRIMARY KEY,
726
+ /**
727
+ * Per-device overrides of an addon's settings (scope: 'device' fields).
728
+ * Resolution chain: schema default -> addon_settings (global) -> this.
729
+ */
730
+ `CREATE TABLE IF NOT EXISTS addon_device_settings (
731
+ addon_id TEXT NOT NULL,
942
732
  device_id TEXT NOT NULL,
943
- class_name TEXT NOT NULL,
944
- first_seen INTEGER NOT NULL,
945
- last_seen INTEGER NOT NULL,
946
- positions JSON NOT NULL,
947
- snapshots JSON,
948
- total_distance REAL,
949
- zones_visited JSON
733
+ key TEXT NOT NULL,
734
+ value JSON NOT NULL,
735
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
736
+ PRIMARY KEY (addon_id, device_id, key)
950
737
  )`,
951
- `CREATE INDEX IF NOT EXISTS idx_trails_device_ts ON track_trails(device_id, first_seen)`
738
+ // P12b sweep: removed static DDL for `detection_events`, `audio_levels`,
739
+ // `track_trails` — these were consumed exclusively by the old
740
+ // analytics sub-addon (deleted) and its analysis-data-persistence cap.
741
+ // addon-pipeline-analytics now owns all detection/audio/track tables
742
+ // via `declareCollection` (pipeline-analytics:motion-events,
743
+ // :object-events, :audio-events, :tracks, :media).
744
+ // Device registry
745
+ `CREATE TABLE IF NOT EXISTS devices (
746
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
747
+ addon_id TEXT NOT NULL,
748
+ stable_id TEXT NOT NULL,
749
+ type TEXT NOT NULL,
750
+ name TEXT NOT NULL,
751
+ parent_stable_id TEXT,
752
+ role TEXT,
753
+ enabled INTEGER DEFAULT 1,
754
+ created_at TEXT DEFAULT (datetime('now')),
755
+ UNIQUE(addon_id, stable_id)
756
+ )`,
757
+ `CREATE INDEX IF NOT EXISTS idx_devices_addon ON devices(addon_id)`,
758
+ `CREATE INDEX IF NOT EXISTS idx_devices_parent ON devices(addon_id, parent_stable_id)`,
759
+ // Addon / device config store
760
+ // stable_id IS NULL -> addon-global row (one per addon_id)
761
+ // stable_id IS NOT NULL -> per-device row (one per addon_id + stable_id pair)
762
+ // Two partial unique indexes enforce the constraint because SQLite does not
763
+ // support expressions inside UNIQUE(...) declarations.
764
+ `CREATE TABLE IF NOT EXISTS addon_config (
765
+ addon_id TEXT NOT NULL,
766
+ stable_id TEXT,
767
+ data TEXT NOT NULL DEFAULT '{}',
768
+ updated_at TEXT DEFAULT (datetime('now'))
769
+ )`,
770
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_addon_config_global ON addon_config(addon_id) WHERE stable_id IS NULL`,
771
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_addon_config_device ON addon_config(addon_id, stable_id) WHERE stable_id IS NOT NULL`,
772
+ `CREATE INDEX IF NOT EXISTS idx_addon_config_addon ON addon_config(addon_id)`
773
+ ];
774
+ var CORE_TABLE_MIGRATIONS = [
775
+ // 2026-04 — DeviceRole support: optional role hint per device, used by
776
+ // admin UI to pick icons/widgets for accessory children (siren,
777
+ // floodlight, PIR, chime, doorbell, …). Nullable; existing rows stay.
778
+ `ALTER TABLE devices ADD COLUMN role TEXT`
952
779
  ];
953
780
  function addonTableToDdl(schema) {
954
781
  const pks = schema.columns.filter((c) => c.primaryKey).map((c) => c.name);
@@ -973,11 +800,11 @@ function addonTableToDdl(schema) {
973
800
  }
974
801
 
975
802
  // src/builtins/sqlite-storage/settings-store.ts
976
- var import_kernel = require("@camstack/kernel");
803
+ var import_types4 = require("@camstack/types");
977
804
  var SettingsStore = class {
978
805
  db;
979
806
  constructor(dbPath) {
980
- this.db = new import_better_sqlite33.default(dbPath);
807
+ this.db = new import_better_sqlite32.default(dbPath);
981
808
  this.db.pragma("journal_mode = WAL");
982
809
  this.db.pragma("foreign_keys = ON");
983
810
  this.initTables();
@@ -1080,6 +907,42 @@ var SettingsStore = class {
1080
907
  return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
1081
908
  }
1082
909
  // ---------------------------------------------------------------------------
910
+ // Addon-device settings (per-device overrides of an addon's config)
911
+ //
912
+ // Implements the third level of the resolution chain used by the
913
+ // multi-level settings system:
914
+ // schema default -> addon_settings (global) -> addon_device_settings
915
+ // ---------------------------------------------------------------------------
916
+ getAddonDevice(addonId, deviceId) {
917
+ const rows = this.db.prepare(
918
+ "SELECT key, value FROM addon_device_settings WHERE addon_id = ? AND device_id = ?"
919
+ ).all(addonId, deviceId);
920
+ return Object.fromEntries(rows.map((r) => [r.key, JSON.parse(r.value)]));
921
+ }
922
+ /**
923
+ * Bulk-replace all per-device overrides for (addonId, deviceId) in a
924
+ * single transaction. Empty input clears the override set — caller
925
+ * should use `clearAddonDevice` for explicit resets.
926
+ */
927
+ setAddonDevice(addonId, deviceId, values) {
928
+ const deleteStmt = this.db.prepare(
929
+ "DELETE FROM addon_device_settings WHERE addon_id = ? AND device_id = ?"
930
+ );
931
+ const insertStmt = this.db.prepare(
932
+ `INSERT INTO addon_device_settings (addon_id, device_id, key, value, updated_at)
933
+ VALUES (?, ?, ?, json(?), unixepoch())`
934
+ );
935
+ this.db.transaction(() => {
936
+ deleteStmt.run(addonId, deviceId);
937
+ for (const [key, value] of Object.entries(values)) {
938
+ insertStmt.run(addonId, deviceId, key, JSON.stringify(value));
939
+ }
940
+ })();
941
+ }
942
+ clearAddonDevice(addonId, deviceId) {
943
+ this.db.prepare("DELETE FROM addon_device_settings WHERE addon_id = ? AND device_id = ?").run(addonId, deviceId);
944
+ }
945
+ // ---------------------------------------------------------------------------
1083
946
  // Lifecycle
1084
947
  // ---------------------------------------------------------------------------
1085
948
  /** Close the SQLite connection (call on shutdown). */
@@ -1097,7 +960,7 @@ var SettingsStore = class {
1097
960
  `INSERT OR IGNORE INTO system_settings (key, value, updated_at) VALUES (?, json(?), unixepoch())`
1098
961
  );
1099
962
  this.db.transaction(() => {
1100
- for (const [key, value] of Object.entries(import_kernel.RUNTIME_DEFAULTS)) {
963
+ for (const [key, value] of Object.entries(import_types4.RUNTIME_DEFAULTS)) {
1101
964
  insert.run(key, JSON.stringify(value));
1102
965
  }
1103
966
  })();
@@ -1110,20 +973,90 @@ var SettingsStore = class {
1110
973
  for (const stmt of CORE_TABLE_DDL) {
1111
974
  this.db.prepare(stmt).run();
1112
975
  }
976
+ for (const stmt of CORE_TABLE_MIGRATIONS) {
977
+ try {
978
+ this.db.prepare(stmt).run();
979
+ } catch (err) {
980
+ const msg = err instanceof Error ? err.message : String(err);
981
+ if (msg.includes("duplicate column name") || msg.includes("no such table")) continue;
982
+ throw err;
983
+ }
984
+ }
1113
985
  })();
1114
986
  }
1115
987
  };
988
+
989
+ // src/builtins/sqlite-storage/device-store.ts
990
+ var DeviceStore = class {
991
+ constructor(db) {
992
+ this.db = db;
993
+ }
994
+ db;
995
+ insert(addonId, device) {
996
+ this.db.prepare(
997
+ `INSERT INTO devices (addon_id, stable_id, type, name, parent_stable_id) VALUES (?, ?, ?, ?, ?)`
998
+ ).run(addonId, device.stableId, device.type, device.name, device.parentStableId);
999
+ }
1000
+ listByAddon(addonId) {
1001
+ return this.db.prepare(
1002
+ `SELECT stable_id as stableId, type, name, parent_stable_id as parentStableId, enabled FROM devices WHERE addon_id = ?`
1003
+ ).all(addonId);
1004
+ }
1005
+ listChildren(addonId, parentStableId) {
1006
+ return this.db.prepare(
1007
+ `SELECT stable_id as stableId, type, name, parent_stable_id as parentStableId, enabled FROM devices WHERE addon_id = ? AND parent_stable_id = ?`
1008
+ ).all(addonId, parentStableId);
1009
+ }
1010
+ remove(addonId, stableId) {
1011
+ this.db.prepare(`DELETE FROM devices WHERE addon_id = ? AND stable_id = ?`).run(addonId, stableId);
1012
+ }
1013
+ };
1014
+
1015
+ // src/builtins/sqlite-storage/config-store.ts
1016
+ var ConfigStore = class {
1017
+ constructor(db) {
1018
+ this.db = db;
1019
+ }
1020
+ db;
1021
+ save(addonId, stableId, data) {
1022
+ if (stableId === null) {
1023
+ this.db.prepare(
1024
+ `INSERT INTO addon_config (addon_id, stable_id, data, updated_at)
1025
+ VALUES (?, NULL, ?, datetime('now'))
1026
+ ON CONFLICT(addon_id) WHERE stable_id IS NULL
1027
+ DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
1028
+ ).run(addonId, JSON.stringify(data));
1029
+ } else {
1030
+ this.db.prepare(
1031
+ `INSERT INTO addon_config (addon_id, stable_id, data, updated_at)
1032
+ VALUES (?, ?, ?, datetime('now'))
1033
+ ON CONFLICT(addon_id, stable_id) WHERE stable_id IS NOT NULL
1034
+ DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
1035
+ ).run(addonId, stableId, JSON.stringify(data));
1036
+ }
1037
+ }
1038
+ load(addonId, stableId) {
1039
+ 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);
1040
+ return row ? JSON.parse(row.data) : {};
1041
+ }
1042
+ remove(addonId, stableId) {
1043
+ if (stableId === null) {
1044
+ this.db.prepare(`DELETE FROM addon_config WHERE addon_id = ? AND stable_id IS NULL`).run(addonId);
1045
+ } else {
1046
+ this.db.prepare(`DELETE FROM addon_config WHERE addon_id = ? AND stable_id = ?`).run(addonId, stableId);
1047
+ }
1048
+ }
1049
+ };
1116
1050
  // Annotate the CommonJS export names for ESM import in node:
1117
1051
  0 && (module.exports = {
1118
1052
  CORE_TABLE_DDL,
1119
- FileSystemStorage,
1053
+ ConfigStore,
1054
+ DeviceStore,
1120
1055
  FilesystemStorageAddon,
1121
1056
  FilesystemStorageProvider,
1122
1057
  SettingsStore,
1123
1058
  SqliteSettingsAddon,
1124
1059
  SqliteSettingsBackend,
1125
- SqliteStorageAddon,
1126
- SqliteStorageProvider,
1127
1060
  addonTableToDdl
1128
1061
  });
1129
1062
  //# sourceMappingURL=index.js.map