@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.
- package/dist/stores/history-store.d.ts +25 -1
- package/dist/stores/history-store.d.ts.map +1 -1
- package/dist/stores/history-store.js +67 -0
- package/dist/stores/history-store.js.map +1 -1
- package/dist/stores/log-store.d.ts.map +1 -1
- package/dist/stores/log-store.js +37 -9
- package/dist/stores/log-store.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/stores/history-store.ts +74 -0
- package/src/stores/log-store.ts +34 -7
- package/src/types.ts +5 -0
|
@@ -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;
|
|
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,
|
|
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,GAMP,MAAM,oBAAoB,CAAA;AAG3B,MAAM,gBAAgB,GAAG,KAAK,CAAA;AAE9B,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;IAED;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,KAAK,CAAC,SAA2B;QACrC,IAAI,OAAO,GAAG,CAAC,CAAA;QAEf,IAAI,SAAS,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACrC,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,QAAQ,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;YAC9E,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAC/C,KAAK,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE;aACrC,CAAC,CAAA;YACF,OAAO,IAAI,KAAK,CAAA;QAClB,CAAC;QAED,IAAI,SAAS,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YACrC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAA;YACxD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAA;YACxC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,MAAM,KAAK,GAAG,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAA;gBACxE,OAAO,IAAI,EAAE,CAAC;oBACZ,MAAM,QAAQ,GAAG,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;wBAC7C,KAAK,EAAE,KAAK;wBACZ,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;wBAChD,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE;wBACpB,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBACnC,IAAI,EAAE,gBAAgB;qBACvB,CAAC,CAAqC,CAAA;oBACvC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;wBAAE,MAAK;oBAEhC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;wBAC/C,KAAK,EAAE,EAAE,GAAG,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE;qBAC/D,CAAC,CAAA;oBACF,OAAO,IAAI,KAAK,CAAA;gBAClB,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,OAAO,CAAA;IAChB,CAAC;IAED;;;;;;;;OAQG;IACK,KAAK,CAAC,YAAY;QACxB,IAAI,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;YAChD,OAAO,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAClC,EAAE,EAAE,CAAC,YAAY,EAAE,UAAU,CAAC;aAC/B,CAAC,CAAoD,CAAA;QACxD,CAAC;QACD,OAAO,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACnC,QAAQ,EAAE,CAAC,YAAY,EAAE,UAAU,CAAC;YACpC,MAAM,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE;SAC7C,CAAC,CAA+D,CAAA;IACnE,CAAC;CACF"}
|
|
@@ -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;
|
|
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"}
|
package/dist/stores/log-store.js
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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,
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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.
|
|
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.
|
|
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
|
}
|
package/src/stores/log-store.ts
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
/**
|