@cosmicdrift/kumiko-bundled-features 0.255.0 → 0.255.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.255.
|
|
3
|
+
"version": "0.255.2",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -130,12 +130,12 @@
|
|
|
130
130
|
"./workflow-runner": "./src/workflow-runner/index.ts"
|
|
131
131
|
},
|
|
132
132
|
"dependencies": {
|
|
133
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.255.
|
|
134
|
-
"@cosmicdrift/kumiko-framework": "0.255.
|
|
135
|
-
"@cosmicdrift/kumiko-headless": "0.255.
|
|
136
|
-
"@cosmicdrift/kumiko-renderer": "0.255.
|
|
137
|
-
"@cosmicdrift/kumiko-renderer-web": "0.255.
|
|
138
|
-
"@cosmicdrift/kumiko-types": "0.255.
|
|
133
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.255.2",
|
|
134
|
+
"@cosmicdrift/kumiko-framework": "0.255.2",
|
|
135
|
+
"@cosmicdrift/kumiko-headless": "0.255.2",
|
|
136
|
+
"@cosmicdrift/kumiko-renderer": "0.255.2",
|
|
137
|
+
"@cosmicdrift/kumiko-renderer-web": "0.255.2",
|
|
138
|
+
"@cosmicdrift/kumiko-types": "0.255.2",
|
|
139
139
|
"@mollie/api-client": "^4.5.0",
|
|
140
140
|
"@node-rs/argon2": "^2.0.2",
|
|
141
141
|
"@types/mailparser": "^3.4.6",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@testing-library/user-event": "^14.6.1",
|
|
166
166
|
"@types/qrcode": "^1.5.5",
|
|
167
|
-
"@cosmicdrift/kumiko-locale-de": "0.255.
|
|
168
|
-
"@cosmicdrift/kumiko-locale-es": "0.255.
|
|
167
|
+
"@cosmicdrift/kumiko-locale-de": "0.255.2",
|
|
168
|
+
"@cosmicdrift/kumiko-locale-es": "0.255.2"
|
|
169
169
|
}
|
|
170
170
|
}
|
|
@@ -855,12 +855,13 @@ describe("scenario 10: PII is encrypted at rest, not just erasable on destroy",
|
|
|
855
855
|
// is written through the encrypted path above. If that changes (billing-
|
|
856
856
|
// foundation goes live before a KMS is configured somewhere), note for
|
|
857
857
|
// whoever builds the backfill: the existing backfillEventPiiEncryption
|
|
858
|
-
// (#799, db/queries/backfill-pii.ts) does NOT cover this feature yet —
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
// (
|
|
863
|
-
//
|
|
858
|
+
// (#799, db/queries/backfill-pii.ts) does NOT cover this feature yet — its
|
|
859
|
+
// lifecycle-event matcher expects `<aggregateType>.<verb>` event names,
|
|
860
|
+
// but billing's are `billing-foundation:event:subscription-created`. The
|
|
861
|
+
// custom-event catalog branch itself already resolves user/tenant/self
|
|
862
|
+
// subjects (fw#2801/#2819) — no framework gap to close here, only the
|
|
863
|
+
// lifecycle-event matcher would need widening if this feature ever needs
|
|
864
|
+
// a backfill.
|
|
864
865
|
});
|
|
865
866
|
|
|
866
867
|
// =============================================================================
|
|
@@ -3,8 +3,8 @@ import { requestContext } from "@cosmicdrift/kumiko-framework/api";
|
|
|
3
3
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
4
4
|
import {
|
|
5
5
|
configuredPiiSubjectKms,
|
|
6
|
-
RECORD_ENTITY_PATTERN,
|
|
7
6
|
type SubjectId,
|
|
7
|
+
subjectIdSchema,
|
|
8
8
|
subjectIdToKey,
|
|
9
9
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
10
10
|
import {
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
type FeatureDefinition,
|
|
20
20
|
type HandlerContext,
|
|
21
21
|
type SessionUser,
|
|
22
|
-
type TenantId,
|
|
23
22
|
type WriteEvent,
|
|
24
23
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
25
24
|
import {
|
|
@@ -49,15 +48,7 @@ import {
|
|
|
49
48
|
TARGET_TENANT_NOT_ADMIN_TENANT,
|
|
50
49
|
} from "../constants";
|
|
51
50
|
|
|
52
|
-
export
|
|
53
|
-
z.object({ kind: z.literal("user"), userId: z.uuid() }),
|
|
54
|
-
z.object({ kind: z.literal("tenant"), tenantId: z.uuid() }),
|
|
55
|
-
z.object({
|
|
56
|
-
kind: z.literal("record"),
|
|
57
|
-
entity: z.string().regex(RECORD_ENTITY_PATTERN),
|
|
58
|
-
id: z.uuid(),
|
|
59
|
-
}),
|
|
60
|
-
]);
|
|
51
|
+
export { subjectIdSchema };
|
|
61
52
|
|
|
62
53
|
export const forgetSubjectSchema = z.object({
|
|
63
54
|
subject: subjectIdSchema,
|
|
@@ -296,7 +287,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
296
287
|
raw.kind === "user"
|
|
297
288
|
? { kind: "user", userId: raw.userId }
|
|
298
289
|
: raw.kind === "tenant"
|
|
299
|
-
? { kind: "tenant", tenantId: raw.tenantId
|
|
290
|
+
? { kind: "tenant", tenantId: raw.tenantId }
|
|
300
291
|
: { kind: "record", entity: raw.entity, id: raw.id };
|
|
301
292
|
const subjectKey = subjectIdToKey(subject);
|
|
302
293
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createStaticLocaleResolver,
|
|
4
|
+
LocaleProvider,
|
|
5
|
+
PrimitivesProvider,
|
|
6
|
+
} from "@cosmicdrift/kumiko-renderer";
|
|
7
|
+
import { defaultPrimitives } from "@cosmicdrift/kumiko-renderer-web";
|
|
8
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
9
|
+
import { type ReactNode, useState } from "react";
|
|
10
|
+
import { TagsQueries } from "../../constants";
|
|
11
|
+
import { defaultTranslations } from "../i18n";
|
|
12
|
+
import type { TagPicker as TagPickerComponent } from "../tag-picker";
|
|
13
|
+
|
|
14
|
+
// tag-filter.test.tsx and tag-section.test.tsx register a process-wide
|
|
15
|
+
// mock.module stub on the resolved "../tag-picker" path; the query-string
|
|
16
|
+
// specifier loads the real component past it. It goes through a variable
|
|
17
|
+
// because tsc cannot resolve the suffixed specifier, so the namespace type
|
|
18
|
+
// comes from the type-only import above.
|
|
19
|
+
const REAL_TAG_PICKER_SPECIFIER = "../tag-picker?real=tag-picker-test";
|
|
20
|
+
const { TagPicker } = (await import(REAL_TAG_PICKER_SPECIFIER)) as {
|
|
21
|
+
readonly TagPicker: typeof TagPickerComponent;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type TagRow = { id: string; name: string; color?: string; scope?: string; version: number };
|
|
25
|
+
type AssignmentRow = { tagId: string };
|
|
26
|
+
|
|
27
|
+
let catalogRows: readonly TagRow[] = [];
|
|
28
|
+
let assignmentRows: readonly AssignmentRow[] = [];
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
catalogRows = [
|
|
32
|
+
{ id: "t1", name: "urgent", color: "#ef4444", version: 1 },
|
|
33
|
+
{ id: "t2", name: "later", version: 1 },
|
|
34
|
+
];
|
|
35
|
+
assignmentRows = [];
|
|
36
|
+
dispatchSpy.mockClear();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const dispatchSpy = mock(async () => ({ isSuccess: true, data: undefined }));
|
|
40
|
+
|
|
41
|
+
const useQuerySpy = mock((type: string) => {
|
|
42
|
+
if (type === TagsQueries.tagList) {
|
|
43
|
+
return {
|
|
44
|
+
data: { rows: catalogRows },
|
|
45
|
+
loading: false,
|
|
46
|
+
error: null,
|
|
47
|
+
refetch: mock(async () => {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
data: { rows: assignmentRows },
|
|
52
|
+
loading: false,
|
|
53
|
+
error: null,
|
|
54
|
+
refetch: mock(async () => {}),
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const actual_renderer = await import("@cosmicdrift/kumiko-renderer");
|
|
59
|
+
mock.module("@cosmicdrift/kumiko-renderer", () => ({
|
|
60
|
+
...actual_renderer,
|
|
61
|
+
useDispatcher: mock(() => ({ write: dispatchSpy, query: mock(), batch: mock() })),
|
|
62
|
+
useQuery: useQuerySpy,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
function Wrapper({ children }: { readonly children: ReactNode }): ReactNode {
|
|
66
|
+
return (
|
|
67
|
+
<LocaleProvider resolver={createStaticLocaleResolver()} fallbackBundles={[defaultTranslations]}>
|
|
68
|
+
<PrimitivesProvider value={defaultPrimitives}>{children}</PrimitivesProvider>
|
|
69
|
+
</LocaleProvider>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function Host({
|
|
74
|
+
initialValue,
|
|
75
|
+
initialOpen,
|
|
76
|
+
}: {
|
|
77
|
+
readonly initialValue: readonly string[];
|
|
78
|
+
readonly initialOpen: boolean;
|
|
79
|
+
}): ReactNode {
|
|
80
|
+
const [open, setOpen] = useState(initialOpen);
|
|
81
|
+
const [applied, setApplied] = useState<readonly string[]>(initialValue);
|
|
82
|
+
const [bump, setBump] = useState(0);
|
|
83
|
+
const value = applied.map((id) => id);
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<button type="button" data-testid="host-open" onClick={() => setOpen(true)}>
|
|
87
|
+
open
|
|
88
|
+
</button>
|
|
89
|
+
<button type="button" data-testid="host-rerender" onClick={() => setBump(bump + 1)}>
|
|
90
|
+
rerender {bump}
|
|
91
|
+
</button>
|
|
92
|
+
<span data-testid="host-applied">{value.join(",")}</span>
|
|
93
|
+
<TagPicker
|
|
94
|
+
entityType="note"
|
|
95
|
+
value={value}
|
|
96
|
+
onChange={setApplied}
|
|
97
|
+
open={open}
|
|
98
|
+
onOpenChange={setOpen}
|
|
99
|
+
/>
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
describe("TagPicker — open/confirm/cancel sync", () => {
|
|
105
|
+
test("confirm hands the toggled selection back to the caller", async () => {
|
|
106
|
+
render(
|
|
107
|
+
<Wrapper>
|
|
108
|
+
<Host initialValue={[]} initialOpen={true} />
|
|
109
|
+
</Wrapper>,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
fireEvent.click(await screen.findByTestId("tag-manager-toggle-t1"));
|
|
113
|
+
fireEvent.click(screen.getByTestId("tag-picker-dialog-confirm"));
|
|
114
|
+
|
|
115
|
+
await waitFor(() => expect(screen.getByTestId("host-applied").textContent).toBe("t1"));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("cancel discards the buffered selection; reopening starts from the caller's value", async () => {
|
|
119
|
+
render(
|
|
120
|
+
<Wrapper>
|
|
121
|
+
<Host initialValue={["t1"]} initialOpen={true} />
|
|
122
|
+
</Wrapper>,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
fireEvent.click(await screen.findByTestId("tag-manager-toggle-t2"));
|
|
126
|
+
fireEvent.click(screen.getByTestId("tag-picker-dialog-cancel"));
|
|
127
|
+
|
|
128
|
+
await waitFor(() => expect(screen.queryByTestId("tag-picker-dialog")).toBeNull());
|
|
129
|
+
expect(screen.getByTestId("host-applied").textContent).toBe("t1");
|
|
130
|
+
|
|
131
|
+
fireEvent.click(screen.getByTestId("host-open"));
|
|
132
|
+
fireEvent.click(await screen.findByTestId("tag-picker-dialog-confirm"));
|
|
133
|
+
|
|
134
|
+
await waitFor(() => expect(screen.getByTestId("host-applied").textContent).toBe("t1"));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a parent re-render while open keeps the in-flight selection", async () => {
|
|
138
|
+
render(
|
|
139
|
+
<Wrapper>
|
|
140
|
+
<Host initialValue={[]} initialOpen={true} />
|
|
141
|
+
</Wrapper>,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
fireEvent.click(await screen.findByTestId("tag-manager-toggle-t1"));
|
|
145
|
+
expect(screen.getByTestId("tag-manager-toggle-t1").textContent).toBe("✓");
|
|
146
|
+
|
|
147
|
+
fireEvent.click(screen.getByTestId("host-rerender"));
|
|
148
|
+
await waitFor(() => expect(screen.getByTestId("tag-manager-toggle-t1").textContent).toBe("✓"));
|
|
149
|
+
|
|
150
|
+
fireEvent.click(screen.getByTestId("tag-picker-dialog-confirm"));
|
|
151
|
+
await waitFor(() => expect(screen.getByTestId("host-applied").textContent).toBe("t1"));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// caller's entityType so only global + matching labels are offered.
|
|
8
8
|
|
|
9
9
|
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
10
|
-
import { type ReactNode,
|
|
10
|
+
import { type ReactNode, useState } from "react";
|
|
11
11
|
import { TagManager } from "./tag-manager";
|
|
12
12
|
|
|
13
13
|
export function TagPicker({
|
|
@@ -26,11 +26,14 @@ export function TagPicker({
|
|
|
26
26
|
const { Dialog } = usePrimitives();
|
|
27
27
|
const t = useTranslation();
|
|
28
28
|
const [buffer, setBuffer] = useState<readonly string[]>(value);
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
29
|
+
const [wasOpen, setWasOpen] = useState(open);
|
|
30
|
+
// Syncing on the open transition alone, not on `value`: both call sites derive
|
|
31
|
+
// `value` freshly each render, so a value-keyed effect discarded the in-flight
|
|
32
|
+
// selection on every parent re-render.
|
|
33
|
+
if (open !== wasOpen) {
|
|
34
|
+
setWasOpen(open);
|
|
32
35
|
if (open) setBuffer(value);
|
|
33
|
-
}
|
|
36
|
+
}
|
|
34
37
|
|
|
35
38
|
return (
|
|
36
39
|
<Dialog
|