@cosmicdrift/kumiko-framework 0.362.0 → 0.364.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/AGENTS.md +307 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +443 -0
- package/agents/examples/anonymous-access.md +300 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +323 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +314 -0
- package/agents/examples/custom-handlers.md +397 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +405 -0
- package/agents/examples/error-contract.md +539 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +263 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +214 -0
- package/agents/examples/ledger-invoicing.md +235 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +345 -0
- package/agents/examples/ownership.md +396 -0
- package/agents/examples/pdf-audit-export.md +270 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +259 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +410 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +74 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +14 -5
- package/dist/api/auth-routes.js +5 -3
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/redis-sse-broker.js +10 -8
- package/dist/api/routes.js +48 -19
- package/dist/api/server.d.ts +2 -1
- package/dist/api/server.js +18 -6
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +12 -5
- package/dist/bun-db/query.js +88 -94
- package/dist/crypto/kek-source.js +5 -3
- package/dist/crypto/pii-field-encryption.d.ts +4 -1
- package/dist/crypto/pii-field-encryption.js +2 -2
- package/dist/db/apply-entity-event.js +2 -2
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/cursor.js +2 -1
- package/dist/db/dialect.d.ts +1 -0
- package/dist/db/dialect.js +28 -13
- package/dist/db/eagerload.js +2 -2
- package/dist/db/entity-table-from-registry.js +1 -1
- package/dist/db/entity-table-meta.js +5 -13
- package/dist/db/event-store-executor-context.d.ts +5 -6
- package/dist/db/event-store-executor-context.js +29 -27
- package/dist/db/event-store-executor-read.js +40 -25
- package/dist/db/event-store-executor-write.js +18 -20
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +6 -4
- package/dist/db/migrate-runner.js +32 -35
- package/dist/db/parent-ref-clause.js +2 -2
- package/dist/db/queries/backfill-pii.js +11 -9
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +17 -17
- package/dist/db/queries/event-store-admin.js +6 -6
- package/dist/db/queries/event-store-transfer.js +2 -2
- package/dist/db/queries/event-store.js +2 -2
- package/dist/db/queries/pii-backfill-state.js +2 -2
- package/dist/db/queries/raw-sql.js +1 -1
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/reference-data.js +3 -5
- package/dist/db/table-builder.js +6 -4
- package/dist/db/table-introspection.d.ts +16 -0
- package/dist/db/table-introspection.js +76 -0
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +5 -2
- package/dist/db/tenant-db.js +34 -14
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/variant-key.js +2 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +84 -35
- package/dist/engine/boot-validator/entity-list-screens.js +3 -3
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +9 -9
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +155 -96
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/changeset-changes.js +3 -3
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +11 -7
- package/dist/engine/embedded-derived.js +10 -6
- package/dist/engine/extensions/escape-hatch-usage.js +2 -1
- package/dist/engine/factories.js +5 -6
- package/dist/engine/feature-ast/extractors/events.js +43 -33
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/patcher.d.ts +3 -3
- package/dist/engine/feature-ast/patcher.js +3 -3
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
- package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
- package/dist/engine/feature-ast/patterns.d.ts +3 -3
- package/dist/engine/feature-ast/render.js +16 -5
- package/dist/engine/feature-changelog.d.ts +2 -1
- package/dist/engine/feature-changelog.js +8 -6
- package/dist/engine/feature-config-events-jobs.d.ts +17 -7
- package/dist/engine/feature-config-events-jobs.js +20 -5
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/feature-ui-extensions.js +2 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -2
- package/dist/engine/index.js +1 -0
- package/dist/engine/ownership.js +7 -20
- package/dist/engine/pattern-library/mixed-schemas.js +0 -1
- package/dist/engine/registry-ingest.d.ts +2 -1
- package/dist/engine/registry-ingest.js +85 -2
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +38 -8
- package/dist/engine/registry.js +2 -1
- package/dist/engine/schema-builder.js +3 -4
- package/dist/engine/soft-delete-cleanup.js +12 -4
- package/dist/engine/steps/call-feature.js +1 -3
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
- package/dist/engine/template-variables.d.ts +32 -0
- package/dist/engine/template-variables.js +80 -0
- package/dist/engine/tier-resolver-extension.js +2 -3
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/validate-projection-allowlist.js +12 -13
- package/dist/engine/where-rule-lint.js +8 -7
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +22 -0
- package/dist/files/create-file-ref.js +53 -25
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +8 -0
- package/dist/files/file-routes.js +2 -2
- package/dist/files/index.d.ts +2 -2
- package/dist/files/index.js +1 -1
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/local-provider.js +2 -1
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +45 -19
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/mail-registry.d.ts +2 -0
- package/dist/i18n/mail-registry.js +7 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +4 -1
- package/dist/jobs/job-runner.js +13 -9
- package/dist/observability/escape-hatch-report.d.ts +1 -1
- package/dist/observability/escape-hatch-report.js +17 -5
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.js +2 -3
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +142 -34
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/event-dispatcher-delivery.js +2 -3
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +2 -0
- package/dist/pipeline/member-read-only-transaction.js +1 -1
- package/dist/pipeline/notify-scope.d.ts +13 -0
- package/dist/pipeline/notify-scope.js +32 -0
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +28 -9
- package/dist/pipeline/system-hooks.js +2 -1
- package/dist/pipeline/system-identity-switch.js +2 -2
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/search/meilisearch-adapter.js +39 -13
- package/dist/secrets/stored-envelope.js +7 -7
- package/dist/stack/push-entity-projection-tables.js +2 -3
- package/dist/stack/table-helpers.js +10 -26
- package/dist/stack/test-stack.js +2 -5
- package/dist/testing/multipart-helper.d.ts +0 -6
- package/dist/testing/multipart-helper.js +5 -5
- package/dist/testing/without-ambient-temporal.js +4 -4
- package/dist/utils/is-plain-object.d.ts +1 -2
- package/dist/utils/is-plain-object.js +1 -4
- package/package.json +7 -5
- package/src/changes.json +251 -8
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/user-data-rights. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# user-data-rights
|
|
4
|
+
|
|
5
|
+
DSGVO-compliant in 5 LOC: r.useExtension(EXT_USER_DATA, ...) haengt eine Domain an Forget+Export-Pipeline. Forget-Cron, Export-ZIP, Magic-Link kommen alle aus user-data-rights.
|
|
6
|
+
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# user-data-rights
|
|
10
|
+
|
|
11
|
+
GDPR-compliant domain wiring in ~5 lines: `r.useExtension(EXT_USER_DATA, …)`
|
|
12
|
+
hooks any entity into the forget cron, export ZIP, and magic-link pipeline.
|
|
13
|
+
You write two hooks per entity — the framework runs the rest.
|
|
14
|
+
|
|
15
|
+
## What it shows
|
|
16
|
+
|
|
17
|
+
A minimal notes domain with GDPR Art. 15+17+20 integrated without hand-written
|
|
18
|
+
forget logic:
|
|
19
|
+
|
|
20
|
+
```ts illustration
|
|
21
|
+
defineFeature("notes", (r) => {
|
|
22
|
+
r.requires("user-data-rights");
|
|
23
|
+
r.entity("note", noteEntity);
|
|
24
|
+
|
|
25
|
+
r.useExtension(EXT_USER_DATA, "note", {
|
|
26
|
+
export: async (ctx) => ({
|
|
27
|
+
entity: "note",
|
|
28
|
+
rows: await ctx.db.select().from(notesTable)
|
|
29
|
+
.where(and(eq(notesTable.tenantId, ctx.tenantId),
|
|
30
|
+
eq(notesTable.authorId, ctx.userId))),
|
|
31
|
+
}),
|
|
32
|
+
delete: async (ctx, strategy) => {
|
|
33
|
+
const where = and(eq(notesTable.tenantId, ctx.tenantId),
|
|
34
|
+
eq(notesTable.authorId, ctx.userId));
|
|
35
|
+
if (strategy === "anonymize") {
|
|
36
|
+
await ctx.db.update(notesTable).set({ authorId: null }).where(where);
|
|
37
|
+
} else {
|
|
38
|
+
await ctx.db.delete(notesTable).where(where);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What runs automatically
|
|
46
|
+
|
|
47
|
+
| Article | What happens | Trigger |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| Art. 15 + 20 | Notes snippet in export ZIP + magic-link email | `runUserExport` (via `request-export`) |
|
|
50
|
+
| Art. 17 | Notes deleted or anonymized after grace | `runForgetCleanup` cron |
|
|
51
|
+
| Art. 18 | Restriction blocks login while active | Auth middleware |
|
|
52
|
+
| Operator | Brute-force detection on magic-link endpoint | Edge rate-limit + audit log |
|
|
53
|
+
|
|
54
|
+
## Strategy-aware delete
|
|
55
|
+
|
|
56
|
+
`retention.policyFor("note")` resolves per entity:
|
|
57
|
+
|
|
58
|
+
| Profile | Strategy | Effect |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `eu-dsgvo` (default) | `delete` | Notes hard-deleted |
|
|
61
|
+
| `de-hr-dsgvo-hgb` | `anonymize` for HR entities | `authorId=null`, row kept for multi-user refs |
|
|
62
|
+
|
|
63
|
+
The integration test pins both paths.
|
|
64
|
+
|
|
65
|
+
## Flow
|
|
66
|
+
|
|
67
|
+
1. User creates notes → rows stored with `authorId`.
|
|
68
|
+
2. `request-export` queues job → `runUserExport` bundles user + note rows.
|
|
69
|
+
3. `request-deletion` → grace period from compliance profile.
|
|
70
|
+
4. `runForgetCleanup` calls your `delete` hook with resolved strategy.
|
|
71
|
+
|
|
72
|
+
## Tests
|
|
73
|
+
|
|
74
|
+
Needs a running Postgres and `TEST_DATABASE_URL` set (e.g. `postgres://postgres:postgres@127.0.0.1:5432/postgres`, see `demo/.env.example`).
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bun test src/__tests__/feature.integration.test.ts
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Step-by-step proof:
|
|
81
|
+
|
|
82
|
+
1. Alice creates 2 notes → export bundle includes note snippet
|
|
83
|
+
2. Deletion + expired grace → `runForgetCleanup` removes all notes
|
|
84
|
+
3. Strategy `anonymize` → `authorId=null`, title/body remain
|
|
85
|
+
|
|
86
|
+
## Full app vs this recipe
|
|
87
|
+
|
|
88
|
+
| | This recipe | [user-data-rights-demo](/en/samples/apps-user-data-rights-demo/) |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| Scope | One hook pattern | Runnable app + todos + files |
|
|
91
|
+
| Boot | Integration test only | `bun dev` on port 4291 |
|
|
92
|
+
|
|
93
|
+
## Dependencies
|
|
94
|
+
|
|
95
|
+
```ts illustration
|
|
96
|
+
import { createUserDataRightsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
|
|
97
|
+
import { createUserDataRightsDefaultsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights-defaults";
|
|
98
|
+
import { createDataRetentionFeature } from "@cosmicdrift/kumiko-bundled-features/data-retention";
|
|
99
|
+
import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
|
|
100
|
+
|
|
101
|
+
export const features = [
|
|
102
|
+
createDataRetentionFeature(),
|
|
103
|
+
createComplianceProfilesFeature(),
|
|
104
|
+
createUserDataRightsFeature(),
|
|
105
|
+
createUserDataRightsDefaultsFeature(),
|
|
106
|
+
notesFeature,
|
|
107
|
+
];
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Source files
|
|
111
|
+
|
|
112
|
+
### src/__tests__/feature.integration.test.ts
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
// User-Data-Rights Recipe — Integration-Test.
|
|
116
|
+
//
|
|
117
|
+
// Pinst dass die zwei Hooks aus feature.ts genau das tun was die Recipe-
|
|
118
|
+
// README verspricht:
|
|
119
|
+
// 1. Notes des Users landen via export-Hook im Bundle-Snippet.
|
|
120
|
+
// 2. Forget-Cleanup-Cron entfernt die Notes via delete-Hook (strategy
|
|
121
|
+
// "delete", default).
|
|
122
|
+
// 3. Strategy "anonymize" (HR-Override) setzt authorId=null statt zu
|
|
123
|
+
// löschen — Row bleibt für Multi-User-Refs intakt.
|
|
124
|
+
|
|
125
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
126
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
127
|
+
import {
|
|
128
|
+
createComplianceProfilesFeature,
|
|
129
|
+
tenantComplianceProfileEntity,
|
|
130
|
+
} from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
|
|
131
|
+
import {
|
|
132
|
+
createDataRetentionFeature,
|
|
133
|
+
tenantRetentionOverrideEntity,
|
|
134
|
+
} from "@cosmicdrift/kumiko-bundled-features/data-retention";
|
|
135
|
+
import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
136
|
+
import {
|
|
137
|
+
createUserFeature,
|
|
138
|
+
USER_STATUS,
|
|
139
|
+
userEntity,
|
|
140
|
+
userTable,
|
|
141
|
+
} from "@cosmicdrift/kumiko-bundled-features/user";
|
|
142
|
+
import {
|
|
143
|
+
createUserDataRightsFeature,
|
|
144
|
+
runForgetCleanup,
|
|
145
|
+
runUserExport,
|
|
146
|
+
} from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
|
|
147
|
+
import { asRawClient, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
148
|
+
import { EXT_USER_DATA } from "@cosmicdrift/kumiko-framework/engine";
|
|
149
|
+
import {
|
|
150
|
+
setupTestStack,
|
|
151
|
+
type TestStack,
|
|
152
|
+
unsafeCreateEntityTable,
|
|
153
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
154
|
+
import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
|
|
155
|
+
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
156
|
+
import { noteEntity, notesFeature, notesTable } from "../feature";
|
|
157
|
+
|
|
158
|
+
let stack: TestStack;
|
|
159
|
+
|
|
160
|
+
const TENANT_A = "00000000-0000-4000-8000-00000000000a";
|
|
161
|
+
const TENANT_SYSTEM = "00000000-0000-4000-8000-000000000001";
|
|
162
|
+
const ALICE_ID = "11111111-1111-4111-8111-000000000001";
|
|
163
|
+
|
|
164
|
+
type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
|
|
165
|
+
const NOW = (): Instant => getTemporal().Now.instant();
|
|
166
|
+
const PAST = (): Instant => getTemporal().Instant.fromEpochMilliseconds(Date.now() - 60_000);
|
|
167
|
+
|
|
168
|
+
beforeAll(async () => {
|
|
169
|
+
stack = await setupTestStack({
|
|
170
|
+
features: [
|
|
171
|
+
createUserFeature(),
|
|
172
|
+
authFoundationFeature,
|
|
173
|
+
createSessionsFeature(),
|
|
174
|
+
createDataRetentionFeature(),
|
|
175
|
+
createComplianceProfilesFeature(),
|
|
176
|
+
createUserDataRightsFeature(),
|
|
177
|
+
notesFeature,
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
await unsafeCreateEntityTable(stack.db, userEntity);
|
|
181
|
+
await unsafeCreateEntityTable(stack.db, noteEntity);
|
|
182
|
+
await unsafeCreateEntityTable(stack.db, tenantRetentionOverrideEntity);
|
|
183
|
+
await unsafeCreateEntityTable(stack.db, tenantComplianceProfileEntity);
|
|
184
|
+
await asRawClient(stack.db).unsafe(`
|
|
185
|
+
CREATE TABLE IF NOT EXISTS read_tenant_memberships (
|
|
186
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
187
|
+
tenant_id UUID NOT NULL,
|
|
188
|
+
user_id TEXT NOT NULL,
|
|
189
|
+
version INTEGER NOT NULL DEFAULT 0,
|
|
190
|
+
inserted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
191
|
+
modified_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
192
|
+
inserted_by_id TEXT,
|
|
193
|
+
modified_by_id TEXT,
|
|
194
|
+
is_deleted BOOLEAN NOT NULL DEFAULT false,
|
|
195
|
+
deleted_at TIMESTAMPTZ,
|
|
196
|
+
deleted_by_id TEXT,
|
|
197
|
+
roles TEXT NOT NULL DEFAULT '[]',
|
|
198
|
+
UNIQUE(user_id, tenant_id)
|
|
199
|
+
)
|
|
200
|
+
`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
afterAll(async () => {
|
|
204
|
+
await stack.cleanup();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
beforeEach(async () => {
|
|
208
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM store_notes`);
|
|
209
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
|
|
210
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_tenant_memberships`);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
async function seedAlice(opts: { gracePeriodEnd?: Instant; status?: string } = {}): Promise<void> {
|
|
214
|
+
await seedRow(stack.db, userTable, {
|
|
215
|
+
id: ALICE_ID,
|
|
216
|
+
tenantId: TENANT_SYSTEM,
|
|
217
|
+
email: "alice@recipe.test",
|
|
218
|
+
passwordHash: "h",
|
|
219
|
+
displayName: "Alice",
|
|
220
|
+
locale: "de",
|
|
221
|
+
emailVerified: true,
|
|
222
|
+
roles: '["Member"]',
|
|
223
|
+
status: opts.status ?? USER_STATUS.Active,
|
|
224
|
+
gracePeriodEnd: opts.gracePeriodEnd ?? null,
|
|
225
|
+
});
|
|
226
|
+
await asRawClient(stack.db).unsafe(
|
|
227
|
+
`
|
|
228
|
+
INSERT INTO read_tenant_memberships (tenant_id, user_id, roles)
|
|
229
|
+
VALUES ($1, $2, '["Member"]')
|
|
230
|
+
ON CONFLICT DO NOTHING
|
|
231
|
+
`,
|
|
232
|
+
[TENANT_A, ALICE_ID],
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function seedNote(id: string, title: string): Promise<void> {
|
|
237
|
+
await insertOne(stack.db, notesTable, {
|
|
238
|
+
id,
|
|
239
|
+
tenantId: TENANT_A,
|
|
240
|
+
authorId: ALICE_ID,
|
|
241
|
+
title,
|
|
242
|
+
body: `body for ${title}`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
describe("user-data-rights recipe :: EXT_USER_DATA-Hooks integrieren Notes-Domain", () => {
|
|
247
|
+
test("Export-Bundle enthaelt Note-Snippet via Hook", async () => {
|
|
248
|
+
await seedAlice();
|
|
249
|
+
await seedNote("aaaaaaaa-aaaa-4aaa-8aaa-000000000001", "first");
|
|
250
|
+
await seedNote("aaaaaaaa-aaaa-4aaa-8aaa-000000000002", "second");
|
|
251
|
+
|
|
252
|
+
const bundle = await runUserExport({
|
|
253
|
+
db: stack.db,
|
|
254
|
+
registry: stack.registry,
|
|
255
|
+
userId: ALICE_ID,
|
|
256
|
+
now: NOW(),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
const tenantA = bundle.tenants.find((t) => t.tenantId === TENANT_A);
|
|
260
|
+
const noteSnippet = tenantA?.entities.find((e) => e.entity === "note");
|
|
261
|
+
expect(noteSnippet?.rows).toHaveLength(2);
|
|
262
|
+
expect((noteSnippet?.rows ?? []).map((r) => String(r["title"])).sort()).toEqual([
|
|
263
|
+
"first",
|
|
264
|
+
"second",
|
|
265
|
+
]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("Forget-Cron strategy=delete entfernt Notes (Default-Pfad)", async () => {
|
|
269
|
+
await seedAlice({ status: USER_STATUS.DeletionRequested, gracePeriodEnd: PAST() });
|
|
270
|
+
await seedNote("aaaaaaaa-aaaa-4aaa-8aaa-000000000003", "to-delete");
|
|
271
|
+
|
|
272
|
+
const result = await runForgetCleanup({
|
|
273
|
+
db: stack.db,
|
|
274
|
+
registry: stack.registry,
|
|
275
|
+
now: NOW(),
|
|
276
|
+
});
|
|
277
|
+
expect(result.processedUserIds).toContain(ALICE_ID);
|
|
278
|
+
|
|
279
|
+
const remaining = await asRawClient(stack.db).unsafe(`SELECT id FROM store_notes`);
|
|
280
|
+
expect(remaining.length).toBe(0);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("Strategy=anonymize: authorId=null, Row bleibt (HR-Compliance-Pfad)", async () => {
|
|
284
|
+
await seedAlice();
|
|
285
|
+
await seedNote("aaaaaaaa-aaaa-4aaa-8aaa-000000000004", "stays-anonymous");
|
|
286
|
+
|
|
287
|
+
const usage = stack.registry
|
|
288
|
+
.getExtensionUsages(EXT_USER_DATA)
|
|
289
|
+
.find((u) => u.entityName === "note");
|
|
290
|
+
const hooks = usage?.options as
|
|
291
|
+
| {
|
|
292
|
+
delete?: (
|
|
293
|
+
ctx: { db: typeof stack.db; tenantId: string; userId: string },
|
|
294
|
+
strategy: "delete" | "anonymize",
|
|
295
|
+
) => Promise<void>;
|
|
296
|
+
}
|
|
297
|
+
| undefined;
|
|
298
|
+
await hooks?.delete?.({ db: stack.db, tenantId: TENANT_A, userId: ALICE_ID }, "anonymize");
|
|
299
|
+
|
|
300
|
+
const after = (await asRawClient(stack.db).unsafe(
|
|
301
|
+
`SELECT title, author_id FROM store_notes`,
|
|
302
|
+
)) as Array<{ title: string; author_id: string | null }>;
|
|
303
|
+
expect(after).toHaveLength(1);
|
|
304
|
+
expect(after[0]?.author_id).toBeNull();
|
|
305
|
+
expect(after[0]?.title).toBe("stays-anonymous");
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### src/feature.ts
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
// User-Data-Rights — Recipe
|
|
314
|
+
//
|
|
315
|
+
// Wie eine App-Domain DSGVO-Pipeline integriert: pro Entity einen
|
|
316
|
+
// (export, delete)-Hook ueber EXT_USER_DATA. Forget-Cron, Export-ZIP-Bau
|
|
317
|
+
// und Magic-Link-Versand kommen vollstaendig aus user-data-rights —
|
|
318
|
+
// App-Author schreibt nur die zwei Hooks pro Entity.
|
|
319
|
+
//
|
|
320
|
+
// Demo-Domain: minimaler Note-Service. Note hat (id, tenantId, authorId,
|
|
321
|
+
// title, body). Pinst:
|
|
322
|
+
// 1. Hook export liefert Note-Rows als JSON-Snippet ins Export-Bundle.
|
|
323
|
+
// 2. Hook delete entfernt Note-Rows beim Forget-Cleanup-Cron.
|
|
324
|
+
//
|
|
325
|
+
// Was nicht im Recipe ist (siehe samples/apps/user-data-rights-demo
|
|
326
|
+
// fuer eine vollstaendige App):
|
|
327
|
+
// - Strategy-aware delete (anonymize vs hardDelete)
|
|
328
|
+
// - HTTP-Endpoints fuer create/list
|
|
329
|
+
// - Compliance-Profile-Wiring + Cron-Scheduling
|
|
330
|
+
|
|
331
|
+
import { deleteMany, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
332
|
+
import { deriveEntityTableMeta } from "@cosmicdrift/kumiko-framework/db";
|
|
333
|
+
import {
|
|
334
|
+
createEntity,
|
|
335
|
+
createTextField,
|
|
336
|
+
defineFeature,
|
|
337
|
+
EXT_USER_DATA,
|
|
338
|
+
type UserDataDeleteHook,
|
|
339
|
+
type UserDataExportHook,
|
|
340
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
341
|
+
|
|
342
|
+
export const noteEntity = createEntity({
|
|
343
|
+
table: "store_notes",
|
|
344
|
+
idType: "uuid",
|
|
345
|
+
fields: {
|
|
346
|
+
authorId: createTextField({ personal: "ref" }),
|
|
347
|
+
title: createTextField({
|
|
348
|
+
personal: false,
|
|
349
|
+
reason: "is_business_data",
|
|
350
|
+
required: true,
|
|
351
|
+
maxLength: 200,
|
|
352
|
+
}),
|
|
353
|
+
// Plaintext on purpose: store_notes is an unmanaged direct-write store, so the
|
|
354
|
+
// PII executor never encrypts it. Art. 17 runs through the EXT_USER_DATA
|
|
355
|
+
// hook below instead (deletes the author's rows, or unlinks them on anonymize).
|
|
356
|
+
body: createTextField({
|
|
357
|
+
personal: false,
|
|
358
|
+
reason: "covered_by_user_data_hook",
|
|
359
|
+
maxLength: 4000,
|
|
360
|
+
}),
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Plain EntityTableMeta, NOT a branded EntityTable: store_notes is a deliberate
|
|
365
|
+
// unmanaged direct-write store (r.storeTable below), so the forget hook may
|
|
366
|
+
// updateMany/deleteMany it directly — the meta carries no executor-only brand.
|
|
367
|
+
export const notesTable = deriveEntityTableMeta("note", noteEntity, { source: "unmanaged" });
|
|
368
|
+
|
|
369
|
+
export const notesFeature = defineFeature("notes", (r) => {
|
|
370
|
+
r.requires("user-data-rights");
|
|
371
|
+
// store_notes is a direct-write store: the forget hook below `updateMany`/
|
|
372
|
+
// `deleteMany`s rows WITHOUT emitting lifecycle events. r.entity would make
|
|
373
|
+
// it a rebuildable implicit projection whose replay finds zero note events
|
|
374
|
+
// and wipes the table (or un-forgets anonymized rows) on the next rebuild
|
|
375
|
+
// (#498). r.storeTable keeps the DDL, opts out of implicit rebuild.
|
|
376
|
+
r.storeTable(notesTable, {
|
|
377
|
+
reason: "read_side.notes_direct_write",
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const exportNotes: UserDataExportHook = async (ctx) => {
|
|
381
|
+
const rows = await selectMany(ctx.db, notesTable, {
|
|
382
|
+
tenantId: ctx.tenantId,
|
|
383
|
+
authorId: ctx.userId,
|
|
384
|
+
});
|
|
385
|
+
if (rows.length === 0) return null;
|
|
386
|
+
return {
|
|
387
|
+
entity: "note",
|
|
388
|
+
rows: rows.map((row) => ({
|
|
389
|
+
id: String(row.id),
|
|
390
|
+
title: row.title ?? "",
|
|
391
|
+
body: row.body ?? "",
|
|
392
|
+
})),
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const deleteNotes: UserDataDeleteHook = async (ctx, strategy) => {
|
|
397
|
+
const where = { tenantId: ctx.tenantId, authorId: ctx.userId };
|
|
398
|
+
if (strategy === "anonymize") {
|
|
399
|
+
await updateMany(ctx.db, notesTable, { authorId: null }, where);
|
|
400
|
+
} else {
|
|
401
|
+
await deleteMany(ctx.db, notesTable, where);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
r.useExtension(EXT_USER_DATA, "note", {
|
|
406
|
+
export: exportNotes,
|
|
407
|
+
delete: deleteNotes,
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
```
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/user-profile. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# user-profile
|
|
4
|
+
|
|
5
|
+
user-profile bundled feature — Self-Service-Kontoseite (Passwort/E-Mail ändern, Konto löschen) als App-Screen verdrahten.
|
|
6
|
+
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# user-profile
|
|
10
|
+
|
|
11
|
+
Self-service account page as a bundled feature: change password, change email
|
|
12
|
+
(with re-auth + verification reset), and request or cancel account deletion
|
|
13
|
+
(with grace period from `user-data-rights`).
|
|
14
|
+
|
|
15
|
+
The bundled feature ships the `profile` screen itself (a declarative
|
|
16
|
+
`projectionDetail`, fw#2312) plus handlers and i18n. **Your app** only points
|
|
17
|
+
a nav entry at it and registers the two extension-section components
|
|
18
|
+
(change-password, change-email — re-auth flows a declarative action can't
|
|
19
|
+
express) — this recipe shows the wiring.
|
|
20
|
+
|
|
21
|
+
## What it shows
|
|
22
|
+
|
|
23
|
+
- **`user-profile:write:change-email`** — re-authenticated email change;
|
|
24
|
+
resets `emailVerified` until the user confirms the new address.
|
|
25
|
+
- **`auth-email-password:write:change-password`** — used from the same
|
|
26
|
+
screen; lives on `auth-email-password`, not duplicated in user-profile.
|
|
27
|
+
- **Account deletion** — `user-data-rights` handlers for request / cancel
|
|
28
|
+
deletion; grace period comes from the tenant's compliance profile.
|
|
29
|
+
- **App-side nav wiring only** — `r.nav({ screen: "user-profile:screen:profile", ... })`;
|
|
30
|
+
no `r.screen()` registration needed, the bundled feature owns the screen.
|
|
31
|
+
- **Full require chain** — `user` → `tenant` → `auth-email-password` →
|
|
32
|
+
`user-data-rights` (which pulls `data-retention`, `compliance-profiles`,
|
|
33
|
+
`sessions`). `composeAccountApp()` mounts everything boot needs.
|
|
34
|
+
|
|
35
|
+
## Feature composition
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
user → cross-tenant identity (email, roles, status)
|
|
39
|
+
tenant → memberships + tenant-scoped roles
|
|
40
|
+
auth-email-password → login + change-password handlers
|
|
41
|
+
data-retention → retention policies (via user-data-rights)
|
|
42
|
+
compliance-profiles → region profiles for grace periods
|
|
43
|
+
sessions → revocable JWTs (via user-data-rights chain)
|
|
44
|
+
user-data-rights → deletion request / export pipeline
|
|
45
|
+
user-profile → declarative `profile` screen + change-email handler
|
|
46
|
+
account → this recipe: nav wiring only
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Client wiring
|
|
50
|
+
|
|
51
|
+
Register the two extension-section components once at app boot (the screen
|
|
52
|
+
itself needs no client registration):
|
|
53
|
+
|
|
54
|
+
```tsx illustration
|
|
55
|
+
import { userProfileClient } from "@cosmicdrift/kumiko-bundled-features/user-profile/web";
|
|
56
|
+
import { emailPasswordClient } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";
|
|
57
|
+
import { createKumikoApp } from "@cosmicdrift/kumiko-renderer-web";
|
|
58
|
+
|
|
59
|
+
createKumikoApp({
|
|
60
|
+
clientFeatures: [emailPasswordClient(), userProfileClient()],
|
|
61
|
+
// ... schema, nav from server features
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Flow
|
|
66
|
+
|
|
67
|
+
1. User opens the profile nav entry → renderer loads the declarative `profile`
|
|
68
|
+
screen.
|
|
69
|
+
2. Password change dispatches `auth-email-password:write:change-password`.
|
|
70
|
+
3. Email change dispatches `user-profile:write:change-email` (re-auth required).
|
|
71
|
+
4. Delete account dispatches `user-data-rights:write:request-deletion`; cancel
|
|
72
|
+
uses the matching cancel handler during the grace window.
|
|
73
|
+
|
|
74
|
+
## Tests
|
|
75
|
+
|
|
76
|
+
Boot validation only — no DB/HTTP in this recipe (HTTP proof lives in the
|
|
77
|
+
bundled-feature integration tests):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bun test src/__tests__/feature.test.ts
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The test asserts `validateBoot(composeAccountApp())` passes, the profile
|
|
84
|
+
screen/nav are registered, and the documented change-email qualified name
|
|
85
|
+
matches `UserProfileHandlers.changeEmail`.
|
|
86
|
+
|
|
87
|
+
## Related samples
|
|
88
|
+
|
|
89
|
+
- [apex-surface-auth](/en/samples/recipes-apex-surface-auth/) — public login/
|
|
90
|
+
signup on the marketing apex (no admin nav).
|
|
91
|
+
- [user-data-rights](/en/samples/recipes-user-data-rights/) — `EXT_USER_DATA`
|
|
92
|
+
hooks for export/forget on your domain entities.
|
|
93
|
+
- [apps-user-data-rights-demo](/en/samples/apps-user-data-rights-demo/) —
|
|
94
|
+
full runnable app with todos + export ZIP.
|
|
95
|
+
|
|
96
|
+
## Source files
|
|
97
|
+
|
|
98
|
+
### src/__tests__/feature.test.ts
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// Boot-Validation der vollen user-profile-Komposition: Require-Kette
|
|
102
|
+
// erfüllt, Screen + Nav registriert, change-email-Handler unter der
|
|
103
|
+
// dokumentierten QN. Kein DB/HTTP nötig — der HTTP-Beweis lebt im
|
|
104
|
+
// bundled-feature-Test (change-email.integration.test.ts).
|
|
105
|
+
|
|
106
|
+
import { describe, expect, test } from "bun:test";
|
|
107
|
+
import { UserProfileHandlers } from "@cosmicdrift/kumiko-bundled-features/user-profile";
|
|
108
|
+
import {
|
|
109
|
+
createRegistry,
|
|
110
|
+
validateBoot as validateBootRaw,
|
|
111
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
112
|
+
import { withBootValidatorFixture } from "@cosmicdrift/kumiko-framework/testing";
|
|
113
|
+
import { composeAccountApp } from "../feature";
|
|
114
|
+
|
|
115
|
+
function validateBoot(features: Parameters<typeof validateBootRaw>[0]): void {
|
|
116
|
+
validateBootRaw(withBootValidatorFixture(features));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const features = composeAccountApp();
|
|
120
|
+
|
|
121
|
+
describe("user-profile recipe — boot validation", () => {
|
|
122
|
+
test("validateBoot accepts the full composition (require chain satisfied)", () => {
|
|
123
|
+
expect(() => validateBoot(features)).not.toThrow();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("profile screen + nav registered with qualified ids", () => {
|
|
127
|
+
const registry = createRegistry(features);
|
|
128
|
+
expect(registry.getScreen("user-profile:screen:profile")?.id).toBe(
|
|
129
|
+
"user-profile:screen:profile",
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("user-profile deklariert seine Require-Kette (Manifest-Quelle)", () => {
|
|
134
|
+
const userProfile = features.find((f) => f.name === "user-profile");
|
|
135
|
+
if (!userProfile) throw new Error("user-profile feature missing in composition");
|
|
136
|
+
expect(userProfile.requires).toEqual([
|
|
137
|
+
"user",
|
|
138
|
+
"auth-email-password",
|
|
139
|
+
"user-data-rights",
|
|
140
|
+
"user-data-rights-defaults",
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("change-email QN entspricht der dokumentierten Konstante", () => {
|
|
145
|
+
expect(UserProfileHandlers.changeEmail).toBe("user-profile:write:change-email");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### src/feature.ts
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// user-profile Recipe — shows the app-side wiring for the self-service
|
|
154
|
+
// account page: the bundled feature ships the screen itself (fw#2312:
|
|
155
|
+
// declarative `projectionDetail`, id "profile") including handler + i18n;
|
|
156
|
+
// the app only has to navigate to it, no own `r.screen()` registration
|
|
157
|
+
// anymore (unlike before #2312 — see user-data-rights' recipe/demo pattern:
|
|
158
|
+
// `r.nav({ screen: "<feature>:screen:<id>", ... })`).
|
|
159
|
+
//
|
|
160
|
+
// Client-side, the app only registers the two extension-section components
|
|
161
|
+
// (change-password/change-email stay React, re-auth):
|
|
162
|
+
// import { userProfileClient } from
|
|
163
|
+
// "@cosmicdrift/kumiko-bundled-features/user-profile/web";
|
|
164
|
+
// createKumikoApp({
|
|
165
|
+
// clientFeatures: [emailPasswordClient(), userProfileClient()],
|
|
166
|
+
// })
|
|
167
|
+
|
|
168
|
+
import { createAuthEmailPasswordFeature } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
|
|
169
|
+
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
170
|
+
import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
|
|
171
|
+
import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
|
|
172
|
+
import { createDataRetentionFeature } from "@cosmicdrift/kumiko-bundled-features/data-retention";
|
|
173
|
+
import { createFilesFeature } from "@cosmicdrift/kumiko-bundled-features/files";
|
|
174
|
+
import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
|
|
175
|
+
import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
176
|
+
import { createTenantFeature } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
177
|
+
import { createUserFeature } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
178
|
+
import { createUserDataRightsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
|
|
179
|
+
import { createUserDataRightsDefaultsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights-defaults";
|
|
180
|
+
import { createUserProfileFeature } from "@cosmicdrift/kumiko-bundled-features/user-profile";
|
|
181
|
+
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
182
|
+
|
|
183
|
+
export function createAccountFeature(): FeatureDefinition {
|
|
184
|
+
return defineFeature("account", (r) => {
|
|
185
|
+
r.describe(
|
|
186
|
+
"App-side wiring for the user-profile bundled feature: just the nav " +
|
|
187
|
+
"entry — user-profile registers the `profile` screen itself " +
|
|
188
|
+
"(fw#2312 declarative projectionDetail).",
|
|
189
|
+
);
|
|
190
|
+
r.requires("user-profile");
|
|
191
|
+
|
|
192
|
+
r.nav({
|
|
193
|
+
id: "profile",
|
|
194
|
+
label: "account:nav.profile",
|
|
195
|
+
icon: "user",
|
|
196
|
+
screen: "user-profile:screen:profile",
|
|
197
|
+
order: 90,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return {};
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Volle Feature-Komposition inkl. der user-profile-Require-Kette
|
|
205
|
+
* (user-data-rights → data-retention + compliance-profiles + sessions). */
|
|
206
|
+
export function composeAccountApp(): FeatureDefinition[] {
|
|
207
|
+
return [
|
|
208
|
+
createConfigFeature(),
|
|
209
|
+
createUserFeature(),
|
|
210
|
+
createTenantFeature(),
|
|
211
|
+
createAuthEmailPasswordFeature(),
|
|
212
|
+
createDataRetentionFeature(),
|
|
213
|
+
createComplianceProfilesFeature(),
|
|
214
|
+
authFoundationFeature,
|
|
215
|
+
createPersonalAccessTokensFeature({ scopes: {} }),
|
|
216
|
+
createSessionsFeature(),
|
|
217
|
+
createFilesFeature(),
|
|
218
|
+
createUserDataRightsFeature(),
|
|
219
|
+
// registers the default export/erase hooks for core PII entities (user,
|
|
220
|
+
// fileRef, folder) so the GDPR boot gate (V3) is satisfied for the stack.
|
|
221
|
+
createUserDataRightsDefaultsFeature(),
|
|
222
|
+
createUserProfileFeature(),
|
|
223
|
+
createAccountFeature(),
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
```
|