@rotorsoft/act-sqlite 1.4.0 → 1.4.2

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/index.cjs CHANGED
@@ -40,6 +40,8 @@ var import_client = require("@libsql/client");
40
40
  var DEFAULT_CONFIG = {
41
41
  url: "file::memory:"
42
42
  };
43
+ var parse_pii = (raw) => raw == null ? null : JSON.parse(raw);
44
+ var stringify_pii = (pii) => pii == null ? null : JSON.stringify(pii);
43
45
  function streamPatternToLike(input) {
44
46
  let s = input;
45
47
  const start = s.startsWith("^");
@@ -70,9 +72,14 @@ var SqliteStore = class {
70
72
  data TEXT NOT NULL,
71
73
  meta TEXT NOT NULL,
72
74
  created TEXT NOT NULL,
75
+ pii TEXT,
73
76
  UNIQUE(stream, version)
74
77
  )
75
78
  `);
79
+ try {
80
+ await this.client.execute("ALTER TABLE events ADD COLUMN pii TEXT");
81
+ } catch {
82
+ }
76
83
  await this.client.execute(
77
84
  "CREATE INDEX IF NOT EXISTS idx_events_stream ON events(stream)"
78
85
  );
@@ -124,33 +131,34 @@ var SqliteStore = class {
124
131
  async commit(stream, msgs, meta, expectedVersion) {
125
132
  const tx = await this.client.transaction("write");
126
133
  try {
127
- const versionRow = await tx.execute({
134
+ const version_row = await tx.execute({
128
135
  sql: "SELECT COALESCE(MAX(version), -1) as v FROM events WHERE stream = ?",
129
136
  args: [stream]
130
137
  });
131
- const currentVersion = Number(versionRow.rows[0].v);
132
- if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
138
+ const current_version = Number(version_row.rows[0].v);
139
+ if (typeof expectedVersion === "number" && current_version !== expectedVersion) {
133
140
  const { ConcurrencyError } = await import("@rotorsoft/act");
134
141
  throw new ConcurrencyError(
135
142
  stream,
136
- currentVersion,
143
+ current_version,
137
144
  msgs,
138
145
  expectedVersion
139
146
  );
140
147
  }
141
148
  const now = (/* @__PURE__ */ new Date()).toISOString();
142
149
  const committed = [];
143
- let version = currentVersion + 1;
144
- for (const { name, data } of msgs) {
150
+ let version = current_version + 1;
151
+ for (const { name, data, pii } of msgs) {
145
152
  const result = await tx.execute({
146
- sql: "INSERT INTO events (stream, version, name, data, meta, created) VALUES (?, ?, ?, ?, ?, ?)",
153
+ sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
147
154
  args: [
148
155
  stream,
149
156
  version,
150
157
  name,
151
158
  JSON.stringify(data),
152
159
  JSON.stringify(meta),
153
- now
160
+ now,
161
+ stringify_pii(pii)
154
162
  ]
155
163
  });
156
164
  committed.push({
@@ -160,7 +168,8 @@ var SqliteStore = class {
160
168
  created: new Date(now),
161
169
  name,
162
170
  data,
163
- meta
171
+ meta,
172
+ ...pii == null ? {} : { pii }
164
173
  });
165
174
  version++;
166
175
  }
@@ -227,7 +236,8 @@ var SqliteStore = class {
227
236
  created: new Date(row.created),
228
237
  name: row.name,
229
238
  data: JSON.parse(row.data),
230
- meta: JSON.parse(row.meta)
239
+ meta: JSON.parse(row.meta),
240
+ pii: parse_pii(row.pii)
231
241
  })
232
242
  );
233
243
  count++;
@@ -283,10 +293,10 @@ var SqliteStore = class {
283
293
  const tx = await this.client.transaction("write");
284
294
  try {
285
295
  const now = (/* @__PURE__ */ new Date()).toISOString();
286
- const laneClause = lane !== void 0 ? " AND lane = ?" : "";
296
+ const lane_clause = lane !== void 0 ? " AND lane = ?" : "";
287
297
  const result = await tx.execute({
288
298
  sql: `SELECT stream, source, at, priority, lane FROM streams
289
- WHERE blocked = 0 AND (leased_until IS NULL OR leased_until <= ?)${laneClause}
299
+ WHERE blocked = 0 AND (leased_until IS NULL OR leased_until <= ?)${lane_clause}
290
300
  ORDER BY priority DESC, at ASC`,
291
301
  args: lane !== void 0 ? [now, lane] : [now]
292
302
  });
@@ -295,21 +305,21 @@ var SqliteStore = class {
295
305
  const stream = row.stream;
296
306
  const source = row.source;
297
307
  const at = Number(row.at);
298
- let hasEvents;
308
+ let has_events;
299
309
  if (source) {
300
310
  const check = await tx.execute({
301
311
  sql: `SELECT 1 FROM events WHERE id > ? AND name != '__snapshot__' AND stream LIKE ? LIMIT 1`,
302
312
  args: [at, streamPatternToLike(source)]
303
313
  });
304
- hasEvents = check.rows.length > 0;
314
+ has_events = check.rows.length > 0;
305
315
  } else {
306
316
  const check = await tx.execute({
307
317
  sql: `SELECT 1 FROM events WHERE id > ? AND name != '__snapshot__' LIMIT 1`,
308
318
  args: [at]
309
319
  });
310
- hasEvents = check.rows.length > 0;
320
+ has_events = check.rows.length > 0;
311
321
  }
312
- if (hasEvents) {
322
+ if (has_events) {
313
323
  candidates.push({
314
324
  stream,
315
325
  source: source ?? void 0,
@@ -396,7 +406,7 @@ var SqliteStore = class {
396
406
  * plus positional args. Returns `"1"` (always true) when empty so
397
407
  * callers can compose it unconditionally.
398
408
  */
399
- _filterClause(filter) {
409
+ _filter_clause(filter) {
400
410
  const conditions = [];
401
411
  const args = [];
402
412
  if (filter.stream !== void 0) {
@@ -430,7 +440,7 @@ var SqliteStore = class {
430
440
  }
431
441
  // --- reset: transactional, accepts names or filter ---
432
442
  async reset(input) {
433
- const setClause = `SET at = -1, retry = 0, blocked = 0, error = '',
443
+ const set_clause = `SET at = -1, retry = 0, blocked = 0, error = '',
434
444
  leased_by = NULL, leased_until = NULL`;
435
445
  const tx = await this.client.transaction("write");
436
446
  try {
@@ -438,15 +448,15 @@ var SqliteStore = class {
438
448
  if (Array.isArray(input)) {
439
449
  for (const stream of input) {
440
450
  const r = await tx.execute({
441
- sql: `UPDATE streams ${setClause} WHERE stream = ?`,
451
+ sql: `UPDATE streams ${set_clause} WHERE stream = ?`,
442
452
  args: [stream]
443
453
  });
444
454
  count += r.rowsAffected;
445
455
  }
446
456
  } else {
447
- const { clause, args } = this._filterClause(input);
457
+ const { clause, args } = this._filter_clause(input);
448
458
  const r = await tx.execute({
449
- sql: `UPDATE streams ${setClause} WHERE ${clause}`,
459
+ sql: `UPDATE streams ${set_clause} WHERE ${clause}`,
450
460
  args
451
461
  });
452
462
  count = r.rowsAffected;
@@ -462,7 +472,7 @@ var SqliteStore = class {
462
472
  // `retry = -1` so claim's post-bump returns retry=0 (first attempt),
463
473
  // matching the InMemoryStore convention.
464
474
  async unblock(input) {
465
- const setClause = `SET retry = -1, blocked = 0, error = '',
475
+ const set_clause = `SET retry = -1, blocked = 0, error = '',
466
476
  leased_by = NULL, leased_until = NULL`;
467
477
  const tx = await this.client.transaction("write");
468
478
  try {
@@ -470,19 +480,19 @@ var SqliteStore = class {
470
480
  if (Array.isArray(input)) {
471
481
  for (const stream of input) {
472
482
  const r = await tx.execute({
473
- sql: `UPDATE streams ${setClause}
483
+ sql: `UPDATE streams ${set_clause}
474
484
  WHERE stream = ? AND blocked = 1`,
475
485
  args: [stream]
476
486
  });
477
487
  count += r.rowsAffected;
478
488
  }
479
489
  } else {
480
- const { clause, args } = this._filterClause({
490
+ const { clause, args } = this._filter_clause({
481
491
  ...input,
482
492
  blocked: true
483
493
  });
484
494
  const r = await tx.execute({
485
- sql: `UPDATE streams ${setClause} WHERE ${clause}`,
495
+ sql: `UPDATE streams ${set_clause} WHERE ${clause}`,
486
496
  args
487
497
  });
488
498
  count = r.rowsAffected;
@@ -581,11 +591,11 @@ var SqliteStore = class {
581
591
  */
582
592
  async query_stats(input, options) {
583
593
  const exclude = options?.exclude ?? [];
584
- const wantTail = options?.tail ?? false;
585
- const wantCount = options?.count ?? false;
586
- const wantNames = options?.names ?? false;
594
+ const want_tail = options?.tail ?? false;
595
+ const want_count = options?.count ?? false;
596
+ const want_names = options?.names ?? false;
587
597
  const before = options?.before;
588
- const fullScan = wantCount || wantNames;
598
+ const full_scan = want_count || want_names;
589
599
  if (Array.isArray(input) && input.length === 0) {
590
600
  return /* @__PURE__ */ new Map();
591
601
  }
@@ -613,55 +623,61 @@ var SqliteStore = class {
613
623
  where.push(`e.id < ?`);
614
624
  args.push(before);
615
625
  }
616
- const fromClause = `events e`;
617
- const whereClause = `WHERE ${where.length ? where.join(" AND ") : "1=1"}`;
618
- return fullScan ? this._queryStatsFullScan(
619
- fromClause,
620
- whereClause,
626
+ const from_clause = `events e`;
627
+ const where_clause = `WHERE ${where.length ? where.join(" AND ") : "1=1"}`;
628
+ return full_scan ? this._query_stats_full_scan(
629
+ from_clause,
630
+ where_clause,
621
631
  args,
622
- wantTail,
623
- wantCount,
624
- wantNames
625
- ) : this._queryStatsHeadsOnly(fromClause, whereClause, args, wantTail);
632
+ want_tail,
633
+ want_count,
634
+ want_names
635
+ ) : this._query_stats_heads_only(
636
+ from_clause,
637
+ where_clause,
638
+ args,
639
+ want_tail
640
+ );
626
641
  }
627
642
  /**
628
643
  * Cheap path — head (and optional tail) via ROW_NUMBER() over the
629
644
  * `(stream, version)` unique index. Parallel queries when tail set.
630
645
  */
631
- async _queryStatsHeadsOnly(fromClause, whereClause, args, wantTail) {
632
- const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta`;
633
- const headSql = `SELECT * FROM (
646
+ async _query_stats_heads_only(from_clause, where_clause, args, want_tail) {
647
+ const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta, e.pii`;
648
+ const head_sql = `SELECT * FROM (
634
649
  SELECT ${cols}, ROW_NUMBER() OVER (PARTITION BY e.stream ORDER BY e.version DESC) AS rn
635
- FROM ${fromClause}
636
- ${whereClause}
650
+ FROM ${from_clause}
651
+ ${where_clause}
637
652
  ) WHERE rn = 1`;
638
- const tailSql = wantTail ? `SELECT * FROM (
653
+ const tail_sql = want_tail ? `SELECT * FROM (
639
654
  SELECT ${cols}, ROW_NUMBER() OVER (PARTITION BY e.stream ORDER BY e.version ASC) AS rn
640
- FROM ${fromClause}
641
- ${whereClause}
655
+ FROM ${from_clause}
656
+ ${where_clause}
642
657
  ) WHERE rn = 1` : null;
643
658
  const [headRes, tailRes] = await Promise.all([
644
- this.client.execute({ sql: headSql, args }),
645
- tailSql ? this.client.execute({ sql: tailSql, args }) : null
659
+ this.client.execute({ sql: head_sql, args }),
660
+ tail_sql ? this.client.execute({ sql: tail_sql, args }) : null
646
661
  ]);
647
- const toCommitted = (row) => ({
662
+ const to_committed = (row) => ({
648
663
  id: Number(row.id),
649
664
  stream: row.stream,
650
665
  version: Number(row.version),
651
666
  name: row.name,
652
667
  data: JSON.parse(row.data),
653
668
  meta: JSON.parse(row.meta),
654
- created: new Date(row.created)
669
+ created: new Date(row.created),
670
+ pii: parse_pii(row.pii)
655
671
  });
656
672
  const out = /* @__PURE__ */ new Map();
657
673
  for (const row of headRes.rows) {
658
674
  out.set(row.stream, {
659
- head: toCommitted(row)
675
+ head: to_committed(row)
660
676
  });
661
677
  }
662
678
  if (tailRes) {
663
679
  for (const row of tailRes.rows) {
664
- out.get(row.stream).tail = toCommitted(row);
680
+ out.get(row.stream).tail = to_committed(row);
665
681
  }
666
682
  }
667
683
  return out;
@@ -671,20 +687,20 @@ var SqliteStore = class {
671
687
  * `json_group_object(name, n)`. Heads (and optional tails) ride free
672
688
  * on the same scan.
673
689
  */
674
- async _queryStatsFullScan(fromClause, whereClause, args, wantTail, wantCount, wantNames) {
675
- const tailCte = wantTail ? `, tails AS (
690
+ async _query_stats_full_scan(from_clause, where_clause, args, want_tail, want_count, want_names) {
691
+ const tail_cte = want_tail ? `, tails AS (
676
692
  SELECT * FROM (
677
693
  SELECT *, ROW_NUMBER() OVER (PARTITION BY stream ORDER BY version ASC) AS rn FROM ef
678
694
  ) WHERE rn = 1
679
695
  )` : "";
680
- const tailJoin = wantTail ? `LEFT JOIN tails t ON t.stream = h.stream` : "";
681
- const tailCols = wantTail ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,
682
- t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta` : "";
696
+ const tail_join = want_tail ? `LEFT JOIN tails t ON t.stream = h.stream` : "";
697
+ const tail_cols = want_tail ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,
698
+ t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta, t.pii AS t_pii` : "";
683
699
  const sql = `
684
700
  WITH ef AS (
685
- SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta
686
- FROM ${fromClause}
687
- ${whereClause}
701
+ SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta, e.pii
702
+ FROM ${from_clause}
703
+ ${where_clause}
688
704
  ),
689
705
  agg AS (
690
706
  SELECT stream,
@@ -702,60 +718,63 @@ var SqliteStore = class {
702
718
  SELECT *, ROW_NUMBER() OVER (PARTITION BY stream ORDER BY version DESC) AS rn FROM ef
703
719
  ) WHERE rn = 1
704
720
  )
705
- ${tailCte}
721
+ ${tail_cte}
706
722
  SELECT
707
- h.id, h.stream, h.version, h.name, h.data, h.created, h.meta,
723
+ h.id, h.stream, h.version, h.name, h.data, h.created, h.meta, h.pii,
708
724
  a.cnt AS agg_count,
709
725
  a.names AS agg_names
710
- ${tailCols}
726
+ ${tail_cols}
711
727
  FROM heads h
712
728
  LEFT JOIN agg a ON a.stream = h.stream
713
- ${tailJoin}
729
+ ${tail_join}
714
730
  `;
715
731
  const res = await this.client.execute({ sql, args });
716
- const toCommitted = (id, stream, version, name, data, meta, created) => ({
732
+ const to_committed = (id, stream, version, name, data, meta, created, pii) => ({
717
733
  id: Number(id),
718
734
  stream,
719
735
  version: Number(version),
720
736
  name,
721
737
  data: JSON.parse(data),
722
738
  meta: JSON.parse(meta),
723
- created: new Date(created)
739
+ created: new Date(created),
740
+ pii: parse_pii(pii)
724
741
  });
725
742
  const out = /* @__PURE__ */ new Map();
726
743
  for (const row of res.rows) {
727
744
  const r = row;
728
745
  const stats = {
729
- head: toCommitted(
746
+ head: to_committed(
730
747
  r.id,
731
748
  r.stream,
732
749
  r.version,
733
750
  r.name,
734
751
  r.data,
735
752
  r.meta,
736
- r.created
753
+ r.created,
754
+ r.pii
737
755
  )
738
756
  };
739
- if (wantTail && r.t_id !== null && r.t_id !== void 0) {
740
- stats.tail = toCommitted(
757
+ if (want_tail && r.t_id !== null && r.t_id !== void 0) {
758
+ stats.tail = to_committed(
741
759
  r.t_id,
742
760
  r.t_stream,
743
761
  r.t_version,
744
762
  r.t_name,
745
763
  r.t_data,
746
764
  r.t_meta,
747
- r.t_created
765
+ r.t_created,
766
+ r.t_pii
748
767
  );
749
768
  }
750
- if (wantCount) stats.count = Number(r.agg_count);
751
- if (wantNames) stats.names = JSON.parse(r.agg_names);
769
+ if (want_count) stats.count = Number(r.agg_count);
770
+ if (want_names) stats.names = JSON.parse(r.agg_names);
752
771
  out.set(r.stream, stats);
753
772
  }
754
773
  return out;
755
774
  }
756
775
  // --- prioritize: bulk priority update with filter (ACT-102) ---
757
776
  async prioritize(filter, priority) {
758
- const { clause, args: filterArgs } = this._filterClause(filter);
777
+ const { clause, args: filterArgs } = this._filter_clause(filter);
759
778
  const sql = `UPDATE streams SET priority = ?
760
779
  WHERE priority <> ? AND ${clause}`;
761
780
  const result = await this.client.execute({
@@ -770,11 +789,11 @@ var SqliteStore = class {
770
789
  const tx = await this.client.transaction("write");
771
790
  try {
772
791
  for (const { stream, snapshot, meta } of targets) {
773
- const countRow = await tx.execute({
792
+ const count_row = await tx.execute({
774
793
  sql: "SELECT COUNT(*) as c FROM events WHERE stream = ?",
775
794
  args: [stream]
776
795
  });
777
- const deleted = Number(countRow.rows[0].c);
796
+ const deleted = Number(count_row.rows[0].c);
778
797
  await tx.execute({
779
798
  sql: "DELETE FROM events WHERE stream = ?",
780
799
  args: [stream]
@@ -783,16 +802,16 @@ var SqliteStore = class {
783
802
  sql: "DELETE FROM streams WHERE stream = ?",
784
803
  args: [stream]
785
804
  });
786
- const eventName = snapshot !== void 0 ? "__snapshot__" : "__tombstone__";
787
- const eventMeta = meta ?? { correlation: "", causation: {} };
805
+ const event_name = snapshot !== void 0 ? "__snapshot__" : "__tombstone__";
806
+ const event_meta = meta ?? { correlation: "", causation: {} };
788
807
  const now = (/* @__PURE__ */ new Date()).toISOString();
789
808
  const ins = await tx.execute({
790
809
  sql: "INSERT INTO events (stream, version, name, data, meta, created) VALUES (?, 0, ?, ?, ?, ?)",
791
810
  args: [
792
811
  stream,
793
- eventName,
812
+ event_name,
794
813
  JSON.stringify(snapshot ?? {}),
795
- JSON.stringify(eventMeta),
814
+ JSON.stringify(event_meta),
796
815
  now
797
816
  ]
798
817
  });
@@ -803,9 +822,9 @@ var SqliteStore = class {
803
822
  stream,
804
823
  version: 0,
805
824
  created: new Date(now),
806
- name: eventName,
825
+ name: event_name,
807
826
  data: snapshot ?? {},
808
- meta: eventMeta
827
+ meta: event_meta
809
828
  }
810
829
  });
811
830
  }
@@ -835,14 +854,15 @@ var SqliteStore = class {
835
854
  await tx.execute("DELETE FROM sqlite_sequence WHERE name = 'events'");
836
855
  await driver(async (event) => {
837
856
  const ins = await tx.execute({
838
- sql: "INSERT INTO events (stream, version, name, data, meta, created) VALUES (?, ?, ?, ?, ?, ?)",
857
+ sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
839
858
  args: [
840
859
  event.stream,
841
860
  event.version,
842
861
  event.name,
843
862
  JSON.stringify(event.data),
844
863
  JSON.stringify(event.meta),
845
- event.created.toISOString()
864
+ event.created.toISOString(),
865
+ stringify_pii(event.pii)
846
866
  ]
847
867
  });
848
868
  return Number(ins.lastInsertRowid);
@@ -853,6 +873,30 @@ var SqliteStore = class {
853
873
  throw error;
854
874
  }
855
875
  }
876
+ /**
877
+ * Wipe the sensitive-data payload for every event on the stream — the
878
+ * physical-erasure side of the sensitive-data epic (#566). Sets
879
+ * `events.pii` to `NULL` for the stream's events; `events.data` and
880
+ * the rest of the row are never touched.
881
+ *
882
+ * Single `UPDATE` under SQLite's writer lock, bounded by events-per-
883
+ * stream. Idempotent — the `pii IS NOT NULL` predicate filters out
884
+ * already-wiped rows so a second call returns `0`.
885
+ *
886
+ * SQLite doesn't auto-reclaim space; freed pages stay in the file
887
+ * until an operator-scheduled `PRAGMA incremental_vacuum` or a full
888
+ * `VACUUM`. The production checklist documents the cadence.
889
+ *
890
+ * @param stream Target stream
891
+ * @returns Count of events whose `pii` was set to `NULL`
892
+ */
893
+ async forget_pii(stream) {
894
+ const r = await this.client.execute({
895
+ sql: "UPDATE events SET pii = NULL WHERE stream = ? AND pii IS NOT NULL",
896
+ args: [stream]
897
+ });
898
+ return r.rowsAffected ?? 0;
899
+ }
856
900
  };
857
901
  // Annotate the CommonJS export names for ESM import in node:
858
902
  0 && (module.exports = {