@cosmicdrift/kumiko-samples 0.365.0 → 0.366.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/package.json +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/admin-shell/feature.ts +9 -9
- package/packages/bundled-features/src/agent-tools/changes.json +6 -0
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +87 -5
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +13 -0
- package/packages/bundled-features/src/auth-email-password/constants.ts +0 -2
- package/packages/bundled-features/src/auth-email-password/feature.ts +3 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +1 -0
- package/packages/bundled-features/src/auth-mfa/feature.ts +4 -4
- package/packages/bundled-features/src/auth-mfa-user-data/index.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +89 -28
- package/packages/bundled-features/src/cap-counter/changes.json +7 -0
- package/packages/bundled-features/src/cap-counter/constants.ts +3 -0
- package/packages/bundled-features/src/cap-counter/feature.ts +2 -2
- package/packages/bundled-features/src/cap-counter/tables.ts +5 -2
- package/packages/bundled-features/src/cap-overview/feature.ts +3 -3
- package/packages/bundled-features/src/channel-discord/feature.ts +2 -2
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/channel-in-app/feature.ts +1 -1
- package/packages/bundled-features/src/channel-push/feature.ts +1 -1
- package/packages/bundled-features/src/channel-slack/feature.ts +2 -2
- package/packages/bundled-features/src/channel-teams/feature.ts +2 -2
- package/packages/bundled-features/src/channel-telegram/feature.ts +2 -2
- package/packages/bundled-features/src/compliance-profiles/feature.ts +1 -1
- package/packages/bundled-features/src/compliance-profiles-ops/index.ts +3 -3
- package/packages/bundled-features/src/config/feature.ts +1 -1
- package/packages/bundled-features/src/crypto-shredding/changes.json +13 -0
- package/packages/bundled-features/src/custom-fields/feature.ts +2 -2
- package/packages/bundled-features/src/custom-fields/wire-for-entity.ts +17 -13
- package/packages/bundled-features/src/data-retention/feature.ts +1 -1
- package/packages/bundled-features/src/data-retention/presets.ts +4 -4
- package/packages/bundled-features/src/delivery/changes.json +6 -0
- package/packages/bundled-features/src/delivery/db/queries/address-opt-outs.ts +6 -2
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +4 -4
- package/packages/bundled-features/src/delivery/delivery-service.ts +17 -2
- package/packages/bundled-features/src/delivery/feature.ts +9 -5
- package/packages/bundled-features/src/delivery/handlers/resubscribe-address.write.ts +12 -8
- package/packages/bundled-features/src/delivery/handlers/resubscribe-user.write.ts +3 -1
- package/packages/bundled-features/src/delivery/handlers/set-preference.write.ts +3 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-address.write.ts +3 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-user.write.ts +3 -1
- package/packages/bundled-features/src/delivery/notification-type-aliases.ts +13 -0
- package/packages/bundled-features/src/derivatives-sharp/feature.ts +1 -1
- package/packages/bundled-features/src/document-ingest-foundation/feature.ts +2 -2
- package/packages/bundled-features/src/file-derivatives/feature.ts +3 -3
- package/packages/bundled-features/src/file-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/file-provider-inmemory/feature.ts +2 -2
- package/packages/bundled-features/src/file-provider-s3/feature.ts +3 -3
- package/packages/bundled-features/src/file-provider-s3-env/feature.ts +2 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/files-tenant-data/index.ts +1 -1
- package/packages/bundled-features/src/folders/feature.ts +2 -2
- package/packages/bundled-features/src/folders-user-data/index.ts +1 -1
- package/packages/bundled-features/src/form-draft/feature.ts +2 -2
- package/packages/bundled-features/src/form-draft-user-data/index.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +13 -0
- package/packages/bundled-features/src/inbound-mail-foundation/connect-routes.ts +9 -1
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +19 -7
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +21 -2
- package/packages/bundled-features/src/inbound-provider-imap/feature.ts +2 -2
- package/packages/bundled-features/src/inbound-provider-inmemory/feature.ts +1 -1
- package/packages/bundled-features/src/ledger/feature.ts +5 -5
- package/packages/bundled-features/src/legal-pages/feature.ts +1 -1
- package/packages/bundled-features/src/mail-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/mail-transport-inmemory/feature.ts +2 -2
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +3 -3
- package/packages/bundled-features/src/managed-pages/css-gate.ts +1 -1
- package/packages/bundled-features/src/managed-pages/feature.ts +2 -2
- package/packages/bundled-features/src/metrics/feature.ts +2 -2
- package/packages/bundled-features/src/notes-history/feature.ts +2 -2
- package/packages/bundled-features/src/notes-history-user-data/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +15 -11
- package/packages/bundled-features/src/readiness/feature.ts +2 -2
- package/packages/bundled-features/src/renderer-foundation/README.md +1 -1
- package/packages/bundled-features/src/renderer-foundation/feature.ts +1 -1
- package/packages/bundled-features/src/renderer-pdf/feature.ts +1 -1
- package/packages/bundled-features/src/secrets/feature.ts +1 -1
- package/packages/bundled-features/src/seo/feature.ts +1 -1
- package/packages/bundled-features/src/sessions/feature.ts +17 -13
- package/packages/bundled-features/src/step-dispatcher/changes.json +8 -1
- package/packages/bundled-features/src/step-dispatcher/constants.ts +22 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +236 -0
- package/packages/bundled-features/src/step-dispatcher/feature.ts +66 -119
- package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +40 -14
- package/packages/bundled-features/src/subscription-mollie/feature.ts +1 -1
- package/packages/bundled-features/src/subscription-stripe/feature.ts +3 -3
- package/packages/bundled-features/src/tags/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
- package/packages/bundled-features/src/template-resolver-user-data/index.ts +1 -1
- package/packages/bundled-features/src/tenant/feature.ts +4 -4
- package/packages/bundled-features/src/tenant/seeding.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/feature.ts +2 -3
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +6 -1
- package/packages/bundled-features/src/tenant-settings/feature.ts +1 -1
- package/packages/bundled-features/src/tier-engine/feature.ts +36 -27
- package/packages/bundled-features/src/user/feature.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/README.md +1 -1
- package/packages/bundled-features/src/user-data-rights/feature.ts +4 -4
- package/packages/bundled-features/src/user-data-rights-defaults/feature.ts +2 -2
- package/packages/bundled-features/src/user-profile/feature.ts +4 -4
- package/packages/bundled-features/src/waitlist/feature.ts +2 -2
- package/samples/apps/cap-billing-demo/README.md +1 -1
- package/samples/apps/cap-billing-demo/src/feature.ts +4 -4
- package/samples/apps/cap-billing-demo/src/run-config.ts +1 -1
- package/samples/apps/config-demo/src/features/demo/feature.ts +2 -2
- package/samples/apps/styleguide/src/features/ai-settings/feature.ts +43 -28
- package/samples/apps/styleguide/src/features/demo/feature.ts +18 -18
- package/samples/apps/styleguide/src/features/widgets/ai-text-demo.ts +10 -12
- package/samples/apps/styleguide/src/features/widgets/feature.ts +90 -108
- package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +36 -36
- package/samples/apps/ui-walkthrough/src/features/vehicles/feature.ts +6 -6
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +2 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +1 -1
- package/samples/apps/use-all-bundled/scripts/gen-sample-index.ts +7 -7
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +3 -3
- package/samples/apps/use-all-bundled/src/app/screens-feature.ts +1 -1
- package/samples/apps/user-data-rights-demo/src/feature.ts +5 -5
- package/samples/recipes/access-control/src/feature.ts +21 -25
- package/samples/recipes/anonymous-access/src/feature.ts +36 -43
- package/samples/recipes/anonymous-access-multitenant/src/feature.ts +14 -16
- package/samples/recipes/auth-foundation-providers/src/feature.ts +1 -1
- package/samples/recipes/basic-entity/README.md +1 -1
- package/samples/recipes/boot-check/README.md +1 -1
- package/samples/recipes/boot-check/src/feature.ts +2 -2
- package/samples/recipes/chat-channels/src/feature.ts +1 -1
- package/samples/recipes/cross-feature-events/src/feature.ts +7 -7
- package/samples/recipes/currencies-global/src/feature.ts +3 -3
- package/samples/recipes/currencies-per-tenant/src/feature.ts +2 -2
- package/samples/recipes/custom-fields-basic/README.md +1 -1
- package/samples/recipes/custom-fields-basic/src/feature.ts +3 -2
- package/samples/recipes/custom-handlers/README.md +3 -3
- package/samples/recipes/custom-handlers/src/feature.ts +37 -41
- package/samples/recipes/dashboard-stats/src/feature.ts +6 -6
- package/samples/recipes/delivery-notifications/README.md +1 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +8 -8
- package/samples/recipes/designer-demo/src/feature.ts +2 -1
- package/samples/recipes/embedded/src/feature.ts +1 -1
- package/samples/recipes/embedded-entity-form/src/feature.ts +13 -13
- package/samples/recipes/encrypted-tenant-config/src/feature.ts +1 -1
- package/samples/recipes/error-contract/src/feature.ts +25 -25
- package/samples/recipes/event-sourcing/src/feature.ts +70 -76
- package/samples/recipes/feature-toggles/src/feature.ts +28 -20
- package/samples/recipes/files-post-processing/src/feature.ts +22 -26
- package/samples/recipes/idempotency/src/feature.ts +16 -18
- package/samples/recipes/inbound-mail-triage/src/feature.ts +1 -1
- package/samples/recipes/lane-routing/src/feature.ts +10 -12
- package/samples/recipes/legal-pages/src/feature.ts +1 -1
- package/samples/recipes/lifecycle-hooks/README.md +4 -4
- package/samples/recipes/lifecycle-hooks/src/feature.ts +55 -35
- package/samples/recipes/managed-config/src/feature.ts +8 -8
- package/samples/recipes/managed-pages/src/feature.ts +1 -1
- package/samples/recipes/notes-history-basic/README.md +1 -1
- package/samples/recipes/notes-history-basic/src/feature.ts +2 -2
- package/samples/recipes/pdf-audit-export/src/feature.ts +7 -7
- package/samples/recipes/pipeline-basics/README.md +1 -1
- package/samples/recipes/pipeline-basics/src/feature.ts +7 -7
- package/samples/recipes/public-share-token/src/feature.ts +1 -1
- package/samples/recipes/rate-limiting/src/feature.ts +7 -9
- package/samples/recipes/record-detail-layout/src/feature.ts +18 -18
- package/samples/recipes/reference-data/src/feature.ts +1 -1
- package/samples/recipes/relations/src/feature.ts +2 -2
- package/samples/recipes/screens-nav/src/feature.ts +1 -1
- package/samples/recipes/search/src/feature.ts +17 -19
- package/samples/recipes/security-baseline/README.md +1 -1
- package/samples/recipes/state-machine/src/feature.ts +1 -1
- package/samples/recipes/tags-basic/README.md +1 -1
- package/samples/recipes/tags-basic/src/feature.ts +2 -2
- package/samples/recipes/tenant-settings/README.md +1 -1
- package/samples/recipes/tenant-settings/src/feature.ts +1 -1
- package/samples/recipes/timezones/src/feature.ts +12 -12
- package/samples/recipes/user-data-rights/README.md +2 -2
- package/samples/recipes/user-data-rights/src/feature.ts +1 -1
- package/samples/recipes/user-profile/src/feature.ts +1 -1
- package/samples/recipes/webhook-step/src/feature.ts +1 -1
- package/samples/recipes/writeform-section/src/feature.ts +12 -12
|
@@ -21,8 +21,8 @@ export function createChannelSlackFeature(options: SlackChannelOptions = {}): Fe
|
|
|
21
21
|
category: "notifications",
|
|
22
22
|
recommended: false,
|
|
23
23
|
});
|
|
24
|
-
r.requires("delivery");
|
|
25
|
-
r.requires("secrets");
|
|
24
|
+
r.requires({ features: ["delivery"] });
|
|
25
|
+
r.requires({ features: ["secrets"] });
|
|
26
26
|
|
|
27
27
|
const webhookTarget = resolveChatWebhookTarget(
|
|
28
28
|
{ defaultAllowedHosts: SLACK_DEFAULT_ALLOWED_HOSTS },
|
|
@@ -21,8 +21,8 @@ export function createChannelTeamsFeature(options: TeamsChannelOptions = {}): Fe
|
|
|
21
21
|
category: "notifications",
|
|
22
22
|
recommended: false,
|
|
23
23
|
});
|
|
24
|
-
r.requires("delivery");
|
|
25
|
-
r.requires("secrets");
|
|
24
|
+
r.requires({ features: ["delivery"] });
|
|
25
|
+
r.requires({ features: ["secrets"] });
|
|
26
26
|
|
|
27
27
|
const webhookTarget = resolveChatWebhookTarget(
|
|
28
28
|
{ defaultAllowedHosts: TEAMS_DEFAULT_ALLOWED_HOSTS },
|
|
@@ -19,8 +19,8 @@ export function createChannelTelegramFeature(
|
|
|
19
19
|
category: "notifications",
|
|
20
20
|
recommended: false,
|
|
21
21
|
});
|
|
22
|
-
r.requires("delivery");
|
|
23
|
-
r.requires("secrets");
|
|
22
|
+
r.requires({ features: ["delivery"] });
|
|
23
|
+
r.requires({ features: ["secrets"] });
|
|
24
24
|
|
|
25
25
|
const botToken = r.secret("botToken", {
|
|
26
26
|
label: { en: "Telegram bot token" },
|
|
@@ -63,7 +63,7 @@ export function createComplianceProfilesFeature(
|
|
|
63
63
|
// Context, Profile-Selection ist eigene Entity, sub-processor-Liste
|
|
64
64
|
// sind Constants. Wenn S1.4+ Cross-Feature-Reads dazukommen, kommt
|
|
65
65
|
// r.requires hier rein.
|
|
66
|
-
r.entity("tenant-compliance-profile", tenantComplianceProfileEntity);
|
|
66
|
+
r.entity({ name: "tenant-compliance-profile", ...tenantComplianceProfileEntity });
|
|
67
67
|
|
|
68
68
|
r.exposesApi("compliance.forTenant");
|
|
69
69
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// a separate feature (mirrors folders-user-data/notes-history-user-data)
|
|
8
8
|
// rather than folded into compliance-profiles: this is the one genuinely
|
|
9
9
|
// cross-tenant capability in the picker's orbit, and it alone needs
|
|
10
|
-
// r.systemScope() + a hard r.requires("tenant") — apps that only want the
|
|
10
|
+
// r.systemScope() + a hard r.requires({ features: ["tenant"] }) — apps that only want the
|
|
11
11
|
// per-tenant picker (the vast majority) stay unaffected.
|
|
12
12
|
|
|
13
13
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -23,8 +23,8 @@ export const complianceProfilesOpsFeature = defineFeature("compliance-profiles-o
|
|
|
23
23
|
recommended: false,
|
|
24
24
|
});
|
|
25
25
|
r.systemScope();
|
|
26
|
-
r.requires("compliance-profiles");
|
|
27
|
-
r.requires("tenant");
|
|
26
|
+
r.requires({ features: ["compliance-profiles"] });
|
|
27
|
+
r.requires({ features: ["tenant"] });
|
|
28
28
|
|
|
29
29
|
const queries = {
|
|
30
30
|
tenantsMissingProfile: r.queryHandler(tenantsMissingProfileQuery),
|
|
@@ -45,7 +45,7 @@ export function createConfigFeature(): FeatureDefinition {
|
|
|
45
45
|
// projection write in one TX. Subscribers that need config-change
|
|
46
46
|
// semantics listen to those auto-events via r.multiStreamProjection
|
|
47
47
|
// (see docs/plans/architecture/event-sourcing-pivot.md §4.7).
|
|
48
|
-
r.entity("config-value", configValueEntity);
|
|
48
|
+
r.entity({ name: "config-value", ...configValueEntity });
|
|
49
49
|
|
|
50
50
|
const handlers = {
|
|
51
51
|
set: r.writeHandler(setWrite),
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.366.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "forget-subject is covered for users stored before the KMS and users without an event stream",
|
|
6
|
+
"detail": "Tests now cover forget-subject for users stored as plaintext before the KMS was configured and for users without an event stream. Both succeed since the KMS tombstone fix, close the login door and write the audit event."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.1",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "eraseKey leaves a tombstone for subjects that never had a key",
|
|
12
|
+
"detail": "eraseKey now records an erased entry even when the subject has no key (pre-KMS plaintext). A later createKey or write for that subject fails with subject_erased instead of minting a fresh key and re-encrypting the plaintext. Erase flows must write with writePurpose erasure after eraseKey. kumiko_subject_keys now gets one tombstone row per forgotten record subject and per step dispatch. The tenant destruction stage tenant-row now fails loudly when marking the tenant destroyed does not succeed.",
|
|
13
|
+
"migration": "Run backfill-pii before erasing Pre-KMS data, otherwise untouched plaintext stays readable after eraseKey."
|
|
14
|
+
},
|
|
2
15
|
{
|
|
3
16
|
"version": "0.364.0",
|
|
4
17
|
"type": "fix",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// (crypto-shredding, event ciphertext, forget pipeline).
|
|
11
11
|
//
|
|
12
12
|
// **Was diese Feature liefert (B1 + B2, 2026-05-23):**
|
|
13
|
-
// 1. r.entity("field-definition") — Definition-Storage (event-sourced).
|
|
13
|
+
// 1. r.entity({ name: "field-definition" }) — Definition-Storage (event-sourced).
|
|
14
14
|
// 2. define-tenant-field / define-system-field — RBAC write-handlers für
|
|
15
15
|
// Definition-CRUD.
|
|
16
16
|
// 3. delete-tenant-field / delete-system-field — RBAC write-handlers.
|
|
@@ -123,7 +123,7 @@ function registerCustomFields(
|
|
|
123
123
|
category: "data",
|
|
124
124
|
recommended: false,
|
|
125
125
|
});
|
|
126
|
-
r.entity("field-definition", fieldDefinitionEntity);
|
|
126
|
+
r.entity({ name: "field-definition", ...fieldDefinitionEntity });
|
|
127
127
|
|
|
128
128
|
// Event-types — qualified als "custom-fields:event:<short-name>".
|
|
129
129
|
// Returned EventDefs liefern .name als compile-time literal-typed string,
|
|
@@ -44,8 +44,8 @@ export function customFieldsField(): JsonbFieldDef {
|
|
|
44
44
|
// Consumer-side:
|
|
45
45
|
//
|
|
46
46
|
// defineFeature("property-mgmt", (r) => {
|
|
47
|
-
// r.entity("property", propertyEntity); // muss customFieldsField() haben
|
|
48
|
-
// r.requires(CUSTOM_FIELDS_FEATURE_NAME);
|
|
47
|
+
// r.entity({ name: "property", ...propertyEntity }); // muss customFieldsField() haben
|
|
48
|
+
// r.requires({ features: [CUSTOM_FIELDS_FEATURE_NAME] });
|
|
49
49
|
// wireCustomFieldsFor(r, "property", propertyTable);
|
|
50
50
|
// });
|
|
51
51
|
//
|
|
@@ -198,18 +198,22 @@ export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
|
|
|
198
198
|
|
|
199
199
|
// postQuery-hook: flatten row.customFields jsonb auf root-level der
|
|
200
200
|
// API-response. Spec-Promise Z.4 "indistinguishable von Stammfeldern".
|
|
201
|
-
r.hook(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
201
|
+
r.hook({
|
|
202
|
+
type: "postQuery",
|
|
203
|
+
target: { allOf: entityName },
|
|
204
|
+
handler: async ({ rows }) => ({
|
|
205
|
+
rows: rows.map((row) => {
|
|
206
|
+
const customFields = row["customFields"];
|
|
207
|
+
if (customFields && typeof customFields === "object" && !Array.isArray(customFields)) {
|
|
208
|
+
return {
|
|
209
|
+
...(customFields as Record<string, unknown>), // @cast-boundary db-row jsonb runtime-untyped
|
|
210
|
+
...row, // base fields win: a custom fieldKey named `id`/`name` must not shadow the real column
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return row;
|
|
214
|
+
}),
|
|
211
215
|
}),
|
|
212
|
-
})
|
|
216
|
+
});
|
|
213
217
|
|
|
214
218
|
// Search-Payload-Extension: customFields-keys flach ins Index-Doc.
|
|
215
219
|
// Anders als postQuery-hook (der ALLE keys merged) trägt der Search-
|
|
@@ -65,7 +65,7 @@ export function createDataRetentionFeature(): FeatureDefinition {
|
|
|
65
65
|
category: "compliance",
|
|
66
66
|
recommended: false,
|
|
67
67
|
});
|
|
68
|
-
r.entity("tenant-retention-override", tenantRetentionOverrideEntity);
|
|
68
|
+
r.entity({ name: "tenant-retention-override", ...tenantRetentionOverrideEntity });
|
|
69
69
|
|
|
70
70
|
// S2.D3: Cross-Feature-API fuer Forget-Flow + Cleanup-Job
|
|
71
71
|
r.exposesApi("retention.policyFor");
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
import type { RetentionDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Retention policy keyed by entity name, i.e. the `name` from
|
|
17
|
+
* r.entity({ name, ... }) (kebab-case or lowercase). The cleanup job walks
|
|
18
|
+
* all known entities; anything missing from the preset falls back to the
|
|
19
|
+
* entity default (layer 1).
|
|
20
20
|
*/
|
|
21
21
|
export type RetentionPreset = Readonly<Record<string, RetentionDef>>;
|
|
22
22
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.366.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Renaming a notification type keeps user preferences and address opt-outs",
|
|
6
|
+
"detail": "r.notification accepts renamedFrom, a list of former qualified names. Preferences and address opt-outs stored under a former name keep applying to the renamed notification, and a stored choice under the current name wins over one under a former name. New writes via setPreference and the unsubscribe and resubscribe handlers are stored under the current name, resubscribeAddress also lifts opt-outs under former names. Boot fails if an alias is a current notification name, is the wildcard, or is claimed by two notifications."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.365.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -16,14 +16,18 @@ export function addressOptOutKey(addressHash: string, channel: string): AddressO
|
|
|
16
16
|
export async function selectOptedOutAddresses(
|
|
17
17
|
db: DbConnection,
|
|
18
18
|
tenantId: TenantId,
|
|
19
|
-
|
|
19
|
+
notificationTypeNames: readonly string[],
|
|
20
20
|
addressHashes: readonly string[],
|
|
21
21
|
): Promise<ReadonlySet<AddressOptOutKey>> {
|
|
22
22
|
if (addressHashes.length === 0) return new Set();
|
|
23
23
|
const rows = await selectMany<{ readonly addressHash: string; readonly channel: string }>(
|
|
24
24
|
db,
|
|
25
25
|
notificationAddressOptOutsTable,
|
|
26
|
-
{
|
|
26
|
+
{
|
|
27
|
+
tenantId,
|
|
28
|
+
notificationType: { in: notificationTypeNames },
|
|
29
|
+
addressHash: { in: addressHashes },
|
|
30
|
+
},
|
|
27
31
|
);
|
|
28
32
|
return new Set(rows.map((row) => addressOptOutKey(row.addressHash, row.channel)));
|
|
29
33
|
}
|
|
@@ -13,7 +13,7 @@ export async function selectNotificationPreferences(
|
|
|
13
13
|
db: DbConnection,
|
|
14
14
|
tenantId: TenantId,
|
|
15
15
|
userId: string,
|
|
16
|
-
|
|
16
|
+
notificationTypeNames: readonly string[],
|
|
17
17
|
channelName: string,
|
|
18
18
|
): Promise<readonly NotificationPreferenceRow[]> {
|
|
19
19
|
return unsafeReadRetrying<NotificationPreferenceRow>(
|
|
@@ -23,10 +23,10 @@ export async function selectNotificationPreferences(
|
|
|
23
23
|
WHERE tenant_id = $1
|
|
24
24
|
AND user_id = $2
|
|
25
25
|
AND (
|
|
26
|
-
(notification_type = $3 AND channel = $4)
|
|
26
|
+
(notification_type = ANY($3::text[]) AND channel = $4)
|
|
27
27
|
OR (notification_type = '*' AND channel = $4)
|
|
28
|
-
OR (notification_type = $3 AND channel = '*')
|
|
28
|
+
OR (notification_type = ANY($3::text[]) AND channel = '*')
|
|
29
29
|
)`,
|
|
30
|
-
[tenantId, userId,
|
|
30
|
+
[tenantId, userId, notificationTypeNames, channelName],
|
|
31
31
|
);
|
|
32
32
|
}
|
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
selectOptedOutAddresses,
|
|
45
45
|
} from "./db/queries/address-opt-outs.js";
|
|
46
46
|
import { selectNotificationPreferences } from "./db/queries/preferences.js";
|
|
47
|
+
import { notificationTypeNames } from "./notification-type-aliases.js";
|
|
47
48
|
import {
|
|
48
49
|
type ChannelContext,
|
|
49
50
|
type ChannelMessage,
|
|
@@ -503,7 +504,12 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
503
504
|
const addressHash = hashUnsubscribeAddress(address);
|
|
504
505
|
if (addressHash !== undefined) addressHashes.add(addressHash);
|
|
505
506
|
}
|
|
506
|
-
return selectOptedOutAddresses(
|
|
507
|
+
return selectOptedOutAddresses(
|
|
508
|
+
run.db,
|
|
509
|
+
tenantId,
|
|
510
|
+
notificationTypeNames(registry, notificationType),
|
|
511
|
+
[...addressHashes],
|
|
512
|
+
);
|
|
507
513
|
}
|
|
508
514
|
|
|
509
515
|
function isAddressSuppressed(
|
|
@@ -617,7 +623,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
617
623
|
run.db,
|
|
618
624
|
tenantId,
|
|
619
625
|
userId,
|
|
620
|
-
notificationType,
|
|
626
|
+
notificationTypeNames(registry, notificationType),
|
|
621
627
|
channelName,
|
|
622
628
|
);
|
|
623
629
|
|
|
@@ -629,6 +635,15 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
629
635
|
);
|
|
630
636
|
if (exact) return exact.enabled;
|
|
631
637
|
|
|
638
|
+
// Rows stored under a former name: the user's choice from before the rename
|
|
639
|
+
const aliasExact = prefs.filter(
|
|
640
|
+
(p) =>
|
|
641
|
+
p.notificationType !== "*" &&
|
|
642
|
+
p.notificationType !== notificationType &&
|
|
643
|
+
p.channel === channelName,
|
|
644
|
+
);
|
|
645
|
+
if (aliasExact.length > 0) return !aliasExact.some((p) => p.enabled === false);
|
|
646
|
+
|
|
632
647
|
// Only wildcards matched: any disabled entry disables delivery (deterministic
|
|
633
648
|
// and conservative — DB ordering no longer decides the outcome).
|
|
634
649
|
return !prefs.some((p) => p.enabled === false);
|
|
@@ -70,18 +70,22 @@ export function createDeliveryFeature(options?: DeliveryFeatureOptions): Feature
|
|
|
70
70
|
});
|
|
71
71
|
r.systemScope();
|
|
72
72
|
// delivery-log's tenantId column has refEntity: "tenant:tenant" (boot-checked).
|
|
73
|
-
r.requires("tenant");
|
|
73
|
+
r.requires({ features: ["tenant"] });
|
|
74
74
|
// Backing table: the (tenant,user,type,channel) uniqueIndex lives only on
|
|
75
75
|
// the physical table, not on the entity fields, so the generator would
|
|
76
76
|
// otherwise omit it → duplicate preference rows on concurrent upserts.
|
|
77
|
-
r.entity(
|
|
78
|
-
|
|
77
|
+
r.entity({
|
|
78
|
+
name: "notification-preference",
|
|
79
|
+
...notificationPreferenceEntity,
|
|
80
|
+
backingTable: notificationPreferencesTable,
|
|
79
81
|
});
|
|
80
82
|
// Same ride-along reason as notification-preference above: the
|
|
81
83
|
// (tenant,addressHash,type,channel) uniqueIndex lives only on the
|
|
82
84
|
// physical table.
|
|
83
|
-
r.entity(
|
|
84
|
-
|
|
85
|
+
r.entity({
|
|
86
|
+
name: "notification-address-opt-out",
|
|
87
|
+
...notificationAddressOptOutEntity,
|
|
88
|
+
backingTable: notificationAddressOptOutsTable,
|
|
85
89
|
});
|
|
86
90
|
r.storeTable(deliveryAttemptsTableMeta, {
|
|
87
91
|
reason: "read_side.delivery_attempt_log",
|
|
@@ -2,6 +2,7 @@ import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine
|
|
|
2
2
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { removeAddressOptOut } from "../address-opt-out.js";
|
|
5
|
+
import { currentNotificationType, notificationTypeNames } from "../notification-type-aliases.js";
|
|
5
6
|
|
|
6
7
|
export const resubscribeAddressWrite = defineWriteHandler({
|
|
7
8
|
name: "resubscribeAddress",
|
|
@@ -20,7 +21,8 @@ export const resubscribeAddressWrite = defineWriteHandler({
|
|
|
20
21
|
description:
|
|
21
22
|
"Removes a no-account address opt-out for one notificationType/channel combination; dispatched by the signed resubscribe route, not meant for UI callers.",
|
|
22
23
|
handler: async (event, ctx) => {
|
|
23
|
-
const { addressHash,
|
|
24
|
+
const { addressHash, channel } = event.payload;
|
|
25
|
+
const notificationType = currentNotificationType(ctx.registry, event.payload.notificationType);
|
|
24
26
|
const { tenantId } = event.user;
|
|
25
27
|
|
|
26
28
|
if (!ctx.systemDb) {
|
|
@@ -30,13 +32,15 @@ export const resubscribeAddressWrite = defineWriteHandler({
|
|
|
30
32
|
}
|
|
31
33
|
const db = ctx.systemDb.assertTenantMatch(tenantId);
|
|
32
34
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
for (const name of notificationTypeNames(ctx.registry, notificationType)) {
|
|
36
|
+
const result = await removeAddressOptOut(db, event.user, {
|
|
37
|
+
tenantId,
|
|
38
|
+
addressHash,
|
|
39
|
+
notificationType: name,
|
|
40
|
+
channel,
|
|
41
|
+
});
|
|
42
|
+
if (!result.isSuccess) return result;
|
|
43
|
+
}
|
|
40
44
|
return { isSuccess: true, data: { notificationType, channel } };
|
|
41
45
|
},
|
|
42
46
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { currentNotificationType } from "../notification-type-aliases.js";
|
|
4
5
|
import { upsertPreference } from "../upsert-preference.js";
|
|
5
6
|
|
|
6
7
|
export const resubscribeUserWrite = defineWriteHandler({
|
|
@@ -18,7 +19,8 @@ export const resubscribeUserWrite = defineWriteHandler({
|
|
|
18
19
|
description:
|
|
19
20
|
"Re-enables one notification type and channel combination for the userId carried in a verified unsubscribe token; dispatched by the signed resubscribe route, not meant for UI callers.",
|
|
20
21
|
handler: async (event, ctx) => {
|
|
21
|
-
const { userId,
|
|
22
|
+
const { userId, channel } = event.payload;
|
|
23
|
+
const notificationType = currentNotificationType(ctx.registry, event.payload.notificationType);
|
|
22
24
|
const { tenantId } = event.user;
|
|
23
25
|
|
|
24
26
|
if (!ctx.systemDb) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { currentNotificationType } from "../notification-type-aliases.js";
|
|
4
5
|
import { upsertPreference } from "../upsert-preference.js";
|
|
5
6
|
|
|
6
7
|
export const setPreferenceWrite = defineWriteHandler({
|
|
@@ -21,7 +22,8 @@ export const setPreferenceWrite = defineWriteHandler({
|
|
|
21
22
|
description:
|
|
22
23
|
"Enables or disables one notification type and channel combination for the calling user, where either side may be the wildcard * as a catch-all; use it when a user changes their own notification settings.",
|
|
23
24
|
handler: async (event, ctx) => {
|
|
24
|
-
const {
|
|
25
|
+
const { channel, enabled } = event.payload;
|
|
26
|
+
const notificationType = currentNotificationType(ctx.registry, event.payload.notificationType);
|
|
25
27
|
const { id: userId, tenantId } = event.user;
|
|
26
28
|
|
|
27
29
|
// delivery runs in system-scope (feature.ts r.systemScope()) — ctx.systemDb
|
|
@@ -2,6 +2,7 @@ import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine
|
|
|
2
2
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { upsertAddressOptOut } from "../address-opt-out.js";
|
|
5
|
+
import { currentNotificationType } from "../notification-type-aliases.js";
|
|
5
6
|
|
|
6
7
|
export const unsubscribeAddressWrite = defineWriteHandler({
|
|
7
8
|
name: "unsubscribeAddress",
|
|
@@ -18,7 +19,8 @@ export const unsubscribeAddressWrite = defineWriteHandler({
|
|
|
18
19
|
description:
|
|
19
20
|
"Records a no-account address opt-out for one notificationType/channel combination; dispatched by the signed unsubscribe route, not meant for UI callers.",
|
|
20
21
|
handler: async (event, ctx) => {
|
|
21
|
-
const { addressHash,
|
|
22
|
+
const { addressHash, channel } = event.payload;
|
|
23
|
+
const notificationType = currentNotificationType(ctx.registry, event.payload.notificationType);
|
|
22
24
|
const { tenantId } = event.user;
|
|
23
25
|
|
|
24
26
|
if (!ctx.systemDb) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { currentNotificationType } from "../notification-type-aliases.js";
|
|
4
5
|
import { upsertPreference } from "../upsert-preference.js";
|
|
5
6
|
|
|
6
7
|
export const unsubscribeUserWrite = defineWriteHandler({
|
|
@@ -20,7 +21,8 @@ export const unsubscribeUserWrite = defineWriteHandler({
|
|
|
20
21
|
handler: async (event, ctx) => {
|
|
21
22
|
// userId comes from the payload, not event.user.id — the caller here is
|
|
22
23
|
// the system user dispatchSystemWrite runs as, not the user unsubscribing.
|
|
23
|
-
const { userId,
|
|
24
|
+
const { userId, channel } = event.payload;
|
|
25
|
+
const notificationType = currentNotificationType(ctx.registry, event.payload.notificationType);
|
|
24
26
|
const { tenantId } = event.user;
|
|
25
27
|
|
|
26
28
|
if (!ctx.systemDb) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
|
|
3
|
+
export function notificationTypeNames(registry: Registry, type: string): readonly string[] {
|
|
4
|
+
const renamedFrom = registry.getAllNotifications().get(type)?.renamedFrom ?? [];
|
|
5
|
+
return [type, ...renamedFrom];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function currentNotificationType(registry: Registry, type: string): string {
|
|
9
|
+
for (const [name, definition] of registry.getAllNotifications()) {
|
|
10
|
+
if (definition.renamedFrom?.includes(type)) return name;
|
|
11
|
+
}
|
|
12
|
+
return type;
|
|
13
|
+
}
|
|
@@ -15,7 +15,7 @@ export const derivativesSharpFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
15
15
|
category: "storage",
|
|
16
16
|
recommended: false,
|
|
17
17
|
});
|
|
18
|
-
r.requires("file-derivatives");
|
|
18
|
+
r.requires({ features: ["file-derivatives"] });
|
|
19
19
|
|
|
20
20
|
const plugin: DerivativeRendererPlugin = { render: renderImage };
|
|
21
21
|
r.useExtension(EXT_DERIVATIVE_RENDERER, "image/*", plugin);
|
|
@@ -132,9 +132,9 @@ export const documentIngestFoundationFeature = defineFeature(FEATURE_NAME, (r) =
|
|
|
132
132
|
});
|
|
133
133
|
// tenant-lifecycle hosts EXT_TENANT_DATA — documentExtract.pages is
|
|
134
134
|
// tenant-subject ciphertext and needs the destroy hook below.
|
|
135
|
-
r.requires("config", "tenant-lifecycle");
|
|
135
|
+
r.requires({ features: ["config", "tenant-lifecycle"] });
|
|
136
136
|
|
|
137
|
-
r.entity("documentExtract", documentExtractEntity);
|
|
137
|
+
r.entity({ name: "documentExtract", ...documentExtractEntity });
|
|
138
138
|
r.useExtension(EXT_TENANT_DATA, "documentExtract", {
|
|
139
139
|
destroy: documentExtractTenantDestroyHook,
|
|
140
140
|
});
|
|
@@ -111,12 +111,12 @@ export function createFileDerivativesFeature(opts: FileDerivativesOptions = {}):
|
|
|
111
111
|
});
|
|
112
112
|
// Needs a storage provider to read the original + write the variant —
|
|
113
113
|
// without file-foundation there's nothing to derive from or write to.
|
|
114
|
-
r.requires("file-foundation");
|
|
114
|
+
r.requires({ features: ["file-foundation"] });
|
|
115
115
|
// variant() reads fileRefsTable, which "files" (not file-foundation)
|
|
116
116
|
// owns — without this, mounting file-derivatives + file-foundation
|
|
117
117
|
// alone boots fine but fails at request-time with a SQL error the first
|
|
118
118
|
// time variant() runs.
|
|
119
|
-
r.requires("files");
|
|
119
|
+
r.requires({ features: ["files"] });
|
|
120
120
|
|
|
121
121
|
r.extendsRegistrar(EXT_DERIVATIVE_RENDERER, {
|
|
122
122
|
onRegister: () => {
|
|
@@ -164,7 +164,7 @@ export function createFileDerivativesFeature(opts: FileDerivativesOptions = {}):
|
|
|
164
164
|
// tenant's public variants from staying reachable — require the
|
|
165
165
|
// dependency so a missing mount fails boot, not the tenant check
|
|
166
166
|
// silently no-op-ing at request time.
|
|
167
|
-
r.requires("tenant");
|
|
167
|
+
r.requires({ features: ["tenant"] });
|
|
168
168
|
r.queryHandler(publicVariantByFileRefQuery);
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -46,7 +46,7 @@ export const fileFoundationFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
46
46
|
category: "storage",
|
|
47
47
|
recommended: false,
|
|
48
48
|
});
|
|
49
|
-
r.requires("config");
|
|
49
|
+
r.requires({ features: ["config"] });
|
|
50
50
|
|
|
51
51
|
r.extendsRegistrar(EXT_FILE_PROVIDER, {
|
|
52
52
|
onRegister: () => {
|
|
@@ -68,10 +68,10 @@ export const fileProviderInMemoryFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
68
68
|
category: "storage",
|
|
69
69
|
recommended: false,
|
|
70
70
|
});
|
|
71
|
-
// Kein
|
|
71
|
+
// Kein requires auf "config" oder "secrets" — der
|
|
72
72
|
// In-Memory-Provider hat keine Config + kein Secret. Nur die
|
|
73
73
|
// file-foundation muss da sein (Plugin-extension-point).
|
|
74
|
-
r.requires("file-foundation");
|
|
74
|
+
r.requires({ features: ["file-foundation"] });
|
|
75
75
|
|
|
76
76
|
const plugin: FileProviderPlugin = {
|
|
77
77
|
build: async (_ctx: FileProviderContext, tenantId: string): Promise<FileStorageProvider> => {
|
|
@@ -47,9 +47,9 @@ export const fileProviderS3Feature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
47
47
|
category: "storage",
|
|
48
48
|
recommended: false,
|
|
49
49
|
});
|
|
50
|
-
r.requires("config");
|
|
51
|
-
r.requires("secrets");
|
|
52
|
-
r.requires("file-foundation");
|
|
50
|
+
r.requires({ features: ["config"] });
|
|
51
|
+
r.requires({ features: ["secrets"] });
|
|
52
|
+
r.requires({ features: ["file-foundation"] });
|
|
53
53
|
|
|
54
54
|
const secretAccessKey = r.secret("s3.secretAccessKey", {
|
|
55
55
|
label: { en: "S3 secret access key" },
|
|
@@ -42,10 +42,10 @@ export const fileProviderS3EnvFeature = defineFeature(FEATURE_NAME, (r) => {
|
|
|
42
42
|
category: "storage",
|
|
43
43
|
recommended: false,
|
|
44
44
|
});
|
|
45
|
-
// No
|
|
45
|
+
// No requires on "config" or "secrets" — credentials come from
|
|
46
46
|
// env, not the per-tenant config + secrets store. Only the file-foundation
|
|
47
47
|
// plugin-extension-point must be mounted.
|
|
48
|
-
r.requires("file-foundation");
|
|
48
|
+
r.requires({ features: ["file-foundation"] });
|
|
49
49
|
|
|
50
50
|
const plugin: FileProviderPlugin = {
|
|
51
51
|
// tenantId ignored: one shared bucket serves all tenants (isolation via
|
|
@@ -5,7 +5,7 @@ Sprint-1.5 Refactor (Pre-Sprint-2).
|
|
|
5
5
|
|
|
6
6
|
## Was es macht
|
|
7
7
|
|
|
8
|
-
Deklariert `r.entity("fileRef", ...)` für die DB-Tabelle die das
|
|
8
|
+
Deklariert `r.entity({ name: "fileRef", ... })` für die DB-Tabelle die das
|
|
9
9
|
Framework via `createFileRoutes` (multipart-Upload + binary-Download)
|
|
10
10
|
bewirtschaftet. Das öffnet die Tür für Cross-Feature-Hooks:
|
|
11
11
|
|
|
@@ -39,7 +39,7 @@ export function createFilesTenantDataFeature(): FeatureDefinition {
|
|
|
39
39
|
category: "compliance",
|
|
40
40
|
recommended: false,
|
|
41
41
|
});
|
|
42
|
-
r.requires("tenant-lifecycle");
|
|
42
|
+
r.requires({ features: ["tenant-lifecycle"] });
|
|
43
43
|
r.optionalRequires("files");
|
|
44
44
|
|
|
45
45
|
r.useExtension(EXT_TENANT_DATA, "fileRef", { destroy: fileRefTenantDestroyHook });
|
|
@@ -58,8 +58,8 @@ function registerFolders(
|
|
|
58
58
|
if (toggleable !== undefined) r.toggleable(toggleable);
|
|
59
59
|
|
|
60
60
|
const folderAssignmentEntity = createFolderAssignmentEntity(ownership, parents);
|
|
61
|
-
r.entity("folder", folderEntity);
|
|
62
|
-
r.entity("folder-assignment", folderAssignmentEntity);
|
|
61
|
+
r.entity({ name: "folder", ...folderEntity });
|
|
62
|
+
r.entity({ name: "folder-assignment", ...folderAssignmentEntity });
|
|
63
63
|
|
|
64
64
|
// Folder catalog — plain CRUD, no custom logic. update is rename (and, in a
|
|
65
65
|
// later stage, reparent: it accepts changes.parentId, optimistic-locked).
|
|
@@ -20,7 +20,7 @@ export const foldersUserDataFeature = defineFeature("folders-user-data", (r) =>
|
|
|
20
20
|
// OPTIONAL: if it's mounted toggleable(default=false) (e.g. per-tenant via
|
|
21
21
|
// tier), a hard r.requires would throw an "effectively disabled" boot
|
|
22
22
|
// warning even though the folder entities exist and the hooks work fine.
|
|
23
|
-
r.requires("user-data-rights");
|
|
23
|
+
r.requires({ features: ["user-data-rights"] });
|
|
24
24
|
r.optionalRequires("folders");
|
|
25
25
|
r.useExtension(EXT_USER_DATA, "folder", {
|
|
26
26
|
export: folderExportHook,
|
|
@@ -30,11 +30,11 @@ function registerFormDraft(r: FeatureRegistrar<typeof FORM_DRAFT_FEATURE_NAME>):
|
|
|
30
30
|
// config is optional: the retention-days key only resolves when the
|
|
31
31
|
// config feature is mounted (ctx.configResolver present) — the cleanup
|
|
32
32
|
// job already falls back to FORM_DRAFT_DEFAULT_RETENTION_DAYS otherwise
|
|
33
|
-
// (see handlers/cleanup.job.ts). A hard r.requires("config") would break
|
|
33
|
+
// (see handlers/cleanup.job.ts). A hard r.requires({ features: ["config"] }) would break
|
|
34
34
|
// every app that mounts form-draft without config, which is a breaking
|
|
35
35
|
// change no minor/patch release should introduce silently.
|
|
36
36
|
r.optionalRequires("config");
|
|
37
|
-
r.entity("form-draft", formDraftEntity);
|
|
37
|
+
r.entity({ name: "form-draft", ...formDraftEntity });
|
|
38
38
|
|
|
39
39
|
r.writeHandler(saveDraftWrite);
|
|
40
40
|
r.writeHandler(discardDraftWrite);
|
|
@@ -12,7 +12,7 @@ export const formDraftUserDataFeature = defineFeature("form-draft-user-data", (r
|
|
|
12
12
|
r.describe(
|
|
13
13
|
"GDPR (Art. 20 export / Art. 17 erasure) coverage for the `form-draft` feature's `form-draft` entity. Mounts the EXT_USER_DATA export hook so a user's saved drafts are included in the user-data export bundle, and a delete hook that physically removes the user's own draft rows on forget. Kept separate from `form-draft` so form-draft consumers without the user-data-rights pipeline don't pull a hard dependency — requires `user-data-rights`, optionalRequires `form-draft`.",
|
|
14
14
|
);
|
|
15
|
-
r.requires("user-data-rights");
|
|
15
|
+
r.requires({ features: ["user-data-rights"] });
|
|
16
16
|
r.optionalRequires("form-draft");
|
|
17
17
|
r.useExtension(EXT_USER_DATA, "form-draft", {
|
|
18
18
|
export: formDraftExportHook,
|