@endora-commerce/mod-audit-logs 0.100.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/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/admin/index.d.ts +34 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +39 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/AuditLogViewer.d.ts +3 -0
- package/dist/admin/pages/AuditLogViewer.d.ts.map +1 -0
- package/dist/admin/pages/AuditLogViewer.js +112 -0
- package/dist/admin/pages/AuditLogViewer.js.map +1 -0
- package/dist/backend/cli/read.d.ts +20 -0
- package/dist/backend/cli/read.d.ts.map +1 -0
- package/dist/backend/cli/read.js +94 -0
- package/dist/backend/cli/read.js.map +1 -0
- package/dist/backend/commands/recent-activity-visibility.commands.d.ts +31 -0
- package/dist/backend/commands/recent-activity-visibility.commands.d.ts.map +1 -0
- package/dist/backend/commands/recent-activity-visibility.commands.js +55 -0
- package/dist/backend/commands/recent-activity-visibility.commands.js.map +1 -0
- package/dist/backend/index.d.ts +91 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +117 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +29 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +85 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.admin.recent-activity.d.ts +126 -0
- package/dist/backend/routes.admin.recent-activity.d.ts.map +1 -0
- package/dist/backend/routes.admin.recent-activity.js +106 -0
- package/dist/backend/routes.admin.recent-activity.js.map +1 -0
- package/dist/backend/services/audit-reference-registry.d.ts +28 -0
- package/dist/backend/services/audit-reference-registry.d.ts.map +1 -0
- package/dist/backend/services/audit-reference-registry.js +57 -0
- package/dist/backend/services/audit-reference-registry.js.map +1 -0
- package/dist/backend/services/recent-activity-catalog.d.ts +92 -0
- package/dist/backend/services/recent-activity-catalog.d.ts.map +1 -0
- package/dist/backend/services/recent-activity-catalog.js +82 -0
- package/dist/backend/services/recent-activity-catalog.js.map +1 -0
- package/dist/backend/services/recent-activity-service.d.ts +105 -0
- package/dist/backend/services/recent-activity-service.d.ts.map +1 -0
- package/dist/backend/services/recent-activity-service.js +308 -0
- package/dist/backend/services/recent-activity-service.js.map +1 -0
- package/dist/backend/services/recent-activity-visibility.d.ts +37 -0
- package/dist/backend/services/recent-activity-visibility.d.ts.map +1 -0
- package/dist/backend/services/recent-activity-visibility.js +81 -0
- package/dist/backend/services/recent-activity-visibility.js.map +1 -0
- package/dist/manifest.d.ts +191 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +123 -0
- package/dist/manifest.js.map +1 -0
- package/docs/audit_logs.md +47 -0
- package/i18n/en.json +5 -0
- package/i18n/pl.json +5 -0
- package/package.json +86 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAI5E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAI5D,OAAO,EACL,qBAAqB,EACrB,KAAK,+BAA+B,EACrC,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,MAAM,0CAA0C,CAAC;AAEpF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,kBAAkB,EACvB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC,GAClD,SAAS,CAAC;IACd,QAAQ,CAAC,sBAAsB,EAAE,0BAA0B,CAAC;IAC5D,QAAQ,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;IACtD,QAAQ,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;IACtD,QAAQ,CAAC,wBAAwB,EAAE,wBAAwB,CAAC;IAC5D;;;;;;;OAOG;IACH,QAAQ,CAAC,sBAAsB,EAAE,aAAa,CAAC,+BAA+B,CAAC,CAAC;IAChF,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,UAAU,GAAG,SAAS,CAAC;CAC7C;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAiIvD;AAGD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ,EAAE,SAAS,KAAK,EAAO,CAAC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { lazyPort } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { effectiveState } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import { ModuleDisabledError } from '@endora-commerce/platform/kernel';
|
|
4
|
+
import { registerAuditLogAdminRoutes } from './routes.admin.js';
|
|
5
|
+
import { registerRecentActivityRoutes } from './routes.admin.recent-activity.js';
|
|
6
|
+
import { AuditReferenceRegistry } from './services/audit-reference-registry.js';
|
|
7
|
+
import { RecentActivityCatalog, } from './services/recent-activity-catalog.js';
|
|
8
|
+
import { RecentActivityService } from './services/recent-activity-service.js';
|
|
9
|
+
import { RecentActivityVisibility } from './services/recent-activity-visibility.js';
|
|
10
|
+
export function registerModule(ctx) {
|
|
11
|
+
/**
|
|
12
|
+
* Turning an actor id into a name, through the contribution point above.
|
|
13
|
+
*
|
|
14
|
+
* One closure, two readers — the audit-log list and the dashboard card — so
|
|
15
|
+
* there is exactly one place that decides what an absent directory means, and
|
|
16
|
+
* exactly one `catch` for a reviewer to check. The dashboard card used to
|
|
17
|
+
* decide it for itself, by running `select … from admin_users` (feature 075,
|
|
18
|
+
* D-87).
|
|
19
|
+
*/
|
|
20
|
+
const resolveActors = async (ids) => {
|
|
21
|
+
const resolve = ctx.cradle().auditActorResolver;
|
|
22
|
+
if (resolve === undefined)
|
|
23
|
+
return [];
|
|
24
|
+
try {
|
|
25
|
+
return await resolve(ids);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// The enrichment is optional; the record is not. Anything else is
|
|
29
|
+
// a real failure and must not be swallowed into a blank column.
|
|
30
|
+
if (error instanceof ModuleDisabledError)
|
|
31
|
+
return [];
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
ctx.di.register({
|
|
36
|
+
/**
|
|
37
|
+
* Who can turn one of *their* ids into a name and a link (feature 075,
|
|
38
|
+
* D-87). A **contribution seam**, so a plain `ctx.di.register`: contributors
|
|
39
|
+
* push from their own boot hooks, and a gate here would refuse them at a
|
|
40
|
+
* point in the boot where there is nothing to answer.
|
|
41
|
+
*
|
|
42
|
+
* The presence predicate is this module's enumeration policy — skip — and it
|
|
43
|
+
* is asked per read rather than per registration, so an operator's flip
|
|
44
|
+
* takes effect without a restart.
|
|
45
|
+
*/
|
|
46
|
+
auditReferenceRegistry: ctx
|
|
47
|
+
.asFunction(() => new AuditReferenceRegistry((moduleId) => effectiveState.isPresent(moduleId)))
|
|
48
|
+
.singleton(),
|
|
49
|
+
/**
|
|
50
|
+
* The four host-owned action tables, as one derivation (D-163.1).
|
|
51
|
+
*
|
|
52
|
+
* A singleton, and legitimately so: the composed manifest set does not
|
|
53
|
+
* change while the process runs — an installed package is discovered before
|
|
54
|
+
* the first module registers — so what is captured here is a fact about the
|
|
55
|
+
* build. The *operator's* half is captured nowhere and is asked per request,
|
|
56
|
+
* which is the half that moves.
|
|
57
|
+
*/
|
|
58
|
+
recentActivityCatalog: ctx
|
|
59
|
+
.asFunction(({ resolvedModuleRegistry }) => new RecentActivityCatalog(resolvedModuleRegistry))
|
|
60
|
+
.singleton(),
|
|
61
|
+
/**
|
|
62
|
+
* The operator's half, and the reason `settingsReadPort` arrives as a
|
|
63
|
+
* `lazyPort` rather than off the cradle: this registration is a singleton,
|
|
64
|
+
* and a cradle parameter would resolve the settings reader **once**, when
|
|
65
|
+
* the registration is first constructed. Awilix's strict mode refuses that
|
|
66
|
+
* capture and `check:port-dependencies` reports it — rightly, since a
|
|
67
|
+
* captured reader is a preference frozen at composition, which is exactly
|
|
68
|
+
* what an operator flipping a switch must not meet.
|
|
69
|
+
*/
|
|
70
|
+
recentActivityVisibility: ctx
|
|
71
|
+
.asFunction(({ recentActivityCatalog }) => new RecentActivityVisibility(recentActivityCatalog, lazyPort(ctx, 'settingsReadPort')))
|
|
72
|
+
.singleton(),
|
|
73
|
+
recentActivityService: ctx
|
|
74
|
+
.asFunction(({ emFactory, auditReferenceRegistry, recentActivityCatalog, recentActivityVisibility, }) => new RecentActivityService(emFactory, recentActivityCatalog, recentActivityVisibility, resolveActors, auditReferenceRegistry))
|
|
75
|
+
.singleton(),
|
|
76
|
+
// Contribution point, defaulted to absent by its owner. A deployment
|
|
77
|
+
// without `admin_users` reads the audit log with raw actor ids rather than
|
|
78
|
+
// failing to read it.
|
|
79
|
+
auditActorResolver: ctx
|
|
80
|
+
.asFunction(() => undefined)
|
|
81
|
+
.singleton(),
|
|
82
|
+
});
|
|
83
|
+
ctx.routes(async (app) => {
|
|
84
|
+
const { auditLogService, recentActivityService, recentActivityCatalog, recentActivityVisibility, requireAdmin, commandBus, } = ctx.cradle();
|
|
85
|
+
await registerAuditLogAdminRoutes(app, {
|
|
86
|
+
auditLogService,
|
|
87
|
+
requireAdmin,
|
|
88
|
+
// Resolved per request, and per request is what makes the degradation
|
|
89
|
+
// work: a contributor captured once would keep naming actors long after
|
|
90
|
+
// its module went away.
|
|
91
|
+
resolveActors,
|
|
92
|
+
});
|
|
93
|
+
await registerRecentActivityRoutes(app, {
|
|
94
|
+
recentActivityService,
|
|
95
|
+
recentActivityCatalog,
|
|
96
|
+
recentActivityVisibility,
|
|
97
|
+
requireAdmin,
|
|
98
|
+
...(commandBus ? { commandBus } : {}),
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* This module owns **no persisted entity**, and says so with an empty array
|
|
104
|
+
* rather than by omission (D-168).
|
|
105
|
+
*
|
|
106
|
+
* The two are not the same thing to the platform. When the package is
|
|
107
|
+
* *installed*, `src/packages/package-runtime.ts` reads `exported['entities']`
|
|
108
|
+
* and answers a missing export with `[]` — so "this module has no table" and
|
|
109
|
+
* "somebody forgot the array" arrive at the host as the same silence, and the
|
|
110
|
+
* only symptom of the second is a query against a table nobody created. The
|
|
111
|
+
* declaration is what makes the first case a statement.
|
|
112
|
+
*
|
|
113
|
+
* A first entity added here goes in this array in the same merge request, and
|
|
114
|
+
* `test/unit/packages/module-package-entity-surface.test.ts` is what says so.
|
|
115
|
+
*/
|
|
116
|
+
export const entities = [];
|
|
117
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;AAG5D,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAEvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EACL,qBAAqB,GAEtB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,MAAM,0CAA0C,CAAC;AAsEpF,MAAM,UAAU,cAAc,CAAC,GAAkB;IAC/C;;;;;;;;OAQG;IACH,MAAM,aAAa,GAAG,KAAK,EAAE,GAAa,EAAiC,EAAE;QAC3E,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,EAAmB,CAAC,kBAAkB,CAAC;QACjE,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,OAAO,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,kEAAkE;YAClE,gEAAgE;YAChE,IAAI,KAAK,YAAY,mBAAmB;gBAAE,OAAO,EAAE,CAAC;YACpD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC,CAAC;IAEF,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC;QACd;;;;;;;;;WASG;QACH,sBAAsB,EAAE,GAAG;aACxB,UAAU,CACT,GAA+B,EAAE,CAC/B,IAAI,sBAAsB,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAC/E;aACA,SAAS,EAAE;QAEd;;;;;;;;WAQG;QACH,qBAAqB,EAAE,GAAG;aACvB,UAAU,CACT,CAAC,EAAE,sBAAsB,EAAmB,EAAE,EAAE,CAC9C,IAAI,qBAAqB,CAAC,sBAAsB,CAAC,CACpD;aACA,SAAS,EAAE;QAEd;;;;;;;;WAQG;QACH,wBAAwB,EAAE,GAAG;aAC1B,UAAU,CACT,CAAC,EAAE,qBAAqB,EAAmB,EAAE,EAAE,CAC7C,IAAI,wBAAwB,CAC1B,qBAAqB,EACrB,QAAQ,CAAmB,GAAG,EAAE,kBAAkB,CAAC,CACpD,CACJ;aACA,SAAS,EAAE;QAEd,qBAAqB,EAAE,GAAG;aACvB,UAAU,CACT,CAAC,EACC,SAAS,EACT,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,GACR,EAAE,EAAE,CACpB,IAAI,qBAAqB,CACvB,SAAS,EACT,qBAAqB,EACrB,wBAAwB,EACxB,aAAa,EACb,sBAAsB,CACvB,CACJ;aACA,SAAS,EAAE;QAEd,qEAAqE;QACrE,2EAA2E;QAC3E,sBAAsB;QACtB,kBAAkB,EAAE,GAAG;aACpB,UAAU,CAAC,GAAmE,EAAE,CAAC,SAAS,CAAC;aAC3F,SAAS,EAAE;KACf,CAAC,CAAC;IAEH,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,MAAM,EACJ,eAAe,EACf,qBAAqB,EACrB,qBAAqB,EACrB,wBAAwB,EACxB,YAAY,EACZ,UAAU,GACX,GAAG,GAAG,CAAC,MAAM,EAAmB,CAAC;QAElC,MAAM,2BAA2B,CAAC,GAAG,EAAE;YACrC,eAAe;YACf,YAAY;YACZ,sEAAsE;YACtE,wEAAwE;YACxE,wBAAwB;YACxB,aAAa;SACd,CAAC,CAAC;QAEH,MAAM,4BAA4B,CAAC,GAAG,EAAE;YACtC,qBAAqB;YACrB,qBAAqB;YACrB,wBAAwB;YACxB,YAAY;YACZ,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAGD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAqB,EAAE,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { AuditPort } from '@endora-commerce/platform/kernel';
|
|
3
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
4
|
+
/**
|
|
5
|
+
* Admin Audit Log query route (T195 / FR-084).
|
|
6
|
+
*
|
|
7
|
+
* Always reads — never mutates — the append-only audit log. Filters are
|
|
8
|
+
* accepted as query params (`filter[actor]`, `filter[action]`, …); a
|
|
9
|
+
* default `limit=100` keeps the page lightweight, capped at 500.
|
|
10
|
+
*/
|
|
11
|
+
/** Minimal actor identity used to enrich the audit-log actor column. */
|
|
12
|
+
export interface AuditActorIdentity {
|
|
13
|
+
id: string;
|
|
14
|
+
firstName: string;
|
|
15
|
+
lastName: string;
|
|
16
|
+
email: string;
|
|
17
|
+
}
|
|
18
|
+
export interface AuditLogAdminDeps {
|
|
19
|
+
auditLogService: AuditPort;
|
|
20
|
+
requireAdmin: RequireAdminFactory;
|
|
21
|
+
/**
|
|
22
|
+
* Resolves admin-user identities for the actor column (name + email instead
|
|
23
|
+
* of a bare id). Unknown ids may be omitted. Optional — when absent the
|
|
24
|
+
* column falls back to the raw id.
|
|
25
|
+
*/
|
|
26
|
+
resolveActors?: (ids: string[]) => Promise<AuditActorIdentity[]>;
|
|
27
|
+
}
|
|
28
|
+
export declare function registerAuditLogAdminRoutes(app: FastifyInstance, deps: AuditLogAdminDeps): Promise<void>;
|
|
29
|
+
//# sourceMappingURL=routes.admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAElE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;;;GAMG;AAEH,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,eAAe,EAAE,SAAS,CAAC;IAC3B,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;CAClE;AAED,wBAAsB,2BAA2B,CAC/C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,IAAI,CAAC,CA+Bf"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export async function registerAuditLogAdminRoutes(app, deps) {
|
|
2
|
+
const { auditLogService, requireAdmin, resolveActors } = deps;
|
|
3
|
+
app.get('/api/v1/admin/audit-log', { preHandler: requireAdmin('audit_log:read') }, async (request) => {
|
|
4
|
+
const q = (request.query ?? {});
|
|
5
|
+
const limitNum = q['limit'] ? Number.parseInt(q['limit'], 10) : undefined;
|
|
6
|
+
const entries = await auditLogService.query({
|
|
7
|
+
...(q['filter[actor]'] ? { actorAdminUserId: q['filter[actor]'] } : {}),
|
|
8
|
+
...(q['filter[customer]']
|
|
9
|
+
? { impersonatedCustomerAccountId: q['filter[customer]'] }
|
|
10
|
+
: {}),
|
|
11
|
+
...(q['filter[action]'] ? { action: q['filter[action]'] } : {}),
|
|
12
|
+
...(q['filter[objectType]'] ? { objectType: q['filter[objectType]'] } : {}),
|
|
13
|
+
...(q['filter[objectId]'] ? { objectId: q['filter[objectId]'] } : {}),
|
|
14
|
+
...(limitNum !== undefined && Number.isFinite(limitNum) ? { limit: limitNum } : {}),
|
|
15
|
+
});
|
|
16
|
+
// Enrich the actor column with name + email (one batched lookup).
|
|
17
|
+
const actorIds = entries
|
|
18
|
+
.map((e) => e.actorAdminUserId)
|
|
19
|
+
.filter((id) => Boolean(id));
|
|
20
|
+
const actors = resolveActors ? await resolveActors(actorIds) : [];
|
|
21
|
+
const actorById = new Map(actors.map((a) => [a.id, a]));
|
|
22
|
+
return {
|
|
23
|
+
data: entries.map((e) => serialize(e, actorById)),
|
|
24
|
+
pagination: { cursor: null, hasMore: false, limit: entries.length },
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function serialize(e, actorById) {
|
|
29
|
+
const actor = e.actorAdminUserId ? actorById.get(e.actorAdminUserId) : undefined;
|
|
30
|
+
return {
|
|
31
|
+
id: e.id,
|
|
32
|
+
actorAdminUserId: e.actorAdminUserId ?? null,
|
|
33
|
+
actorName: actor ? `${actor.firstName} ${actor.lastName}`.trim() : null,
|
|
34
|
+
actorEmail: actor?.email ?? null,
|
|
35
|
+
impersonatedCustomerAccountId: e.impersonatedCustomerAccountId ?? null,
|
|
36
|
+
actedAt: e.actedAt.toISOString(),
|
|
37
|
+
action: e.action,
|
|
38
|
+
actionModuleId: moduleIdForAuditAction(e.action),
|
|
39
|
+
objectType: e.objectType,
|
|
40
|
+
objectId: e.objectId,
|
|
41
|
+
stateBefore: e.stateBefore ?? null,
|
|
42
|
+
stateAfter: e.stateAfter ?? null,
|
|
43
|
+
ipAddress: e.ipAddress ?? null,
|
|
44
|
+
userAgent: e.userAgent ?? null,
|
|
45
|
+
requestId: e.requestId ?? null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function moduleIdForAuditAction(action) {
|
|
49
|
+
if (action.startsWith('setting.'))
|
|
50
|
+
return 'settings';
|
|
51
|
+
if (action.startsWith('setting_group.'))
|
|
52
|
+
return 'settings';
|
|
53
|
+
if (action.startsWith('module.'))
|
|
54
|
+
return 'core';
|
|
55
|
+
if (action.startsWith('product.'))
|
|
56
|
+
return 'catalog';
|
|
57
|
+
if (action.startsWith('attribute_set.'))
|
|
58
|
+
return 'catalog';
|
|
59
|
+
if (action.startsWith('gallery.'))
|
|
60
|
+
return 'catalog';
|
|
61
|
+
if (action.startsWith('attachment.'))
|
|
62
|
+
return 'catalog';
|
|
63
|
+
if (action.startsWith('attachment_type.'))
|
|
64
|
+
return 'catalog';
|
|
65
|
+
if (action.startsWith('product_link.'))
|
|
66
|
+
return 'catalog';
|
|
67
|
+
if (action.startsWith('grouped_item.'))
|
|
68
|
+
return 'catalog';
|
|
69
|
+
if (action.startsWith('bundle_slot.'))
|
|
70
|
+
return 'catalog';
|
|
71
|
+
if (action.startsWith('bundle_slot_option.'))
|
|
72
|
+
return 'catalog';
|
|
73
|
+
if (action.startsWith('sales_channel.'))
|
|
74
|
+
return 'sales_channels';
|
|
75
|
+
if (action.startsWith('api_key.'))
|
|
76
|
+
return 'core';
|
|
77
|
+
if (action.startsWith('impersonation.'))
|
|
78
|
+
return 'core';
|
|
79
|
+
if (action.startsWith('order.'))
|
|
80
|
+
return 'core';
|
|
81
|
+
if (action.startsWith('organization.'))
|
|
82
|
+
return 'core';
|
|
83
|
+
return 'core';
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=routes.admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.js","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { CommandBus } from '@endora-commerce/platform/commands';
|
|
4
|
+
import type { RecentActivityCatalog } from './services/recent-activity-catalog.js';
|
|
5
|
+
import type { RecentActivityService } from './services/recent-activity-service.js';
|
|
6
|
+
import type { RecentActivityVisibility } from './services/recent-activity-visibility.js';
|
|
7
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
8
|
+
/**
|
|
9
|
+
* Feature 024 — `GET /api/v1/admin/audit-log/recent-activity`, and since
|
|
10
|
+
* feature 080's T042j the two routes behind the operator's control over it.
|
|
11
|
+
*
|
|
12
|
+
* **The response schema is derived** (D-163.1). `action` and `module` used to
|
|
13
|
+
* be `z.enum` over two host-written arrays — the second three members long,
|
|
14
|
+
* against a server that could emit four — so the contract and the server
|
|
15
|
+
* disagreed about `prompt_action.execute` for as long as feature 043 had
|
|
16
|
+
* shipped. Both enums are built from the composed modules' `recentActivity`
|
|
17
|
+
* declarations now, which is the same input the query's `$in` is built from, so
|
|
18
|
+
* they cannot disagree with it and a packaged module is in both.
|
|
19
|
+
*
|
|
20
|
+
* The **declaration** axis decides the schema and the **operator** axis decides
|
|
21
|
+
* the data, deliberately: a schema that narrowed to the currently-visible set
|
|
22
|
+
* would change shape when an operator flipped a switch, and a client holding
|
|
23
|
+
* the OpenAPI document would see a breaking change in a display preference.
|
|
24
|
+
*
|
|
25
|
+
* Permission: the same `audit_log:read` scope as the wider audit-log read
|
|
26
|
+
* endpoint for the card itself; `platform.modules.activate` for the visibility
|
|
27
|
+
* surface, which is the code that already governs this operator's per-module
|
|
28
|
+
* switches on the screen the control renders on.
|
|
29
|
+
*/
|
|
30
|
+
export interface RecentActivityAdminDeps {
|
|
31
|
+
recentActivityService: RecentActivityService;
|
|
32
|
+
recentActivityCatalog: RecentActivityCatalog;
|
|
33
|
+
recentActivityVisibility: RecentActivityVisibility;
|
|
34
|
+
requireAdmin: RequireAdminFactory;
|
|
35
|
+
/**
|
|
36
|
+
* Omitted in a composition with no Command Bus — the card still renders,
|
|
37
|
+
* because reading the dashboard must not depend on being able to reconfigure
|
|
38
|
+
* it. The same shape `_lifecycle`'s activation write uses.
|
|
39
|
+
*/
|
|
40
|
+
commandBus?: CommandBus;
|
|
41
|
+
}
|
|
42
|
+
export declare function buildRecentActivityResponseSchema(catalog: RecentActivityCatalog): z.ZodObject<{
|
|
43
|
+
data: z.ZodArray<z.ZodObject<{
|
|
44
|
+
id: z.ZodString;
|
|
45
|
+
actedAt: z.ZodString;
|
|
46
|
+
action: z.ZodType<string, unknown, z.core.$ZodTypeInternals<string, unknown>>;
|
|
47
|
+
module: z.ZodType<string, unknown, z.core.$ZodTypeInternals<string, unknown>>;
|
|
48
|
+
icon: z.ZodEnum<{
|
|
49
|
+
Plus: "Plus";
|
|
50
|
+
Sparkles: "Sparkles";
|
|
51
|
+
Settings: "Settings";
|
|
52
|
+
Search: "Search";
|
|
53
|
+
Boxes: "Boxes";
|
|
54
|
+
Layers: "Layers";
|
|
55
|
+
Menu: "Menu";
|
|
56
|
+
PlusCircle: "PlusCircle";
|
|
57
|
+
PlusSquare: "PlusSquare";
|
|
58
|
+
FilePlus: "FilePlus";
|
|
59
|
+
FolderPlus: "FolderPlus";
|
|
60
|
+
Upload: "Upload";
|
|
61
|
+
FileUp: "FileUp";
|
|
62
|
+
CloudUpload: "CloudUpload";
|
|
63
|
+
Download: "Download";
|
|
64
|
+
FileDown: "FileDown";
|
|
65
|
+
FileText: "FileText";
|
|
66
|
+
BookOpen: "BookOpen";
|
|
67
|
+
Rss: "Rss";
|
|
68
|
+
Package: "Package";
|
|
69
|
+
Tag: "Tag";
|
|
70
|
+
ShoppingCart: "ShoppingCart";
|
|
71
|
+
Receipt: "Receipt";
|
|
72
|
+
CreditCard: "CreditCard";
|
|
73
|
+
Users: "Users";
|
|
74
|
+
UserPlus: "UserPlus";
|
|
75
|
+
Inbox: "Inbox";
|
|
76
|
+
ListChecks: "ListChecks";
|
|
77
|
+
ClipboardList: "ClipboardList";
|
|
78
|
+
Image: "Image";
|
|
79
|
+
Video: "Video";
|
|
80
|
+
LayoutDashboard: "LayoutDashboard";
|
|
81
|
+
PanelLeft: "PanelLeft";
|
|
82
|
+
KeyRound: "KeyRound";
|
|
83
|
+
ShieldCheck: "ShieldCheck";
|
|
84
|
+
Edit: "Edit";
|
|
85
|
+
Archive: "Archive";
|
|
86
|
+
Box: "Box";
|
|
87
|
+
Truck: "Truck";
|
|
88
|
+
CircleDollarSign: "CircleDollarSign";
|
|
89
|
+
Activity: "Activity";
|
|
90
|
+
LineChart: "LineChart";
|
|
91
|
+
Smartphone: "Smartphone";
|
|
92
|
+
Webhook: "Webhook";
|
|
93
|
+
Scale: "Scale";
|
|
94
|
+
PlugZap: "PlugZap";
|
|
95
|
+
PercentDiamond: "PercentDiamond";
|
|
96
|
+
Newspaper: "Newspaper";
|
|
97
|
+
Languages: "Languages";
|
|
98
|
+
Eraser: "Eraser";
|
|
99
|
+
Warehouse: "Warehouse";
|
|
100
|
+
TrendingDown: "TrendingDown";
|
|
101
|
+
Bell: "Bell";
|
|
102
|
+
PackageOpen: "PackageOpen";
|
|
103
|
+
Building2: "Building2";
|
|
104
|
+
Store: "Store";
|
|
105
|
+
ClipboardCheck: "ClipboardCheck";
|
|
106
|
+
}>;
|
|
107
|
+
labelKey: z.ZodString;
|
|
108
|
+
actorDisplayName: z.ZodString;
|
|
109
|
+
actorKind: z.ZodEnum<{
|
|
110
|
+
admin: "admin";
|
|
111
|
+
system: "system";
|
|
112
|
+
}>;
|
|
113
|
+
targetType: z.ZodString;
|
|
114
|
+
targetId: z.ZodString;
|
|
115
|
+
targetDisplayName: z.ZodString;
|
|
116
|
+
targetUrl: z.ZodNullable<z.ZodString>;
|
|
117
|
+
summary: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
118
|
+
}, z.core.$strip>>;
|
|
119
|
+
pagination: z.ZodObject<{
|
|
120
|
+
limit: z.ZodNumber;
|
|
121
|
+
fetchedAt: z.ZodString;
|
|
122
|
+
}, z.core.$strip>;
|
|
123
|
+
}, z.core.$strip>;
|
|
124
|
+
export type RecentActivityRouteResponse = z.infer<ReturnType<typeof buildRecentActivityResponseSchema>>;
|
|
125
|
+
export declare function registerRecentActivityRoutes(app: FastifyInstance, deps: RecentActivityAdminDeps): Promise<void>;
|
|
126
|
+
//# sourceMappingURL=routes.admin.recent-activity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.recent-activity.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.recent-activity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AASxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAGrE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0CAA0C,CAAC;AACzF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,uBAAuB;IACtC,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C,wBAAwB,EAAE,wBAAwB,CAAC;IACnD,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;OAIG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAmBD,wBAAgB,iCAAiC,CAAC,OAAO,EAAE,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAwB/E;AAED,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAC/C,UAAU,CAAC,OAAO,iCAAiC,CAAC,CACrD,CAAC;AAEF,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,uBAAuB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAuFf"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { KnownIconNameSchema, RecentActivityVisibilityListSchema, RecentActivityVisibilityRequestSchema, RecentActivityVisibilityResponseSchema, } from '@endora-commerce/contracts';
|
|
3
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
4
|
+
import { makeSetRecentActivityVisibilityCommand } from './commands/recent-activity-visibility.commands.js';
|
|
5
|
+
const querySchema = z.object({
|
|
6
|
+
limit: z.coerce.number().int().min(1).max(12).optional(),
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* A closed enum where the composition declares something, a plain string where
|
|
10
|
+
* it declares nothing.
|
|
11
|
+
*
|
|
12
|
+
* `z.enum([])` is not a schema, and a bare-core build with every declaring
|
|
13
|
+
* module removed is a legitimate composition. The fallback describes the shape
|
|
14
|
+
* honestly rather than pretending to a closed set of nothing.
|
|
15
|
+
*/
|
|
16
|
+
function tokenSchema(values) {
|
|
17
|
+
if (values.length === 0)
|
|
18
|
+
return z.string().min(1);
|
|
19
|
+
return z.enum(values);
|
|
20
|
+
}
|
|
21
|
+
export function buildRecentActivityResponseSchema(catalog) {
|
|
22
|
+
const itemSchema = z.object({
|
|
23
|
+
id: z.string().uuid(),
|
|
24
|
+
actedAt: z.string(),
|
|
25
|
+
action: tokenSchema(catalog.actions()),
|
|
26
|
+
module: tokenSchema(catalog.moduleIds()),
|
|
27
|
+
icon: KnownIconNameSchema,
|
|
28
|
+
labelKey: z.string().min(1),
|
|
29
|
+
actorDisplayName: z.string().min(1),
|
|
30
|
+
actorKind: z.enum(['admin', 'system']),
|
|
31
|
+
targetType: z.string().min(1),
|
|
32
|
+
targetId: z.string().min(1),
|
|
33
|
+
targetDisplayName: z.string().min(1),
|
|
34
|
+
targetUrl: z.string().nullable(),
|
|
35
|
+
summary: z.record(z.string(), z.unknown()).nullable(),
|
|
36
|
+
});
|
|
37
|
+
return z.object({
|
|
38
|
+
data: z.array(itemSchema),
|
|
39
|
+
pagination: z.object({
|
|
40
|
+
limit: z.number().int().min(1).max(12),
|
|
41
|
+
fetchedAt: z.string(),
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export async function registerRecentActivityRoutes(app, deps) {
|
|
46
|
+
const { recentActivityService, recentActivityCatalog, recentActivityVisibility, requireAdmin, } = deps;
|
|
47
|
+
app.get('/api/v1/admin/audit-log/recent-activity', { preHandler: requireAdmin('audit_log:read') }, async (request, reply) => {
|
|
48
|
+
const parsed = querySchema.safeParse(request.query ?? {});
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
return reply.status(400).send({
|
|
51
|
+
error: {
|
|
52
|
+
code: 'validation_error',
|
|
53
|
+
message: 'Invalid query parameters',
|
|
54
|
+
details: parsed.error.flatten(),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const input = {};
|
|
59
|
+
if (parsed.data.limit !== undefined)
|
|
60
|
+
input.limit = parsed.data.limit;
|
|
61
|
+
return recentActivityService.list(input);
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* The operator axis of D-163.1, read.
|
|
65
|
+
*
|
|
66
|
+
* Gated on `platform.modules.activate` rather than on `audit_log:read`: this
|
|
67
|
+
* is a per-module operator switch, it renders beside the activation control
|
|
68
|
+
* on `/platform/modules`, and whoever may switch a module off may certainly
|
|
69
|
+
* decide whether its rows reach a dashboard. Declaring a second code for the
|
|
70
|
+
* same operator on the same screen would be a checkbox that grants nothing
|
|
71
|
+
* anybody would grant separately.
|
|
72
|
+
*/
|
|
73
|
+
app.get('/api/v1/admin/audit-log/recent-activity/visibility', {
|
|
74
|
+
preHandler: requireAdmin('platform.modules.activate'),
|
|
75
|
+
schema: { response: { 200: RecentActivityVisibilityListSchema } },
|
|
76
|
+
}, async () => ({
|
|
77
|
+
modules: await recentActivityVisibility.list(),
|
|
78
|
+
}));
|
|
79
|
+
const commandBus = deps.commandBus;
|
|
80
|
+
if (!commandBus)
|
|
81
|
+
return;
|
|
82
|
+
app.post('/api/v1/admin/audit-log/recent-activity/visibility/:moduleId', {
|
|
83
|
+
preHandler: requireAdmin('platform.modules.activate'),
|
|
84
|
+
schema: {
|
|
85
|
+
body: RecentActivityVisibilityRequestSchema,
|
|
86
|
+
response: { 200: RecentActivityVisibilityResponseSchema },
|
|
87
|
+
},
|
|
88
|
+
}, async (request) => {
|
|
89
|
+
const body = RecentActivityVisibilityRequestSchema.parse(request.body);
|
|
90
|
+
const { moduleId } = request.params;
|
|
91
|
+
const result = await commandBus.run(makeSetRecentActivityVisibilityCommand(recentActivityCatalog, {
|
|
92
|
+
moduleId,
|
|
93
|
+
visible: body.visible,
|
|
94
|
+
}));
|
|
95
|
+
// Re-read rather than echo, for the reason the activation control does
|
|
96
|
+
// not update optimistically: what the operator sees is what the platform
|
|
97
|
+
// resolved, not what this handler assumed.
|
|
98
|
+
const row = (await recentActivityVisibility.list()).find((entry) => entry.moduleId === result.moduleId);
|
|
99
|
+
if (!row) {
|
|
100
|
+
throw new HttpError(500, 'INTERNAL', `Module "${result.moduleId}" vanished from the recent-activity catalog after its ` +
|
|
101
|
+
`dashboard visibility was written.`);
|
|
102
|
+
}
|
|
103
|
+
return { module: row };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=routes.admin.recent-activity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.recent-activity.js","sourceRoot":"","sources":["../../src/backend/routes.admin.recent-activity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AuditReferenceLabel, AuditReferenceRegistryPort, AuditReferenceResolver } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* In-process registry of "what is this audit row about, and where does it live?"
|
|
4
|
+
* resolvers (feature 075, D-87 drain).
|
|
5
|
+
*
|
|
6
|
+
* Registered under `auditReferenceRegistry` and contributed to from other
|
|
7
|
+
* modules' boot hooks. `RecentActivityService` is the only reader today.
|
|
8
|
+
*
|
|
9
|
+
* **Enumeration policy: an absent contributor's resolver is skipped.** The
|
|
10
|
+
* reasoning is at `AuditReferenceRegistryPort` in `@endora-commerce/contracts`, and the
|
|
11
|
+
* short of it is that the descriptor carries a deep link into the contributor's
|
|
12
|
+
* own admin screen: a module that is not present contributes no screen, so
|
|
13
|
+
* nothing may link to one. The audit row still renders — with the label the
|
|
14
|
+
* entry's own snapshot carries, or the raw id, and no link.
|
|
15
|
+
*
|
|
16
|
+
* The predicate is injected rather than read from `effectiveState` here, because
|
|
17
|
+
* a registry that answers a presence question is exactly the thing a unit test
|
|
18
|
+
* has to be able to put in either state.
|
|
19
|
+
*/
|
|
20
|
+
export declare class AuditReferenceRegistry implements AuditReferenceRegistryPort {
|
|
21
|
+
private readonly isModulePresent;
|
|
22
|
+
private readonly resolvers;
|
|
23
|
+
constructor(isModulePresent: (moduleId: string) => boolean);
|
|
24
|
+
register(resolver: AuditReferenceResolver): void;
|
|
25
|
+
owners(): readonly string[];
|
|
26
|
+
resolve(referenceType: string, ids: readonly string[]): Promise<ReadonlyMap<string, AuditReferenceLabel>>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=audit-reference-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-reference-registry.d.ts","sourceRoot":"","sources":["../../../src/backend/services/audit-reference-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,0BAA0B,EAC1B,sBAAsB,EACvB,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,sBAAuB,YAAW,0BAA0B;IAG3D,OAAO,CAAC,QAAQ,CAAC,eAAe;IAF5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;gBAE7B,eAAe,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO;IAE3E,QAAQ,CAAC,QAAQ,EAAE,sBAAsB,GAAG,IAAI;IAehD,MAAM,IAAI,SAAS,MAAM,EAAE;IAIrB,OAAO,CACX,aAAa,EAAE,MAAM,EACrB,GAAG,EAAE,SAAS,MAAM,EAAE,GACrB,OAAO,CAAC,WAAW,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAcrD"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process registry of "what is this audit row about, and where does it live?"
|
|
3
|
+
* resolvers (feature 075, D-87 drain).
|
|
4
|
+
*
|
|
5
|
+
* Registered under `auditReferenceRegistry` and contributed to from other
|
|
6
|
+
* modules' boot hooks. `RecentActivityService` is the only reader today.
|
|
7
|
+
*
|
|
8
|
+
* **Enumeration policy: an absent contributor's resolver is skipped.** The
|
|
9
|
+
* reasoning is at `AuditReferenceRegistryPort` in `@endora-commerce/contracts`, and the
|
|
10
|
+
* short of it is that the descriptor carries a deep link into the contributor's
|
|
11
|
+
* own admin screen: a module that is not present contributes no screen, so
|
|
12
|
+
* nothing may link to one. The audit row still renders — with the label the
|
|
13
|
+
* entry's own snapshot carries, or the raw id, and no link.
|
|
14
|
+
*
|
|
15
|
+
* The predicate is injected rather than read from `effectiveState` here, because
|
|
16
|
+
* a registry that answers a presence question is exactly the thing a unit test
|
|
17
|
+
* has to be able to put in either state.
|
|
18
|
+
*/
|
|
19
|
+
export class AuditReferenceRegistry {
|
|
20
|
+
isModulePresent;
|
|
21
|
+
resolvers = [];
|
|
22
|
+
constructor(isModulePresent) {
|
|
23
|
+
this.isModulePresent = isModulePresent;
|
|
24
|
+
}
|
|
25
|
+
register(resolver) {
|
|
26
|
+
if (this.resolvers.includes(resolver))
|
|
27
|
+
return;
|
|
28
|
+
const claimed = this.resolvers.find((r) => r.referenceType === resolver.referenceType);
|
|
29
|
+
if (claimed) {
|
|
30
|
+
// Two answers for one type is not a degrade to pick a winner from: one of
|
|
31
|
+
// the two modules is wrong about what it owns, and the reader would be
|
|
32
|
+
// rendering whichever boot hook happened to run first.
|
|
33
|
+
throw new Error(`Audit reference type '${resolver.referenceType}' is already resolved by ` +
|
|
34
|
+
`'${claimed.ownerModuleId}'; '${resolver.ownerModuleId}' may not claim it too.`);
|
|
35
|
+
}
|
|
36
|
+
this.resolvers.push(resolver);
|
|
37
|
+
}
|
|
38
|
+
owners() {
|
|
39
|
+
return this.resolvers.map((r) => r.ownerModuleId);
|
|
40
|
+
}
|
|
41
|
+
async resolve(referenceType, ids) {
|
|
42
|
+
const out = new Map();
|
|
43
|
+
if (ids.length === 0)
|
|
44
|
+
return out;
|
|
45
|
+
const resolver = this.resolvers.find((r) => r.referenceType === referenceType);
|
|
46
|
+
// Enumeration policy, applied at the read rather than at registration: an
|
|
47
|
+
// operator's flip has to take effect without a restart.
|
|
48
|
+
if (!resolver || !this.isModulePresent(resolver.ownerModuleId))
|
|
49
|
+
return out;
|
|
50
|
+
for (const row of await resolver.resolve(ids)) {
|
|
51
|
+
if (row.label.length > 0)
|
|
52
|
+
out.set(row.id, row);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=audit-reference-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-reference-registry.js","sourceRoot":"","sources":["../../../src/backend/services/audit-reference-registry.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,sBAAsB;IAGJ;IAFZ,SAAS,GAA6B,EAAE,CAAC;IAE1D,YAA6B,eAA8C;QAA9C,oBAAe,GAAf,eAAe,CAA+B;IAAG,CAAC;IAE/E,QAAQ,CAAC,QAAgC;QACvC,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO;QAC9C,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,KAAK,QAAQ,CAAC,aAAa,CAAC,CAAC;QACvF,IAAI,OAAO,EAAE,CAAC;YACZ,0EAA0E;YAC1E,uEAAuE;YACvE,uDAAuD;YACvD,MAAM,IAAI,KAAK,CACb,yBAAyB,QAAQ,CAAC,aAAa,2BAA2B;gBACxE,IAAI,OAAO,CAAC,aAAa,OAAO,QAAQ,CAAC,aAAa,yBAAyB,CAClF,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAED,MAAM;QACJ,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,OAAO,CACX,aAAqB,EACrB,GAAsB;QAEtB,MAAM,GAAG,GAAG,IAAI,GAAG,EAA+B,CAAC;QACnD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,GAAG,CAAC;QAEjC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,KAAK,aAAa,CAAC,CAAC;QAC/E,0EAA0E;QAC1E,wDAAwD;QACxD,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,aAAa,CAAC;YAAE,OAAO,GAAG,CAAC;QAE3E,KAAK,MAAM,GAAG,IAAI,MAAM,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9C,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;CACF"}
|