@jini-ai/core 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +18 -1
- package/dist/agent-tools.d.ts +1 -0
- package/dist/approval.d.ts +134 -0
- package/dist/approval.js +141 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/model-facing-tool-errors.d.ts +152 -0
- package/dist/model-facing-tool-errors.js +124 -0
- package/dist/native-regexp.d.ts +10 -0
- package/dist/native-regexp.js +2 -0
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/sse.d.ts +60 -0
- package/dist/primitives/sse.js +122 -0
- package/dist/redact.js +4 -3
- package/dist/registration-kit.d.ts +13 -0
- package/dist/registration-kit.js +12 -0
- package/dist/settings/agent-tools.d.ts +10 -0
- package/dist/settings/agent-tools.js +204 -0
- package/dist/settings/agent-value-write-tools.d.ts +22 -0
- package/dist/settings/agent-value-write-tools.js +157 -0
- package/dist/settings/agent-writable-preferences.d.ts +67 -0
- package/dist/settings/agent-writable-preferences.js +132 -0
- package/dist/settings/agent-write-denylist.d.ts +19 -0
- package/dist/settings/agent-write-denylist.js +15 -0
- package/dist/settings/change-feed.d.ts +83 -0
- package/dist/settings/change-feed.js +61 -0
- package/dist/settings/definitions-dispatch.d.ts +56 -0
- package/dist/settings/definitions-dispatch.js +86 -0
- package/dist/settings/dictionaries/index.d.ts +48 -0
- package/dist/settings/dictionaries/index.js +69 -0
- package/dist/settings/dictionaries/settings-dialog.ar.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ar.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.bn.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.bn.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.de.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.de.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.es.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.es.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.fa.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.fa.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.fr.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.fr.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.hi.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.hi.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.hu.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.hu.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.id.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.id.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.it.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.it.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ja.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ja.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ko.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ko.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.pl.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.pl.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.pt-BR.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ru.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ru.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.th.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.th.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.tr.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.tr.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.uk.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.uk.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.ur.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.ur.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.zh-CN.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +27 -0
- package/dist/settings/dictionaries/settings-dialog.zh-TW.d.ts +3 -0
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +27 -0
- package/dist/settings/ensure-definitions.d.ts +101 -0
- package/dist/settings/ensure-definitions.js +101 -0
- package/dist/settings/errors.d.ts +42 -0
- package/dist/settings/errors.js +46 -0
- package/dist/settings/express/cms-adapter.d.ts +20 -0
- package/dist/settings/express/cms-adapter.js +124 -0
- package/dist/settings/express/contracts.d.ts +135 -0
- package/dist/settings/express/contracts.js +2 -0
- package/dist/settings/express/http.d.ts +10 -0
- package/dist/settings/express/http.js +211 -0
- package/dist/settings/express/index.d.ts +5 -0
- package/dist/settings/express/index.js +4 -0
- package/dist/settings/express/shared.d.ts +68 -0
- package/dist/settings/express/shared.js +54 -0
- package/dist/settings/express/stream.d.ts +24 -0
- package/dist/settings/express/stream.js +147 -0
- package/dist/settings/index.d.ts +40 -0
- package/dist/settings/index.js +37 -0
- package/dist/settings/ports.d.ts +118 -0
- package/dist/settings/ports.js +2 -0
- package/dist/settings/principal-lookup.d.ts +9 -0
- package/dist/settings/principal-lookup.js +2 -0
- package/dist/settings/purge-service.d.ts +54 -0
- package/dist/settings/purge-service.js +106 -0
- package/dist/settings/repo.memory.d.ts +119 -0
- package/dist/settings/repo.memory.js +199 -0
- package/dist/settings/settings.d.ts +139 -0
- package/dist/settings/settings.js +259 -0
- package/dist/settings/tool-registrations.d.ts +54 -0
- package/dist/settings/tool-registrations.js +250 -0
- package/dist/settings/types.d.ts +125 -0
- package/dist/settings/types.js +3 -0
- package/dist/settings/ui-tab-definitions.d.ts +41 -0
- package/dist/settings/ui-tab-definitions.js +148 -0
- package/dist/settings/write-service.d.ts +320 -0
- package/dist/settings/write-service.js +683 -0
- package/dist/surface-exchanges.d.ts +209 -0
- package/dist/surface-exchanges.js +55 -0
- package/dist/tool-metadata.d.ts +71 -0
- package/dist/tool-metadata.js +56 -0
- package/dist/tool-permissions.d.ts +76 -0
- package/dist/tool-permissions.js +57 -0
- package/dist/tool-registry.d.ts +3 -0
- package/package.json +60 -3
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { UUID } from "../primitives/index.js";
|
|
2
|
+
import type { SettingDefinitionRecord, SettingRevisionRecord, SettingValueRecord } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* @file Port contract for the `settings` library.
|
|
5
|
+
*
|
|
6
|
+
* Purpose:
|
|
7
|
+
* Dependency-inversion seam (port/adapter rule-of-two) for the 5 settings tables.
|
|
8
|
+
* Persistence deliberately has no principal-lookup method: target-principal checks use
|
|
9
|
+
* the separate host-owned SettingsPrincipalLookupPort, keeping membership/status policy
|
|
10
|
+
* outside the settings persistence contract.
|
|
11
|
+
*
|
|
12
|
+
* `transaction` gives `write-service.ts`/`purge-service.ts` the same-tx
|
|
13
|
+
* guarantee depend on (value/definition row + its revision row
|
|
14
|
+
* must commit together, or not at all).
|
|
15
|
+
*
|
|
16
|
+
* Interfaces and types only — no feature logic.
|
|
17
|
+
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
18
|
+
*/
|
|
19
|
+
export interface SettingsRepoPort {
|
|
20
|
+
findActiveDefinition(required: {
|
|
21
|
+
namespace: string;
|
|
22
|
+
key: string;
|
|
23
|
+
workspaceId: UUID | null;
|
|
24
|
+
}): Promise<SettingDefinitionRecord | null>;
|
|
25
|
+
findDefinitionBySettingId(required: {
|
|
26
|
+
settingId: UUID;
|
|
27
|
+
version?: number;
|
|
28
|
+
}): Promise<SettingDefinitionRecord | null>;
|
|
29
|
+
listActiveDefinitions(required: {
|
|
30
|
+
workspaceId: UUID | null;
|
|
31
|
+
}): Promise<SettingDefinitionRecord[]>;
|
|
32
|
+
saveDefinition(record: SettingDefinitionRecord): Promise<void>;
|
|
33
|
+
getGlobalValue(settingId: UUID): Promise<SettingValueRecord | null>;
|
|
34
|
+
getWorkspaceValue(required: {
|
|
35
|
+
workspaceId: UUID;
|
|
36
|
+
settingId: UUID;
|
|
37
|
+
}): Promise<SettingValueRecord | null>;
|
|
38
|
+
getUserValue(required: {
|
|
39
|
+
workspaceId: UUID;
|
|
40
|
+
principalId: UUID;
|
|
41
|
+
settingId: UUID;
|
|
42
|
+
}): Promise<SettingValueRecord | null>;
|
|
43
|
+
saveGlobalValue(record: SettingValueRecord): Promise<void>;
|
|
44
|
+
saveWorkspaceValue(record: SettingValueRecord): Promise<void>;
|
|
45
|
+
saveUserValue(record: SettingValueRecord): Promise<void>;
|
|
46
|
+
listWorkspaceValues(required: {
|
|
47
|
+
workspaceId: UUID;
|
|
48
|
+
}): Promise<SettingValueRecord[]>;
|
|
49
|
+
listUserValues(required: {
|
|
50
|
+
workspaceId: UUID;
|
|
51
|
+
principalId: UUID;
|
|
52
|
+
}): Promise<SettingValueRecord[]>;
|
|
53
|
+
/**
|
|
54
|
+
* Every `setting_values_user` row for a workspace, across ALL principals
|
|
55
|
+
* (not just one) — needed by `purge-service.ts`'s full-tenant teardown path,
|
|
56
|
+
* which must clear the FK-blocking `setting_values_user` rows
|
|
57
|
+
* for every principal in the workspace, not just one caller-known id.
|
|
58
|
+
*/
|
|
59
|
+
listUserValuesByWorkspace(required: {
|
|
60
|
+
workspaceId: UUID;
|
|
61
|
+
}): Promise<SettingValueRecord[]>;
|
|
62
|
+
deleteWorkspaceValue(required: {
|
|
63
|
+
workspaceId: UUID;
|
|
64
|
+
settingId: UUID;
|
|
65
|
+
}): Promise<void>;
|
|
66
|
+
deleteUserValue(required: {
|
|
67
|
+
workspaceId: UUID;
|
|
68
|
+
principalId: UUID;
|
|
69
|
+
settingId: UUID;
|
|
70
|
+
}): Promise<void>;
|
|
71
|
+
/** Appends a revision and returns its assigned `seq` (used to stamp the paired value row). */
|
|
72
|
+
appendRevision(record: Omit<SettingRevisionRecord, "seq">): Promise<number>;
|
|
73
|
+
listRevisions(required: {
|
|
74
|
+
settingId: UUID;
|
|
75
|
+
}): Promise<SettingRevisionRecord[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Revisions with `seq` strictly greater than `sinceSeq`, oldest first, capped at `limit`, and
|
|
78
|
+
* restricted to those `workspaceId` could possibly care about.
|
|
79
|
+
*
|
|
80
|
+
* Exists for change detection across PROCESSES. `seq` is a monotonic autoincrement in the shared
|
|
81
|
+
* database, so a poller in one process observes writes made by another process (e.g. an agent
|
|
82
|
+
* daemon) — a separate OS process with its own connection — without any IPC between them.
|
|
83
|
+
*
|
|
84
|
+
* `limit` bounds a client that reconnects after a long absence: it drains in pages rather than
|
|
85
|
+
* loading an unbounded backlog into memory.
|
|
86
|
+
*
|
|
87
|
+
* **`workspaceId` is a page-sizing predicate, not the disclosure boundary.** The ledger is global
|
|
88
|
+
* — one store holds every workspace's revisions — so without it a `limit`-sized page is filled by
|
|
89
|
+
* whichever tenant writes fastest, and a quiet workspace's own change waits behind a busy
|
|
90
|
+
* neighbour's backlog. That is an observable cross-tenant timing channel and, at sustained write
|
|
91
|
+
* rates above `limit` per poll interval, a feed that never catches up at all. Implementations must
|
|
92
|
+
* match rows whose `workspaceId` is either `null` (platform definitions and `global`-scope values,
|
|
93
|
+
* which every workspace resolves through) or equal to the argument — deliberately a SUPERSET of
|
|
94
|
+
* what `change-feed.ts#isRevisionVisibleTo` permits, which stays the sole authority on disclosure.
|
|
95
|
+
*/
|
|
96
|
+
listRevisionsSince(required: {
|
|
97
|
+
sinceSeq: number;
|
|
98
|
+
limit: number;
|
|
99
|
+
workspaceId: UUID;
|
|
100
|
+
}): Promise<SettingRevisionRecord[]>;
|
|
101
|
+
/** The newest assigned `seq`, or 0 when the ledger is empty — a subscriber's starting cursor. */
|
|
102
|
+
maxRevisionSeq(): Promise<number>;
|
|
103
|
+
/**
|
|
104
|
+
* Runs `fn` with the guarantee that all repo calls inside it commit or roll
|
|
105
|
+
* back together. Every adapter must honour this — it is what makes a
|
|
106
|
+
* composite write like `resetNamespace` all-or-nothing, and the contract
|
|
107
|
+
* suite asserts it against all of them rather than only the durable one.
|
|
108
|
+
*
|
|
109
|
+
* **Not reentrant.** Calling this while another `transaction` on the same
|
|
110
|
+
* repo is open must fail loudly, never silently join. An adapter cannot tell
|
|
111
|
+
* a genuine nested call from an unrelated concurrent one, and joining a
|
|
112
|
+
* stranger's transaction makes it inherit that transaction's rollback. A
|
|
113
|
+
* caller that needs several writes to commit together opens ONE transaction
|
|
114
|
+
* and passes `skipTransaction` to the inner writes.
|
|
115
|
+
*/
|
|
116
|
+
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Principal } from "../principal.js";
|
|
2
|
+
/** Resolve only an active principal in the requested workspace. The host owns status and membership policy. */
|
|
3
|
+
export interface SettingsPrincipalLookupPort {
|
|
4
|
+
findActiveById(required: {
|
|
5
|
+
workspaceId: string;
|
|
6
|
+
id: string;
|
|
7
|
+
}): Promise<Principal | null>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=principal-lookup.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Clock, UUID } from "../primitives/index.js";
|
|
2
|
+
import type { SettingsRepoPort } from "./ports.js";
|
|
3
|
+
import type { AuthorizeFn } from "./write-service.js";
|
|
4
|
+
/**
|
|
5
|
+
* @file `purgeTenantSettings` — the ledgered tenant/principal purge chokepoint.
|
|
6
|
+
*
|
|
7
|
+
* Purpose:
|
|
8
|
+
* A host's value-table FKs (`setting_values_workspace`/`_user` -> `workspaces`)
|
|
9
|
+
* are typically `ON DELETE RESTRICT`, not `CASCADE` — a raw workspace delete
|
|
10
|
+
* cannot silently drop value rows without a ledgered revision. This module is
|
|
11
|
+
* the explicit, ledgered alternative that requires: `authorize` once
|
|
12
|
+
* -> enumerate every affected row -> append a redacted `op='purge'` revision
|
|
13
|
+
* per row -> delete the row, all in ONE transaction. The `setting_revisions`
|
|
14
|
+
* ledger itself is NEVER touched by a purge — only value rows are removed,
|
|
15
|
+
* with their purge revision left behind. Repo write methods stay
|
|
16
|
+
* package-private to this file and `write-service.ts`.
|
|
17
|
+
*
|
|
18
|
+
* Deviation from the outline's `SettingsWriteServiceDeps`: purge needs
|
|
19
|
+
* neither `ids` (no new definition/value rows are created) nor `principals`
|
|
20
|
+
* ( target-principal check is a write-time guard; purge only removes
|
|
21
|
+
* pre-existing rows, it never writes a new user-scope row for an unverified
|
|
22
|
+
* principal) — so it declares its own smaller `PurgeServiceDeps`.
|
|
23
|
+
* See docs/decisions/DR-003-settings-ledger-invariants.md.
|
|
24
|
+
*/
|
|
25
|
+
export interface PurgeServiceDeps {
|
|
26
|
+
repo: SettingsRepoPort;
|
|
27
|
+
clock: Clock;
|
|
28
|
+
authorize: AuthorizeFn;
|
|
29
|
+
}
|
|
30
|
+
export interface PurgeTenantSettingsRequired {
|
|
31
|
+
deps: PurgeServiceDeps;
|
|
32
|
+
input: {
|
|
33
|
+
workspaceId: UUID;
|
|
34
|
+
/**
|
|
35
|
+
* Omitted -> full tenant teardown: every `setting_values_workspace` row
|
|
36
|
+
* for the workspace AND every `setting_values_user` row for every
|
|
37
|
+
* principal in the workspace.
|
|
38
|
+
* Present -> scope the purge to one principal's `setting_values_user`
|
|
39
|
+
* rows only (GDPR single-subject erasure) — workspace-scope values and
|
|
40
|
+
* every other principal's rows are left untouched.
|
|
41
|
+
*/
|
|
42
|
+
principalId?: UUID;
|
|
43
|
+
callerPrincipalId: UUID;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The purge chokepoint: authorize `settings.definitions.manage` once ->
|
|
48
|
+
* enumerate the affected rows -> append a redacted `op='purge'` revision +
|
|
49
|
+
* delete, per row, all in one transaction.
|
|
50
|
+
*/
|
|
51
|
+
export declare function purgeTenantSettings(required: PurgeTenantSettingsRequired): Promise<{
|
|
52
|
+
purgedCount: number;
|
|
53
|
+
}>;
|
|
54
|
+
//# sourceMappingURL=purge-service.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { nowIso as kernelNowIso } from "../primitives/index.js";
|
|
2
|
+
import { ForbiddenError } from "./errors.js";
|
|
3
|
+
import { invalidateWorkspaceSettingsCache } from "./settings.js";
|
|
4
|
+
/**
|
|
5
|
+
* The purge chokepoint: authorize `settings.definitions.manage` once ->
|
|
6
|
+
* enumerate the affected rows -> append a redacted `op='purge'` revision +
|
|
7
|
+
* delete, per row, all in one transaction.
|
|
8
|
+
*/
|
|
9
|
+
export async function purgeTenantSettings(required) {
|
|
10
|
+
const { deps, input } = required;
|
|
11
|
+
const authResult = await deps.authorize({
|
|
12
|
+
principalId: input.callerPrincipalId,
|
|
13
|
+
permission: "settings.definitions.manage",
|
|
14
|
+
workspaceId: input.workspaceId,
|
|
15
|
+
entityType: "setting-workspace",
|
|
16
|
+
entityId: input.workspaceId,
|
|
17
|
+
});
|
|
18
|
+
if (!authResult.allowed) {
|
|
19
|
+
throw new ForbiddenError(`principal '${input.callerPrincipalId}' is not authorized for 'settings.definitions.manage' (${authResult.reason})`);
|
|
20
|
+
}
|
|
21
|
+
const result = await deps.repo.transaction(async () => {
|
|
22
|
+
const now = kernelNowIso({ clock: deps.clock });
|
|
23
|
+
let purgedCount = 0;
|
|
24
|
+
if (input.principalId) {
|
|
25
|
+
const userValues = await deps.repo.listUserValues({
|
|
26
|
+
workspaceId: input.workspaceId,
|
|
27
|
+
principalId: input.principalId,
|
|
28
|
+
});
|
|
29
|
+
for (const value of userValues) {
|
|
30
|
+
await deps.repo.appendRevision({
|
|
31
|
+
entityKind: "value",
|
|
32
|
+
settingId: value.settingId,
|
|
33
|
+
scope: "user",
|
|
34
|
+
workspaceId: input.workspaceId,
|
|
35
|
+
principalId: input.principalId,
|
|
36
|
+
op: "purge",
|
|
37
|
+
beforeJson: null,
|
|
38
|
+
afterJson: null,
|
|
39
|
+
defVersion: value.defVersion,
|
|
40
|
+
actor: input.callerPrincipalId,
|
|
41
|
+
originPluginId: null,
|
|
42
|
+
changeSetId: null,
|
|
43
|
+
createdAt: now,
|
|
44
|
+
});
|
|
45
|
+
await deps.repo.deleteUserValue({
|
|
46
|
+
workspaceId: input.workspaceId,
|
|
47
|
+
principalId: input.principalId,
|
|
48
|
+
settingId: value.settingId,
|
|
49
|
+
});
|
|
50
|
+
purgedCount++;
|
|
51
|
+
}
|
|
52
|
+
return { purgedCount };
|
|
53
|
+
}
|
|
54
|
+
const workspaceValues = await deps.repo.listWorkspaceValues({ workspaceId: input.workspaceId });
|
|
55
|
+
for (const value of workspaceValues) {
|
|
56
|
+
await deps.repo.appendRevision({
|
|
57
|
+
entityKind: "value",
|
|
58
|
+
settingId: value.settingId,
|
|
59
|
+
scope: "workspace",
|
|
60
|
+
workspaceId: input.workspaceId,
|
|
61
|
+
principalId: null,
|
|
62
|
+
op: "purge",
|
|
63
|
+
beforeJson: null,
|
|
64
|
+
afterJson: null,
|
|
65
|
+
defVersion: value.defVersion,
|
|
66
|
+
actor: input.callerPrincipalId,
|
|
67
|
+
originPluginId: null,
|
|
68
|
+
changeSetId: null,
|
|
69
|
+
createdAt: now,
|
|
70
|
+
});
|
|
71
|
+
await deps.repo.deleteWorkspaceValue({ workspaceId: input.workspaceId, settingId: value.settingId });
|
|
72
|
+
purgedCount++;
|
|
73
|
+
}
|
|
74
|
+
const userValues = await deps.repo.listUserValuesByWorkspace({ workspaceId: input.workspaceId });
|
|
75
|
+
for (const value of userValues) {
|
|
76
|
+
const targetPrincipalId = value.principalId;
|
|
77
|
+
if (!targetPrincipalId)
|
|
78
|
+
continue; // defensive: every setting_values_user row carries a principal_id
|
|
79
|
+
await deps.repo.appendRevision({
|
|
80
|
+
entityKind: "value",
|
|
81
|
+
settingId: value.settingId,
|
|
82
|
+
scope: "user",
|
|
83
|
+
workspaceId: input.workspaceId,
|
|
84
|
+
principalId: targetPrincipalId,
|
|
85
|
+
op: "purge",
|
|
86
|
+
beforeJson: null,
|
|
87
|
+
afterJson: null,
|
|
88
|
+
defVersion: value.defVersion,
|
|
89
|
+
actor: input.callerPrincipalId,
|
|
90
|
+
originPluginId: null,
|
|
91
|
+
changeSetId: null,
|
|
92
|
+
createdAt: now,
|
|
93
|
+
});
|
|
94
|
+
await deps.repo.deleteUserValue({
|
|
95
|
+
workspaceId: input.workspaceId,
|
|
96
|
+
principalId: targetPrincipalId,
|
|
97
|
+
settingId: value.settingId,
|
|
98
|
+
});
|
|
99
|
+
purgedCount++;
|
|
100
|
+
}
|
|
101
|
+
return { purgedCount };
|
|
102
|
+
});
|
|
103
|
+
invalidateWorkspaceSettingsCache({ repo: deps.repo, workspaceId: input.workspaceId }, { principalId: input.principalId });
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=purge-service.js.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { SettingsRepoPort } from "./ports.js";
|
|
2
|
+
import type { SettingDefinitionRecord, SettingRevisionRecord, SettingValueRecord } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* @file In-memory adapter for `SettingsRepoPort` (the rule-of-two: in-memory + production adapters).
|
|
5
|
+
*
|
|
6
|
+
* Purpose:
|
|
7
|
+
* Test/dev implementation. Mirrors the exact style of `identity/repo.memory.ts`:
|
|
8
|
+
* constructor takes seed rows, methods filter/mutate internal arrays.
|
|
9
|
+
*
|
|
10
|
+
* `transaction` is a real snapshot/restore journal, not a no-op wrapper. It
|
|
11
|
+
* used to be `async (fn) => fn()` on the reasoning that each individual
|
|
12
|
+
* mutation is already synchronous and atomic in-process — true, but the
|
|
13
|
+
* guarantee `SettingsRepoPort.transaction` actually sells is that a COMPOSITE
|
|
14
|
+
* of several mutations is all-or-nothing. `resetNamespace` clears a whole
|
|
15
|
+
* namespace inside one transaction; under a no-op wrapper a failure partway
|
|
16
|
+
* through left the namespace half reset while the caller saw an error. This
|
|
17
|
+
* adapter is a common wired default for dev/test composition, so that was
|
|
18
|
+
* live in dev, and the contract suite could not see it because the rollback
|
|
19
|
+
* assertion only ran against the SQLite adapter.
|
|
20
|
+
*/
|
|
21
|
+
export declare class InMemorySettingsRepo implements SettingsRepoPort {
|
|
22
|
+
private definitions;
|
|
23
|
+
private globalValues;
|
|
24
|
+
private workspaceValues;
|
|
25
|
+
private userValues;
|
|
26
|
+
private revisions;
|
|
27
|
+
private nextSeq;
|
|
28
|
+
/** True while `transaction` is open — see its doc for why this refuses
|
|
29
|
+
* rather than nests. */
|
|
30
|
+
private inTransaction;
|
|
31
|
+
constructor(seed?: {
|
|
32
|
+
definitions?: SettingDefinitionRecord[];
|
|
33
|
+
globalValues?: SettingValueRecord[];
|
|
34
|
+
workspaceValues?: SettingValueRecord[];
|
|
35
|
+
userValues?: SettingValueRecord[];
|
|
36
|
+
revisions?: SettingRevisionRecord[];
|
|
37
|
+
});
|
|
38
|
+
findActiveDefinition(required: {
|
|
39
|
+
namespace: string;
|
|
40
|
+
key: string;
|
|
41
|
+
workspaceId: string | null;
|
|
42
|
+
}): Promise<SettingDefinitionRecord | null>;
|
|
43
|
+
findDefinitionBySettingId(required: {
|
|
44
|
+
settingId: string;
|
|
45
|
+
version?: number;
|
|
46
|
+
}): Promise<SettingDefinitionRecord | null>;
|
|
47
|
+
listActiveDefinitions(required: {
|
|
48
|
+
workspaceId: string | null;
|
|
49
|
+
}): Promise<SettingDefinitionRecord[]>;
|
|
50
|
+
saveDefinition(record: SettingDefinitionRecord): Promise<void>;
|
|
51
|
+
getGlobalValue(settingId: string): Promise<SettingValueRecord | null>;
|
|
52
|
+
getWorkspaceValue(required: {
|
|
53
|
+
workspaceId: string;
|
|
54
|
+
settingId: string;
|
|
55
|
+
}): Promise<SettingValueRecord | null>;
|
|
56
|
+
getUserValue(required: {
|
|
57
|
+
workspaceId: string;
|
|
58
|
+
principalId: string;
|
|
59
|
+
settingId: string;
|
|
60
|
+
}): Promise<SettingValueRecord | null>;
|
|
61
|
+
saveGlobalValue(record: SettingValueRecord): Promise<void>;
|
|
62
|
+
saveWorkspaceValue(record: SettingValueRecord): Promise<void>;
|
|
63
|
+
saveUserValue(record: SettingValueRecord): Promise<void>;
|
|
64
|
+
listWorkspaceValues(required: {
|
|
65
|
+
workspaceId: string;
|
|
66
|
+
}): Promise<SettingValueRecord[]>;
|
|
67
|
+
listUserValues(required: {
|
|
68
|
+
workspaceId: string;
|
|
69
|
+
principalId: string;
|
|
70
|
+
}): Promise<SettingValueRecord[]>;
|
|
71
|
+
listUserValuesByWorkspace(required: {
|
|
72
|
+
workspaceId: string;
|
|
73
|
+
}): Promise<SettingValueRecord[]>;
|
|
74
|
+
deleteWorkspaceValue(required: {
|
|
75
|
+
workspaceId: string;
|
|
76
|
+
settingId: string;
|
|
77
|
+
}): Promise<void>;
|
|
78
|
+
deleteUserValue(required: {
|
|
79
|
+
workspaceId: string;
|
|
80
|
+
principalId: string;
|
|
81
|
+
settingId: string;
|
|
82
|
+
}): Promise<void>;
|
|
83
|
+
appendRevision(record: Omit<SettingRevisionRecord, "seq">): Promise<number>;
|
|
84
|
+
listRevisions(required: {
|
|
85
|
+
settingId: string;
|
|
86
|
+
}): Promise<SettingRevisionRecord[]>;
|
|
87
|
+
listRevisionsSince(required: {
|
|
88
|
+
sinceSeq: number;
|
|
89
|
+
limit: number;
|
|
90
|
+
workspaceId: string;
|
|
91
|
+
}): Promise<SettingRevisionRecord[]>;
|
|
92
|
+
maxRevisionSeq(): Promise<number>;
|
|
93
|
+
/**
|
|
94
|
+
* Runs `fn` atomically: every mutation it makes is discarded if it throws.
|
|
95
|
+
*
|
|
96
|
+
* A shallow copy of each array is enough because no method here mutates a
|
|
97
|
+
* record in place — `save*` REPLACES the element at its index, `delete*`
|
|
98
|
+
* reassigns a filtered array, `appendRevision` pushes. Restoring the arrays
|
|
99
|
+
* therefore restores the rows too. If a future method starts editing a
|
|
100
|
+
* record object in place, this must deep-copy instead.
|
|
101
|
+
*
|
|
102
|
+
* `nextSeq` is rolled back with the rest, matching SQLite: `sqlite_sequence`
|
|
103
|
+
* is an ordinary table, so an AUTOINCREMENT counter bumped inside a
|
|
104
|
+
* transaction is undone by its ROLLBACK. A seq handed out by a failed
|
|
105
|
+
* transaction is safe to reuse precisely because that revision no longer
|
|
106
|
+
* exists.
|
|
107
|
+
*
|
|
108
|
+
* **Deliberately NOT reentrant**, matching a SQLite adapter's `transaction`,
|
|
109
|
+
* where `BEGIN IMMEDIATE` inside an open transaction is an error. Refusing
|
|
110
|
+
* loudly is the point: an instance-level flag cannot tell "nested inside my
|
|
111
|
+
* caller's transaction" from "an unrelated transaction that started while
|
|
112
|
+
* mine was awaiting". Silently joining a stranger's transaction — and
|
|
113
|
+
* inheriting its rollback — is a worse defect than refusing. A caller that
|
|
114
|
+
* needs several writes to commit together opens ONE transaction and passes
|
|
115
|
+
* `skipTransaction` to the inner writes, as `resetNamespace` does.
|
|
116
|
+
*/
|
|
117
|
+
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=repo.memory.d.ts.map
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file In-memory adapter for `SettingsRepoPort` (the rule-of-two: in-memory + production adapters).
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* Test/dev implementation. Mirrors the exact style of `identity/repo.memory.ts`:
|
|
6
|
+
* constructor takes seed rows, methods filter/mutate internal arrays.
|
|
7
|
+
*
|
|
8
|
+
* `transaction` is a real snapshot/restore journal, not a no-op wrapper. It
|
|
9
|
+
* used to be `async (fn) => fn()` on the reasoning that each individual
|
|
10
|
+
* mutation is already synchronous and atomic in-process — true, but the
|
|
11
|
+
* guarantee `SettingsRepoPort.transaction` actually sells is that a COMPOSITE
|
|
12
|
+
* of several mutations is all-or-nothing. `resetNamespace` clears a whole
|
|
13
|
+
* namespace inside one transaction; under a no-op wrapper a failure partway
|
|
14
|
+
* through left the namespace half reset while the caller saw an error. This
|
|
15
|
+
* adapter is a common wired default for dev/test composition, so that was
|
|
16
|
+
* live in dev, and the contract suite could not see it because the rollback
|
|
17
|
+
* assertion only ran against the SQLite adapter.
|
|
18
|
+
*/
|
|
19
|
+
export class InMemorySettingsRepo {
|
|
20
|
+
definitions;
|
|
21
|
+
globalValues;
|
|
22
|
+
workspaceValues;
|
|
23
|
+
userValues;
|
|
24
|
+
revisions;
|
|
25
|
+
nextSeq;
|
|
26
|
+
/** True while `transaction` is open — see its doc for why this refuses
|
|
27
|
+
* rather than nests. */
|
|
28
|
+
inTransaction = false;
|
|
29
|
+
constructor(seed = {}) {
|
|
30
|
+
this.definitions = [...(seed.definitions ?? [])];
|
|
31
|
+
this.globalValues = [...(seed.globalValues ?? [])];
|
|
32
|
+
this.workspaceValues = [...(seed.workspaceValues ?? [])];
|
|
33
|
+
this.userValues = [...(seed.userValues ?? [])];
|
|
34
|
+
this.revisions = [...(seed.revisions ?? [])];
|
|
35
|
+
this.nextSeq = (seed.revisions?.reduce((max, r) => Math.max(max, r.seq), 0) ?? 0) + 1;
|
|
36
|
+
}
|
|
37
|
+
async findActiveDefinition(required) {
|
|
38
|
+
// Matches active/alias/tombstone (the "current" row for this (ns,key,ws)
|
|
39
|
+
// slot — callers decide how to react to each status, e.g. settings.ts
|
|
40
|
+
// treats tombstone as read-absent but write-service.ts reports it
|
|
41
|
+
// distinctly as DEFINITION_TOMBSTONED). Deliberately excludes
|
|
42
|
+
// `deprecated`: those are stale prior-version rows superseded by a newer
|
|
43
|
+
// active row at the same (ns,key,ws) slot (the retype mechanism), so
|
|
44
|
+
// including them would make this lookup ambiguous.
|
|
45
|
+
return (this.definitions.find((d) => d.namespace === required.namespace &&
|
|
46
|
+
d.key === required.key &&
|
|
47
|
+
d.workspaceId === required.workspaceId &&
|
|
48
|
+
(d.status === "active" || d.status === "alias" || d.status === "tombstone")) ?? null);
|
|
49
|
+
}
|
|
50
|
+
async findDefinitionBySettingId(required) {
|
|
51
|
+
const candidates = this.definitions.filter((d) => d.settingId === required.settingId);
|
|
52
|
+
if (required.version != null) {
|
|
53
|
+
return candidates.find((d) => d.version === required.version) ?? null;
|
|
54
|
+
}
|
|
55
|
+
return (candidates.find((d) => d.status === "active") ??
|
|
56
|
+
candidates.sort((a, b) => b.version - a.version)[0] ??
|
|
57
|
+
null);
|
|
58
|
+
}
|
|
59
|
+
async listActiveDefinitions(required) {
|
|
60
|
+
return this.definitions.filter((d) => d.workspaceId === required.workspaceId && (d.status === "active" || d.status === "alias"));
|
|
61
|
+
}
|
|
62
|
+
async saveDefinition(record) {
|
|
63
|
+
const index = this.definitions.findIndex((d) => d.settingId === record.settingId && d.version === record.version);
|
|
64
|
+
if (index === -1) {
|
|
65
|
+
this.definitions.push(record);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.definitions[index] = record;
|
|
69
|
+
}
|
|
70
|
+
async getGlobalValue(settingId) {
|
|
71
|
+
return this.globalValues.find((v) => v.settingId === settingId) ?? null;
|
|
72
|
+
}
|
|
73
|
+
async getWorkspaceValue(required) {
|
|
74
|
+
return (this.workspaceValues.find((v) => v.workspaceId === required.workspaceId && v.settingId === required.settingId) ?? null);
|
|
75
|
+
}
|
|
76
|
+
async getUserValue(required) {
|
|
77
|
+
return (this.userValues.find((v) => v.workspaceId === required.workspaceId &&
|
|
78
|
+
v.principalId === required.principalId &&
|
|
79
|
+
v.settingId === required.settingId) ?? null);
|
|
80
|
+
}
|
|
81
|
+
async saveGlobalValue(record) {
|
|
82
|
+
const index = this.globalValues.findIndex((v) => v.settingId === record.settingId);
|
|
83
|
+
if (index === -1) {
|
|
84
|
+
this.globalValues.push(record);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.globalValues[index] = record;
|
|
88
|
+
}
|
|
89
|
+
async saveWorkspaceValue(record) {
|
|
90
|
+
const index = this.workspaceValues.findIndex((v) => v.workspaceId === record.workspaceId && v.settingId === record.settingId);
|
|
91
|
+
if (index === -1) {
|
|
92
|
+
this.workspaceValues.push(record);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.workspaceValues[index] = record;
|
|
96
|
+
}
|
|
97
|
+
async saveUserValue(record) {
|
|
98
|
+
const index = this.userValues.findIndex((v) => v.workspaceId === record.workspaceId &&
|
|
99
|
+
v.principalId === record.principalId &&
|
|
100
|
+
v.settingId === record.settingId);
|
|
101
|
+
if (index === -1) {
|
|
102
|
+
this.userValues.push(record);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.userValues[index] = record;
|
|
106
|
+
}
|
|
107
|
+
async listWorkspaceValues(required) {
|
|
108
|
+
return this.workspaceValues.filter((v) => v.workspaceId === required.workspaceId);
|
|
109
|
+
}
|
|
110
|
+
async listUserValues(required) {
|
|
111
|
+
return this.userValues.filter((v) => v.workspaceId === required.workspaceId && v.principalId === required.principalId);
|
|
112
|
+
}
|
|
113
|
+
async listUserValuesByWorkspace(required) {
|
|
114
|
+
return this.userValues.filter((v) => v.workspaceId === required.workspaceId);
|
|
115
|
+
}
|
|
116
|
+
async deleteWorkspaceValue(required) {
|
|
117
|
+
this.workspaceValues = this.workspaceValues.filter((v) => !(v.workspaceId === required.workspaceId && v.settingId === required.settingId));
|
|
118
|
+
}
|
|
119
|
+
async deleteUserValue(required) {
|
|
120
|
+
this.userValues = this.userValues.filter((v) => !(v.workspaceId === required.workspaceId &&
|
|
121
|
+
v.principalId === required.principalId &&
|
|
122
|
+
v.settingId === required.settingId));
|
|
123
|
+
}
|
|
124
|
+
async appendRevision(record) {
|
|
125
|
+
const seq = this.nextSeq++;
|
|
126
|
+
this.revisions.push({ ...record, seq });
|
|
127
|
+
return seq;
|
|
128
|
+
}
|
|
129
|
+
async listRevisions(required) {
|
|
130
|
+
return this.revisions.filter((r) => r.settingId === required.settingId).sort((a, b) => a.seq - b.seq);
|
|
131
|
+
}
|
|
132
|
+
async listRevisionsSince(required) {
|
|
133
|
+
return this.revisions
|
|
134
|
+
.filter((r) => r.seq > required.sinceSeq &&
|
|
135
|
+
// Mirrors the SQL predicate exactly — see the port's doc. `null` means
|
|
136
|
+
// platform-wide (every workspace resolves through it); anything else is one tenant's.
|
|
137
|
+
(r.workspaceId === null || r.workspaceId === required.workspaceId))
|
|
138
|
+
.sort((a, b) => a.seq - b.seq)
|
|
139
|
+
.slice(0, required.limit);
|
|
140
|
+
}
|
|
141
|
+
async maxRevisionSeq() {
|
|
142
|
+
return this.revisions.reduce((max, r) => (r.seq > max ? r.seq : max), 0);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Runs `fn` atomically: every mutation it makes is discarded if it throws.
|
|
146
|
+
*
|
|
147
|
+
* A shallow copy of each array is enough because no method here mutates a
|
|
148
|
+
* record in place — `save*` REPLACES the element at its index, `delete*`
|
|
149
|
+
* reassigns a filtered array, `appendRevision` pushes. Restoring the arrays
|
|
150
|
+
* therefore restores the rows too. If a future method starts editing a
|
|
151
|
+
* record object in place, this must deep-copy instead.
|
|
152
|
+
*
|
|
153
|
+
* `nextSeq` is rolled back with the rest, matching SQLite: `sqlite_sequence`
|
|
154
|
+
* is an ordinary table, so an AUTOINCREMENT counter bumped inside a
|
|
155
|
+
* transaction is undone by its ROLLBACK. A seq handed out by a failed
|
|
156
|
+
* transaction is safe to reuse precisely because that revision no longer
|
|
157
|
+
* exists.
|
|
158
|
+
*
|
|
159
|
+
* **Deliberately NOT reentrant**, matching a SQLite adapter's `transaction`,
|
|
160
|
+
* where `BEGIN IMMEDIATE` inside an open transaction is an error. Refusing
|
|
161
|
+
* loudly is the point: an instance-level flag cannot tell "nested inside my
|
|
162
|
+
* caller's transaction" from "an unrelated transaction that started while
|
|
163
|
+
* mine was awaiting". Silently joining a stranger's transaction — and
|
|
164
|
+
* inheriting its rollback — is a worse defect than refusing. A caller that
|
|
165
|
+
* needs several writes to commit together opens ONE transaction and passes
|
|
166
|
+
* `skipTransaction` to the inner writes, as `resetNamespace` does.
|
|
167
|
+
*/
|
|
168
|
+
async transaction(fn) {
|
|
169
|
+
if (this.inTransaction) {
|
|
170
|
+
throw new Error("InMemorySettingsRepo.transaction is not reentrant, and cannot host a second overlapping transaction. " +
|
|
171
|
+
"Open one transaction and pass `skipTransaction` to the inner writes (see resetNamespace).");
|
|
172
|
+
}
|
|
173
|
+
const snapshot = {
|
|
174
|
+
definitions: [...this.definitions],
|
|
175
|
+
globalValues: [...this.globalValues],
|
|
176
|
+
workspaceValues: [...this.workspaceValues],
|
|
177
|
+
userValues: [...this.userValues],
|
|
178
|
+
revisions: [...this.revisions],
|
|
179
|
+
nextSeq: this.nextSeq,
|
|
180
|
+
};
|
|
181
|
+
this.inTransaction = true;
|
|
182
|
+
try {
|
|
183
|
+
return await fn();
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
this.definitions = snapshot.definitions;
|
|
187
|
+
this.globalValues = snapshot.globalValues;
|
|
188
|
+
this.workspaceValues = snapshot.workspaceValues;
|
|
189
|
+
this.userValues = snapshot.userValues;
|
|
190
|
+
this.revisions = snapshot.revisions;
|
|
191
|
+
this.nextSeq = snapshot.nextSeq;
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
this.inTransaction = false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=repo.memory.js.map
|