@camstack/system 1.2.108 → 1.2.109
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/device-manager/device-manager.addon.js +20 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +20 -1
- package/dist/builtins/device-manager/device-row-store.d.ts +20 -1
- package/dist/builtins/sqlite-storage/integration-registry.d.ts +15 -12
- package/dist/builtins/sqlite-storage/retired-collections.d.ts +52 -0
- package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +22 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +100 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +100 -0
- package/dist/index.js +11 -292
- package/dist/index.mjs +11 -292
- package/package.json +1 -2
|
@@ -3810,13 +3810,32 @@ var DeviceMetaStore = class {
|
|
|
3810
3810
|
//#endregion
|
|
3811
3811
|
//#region src/builtins/device-manager/device-row-store.ts
|
|
3812
3812
|
/**
|
|
3813
|
-
* @durable class=
|
|
3813
|
+
* @durable class=ledger owner=device-manager
|
|
3814
3814
|
* write="one row per device, written by `allocateDeviceId` (identity placeholder),
|
|
3815
3815
|
* `registerDevice` (full reconcile) and every meta setter (single-column patch);
|
|
3816
3816
|
* `setMetadata` patches the `metadata` column of the same row"
|
|
3817
3817
|
* retention="none — a row goes only when the operator removes the device
|
|
3818
3818
|
* (`removeDevice`), or when an integration is deleted and cascades. Bounded by the
|
|
3819
3819
|
* fleet an operator configures (974 rows on the reference hub)."
|
|
3820
|
+
*
|
|
3821
|
+
* ### Why `ledger` and not `config`
|
|
3822
|
+
*
|
|
3823
|
+
* This shipped as `class=registry`, a sixth class that does not exist —
|
|
3824
|
+
* `DURABLE_CLASSES` is `ledger | mirror | ephemeral | config | audit`, so
|
|
3825
|
+
* `check-durable-state-declared.ts` has failed since. `config` is the tempting
|
|
3826
|
+
* answer, because a device's `name` and `location` ARE operator intent. It is
|
|
3827
|
+
* the wrong one, and wrong in the dangerous direction: `config`'s rule is
|
|
3828
|
+
* *"already durable everywhere"*, and that is exactly the premise that made the
|
|
3829
|
+
* dead `devices` table look harmless right up to D183.
|
|
3830
|
+
*
|
|
3831
|
+
* Nothing else holds a device's NUMERIC id. `allocateDeviceId` mints it here
|
|
3832
|
+
* and every other store in the system keys on it — `addon-device-settings`,
|
|
3833
|
+
* `device-runtime-state`, every `pipeline-analytics:*` row, every recorder path.
|
|
3834
|
+
* That is `ledger`'s definition verbatim ("a fact only this component ever
|
|
3835
|
+
* held"), and `ledger`'s rule — *must not be pruned against a fallible read* —
|
|
3836
|
+
* is precisely the rule this collection needs (D49): a `getBindings` or an
|
|
3837
|
+
* integration listing that answers empty because it FAILED must never be
|
|
3838
|
+
* allowed to retire a device row.
|
|
3820
3839
|
*/
|
|
3821
3840
|
var DEVICE_ROWS_COLLECTION = "device-manager:devices";
|
|
3822
3841
|
/**
|
|
@@ -3805,13 +3805,32 @@ var DeviceMetaStore = class {
|
|
|
3805
3805
|
//#endregion
|
|
3806
3806
|
//#region src/builtins/device-manager/device-row-store.ts
|
|
3807
3807
|
/**
|
|
3808
|
-
* @durable class=
|
|
3808
|
+
* @durable class=ledger owner=device-manager
|
|
3809
3809
|
* write="one row per device, written by `allocateDeviceId` (identity placeholder),
|
|
3810
3810
|
* `registerDevice` (full reconcile) and every meta setter (single-column patch);
|
|
3811
3811
|
* `setMetadata` patches the `metadata` column of the same row"
|
|
3812
3812
|
* retention="none — a row goes only when the operator removes the device
|
|
3813
3813
|
* (`removeDevice`), or when an integration is deleted and cascades. Bounded by the
|
|
3814
3814
|
* fleet an operator configures (974 rows on the reference hub)."
|
|
3815
|
+
*
|
|
3816
|
+
* ### Why `ledger` and not `config`
|
|
3817
|
+
*
|
|
3818
|
+
* This shipped as `class=registry`, a sixth class that does not exist —
|
|
3819
|
+
* `DURABLE_CLASSES` is `ledger | mirror | ephemeral | config | audit`, so
|
|
3820
|
+
* `check-durable-state-declared.ts` has failed since. `config` is the tempting
|
|
3821
|
+
* answer, because a device's `name` and `location` ARE operator intent. It is
|
|
3822
|
+
* the wrong one, and wrong in the dangerous direction: `config`'s rule is
|
|
3823
|
+
* *"already durable everywhere"*, and that is exactly the premise that made the
|
|
3824
|
+
* dead `devices` table look harmless right up to D183.
|
|
3825
|
+
*
|
|
3826
|
+
* Nothing else holds a device's NUMERIC id. `allocateDeviceId` mints it here
|
|
3827
|
+
* and every other store in the system keys on it — `addon-device-settings`,
|
|
3828
|
+
* `device-runtime-state`, every `pipeline-analytics:*` row, every recorder path.
|
|
3829
|
+
* That is `ledger`'s definition verbatim ("a fact only this component ever
|
|
3830
|
+
* held"), and `ledger`'s rule — *must not be pruned against a fallible read* —
|
|
3831
|
+
* is precisely the rule this collection needs (D49): a `getBindings` or an
|
|
3832
|
+
* integration listing that answers empty because it FAILED must never be
|
|
3833
|
+
* allowed to retire a device row.
|
|
3815
3834
|
*/
|
|
3816
3835
|
var DEVICE_ROWS_COLLECTION = "device-manager:devices";
|
|
3817
3836
|
/**
|
|
@@ -2,13 +2,32 @@ import { ChildLayout, CollectionColumn, CollectionIndex, DeviceDisplayOverride,
|
|
|
2
2
|
import { RetiredRowStore } from '../sqlite-storage/retired-settings-keys.js';
|
|
3
3
|
import { PersistedDeviceMeta } from './device-meta-types.js';
|
|
4
4
|
/**
|
|
5
|
-
* @durable class=
|
|
5
|
+
* @durable class=ledger owner=device-manager
|
|
6
6
|
* write="one row per device, written by `allocateDeviceId` (identity placeholder),
|
|
7
7
|
* `registerDevice` (full reconcile) and every meta setter (single-column patch);
|
|
8
8
|
* `setMetadata` patches the `metadata` column of the same row"
|
|
9
9
|
* retention="none — a row goes only when the operator removes the device
|
|
10
10
|
* (`removeDevice`), or when an integration is deleted and cascades. Bounded by the
|
|
11
11
|
* fleet an operator configures (974 rows on the reference hub)."
|
|
12
|
+
*
|
|
13
|
+
* ### Why `ledger` and not `config`
|
|
14
|
+
*
|
|
15
|
+
* This shipped as `class=registry`, a sixth class that does not exist —
|
|
16
|
+
* `DURABLE_CLASSES` is `ledger | mirror | ephemeral | config | audit`, so
|
|
17
|
+
* `check-durable-state-declared.ts` has failed since. `config` is the tempting
|
|
18
|
+
* answer, because a device's `name` and `location` ARE operator intent. It is
|
|
19
|
+
* the wrong one, and wrong in the dangerous direction: `config`'s rule is
|
|
20
|
+
* *"already durable everywhere"*, and that is exactly the premise that made the
|
|
21
|
+
* dead `devices` table look harmless right up to D183.
|
|
22
|
+
*
|
|
23
|
+
* Nothing else holds a device's NUMERIC id. `allocateDeviceId` mints it here
|
|
24
|
+
* and every other store in the system keys on it — `addon-device-settings`,
|
|
25
|
+
* `device-runtime-state`, every `pipeline-analytics:*` row, every recorder path.
|
|
26
|
+
* That is `ledger`'s definition verbatim ("a fact only this component ever
|
|
27
|
+
* held"), and `ledger`'s rule — *must not be pruned against a fallible read* —
|
|
28
|
+
* is precisely the rule this collection needs (D49): a `getBindings` or an
|
|
29
|
+
* integration listing that answers empty because it FAILED must never be
|
|
30
|
+
* allowed to retire a device row.
|
|
12
31
|
*/
|
|
13
32
|
export declare const DEVICE_ROWS_COLLECTION = "device-manager:devices";
|
|
14
33
|
/**
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CreateIntegrationInput, IIntegrationRegistry, Integration, ISettingsStoreProvider } from '@camstack/types';
|
|
2
|
+
/**
|
|
3
|
+
* The persisted INTEGRATIONS — one row per provider instance an operator (or a
|
|
4
|
+
* declaring addon) added, plus that instance's settings.
|
|
5
|
+
*
|
|
6
|
+
* It had a device half once: `devices` + `device_settings_kv`, `dev_NNNN` ids,
|
|
7
|
+
* `createDevice` / `listCameras` / `setDeviceSetting`. It was removed on
|
|
8
|
+
* 2026-08-19 with both tables ([D183](../../../../../docs/decisions/adr-0183-code-that-is-deleted-takes-its-tables-with-it.md)):
|
|
9
|
+
* measured on the reference hub it held 0 rows against 974 live devices, no
|
|
10
|
+
* production caller ever reached it, and its id-space could not be reconciled
|
|
11
|
+
* with the numeric ids every track, rule and binding in the system uses. The
|
|
12
|
+
* fleet is `device-manager:devices`. Do not add a device method here — the
|
|
13
|
+
* device authority is the device-manager, and a second one that agrees with
|
|
14
|
+
* nothing is how this half came to exist.
|
|
15
|
+
*/
|
|
2
16
|
export declare class IntegrationRegistry implements IIntegrationRegistry {
|
|
3
17
|
private readonly store;
|
|
4
18
|
constructor(backend: ISettingsStoreProvider);
|
|
@@ -22,16 +36,5 @@ export declare class IntegrationRegistry implements IIntegrationRegistry {
|
|
|
22
36
|
getIntegrationSettings(integrationId: string): Promise<Record<string, unknown>>;
|
|
23
37
|
setIntegrationSetting(integrationId: string, key: string, value: unknown): Promise<void>;
|
|
24
38
|
setIntegrationSettings(integrationId: string, settings: Record<string, unknown>): Promise<void>;
|
|
25
|
-
createDevice(input: CreateDeviceInput): Promise<PersistedDevice>;
|
|
26
|
-
getDevice(id: string): Promise<PersistedDevice | null>;
|
|
27
|
-
getDeviceByStableId(stableId: string): Promise<PersistedDevice | null>;
|
|
28
|
-
listDevices(integrationId?: string): Promise<readonly PersistedDevice[]>;
|
|
29
|
-
listCameras(): Promise<readonly PersistedDevice[]>;
|
|
30
|
-
updateDevice(id: string, updates: Partial<Pick<PersistedDevice, 'name' | 'enabled' | 'info'>>): Promise<PersistedDevice | null>;
|
|
31
|
-
deleteDevice(id: string): Promise<boolean>;
|
|
32
|
-
getDeviceSettings(deviceId: string): Promise<Record<string, unknown>>;
|
|
33
|
-
setDeviceSetting(deviceId: string, key: string, value: unknown): Promise<void>;
|
|
34
|
-
setDeviceSettings(deviceId: string, settings: Record<string, unknown>): Promise<void>;
|
|
35
39
|
private mapIntegration;
|
|
36
|
-
private mapDevice;
|
|
37
40
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { RetiredKeyLogger } from './retired-settings-keys.js';
|
|
2
|
+
import { DroppedTable, SqliteSettingsBackend } from './sqlite-settings-backend.js';
|
|
3
|
+
/** One retired-table declaration. */
|
|
4
|
+
export interface RetiredCollectionSpec {
|
|
5
|
+
/**
|
|
6
|
+
* The table name as it exists on disk. These tables are NOT namespaced —
|
|
7
|
+
* the retired code declared them bare — so this is the literal name, not a
|
|
8
|
+
* `<namespace>:<collection>` pair to be resolved.
|
|
9
|
+
*/
|
|
10
|
+
readonly collection: string;
|
|
11
|
+
/** The code that used to own it — reported, never used to address it. */
|
|
12
|
+
readonly owner: string;
|
|
13
|
+
/** Why it is dead. Cited in the log line, not re-derived at the call site. */
|
|
14
|
+
readonly reason: string;
|
|
15
|
+
}
|
|
16
|
+
/** What one drop destroyed. */
|
|
17
|
+
export interface RetiredCollectionDrop {
|
|
18
|
+
readonly collection: string;
|
|
19
|
+
readonly owner: string;
|
|
20
|
+
/** Rows the table held. Zero is the expected case and is still reported. */
|
|
21
|
+
readonly rows: number;
|
|
22
|
+
}
|
|
23
|
+
/** The engine access the purge needs. Narrow on purpose — trivially fakeable. */
|
|
24
|
+
export interface RetiredCollectionStore {
|
|
25
|
+
/** Drop `collection`, or answer `null` when it is not there. Throws on fault. */
|
|
26
|
+
drop(collection: string): Promise<DroppedTable | null>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The dead tables.
|
|
30
|
+
*
|
|
31
|
+
* `devices` / `device_settings_kv` — the device half of the
|
|
32
|
+
* integration-registry ([D183](../../../../../docs/decisions/adr-0183.md)).
|
|
33
|
+
* The registry's INTEGRATION half is live (`integrations`,
|
|
34
|
+
* `integration_settings`); its device half never had a production caller and
|
|
35
|
+
* never had a row. The fleet lives in `device-manager:devices`, under numeric
|
|
36
|
+
* ids, while these tables key on `dev_NNNN` — the two id-spaces could not be
|
|
37
|
+
* reconciled even if something had wanted to.
|
|
38
|
+
*/
|
|
39
|
+
export declare const RETIRED_COLLECTIONS: readonly RetiredCollectionSpec[];
|
|
40
|
+
/**
|
|
41
|
+
* Adapt the SQLite engine to {@link RetiredCollectionStore}.
|
|
42
|
+
*
|
|
43
|
+
* The DDL itself is the engine's — it owns the handle and the memos a drop has
|
|
44
|
+
* to evict. This module owns only the list and the reporting.
|
|
45
|
+
*/
|
|
46
|
+
export declare function retiredCollectionStoreOf(backend: SqliteSettingsBackend): RetiredCollectionStore;
|
|
47
|
+
/**
|
|
48
|
+
* Drop every retired table that is still present. Returns one entry per table
|
|
49
|
+
* actually dropped — empty when the schema is already clean, which is the
|
|
50
|
+
* steady state after the first boot that ran it.
|
|
51
|
+
*/
|
|
52
|
+
export declare function purgeRetiredCollections(store: RetiredCollectionStore, logger: RetiredKeyLogger, specs?: readonly RetiredCollectionSpec[]): Promise<readonly RetiredCollectionDrop[]>;
|
|
@@ -20,6 +20,11 @@ interface SettingsUpdateWhereInput {
|
|
|
20
20
|
interface SettingsUpdateWhereResult {
|
|
21
21
|
readonly updated: number;
|
|
22
22
|
}
|
|
23
|
+
/** What {@link SqliteSettingsBackend.dropRetiredTable} destroyed. */
|
|
24
|
+
export interface DroppedTable {
|
|
25
|
+
/** Rows the table held at the moment it was dropped. */
|
|
26
|
+
readonly rows: number;
|
|
27
|
+
}
|
|
23
28
|
/**
|
|
24
29
|
* SQLite implementation of ISettingsBackend.
|
|
25
30
|
*
|
|
@@ -213,6 +218,23 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
|
|
|
213
218
|
/** Serialise per-column values for SQL binding: objects → JSON, booleans → 0/1. */
|
|
214
219
|
private serializeColumnValue;
|
|
215
220
|
private ensureTable;
|
|
221
|
+
/**
|
|
222
|
+
* Drop a table nothing declares any more, reporting what went with it.
|
|
223
|
+
*
|
|
224
|
+
* The DDL lives here because the engine owns the handle AND the two memos
|
|
225
|
+
* that outlive a table: `structuredTables` (schema fingerprint) and
|
|
226
|
+
* `declaredCollections` (the column set `compileFilter` resolves against).
|
|
227
|
+
* Dropping behind their backs would leave a ghost — a later declaration of
|
|
228
|
+
* the same name would be memoised as "already materialised" against a table
|
|
229
|
+
* that no longer exists.
|
|
230
|
+
*
|
|
231
|
+
* WHICH tables are retired, and the logging, are policy: they live in
|
|
232
|
+
* `retired-collections.ts`. This method knows only how to drop one.
|
|
233
|
+
*
|
|
234
|
+
* Answers `null` when the table is not there — the steady state after the
|
|
235
|
+
* first boot that ran the purge, and the case on a fresh install.
|
|
236
|
+
*/
|
|
237
|
+
dropRetiredTable(table: string): DroppedTable | null;
|
|
216
238
|
private tableInsert;
|
|
217
239
|
private tableUpdate;
|
|
218
240
|
private tableDelete;
|
|
@@ -85,6 +85,77 @@ var SQLITE_MMAP_SIZE_BYTES = 268435456;
|
|
|
85
85
|
*/
|
|
86
86
|
var SQLITE_JOURNAL_SIZE_LIMIT_BYTES = 67108864;
|
|
87
87
|
//#endregion
|
|
88
|
+
//#region src/builtins/sqlite-storage/retired-collections.ts
|
|
89
|
+
/**
|
|
90
|
+
* The dead tables.
|
|
91
|
+
*
|
|
92
|
+
* `devices` / `device_settings_kv` — the device half of the
|
|
93
|
+
* integration-registry ([D183](../../../../../docs/decisions/adr-0183.md)).
|
|
94
|
+
* The registry's INTEGRATION half is live (`integrations`,
|
|
95
|
+
* `integration_settings`); its device half never had a production caller and
|
|
96
|
+
* never had a row. The fleet lives in `device-manager:devices`, under numeric
|
|
97
|
+
* ids, while these tables key on `dev_NNNN` — the two id-spaces could not be
|
|
98
|
+
* reconciled even if something had wanted to.
|
|
99
|
+
*/
|
|
100
|
+
var RETIRED_COLLECTIONS = [{
|
|
101
|
+
collection: "devices",
|
|
102
|
+
owner: "integration-registry",
|
|
103
|
+
reason: "the device half of the integration-registry is deleted; the fleet is `device-manager:devices` and always was (0 rows here against 974 live devices)"
|
|
104
|
+
}, {
|
|
105
|
+
collection: "device_settings_kv",
|
|
106
|
+
owner: "integration-registry",
|
|
107
|
+
reason: "per-device settings of a device table that never had a row; the live per-camera store is the device-manager row plus `addon-device-settings`"
|
|
108
|
+
}];
|
|
109
|
+
/**
|
|
110
|
+
* Adapt the SQLite engine to {@link RetiredCollectionStore}.
|
|
111
|
+
*
|
|
112
|
+
* The DDL itself is the engine's — it owns the handle and the memos a drop has
|
|
113
|
+
* to evict. This module owns only the list and the reporting.
|
|
114
|
+
*/
|
|
115
|
+
function retiredCollectionStoreOf(backend) {
|
|
116
|
+
return { async drop(collection) {
|
|
117
|
+
return backend.dropRetiredTable(collection);
|
|
118
|
+
} };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Drop every retired table that is still present. Returns one entry per table
|
|
122
|
+
* actually dropped — empty when the schema is already clean, which is the
|
|
123
|
+
* steady state after the first boot that ran it.
|
|
124
|
+
*/
|
|
125
|
+
async function purgeRetiredCollections(store, logger, specs = RETIRED_COLLECTIONS) {
|
|
126
|
+
const results = [];
|
|
127
|
+
for (const spec of specs) {
|
|
128
|
+
let dropped;
|
|
129
|
+
try {
|
|
130
|
+
dropped = await store.drop(spec.collection);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
logger.warn("retired collection — drop failed, table left in place", { meta: {
|
|
133
|
+
addon: spec.owner,
|
|
134
|
+
collection: spec.collection,
|
|
135
|
+
error: err instanceof Error ? err.message : String(err)
|
|
136
|
+
} });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (dropped === null) continue;
|
|
140
|
+
const result = {
|
|
141
|
+
collection: spec.collection,
|
|
142
|
+
owner: spec.owner,
|
|
143
|
+
rows: dropped.rows
|
|
144
|
+
};
|
|
145
|
+
results.push(result);
|
|
146
|
+
const line = "dropped a retired collection";
|
|
147
|
+
if (dropped.rows > 0) logger.warn(`${line} — it still held rows`, { meta: {
|
|
148
|
+
...result,
|
|
149
|
+
reason: spec.reason
|
|
150
|
+
} });
|
|
151
|
+
else logger.info(line, { meta: {
|
|
152
|
+
...result,
|
|
153
|
+
reason: spec.reason
|
|
154
|
+
} });
|
|
155
|
+
}
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
//#endregion
|
|
88
159
|
//#region src/builtins/sqlite-storage/filter-compiler.ts
|
|
89
160
|
/** Thrown by `mutate` mode. Distinct type so a caller can map it to a 400. */
|
|
90
161
|
var UnsafeFilterError = class extends Error {
|
|
@@ -290,6 +361,9 @@ function isImposedBound(source) {
|
|
|
290
361
|
}
|
|
291
362
|
//#endregion
|
|
292
363
|
//#region src/builtins/sqlite-storage/sqlite-settings-backend.ts
|
|
364
|
+
function isCountRow(raw) {
|
|
365
|
+
return raw !== null && typeof raw === "object" && typeof Reflect.get(raw, "n") === "number";
|
|
366
|
+
}
|
|
293
367
|
function parseRowData(raw) {
|
|
294
368
|
return require_dist.asJsonObject(require_dist.parseJsonUnknown(raw)) ?? {};
|
|
295
369
|
}
|
|
@@ -1017,6 +1091,31 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
1017
1091
|
}
|
|
1018
1092
|
this.structuredTables.set(table, fingerprint);
|
|
1019
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Drop a table nothing declares any more, reporting what went with it.
|
|
1096
|
+
*
|
|
1097
|
+
* The DDL lives here because the engine owns the handle AND the two memos
|
|
1098
|
+
* that outlive a table: `structuredTables` (schema fingerprint) and
|
|
1099
|
+
* `declaredCollections` (the column set `compileFilter` resolves against).
|
|
1100
|
+
* Dropping behind their backs would leave a ghost — a later declaration of
|
|
1101
|
+
* the same name would be memoised as "already materialised" against a table
|
|
1102
|
+
* that no longer exists.
|
|
1103
|
+
*
|
|
1104
|
+
* WHICH tables are retired, and the logging, are policy: they live in
|
|
1105
|
+
* `retired-collections.ts`. This method knows only how to drop one.
|
|
1106
|
+
*
|
|
1107
|
+
* Answers `null` when the table is not there — the steady state after the
|
|
1108
|
+
* first boot that ran the purge, and the case on a fresh install.
|
|
1109
|
+
*/
|
|
1110
|
+
dropRetiredTable(table) {
|
|
1111
|
+
if (this.getDb().prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(table) === void 0) return null;
|
|
1112
|
+
const counted = this.getDb().prepare(`SELECT COUNT(*) AS n FROM "${table}"`).get();
|
|
1113
|
+
const rows = isCountRow(counted) ? counted.n : 0;
|
|
1114
|
+
this.getDb().exec(`DROP TABLE "${table}"`);
|
|
1115
|
+
this.structuredTables.delete(table);
|
|
1116
|
+
this.declaredCollections.delete(table);
|
|
1117
|
+
return { rows };
|
|
1118
|
+
}
|
|
1020
1119
|
async tableInsert(table, row) {
|
|
1021
1120
|
const keys = Object.keys(row);
|
|
1022
1121
|
const cols = keys.map((k) => `"${k}"`).join(", ");
|
|
@@ -1653,6 +1752,7 @@ var SqliteSettingsAddon = class extends require_dist.BaseAddon {
|
|
|
1653
1752
|
} catch (err) {
|
|
1654
1753
|
this.ctx.logger.warn("Retired-key purge failed", { meta: { error: require_dist.errMsg(err) } });
|
|
1655
1754
|
}
|
|
1755
|
+
await purgeRetiredCollections(retiredCollectionStoreOf(this.backend), this.ctx.logger.child("RetiredCollections"));
|
|
1656
1756
|
const vectorIndex = this.createVectorIndex(this.backend);
|
|
1657
1757
|
await vectorIndex.loadIndexRegistry();
|
|
1658
1758
|
this.vectorIndex = vectorIndex;
|
|
@@ -79,6 +79,77 @@ var SQLITE_MMAP_SIZE_BYTES = 268435456;
|
|
|
79
79
|
*/
|
|
80
80
|
var SQLITE_JOURNAL_SIZE_LIMIT_BYTES = 67108864;
|
|
81
81
|
//#endregion
|
|
82
|
+
//#region src/builtins/sqlite-storage/retired-collections.ts
|
|
83
|
+
/**
|
|
84
|
+
* The dead tables.
|
|
85
|
+
*
|
|
86
|
+
* `devices` / `device_settings_kv` — the device half of the
|
|
87
|
+
* integration-registry ([D183](../../../../../docs/decisions/adr-0183.md)).
|
|
88
|
+
* The registry's INTEGRATION half is live (`integrations`,
|
|
89
|
+
* `integration_settings`); its device half never had a production caller and
|
|
90
|
+
* never had a row. The fleet lives in `device-manager:devices`, under numeric
|
|
91
|
+
* ids, while these tables key on `dev_NNNN` — the two id-spaces could not be
|
|
92
|
+
* reconciled even if something had wanted to.
|
|
93
|
+
*/
|
|
94
|
+
var RETIRED_COLLECTIONS = [{
|
|
95
|
+
collection: "devices",
|
|
96
|
+
owner: "integration-registry",
|
|
97
|
+
reason: "the device half of the integration-registry is deleted; the fleet is `device-manager:devices` and always was (0 rows here against 974 live devices)"
|
|
98
|
+
}, {
|
|
99
|
+
collection: "device_settings_kv",
|
|
100
|
+
owner: "integration-registry",
|
|
101
|
+
reason: "per-device settings of a device table that never had a row; the live per-camera store is the device-manager row plus `addon-device-settings`"
|
|
102
|
+
}];
|
|
103
|
+
/**
|
|
104
|
+
* Adapt the SQLite engine to {@link RetiredCollectionStore}.
|
|
105
|
+
*
|
|
106
|
+
* The DDL itself is the engine's — it owns the handle and the memos a drop has
|
|
107
|
+
* to evict. This module owns only the list and the reporting.
|
|
108
|
+
*/
|
|
109
|
+
function retiredCollectionStoreOf(backend) {
|
|
110
|
+
return { async drop(collection) {
|
|
111
|
+
return backend.dropRetiredTable(collection);
|
|
112
|
+
} };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Drop every retired table that is still present. Returns one entry per table
|
|
116
|
+
* actually dropped — empty when the schema is already clean, which is the
|
|
117
|
+
* steady state after the first boot that ran it.
|
|
118
|
+
*/
|
|
119
|
+
async function purgeRetiredCollections(store, logger, specs = RETIRED_COLLECTIONS) {
|
|
120
|
+
const results = [];
|
|
121
|
+
for (const spec of specs) {
|
|
122
|
+
let dropped;
|
|
123
|
+
try {
|
|
124
|
+
dropped = await store.drop(spec.collection);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger.warn("retired collection — drop failed, table left in place", { meta: {
|
|
127
|
+
addon: spec.owner,
|
|
128
|
+
collection: spec.collection,
|
|
129
|
+
error: err instanceof Error ? err.message : String(err)
|
|
130
|
+
} });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (dropped === null) continue;
|
|
134
|
+
const result = {
|
|
135
|
+
collection: spec.collection,
|
|
136
|
+
owner: spec.owner,
|
|
137
|
+
rows: dropped.rows
|
|
138
|
+
};
|
|
139
|
+
results.push(result);
|
|
140
|
+
const line = "dropped a retired collection";
|
|
141
|
+
if (dropped.rows > 0) logger.warn(`${line} — it still held rows`, { meta: {
|
|
142
|
+
...result,
|
|
143
|
+
reason: spec.reason
|
|
144
|
+
} });
|
|
145
|
+
else logger.info(line, { meta: {
|
|
146
|
+
...result,
|
|
147
|
+
reason: spec.reason
|
|
148
|
+
} });
|
|
149
|
+
}
|
|
150
|
+
return results;
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
82
153
|
//#region src/builtins/sqlite-storage/filter-compiler.ts
|
|
83
154
|
/** Thrown by `mutate` mode. Distinct type so a caller can map it to a 400. */
|
|
84
155
|
var UnsafeFilterError = class extends Error {
|
|
@@ -284,6 +355,9 @@ function isImposedBound(source) {
|
|
|
284
355
|
}
|
|
285
356
|
//#endregion
|
|
286
357
|
//#region src/builtins/sqlite-storage/sqlite-settings-backend.ts
|
|
358
|
+
function isCountRow(raw) {
|
|
359
|
+
return raw !== null && typeof raw === "object" && typeof Reflect.get(raw, "n") === "number";
|
|
360
|
+
}
|
|
287
361
|
function parseRowData(raw) {
|
|
288
362
|
return asJsonObject(parseJsonUnknown(raw)) ?? {};
|
|
289
363
|
}
|
|
@@ -1011,6 +1085,31 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
|
|
|
1011
1085
|
}
|
|
1012
1086
|
this.structuredTables.set(table, fingerprint);
|
|
1013
1087
|
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Drop a table nothing declares any more, reporting what went with it.
|
|
1090
|
+
*
|
|
1091
|
+
* The DDL lives here because the engine owns the handle AND the two memos
|
|
1092
|
+
* that outlive a table: `structuredTables` (schema fingerprint) and
|
|
1093
|
+
* `declaredCollections` (the column set `compileFilter` resolves against).
|
|
1094
|
+
* Dropping behind their backs would leave a ghost — a later declaration of
|
|
1095
|
+
* the same name would be memoised as "already materialised" against a table
|
|
1096
|
+
* that no longer exists.
|
|
1097
|
+
*
|
|
1098
|
+
* WHICH tables are retired, and the logging, are policy: they live in
|
|
1099
|
+
* `retired-collections.ts`. This method knows only how to drop one.
|
|
1100
|
+
*
|
|
1101
|
+
* Answers `null` when the table is not there — the steady state after the
|
|
1102
|
+
* first boot that ran the purge, and the case on a fresh install.
|
|
1103
|
+
*/
|
|
1104
|
+
dropRetiredTable(table) {
|
|
1105
|
+
if (this.getDb().prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(table) === void 0) return null;
|
|
1106
|
+
const counted = this.getDb().prepare(`SELECT COUNT(*) AS n FROM "${table}"`).get();
|
|
1107
|
+
const rows = isCountRow(counted) ? counted.n : 0;
|
|
1108
|
+
this.getDb().exec(`DROP TABLE "${table}"`);
|
|
1109
|
+
this.structuredTables.delete(table);
|
|
1110
|
+
this.declaredCollections.delete(table);
|
|
1111
|
+
return { rows };
|
|
1112
|
+
}
|
|
1014
1113
|
async tableInsert(table, row) {
|
|
1015
1114
|
const keys = Object.keys(row);
|
|
1016
1115
|
const cols = keys.map((k) => `"${k}"`).join(", ");
|
|
@@ -1647,6 +1746,7 @@ var SqliteSettingsAddon = class extends BaseAddon {
|
|
|
1647
1746
|
} catch (err) {
|
|
1648
1747
|
this.ctx.logger.warn("Retired-key purge failed", { meta: { error: errMsg(err) } });
|
|
1649
1748
|
}
|
|
1749
|
+
await purgeRetiredCollections(retiredCollectionStoreOf(this.backend), this.ctx.logger.child("RetiredCollections"));
|
|
1650
1750
|
const vectorIndex = this.createVectorIndex(this.backend);
|
|
1651
1751
|
await vectorIndex.loadIndexRegistry();
|
|
1652
1752
|
this.vectorIndex = vectorIndex;
|
package/dist/index.js
CHANGED
|
@@ -2151,13 +2151,9 @@ function readInfo(raw) {
|
|
|
2151
2151
|
return {};
|
|
2152
2152
|
}
|
|
2153
2153
|
var integrationCounter = 0;
|
|
2154
|
-
var deviceCounter = 0;
|
|
2155
2154
|
function nextIntegrationId() {
|
|
2156
2155
|
return `int_${String(++integrationCounter).padStart(4, "0")}`;
|
|
2157
2156
|
}
|
|
2158
|
-
function nextDeviceId() {
|
|
2159
|
-
return `dev_${String(++deviceCounter).padStart(4, "0")}`;
|
|
2160
|
-
}
|
|
2161
2157
|
var INTEGRATIONS_COLUMNS = [
|
|
2162
2158
|
{
|
|
2163
2159
|
name: "id",
|
|
@@ -2224,95 +2220,6 @@ var INTEGRATION_SETTINGS_COLUMNS = [
|
|
|
2224
2220
|
defaultValue: "string"
|
|
2225
2221
|
}
|
|
2226
2222
|
];
|
|
2227
|
-
var DEVICES_COLUMNS = [
|
|
2228
|
-
{
|
|
2229
|
-
name: "id",
|
|
2230
|
-
type: "TEXT",
|
|
2231
|
-
primaryKey: true
|
|
2232
|
-
},
|
|
2233
|
-
{
|
|
2234
|
-
name: "integration_id",
|
|
2235
|
-
type: "TEXT",
|
|
2236
|
-
notNull: true
|
|
2237
|
-
},
|
|
2238
|
-
{
|
|
2239
|
-
name: "stable_id",
|
|
2240
|
-
type: "TEXT",
|
|
2241
|
-
notNull: true,
|
|
2242
|
-
unique: true
|
|
2243
|
-
},
|
|
2244
|
-
{
|
|
2245
|
-
name: "type",
|
|
2246
|
-
type: "TEXT",
|
|
2247
|
-
notNull: true,
|
|
2248
|
-
defaultValue: "camera"
|
|
2249
|
-
},
|
|
2250
|
-
{
|
|
2251
|
-
name: "name",
|
|
2252
|
-
type: "TEXT",
|
|
2253
|
-
notNull: true
|
|
2254
|
-
},
|
|
2255
|
-
{
|
|
2256
|
-
name: "enabled",
|
|
2257
|
-
type: "INTEGER",
|
|
2258
|
-
notNull: true,
|
|
2259
|
-
defaultValue: 1
|
|
2260
|
-
},
|
|
2261
|
-
{
|
|
2262
|
-
name: "info",
|
|
2263
|
-
type: "JSON",
|
|
2264
|
-
notNull: true,
|
|
2265
|
-
defaultValue: "{}"
|
|
2266
|
-
},
|
|
2267
|
-
{
|
|
2268
|
-
name: "created_at",
|
|
2269
|
-
type: "INTEGER",
|
|
2270
|
-
notNull: true
|
|
2271
|
-
},
|
|
2272
|
-
{
|
|
2273
|
-
name: "updated_at",
|
|
2274
|
-
type: "INTEGER",
|
|
2275
|
-
notNull: true
|
|
2276
|
-
}
|
|
2277
|
-
];
|
|
2278
|
-
var DEVICES_INDEXES = [
|
|
2279
|
-
{
|
|
2280
|
-
name: "idx_devices_integration",
|
|
2281
|
-
columns: ["integration_id"]
|
|
2282
|
-
},
|
|
2283
|
-
{
|
|
2284
|
-
name: "idx_devices_stable",
|
|
2285
|
-
columns: ["stable_id"],
|
|
2286
|
-
unique: true
|
|
2287
|
-
},
|
|
2288
|
-
{
|
|
2289
|
-
name: "idx_devices_type",
|
|
2290
|
-
columns: ["type"]
|
|
2291
|
-
}
|
|
2292
|
-
];
|
|
2293
|
-
var DEVICE_SETTINGS_COLUMNS = [
|
|
2294
|
-
{
|
|
2295
|
-
name: "device_id",
|
|
2296
|
-
type: "TEXT",
|
|
2297
|
-
notNull: true
|
|
2298
|
-
},
|
|
2299
|
-
{
|
|
2300
|
-
name: "key",
|
|
2301
|
-
type: "TEXT",
|
|
2302
|
-
notNull: true
|
|
2303
|
-
},
|
|
2304
|
-
{
|
|
2305
|
-
name: "value",
|
|
2306
|
-
type: "TEXT",
|
|
2307
|
-
notNull: true
|
|
2308
|
-
},
|
|
2309
|
-
{
|
|
2310
|
-
name: "value_type",
|
|
2311
|
-
type: "TEXT",
|
|
2312
|
-
notNull: true,
|
|
2313
|
-
defaultValue: "string"
|
|
2314
|
-
}
|
|
2315
|
-
];
|
|
2316
2223
|
/**
|
|
2317
2224
|
* @durable class=config owner=integration-registry
|
|
2318
2225
|
* write="`createIntegration` — an operator adds a provider, or an addon declares
|
|
@@ -2331,29 +2238,19 @@ var INTEGRATIONS = "integrations";
|
|
|
2331
2238
|
*/
|
|
2332
2239
|
var INTEGRATION_SETTINGS = "integration_settings";
|
|
2333
2240
|
/**
|
|
2334
|
-
*
|
|
2335
|
-
*
|
|
2336
|
-
* same physical camera from creating a second device row. An insert that
|
|
2337
|
-
* collides throws rather than duplicating.
|
|
2241
|
+
* The persisted INTEGRATIONS — one row per provider instance an operator (or a
|
|
2242
|
+
* declaring addon) added, plus that instance's settings.
|
|
2338
2243
|
*
|
|
2339
|
-
*
|
|
2340
|
-
*
|
|
2341
|
-
*
|
|
2342
|
-
*
|
|
2343
|
-
*
|
|
2344
|
-
*
|
|
2345
|
-
*
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
/**
|
|
2349
|
-
* @durable class=config owner=integration-registry
|
|
2350
|
-
* write="`setDeviceSetting` — one row per (device_id, key), deleted and
|
|
2351
|
-
* re-inserted on every write; per-camera operator intent lands here"
|
|
2352
|
-
* retention="cascade only — `deleteDevice` and `deleteIntegration` remove a
|
|
2353
|
-
* device's rows. No sweep; losing the table silently reverts every camera to
|
|
2354
|
-
* its defaults."
|
|
2244
|
+
* It had a device half once: `devices` + `device_settings_kv`, `dev_NNNN` ids,
|
|
2245
|
+
* `createDevice` / `listCameras` / `setDeviceSetting`. It was removed on
|
|
2246
|
+
* 2026-08-19 with both tables ([D183](../../../../../docs/decisions/adr-0183-code-that-is-deleted-takes-its-tables-with-it.md)):
|
|
2247
|
+
* measured on the reference hub it held 0 rows against 974 live devices, no
|
|
2248
|
+
* production caller ever reached it, and its id-space could not be reconciled
|
|
2249
|
+
* with the numeric ids every track, rule and binding in the system uses. The
|
|
2250
|
+
* fleet is `device-manager:devices`. Do not add a device method here — the
|
|
2251
|
+
* device authority is the device-manager, and a second one that agrees with
|
|
2252
|
+
* nothing is how this half came to exist.
|
|
2355
2253
|
*/
|
|
2356
|
-
var DEVICE_SETTINGS = "device_settings_kv";
|
|
2357
2254
|
var IntegrationRegistry = class IntegrationRegistry {
|
|
2358
2255
|
store;
|
|
2359
2256
|
constructor(backend) {
|
|
@@ -2369,15 +2266,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2369
2266
|
collection: INTEGRATION_SETTINGS,
|
|
2370
2267
|
columns: INTEGRATION_SETTINGS_COLUMNS
|
|
2371
2268
|
});
|
|
2372
|
-
await this.store.declareCollection({
|
|
2373
|
-
collection: DEVICES,
|
|
2374
|
-
columns: DEVICES_COLUMNS,
|
|
2375
|
-
indexes: DEVICES_INDEXES
|
|
2376
|
-
});
|
|
2377
|
-
await this.store.declareCollection({
|
|
2378
|
-
collection: DEVICE_SETTINGS,
|
|
2379
|
-
columns: DEVICE_SETTINGS_COLUMNS
|
|
2380
|
-
});
|
|
2381
2269
|
const ints = await this.store.query({
|
|
2382
2270
|
collection: INTEGRATIONS,
|
|
2383
2271
|
filter: {
|
|
@@ -2392,20 +2280,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2392
2280
|
const num = parseInt(ints[0].id.replace("int_", ""), 10);
|
|
2393
2281
|
if (!isNaN(num)) integrationCounter = num;
|
|
2394
2282
|
}
|
|
2395
|
-
const devs = await this.store.query({
|
|
2396
|
-
collection: DEVICES,
|
|
2397
|
-
filter: {
|
|
2398
|
-
orderBy: {
|
|
2399
|
-
field: "id",
|
|
2400
|
-
direction: "desc"
|
|
2401
|
-
},
|
|
2402
|
-
limit: 1
|
|
2403
|
-
}
|
|
2404
|
-
});
|
|
2405
|
-
if (devs[0]) {
|
|
2406
|
-
const num = parseInt(devs[0].id.replace("dev_", ""), 10);
|
|
2407
|
-
if (!isNaN(num)) deviceCounter = num;
|
|
2408
|
-
}
|
|
2409
2283
|
}
|
|
2410
2284
|
async createIntegration(input) {
|
|
2411
2285
|
const id = nextIntegrationId();
|
|
@@ -2489,15 +2363,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2489
2363
|
async deleteIntegration(id) {
|
|
2490
2364
|
const integration = await this.getIntegration(id);
|
|
2491
2365
|
if (integration !== null && IntegrationRegistry.isFixed(integration)) throw new Error(`integration "${id}" is fixed — it belongs to the addon that declared it and cannot be deleted`);
|
|
2492
|
-
const devices = await this.listDevices(id);
|
|
2493
|
-
for (const d of devices) await this.store.deleteWhere({
|
|
2494
|
-
collection: DEVICE_SETTINGS,
|
|
2495
|
-
filter: { where: { device_id: d.id } }
|
|
2496
|
-
});
|
|
2497
|
-
await this.store.deleteWhere({
|
|
2498
|
-
collection: DEVICES,
|
|
2499
|
-
filter: { where: { integration_id: id } }
|
|
2500
|
-
});
|
|
2501
2366
|
await this.store.deleteWhere({
|
|
2502
2367
|
collection: INTEGRATION_SETTINGS,
|
|
2503
2368
|
filter: { where: { integration_id: id } }
|
|
@@ -2542,139 +2407,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2542
2407
|
async setIntegrationSettings(integrationId, settings) {
|
|
2543
2408
|
for (const [key, value] of Object.entries(settings)) await this.setIntegrationSetting(integrationId, key, value);
|
|
2544
2409
|
}
|
|
2545
|
-
async createDevice(input) {
|
|
2546
|
-
const id = nextDeviceId();
|
|
2547
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
2548
|
-
await this.store.insert({
|
|
2549
|
-
collection: DEVICES,
|
|
2550
|
-
record: {
|
|
2551
|
-
id,
|
|
2552
|
-
data: {
|
|
2553
|
-
integration_id: input.integrationId,
|
|
2554
|
-
stable_id: input.stableId,
|
|
2555
|
-
type: input.type,
|
|
2556
|
-
name: input.name,
|
|
2557
|
-
enabled: input.enabled !== false ? 1 : 0,
|
|
2558
|
-
info: input.info ?? {},
|
|
2559
|
-
created_at: now,
|
|
2560
|
-
updated_at: now
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
});
|
|
2564
|
-
if (input.settings) await this.setDeviceSettings(id, input.settings);
|
|
2565
|
-
return {
|
|
2566
|
-
id,
|
|
2567
|
-
integrationId: input.integrationId,
|
|
2568
|
-
stableId: input.stableId,
|
|
2569
|
-
type: input.type,
|
|
2570
|
-
name: input.name,
|
|
2571
|
-
enabled: input.enabled !== false,
|
|
2572
|
-
info: input.info ?? {},
|
|
2573
|
-
createdAt: now,
|
|
2574
|
-
updatedAt: now
|
|
2575
|
-
};
|
|
2576
|
-
}
|
|
2577
|
-
async getDevice(id) {
|
|
2578
|
-
const rows = await this.store.query({
|
|
2579
|
-
collection: DEVICES,
|
|
2580
|
-
filter: {
|
|
2581
|
-
where: { id },
|
|
2582
|
-
limit: 1
|
|
2583
|
-
}
|
|
2584
|
-
});
|
|
2585
|
-
return rows[0] ? this.mapDevice(rows[0].data) : null;
|
|
2586
|
-
}
|
|
2587
|
-
async getDeviceByStableId(stableId) {
|
|
2588
|
-
const rows = await this.store.query({
|
|
2589
|
-
collection: DEVICES,
|
|
2590
|
-
filter: {
|
|
2591
|
-
where: { stable_id: stableId },
|
|
2592
|
-
limit: 1
|
|
2593
|
-
}
|
|
2594
|
-
});
|
|
2595
|
-
return rows[0] ? this.mapDevice(rows[0].data) : null;
|
|
2596
|
-
}
|
|
2597
|
-
async listDevices(integrationId) {
|
|
2598
|
-
return (await this.store.query({
|
|
2599
|
-
collection: DEVICES,
|
|
2600
|
-
filter: {
|
|
2601
|
-
...integrationId ? { where: { integration_id: integrationId } } : {},
|
|
2602
|
-
orderBy: {
|
|
2603
|
-
field: "created_at",
|
|
2604
|
-
direction: "asc"
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
})).map((r) => this.mapDevice(r.data));
|
|
2608
|
-
}
|
|
2609
|
-
async listCameras() {
|
|
2610
|
-
return (await this.store.query({
|
|
2611
|
-
collection: DEVICES,
|
|
2612
|
-
filter: {
|
|
2613
|
-
where: { type: "camera" },
|
|
2614
|
-
orderBy: {
|
|
2615
|
-
field: "created_at",
|
|
2616
|
-
direction: "asc"
|
|
2617
|
-
}
|
|
2618
|
-
}
|
|
2619
|
-
})).map((r) => this.mapDevice(r.data));
|
|
2620
|
-
}
|
|
2621
|
-
async updateDevice(id, updates) {
|
|
2622
|
-
const updateRow = { updated_at: Math.floor(Date.now() / 1e3) };
|
|
2623
|
-
if (updates.name !== void 0) updateRow["name"] = updates.name;
|
|
2624
|
-
if (updates.enabled !== void 0) updateRow["enabled"] = updates.enabled ? 1 : 0;
|
|
2625
|
-
if (updates.info !== void 0) updateRow["info"] = updates.info;
|
|
2626
|
-
await this.store.updateWhere({
|
|
2627
|
-
collection: DEVICES,
|
|
2628
|
-
filter: { where: { id } },
|
|
2629
|
-
data: updateRow
|
|
2630
|
-
});
|
|
2631
|
-
return this.getDevice(id);
|
|
2632
|
-
}
|
|
2633
|
-
async deleteDevice(id) {
|
|
2634
|
-
await this.store.deleteWhere({
|
|
2635
|
-
collection: DEVICE_SETTINGS,
|
|
2636
|
-
filter: { where: { device_id: id } }
|
|
2637
|
-
});
|
|
2638
|
-
await this.store.deleteWhere({
|
|
2639
|
-
collection: DEVICES,
|
|
2640
|
-
filter: { where: { id } }
|
|
2641
|
-
});
|
|
2642
|
-
return true;
|
|
2643
|
-
}
|
|
2644
|
-
async getDeviceSettings(deviceId) {
|
|
2645
|
-
const result = {};
|
|
2646
|
-
const rows = await this.store.query({
|
|
2647
|
-
collection: DEVICE_SETTINGS,
|
|
2648
|
-
filter: { where: { device_id: deviceId } }
|
|
2649
|
-
});
|
|
2650
|
-
for (const row of rows) result[String(row.data["key"])] = deserializeSetting(row.data["value"], String(row.data["value_type"]));
|
|
2651
|
-
return result;
|
|
2652
|
-
}
|
|
2653
|
-
async setDeviceSetting(deviceId, key, value) {
|
|
2654
|
-
const s = serializeSetting(value);
|
|
2655
|
-
await this.store.deleteWhere({
|
|
2656
|
-
collection: DEVICE_SETTINGS,
|
|
2657
|
-
filter: { where: {
|
|
2658
|
-
device_id: deviceId,
|
|
2659
|
-
key
|
|
2660
|
-
} }
|
|
2661
|
-
});
|
|
2662
|
-
await this.store.insert({
|
|
2663
|
-
collection: DEVICE_SETTINGS,
|
|
2664
|
-
record: {
|
|
2665
|
-
id: `${deviceId}:${key}`,
|
|
2666
|
-
data: {
|
|
2667
|
-
device_id: deviceId,
|
|
2668
|
-
key,
|
|
2669
|
-
value: s.value,
|
|
2670
|
-
value_type: s.valueType
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
});
|
|
2674
|
-
}
|
|
2675
|
-
async setDeviceSettings(deviceId, settings) {
|
|
2676
|
-
for (const [key, value] of Object.entries(settings)) await this.setDeviceSetting(deviceId, key, value);
|
|
2677
|
-
}
|
|
2678
2410
|
mapIntegration(row) {
|
|
2679
2411
|
return {
|
|
2680
2412
|
id: String(row["id"]),
|
|
@@ -2686,19 +2418,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2686
2418
|
updatedAt: Number(row["updated_at"])
|
|
2687
2419
|
};
|
|
2688
2420
|
}
|
|
2689
|
-
mapDevice(row) {
|
|
2690
|
-
return {
|
|
2691
|
-
id: String(row["id"]),
|
|
2692
|
-
integrationId: String(row["integration_id"]),
|
|
2693
|
-
stableId: String(row["stable_id"]),
|
|
2694
|
-
type: String(row["type"]),
|
|
2695
|
-
name: String(row["name"]),
|
|
2696
|
-
enabled: row["enabled"] === 1,
|
|
2697
|
-
info: readInfo(row["info"]),
|
|
2698
|
-
createdAt: Number(row["created_at"]),
|
|
2699
|
-
updatedAt: Number(row["updated_at"])
|
|
2700
|
-
};
|
|
2701
|
-
}
|
|
2702
2421
|
};
|
|
2703
2422
|
//#endregion
|
|
2704
2423
|
//#region src/kernel/deps/ensure-native-prebuilds.ts
|
package/dist/index.mjs
CHANGED
|
@@ -2144,13 +2144,9 @@ function readInfo(raw) {
|
|
|
2144
2144
|
return {};
|
|
2145
2145
|
}
|
|
2146
2146
|
var integrationCounter = 0;
|
|
2147
|
-
var deviceCounter = 0;
|
|
2148
2147
|
function nextIntegrationId() {
|
|
2149
2148
|
return `int_${String(++integrationCounter).padStart(4, "0")}`;
|
|
2150
2149
|
}
|
|
2151
|
-
function nextDeviceId() {
|
|
2152
|
-
return `dev_${String(++deviceCounter).padStart(4, "0")}`;
|
|
2153
|
-
}
|
|
2154
2150
|
var INTEGRATIONS_COLUMNS = [
|
|
2155
2151
|
{
|
|
2156
2152
|
name: "id",
|
|
@@ -2217,95 +2213,6 @@ var INTEGRATION_SETTINGS_COLUMNS = [
|
|
|
2217
2213
|
defaultValue: "string"
|
|
2218
2214
|
}
|
|
2219
2215
|
];
|
|
2220
|
-
var DEVICES_COLUMNS = [
|
|
2221
|
-
{
|
|
2222
|
-
name: "id",
|
|
2223
|
-
type: "TEXT",
|
|
2224
|
-
primaryKey: true
|
|
2225
|
-
},
|
|
2226
|
-
{
|
|
2227
|
-
name: "integration_id",
|
|
2228
|
-
type: "TEXT",
|
|
2229
|
-
notNull: true
|
|
2230
|
-
},
|
|
2231
|
-
{
|
|
2232
|
-
name: "stable_id",
|
|
2233
|
-
type: "TEXT",
|
|
2234
|
-
notNull: true,
|
|
2235
|
-
unique: true
|
|
2236
|
-
},
|
|
2237
|
-
{
|
|
2238
|
-
name: "type",
|
|
2239
|
-
type: "TEXT",
|
|
2240
|
-
notNull: true,
|
|
2241
|
-
defaultValue: "camera"
|
|
2242
|
-
},
|
|
2243
|
-
{
|
|
2244
|
-
name: "name",
|
|
2245
|
-
type: "TEXT",
|
|
2246
|
-
notNull: true
|
|
2247
|
-
},
|
|
2248
|
-
{
|
|
2249
|
-
name: "enabled",
|
|
2250
|
-
type: "INTEGER",
|
|
2251
|
-
notNull: true,
|
|
2252
|
-
defaultValue: 1
|
|
2253
|
-
},
|
|
2254
|
-
{
|
|
2255
|
-
name: "info",
|
|
2256
|
-
type: "JSON",
|
|
2257
|
-
notNull: true,
|
|
2258
|
-
defaultValue: "{}"
|
|
2259
|
-
},
|
|
2260
|
-
{
|
|
2261
|
-
name: "created_at",
|
|
2262
|
-
type: "INTEGER",
|
|
2263
|
-
notNull: true
|
|
2264
|
-
},
|
|
2265
|
-
{
|
|
2266
|
-
name: "updated_at",
|
|
2267
|
-
type: "INTEGER",
|
|
2268
|
-
notNull: true
|
|
2269
|
-
}
|
|
2270
|
-
];
|
|
2271
|
-
var DEVICES_INDEXES = [
|
|
2272
|
-
{
|
|
2273
|
-
name: "idx_devices_integration",
|
|
2274
|
-
columns: ["integration_id"]
|
|
2275
|
-
},
|
|
2276
|
-
{
|
|
2277
|
-
name: "idx_devices_stable",
|
|
2278
|
-
columns: ["stable_id"],
|
|
2279
|
-
unique: true
|
|
2280
|
-
},
|
|
2281
|
-
{
|
|
2282
|
-
name: "idx_devices_type",
|
|
2283
|
-
columns: ["type"]
|
|
2284
|
-
}
|
|
2285
|
-
];
|
|
2286
|
-
var DEVICE_SETTINGS_COLUMNS = [
|
|
2287
|
-
{
|
|
2288
|
-
name: "device_id",
|
|
2289
|
-
type: "TEXT",
|
|
2290
|
-
notNull: true
|
|
2291
|
-
},
|
|
2292
|
-
{
|
|
2293
|
-
name: "key",
|
|
2294
|
-
type: "TEXT",
|
|
2295
|
-
notNull: true
|
|
2296
|
-
},
|
|
2297
|
-
{
|
|
2298
|
-
name: "value",
|
|
2299
|
-
type: "TEXT",
|
|
2300
|
-
notNull: true
|
|
2301
|
-
},
|
|
2302
|
-
{
|
|
2303
|
-
name: "value_type",
|
|
2304
|
-
type: "TEXT",
|
|
2305
|
-
notNull: true,
|
|
2306
|
-
defaultValue: "string"
|
|
2307
|
-
}
|
|
2308
|
-
];
|
|
2309
2216
|
/**
|
|
2310
2217
|
* @durable class=config owner=integration-registry
|
|
2311
2218
|
* write="`createIntegration` — an operator adds a provider, or an addon declares
|
|
@@ -2324,29 +2231,19 @@ var INTEGRATIONS = "integrations";
|
|
|
2324
2231
|
*/
|
|
2325
2232
|
var INTEGRATION_SETTINGS = "integration_settings";
|
|
2326
2233
|
/**
|
|
2327
|
-
*
|
|
2328
|
-
*
|
|
2329
|
-
* same physical camera from creating a second device row. An insert that
|
|
2330
|
-
* collides throws rather than duplicating.
|
|
2234
|
+
* The persisted INTEGRATIONS — one row per provider instance an operator (or a
|
|
2235
|
+
* declaring addon) added, plus that instance's settings.
|
|
2331
2236
|
*
|
|
2332
|
-
*
|
|
2333
|
-
*
|
|
2334
|
-
*
|
|
2335
|
-
*
|
|
2336
|
-
*
|
|
2337
|
-
*
|
|
2338
|
-
*
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
/**
|
|
2342
|
-
* @durable class=config owner=integration-registry
|
|
2343
|
-
* write="`setDeviceSetting` — one row per (device_id, key), deleted and
|
|
2344
|
-
* re-inserted on every write; per-camera operator intent lands here"
|
|
2345
|
-
* retention="cascade only — `deleteDevice` and `deleteIntegration` remove a
|
|
2346
|
-
* device's rows. No sweep; losing the table silently reverts every camera to
|
|
2347
|
-
* its defaults."
|
|
2237
|
+
* It had a device half once: `devices` + `device_settings_kv`, `dev_NNNN` ids,
|
|
2238
|
+
* `createDevice` / `listCameras` / `setDeviceSetting`. It was removed on
|
|
2239
|
+
* 2026-08-19 with both tables ([D183](../../../../../docs/decisions/adr-0183-code-that-is-deleted-takes-its-tables-with-it.md)):
|
|
2240
|
+
* measured on the reference hub it held 0 rows against 974 live devices, no
|
|
2241
|
+
* production caller ever reached it, and its id-space could not be reconciled
|
|
2242
|
+
* with the numeric ids every track, rule and binding in the system uses. The
|
|
2243
|
+
* fleet is `device-manager:devices`. Do not add a device method here — the
|
|
2244
|
+
* device authority is the device-manager, and a second one that agrees with
|
|
2245
|
+
* nothing is how this half came to exist.
|
|
2348
2246
|
*/
|
|
2349
|
-
var DEVICE_SETTINGS = "device_settings_kv";
|
|
2350
2247
|
var IntegrationRegistry = class IntegrationRegistry {
|
|
2351
2248
|
store;
|
|
2352
2249
|
constructor(backend) {
|
|
@@ -2362,15 +2259,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2362
2259
|
collection: INTEGRATION_SETTINGS,
|
|
2363
2260
|
columns: INTEGRATION_SETTINGS_COLUMNS
|
|
2364
2261
|
});
|
|
2365
|
-
await this.store.declareCollection({
|
|
2366
|
-
collection: DEVICES,
|
|
2367
|
-
columns: DEVICES_COLUMNS,
|
|
2368
|
-
indexes: DEVICES_INDEXES
|
|
2369
|
-
});
|
|
2370
|
-
await this.store.declareCollection({
|
|
2371
|
-
collection: DEVICE_SETTINGS,
|
|
2372
|
-
columns: DEVICE_SETTINGS_COLUMNS
|
|
2373
|
-
});
|
|
2374
2262
|
const ints = await this.store.query({
|
|
2375
2263
|
collection: INTEGRATIONS,
|
|
2376
2264
|
filter: {
|
|
@@ -2385,20 +2273,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2385
2273
|
const num = parseInt(ints[0].id.replace("int_", ""), 10);
|
|
2386
2274
|
if (!isNaN(num)) integrationCounter = num;
|
|
2387
2275
|
}
|
|
2388
|
-
const devs = await this.store.query({
|
|
2389
|
-
collection: DEVICES,
|
|
2390
|
-
filter: {
|
|
2391
|
-
orderBy: {
|
|
2392
|
-
field: "id",
|
|
2393
|
-
direction: "desc"
|
|
2394
|
-
},
|
|
2395
|
-
limit: 1
|
|
2396
|
-
}
|
|
2397
|
-
});
|
|
2398
|
-
if (devs[0]) {
|
|
2399
|
-
const num = parseInt(devs[0].id.replace("dev_", ""), 10);
|
|
2400
|
-
if (!isNaN(num)) deviceCounter = num;
|
|
2401
|
-
}
|
|
2402
2276
|
}
|
|
2403
2277
|
async createIntegration(input) {
|
|
2404
2278
|
const id = nextIntegrationId();
|
|
@@ -2482,15 +2356,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2482
2356
|
async deleteIntegration(id) {
|
|
2483
2357
|
const integration = await this.getIntegration(id);
|
|
2484
2358
|
if (integration !== null && IntegrationRegistry.isFixed(integration)) throw new Error(`integration "${id}" is fixed — it belongs to the addon that declared it and cannot be deleted`);
|
|
2485
|
-
const devices = await this.listDevices(id);
|
|
2486
|
-
for (const d of devices) await this.store.deleteWhere({
|
|
2487
|
-
collection: DEVICE_SETTINGS,
|
|
2488
|
-
filter: { where: { device_id: d.id } }
|
|
2489
|
-
});
|
|
2490
|
-
await this.store.deleteWhere({
|
|
2491
|
-
collection: DEVICES,
|
|
2492
|
-
filter: { where: { integration_id: id } }
|
|
2493
|
-
});
|
|
2494
2359
|
await this.store.deleteWhere({
|
|
2495
2360
|
collection: INTEGRATION_SETTINGS,
|
|
2496
2361
|
filter: { where: { integration_id: id } }
|
|
@@ -2535,139 +2400,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2535
2400
|
async setIntegrationSettings(integrationId, settings) {
|
|
2536
2401
|
for (const [key, value] of Object.entries(settings)) await this.setIntegrationSetting(integrationId, key, value);
|
|
2537
2402
|
}
|
|
2538
|
-
async createDevice(input) {
|
|
2539
|
-
const id = nextDeviceId();
|
|
2540
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
2541
|
-
await this.store.insert({
|
|
2542
|
-
collection: DEVICES,
|
|
2543
|
-
record: {
|
|
2544
|
-
id,
|
|
2545
|
-
data: {
|
|
2546
|
-
integration_id: input.integrationId,
|
|
2547
|
-
stable_id: input.stableId,
|
|
2548
|
-
type: input.type,
|
|
2549
|
-
name: input.name,
|
|
2550
|
-
enabled: input.enabled !== false ? 1 : 0,
|
|
2551
|
-
info: input.info ?? {},
|
|
2552
|
-
created_at: now,
|
|
2553
|
-
updated_at: now
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
});
|
|
2557
|
-
if (input.settings) await this.setDeviceSettings(id, input.settings);
|
|
2558
|
-
return {
|
|
2559
|
-
id,
|
|
2560
|
-
integrationId: input.integrationId,
|
|
2561
|
-
stableId: input.stableId,
|
|
2562
|
-
type: input.type,
|
|
2563
|
-
name: input.name,
|
|
2564
|
-
enabled: input.enabled !== false,
|
|
2565
|
-
info: input.info ?? {},
|
|
2566
|
-
createdAt: now,
|
|
2567
|
-
updatedAt: now
|
|
2568
|
-
};
|
|
2569
|
-
}
|
|
2570
|
-
async getDevice(id) {
|
|
2571
|
-
const rows = await this.store.query({
|
|
2572
|
-
collection: DEVICES,
|
|
2573
|
-
filter: {
|
|
2574
|
-
where: { id },
|
|
2575
|
-
limit: 1
|
|
2576
|
-
}
|
|
2577
|
-
});
|
|
2578
|
-
return rows[0] ? this.mapDevice(rows[0].data) : null;
|
|
2579
|
-
}
|
|
2580
|
-
async getDeviceByStableId(stableId) {
|
|
2581
|
-
const rows = await this.store.query({
|
|
2582
|
-
collection: DEVICES,
|
|
2583
|
-
filter: {
|
|
2584
|
-
where: { stable_id: stableId },
|
|
2585
|
-
limit: 1
|
|
2586
|
-
}
|
|
2587
|
-
});
|
|
2588
|
-
return rows[0] ? this.mapDevice(rows[0].data) : null;
|
|
2589
|
-
}
|
|
2590
|
-
async listDevices(integrationId) {
|
|
2591
|
-
return (await this.store.query({
|
|
2592
|
-
collection: DEVICES,
|
|
2593
|
-
filter: {
|
|
2594
|
-
...integrationId ? { where: { integration_id: integrationId } } : {},
|
|
2595
|
-
orderBy: {
|
|
2596
|
-
field: "created_at",
|
|
2597
|
-
direction: "asc"
|
|
2598
|
-
}
|
|
2599
|
-
}
|
|
2600
|
-
})).map((r) => this.mapDevice(r.data));
|
|
2601
|
-
}
|
|
2602
|
-
async listCameras() {
|
|
2603
|
-
return (await this.store.query({
|
|
2604
|
-
collection: DEVICES,
|
|
2605
|
-
filter: {
|
|
2606
|
-
where: { type: "camera" },
|
|
2607
|
-
orderBy: {
|
|
2608
|
-
field: "created_at",
|
|
2609
|
-
direction: "asc"
|
|
2610
|
-
}
|
|
2611
|
-
}
|
|
2612
|
-
})).map((r) => this.mapDevice(r.data));
|
|
2613
|
-
}
|
|
2614
|
-
async updateDevice(id, updates) {
|
|
2615
|
-
const updateRow = { updated_at: Math.floor(Date.now() / 1e3) };
|
|
2616
|
-
if (updates.name !== void 0) updateRow["name"] = updates.name;
|
|
2617
|
-
if (updates.enabled !== void 0) updateRow["enabled"] = updates.enabled ? 1 : 0;
|
|
2618
|
-
if (updates.info !== void 0) updateRow["info"] = updates.info;
|
|
2619
|
-
await this.store.updateWhere({
|
|
2620
|
-
collection: DEVICES,
|
|
2621
|
-
filter: { where: { id } },
|
|
2622
|
-
data: updateRow
|
|
2623
|
-
});
|
|
2624
|
-
return this.getDevice(id);
|
|
2625
|
-
}
|
|
2626
|
-
async deleteDevice(id) {
|
|
2627
|
-
await this.store.deleteWhere({
|
|
2628
|
-
collection: DEVICE_SETTINGS,
|
|
2629
|
-
filter: { where: { device_id: id } }
|
|
2630
|
-
});
|
|
2631
|
-
await this.store.deleteWhere({
|
|
2632
|
-
collection: DEVICES,
|
|
2633
|
-
filter: { where: { id } }
|
|
2634
|
-
});
|
|
2635
|
-
return true;
|
|
2636
|
-
}
|
|
2637
|
-
async getDeviceSettings(deviceId) {
|
|
2638
|
-
const result = {};
|
|
2639
|
-
const rows = await this.store.query({
|
|
2640
|
-
collection: DEVICE_SETTINGS,
|
|
2641
|
-
filter: { where: { device_id: deviceId } }
|
|
2642
|
-
});
|
|
2643
|
-
for (const row of rows) result[String(row.data["key"])] = deserializeSetting(row.data["value"], String(row.data["value_type"]));
|
|
2644
|
-
return result;
|
|
2645
|
-
}
|
|
2646
|
-
async setDeviceSetting(deviceId, key, value) {
|
|
2647
|
-
const s = serializeSetting(value);
|
|
2648
|
-
await this.store.deleteWhere({
|
|
2649
|
-
collection: DEVICE_SETTINGS,
|
|
2650
|
-
filter: { where: {
|
|
2651
|
-
device_id: deviceId,
|
|
2652
|
-
key
|
|
2653
|
-
} }
|
|
2654
|
-
});
|
|
2655
|
-
await this.store.insert({
|
|
2656
|
-
collection: DEVICE_SETTINGS,
|
|
2657
|
-
record: {
|
|
2658
|
-
id: `${deviceId}:${key}`,
|
|
2659
|
-
data: {
|
|
2660
|
-
device_id: deviceId,
|
|
2661
|
-
key,
|
|
2662
|
-
value: s.value,
|
|
2663
|
-
value_type: s.valueType
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
});
|
|
2667
|
-
}
|
|
2668
|
-
async setDeviceSettings(deviceId, settings) {
|
|
2669
|
-
for (const [key, value] of Object.entries(settings)) await this.setDeviceSetting(deviceId, key, value);
|
|
2670
|
-
}
|
|
2671
2403
|
mapIntegration(row) {
|
|
2672
2404
|
return {
|
|
2673
2405
|
id: String(row["id"]),
|
|
@@ -2679,19 +2411,6 @@ var IntegrationRegistry = class IntegrationRegistry {
|
|
|
2679
2411
|
updatedAt: Number(row["updated_at"])
|
|
2680
2412
|
};
|
|
2681
2413
|
}
|
|
2682
|
-
mapDevice(row) {
|
|
2683
|
-
return {
|
|
2684
|
-
id: String(row["id"]),
|
|
2685
|
-
integrationId: String(row["integration_id"]),
|
|
2686
|
-
stableId: String(row["stable_id"]),
|
|
2687
|
-
type: String(row["type"]),
|
|
2688
|
-
name: String(row["name"]),
|
|
2689
|
-
enabled: row["enabled"] === 1,
|
|
2690
|
-
info: readInfo(row["info"]),
|
|
2691
|
-
createdAt: Number(row["created_at"]),
|
|
2692
|
-
updatedAt: Number(row["updated_at"])
|
|
2693
|
-
};
|
|
2694
|
-
}
|
|
2695
2414
|
};
|
|
2696
2415
|
//#endregion
|
|
2697
2416
|
//#region src/kernel/deps/ensure-native-prebuilds.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/system",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.109",
|
|
4
4
|
"description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -349,7 +349,6 @@
|
|
|
349
349
|
},
|
|
350
350
|
"dependencies": {
|
|
351
351
|
"@camstack/sdk": "*",
|
|
352
|
-
"@camstack/shm-ring": "*",
|
|
353
352
|
"@camstack/types": "*",
|
|
354
353
|
"@msgpack/msgpack": "^3.1.3",
|
|
355
354
|
"@trpc/client": "^11.16.0",
|