@camstack/system 1.2.29 → 1.2.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  12. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  13. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  14. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  15. package/dist/builtins/hub-forwarder/index.js +1 -1
  16. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  17. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  18. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  19. package/dist/builtins/local-network/local-network.addon.js +1 -1
  20. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  21. package/dist/builtins/loki-logging/index.js +1 -1
  22. package/dist/builtins/loki-logging/index.mjs +1 -1
  23. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  24. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  25. package/dist/builtins/platform-probe/index.js +1 -1
  26. package/dist/builtins/platform-probe/index.mjs +1 -1
  27. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  28. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  29. package/dist/builtins/snapshot/index.js +1 -1
  30. package/dist/builtins/snapshot/index.mjs +1 -1
  31. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  32. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  33. package/dist/builtins/sqlite-storage/filter-compiler.d.ts +61 -0
  34. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +43 -0
  35. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +115 -61
  36. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +115 -61
  37. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +3 -1
  38. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +3 -1
  39. package/dist/builtins/system-config/system-config.addon.js +1 -1
  40. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  41. package/dist/builtins/winston-logging/index.js +1 -1
  42. package/dist/builtins/winston-logging/index.mjs +1 -1
  43. package/dist/{dist-B7hOpr5i.mjs → dist-Dr_P0DOB.mjs} +81 -0
  44. package/dist/{dist-CPmFYLqq.js → dist-VIwdvws5.js} +81 -0
  45. package/dist/index.js +45 -1
  46. package/dist/index.mjs +45 -1
  47. package/dist/kernel/addon-installer.d.ts +38 -0
  48. package/package.json +1 -1
@@ -1,6 +1,62 @@
1
- import { Dt as parseJsonUnknown, at as errMsg, c as RUNTIME_DEFAULTS, gt as asJsonObject, ot as BaseAddon, x as dataStoreProviderCapability } from "../../dist-B7hOpr5i.mjs";
1
+ import { Dt as parseJsonUnknown, at as errMsg, c as RUNTIME_DEFAULTS, gt as asJsonObject, ot as BaseAddon, x as dataStoreProviderCapability } from "../../dist-Dr_P0DOB.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import Database from "better-sqlite3";
4
+ //#region src/builtins/sqlite-storage/filter-compiler.ts
5
+ /** Thrown by `mutate` mode. Distinct type so a caller can map it to a 400. */
6
+ var UnsafeFilterError = class extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "UnsafeFilterError";
10
+ }
11
+ };
12
+ /**
13
+ * SQL expression for `field` on this collection, or `null` when it cannot be
14
+ * expressed: a real column → `"field"`; a KV blob field → `json_extract`;
15
+ * anything else on a structured table → unresolvable. Exported because
16
+ * `histogram` needs the same resolution for its bucketed column.
17
+ */
18
+ function fieldExprFor(field, shape) {
19
+ if (field === shape.primaryKey || shape.columns.has(field)) return `"${field}"`;
20
+ if (shape.kvBlobColumn !== null) return `json_extract("${shape.kvBlobColumn}", '$.${field}')`;
21
+ return null;
22
+ }
23
+ function compileFilter(filter, shape, mode, serialize) {
24
+ const clauses = [];
25
+ const params = [];
26
+ const resolve = (field) => {
27
+ const expr = fieldExprFor(field, shape);
28
+ if (expr === null && mode === "mutate") throw new UnsafeFilterError(`filter refers to "${field}", which this collection cannot express — refusing to run a bulk mutation with a dropped predicate`);
29
+ return expr;
30
+ };
31
+ for (const [field, value] of Object.entries(filter?.where ?? {})) {
32
+ const expr = resolve(field);
33
+ if (expr === null) continue;
34
+ clauses.push(`${expr} = ?`);
35
+ params.push(serialize(value));
36
+ }
37
+ for (const [field, values] of Object.entries(filter?.whereIn ?? {})) {
38
+ const expr = resolve(field);
39
+ if (expr === null) continue;
40
+ if (values.length === 0) {
41
+ if (mode === "mutate") throw new UnsafeFilterError(`filter has an empty whereIn list for "${field}" — it matches nothing, which is more likely a caller bug than an intent`);
42
+ continue;
43
+ }
44
+ clauses.push(`${expr} IN (${values.map(() => "?").join(", ")})`);
45
+ for (const v of values) params.push(serialize(v));
46
+ }
47
+ for (const [field, [low, high]] of Object.entries(filter?.whereBetween ?? {})) {
48
+ const expr = resolve(field);
49
+ if (expr === null) continue;
50
+ clauses.push(`${expr} BETWEEN ? AND ?`);
51
+ params.push(serialize(low), serialize(high));
52
+ }
53
+ if (clauses.length === 0 && mode === "mutate") throw new UnsafeFilterError("filter compiled to no predicate — that would affect every row in the collection; use the explicit clear operation if that is the intent");
54
+ return {
55
+ whereSql: clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "",
56
+ params
57
+ };
58
+ }
59
+ //#endregion
4
60
  //#region src/builtins/sqlite-storage/sqlite-settings-backend.ts
5
61
  function parseRowData(raw) {
6
62
  return asJsonObject(parseJsonUnknown(raw)) ?? {};
@@ -183,6 +239,41 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
183
239
  const decl = this.requireDeclared(scoped);
184
240
  await this.tableDelete(scoped, { [decl.primaryKey]: key });
185
241
  }
242
+ /**
243
+ * Bulk delete. One statement, whatever the row count — this is what every
244
+ * retention path in the system was hand-rolling as a select-then-delete
245
+ * loop. The filter is compiled in `mutate` mode, so an unresolvable
246
+ * predicate or an empty one throws before any SQL exists.
247
+ */
248
+ async deleteWhere({ namespace, collection, filter }) {
249
+ const scoped = this.scopedName(namespace, collection);
250
+ const decl = this.requireDeclared(scoped);
251
+ const { whereSql, params } = compileFilter(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
252
+ return { deleted: this.getDb().prepare(`DELETE FROM "${scoped}"${whereSql}`).run(...params).changes };
253
+ }
254
+ /**
255
+ * Bulk update. Same filter contract as {@link deleteWhere}.
256
+ *
257
+ * Every key of `data` must be a REAL column. A KV-shaped collection keeps
258
+ * its fields inside a JSON blob, and setting one would need `json_set` with
259
+ * read-modify-write semantics this method does not have — so it refuses
260
+ * rather than writing a column that does not exist, which is what the
261
+ * uncapped `tableUpdate` would have done.
262
+ */
263
+ async updateWhere({ namespace, collection, filter, data }) {
264
+ const scoped = this.scopedName(namespace, collection);
265
+ const decl = this.requireDeclared(scoped);
266
+ const setClauses = [];
267
+ const setValues = [];
268
+ for (const [key, value] of Object.entries(data)) {
269
+ if (key !== decl.primaryKey && !decl.columns.has(key)) throw new UnsafeFilterError(`updateWhere cannot set "${key}" on "${scoped}" — it is not a column of this collection`);
270
+ setClauses.push(`"${key}" = ?`);
271
+ setValues.push(this.serializeColumnValue(value));
272
+ }
273
+ if (setClauses.length === 0) throw new UnsafeFilterError("updateWhere was given no fields to set");
274
+ const { whereSql, params } = compileFilter(filter, this.shapeOf(decl), "mutate", (v) => this.serializeColumnValue(v));
275
+ return { updated: this.getDb().prepare(`UPDATE "${scoped}" SET ${setClauses.join(", ")}${whereSql}`).run(...setValues, ...params).changes };
276
+ }
186
277
  async count({ namespace, collection, filter }) {
187
278
  const scoped = this.scopedName(namespace, collection);
188
279
  const decl = this.requireDeclared(scoped);
@@ -210,30 +301,10 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
210
301
  async histogram({ namespace, collection, field, bucketSize, origin, filter }) {
211
302
  const scoped = this.scopedName(namespace, collection);
212
303
  const decl = this.requireDeclared(scoped);
213
- const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
214
- const isColumn = (f) => f === decl.primaryKey || decl.columns.has(f);
215
- const fieldExpr = (f) => {
216
- if (isColumn(f)) return `"${f}"`;
217
- if (isKvShape) return `json_extract("data", '$.${f}')`;
218
- return "";
219
- };
220
- const col = fieldExpr(field);
221
- if (!col) return [];
222
- const params = [];
223
- const clauses = [];
224
- if (filter?.where) for (const [f, value] of Object.entries(filter.where)) {
225
- const expr = fieldExpr(f);
226
- if (!expr) continue;
227
- clauses.push(`${expr} = ?`);
228
- params.push(this.serializeColumnValue(value));
229
- }
230
- if (filter?.whereBetween) for (const [f, [lo, hi]] of Object.entries(filter.whereBetween)) {
231
- const expr = fieldExpr(f);
232
- if (!expr) continue;
233
- clauses.push(`${expr} BETWEEN ? AND ?`);
234
- params.push(this.serializeColumnValue(lo), this.serializeColumnValue(hi));
235
- }
236
- const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
304
+ const shape = this.shapeOf(decl);
305
+ const col = fieldExprFor(field, shape);
306
+ if (col === null) return [];
307
+ const { whereSql: where, params } = compileFilter(filter, shape, "select", (v) => this.serializeColumnValue(v));
237
308
  const sql = `SELECT ${`CAST((${col} - ?) / ? AS INTEGER)`} AS bucket, COUNT(*) AS count FROM "${scoped}"${where} GROUP BY bucket ORDER BY bucket`;
238
309
  return this.getDb().prepare(sql).all(origin, bucketSize, ...params).map((r) => ({
239
310
  bucket: r.bucket,
@@ -249,43 +320,13 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
249
320
  const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
250
321
  let sql = `SELECT ${[`"${decl.primaryKey}"`, ...[...decl.columns].map((c) => `"${c}"`)].join(", ")} FROM "${table}"`;
251
322
  const params = [];
252
- const whereClauses = [];
253
- const isColumn = (f) => f === decl.primaryKey || decl.columns.has(f);
254
- /**
255
- * SQL expression for `field` on this table.
256
- * - real column → `"field"`
257
- * - KV blob field (e.g. `username` inside `data`) → `json_extract`
258
- * - structured table + non-column → empty string (filter dropped,
259
- * matches legacy structured-table behaviour)
260
- */
261
- const fieldExpr = (f) => {
262
- if (isColumn(f)) return `"${f}"`;
263
- if (isKvShape) return `json_extract("data", '$.${f}')`;
264
- return "";
265
- };
266
- if (filter?.where) for (const [field, value] of Object.entries(filter.where)) {
267
- const expr = fieldExpr(field);
268
- if (!expr) continue;
269
- whereClauses.push(`${expr} = ?`);
270
- params.push(this.serializeColumnValue(value));
271
- }
272
- if (filter?.whereIn) for (const [field, values] of Object.entries(filter.whereIn)) {
273
- const expr = fieldExpr(field);
274
- if (!expr) continue;
275
- const placeholders = values.map(() => "?").join(", ");
276
- whereClauses.push(`${expr} IN (${placeholders})`);
277
- for (const v of values) params.push(this.serializeColumnValue(v));
278
- }
279
- if (filter?.whereBetween) for (const [field, [low, high]] of Object.entries(filter.whereBetween)) {
280
- const expr = fieldExpr(field);
281
- if (!expr) continue;
282
- whereClauses.push(`${expr} BETWEEN ? AND ?`);
283
- params.push(this.serializeColumnValue(low), this.serializeColumnValue(high));
284
- }
285
- if (whereClauses.length > 0) sql += ` WHERE ${whereClauses.join(" AND ")}`;
323
+ const shape = this.shapeOf(decl);
324
+ const compiled = compileFilter(filter, shape, "select", (v) => this.serializeColumnValue(v));
325
+ sql += compiled.whereSql;
326
+ params.push(...compiled.params);
286
327
  if (filter?.orderBy) {
287
- const expr = fieldExpr(filter.orderBy.field);
288
- if (expr) {
328
+ const expr = fieldExprFor(filter.orderBy.field, shape);
329
+ if (expr !== null) {
289
330
  const dir = filter.orderBy.direction === "desc" ? "DESC" : "ASC";
290
331
  sql += ` ORDER BY ${expr} ${dir}`;
291
332
  }
@@ -461,6 +502,19 @@ var SqliteSettingsBackend = class SqliteSettingsBackend {
461
502
  scopedName(namespace, collection) {
462
503
  return namespace ? `${namespace}:${collection}` : collection;
463
504
  }
505
+ /**
506
+ * Project a declared collection onto what {@link compileFilter} needs. A
507
+ * collection whose only column is `data` is KV-shaped: its fields live
508
+ * inside that blob and are reached with `json_extract`.
509
+ */
510
+ shapeOf(decl) {
511
+ const isKvShape = decl.columns.size === 1 && decl.columns.has("data");
512
+ return {
513
+ primaryKey: decl.primaryKey,
514
+ columns: decl.columns,
515
+ kvBlobColumn: isKvShape ? "data" : null
516
+ };
517
+ }
464
518
  async declareCollection(input) {
465
519
  const table = this.scopedName(input.namespace, input.collection);
466
520
  if (this.declaredCollections.has(table)) return;
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CPmFYLqq.js");
6
+ const require_dist = require("../../dist-VIwdvws5.js");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_chunk.__toESM(node_path);
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -770,6 +770,8 @@ function createDataStoreDispatch(getEngines) {
770
770
  insert: async (input) => (await engine()).insert(input),
771
771
  update: async (input) => (await engine()).update(input),
772
772
  delete: async (input) => (await engine()).delete(input),
773
+ deleteWhere: async (input) => (await engine()).deleteWhere(input),
774
+ updateWhere: async (input) => (await engine()).updateWhere(input),
773
775
  count: async (input) => (await engine()).count(input),
774
776
  histogram: async (input) => (await engine()).histogram(input),
775
777
  isEmpty: async (input) => (await engine()).isEmpty(input),
@@ -1,4 +1,4 @@
1
- import { $ as storageCapability, Et as parseJsonObject, Z as settingsStoreCapability, d as StorageLocationTypeSchema, ot as BaseAddon } from "../../dist-B7hOpr5i.mjs";
1
+ import { $ as storageCapability, Et as parseJsonObject, Z as settingsStoreCapability, d as StorageLocationTypeSchema, ot as BaseAddon } from "../../dist-Dr_P0DOB.mjs";
2
2
  import * as path$1 from "node:path";
3
3
  import * as fs from "node:fs/promises";
4
4
  import { buildStorageLocationRegistry } from "@camstack/system";
@@ -763,6 +763,8 @@ function createDataStoreDispatch(getEngines) {
763
763
  insert: async (input) => (await engine()).insert(input),
764
764
  update: async (input) => (await engine()).update(input),
765
765
  delete: async (input) => (await engine()).delete(input),
766
+ deleteWhere: async (input) => (await engine()).deleteWhere(input),
767
+ updateWhere: async (input) => (await engine()).updateWhere(input),
766
768
  count: async (input) => (await engine()).count(input),
767
769
  histogram: async (input) => (await engine()).histogram(input),
768
770
  isEmpty: async (input) => (await engine()).isEmpty(input),
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CPmFYLqq.js");
6
+ const require_dist = require("../../dist-VIwdvws5.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { Ct as hydrateSchema, at as errMsg, ot as BaseAddon } from "../../dist-B7hOpr5i.mjs";
1
+ import { Ct as hydrateSchema, at as errMsg, ot as BaseAddon } from "../../dist-Dr_P0DOB.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CPmFYLqq.js");
6
+ const require_dist = require("../../dist-VIwdvws5.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { B as logDestinationCapability, ot as BaseAddon } from "../../dist-B7hOpr5i.mjs";
1
+ import { B as logDestinationCapability, ot as BaseAddon } from "../../dist-Dr_P0DOB.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";
@@ -14544,6 +14544,20 @@ var QueryFilterSchema = z.object({
14544
14544
  limit: z.number().optional(),
14545
14545
  offset: z.number().optional()
14546
14546
  });
14547
+ /**
14548
+ * The predicate half of a filter, for BULK MUTATIONS.
14549
+ *
14550
+ * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
14551
+ * meaning for a statement that rewrites a set, and accepting them would invite
14552
+ * a caller to believe `limit` bounds the damage. Every field is optional here
14553
+ * only so the shape stays composable — the implementation REJECTS a filter
14554
+ * that compiles to no predicate, because that is the whole collection.
14555
+ */
14556
+ var MutationFilterSchema = z.object({
14557
+ where: z.record(z.string(), z.unknown()).optional(),
14558
+ whereIn: z.record(z.string(), z.array(z.unknown())).optional(),
14559
+ whereBetween: z.record(z.string(), z.tuple([z.unknown(), z.unknown()])).optional()
14560
+ });
14547
14561
  /** A single stored record: `{ id, data }`. */
14548
14562
  var SettingsRecordSchema = z.object({
14549
14563
  id: z.string(),
@@ -14649,6 +14663,36 @@ var settingsStoreCapability = {
14649
14663
  collection: z.string(),
14650
14664
  key: z.string()
14651
14665
  }), z.void(), { kind: "mutation" }),
14666
+ /**
14667
+ * Delete every record matching `filter`, in ONE statement, returning how
14668
+ * many rows went. This exists because its absence made every retention
14669
+ * path in the system an N+1 drain loop: `delete` takes a key, so a sweep
14670
+ * had to SELECT a page of full rows — every column, including the fat
14671
+ * ones — purely to learn their ids, then issue one call per row.
14672
+ *
14673
+ * **The filter is required and must resolve.** A predicate naming
14674
+ * something the collection cannot express is an ERROR here, not a
14675
+ * widening as it is on `query`, and a filter with no predicates is an
14676
+ * error rather than "every row". Deleting a whole collection is a
14677
+ * legitimate intent, but it must be asked for by name — not reached by
14678
+ * an empty object.
14679
+ */
14680
+ deleteWhere: method(z.object({
14681
+ namespace: z.string().optional(),
14682
+ collection: z.string(),
14683
+ filter: MutationFilterSchema
14684
+ }), z.object({ deleted: z.number().int() }), { kind: "mutation" }),
14685
+ /**
14686
+ * Apply `data` to every record matching `filter`, in one statement,
14687
+ * returning how many rows changed. Same filter contract as
14688
+ * {@link deleteWhere} — an unresolvable predicate is an error.
14689
+ */
14690
+ updateWhere: method(z.object({
14691
+ namespace: z.string().optional(),
14692
+ collection: z.string(),
14693
+ filter: MutationFilterSchema,
14694
+ data: z.record(z.string(), z.unknown())
14695
+ }), z.object({ updated: z.number().int() }), { kind: "mutation" }),
14652
14696
  /** Count entries in a collection, optionally filtered. */
14653
14697
  count: method(z.object({
14654
14698
  namespace: z.string().optional(),
@@ -14784,6 +14828,19 @@ var dataStoreProviderCapability = {
14784
14828
  collection: z.string(),
14785
14829
  key: z.string()
14786
14830
  }), z.void(), { kind: "mutation" }),
14831
+ /** Delete every record matching `filter`, in one statement. */
14832
+ deleteWhere: method(z.object({
14833
+ namespace: z.string().optional(),
14834
+ collection: z.string(),
14835
+ filter: MutationFilterSchema
14836
+ }), z.object({ deleted: z.number().int() }), { kind: "mutation" }),
14837
+ /** Apply `data` to every record matching `filter`, in one statement. */
14838
+ updateWhere: method(z.object({
14839
+ namespace: z.string().optional(),
14840
+ collection: z.string(),
14841
+ filter: MutationFilterSchema,
14842
+ data: z.record(z.string(), z.unknown())
14843
+ }), z.object({ updated: z.number().int() }), { kind: "mutation" }),
14787
14844
  /** Count entries in a collection, optionally filtered. */
14788
14845
  count: method(z.object({
14789
14846
  namespace: z.string().optional(),
@@ -24963,6 +25020,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
24963
25020
  addonId: null,
24964
25021
  access: "delete"
24965
25022
  },
25023
+ "dataStoreProvider.deleteWhere": {
25024
+ capName: "data-store-provider",
25025
+ capScope: "system",
25026
+ addonId: null,
25027
+ access: "delete"
25028
+ },
24966
25029
  "dataStoreProvider.get": {
24967
25030
  capName: "data-store-provider",
24968
25031
  capScope: "system",
@@ -25011,6 +25074,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
25011
25074
  addonId: null,
25012
25075
  access: "create"
25013
25076
  },
25077
+ "dataStoreProvider.updateWhere": {
25078
+ capName: "data-store-provider",
25079
+ capScope: "system",
25080
+ addonId: null,
25081
+ access: "create"
25082
+ },
25014
25083
  "dayNight.getOptions": {
25015
25084
  capName: "day-night",
25016
25085
  capScope: "device",
@@ -28089,6 +28158,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
28089
28158
  addonId: null,
28090
28159
  access: "delete"
28091
28160
  },
28161
+ "settingsStore.deleteWhere": {
28162
+ capName: "settings-store",
28163
+ capScope: "system",
28164
+ addonId: null,
28165
+ access: "delete"
28166
+ },
28092
28167
  "settingsStore.get": {
28093
28168
  capName: "settings-store",
28094
28169
  capScope: "system",
@@ -28131,6 +28206,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
28131
28206
  addonId: null,
28132
28207
  access: "create"
28133
28208
  },
28209
+ "settingsStore.updateWhere": {
28210
+ capName: "settings-store",
28211
+ capScope: "system",
28212
+ addonId: null,
28213
+ access: "create"
28214
+ },
28134
28215
  "smtpProvider.getStatus": {
28135
28216
  capName: "smtp-provider",
28136
28217
  capScope: "system",
@@ -14544,6 +14544,20 @@ var QueryFilterSchema = zod.z.object({
14544
14544
  limit: zod.z.number().optional(),
14545
14545
  offset: zod.z.number().optional()
14546
14546
  });
14547
+ /**
14548
+ * The predicate half of a filter, for BULK MUTATIONS.
14549
+ *
14550
+ * Deliberately not `QueryFilterSchema`: `orderBy` / `limit` / `offset` have no
14551
+ * meaning for a statement that rewrites a set, and accepting them would invite
14552
+ * a caller to believe `limit` bounds the damage. Every field is optional here
14553
+ * only so the shape stays composable — the implementation REJECTS a filter
14554
+ * that compiles to no predicate, because that is the whole collection.
14555
+ */
14556
+ var MutationFilterSchema = zod.z.object({
14557
+ where: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
14558
+ whereIn: zod.z.record(zod.z.string(), zod.z.array(zod.z.unknown())).optional(),
14559
+ whereBetween: zod.z.record(zod.z.string(), zod.z.tuple([zod.z.unknown(), zod.z.unknown()])).optional()
14560
+ });
14547
14561
  /** A single stored record: `{ id, data }`. */
14548
14562
  var SettingsRecordSchema = zod.z.object({
14549
14563
  id: zod.z.string(),
@@ -14649,6 +14663,36 @@ var settingsStoreCapability = {
14649
14663
  collection: zod.z.string(),
14650
14664
  key: zod.z.string()
14651
14665
  }), zod.z.void(), { kind: "mutation" }),
14666
+ /**
14667
+ * Delete every record matching `filter`, in ONE statement, returning how
14668
+ * many rows went. This exists because its absence made every retention
14669
+ * path in the system an N+1 drain loop: `delete` takes a key, so a sweep
14670
+ * had to SELECT a page of full rows — every column, including the fat
14671
+ * ones — purely to learn their ids, then issue one call per row.
14672
+ *
14673
+ * **The filter is required and must resolve.** A predicate naming
14674
+ * something the collection cannot express is an ERROR here, not a
14675
+ * widening as it is on `query`, and a filter with no predicates is an
14676
+ * error rather than "every row". Deleting a whole collection is a
14677
+ * legitimate intent, but it must be asked for by name — not reached by
14678
+ * an empty object.
14679
+ */
14680
+ deleteWhere: method(zod.z.object({
14681
+ namespace: zod.z.string().optional(),
14682
+ collection: zod.z.string(),
14683
+ filter: MutationFilterSchema
14684
+ }), zod.z.object({ deleted: zod.z.number().int() }), { kind: "mutation" }),
14685
+ /**
14686
+ * Apply `data` to every record matching `filter`, in one statement,
14687
+ * returning how many rows changed. Same filter contract as
14688
+ * {@link deleteWhere} — an unresolvable predicate is an error.
14689
+ */
14690
+ updateWhere: method(zod.z.object({
14691
+ namespace: zod.z.string().optional(),
14692
+ collection: zod.z.string(),
14693
+ filter: MutationFilterSchema,
14694
+ data: zod.z.record(zod.z.string(), zod.z.unknown())
14695
+ }), zod.z.object({ updated: zod.z.number().int() }), { kind: "mutation" }),
14652
14696
  /** Count entries in a collection, optionally filtered. */
14653
14697
  count: method(zod.z.object({
14654
14698
  namespace: zod.z.string().optional(),
@@ -14784,6 +14828,19 @@ var dataStoreProviderCapability = {
14784
14828
  collection: zod.z.string(),
14785
14829
  key: zod.z.string()
14786
14830
  }), zod.z.void(), { kind: "mutation" }),
14831
+ /** Delete every record matching `filter`, in one statement. */
14832
+ deleteWhere: method(zod.z.object({
14833
+ namespace: zod.z.string().optional(),
14834
+ collection: zod.z.string(),
14835
+ filter: MutationFilterSchema
14836
+ }), zod.z.object({ deleted: zod.z.number().int() }), { kind: "mutation" }),
14837
+ /** Apply `data` to every record matching `filter`, in one statement. */
14838
+ updateWhere: method(zod.z.object({
14839
+ namespace: zod.z.string().optional(),
14840
+ collection: zod.z.string(),
14841
+ filter: MutationFilterSchema,
14842
+ data: zod.z.record(zod.z.string(), zod.z.unknown())
14843
+ }), zod.z.object({ updated: zod.z.number().int() }), { kind: "mutation" }),
14787
14844
  /** Count entries in a collection, optionally filtered. */
14788
14845
  count: method(zod.z.object({
14789
14846
  namespace: zod.z.string().optional(),
@@ -24963,6 +25020,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
24963
25020
  addonId: null,
24964
25021
  access: "delete"
24965
25022
  },
25023
+ "dataStoreProvider.deleteWhere": {
25024
+ capName: "data-store-provider",
25025
+ capScope: "system",
25026
+ addonId: null,
25027
+ access: "delete"
25028
+ },
24966
25029
  "dataStoreProvider.get": {
24967
25030
  capName: "data-store-provider",
24968
25031
  capScope: "system",
@@ -25011,6 +25074,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
25011
25074
  addonId: null,
25012
25075
  access: "create"
25013
25076
  },
25077
+ "dataStoreProvider.updateWhere": {
25078
+ capName: "data-store-provider",
25079
+ capScope: "system",
25080
+ addonId: null,
25081
+ access: "create"
25082
+ },
25014
25083
  "dayNight.getOptions": {
25015
25084
  capName: "day-night",
25016
25085
  capScope: "device",
@@ -28089,6 +28158,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
28089
28158
  addonId: null,
28090
28159
  access: "delete"
28091
28160
  },
28161
+ "settingsStore.deleteWhere": {
28162
+ capName: "settings-store",
28163
+ capScope: "system",
28164
+ addonId: null,
28165
+ access: "delete"
28166
+ },
28092
28167
  "settingsStore.get": {
28093
28168
  capName: "settings-store",
28094
28169
  capScope: "system",
@@ -28131,6 +28206,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
28131
28206
  addonId: null,
28132
28207
  access: "create"
28133
28208
  },
28209
+ "settingsStore.updateWhere": {
28210
+ capName: "settings-store",
28211
+ capScope: "system",
28212
+ addonId: null,
28213
+ access: "create"
28214
+ },
28134
28215
  "smtpProvider.getStatus": {
28135
28216
  capName: "smtp-provider",
28136
28217
  capScope: "system",
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("./chunk-Cek0wNdY.js");
3
- const require_dist = require("./dist-CPmFYLqq.js");
3
+ const require_dist = require("./dist-VIwdvws5.js");
4
4
  const require_model_download_service = require("./model-download-service-hf0ookyy.js");
5
5
  const require_manifest_python_deps = require("./manifest-python-deps-BqE5j0-O.js");
6
6
  const require_resource_monitor = require("./resource-monitor-DNNomR-i.js");
@@ -3574,6 +3574,25 @@ function readPackageJson(pkgJsonPath) {
3574
3574
  camstackAddons: Array.isArray(addonsField) ? addonsField : null
3575
3575
  };
3576
3576
  }
3577
+ /**
3578
+ * Should the bootstrap SKIP seeding `packageName` into /data/addons?
3579
+ *
3580
+ * True only for the package the closure provides, and only when it actually
3581
+ * resolves. Both halves are load-bearing:
3582
+ *
3583
+ * - seeding a copy the closure already provides creates one that WINS over
3584
+ * the closure and is never refreshed (bootstrap is seed-only, and
3585
+ * `camstack deploy` of a framework package is rejected on an image hub), so
3586
+ * the node runs first-boot builtins forever while reporting the new
3587
+ * version;
3588
+ * - skipping it when it does NOT resolve is how a fresh deployment was once
3589
+ * made unbootable ("No addon provides required infrastructure capability
3590
+ * storage-provider"). That is why the guard is resolvability rather than a
3591
+ * blanket removal from the bootstrap list.
3592
+ */
3593
+ function shouldSkipClosureProvidedSeed(packageName, isResolvable) {
3594
+ return packageName === "@camstack/system" && isResolvable;
3595
+ }
3577
3596
  /** Minimal no-op logger for default parameter */
3578
3597
  var noopLogger = {
3579
3598
  debug() {},
@@ -3593,6 +3612,7 @@ var AddonInstaller = class AddonInstaller {
3593
3612
  workspaceDir;
3594
3613
  installSource;
3595
3614
  logger;
3615
+ isPackageResolvable;
3596
3616
  /**
3597
3617
  * Central manifest of installed addons. Tracks version + source +
3598
3618
  * timestamps + last-backup pointer per package, written atomically
@@ -3608,6 +3628,7 @@ var AddonInstaller = class AddonInstaller {
3608
3628
  this.workspaceDir = config.workspacePackagesDir;
3609
3629
  this.logger = logger;
3610
3630
  this.manifest = new AddonManifest(config.addonsDir);
3631
+ this.isPackageResolvable = config.isPackageResolvable;
3611
3632
  if (config.installSource) this.installSource = config.installSource;
3612
3633
  else this.installSource = "npm";
3613
3634
  }
@@ -3636,6 +3657,25 @@ var AddonInstaller = class AddonInstaller {
3636
3657
  * hardcoded lists — see design spec
3637
3658
  * docs/superpowers/specs/2026-05-20-docker-install-from-npm-design.md.
3638
3659
  */
3660
+ /**
3661
+ * Is `packageName` reachable through normal resolution — i.e. does the
3662
+ * running closure already provide it?
3663
+ *
3664
+ * Overridable via `AddonInstallerConfig.isPackageResolvable` so a spec can
3665
+ * exercise BOTH sides. That matters more than it looks: the branch that
3666
+ * still seeds is the one protecting a fresh deployment from an unbootable
3667
+ * node, and it is unreachable in a test process where `@camstack/system`
3668
+ * always resolves.
3669
+ */
3670
+ isResolvableFromClosure(packageName) {
3671
+ if (this.isPackageResolvable) return this.isPackageResolvable(packageName);
3672
+ try {
3673
+ require.resolve(`${packageName}/package.json`);
3674
+ return true;
3675
+ } catch {
3676
+ return false;
3677
+ }
3678
+ }
3639
3679
  static deriveBootstrapList(metaPackageName) {
3640
3680
  const pkgJsonId = `${metaPackageName}/package.json`;
3641
3681
  let pkgPath;
@@ -3718,6 +3758,10 @@ var AddonInstaller = class AddonInstaller {
3718
3758
  this.logger.debug(`${packageName} — already installed, skipping`);
3719
3759
  continue;
3720
3760
  }
3761
+ if (shouldSkipClosureProvidedSeed(packageName, this.isResolvableFromClosure(packageName))) {
3762
+ this.logger.info(`${packageName} — provided by the server closure, not seeding a copy that would shadow it`);
3763
+ continue;
3764
+ }
3721
3765
  try {
3722
3766
  if (isLocal) {
3723
3767
  const pkgDir = this.findLocalPackage(packageName);