@modern-admin/system-prisma 0.6.0 → 0.7.0

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.
@@ -1,4 +1,4 @@
1
- import { type HistoryEntry, type HistoryOp, type HistoryRow, type IHistoryStore } from '@modern-admin/core';
1
+ import { type HistoryEntry, type HistoryOp, type HistoryRetention, type HistoryRow, type IHistoryStore } from '@modern-admin/core';
2
2
  import type { PrismaDelegate } from '../types.js';
3
3
  export declare class PrismaHistoryStore implements IHistoryStore {
4
4
  private readonly delegate;
@@ -17,5 +17,29 @@ export declare class PrismaHistoryStore implements IHistoryStore {
17
17
  }): Promise<HistoryEntry[]>;
18
18
  get(resourceId: string, recordId: string, revisionId: string): Promise<HistoryEntry | null>;
19
19
  latest(resourceId: string, recordId: string): Promise<HistoryEntry | null>;
20
+ /**
21
+ * Enforce a retention policy. Every revision of every record is kept
22
+ * forever without one — the table grows with each edit and nothing in the
23
+ * framework ever trims it.
24
+ *
25
+ * Bounds combine: a revision survives only if it satisfies all of them, so
26
+ * they are applied in sequence. `keepDays` is one `deleteMany`; `keepLast`
27
+ * is per-record, which no single `deleteMany` can express. Obsolete ids are
28
+ * selected and deleted in bounded batches. Deleting an explicit snapshot
29
+ * of ids is intentional: a concurrent append must never be caught by a
30
+ * broad `notIn` predicate and removed as if it were an old revision. This
31
+ * is a maintenance job (cron, startup), never a request-path operation.
32
+ */
33
+ prune(retention: HistoryRetention): Promise<number>;
34
+ /**
35
+ * The distinct `(resourceId, recordId)` pairs that have revisions.
36
+ *
37
+ * `groupBy` because it becomes a real SQL `GROUP BY` on every connector,
38
+ * whereas Prisma's `distinct` is applied in the query engine — outside
39
+ * Postgres that means fetching every revision row first, which is exactly
40
+ * what pruning a large table must not do. `distinct` remains as a fallback
41
+ * for delegates that do not expose `groupBy` (hand-written test doubles).
42
+ */
43
+ private recordGroups;
20
44
  }
21
45
  //# sourceMappingURL=history-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"history-store.d.ts","sourceRoot":"","sources":["../../src/stores/history-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,UAAU,EACf,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,qBAAa,kBAAmB,YAAW,aAAa;IAC1C,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAArC,YAA6B,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,EAAI;IAE/D,MAAM,CAAC,KAAK,EAAE;QAClB,UAAU,EAAE,MAAM,CAAA;QAClB,QAAQ,EAAE,MAAM,CAAA;QAChB,EAAE,EAAE,SAAS,CAAA;QACb,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACjC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KACzC,GAAG,OAAO,CAAC,YAAY,CAAC,CAaxB;IAEK,IAAI,CACR,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAChD,OAAO,CAAC,YAAY,EAAE,CAAC,CAUzB;IAEK,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAKhG;IAEK,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAM/E;CACF"}
1
+ {"version":3,"file":"history-store.d.ts","sourceRoot":"","sources":["../../src/stores/history-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,qBAAa,kBAAmB,YAAW,aAAa;IAC1C,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAArC,YAA6B,QAAQ,EAAE,cAAc,CAAC,UAAU,CAAC,EAAI;IAE/D,MAAM,CAAC,KAAK,EAAE;QAClB,UAAU,EAAE,MAAM,CAAA;QAClB,QAAQ,EAAE,MAAM,CAAA;QAChB,EAAE,EAAE,SAAS,CAAA;QACb,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACjC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KACzC,GAAG,OAAO,CAAC,YAAY,CAAC,CAaxB;IAEK,IAAI,CACR,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAO,GAChD,OAAO,CAAC,YAAY,EAAE,CAAC,CAUzB;IAEK,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAKhG;IAEK,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAM/E;IAED;;;;;;;;;;;;OAYG;IACG,KAAK,CAAC,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCxD;IAED;;;;;;;;OAQG;YACW,YAAY;CAW3B"}
@@ -1,4 +1,5 @@
1
1
  import { rowToHistoryEntry as rowToEntry, uuidv7, } from '@modern-admin/core';
2
+ const PRUNE_BATCH_SIZE = 1_000;
2
3
  export class PrismaHistoryStore {
3
4
  constructor(delegate) {
4
5
  this.delegate = delegate;
@@ -41,5 +42,71 @@ export class PrismaHistoryStore {
41
42
  });
42
43
  return row ? rowToEntry(row) : null;
43
44
  }
45
+ /**
46
+ * Enforce a retention policy. Every revision of every record is kept
47
+ * forever without one — the table grows with each edit and nothing in the
48
+ * framework ever trims it.
49
+ *
50
+ * Bounds combine: a revision survives only if it satisfies all of them, so
51
+ * they are applied in sequence. `keepDays` is one `deleteMany`; `keepLast`
52
+ * is per-record, which no single `deleteMany` can express. Obsolete ids are
53
+ * selected and deleted in bounded batches. Deleting an explicit snapshot
54
+ * of ids is intentional: a concurrent append must never be caught by a
55
+ * broad `notIn` predicate and removed as if it were an old revision. This
56
+ * is a maintenance job (cron, startup), never a request-path operation.
57
+ */
58
+ async prune(retention) {
59
+ let removed = 0;
60
+ if (retention.keepDays !== undefined) {
61
+ const cutoff = new Date(Date.now() - retention.keepDays * 24 * 60 * 60 * 1000);
62
+ const { count } = await this.delegate.deleteMany({
63
+ where: { createdAt: { lt: cutoff } },
64
+ });
65
+ removed += count;
66
+ }
67
+ if (retention.keepLast !== undefined) {
68
+ const keep = Math.max(0, Math.trunc(retention.keepLast));
69
+ const groups = await this.recordGroups();
70
+ for (const group of groups) {
71
+ const scope = { resourceId: group.resourceId, recordId: group.recordId };
72
+ while (true) {
73
+ const obsolete = (await this.delegate.findMany({
74
+ where: scope,
75
+ orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
76
+ select: { id: true },
77
+ ...(keep > 0 ? { skip: keep } : {}),
78
+ take: PRUNE_BATCH_SIZE,
79
+ }));
80
+ if (obsolete.length === 0)
81
+ break;
82
+ const { count } = await this.delegate.deleteMany({
83
+ where: { ...scope, id: { in: obsolete.map((row) => row.id) } },
84
+ });
85
+ removed += count;
86
+ }
87
+ }
88
+ }
89
+ return removed;
90
+ }
91
+ /**
92
+ * The distinct `(resourceId, recordId)` pairs that have revisions.
93
+ *
94
+ * `groupBy` because it becomes a real SQL `GROUP BY` on every connector,
95
+ * whereas Prisma's `distinct` is applied in the query engine — outside
96
+ * Postgres that means fetching every revision row first, which is exactly
97
+ * what pruning a large table must not do. `distinct` remains as a fallback
98
+ * for delegates that do not expose `groupBy` (hand-written test doubles).
99
+ */
100
+ async recordGroups() {
101
+ if (typeof this.delegate.groupBy === 'function') {
102
+ return (await this.delegate.groupBy({
103
+ by: ['resourceId', 'recordId'],
104
+ }));
105
+ }
106
+ return (await this.delegate.findMany({
107
+ distinct: ['resourceId', 'recordId'],
108
+ select: { resourceId: true, recordId: true },
109
+ }));
110
+ }
44
111
  }
45
112
  //# sourceMappingURL=history-store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"history-store.js","sourceRoot":"","sources":["../../src/stores/history-store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,IAAI,UAAU,EAC/B,MAAM,GAKP,MAAM,oBAAoB,CAAA;AAG3B,MAAM,OAAO,kBAAkB;IAC7B,YAA6B,QAAoC;wBAApC,QAAQ;IAA+B,CAAC;IAErE,KAAK,CAAC,MAAM,CAAC,KAOZ;QACC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;YACrC,IAAI,EAAE;gBACJ,EAAE,EAAE,MAAM,EAAE;gBACZ,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,IAAI;gBAC5B,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,cAAc,EAAE,KAAK,CAAC,cAAc,IAAI,IAAI;aAC7C;SACF,CAAC,CAAA;QACF,OAAO,UAAU,CAAC,GAAG,CAAC,CAAA;IACxB,CAAC;IAED,KAAK,CAAC,IAAI,CACR,UAAkB,EAClB,QAAgB,EAChB,OAAO,GAAwC,EAAE;QAEjD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAA;QACjC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAA;QAClC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACxC,KAAK,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE;YAC/B,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;YAC9B,IAAI,EAAE,KAAK;YACX,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxC,CAAC,CAAA;QACF,OAAO,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IAC7B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,UAAkB,EAAE,QAAgB,EAAE,UAAkB;QAChE,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;YACxC,KAAK,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE;SAChD,CAAC,CAAA;QACF,OAAO,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACrC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,UAAkB,EAAE,QAAgB;QAC/C,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;YACxC,KAAK,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE;YAC/B,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;SAC/B,CAAC,CAAA;QACF,OAAO,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;IACrC,CAAC;CACF"}
1
+ {"version":3,"file":"history-store.js","sourceRoot":"","sources":["../../src/stores/history-store.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"log-store.d.ts","sourceRoot":"","sources":["../../src/stores/log-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,MAAM,EACZ,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,qBAAa,cAAe,YAAW,kBAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAArC,YAA6B,QAAQ,EAAE,cAAc,CAAC,MAAM,CAAC,EAAI;IAE3D,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBjD;IAEK,IAAI,CAAC,MAAM,GAAE,UAAU,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAmB5F;CACF;AAED,kFAAkF;AAClF,YAAY,EAAE,SAAS,EAAE,CAAA"}
1
+ {"version":3,"file":"log-store.d.ts","sourceRoot":"","sources":["../../src/stores/log-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,MAAM,EACZ,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAKjD,qBAAa,cAAe,YAAW,kBAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAArC,YAA6B,QAAQ,EAAE,cAAc,CAAC,MAAM,CAAC,EAAI;IAE3D,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBjD;IAEK,IAAI,CAAC,MAAM,GAAE,UAAU,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CA2C5F;CACF;AAED,kFAAkF;AAClF,YAAY,EAAE,SAAS,EAAE,CAAA"}
@@ -1,4 +1,6 @@
1
1
  import { rowToLogEntry, uuidv7, } from '@modern-admin/core';
2
+ /** Applied when the caller passes no `limit`. Matches `MemoryLogStore`. */
3
+ const DEFAULT_LIST_LIMIT = 50;
2
4
  export class PrismaLogStore {
3
5
  constructor(delegate) {
4
6
  this.delegate = delegate;
@@ -30,18 +32,44 @@ export class PrismaLogStore {
30
32
  where['userId'] = filter.userId;
31
33
  if (filter.actions?.length)
32
34
  where['action'] = { in: filter.actions };
33
- if (filter.from || filter.to) {
34
- const range = {};
35
- if (filter.from)
36
- range['gte'] = BigInt(filter.from.getTime());
37
- if (filter.to)
38
- range['lte'] = BigInt(filter.to.getTime());
39
- where['at'] = range;
35
+ // `from`/`to` bound the window; `before` is the keyset cursor the audit
36
+ // log's "load more" pages on. All three narrow the same column, so they
37
+ // merge into one range clause — dropping `before` (as this store used to)
38
+ // made every extra page return the first page again, forever.
39
+ const range = {};
40
+ if (filter.from)
41
+ range['gte'] = BigInt(filter.from.getTime());
42
+ if (filter.to)
43
+ range['lte'] = BigInt(filter.to.getTime());
44
+ if (filter.before !== undefined) {
45
+ const cursorAt = BigInt(filter.before);
46
+ if (filter.beforeId !== undefined) {
47
+ // Lexicographic continuation of the `(at, id) DESC` order. Keeping
48
+ // the id in the cursor is essential because `at` has ms precision and
49
+ // many audit entries can legitimately share it.
50
+ where['OR'] = [
51
+ { at: { lt: cursorAt } },
52
+ { at: cursorAt, id: { lt: filter.beforeId } },
53
+ ];
54
+ }
55
+ else {
56
+ // Backwards-compatible timestamp-only cursor for existing callers.
57
+ range['lt'] = cursorAt;
58
+ }
40
59
  }
60
+ if (Object.keys(range).length > 0)
61
+ where['at'] = range;
41
62
  const rows = await this.delegate.findMany({
42
63
  where,
43
- orderBy: { at: 'desc' },
44
- ...(filter.limit !== undefined ? { take: filter.limit } : {}),
64
+ // `at` is milliseconds and not unique, so it alone is not a total
65
+ // order two entries in the same millisecond could swap between
66
+ // pages. Ids are UUIDv7, i.e. already time-ordered, so they are the
67
+ // natural tiebreaker.
68
+ orderBy: [{ at: 'desc' }, { id: 'desc' }],
69
+ // The audit log is append-only and unbounded; an unbounded SELECT over
70
+ // it is a memory hazard. Compare `PrismaHistoryStore.list`, which has
71
+ // always defaulted to 50.
72
+ take: filter.limit ?? DEFAULT_LIST_LIMIT,
45
73
  ...(filter.offset !== undefined ? { skip: filter.offset } : {}),
46
74
  });
47
75
  return rows.map(rowToLogEntry);
@@ -1 +1 @@
1
- {"version":3,"file":"log-store.js","sourceRoot":"","sources":["../../src/stores/log-store.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,MAAM,GAKP,MAAM,oBAAoB,CAAA;AAG3B,MAAM,OAAO,cAAc;IACzB,YAA6B,QAAgC;wBAAhC,QAAQ;IAA2B,CAAC;IAEjE,KAAK,CAAC,MAAM,CAAC,KAAqB;QAChC,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;YACzB,IAAI,EAAE;gBACJ,qEAAqE;gBACrE,qEAAqE;gBACrE,EAAE,EAAE,KAAK,CAAC,EAAE,IAAI,MAAM,EAAE;gBACxB,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,IAAI;gBAChC,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI;gBAClC,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,IAAI;gBAC5B,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI;gBAC9B,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,IAAI;gBAC5B,EAAE,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;aACrB;SACF,CAAC,CAAA;IACJ,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,MAAM,GAA8C,EAAE;QAC/D,MAAM,KAAK,GAA4B,EAAE,CAAA;QACzC,IAAI,MAAM,CAAC,UAAU;YAAE,KAAK,CAAC,YAAY,CAAC,GAAG,MAAM,CAAC,UAAU,CAAA;QAC9D,IAAI,MAAM,CAAC,QAAQ;YAAE,KAAK,CAAC,UAAU,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAA;QACxD,IAAI,MAAM,CAAC,MAAM;YAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,MAAM,CAAC,MAAM,CAAA;QAClD,IAAI,MAAM,CAAC,OAAO,EAAE,MAAM;YAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,EAAE,MAAM,CAAC,OAAO,EAAE,CAAA;QACpE,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;YAC7B,MAAM,KAAK,GAA2B,EAAE,CAAA;YACxC,IAAI,MAAM,CAAC,IAAI;gBAAE,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;YAC7D,IAAI,MAAM,CAAC,EAAE;gBAAE,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;YACzD,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAA;QACrB,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACxC,KAAK;YACL,OAAO,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE;YACvB,GAAG,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,GAAG,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAChE,CAAC,CAAA;QACF,OAAO,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;IAChC,CAAC;CACF"}
1
+ {"version":3,"file":"log-store.js","sourceRoot":"","sources":["../../src/stores/log-store.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -34,6 +34,11 @@ export interface PrismaDelegate<TRow = any> {
34
34
  count: number;
35
35
  }>;
36
36
  count(args?: any): Promise<number>;
37
+ /**
38
+ * Present on every generated Prisma delegate. Optional here so hand-written
39
+ * test doubles stay assignable; callers fall back when it is absent.
40
+ */
41
+ groupBy?(args: any): Promise<any[]>;
37
42
  }
38
43
  /**
39
44
  * Whatever `prisma` instance the host injects. We index into it by model
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,WAAW,cAAc,CAAC,IAAI,GAAG,GAAG;IACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IACrC,UAAU,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;IACtD,SAAS,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;IAC3C,MAAM,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1C,MAAM,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,GAAG,CAAC;QAAC,IAAI,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,MAAM,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,GAAG,CAAC;QAAC,MAAM,EAAE,GAAG,CAAC;QAAC,MAAM,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrE,MAAM,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,UAAU,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC9D,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACnC;AAED;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,CAAA;AAE7C,eAAO,MAAM,cAAc;aACzB,GAAG,EAAE,OAAO;aACZ,OAAO,EAAE,WAAW;aACpB,eAAe,EAAE,mBAAmB;aACpC,MAAM,EAAE,UAAU;aAClB,OAAO,EAAE,WAAW;aACpB,MAAM,EAAE,UAAU;aAClB,WAAW,EAAE,eAAe;aAC5B,KAAK,EAAE,SAAS;CACR,CAAA;AAEV,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,cAAc,CAAA;AAClD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAA;AAE9D,wBAAgB,eAAe,CAC7B,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,QAAQ,EACb,SAAS,EAAE,cAAc,GAAG,SAAS,GACpC,cAAc,CAWhB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,WAAW,cAAc,CAAC,IAAI,GAAG,GAAG;IACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IACrC,UAAU,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;IACtD,SAAS,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;IAC3C,MAAM,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1C,MAAM,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,GAAG,CAAC;QAAC,IAAI,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,MAAM,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,GAAG,CAAC;QAAC,MAAM,EAAE,GAAG,CAAC;QAAC,MAAM,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrE,MAAM,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3C,UAAU,CAAC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,GAAG,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC9D,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAClC;;;OAGG;IACH,OAAO,CAAC,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAA;CACpC;AAED;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,CAAA;AAE7C,eAAO,MAAM,cAAc;aACzB,GAAG,EAAE,OAAO;aACZ,OAAO,EAAE,WAAW;aACpB,eAAe,EAAE,mBAAmB;aACpC,MAAM,EAAE,UAAU;aAClB,OAAO,EAAE,WAAW;aACpB,MAAM,EAAE,UAAU;aAClB,WAAW,EAAE,eAAe;aAC5B,KAAK,EAAE,SAAS;CACR,CAAA;AAEV,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,cAAc,CAAA;AAClD,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAA;AAE9D,wBAAgB,eAAe,CAC7B,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,QAAQ,EACb,SAAS,EAAE,cAAc,GAAG,SAAS,GACpC,cAAc,CAWhB"}
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgCH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,GAAG,EAAE,OAAO;IACZ,OAAO,EAAE,WAAW;IACpB,eAAe,EAAE,mBAAmB;IACpC,MAAM,EAAE,UAAU;IAClB,OAAO,EAAE,WAAW;IACpB,MAAM,EAAE,UAAU;IAClB,WAAW,EAAE,eAAe;IAC5B,KAAK,EAAE,SAAS;CACR,CAAA;AAKV,MAAM,UAAU,eAAe,CAC7B,MAAkB,EAClB,GAAa,EACb,SAAqC;IAErC,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC,GAAG,CAAC,IAAI,cAAc,CAAC,GAAG,CAAC,CAAA;IACpD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAA+B,CAAA;IAC3D,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CACb,yDAAyD,IAAI,KAAK;YAClE,+EAA+E;YAC/E,0FAA0F,CAC3F,CAAA;IACH,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqCH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,GAAG,EAAE,OAAO;IACZ,OAAO,EAAE,WAAW;IACpB,eAAe,EAAE,mBAAmB;IACpC,MAAM,EAAE,UAAU;IAClB,OAAO,EAAE,WAAW;IACpB,MAAM,EAAE,UAAU;IAClB,WAAW,EAAE,eAAe;IAC5B,KAAK,EAAE,SAAS;CACR,CAAA;AAKV,MAAM,UAAU,eAAe,CAC7B,MAAkB,EAClB,GAAa,EACb,SAAqC;IAErC,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC,GAAG,CAAC,IAAI,cAAc,CAAC,GAAG,CAAC,CAAA;IACpD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAA+B,CAAA;IAC3D,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CACb,yDAAyD,IAAI,KAAK;YAClE,+EAA+E;YAC/E,0FAA0F,CAC3F,CAAA;IACH,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modern-admin/system-prisma",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Prisma-backed implementation of Modern Admin system stores (logs, history, webhooks, AI tasks).",
5
5
  "homepage": "https://github.com/modern-admin/modern-admin/tree/main/packages/system-prisma#readme",
6
6
  "keywords": [
@@ -45,7 +45,7 @@
45
45
  }
46
46
  },
47
47
  "dependencies": {
48
- "@modern-admin/core": "^0.6.0"
48
+ "@modern-admin/core": "^0.7.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@modern-admin/tsconfig": "0.4.0",
@@ -3,11 +3,14 @@ import {
3
3
  uuidv7,
4
4
  type HistoryEntry,
5
5
  type HistoryOp,
6
+ type HistoryRetention,
6
7
  type HistoryRow,
7
8
  type IHistoryStore,
8
9
  } from '@modern-admin/core'
9
10
  import type { PrismaDelegate } from '../types.js'
10
11
 
12
+ const PRUNE_BATCH_SIZE = 1_000
13
+
11
14
  export class PrismaHistoryStore implements IHistoryStore {
12
15
  constructor(private readonly delegate: PrismaDelegate<HistoryRow>) {}
13
16
 
@@ -63,4 +66,75 @@ export class PrismaHistoryStore implements IHistoryStore {
63
66
  })
64
67
  return row ? rowToEntry(row) : null
65
68
  }
69
+
70
+ /**
71
+ * Enforce a retention policy. Every revision of every record is kept
72
+ * forever without one — the table grows with each edit and nothing in the
73
+ * framework ever trims it.
74
+ *
75
+ * Bounds combine: a revision survives only if it satisfies all of them, so
76
+ * they are applied in sequence. `keepDays` is one `deleteMany`; `keepLast`
77
+ * is per-record, which no single `deleteMany` can express. Obsolete ids are
78
+ * selected and deleted in bounded batches. Deleting an explicit snapshot
79
+ * of ids is intentional: a concurrent append must never be caught by a
80
+ * broad `notIn` predicate and removed as if it were an old revision. This
81
+ * is a maintenance job (cron, startup), never a request-path operation.
82
+ */
83
+ async prune(retention: HistoryRetention): Promise<number> {
84
+ let removed = 0
85
+
86
+ if (retention.keepDays !== undefined) {
87
+ const cutoff = new Date(Date.now() - retention.keepDays * 24 * 60 * 60 * 1000)
88
+ const { count } = await this.delegate.deleteMany({
89
+ where: { createdAt: { lt: cutoff } },
90
+ })
91
+ removed += count
92
+ }
93
+
94
+ if (retention.keepLast !== undefined) {
95
+ const keep = Math.max(0, Math.trunc(retention.keepLast))
96
+ const groups = await this.recordGroups()
97
+ for (const group of groups) {
98
+ const scope = { resourceId: group.resourceId, recordId: group.recordId }
99
+ while (true) {
100
+ const obsolete = (await this.delegate.findMany({
101
+ where: scope,
102
+ orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
103
+ select: { id: true },
104
+ ...(keep > 0 ? { skip: keep } : {}),
105
+ take: PRUNE_BATCH_SIZE,
106
+ })) as unknown as Array<{ id: string }>
107
+ if (obsolete.length === 0) break
108
+
109
+ const { count } = await this.delegate.deleteMany({
110
+ where: { ...scope, id: { in: obsolete.map((row) => row.id) } },
111
+ })
112
+ removed += count
113
+ }
114
+ }
115
+ }
116
+
117
+ return removed
118
+ }
119
+
120
+ /**
121
+ * The distinct `(resourceId, recordId)` pairs that have revisions.
122
+ *
123
+ * `groupBy` because it becomes a real SQL `GROUP BY` on every connector,
124
+ * whereas Prisma's `distinct` is applied in the query engine — outside
125
+ * Postgres that means fetching every revision row first, which is exactly
126
+ * what pruning a large table must not do. `distinct` remains as a fallback
127
+ * for delegates that do not expose `groupBy` (hand-written test doubles).
128
+ */
129
+ private async recordGroups(): Promise<Array<{ resourceId: string; recordId: string }>> {
130
+ if (typeof this.delegate.groupBy === 'function') {
131
+ return (await this.delegate.groupBy({
132
+ by: ['resourceId', 'recordId'],
133
+ })) as Array<{ resourceId: string; recordId: string }>
134
+ }
135
+ return (await this.delegate.findMany({
136
+ distinct: ['resourceId', 'recordId'],
137
+ select: { resourceId: true, recordId: true },
138
+ })) as unknown as Array<{ resourceId: string; recordId: string }>
139
+ }
66
140
  }
@@ -8,6 +8,9 @@ import {
8
8
  } from '@modern-admin/core'
9
9
  import type { PrismaDelegate } from '../types.js'
10
10
 
11
+ /** Applied when the caller passes no `limit`. Matches `MemoryLogStore`. */
12
+ const DEFAULT_LIST_LIMIT = 50
13
+
11
14
  export class PrismaLogStore implements IQueryableLogStore {
12
15
  constructor(private readonly delegate: PrismaDelegate<LogRow>) {}
13
16
 
@@ -35,16 +38,40 @@ export class PrismaLogStore implements IQueryableLogStore {
35
38
  if (filter.recordId) where['recordId'] = filter.recordId
36
39
  if (filter.userId) where['userId'] = filter.userId
37
40
  if (filter.actions?.length) where['action'] = { in: filter.actions }
38
- if (filter.from || filter.to) {
39
- const range: Record<string, bigint> = {}
40
- if (filter.from) range['gte'] = BigInt(filter.from.getTime())
41
- if (filter.to) range['lte'] = BigInt(filter.to.getTime())
42
- where['at'] = range
41
+ // `from`/`to` bound the window; `before` is the keyset cursor the audit
42
+ // log's "load more" pages on. All three narrow the same column, so they
43
+ // merge into one range clause — dropping `before` (as this store used to)
44
+ // made every extra page return the first page again, forever.
45
+ const range: Record<string, bigint> = {}
46
+ if (filter.from) range['gte'] = BigInt(filter.from.getTime())
47
+ if (filter.to) range['lte'] = BigInt(filter.to.getTime())
48
+ if (filter.before !== undefined) {
49
+ const cursorAt = BigInt(filter.before)
50
+ if (filter.beforeId !== undefined) {
51
+ // Lexicographic continuation of the `(at, id) DESC` order. Keeping
52
+ // the id in the cursor is essential because `at` has ms precision and
53
+ // many audit entries can legitimately share it.
54
+ where['OR'] = [
55
+ { at: { lt: cursorAt } },
56
+ { at: cursorAt, id: { lt: filter.beforeId } },
57
+ ]
58
+ } else {
59
+ // Backwards-compatible timestamp-only cursor for existing callers.
60
+ range['lt'] = cursorAt
61
+ }
43
62
  }
63
+ if (Object.keys(range).length > 0) where['at'] = range
44
64
  const rows = await this.delegate.findMany({
45
65
  where,
46
- orderBy: { at: 'desc' },
47
- ...(filter.limit !== undefined ? { take: filter.limit } : {}),
66
+ // `at` is milliseconds and not unique, so it alone is not a total
67
+ // order two entries in the same millisecond could swap between
68
+ // pages. Ids are UUIDv7, i.e. already time-ordered, so they are the
69
+ // natural tiebreaker.
70
+ orderBy: [{ at: 'desc' }, { id: 'desc' }],
71
+ // The audit log is append-only and unbounded; an unbounded SELECT over
72
+ // it is a memory hazard. Compare `PrismaHistoryStore.list`, which has
73
+ // always defaulted to 50.
74
+ take: filter.limit ?? DEFAULT_LIST_LIMIT,
48
75
  ...(filter.offset !== undefined ? { skip: filter.offset } : {}),
49
76
  })
50
77
  return rows.map(rowToLogEntry)
package/src/types.ts CHANGED
@@ -20,6 +20,11 @@ export interface PrismaDelegate<TRow = any> {
20
20
  delete(args: { where: any }): Promise<TRow>
21
21
  deleteMany(args?: { where?: any }): Promise<{ count: number }>
22
22
  count(args?: any): Promise<number>
23
+ /**
24
+ * Present on every generated Prisma delegate. Optional here so hand-written
25
+ * test doubles stay assignable; callers fall back when it is absent.
26
+ */
27
+ groupBy?(args: any): Promise<any[]>
23
28
  }
24
29
 
25
30
  /**