@endora-commerce/mod-admin-actions 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 +51 -0
- package/dist/backend/entities/module-action.entity.d.ts +30 -0
- package/dist/backend/entities/module-action.entity.d.ts.map +1 -0
- package/dist/backend/entities/module-action.entity.js +100 -0
- package/dist/backend/entities/module-action.entity.js.map +1 -0
- package/dist/backend/index.d.ts +85 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +57 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +110 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +77 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +19 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +32 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/services/admin-actions-reconciler.d.ts +49 -0
- package/dist/backend/services/admin-actions-reconciler.d.ts.map +1 -0
- package/dist/backend/services/admin-actions-reconciler.js +78 -0
- package/dist/backend/services/admin-actions-reconciler.js.map +1 -0
- package/dist/backend/services/admin-actions-service.d.ts +136 -0
- package/dist/backend/services/admin-actions-service.d.ts.map +1 -0
- package/dist/backend/services/admin-actions-service.js +168 -0
- package/dist/backend/services/admin-actions-service.js.map +1 -0
- package/dist/manifest.d.ts +196 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +85 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260507T142354_admin_actions_init.d.ts +31 -0
- package/dist/migrations/20260507T142354_admin_actions_init.d.ts.map +1 -0
- package/dist/migrations/20260507T142354_admin_actions_init.js +55 -0
- package/dist/migrations/20260507T142354_admin_actions_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/admin-actions.md +164 -0
- package/i18n/en.json +5 -0
- package/i18n/pl.json +5 -0
- package/package.json +68 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { ModuleAction as ModuleActionDecl } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* Admin Actions Reconciler — feature 020.
|
|
5
|
+
*
|
|
6
|
+
* Idempotent UPSERT-and-prune of `module_actions` rows from a module's
|
|
7
|
+
* declared `manifest.actions` array. Mirrors feature 019's i18n-service
|
|
8
|
+
* UPSERT pattern: a single SQL statement with `INSERT … ON CONFLICT
|
|
9
|
+
* (module_id, action_id) DO UPDATE` so every successful install
|
|
10
|
+
* advances the per-row `version` sequence whether the row existed or
|
|
11
|
+
* not. After UPSERT, any rows owned by `moduleId` whose `action_id` is
|
|
12
|
+
* no longer in the new declaration are deleted (prune).
|
|
13
|
+
*
|
|
14
|
+
* Wraps work in the caller's transaction (the lifecycle orchestrator
|
|
15
|
+
* supplies its own `EntityManager`); idempotent — re-running install
|
|
16
|
+
* with the same payload is a no-op aside from bumping `version`.
|
|
17
|
+
*/
|
|
18
|
+
export interface AdminActionsReconcilerDeps {
|
|
19
|
+
em: () => EntityManager;
|
|
20
|
+
}
|
|
21
|
+
export declare class AdminActionsReconciler {
|
|
22
|
+
private readonly deps;
|
|
23
|
+
constructor(deps: AdminActionsReconcilerDeps);
|
|
24
|
+
/**
|
|
25
|
+
* Install (or update) every declared action for `moduleId`. After
|
|
26
|
+
* inserting/updating the declared rows, prune any rows for the same
|
|
27
|
+
* module whose action_id is not present in the new declaration.
|
|
28
|
+
*/
|
|
29
|
+
installForModule(args: {
|
|
30
|
+
moduleId: string;
|
|
31
|
+
actions: readonly ModuleActionDecl[];
|
|
32
|
+
em?: EntityManager;
|
|
33
|
+
}): Promise<{
|
|
34
|
+
upserted: number;
|
|
35
|
+
pruned: number;
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* Hard-uninstall — drop every action row for the module. Soft-
|
|
39
|
+
* uninstall (state → 'disabled') intentionally leaves rows in place;
|
|
40
|
+
* visibility is gated by the registry-state join at query time.
|
|
41
|
+
*/
|
|
42
|
+
removeForModule(args: {
|
|
43
|
+
moduleId: string;
|
|
44
|
+
em?: EntityManager;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
removed: number;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=admin-actions-reconciler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-actions-reconciler.d.ts","sourceRoot":"","sources":["../../../src/backend/services/admin-actions-reconciler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,IAAI,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAGnF;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,0BAA0B;IACzC,EAAE,EAAE,MAAM,aAAa,CAAC;CACzB;AAED,qBAAa,sBAAsB;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,0BAA0B;IAE7D;;;;OAIG;IACG,gBAAgB,CAAC,IAAI,EAAE;QAC3B,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;QACrC,EAAE,CAAC,EAAE,aAAa,CAAC;KACpB,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IA2DjD;;;;OAIG;IACG,eAAe,CAAC,IAAI,EAAE;QAC1B,QAAQ,EAAE,MAAM,CAAC;QACjB,EAAE,CAAC,EAAE,aAAa,CAAC;KACpB,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAOjC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ModuleAction } from '../entities/module-action.entity.js';
|
|
2
|
+
export class AdminActionsReconciler {
|
|
3
|
+
deps;
|
|
4
|
+
constructor(deps) {
|
|
5
|
+
this.deps = deps;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Install (or update) every declared action for `moduleId`. After
|
|
9
|
+
* inserting/updating the declared rows, prune any rows for the same
|
|
10
|
+
* module whose action_id is not present in the new declaration.
|
|
11
|
+
*/
|
|
12
|
+
async installForModule(args) {
|
|
13
|
+
// command-coverage-ignore: idempotent lifecycle reconciler/seed — a system-
|
|
14
|
+
// invariant repair, not an operator-initiated audited write.
|
|
15
|
+
const targetEm = args.em ?? this.deps.em();
|
|
16
|
+
const declaredIds = new Set();
|
|
17
|
+
for (const action of args.actions) {
|
|
18
|
+
declaredIds.add(action.id);
|
|
19
|
+
// `targetEm.execute`, not `targetEm.getKnex().raw` — the knex instance is
|
|
20
|
+
// connection-level and carries no transaction context, so the UPSERT
|
|
21
|
+
// committed on its own connection while the prune below it (an
|
|
22
|
+
// `em.nativeDelete`) stayed inside the caller's transaction. One method,
|
|
23
|
+
// two answers to a rollback (issue #200).
|
|
24
|
+
await targetEm.execute(`insert into "module_actions" (` +
|
|
25
|
+
`"module_id", "action_id", "label_key", "description_key", "icon", ` +
|
|
26
|
+
`"target_route", "required_permission", "keywords", "weight", ` +
|
|
27
|
+
`"installed_at", "updated_at"` +
|
|
28
|
+
`) values (?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?, now(), now()) ` +
|
|
29
|
+
`on conflict ("module_id", "action_id") do update set ` +
|
|
30
|
+
`"label_key" = excluded."label_key", ` +
|
|
31
|
+
`"description_key" = excluded."description_key", ` +
|
|
32
|
+
`"icon" = excluded."icon", ` +
|
|
33
|
+
`"target_route" = excluded."target_route", ` +
|
|
34
|
+
`"required_permission" = excluded."required_permission", ` +
|
|
35
|
+
`"keywords" = excluded."keywords", ` +
|
|
36
|
+
`"weight" = excluded."weight", ` +
|
|
37
|
+
`"version" = nextval('module_actions_version_seq'), ` +
|
|
38
|
+
`"updated_at" = now()`, [
|
|
39
|
+
args.moduleId,
|
|
40
|
+
action.id,
|
|
41
|
+
action.labelKey,
|
|
42
|
+
action.descriptionKey ?? null,
|
|
43
|
+
action.icon,
|
|
44
|
+
action.targetRoute,
|
|
45
|
+
action.requiredPermission ?? null,
|
|
46
|
+
JSON.stringify(action.keywords ?? []),
|
|
47
|
+
action.weight ?? 100,
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
// Prune: delete any rows for this module whose action_id is not in
|
|
51
|
+
// the newly-declared set. The empty-set case is safe — the
|
|
52
|
+
// whereNotIn(...) becomes a delete-all-for-module.
|
|
53
|
+
let pruned = 0;
|
|
54
|
+
if (declaredIds.size > 0) {
|
|
55
|
+
pruned = await targetEm.nativeDelete(ModuleAction, {
|
|
56
|
+
moduleId: args.moduleId,
|
|
57
|
+
actionId: { $nin: [...declaredIds] },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
pruned = await targetEm.nativeDelete(ModuleAction, { moduleId: args.moduleId });
|
|
62
|
+
}
|
|
63
|
+
return { upserted: args.actions.length, pruned };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Hard-uninstall — drop every action row for the module. Soft-
|
|
67
|
+
* uninstall (state → 'disabled') intentionally leaves rows in place;
|
|
68
|
+
* visibility is gated by the registry-state join at query time.
|
|
69
|
+
*/
|
|
70
|
+
async removeForModule(args) {
|
|
71
|
+
// command-coverage-ignore: idempotent lifecycle reconciler/seed — a system-
|
|
72
|
+
// invariant repair, not an operator-initiated audited write.
|
|
73
|
+
const targetEm = args.em ?? this.deps.em();
|
|
74
|
+
const removed = await targetEm.nativeDelete(ModuleAction, { moduleId: args.moduleId });
|
|
75
|
+
return { removed };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=admin-actions-reconciler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-actions-reconciler.js","sourceRoot":"","sources":["../../../src/backend/services/admin-actions-reconciler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AAqBnE,MAAM,OAAO,sBAAsB;IACJ;IAA7B,YAA6B,IAAgC;QAAhC,SAAI,GAAJ,IAAI,CAA4B;IAAG,CAAC;IAEjE;;;;OAIG;IACH,KAAK,CAAC,gBAAgB,CAAC,IAItB;QACC,4EAA4E;QAC5E,6DAA6D;QAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;QAEtC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC3B,0EAA0E;YAC1E,qEAAqE;YACrE,+DAA+D;YAC/D,yEAAyE;YACzE,0CAA0C;YAC1C,MAAM,QAAQ,CAAC,OAAO,CACpB,gCAAgC;gBAC9B,oEAAoE;gBACpE,+DAA+D;gBAC/D,8BAA8B;gBAC9B,4DAA4D;gBAC5D,uDAAuD;gBACvD,sCAAsC;gBACtC,kDAAkD;gBAClD,4BAA4B;gBAC5B,4CAA4C;gBAC5C,0DAA0D;gBAC1D,oCAAoC;gBACpC,gCAAgC;gBAChC,qDAAqD;gBACrD,sBAAsB,EACxB;gBACE,IAAI,CAAC,QAAQ;gBACb,MAAM,CAAC,EAAE;gBACT,MAAM,CAAC,QAAQ;gBACf,MAAM,CAAC,cAAc,IAAI,IAAI;gBAC7B,MAAM,CAAC,IAAI;gBACX,MAAM,CAAC,WAAW;gBAClB,MAAM,CAAC,kBAAkB,IAAI,IAAI;gBACjC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC;gBACrC,MAAM,CAAC,MAAM,IAAI,GAAG;aACrB,CACF,CAAC;QACJ,CAAC;QAED,mEAAmE;QACnE,2DAA2D;QAC3D,mDAAmD;QACnD,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,IAAI,WAAW,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,YAAY,EAAE;gBACjD,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,WAAW,CAAC,EAAE;aACrC,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QAClF,CAAC;QAED,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IACnD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,eAAe,CAAC,IAGrB;QACC,4EAA4E;QAC5E,6DAA6D;QAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QACvF,OAAO,EAAE,OAAO,EAAE,CAAC;IACrB,CAAC;CACF"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import type { AdminActionView, AdminI18nTranslatePort, PermissionReadPort, SupportedAdminLanguage } from '@endora-commerce/contracts';
|
|
3
|
+
export interface AdminActionsServiceDeps {
|
|
4
|
+
em: () => EntityManager;
|
|
5
|
+
/**
|
|
6
|
+
* `_i18n`'s resolver and `admin_roles`' permission read, both as the contract
|
|
7
|
+
* types those modules publish rather than their service classes (feature 075,
|
|
8
|
+
* Phase C). What this service asks for is two methods — resolve a string,
|
|
9
|
+
* list an operator's permissions — where the classes brought bundle
|
|
10
|
+
* installation, coverage snapshots and role administration across the
|
|
11
|
+
* boundary with them.
|
|
12
|
+
*/
|
|
13
|
+
i18nService: AdminI18nTranslatePort;
|
|
14
|
+
permissionService: PermissionReadPort;
|
|
15
|
+
log?: {
|
|
16
|
+
info(msg: string): void;
|
|
17
|
+
warn(msg: string): void;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Feature 073 / issue #225 / issue #187 — **both** presence axes and the
|
|
21
|
+
* generation the readings came from, injected as one value.
|
|
22
|
+
*
|
|
23
|
+
* The platform axis joined this module's rows to `module_registrations` until
|
|
24
|
+
* feature 080's ledger sweep. It reads through the probe now, from the same
|
|
25
|
+
* `effectiveState` projection the operator axis has always come from, so the
|
|
26
|
+
* palette cannot disagree with the route gates about a module it advertises.
|
|
27
|
+
*
|
|
28
|
+
* Injected rather than read from the lifecycle singleton so this service
|
|
29
|
+
* stays constructible without a warmed process cache — a test that drives
|
|
30
|
+
* presence must be able to hand over the presence it is driving rather than
|
|
31
|
+
* reach for a process-wide seam.
|
|
32
|
+
*
|
|
33
|
+
* The readings and the version travel together, in one object, because a
|
|
34
|
+
* composition that supplied the first without the second would cache an
|
|
35
|
+
* answer it can never drop — and there are two composition roots to keep in
|
|
36
|
+
* step. Omitted ⇒ every module reads as present and the version never moves,
|
|
37
|
+
* which is the behaviour of an unwired composition, not a fall-open: there is
|
|
38
|
+
* no presence state to resolve at all.
|
|
39
|
+
*/
|
|
40
|
+
presence?: ModulePresenceProbe;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Module presence, as this service consumes it: both axes and the generation
|
|
44
|
+
* the two readings came from.
|
|
45
|
+
*
|
|
46
|
+
* Both composition roots build it off `effectiveState`, which is where all
|
|
47
|
+
* three are already one object; stating them together here is what stops a
|
|
48
|
+
* root from wiring one reading without the others.
|
|
49
|
+
*
|
|
50
|
+
* The two axes stay **separate methods** rather than collapsing into
|
|
51
|
+
* `effectiveState.isPresent`, and the reason is the tri-state the combiner
|
|
52
|
+
* itself keeps: a module the deployment does not ship and a module the operator
|
|
53
|
+
* switched off are one answer to the palette and two different sentences on
|
|
54
|
+
* `/platform/modules`, so a probe that could only say "absent" would be a
|
|
55
|
+
* narrower value than the object both roots already hold. The conjunction is
|
|
56
|
+
* applied here, at the row filter, and nowhere else.
|
|
57
|
+
*/
|
|
58
|
+
export interface ModulePresenceProbe {
|
|
59
|
+
/**
|
|
60
|
+
* Is this module installed and enabled on this deployment?
|
|
61
|
+
*
|
|
62
|
+
* The composition roots answer it `false` for a module id the registry and
|
|
63
|
+
* the manifests both know nothing about, which is what keeps an orphan
|
|
64
|
+
* `module_actions` row — a module uninstalled without its rows being reaped —
|
|
65
|
+
* out of the palette. That used to fall out of the `module_registrations`
|
|
66
|
+
* join having no row to match.
|
|
67
|
+
*/
|
|
68
|
+
isPlatformAvailable(moduleId: string): boolean;
|
|
69
|
+
/** Is this module activated by the operator? */
|
|
70
|
+
isActivated(moduleId: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Generation of the presence the two readings above answer from. Changes when
|
|
73
|
+
* a refresh installs presence that differs from the presence before it.
|
|
74
|
+
*/
|
|
75
|
+
version(): number;
|
|
76
|
+
}
|
|
77
|
+
export interface ListVisibleResult {
|
|
78
|
+
actions: AdminActionView[];
|
|
79
|
+
registryVersion: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Namespace this module's palette snapshot is registered under.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately **not** in `CACHE_NAMESPACES` (the `settings` module's operator
|
|
85
|
+
* "clear cache" surface): this layer owns no Redis keys, and the palette is
|
|
86
|
+
* rebuilt from PostgreSQL on the next read anyway. The registration exists for
|
|
87
|
+
* the module-state notification alone.
|
|
88
|
+
*/
|
|
89
|
+
export declare const ADMIN_ACTIONS_CACHE_NAMESPACE = "admin_actions";
|
|
90
|
+
export declare class AdminActionsService {
|
|
91
|
+
private readonly em;
|
|
92
|
+
private readonly i18nService;
|
|
93
|
+
private readonly permissionService;
|
|
94
|
+
private readonly presence;
|
|
95
|
+
private readonly cache;
|
|
96
|
+
/** Presence generation every snapshot currently in {@link cache} was built under. */
|
|
97
|
+
private cachedPresenceVersion;
|
|
98
|
+
/** Counts service work so tests can assert cache hits / misses. */
|
|
99
|
+
stats: {
|
|
100
|
+
dbHits: number;
|
|
101
|
+
cacheHits: number;
|
|
102
|
+
invalidations: number;
|
|
103
|
+
};
|
|
104
|
+
private readonly unregisterCacheLayer;
|
|
105
|
+
constructor(deps: AdminActionsServiceDeps);
|
|
106
|
+
/**
|
|
107
|
+
* Withdraw this service's cache layer from the process registry. Production
|
|
108
|
+
* holds it until process exit; a test that builds a service per case releases
|
|
109
|
+
* it so the registry names a live cache rather than a discarded one.
|
|
110
|
+
*/
|
|
111
|
+
dispose(): void;
|
|
112
|
+
/**
|
|
113
|
+
* Drop every cached snapshot. Called by the registered cache layer above and
|
|
114
|
+
* by the presence-version comparison in {@link listVisibleForOperator}.
|
|
115
|
+
*/
|
|
116
|
+
invalidate(): void;
|
|
117
|
+
/**
|
|
118
|
+
* List the actions an operator may see in the command palette.
|
|
119
|
+
*
|
|
120
|
+
* Permissions are loaded via the admin_roles `PermissionService`. The
|
|
121
|
+
* action filter is the same as the existing `hasPermission()` check:
|
|
122
|
+
* `*` in the operator's role grants everything; otherwise the action's
|
|
123
|
+
* `requiredPermission` (if any) must be present in the role's array.
|
|
124
|
+
*/
|
|
125
|
+
listVisibleForOperator(args: {
|
|
126
|
+
language: SupportedAdminLanguage;
|
|
127
|
+
adminUserId: string;
|
|
128
|
+
}): Promise<ListVisibleResult>;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Stable hash of an operator's permission set. Sorting first makes the
|
|
132
|
+
* fingerprint independent of insertion order; sha1 keeps the cache key
|
|
133
|
+
* compact while making collisions astronomically unlikely.
|
|
134
|
+
*/
|
|
135
|
+
export declare function fingerprintPermissions(permissions: readonly string[]): string;
|
|
136
|
+
//# sourceMappingURL=admin-actions-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-actions-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/admin-actions-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,eAAe,EACf,sBAAsB,EAEtB,kBAAkB,EAClB,sBAAsB,EACvB,MAAM,4BAA4B,CAAC;AAuEpC,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,aAAa,CAAC;IACxB;;;;;;;OAOG;IACH,WAAW,EAAE,sBAAsB,CAAC;IACpC,iBAAiB,EAAE,kBAAkB,CAAC;IACtC,GAAG,CAAC,EAAE;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC3D;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,EAAE,mBAAmB,CAAC;CAChC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;OAQG;IACH,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/C,gDAAgD;IAChD,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC;;;OAGG;IACH,OAAO,IAAI,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,kBAAkB,CAAC;AAE7D,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAsB;IACzC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyB;IACrD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqB;IACvD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwC;IAC9D,qFAAqF;IACrF,OAAO,CAAC,qBAAqB,CAAS;IACtC,mEAAmE;IAC5D,KAAK;;;;MAAiD;IAC7D,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAa;gBAEtC,IAAI,EAAE,uBAAuB;IA8BzC;;;;OAIG;IACH,OAAO,IAAI,IAAI;IAIf;;;OAGG;IACH,UAAU,IAAI,IAAI;IAKlB;;;;;;;OAOG;IACG,sBAAsB,CAAC,IAAI,EAAE;QACjC,QAAQ,EAAE,sBAAsB,CAAC;QACjC,WAAW,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CA+G/B;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAG7E"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { inProcessCaches } from '@endora-commerce/platform/kernel';
|
|
3
|
+
/**
|
|
4
|
+
* Namespace this module's palette snapshot is registered under.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately **not** in `CACHE_NAMESPACES` (the `settings` module's operator
|
|
7
|
+
* "clear cache" surface): this layer owns no Redis keys, and the palette is
|
|
8
|
+
* rebuilt from PostgreSQL on the next read anyway. The registration exists for
|
|
9
|
+
* the module-state notification alone.
|
|
10
|
+
*/
|
|
11
|
+
export const ADMIN_ACTIONS_CACHE_NAMESPACE = 'admin_actions';
|
|
12
|
+
export class AdminActionsService {
|
|
13
|
+
em;
|
|
14
|
+
i18nService;
|
|
15
|
+
permissionService;
|
|
16
|
+
presence;
|
|
17
|
+
cache = new Map();
|
|
18
|
+
/** Presence generation every snapshot currently in {@link cache} was built under. */
|
|
19
|
+
cachedPresenceVersion;
|
|
20
|
+
/** Counts service work so tests can assert cache hits / misses. */
|
|
21
|
+
stats = { dbHits: 0, cacheHits: 0, invalidations: 0 };
|
|
22
|
+
unregisterCacheLayer;
|
|
23
|
+
constructor(deps) {
|
|
24
|
+
this.em = deps.em;
|
|
25
|
+
this.i18nService = deps.i18nService;
|
|
26
|
+
this.permissionService = deps.permissionService;
|
|
27
|
+
this.presence = deps.presence ?? {
|
|
28
|
+
isPlatformAvailable: () => true,
|
|
29
|
+
isActivated: () => true,
|
|
30
|
+
version: () => 0,
|
|
31
|
+
};
|
|
32
|
+
this.cachedPresenceVersion = this.presence.version();
|
|
33
|
+
// D-174 — this process's palette snapshot, announced to the platform so its
|
|
34
|
+
// module-state subscriber can drop it. Unconditional: the registry is
|
|
35
|
+
// process-local and a drop costs a `Map.clear()`, so there is nothing for a
|
|
36
|
+
// composition to get wrong by omission. A second composition in the same
|
|
37
|
+
// process (the harness builds one per test file) replaces the entry.
|
|
38
|
+
this.unregisterCacheLayer = inProcessCaches.register(ADMIN_ACTIONS_CACHE_NAMESPACE, {
|
|
39
|
+
invalidateAll: async () => {
|
|
40
|
+
this.invalidate();
|
|
41
|
+
// No shared layer: this cache is a `Map`, so no Redis key was deleted
|
|
42
|
+
// and the operator-facing count this return value feeds is zero.
|
|
43
|
+
return 0;
|
|
44
|
+
},
|
|
45
|
+
}, { invalidateOnModuleStateChange: true });
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Withdraw this service's cache layer from the process registry. Production
|
|
49
|
+
* holds it until process exit; a test that builds a service per case releases
|
|
50
|
+
* it so the registry names a live cache rather than a discarded one.
|
|
51
|
+
*/
|
|
52
|
+
dispose() {
|
|
53
|
+
this.unregisterCacheLayer();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Drop every cached snapshot. Called by the registered cache layer above and
|
|
57
|
+
* by the presence-version comparison in {@link listVisibleForOperator}.
|
|
58
|
+
*/
|
|
59
|
+
invalidate() {
|
|
60
|
+
this.cache.clear();
|
|
61
|
+
this.stats.invalidations += 1;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* List the actions an operator may see in the command palette.
|
|
65
|
+
*
|
|
66
|
+
* Permissions are loaded via the admin_roles `PermissionService`. The
|
|
67
|
+
* action filter is the same as the existing `hasPermission()` check:
|
|
68
|
+
* `*` in the operator's role grants everything; otherwise the action's
|
|
69
|
+
* `requiredPermission` (if any) must be present in the role's array.
|
|
70
|
+
*/
|
|
71
|
+
async listVisibleForOperator(args) {
|
|
72
|
+
// Issue #225 — before anything is served from the snapshot, ask whether the
|
|
73
|
+
// presence it was built from is still the presence being answered. A read
|
|
74
|
+
// that landed while `registryCache.refreshFromDb` was in flight built its
|
|
75
|
+
// snapshot from the pre-refresh maps; the version moves when that refresh
|
|
76
|
+
// lands, and this is where the snapshot goes with it. The number is recorded
|
|
77
|
+
// *before* the rebuild below, so a refresh completing while that rebuild is
|
|
78
|
+
// running is caught by the next read rather than becoming permanent.
|
|
79
|
+
const presenceVersion = this.presence.version();
|
|
80
|
+
if (presenceVersion !== this.cachedPresenceVersion) {
|
|
81
|
+
this.cachedPresenceVersion = presenceVersion;
|
|
82
|
+
this.invalidate();
|
|
83
|
+
}
|
|
84
|
+
const permissions = await this.permissionService.listPermissions(args.adminUserId);
|
|
85
|
+
const fingerprint = fingerprintPermissions(permissions);
|
|
86
|
+
const cacheKey = `${args.language}::${fingerprint}`;
|
|
87
|
+
const cached = this.cache.get(cacheKey);
|
|
88
|
+
if (cached) {
|
|
89
|
+
this.stats.cacheHits += 1;
|
|
90
|
+
return cached;
|
|
91
|
+
}
|
|
92
|
+
this.stats.dbHits += 1;
|
|
93
|
+
const knex = this.em().getKnex();
|
|
94
|
+
// This module's own table and nothing else. Presence is decided below,
|
|
95
|
+
// against the probe, for both axes at once.
|
|
96
|
+
const rows = (await knex('module_actions as a')
|
|
97
|
+
.select('a.module_id', 'a.action_id', 'a.label_key', 'a.description_key', 'a.icon', 'a.target_route', 'a.required_permission', 'a.keywords', 'a.weight', 'a.version'));
|
|
98
|
+
const visible = [];
|
|
99
|
+
for (const row of rows) {
|
|
100
|
+
// Constitution XVII — effective presence is the conjunction of the two
|
|
101
|
+
// axes, and both are asked here so neither can be forgotten by one of
|
|
102
|
+
// them moving. A palette that advertises an absent module's action leads
|
|
103
|
+
// an operator straight to a 503, which is the same defect as an
|
|
104
|
+
// unfiltered sidebar; one that hides a present module's action is the
|
|
105
|
+
// same defect wearing the other sign.
|
|
106
|
+
if (!this.presence.isPlatformAvailable(row.module_id))
|
|
107
|
+
continue;
|
|
108
|
+
if (!this.presence.isActivated(row.module_id))
|
|
109
|
+
continue;
|
|
110
|
+
if (row.required_permission == null ||
|
|
111
|
+
permissions.includes('*') ||
|
|
112
|
+
permissions.includes(row.required_permission)) {
|
|
113
|
+
visible.push(row);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
let registryVersion = 0;
|
|
117
|
+
const collator = new Intl.Collator(args.language, {
|
|
118
|
+
sensitivity: 'base',
|
|
119
|
+
numeric: true,
|
|
120
|
+
});
|
|
121
|
+
const resolved = [];
|
|
122
|
+
for (const row of visible) {
|
|
123
|
+
const v = Number(row.version);
|
|
124
|
+
if (v > registryVersion)
|
|
125
|
+
registryVersion = v;
|
|
126
|
+
const label = await this.i18nService.translate(row.module_id, row.label_key, args.language);
|
|
127
|
+
let description = null;
|
|
128
|
+
if (row.description_key) {
|
|
129
|
+
const resolvedDescription = await this.i18nService.translate(row.module_id, row.description_key, args.language);
|
|
130
|
+
// The resolver returns `${moduleId}.${key}` as a placeholder when
|
|
131
|
+
// the key is missing — surface that as `null` rather than a
|
|
132
|
+
// raw-key string in the description slot.
|
|
133
|
+
description =
|
|
134
|
+
resolvedDescription === `${row.module_id}.${row.description_key}`
|
|
135
|
+
? null
|
|
136
|
+
: resolvedDescription;
|
|
137
|
+
}
|
|
138
|
+
resolved.push({
|
|
139
|
+
moduleId: row.module_id,
|
|
140
|
+
actionId: row.action_id,
|
|
141
|
+
label,
|
|
142
|
+
description,
|
|
143
|
+
icon: row.icon,
|
|
144
|
+
targetRoute: row.target_route,
|
|
145
|
+
keywords: Array.isArray(row.keywords) ? row.keywords : [],
|
|
146
|
+
weight: row.weight,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
resolved.sort((a, b) => {
|
|
150
|
+
if (a.weight !== b.weight)
|
|
151
|
+
return a.weight - b.weight;
|
|
152
|
+
return collator.compare(a.label, b.label);
|
|
153
|
+
});
|
|
154
|
+
const result = { actions: resolved, registryVersion };
|
|
155
|
+
this.cache.set(cacheKey, result);
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Stable hash of an operator's permission set. Sorting first makes the
|
|
161
|
+
* fingerprint independent of insertion order; sha1 keeps the cache key
|
|
162
|
+
* compact while making collisions astronomically unlikely.
|
|
163
|
+
*/
|
|
164
|
+
export function fingerprintPermissions(permissions) {
|
|
165
|
+
const sorted = [...permissions].sort();
|
|
166
|
+
return createHash('sha1').update(sorted.join('\0')).digest('hex').slice(0, 16);
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=admin-actions-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-actions-service.js","sourceRoot":"","sources":["../../../src/backend/services/admin-actions-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type ModuleLifecycleParticipant } from '@endora-commerce/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* Admin Command Palette Actions Registry — feature 020.
|
|
5
|
+
*
|
|
6
|
+
* Owns the `module_actions` table, the install-time reconciler that
|
|
7
|
+
* ingests every module's manifest `actions` array, the in-process
|
|
8
|
+
* cache, and the read API consumed by the admin SPA's command palette.
|
|
9
|
+
* Itself declares no actions — this module is platform plumbing, not
|
|
10
|
+
* a place to surface UI features.
|
|
11
|
+
*
|
|
12
|
+
* Depends on `_lifecycle` because every module's action install / hard-
|
|
13
|
+
* uninstall is driven by the lifecycle orchestrator's hook surface, and
|
|
14
|
+
* on `_i18n` because action labels and descriptions are resolved
|
|
15
|
+
* through the existing translation-bundle resolver.
|
|
16
|
+
*/
|
|
17
|
+
export declare const manifest: {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
version: string;
|
|
21
|
+
dependencies: string[];
|
|
22
|
+
description?: string | undefined;
|
|
23
|
+
acknowledgedDependencies?: {
|
|
24
|
+
moduleId: string;
|
|
25
|
+
port: string;
|
|
26
|
+
reason: string;
|
|
27
|
+
}[] | undefined;
|
|
28
|
+
nonBindingDependencies?: {
|
|
29
|
+
moduleId: string;
|
|
30
|
+
name: string;
|
|
31
|
+
kind: "contributes-to" | "degrades-without" | "refuses-without";
|
|
32
|
+
reason: string;
|
|
33
|
+
whenAbsent?: string | undefined;
|
|
34
|
+
}[] | undefined;
|
|
35
|
+
activation?: {
|
|
36
|
+
settingCode: string;
|
|
37
|
+
default: boolean;
|
|
38
|
+
} | {
|
|
39
|
+
nonDeactivatable: true;
|
|
40
|
+
reason: string;
|
|
41
|
+
} | undefined;
|
|
42
|
+
settings?: {
|
|
43
|
+
moduleCode: string;
|
|
44
|
+
groups: {
|
|
45
|
+
code: string;
|
|
46
|
+
name: string;
|
|
47
|
+
salesChannelCodes?: string[] | undefined;
|
|
48
|
+
isSystemProtected?: boolean | undefined;
|
|
49
|
+
}[];
|
|
50
|
+
settings: {
|
|
51
|
+
code: string;
|
|
52
|
+
name: string;
|
|
53
|
+
valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
|
|
54
|
+
defaultValue: unknown;
|
|
55
|
+
description?: string | undefined;
|
|
56
|
+
groupCode?: string | undefined;
|
|
57
|
+
previousDefaultValues?: unknown[] | undefined;
|
|
58
|
+
salesChannelCodes?: string[] | undefined;
|
|
59
|
+
enumOptions?: string[] | undefined;
|
|
60
|
+
configurationType?: string | undefined;
|
|
61
|
+
hidden?: boolean | undefined;
|
|
62
|
+
}[];
|
|
63
|
+
} | undefined;
|
|
64
|
+
i18n?: {
|
|
65
|
+
bundlesDir: string;
|
|
66
|
+
} | undefined;
|
|
67
|
+
docs?: false | {
|
|
68
|
+
dir: string;
|
|
69
|
+
} | undefined;
|
|
70
|
+
demo?: false | {
|
|
71
|
+
summary: string;
|
|
72
|
+
seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
|
|
73
|
+
reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
|
|
74
|
+
after?: readonly string[] | undefined;
|
|
75
|
+
package?: string | undefined;
|
|
76
|
+
} | undefined;
|
|
77
|
+
actions?: {
|
|
78
|
+
id: string;
|
|
79
|
+
labelKey: string;
|
|
80
|
+
icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
|
|
81
|
+
targetRoute: string;
|
|
82
|
+
keywords: string[];
|
|
83
|
+
weight: number;
|
|
84
|
+
descriptionKey?: string | undefined;
|
|
85
|
+
requiredPermission?: string | undefined;
|
|
86
|
+
}[] | undefined;
|
|
87
|
+
permissions?: {
|
|
88
|
+
code: string;
|
|
89
|
+
label: string;
|
|
90
|
+
module?: string | undefined;
|
|
91
|
+
description?: string | undefined;
|
|
92
|
+
requires?: string[] | undefined;
|
|
93
|
+
}[] | undefined;
|
|
94
|
+
transactionalEmails?: {
|
|
95
|
+
code: string;
|
|
96
|
+
name: string;
|
|
97
|
+
variables: {
|
|
98
|
+
key: string;
|
|
99
|
+
label: string;
|
|
100
|
+
sampleValue?: string | undefined;
|
|
101
|
+
description?: string | undefined;
|
|
102
|
+
}[];
|
|
103
|
+
description?: string | undefined;
|
|
104
|
+
group?: string | undefined;
|
|
105
|
+
}[] | undefined;
|
|
106
|
+
capabilities?: string[] | undefined;
|
|
107
|
+
exclusiveCapabilities?: {
|
|
108
|
+
key: string;
|
|
109
|
+
errorCode: string;
|
|
110
|
+
}[] | undefined;
|
|
111
|
+
errorCodes?: {
|
|
112
|
+
code: string;
|
|
113
|
+
tokens?: string[] | undefined;
|
|
114
|
+
}[] | undefined;
|
|
115
|
+
blocks?: {
|
|
116
|
+
name: string;
|
|
117
|
+
labelKey: string;
|
|
118
|
+
category: string;
|
|
119
|
+
contexts: ("invoice" | "email" | "cms" | "newsletter")[];
|
|
120
|
+
fields: Record<string, {
|
|
121
|
+
type: "number" | "object" | "uuid" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "richtext";
|
|
122
|
+
label?: string | undefined;
|
|
123
|
+
required?: boolean | undefined;
|
|
124
|
+
options?: {
|
|
125
|
+
label: string;
|
|
126
|
+
value: string | number;
|
|
127
|
+
}[] | undefined;
|
|
128
|
+
refKind?: string | undefined;
|
|
129
|
+
}>;
|
|
130
|
+
descriptionKey?: string | undefined;
|
|
131
|
+
defaultProps?: Record<string, unknown> | undefined;
|
|
132
|
+
responsiveFields?: string[] | undefined;
|
|
133
|
+
previewIcon?: string | undefined;
|
|
134
|
+
weight?: number | undefined;
|
|
135
|
+
}[] | undefined;
|
|
136
|
+
blockCategories?: {
|
|
137
|
+
key: string;
|
|
138
|
+
titleKey: string;
|
|
139
|
+
contexts: ("invoice" | "email" | "cms" | "newsletter")[];
|
|
140
|
+
weight?: number | undefined;
|
|
141
|
+
visible?: boolean | undefined;
|
|
142
|
+
}[] | undefined;
|
|
143
|
+
env?: {
|
|
144
|
+
name: string;
|
|
145
|
+
describes: {
|
|
146
|
+
en: string;
|
|
147
|
+
pl: string;
|
|
148
|
+
};
|
|
149
|
+
requirement: {
|
|
150
|
+
kind: "required";
|
|
151
|
+
} | {
|
|
152
|
+
kind: "requiredWhen";
|
|
153
|
+
input: string;
|
|
154
|
+
equals: string;
|
|
155
|
+
} | {
|
|
156
|
+
kind: "optional";
|
|
157
|
+
without: {
|
|
158
|
+
en: string;
|
|
159
|
+
pl: string;
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
secret: boolean;
|
|
163
|
+
generable: boolean;
|
|
164
|
+
owner: {
|
|
165
|
+
kind: "platform";
|
|
166
|
+
} | {
|
|
167
|
+
kind: "application";
|
|
168
|
+
application: "admin" | "backend" | "storefront";
|
|
169
|
+
} | {
|
|
170
|
+
kind: "module";
|
|
171
|
+
moduleId: string;
|
|
172
|
+
};
|
|
173
|
+
consumers: ("admin" | "backend" | "storefront")[];
|
|
174
|
+
addressOf: "admin" | "backend" | "storefront" | null;
|
|
175
|
+
}[] | undefined;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* `module_actions` follows the manifest set — feature 080, T036a / D-159.
|
|
179
|
+
*
|
|
180
|
+
* `_i18n/manifest.ts` states the shape's reasoning in full; this module is the
|
|
181
|
+
* second instance of it, projecting every other module's `manifest.actions`
|
|
182
|
+
* array instead of its `i18n` block.
|
|
183
|
+
*
|
|
184
|
+
* **It is not gated on this module's own activation, and that is a ruling**
|
|
185
|
+
* (D-159 §9, the owner, 2026-08-22). `composition.ts` used to forward the
|
|
186
|
+
* reconciler as a lazily-resolved port, so an operator who had switched the
|
|
187
|
+
* command palette off could not install an *unrelated* module — the install
|
|
188
|
+
* aborted on a `MODULE_DISABLED` from a discovery surface that has nothing to
|
|
189
|
+
* do with it. That was the better of the only two options then on the table,
|
|
190
|
+
* the other being a backend that would not start; the third, which this takes,
|
|
191
|
+
* is that a projection of manifest data is written whether or not anything is
|
|
192
|
+
* serving it. The rows are inert while the palette is off and the palette
|
|
193
|
+
* answers from them, unchanged, the moment it is switched back on.
|
|
194
|
+
*/
|
|
195
|
+
export declare const lifecycleParticipant: ModuleLifecycleParticipant<EntityManager>;
|
|
196
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAwB,KAAK,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAEnG;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCnB,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,oBAAoB,EAAE,0BAA0B,CAAC,aAAa,CAoB1E,CAAC"}
|