@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.js CHANGED
@@ -3,6 +3,8 @@ import { createClient } from "@libsql/client";
3
3
  var DEFAULT_CONFIG = {
4
4
  url: "file::memory:"
5
5
  };
6
+ var parse_pii = (raw) => raw == null ? null : JSON.parse(raw);
7
+ var stringify_pii = (pii) => pii == null ? null : JSON.stringify(pii);
6
8
  function streamPatternToLike(input) {
7
9
  let s = input;
8
10
  const start = s.startsWith("^");
@@ -33,9 +35,14 @@ var SqliteStore = class {
33
35
  data TEXT NOT NULL,
34
36
  meta TEXT NOT NULL,
35
37
  created TEXT NOT NULL,
38
+ pii TEXT,
36
39
  UNIQUE(stream, version)
37
40
  )
38
41
  `);
42
+ try {
43
+ await this.client.execute("ALTER TABLE events ADD COLUMN pii TEXT");
44
+ } catch {
45
+ }
39
46
  await this.client.execute(
40
47
  "CREATE INDEX IF NOT EXISTS idx_events_stream ON events(stream)"
41
48
  );
@@ -87,33 +94,34 @@ var SqliteStore = class {
87
94
  async commit(stream, msgs, meta, expectedVersion) {
88
95
  const tx = await this.client.transaction("write");
89
96
  try {
90
- const versionRow = await tx.execute({
97
+ const version_row = await tx.execute({
91
98
  sql: "SELECT COALESCE(MAX(version), -1) as v FROM events WHERE stream = ?",
92
99
  args: [stream]
93
100
  });
94
- const currentVersion = Number(versionRow.rows[0].v);
95
- if (typeof expectedVersion === "number" && currentVersion !== expectedVersion) {
101
+ const current_version = Number(version_row.rows[0].v);
102
+ if (typeof expectedVersion === "number" && current_version !== expectedVersion) {
96
103
  const { ConcurrencyError } = await import("@rotorsoft/act");
97
104
  throw new ConcurrencyError(
98
105
  stream,
99
- currentVersion,
106
+ current_version,
100
107
  msgs,
101
108
  expectedVersion
102
109
  );
103
110
  }
104
111
  const now = (/* @__PURE__ */ new Date()).toISOString();
105
112
  const committed = [];
106
- let version = currentVersion + 1;
107
- for (const { name, data } of msgs) {
113
+ let version = current_version + 1;
114
+ for (const { name, data, pii } of msgs) {
108
115
  const result = await tx.execute({
109
- sql: "INSERT INTO events (stream, version, name, data, meta, created) VALUES (?, ?, ?, ?, ?, ?)",
116
+ sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
110
117
  args: [
111
118
  stream,
112
119
  version,
113
120
  name,
114
121
  JSON.stringify(data),
115
122
  JSON.stringify(meta),
116
- now
123
+ now,
124
+ stringify_pii(pii)
117
125
  ]
118
126
  });
119
127
  committed.push({
@@ -123,7 +131,8 @@ var SqliteStore = class {
123
131
  created: new Date(now),
124
132
  name,
125
133
  data,
126
- meta
134
+ meta,
135
+ ...pii == null ? {} : { pii }
127
136
  });
128
137
  version++;
129
138
  }
@@ -190,7 +199,8 @@ var SqliteStore = class {
190
199
  created: new Date(row.created),
191
200
  name: row.name,
192
201
  data: JSON.parse(row.data),
193
- meta: JSON.parse(row.meta)
202
+ meta: JSON.parse(row.meta),
203
+ pii: parse_pii(row.pii)
194
204
  })
195
205
  );
196
206
  count++;
@@ -246,10 +256,10 @@ var SqliteStore = class {
246
256
  const tx = await this.client.transaction("write");
247
257
  try {
248
258
  const now = (/* @__PURE__ */ new Date()).toISOString();
249
- const laneClause = lane !== void 0 ? " AND lane = ?" : "";
259
+ const lane_clause = lane !== void 0 ? " AND lane = ?" : "";
250
260
  const result = await tx.execute({
251
261
  sql: `SELECT stream, source, at, priority, lane FROM streams
252
- WHERE blocked = 0 AND (leased_until IS NULL OR leased_until <= ?)${laneClause}
262
+ WHERE blocked = 0 AND (leased_until IS NULL OR leased_until <= ?)${lane_clause}
253
263
  ORDER BY priority DESC, at ASC`,
254
264
  args: lane !== void 0 ? [now, lane] : [now]
255
265
  });
@@ -258,21 +268,21 @@ var SqliteStore = class {
258
268
  const stream = row.stream;
259
269
  const source = row.source;
260
270
  const at = Number(row.at);
261
- let hasEvents;
271
+ let has_events;
262
272
  if (source) {
263
273
  const check = await tx.execute({
264
274
  sql: `SELECT 1 FROM events WHERE id > ? AND name != '__snapshot__' AND stream LIKE ? LIMIT 1`,
265
275
  args: [at, streamPatternToLike(source)]
266
276
  });
267
- hasEvents = check.rows.length > 0;
277
+ has_events = check.rows.length > 0;
268
278
  } else {
269
279
  const check = await tx.execute({
270
280
  sql: `SELECT 1 FROM events WHERE id > ? AND name != '__snapshot__' LIMIT 1`,
271
281
  args: [at]
272
282
  });
273
- hasEvents = check.rows.length > 0;
283
+ has_events = check.rows.length > 0;
274
284
  }
275
- if (hasEvents) {
285
+ if (has_events) {
276
286
  candidates.push({
277
287
  stream,
278
288
  source: source ?? void 0,
@@ -359,7 +369,7 @@ var SqliteStore = class {
359
369
  * plus positional args. Returns `"1"` (always true) when empty so
360
370
  * callers can compose it unconditionally.
361
371
  */
362
- _filterClause(filter) {
372
+ _filter_clause(filter) {
363
373
  const conditions = [];
364
374
  const args = [];
365
375
  if (filter.stream !== void 0) {
@@ -393,7 +403,7 @@ var SqliteStore = class {
393
403
  }
394
404
  // --- reset: transactional, accepts names or filter ---
395
405
  async reset(input) {
396
- const setClause = `SET at = -1, retry = 0, blocked = 0, error = '',
406
+ const set_clause = `SET at = -1, retry = 0, blocked = 0, error = '',
397
407
  leased_by = NULL, leased_until = NULL`;
398
408
  const tx = await this.client.transaction("write");
399
409
  try {
@@ -401,15 +411,15 @@ var SqliteStore = class {
401
411
  if (Array.isArray(input)) {
402
412
  for (const stream of input) {
403
413
  const r = await tx.execute({
404
- sql: `UPDATE streams ${setClause} WHERE stream = ?`,
414
+ sql: `UPDATE streams ${set_clause} WHERE stream = ?`,
405
415
  args: [stream]
406
416
  });
407
417
  count += r.rowsAffected;
408
418
  }
409
419
  } else {
410
- const { clause, args } = this._filterClause(input);
420
+ const { clause, args } = this._filter_clause(input);
411
421
  const r = await tx.execute({
412
- sql: `UPDATE streams ${setClause} WHERE ${clause}`,
422
+ sql: `UPDATE streams ${set_clause} WHERE ${clause}`,
413
423
  args
414
424
  });
415
425
  count = r.rowsAffected;
@@ -425,7 +435,7 @@ var SqliteStore = class {
425
435
  // `retry = -1` so claim's post-bump returns retry=0 (first attempt),
426
436
  // matching the InMemoryStore convention.
427
437
  async unblock(input) {
428
- const setClause = `SET retry = -1, blocked = 0, error = '',
438
+ const set_clause = `SET retry = -1, blocked = 0, error = '',
429
439
  leased_by = NULL, leased_until = NULL`;
430
440
  const tx = await this.client.transaction("write");
431
441
  try {
@@ -433,19 +443,19 @@ var SqliteStore = class {
433
443
  if (Array.isArray(input)) {
434
444
  for (const stream of input) {
435
445
  const r = await tx.execute({
436
- sql: `UPDATE streams ${setClause}
446
+ sql: `UPDATE streams ${set_clause}
437
447
  WHERE stream = ? AND blocked = 1`,
438
448
  args: [stream]
439
449
  });
440
450
  count += r.rowsAffected;
441
451
  }
442
452
  } else {
443
- const { clause, args } = this._filterClause({
453
+ const { clause, args } = this._filter_clause({
444
454
  ...input,
445
455
  blocked: true
446
456
  });
447
457
  const r = await tx.execute({
448
- sql: `UPDATE streams ${setClause} WHERE ${clause}`,
458
+ sql: `UPDATE streams ${set_clause} WHERE ${clause}`,
449
459
  args
450
460
  });
451
461
  count = r.rowsAffected;
@@ -544,11 +554,11 @@ var SqliteStore = class {
544
554
  */
545
555
  async query_stats(input, options) {
546
556
  const exclude = options?.exclude ?? [];
547
- const wantTail = options?.tail ?? false;
548
- const wantCount = options?.count ?? false;
549
- const wantNames = options?.names ?? false;
557
+ const want_tail = options?.tail ?? false;
558
+ const want_count = options?.count ?? false;
559
+ const want_names = options?.names ?? false;
550
560
  const before = options?.before;
551
- const fullScan = wantCount || wantNames;
561
+ const full_scan = want_count || want_names;
552
562
  if (Array.isArray(input) && input.length === 0) {
553
563
  return /* @__PURE__ */ new Map();
554
564
  }
@@ -576,55 +586,61 @@ var SqliteStore = class {
576
586
  where.push(`e.id < ?`);
577
587
  args.push(before);
578
588
  }
579
- const fromClause = `events e`;
580
- const whereClause = `WHERE ${where.length ? where.join(" AND ") : "1=1"}`;
581
- return fullScan ? this._queryStatsFullScan(
582
- fromClause,
583
- whereClause,
589
+ const from_clause = `events e`;
590
+ const where_clause = `WHERE ${where.length ? where.join(" AND ") : "1=1"}`;
591
+ return full_scan ? this._query_stats_full_scan(
592
+ from_clause,
593
+ where_clause,
584
594
  args,
585
- wantTail,
586
- wantCount,
587
- wantNames
588
- ) : this._queryStatsHeadsOnly(fromClause, whereClause, args, wantTail);
595
+ want_tail,
596
+ want_count,
597
+ want_names
598
+ ) : this._query_stats_heads_only(
599
+ from_clause,
600
+ where_clause,
601
+ args,
602
+ want_tail
603
+ );
589
604
  }
590
605
  /**
591
606
  * Cheap path — head (and optional tail) via ROW_NUMBER() over the
592
607
  * `(stream, version)` unique index. Parallel queries when tail set.
593
608
  */
594
- async _queryStatsHeadsOnly(fromClause, whereClause, args, wantTail) {
595
- const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta`;
596
- const headSql = `SELECT * FROM (
609
+ async _query_stats_heads_only(from_clause, where_clause, args, want_tail) {
610
+ const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta, e.pii`;
611
+ const head_sql = `SELECT * FROM (
597
612
  SELECT ${cols}, ROW_NUMBER() OVER (PARTITION BY e.stream ORDER BY e.version DESC) AS rn
598
- FROM ${fromClause}
599
- ${whereClause}
613
+ FROM ${from_clause}
614
+ ${where_clause}
600
615
  ) WHERE rn = 1`;
601
- const tailSql = wantTail ? `SELECT * FROM (
616
+ const tail_sql = want_tail ? `SELECT * FROM (
602
617
  SELECT ${cols}, ROW_NUMBER() OVER (PARTITION BY e.stream ORDER BY e.version ASC) AS rn
603
- FROM ${fromClause}
604
- ${whereClause}
618
+ FROM ${from_clause}
619
+ ${where_clause}
605
620
  ) WHERE rn = 1` : null;
606
621
  const [headRes, tailRes] = await Promise.all([
607
- this.client.execute({ sql: headSql, args }),
608
- tailSql ? this.client.execute({ sql: tailSql, args }) : null
622
+ this.client.execute({ sql: head_sql, args }),
623
+ tail_sql ? this.client.execute({ sql: tail_sql, args }) : null
609
624
  ]);
610
- const toCommitted = (row) => ({
625
+ const to_committed = (row) => ({
611
626
  id: Number(row.id),
612
627
  stream: row.stream,
613
628
  version: Number(row.version),
614
629
  name: row.name,
615
630
  data: JSON.parse(row.data),
616
631
  meta: JSON.parse(row.meta),
617
- created: new Date(row.created)
632
+ created: new Date(row.created),
633
+ pii: parse_pii(row.pii)
618
634
  });
619
635
  const out = /* @__PURE__ */ new Map();
620
636
  for (const row of headRes.rows) {
621
637
  out.set(row.stream, {
622
- head: toCommitted(row)
638
+ head: to_committed(row)
623
639
  });
624
640
  }
625
641
  if (tailRes) {
626
642
  for (const row of tailRes.rows) {
627
- out.get(row.stream).tail = toCommitted(row);
643
+ out.get(row.stream).tail = to_committed(row);
628
644
  }
629
645
  }
630
646
  return out;
@@ -634,20 +650,20 @@ var SqliteStore = class {
634
650
  * `json_group_object(name, n)`. Heads (and optional tails) ride free
635
651
  * on the same scan.
636
652
  */
637
- async _queryStatsFullScan(fromClause, whereClause, args, wantTail, wantCount, wantNames) {
638
- const tailCte = wantTail ? `, tails AS (
653
+ async _query_stats_full_scan(from_clause, where_clause, args, want_tail, want_count, want_names) {
654
+ const tail_cte = want_tail ? `, tails AS (
639
655
  SELECT * FROM (
640
656
  SELECT *, ROW_NUMBER() OVER (PARTITION BY stream ORDER BY version ASC) AS rn FROM ef
641
657
  ) WHERE rn = 1
642
658
  )` : "";
643
- const tailJoin = wantTail ? `LEFT JOIN tails t ON t.stream = h.stream` : "";
644
- const tailCols = wantTail ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,
645
- t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta` : "";
659
+ const tail_join = want_tail ? `LEFT JOIN tails t ON t.stream = h.stream` : "";
660
+ const tail_cols = want_tail ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,
661
+ 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` : "";
646
662
  const sql = `
647
663
  WITH ef AS (
648
- SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta
649
- FROM ${fromClause}
650
- ${whereClause}
664
+ SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta, e.pii
665
+ FROM ${from_clause}
666
+ ${where_clause}
651
667
  ),
652
668
  agg AS (
653
669
  SELECT stream,
@@ -665,60 +681,63 @@ var SqliteStore = class {
665
681
  SELECT *, ROW_NUMBER() OVER (PARTITION BY stream ORDER BY version DESC) AS rn FROM ef
666
682
  ) WHERE rn = 1
667
683
  )
668
- ${tailCte}
684
+ ${tail_cte}
669
685
  SELECT
670
- h.id, h.stream, h.version, h.name, h.data, h.created, h.meta,
686
+ h.id, h.stream, h.version, h.name, h.data, h.created, h.meta, h.pii,
671
687
  a.cnt AS agg_count,
672
688
  a.names AS agg_names
673
- ${tailCols}
689
+ ${tail_cols}
674
690
  FROM heads h
675
691
  LEFT JOIN agg a ON a.stream = h.stream
676
- ${tailJoin}
692
+ ${tail_join}
677
693
  `;
678
694
  const res = await this.client.execute({ sql, args });
679
- const toCommitted = (id, stream, version, name, data, meta, created) => ({
695
+ const to_committed = (id, stream, version, name, data, meta, created, pii) => ({
680
696
  id: Number(id),
681
697
  stream,
682
698
  version: Number(version),
683
699
  name,
684
700
  data: JSON.parse(data),
685
701
  meta: JSON.parse(meta),
686
- created: new Date(created)
702
+ created: new Date(created),
703
+ pii: parse_pii(pii)
687
704
  });
688
705
  const out = /* @__PURE__ */ new Map();
689
706
  for (const row of res.rows) {
690
707
  const r = row;
691
708
  const stats = {
692
- head: toCommitted(
709
+ head: to_committed(
693
710
  r.id,
694
711
  r.stream,
695
712
  r.version,
696
713
  r.name,
697
714
  r.data,
698
715
  r.meta,
699
- r.created
716
+ r.created,
717
+ r.pii
700
718
  )
701
719
  };
702
- if (wantTail && r.t_id !== null && r.t_id !== void 0) {
703
- stats.tail = toCommitted(
720
+ if (want_tail && r.t_id !== null && r.t_id !== void 0) {
721
+ stats.tail = to_committed(
704
722
  r.t_id,
705
723
  r.t_stream,
706
724
  r.t_version,
707
725
  r.t_name,
708
726
  r.t_data,
709
727
  r.t_meta,
710
- r.t_created
728
+ r.t_created,
729
+ r.t_pii
711
730
  );
712
731
  }
713
- if (wantCount) stats.count = Number(r.agg_count);
714
- if (wantNames) stats.names = JSON.parse(r.agg_names);
732
+ if (want_count) stats.count = Number(r.agg_count);
733
+ if (want_names) stats.names = JSON.parse(r.agg_names);
715
734
  out.set(r.stream, stats);
716
735
  }
717
736
  return out;
718
737
  }
719
738
  // --- prioritize: bulk priority update with filter (ACT-102) ---
720
739
  async prioritize(filter, priority) {
721
- const { clause, args: filterArgs } = this._filterClause(filter);
740
+ const { clause, args: filterArgs } = this._filter_clause(filter);
722
741
  const sql = `UPDATE streams SET priority = ?
723
742
  WHERE priority <> ? AND ${clause}`;
724
743
  const result = await this.client.execute({
@@ -733,11 +752,11 @@ var SqliteStore = class {
733
752
  const tx = await this.client.transaction("write");
734
753
  try {
735
754
  for (const { stream, snapshot, meta } of targets) {
736
- const countRow = await tx.execute({
755
+ const count_row = await tx.execute({
737
756
  sql: "SELECT COUNT(*) as c FROM events WHERE stream = ?",
738
757
  args: [stream]
739
758
  });
740
- const deleted = Number(countRow.rows[0].c);
759
+ const deleted = Number(count_row.rows[0].c);
741
760
  await tx.execute({
742
761
  sql: "DELETE FROM events WHERE stream = ?",
743
762
  args: [stream]
@@ -746,16 +765,16 @@ var SqliteStore = class {
746
765
  sql: "DELETE FROM streams WHERE stream = ?",
747
766
  args: [stream]
748
767
  });
749
- const eventName = snapshot !== void 0 ? "__snapshot__" : "__tombstone__";
750
- const eventMeta = meta ?? { correlation: "", causation: {} };
768
+ const event_name = snapshot !== void 0 ? "__snapshot__" : "__tombstone__";
769
+ const event_meta = meta ?? { correlation: "", causation: {} };
751
770
  const now = (/* @__PURE__ */ new Date()).toISOString();
752
771
  const ins = await tx.execute({
753
772
  sql: "INSERT INTO events (stream, version, name, data, meta, created) VALUES (?, 0, ?, ?, ?, ?)",
754
773
  args: [
755
774
  stream,
756
- eventName,
775
+ event_name,
757
776
  JSON.stringify(snapshot ?? {}),
758
- JSON.stringify(eventMeta),
777
+ JSON.stringify(event_meta),
759
778
  now
760
779
  ]
761
780
  });
@@ -766,9 +785,9 @@ var SqliteStore = class {
766
785
  stream,
767
786
  version: 0,
768
787
  created: new Date(now),
769
- name: eventName,
788
+ name: event_name,
770
789
  data: snapshot ?? {},
771
- meta: eventMeta
790
+ meta: event_meta
772
791
  }
773
792
  });
774
793
  }
@@ -798,14 +817,15 @@ var SqliteStore = class {
798
817
  await tx.execute("DELETE FROM sqlite_sequence WHERE name = 'events'");
799
818
  await driver(async (event) => {
800
819
  const ins = await tx.execute({
801
- sql: "INSERT INTO events (stream, version, name, data, meta, created) VALUES (?, ?, ?, ?, ?, ?)",
820
+ sql: "INSERT INTO events (stream, version, name, data, meta, created, pii) VALUES (?, ?, ?, ?, ?, ?, ?)",
802
821
  args: [
803
822
  event.stream,
804
823
  event.version,
805
824
  event.name,
806
825
  JSON.stringify(event.data),
807
826
  JSON.stringify(event.meta),
808
- event.created.toISOString()
827
+ event.created.toISOString(),
828
+ stringify_pii(event.pii)
809
829
  ]
810
830
  });
811
831
  return Number(ins.lastInsertRowid);
@@ -816,6 +836,30 @@ var SqliteStore = class {
816
836
  throw error;
817
837
  }
818
838
  }
839
+ /**
840
+ * Wipe the sensitive-data payload for every event on the stream — the
841
+ * physical-erasure side of the sensitive-data epic (#566). Sets
842
+ * `events.pii` to `NULL` for the stream's events; `events.data` and
843
+ * the rest of the row are never touched.
844
+ *
845
+ * Single `UPDATE` under SQLite's writer lock, bounded by events-per-
846
+ * stream. Idempotent — the `pii IS NOT NULL` predicate filters out
847
+ * already-wiped rows so a second call returns `0`.
848
+ *
849
+ * SQLite doesn't auto-reclaim space; freed pages stay in the file
850
+ * until an operator-scheduled `PRAGMA incremental_vacuum` or a full
851
+ * `VACUUM`. The production checklist documents the cadence.
852
+ *
853
+ * @param stream Target stream
854
+ * @returns Count of events whose `pii` was set to `NULL`
855
+ */
856
+ async forget_pii(stream) {
857
+ const r = await this.client.execute({
858
+ sql: "UPDATE events SET pii = NULL WHERE stream = ? AND pii IS NOT NULL",
859
+ args: [stream]
860
+ });
861
+ return r.rowsAffected ?? 0;
862
+ }
819
863
  };
820
864
  export {
821
865
  SqliteStore,