@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.
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +220 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js.map +1 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +9 -0
- package/dist/builtins/addon-pages-aggregator/index.js +222 -0
- package/dist/builtins/addon-pages-aggregator/index.js.map +1 -0
- package/dist/builtins/addon-pages-aggregator/index.mjs +9 -0
- package/dist/builtins/addon-pages-aggregator/index.mjs.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +200 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +9 -0
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/index.js +202 -0
- package/dist/builtins/addon-widgets-aggregator/index.js.map +1 -0
- package/dist/builtins/addon-widgets-aggregator/index.mjs +9 -0
- package/dist/builtins/addon-widgets-aggregator/index.mjs.map +1 -0
- package/dist/builtins/alerts/alerts.addon.js +443 -0
- package/dist/builtins/alerts/alerts.addon.js.map +1 -0
- package/dist/builtins/alerts/alerts.addon.mjs +9 -0
- package/dist/builtins/alerts/alerts.addon.mjs.map +1 -0
- package/dist/builtins/alerts/index.js +443 -0
- package/dist/builtins/alerts/index.js.map +1 -0
- package/dist/builtins/alerts/index.mjs +8 -0
- package/dist/builtins/alerts/index.mjs.map +1 -0
- package/dist/builtins/console-logging/index.js +242 -0
- package/dist/builtins/console-logging/index.js.map +1 -0
- package/dist/builtins/console-logging/index.mjs +11 -0
- package/dist/builtins/console-logging/index.mjs.map +1 -0
- package/dist/builtins/device-manager/device-manager.addon.js +2155 -0
- package/dist/builtins/device-manager/device-manager.addon.js.map +1 -0
- package/dist/builtins/device-manager/device-manager.addon.mjs +9 -0
- package/dist/builtins/device-manager/device-manager.addon.mjs.map +1 -0
- package/dist/builtins/device-manager/index.js +2157 -0
- package/dist/builtins/device-manager/index.js.map +1 -0
- package/dist/builtins/device-manager/index.mjs +10 -0
- package/dist/builtins/device-manager/index.mjs.map +1 -0
- package/dist/builtins/hub-forwarder/index.js +297 -0
- package/dist/builtins/hub-forwarder/index.js.map +1 -0
- package/dist/builtins/hub-forwarder/index.mjs +11 -0
- package/dist/builtins/hub-forwarder/index.mjs.map +1 -0
- package/dist/builtins/local-auth/index.js +623 -0
- package/dist/builtins/local-auth/index.js.map +1 -0
- package/dist/builtins/local-auth/index.mjs +8 -0
- package/dist/builtins/local-auth/index.mjs.map +1 -0
- package/dist/builtins/local-auth/local-auth.addon.js +623 -0
- package/dist/builtins/local-auth/local-auth.addon.js.map +1 -0
- package/dist/builtins/local-auth/local-auth.addon.mjs +9 -0
- package/dist/builtins/local-auth/local-auth.addon.mjs.map +1 -0
- package/dist/builtins/local-backup/index.js +53 -68
- package/dist/builtins/local-backup/index.js.map +1 -1
- package/dist/builtins/local-backup/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +898 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js.map +1 -0
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +7 -0
- package/dist/builtins/native-metrics/native-metrics.addon.mjs.map +1 -0
- package/dist/builtins/snapshot/index.js +504 -0
- package/dist/builtins/snapshot/index.js.map +1 -0
- package/dist/builtins/snapshot/index.mjs +477 -0
- package/dist/builtins/snapshot/index.mjs.map +1 -0
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +16 -166
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js.map +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +554 -621
- package/dist/builtins/sqlite-storage/index.js.map +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +9 -11
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +368 -130
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js.map +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/system-config/index.js +189 -0
- package/dist/builtins/system-config/index.js.map +1 -0
- package/dist/builtins/system-config/index.mjs +10 -0
- package/dist/builtins/system-config/index.mjs.map +1 -0
- package/dist/builtins/system-config/system-config.addon.js +187 -0
- package/dist/builtins/system-config/system-config.addon.js.map +1 -0
- package/dist/builtins/system-config/system-config.addon.mjs +9 -0
- package/dist/builtins/system-config/system-config.addon.mjs.map +1 -0
- package/dist/builtins/winston-logging/index.js +185 -65
- package/dist/builtins/winston-logging/index.js.map +1 -1
- package/dist/builtins/winston-logging/index.mjs +2 -1
- package/dist/chunk-2CIYKDRN.mjs +1 -0
- package/dist/chunk-2CIYKDRN.mjs.map +1 -0
- package/dist/chunk-2F76X6NL.mjs +136 -0
- package/dist/chunk-2F76X6NL.mjs.map +1 -0
- package/dist/chunk-2QUFBZ7M.mjs +1 -0
- package/dist/chunk-2QUFBZ7M.mjs.map +1 -0
- package/dist/chunk-3BK2Y7GY.mjs +593 -0
- package/dist/chunk-3BK2Y7GY.mjs.map +1 -0
- package/dist/chunk-4OOHFJHT.mjs +421 -0
- package/dist/chunk-4OOHFJHT.mjs.map +1 -0
- package/dist/chunk-4XHB7IHT.mjs +809 -0
- package/dist/chunk-4XHB7IHT.mjs.map +1 -0
- package/dist/{chunk-2F3XZYRW.mjs → chunk-6M2HSSTQ.mjs} +16 -7
- package/dist/chunk-6M2HSSTQ.mjs.map +1 -0
- package/dist/{chunk-SO4LROOT.mjs → chunk-7FI7SQS7.mjs} +54 -69
- package/dist/chunk-7FI7SQS7.mjs.map +1 -0
- package/dist/chunk-ED57RCQE.mjs +171 -0
- package/dist/chunk-ED57RCQE.mjs.map +1 -0
- package/dist/chunk-FZN56HGQ.mjs +626 -0
- package/dist/chunk-FZN56HGQ.mjs.map +1 -0
- package/dist/chunk-GL4OOB25.mjs +51 -0
- package/dist/chunk-GL4OOB25.mjs.map +1 -0
- package/dist/chunk-KDG2NTDB.mjs +137 -0
- package/dist/chunk-KDG2NTDB.mjs.map +1 -0
- package/dist/chunk-NRBQWBDM.mjs +191 -0
- package/dist/chunk-NRBQWBDM.mjs.map +1 -0
- package/dist/chunk-O4V246GG.mjs +2137 -0
- package/dist/chunk-O4V246GG.mjs.map +1 -0
- package/dist/chunk-QT57H266.mjs +163 -0
- package/dist/chunk-QT57H266.mjs.map +1 -0
- package/dist/chunk-QX4RH25I.mjs +141 -0
- package/dist/chunk-QX4RH25I.mjs.map +1 -0
- package/dist/chunk-TB562PZX.mjs +86 -0
- package/dist/chunk-TB562PZX.mjs.map +1 -0
- package/dist/chunk-TDYPZXK5.mjs +1 -0
- package/dist/chunk-TDYPZXK5.mjs.map +1 -0
- package/dist/chunk-UJI4LN5P.mjs +36 -0
- package/dist/chunk-UJI4LN5P.mjs.map +1 -0
- package/dist/chunk-W6RTHQGP.mjs +1 -0
- package/dist/chunk-W6RTHQGP.mjs.map +1 -0
- package/dist/chunk-ZELBCPDC.mjs +369 -0
- package/dist/chunk-ZELBCPDC.mjs.map +1 -0
- package/dist/index.d.mts +1103 -544
- package/dist/index.d.ts +1103 -544
- package/dist/index.js +7032 -6033
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +568 -2226
- package/dist/index.mjs.map +1 -1
- package/dist/resource-monitor-UZUGPIAU.mjs +9 -0
- package/dist/resource-monitor-UZUGPIAU.mjs.map +1 -0
- package/dist/storage-location-manager-HFNB3PCS.mjs +7 -0
- package/dist/storage-location-manager-HFNB3PCS.mjs.map +1 -0
- package/package.json +123 -2
- package/dist/builtins/local-backup/index.d.mts +0 -42
- package/dist/builtins/local-backup/index.d.ts +0 -42
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.mts +0 -2
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.d.ts +0 -2
- package/dist/builtins/sqlite-storage/index.d.mts +0 -4
- package/dist/builtins/sqlite-storage/index.d.ts +0 -4
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.mts +0 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +0 -2
- package/dist/builtins/winston-logging/index.d.mts +0 -30
- package/dist/builtins/winston-logging/index.d.ts +0 -30
- package/dist/chunk-2F3XZYRW.mjs.map +0 -1
- package/dist/chunk-LQFPAEQF.mjs +0 -147
- package/dist/chunk-LQFPAEQF.mjs.map +0 -1
- package/dist/chunk-R3DIIBBX.mjs +0 -532
- package/dist/chunk-R3DIIBBX.mjs.map +0 -1
- package/dist/chunk-SMNR44VG.mjs +0 -386
- package/dist/chunk-SMNR44VG.mjs.map +0 -1
- package/dist/chunk-SO4LROOT.mjs.map +0 -1
- package/dist/chunk-SPA4JBKN.mjs +0 -175
- package/dist/chunk-SPA4JBKN.mjs.map +0 -1
- package/dist/dist-3BY63UQ5.mjs +0 -2151
- package/dist/dist-3BY63UQ5.mjs.map +0 -1
- package/dist/filesystem-storage.addon-C42r589X.d.mts +0 -57
- package/dist/filesystem-storage.addon-C42r589X.d.ts +0 -57
- package/dist/sql-schema-CKz78rId.d.mts +0 -97
- package/dist/sql-schema-CKz78rId.d.ts +0 -97
- package/dist/sqlite-settings.addon-KwG-uKMP.d.mts +0 -79
- package/dist/sqlite-settings.addon-KwG-uKMP.d.ts +0 -79
- package/dist/storage-location-manager-KKDQNAKA.mjs +0 -7
- /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
|
-
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
176
|
-
rootPath: "camstack-data"
|
|
177
|
-
}
|
|
178
|
-
async
|
|
179
|
-
const 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
|
-
|
|
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] =
|
|
62
|
+
overrides[loc] = addonCfg[key];
|
|
186
63
|
}
|
|
187
64
|
}
|
|
188
|
-
this.provider = new FilesystemStorageProvider(rootPath, overrides);
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
|
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
|
|
231
|
-
|
|
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
|
|
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.
|
|
250
|
-
const
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
259
|
-
this.
|
|
260
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
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
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
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((
|
|
316
|
-
id
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
384
|
+
const parsed = parseRowData(row.data);
|
|
376
385
|
const key = row.id.startsWith(`${addonId}.`) ? row.id.slice(addonId.length + 1) : row.id;
|
|
377
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
558
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
683
|
+
this.ctx.logger.info("Initialized successfully");
|
|
684
|
+
return [{ capability: import_types3.settingsStoreCapability, provider: this.backend }];
|
|
571
685
|
}
|
|
572
|
-
async
|
|
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
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
|
|
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
|
|
803
|
+
var import_types4 = require("@camstack/types");
|
|
977
804
|
var SettingsStore = class {
|
|
978
805
|
db;
|
|
979
806
|
constructor(dbPath) {
|
|
980
|
-
this.db = new
|
|
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(
|
|
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
|
-
|
|
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
|