@cosmicdrift/kumiko-bundled-features 0.13.0 → 0.15.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 +6 -6
- package/src/__tests__/env-schemas.test.ts +1 -1
- package/src/__tests__/es-ops-e2e.integration.ts +10 -9
- package/src/audit/__tests__/audit.integration.ts +3 -3
- package/src/audit/handlers/list.query.ts +39 -51
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +4 -3
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +4 -3
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +5 -4
- package/src/auth-email-password/__tests__/auth.integration.ts +4 -3
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-templates.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-verification.integration.ts +7 -10
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +1 -1
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +4 -3
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +16 -43
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +6 -9
- package/src/auth-email-password/__tests__/password-reset.integration.ts +8 -7
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +4 -3
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +19 -32
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +6 -5
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +1 -1
- package/src/auth-email-password/__tests__/signed-token.test.ts +1 -1
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +11 -15
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +26 -26
- package/src/auth-email-password/handlers/invite-accept.write.ts +24 -21
- package/src/auth-email-password/handlers/invite-create.write.ts +3 -8
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +20 -17
- package/src/auth-email-password/handlers/signup-confirm.write.ts +3 -7
- package/src/auth-email-password/seeding.ts +1 -1
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +1 -2
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +10 -19
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +12 -18
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +12 -17
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +1 -1
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +1 -8
- package/src/auth-email-password/web/__tests__/test-utils.tsx +4 -8
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +2 -8
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +10 -15
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +1 -1
- package/src/billing-foundation/__tests__/feature.test.ts +1 -1
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +6 -5
- package/src/billing-foundation/db/queries/subscription-projection.ts +15 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +2 -6
- package/src/billing-foundation/handlers/create-portal-session.write.ts +2 -2
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +4 -1
- package/src/billing-foundation/projection.ts +32 -13
- package/src/cap-counter/__tests__/cap-counter.integration.ts +1 -1
- package/src/cap-counter/__tests__/enforce-cap.test.ts +37 -32
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +1 -1
- package/src/cap-counter/enforce-cap.ts +14 -20
- package/src/cap-counter/handlers/get-counter.query.ts +7 -13
- package/src/cap-counter/handlers/increment.write.ts +2 -2
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
- package/src/channel-in-app/handlers/inbox.query.ts +7 -13
- package/src/channel-in-app/handlers/mark-all-read.write.ts +7 -9
- package/src/channel-in-app/handlers/mark-read.write.ts +8 -14
- package/src/channel-in-app/handlers/unread-count.query.ts +10 -9
- package/src/channel-in-app/in-app-channel.ts +10 -12
- package/src/channel-in-app/tables.ts +1 -1
- package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +1 -1
- package/src/compliance-profiles/__tests__/seeding.integration.ts +1 -1
- package/src/compliance-profiles/handlers/for-tenant.query.ts +4 -7
- package/src/compliance-profiles/handlers/needs-profile.query.ts +4 -7
- package/src/compliance-profiles/handlers/set-profile.write.ts +5 -7
- package/src/compliance-profiles/resolve-for-tenant.ts +5 -7
- package/src/compliance-profiles/schema/profile-selection.ts +2 -2
- package/src/compliance-profiles/seeding.ts +4 -7
- package/src/config/__tests__/app-overrides.test.ts +1 -1
- package/src/config/__tests__/cascade.integration.ts +1 -1
- package/src/config/__tests__/config.integration.ts +8 -27
- package/src/config/db/queries/resolver.ts +47 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +1 -1
- package/src/config/resolver.ts +14 -62
- package/src/config/table.ts +4 -4
- package/src/config/write-helpers.ts +7 -11
- package/src/custom-fields/__tests__/audit-integration.integration.ts +6 -6
- package/src/custom-fields/__tests__/custom-fields.integration.ts +7 -7
- package/src/custom-fields/__tests__/feature.test.ts +1 -1
- package/src/custom-fields/__tests__/field-access.integration.ts +6 -6
- package/src/custom-fields/__tests__/quota.integration.ts +6 -6
- package/src/custom-fields/__tests__/retention.integration.ts +12 -10
- package/src/custom-fields/__tests__/user-data-rights.integration.ts +27 -17
- package/src/custom-fields/__tests__/wire-for-entity.test.ts +5 -5
- package/src/custom-fields/db/queries/field-access.ts +16 -0
- package/src/custom-fields/db/queries/projection.ts +43 -0
- package/src/custom-fields/db/queries/quota.ts +14 -0
- package/src/custom-fields/db/queries/retention.ts +39 -0
- package/src/custom-fields/db/queries/user-data-rights.ts +54 -0
- package/src/custom-fields/lib/field-access.ts +2 -41
- package/src/custom-fields/lib/quota.ts +2 -25
- package/src/custom-fields/run-retention.ts +19 -21
- package/src/custom-fields/wire-for-entity.ts +30 -23
- package/src/custom-fields/wire-user-data-rights.ts +33 -85
- package/src/data-retention/__tests__/data-retention.integration.ts +1 -1
- package/src/data-retention/__tests__/keep-for.test.ts +1 -1
- package/src/data-retention/__tests__/override-schema.test.ts +1 -1
- package/src/data-retention/__tests__/policy-for.integration.ts +1 -1
- package/src/data-retention/__tests__/resolver.test.ts +1 -1
- package/src/data-retention/handlers/policy-for.query.ts +5 -8
- package/src/data-retention/resolve-for-tenant.ts +6 -8
- package/src/data-retention/schema/tenant-retention-override.ts +2 -2
- package/src/delivery/__tests__/delivery-events.integration.ts +8 -21
- package/src/delivery/__tests__/delivery.integration.ts +100 -190
- package/src/delivery/db/queries/preferences.ts +30 -0
- package/src/delivery/delivery-service.ts +8 -36
- package/src/delivery/feature.ts +2 -1
- package/src/delivery/handlers/log.query.ts +5 -7
- package/src/delivery/handlers/preferences.query.ts +2 -5
- package/src/delivery/tables.ts +26 -1
- package/src/delivery/upsert-preference.ts +8 -14
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +30 -30
- package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +7 -6
- package/src/feature-toggles/db/queries/toggle-state.ts +25 -0
- package/src/feature-toggles/feature.ts +16 -2
- package/src/feature-toggles/global-feature-state-table.ts +1 -1
- package/src/feature-toggles/handlers/list.query.ts +9 -2
- package/src/feature-toggles/handlers/registered.query.ts +3 -7
- package/src/feature-toggles/handlers/set.write.ts +37 -25
- package/src/feature-toggles/toggle-runtime.ts +3 -6
- package/src/file-foundation/__tests__/feature.test.ts +1 -1
- package/src/file-foundation/__tests__/file-foundation.integration.ts +1 -1
- package/src/file-provider-inmemory/__tests__/feature.test.ts +1 -1
- package/src/file-provider-s3/__tests__/feature.test.ts +1 -1
- package/src/files/__tests__/files.integration.ts +18 -7
- package/src/files/schema/file-ref.ts +1 -1
- package/src/files-provider-s3/__tests__/env-helper.test.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +1 -1
- package/src/jobs/__tests__/job-system-user.integration.ts +1 -1
- package/src/jobs/__tests__/jobs-events.integration.ts +8 -21
- package/src/jobs/__tests__/jobs-feature.integration.ts +1 -1
- package/src/jobs/feature.ts +22 -14
- package/src/jobs/handlers/detail.query.ts +10 -8
- package/src/jobs/handlers/list.query.ts +9 -21
- package/src/jobs/handlers/retry.write.ts +2 -7
- package/src/jobs/job-run-logger.ts +3 -9
- package/src/jobs/job-run-table.ts +49 -17
- package/src/legal-pages/__tests__/legal-pages.integration.ts +1 -1
- package/src/mail-foundation/__tests__/feature.test.ts +1 -1
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +1 -1
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +1 -1
- package/src/mail-transport-smtp/__tests__/feature.test.ts +1 -1
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +1 -1
- package/src/renderer-foundation/__tests__/api.test.ts +2 -2
- package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +1 -1
- package/src/renderer-simple/__tests__/adapter.test.ts +2 -2
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +1 -1
- package/src/secrets/__tests__/require-secrets-context.test.ts +6 -5
- package/src/secrets/__tests__/rotate.integration.ts +6 -9
- package/src/secrets/__tests__/secrets-events.integration.ts +6 -12
- package/src/secrets/__tests__/secrets.integration.ts +6 -11
- package/src/secrets/db/queries/read.ts +16 -0
- package/src/secrets/handlers/list.query.ts +16 -17
- package/src/secrets/handlers/rotate.job.ts +8 -12
- package/src/secrets/secrets-context.ts +9 -21
- package/src/secrets/table.ts +1 -1
- package/src/sessions/__tests__/cleanup.integration.ts +8 -6
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +7 -6
- package/src/sessions/__tests__/sessions.integration.ts +23 -38
- package/src/sessions/__tests__/test-helpers.ts +1 -1
- package/src/sessions/db/queries/cleanup.ts +21 -0
- package/src/sessions/handlers/cleanup.job.ts +6 -29
- package/src/sessions/handlers/list.query.ts +24 -24
- package/src/sessions/handlers/mine.query.ts +24 -23
- package/src/sessions/handlers/revoke-all-for-user.write.ts +7 -11
- package/src/sessions/handlers/revoke-all-others.write.ts +7 -12
- package/src/sessions/handlers/revoke.write.ts +11 -18
- package/src/sessions/schema/user-session.ts +2 -2
- package/src/sessions/session-callbacks.ts +19 -21
- package/src/subscription-mollie/__tests__/feature.test.ts +1 -1
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +1 -1
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +8 -7
- package/src/subscription-stripe/__tests__/feature.test.ts +1 -1
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +14 -15
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +1 -1
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +14 -14
- package/src/subscription-stripe/verify-webhook.ts +1 -1
- package/src/template-resolver/__tests__/handlers.integration.ts +1 -1
- package/src/template-resolver/__tests__/template-resolver.integration.ts +3 -2
- package/src/template-resolver/api.ts +7 -13
- package/src/template-resolver/handlers/archive.write.ts +4 -7
- package/src/template-resolver/handlers/find-by-id.query.ts +4 -7
- package/src/template-resolver/handlers/list.query.ts +13 -21
- package/src/template-resolver/handlers/publish.write.ts +4 -7
- package/src/template-resolver/handlers/upsert-system.write.ts +7 -10
- package/src/template-resolver/handlers/upsert-tenant.write.ts +7 -10
- package/src/template-resolver/table.ts +2 -5
- package/src/tenant/__tests__/multi-tenant.integration.ts +1 -1
- package/src/tenant/__tests__/seed-testing.integration.ts +19 -45
- package/src/tenant/__tests__/tenant.integration.ts +1 -1
- package/src/tenant/handlers/active-tenant-ids.query.ts +3 -8
- package/src/tenant/handlers/add-member.write.ts +6 -8
- package/src/tenant/handlers/cancel-invitation.write.ts +5 -7
- package/src/tenant/handlers/invitations.query.ts +5 -10
- package/src/tenant/handlers/me.query.ts +2 -3
- package/src/tenant/handlers/members.query.ts +4 -5
- package/src/tenant/handlers/memberships.query.ts +2 -5
- package/src/tenant/handlers/remove-member.write.ts +6 -8
- package/src/tenant/handlers/resolve-user-ids.query.ts +6 -16
- package/src/tenant/handlers/update-member-roles.write.ts +6 -8
- package/src/tenant/invitation-table.ts +2 -5
- package/src/tenant/membership-table.ts +3 -6
- package/src/tenant/schema/tenant.ts +2 -2
- package/src/tenant/seeding.ts +12 -18
- package/src/text-content/README.md +1 -1
- package/src/text-content/__tests__/text-content.integration.ts +2 -2
- package/src/text-content/api.ts +2 -9
- package/src/text-content/handlers/by-slug.query.ts +6 -9
- package/src/text-content/handlers/by-tenant.query.ts +2 -2
- package/src/text-content/handlers/set.write.ts +7 -9
- package/src/text-content/seeding.ts +6 -9
- package/src/text-content/table.ts +2 -2
- package/src/text-content/web/__tests__/editor-read-only.test.tsx +31 -45
- package/src/text-content/web/__tests__/group-blocks.test.ts +1 -18
- package/src/text-content/web/client-plugin.tsx +11 -23
- package/src/tier-engine/__tests__/auto-default-tier.integration.ts +10 -16
- package/src/tier-engine/__tests__/compose-app.test.ts +1 -1
- package/src/tier-engine/__tests__/drift.test.ts +1 -1
- package/src/tier-engine/__tests__/resolver.integration.ts +6 -6
- package/src/tier-engine/__tests__/tier-engine.integration.ts +1 -1
- package/src/tier-engine/feature.ts +9 -16
- package/src/user/__tests__/seed-testing.integration.ts +10 -22
- package/src/user/__tests__/user-status.test.ts +1 -1
- package/src/user/__tests__/user.integration.ts +6 -5
- package/src/user/handlers/create.write.ts +5 -7
- package/src/user/handlers/find-for-auth.query.ts +5 -7
- package/src/user/schema/user.ts +2 -2
- package/src/user/seeding.ts +2 -3
- package/src/user-data-rights/__tests__/audit-log.integration.ts +24 -12
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +64 -37
- package/src/user-data-rights/__tests__/download.integration.ts +29 -46
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +35 -28
- package/src/user-data-rights/__tests__/export-job-schema.test.ts +2 -2
- package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +1 -1
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +11 -15
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +10 -12
- package/src/user-data-rights/__tests__/request-export.integration.ts +23 -16
- package/src/user-data-rights/__tests__/restriction-flow.integration.ts +24 -32
- package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +142 -137
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +46 -28
- package/src/user-data-rights/__tests__/run-user-export.integration.ts +20 -14
- package/src/user-data-rights/__tests__/token-helpers.test.ts +1 -1
- package/src/user-data-rights/__tests__/user-data-rights.integration.ts +1 -1
- package/src/user-data-rights/__tests__/zip-path.test.ts +1 -1
- package/src/user-data-rights/audit-download.ts +3 -3
- package/src/user-data-rights/db/queries/export-jobs.ts +23 -0
- package/src/user-data-rights/db/queries/forget-cleanup.ts +13 -0
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +28 -22
- package/src/user-data-rights/handlers/download-by-job.query.ts +11 -21
- package/src/user-data-rights/handlers/download-by-token.query.ts +20 -35
- package/src/user-data-rights/handlers/export-status.query.ts +19 -33
- package/src/user-data-rights/handlers/lift-restriction.write.ts +7 -12
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +14 -23
- package/src/user-data-rights/handlers/my-audit-log.query.ts +33 -23
- package/src/user-data-rights/handlers/request-deletion.write.ts +15 -15
- package/src/user-data-rights/handlers/request-export.write.ts +7 -11
- package/src/user-data-rights/handlers/restrict-account.write.ts +12 -12
- package/src/user-data-rights/run-export-jobs.ts +20 -60
- package/src/user-data-rights/run-forget-cleanup.ts +19 -33
- package/src/user-data-rights/run-user-export.ts +4 -6
- package/src/user-data-rights/schema/download-attempt.ts +2 -2
- package/src/user-data-rights/schema/download-token.ts +2 -2
- package/src/user-data-rights/schema/export-job.ts +2 -3
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +37 -30
- package/src/user-data-rights-defaults/db/queries/user-hook.ts +17 -0
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +12 -27
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +16 -18
- package/CHANGELOG.md +0 -680
|
@@ -1,53 +1,26 @@
|
|
|
1
1
|
// T1.5c — user-data-rights wiring for custom-fields.
|
|
2
|
-
//
|
|
3
|
-
// A consumer that wires `customFields` onto a user-owned host entity
|
|
4
|
-
// (e.g. `comment`, `note`, anything with an inserted_by_id column) calls
|
|
5
|
-
// this in addition to `wireCustomFieldsFor`:
|
|
6
|
-
//
|
|
7
|
-
// wireCustomFieldsFor(r, "comment", commentTable);
|
|
8
|
-
// wireCustomFieldsUserDataRightsFor(r, {
|
|
9
|
-
// entityName: "comment",
|
|
10
|
-
// entityTable: commentTable,
|
|
11
|
-
// userIdColumn: "inserted_by_id",
|
|
12
|
-
// });
|
|
13
|
-
//
|
|
14
|
-
// Result: a second `r.useExtension(EXT_USER_DATA, "comment", { export, delete })`
|
|
15
|
-
// registration whose hooks read/write the customFields jsonb column.
|
|
16
|
-
//
|
|
17
|
-
// **Export** — every row owned by the user is included; the full customFields
|
|
18
|
-
// jsonb travels into the user's export bundle so they can see *all* their
|
|
19
|
-
// custom-field data, sensitive or not (DSGVO Art. 15+20 — completeness wins).
|
|
20
|
-
//
|
|
21
|
-
// **Forget (strategy=anonymize)** — only `sensitive=true` customField keys are
|
|
22
|
-
// stripped from the jsonb (`customFields - 'sensitiveKey1' - 'sensitiveKey2'`).
|
|
23
|
-
// Non-sensitive customFields stay so the row remains useful to other tenants
|
|
24
|
-
// / co-authors. Matches the host-entity anonymize-then-keep contract.
|
|
25
|
-
//
|
|
26
|
-
// **Forget (strategy=delete)** — no-op. The host entity's own user-data-rights
|
|
27
|
-
// hook will delete the row entirely; jsonb goes with it.
|
|
28
|
-
//
|
|
29
|
-
// Side-step: this wiring requires `user-data-rights` to be installed in the
|
|
30
|
-
// composed feature set; if it's not, the boot-validator will reject the
|
|
31
|
-
// extension as unknown. That is the consumer's call — it's explicitly opt-in
|
|
32
|
-
// (call this function or don't), exactly because some consumers wire custom-
|
|
33
|
-
// fields onto tenant-owned entities (e.g. `property`) where DSGVO forget
|
|
34
|
-
// doesn't apply per-user.
|
|
35
2
|
|
|
36
3
|
import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
|
|
37
4
|
import { EXT_USER_DATA, type FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
|
|
38
|
-
import {
|
|
39
|
-
|
|
5
|
+
import {
|
|
6
|
+
selectCustomFieldsHostRows,
|
|
7
|
+
selectFieldDefinitionsForEntity,
|
|
8
|
+
stripSensitiveCustomFieldKeys,
|
|
9
|
+
} from "./db/queries/user-data-rights";
|
|
40
10
|
import { parseSerializedField } from "./lib/parse-serialized-field";
|
|
41
11
|
|
|
12
|
+
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
13
|
+
function getTableName(table: unknown): string {
|
|
14
|
+
if (typeof table === "object" && table !== null) {
|
|
15
|
+
const sym = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
|
|
16
|
+
if (typeof sym === "string") return sym;
|
|
17
|
+
}
|
|
18
|
+
throw new Error("wire-user-data-rights: table missing kumiko:schema:Name symbol");
|
|
19
|
+
}
|
|
20
|
+
|
|
42
21
|
export interface WireCustomFieldsUserDataRightsOptions {
|
|
43
|
-
/** Host entity name as registered with wireCustomFieldsFor. */
|
|
44
22
|
readonly entityName: string;
|
|
45
|
-
|
|
46
|
-
readonly entityTable: PgTable;
|
|
47
|
-
/**
|
|
48
|
-
* Snake-case DB column that holds the owning user's id (e.g. `inserted_by_id`,
|
|
49
|
-
* `author_id`, `assignee_id`). The hooks filter rows on this + tenant_id.
|
|
50
|
-
*/
|
|
23
|
+
readonly entityTable: unknown;
|
|
51
24
|
readonly userIdColumn: string;
|
|
52
25
|
}
|
|
53
26
|
|
|
@@ -56,11 +29,6 @@ interface CustomFieldsHostRow {
|
|
|
56
29
|
readonly customFields: Record<string, unknown> | null;
|
|
57
30
|
}
|
|
58
31
|
|
|
59
|
-
// Drizzle's raw `execute(sql\`SELECT id, custom_fields\`)` returns rows
|
|
60
|
-
// keyed in db-column casing (snake_case), not the field-mapping casing.
|
|
61
|
-
// The typeguard normalises into the camel-cased internal shape so the
|
|
62
|
-
// rest of the hook can stay JS-idiomatic. `in` + `instanceof Object` keep
|
|
63
|
-
// the narrowing cast-free.
|
|
64
32
|
function asCustomFieldsHostRow(value: unknown): CustomFieldsHostRow | null {
|
|
65
33
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
66
34
|
if (!("id" in value) || typeof value.id !== "string") return null;
|
|
@@ -68,8 +36,6 @@ function asCustomFieldsHostRow(value: unknown): CustomFieldsHostRow | null {
|
|
|
68
36
|
const cf = value.custom_fields;
|
|
69
37
|
if (cf === null) return { id: value.id, customFields: null };
|
|
70
38
|
if (!cf || typeof cf !== "object" || Array.isArray(cf)) return null;
|
|
71
|
-
// Object.entries on a narrowed `object` returns `[string, unknown][]` —
|
|
72
|
-
// fromEntries widens that back into a typed Record without a cast.
|
|
73
39
|
return { id: value.id, customFields: Object.fromEntries(Object.entries(cf)) };
|
|
74
40
|
}
|
|
75
41
|
|
|
@@ -77,22 +43,19 @@ export function wireCustomFieldsUserDataRightsFor<TReg extends FeatureRegistrar<
|
|
|
77
43
|
r: TReg,
|
|
78
44
|
opts: WireCustomFieldsUserDataRightsOptions,
|
|
79
45
|
): void {
|
|
80
|
-
const tableName =
|
|
81
|
-
const userCol = sql.identifier(opts.userIdColumn);
|
|
46
|
+
const tableName = getTableName(opts.entityTable);
|
|
82
47
|
|
|
83
48
|
const exportHook: UserDataExportHook = async (ctx) => {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
49
|
+
const rows = await selectCustomFieldsHostRows(
|
|
50
|
+
ctx.db,
|
|
51
|
+
tableName,
|
|
52
|
+
opts.userIdColumn,
|
|
53
|
+
ctx.userId,
|
|
54
|
+
ctx.tenantId,
|
|
55
|
+
);
|
|
90
56
|
const snippetRows: Array<{ id: string; customFields: Record<string, unknown> }> = [];
|
|
91
57
|
for (const raw of rows) {
|
|
92
58
|
const row = asCustomFieldsHostRow(raw);
|
|
93
|
-
// skip: drizzle-execute can hand back loosely-typed rows from raw
|
|
94
|
-
// queries; if a row's shape doesn't fit, skip rather than guess.
|
|
95
|
-
// Real schemas always match — this is defense in depth.
|
|
96
59
|
if (!row) continue;
|
|
97
60
|
const customFields = row.customFields;
|
|
98
61
|
if (customFields && Object.keys(customFields).length > 0) {
|
|
@@ -104,30 +67,22 @@ export function wireCustomFieldsUserDataRightsFor<TReg extends FeatureRegistrar<
|
|
|
104
67
|
};
|
|
105
68
|
|
|
106
69
|
const deleteHook: UserDataDeleteHook = async (ctx, strategy) => {
|
|
107
|
-
// skip:
|
|
108
|
-
// data-rights hook (it removes the row; customFields jsonb travels
|
|
109
|
-
// with it). Nothing left for this layer to do.
|
|
70
|
+
// skip: delete strategy removes rows wholesale — custom-field redaction N/A.
|
|
110
71
|
if (strategy === "delete") return;
|
|
111
72
|
const sensitiveKeys = await loadSensitiveFieldKeys(ctx.db, ctx.tenantId, opts.entityName);
|
|
112
|
-
// skip: no sensitive
|
|
113
|
-
// no-op. Avoids a useless UPDATE statement.
|
|
73
|
+
// skip: no sensitive custom fields configured for this entity.
|
|
114
74
|
if (sensitiveKeys.length === 0) return;
|
|
115
75
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
76
|
+
await stripSensitiveCustomFieldKeys(
|
|
77
|
+
ctx.db,
|
|
78
|
+
tableName,
|
|
79
|
+
opts.userIdColumn,
|
|
80
|
+
sensitiveKeys,
|
|
81
|
+
ctx.userId,
|
|
82
|
+
ctx.tenantId,
|
|
120
83
|
);
|
|
121
|
-
await ctx.db.execute(sql`
|
|
122
|
-
UPDATE ${tableName}
|
|
123
|
-
SET custom_fields = ${minusChain}
|
|
124
|
-
WHERE ${userCol} = ${ctx.userId} AND tenant_id = ${ctx.tenantId}
|
|
125
|
-
`);
|
|
126
84
|
};
|
|
127
85
|
|
|
128
|
-
// r.useExtension's options-bag accepts a structural object — pass the
|
|
129
|
-
// hooks inline so TS sees the literal-typed shape and Drizzle's strict
|
|
130
|
-
// mode doesn't reject the nominal UserDataExtensionHooks branding.
|
|
131
86
|
// biome-ignore lint/correctness/useHookAtTopLevel: r.useExtension is a registrar API, not a React hook.
|
|
132
87
|
r.useExtension(EXT_USER_DATA, opts.entityName, {
|
|
133
88
|
export: exportHook,
|
|
@@ -151,16 +106,9 @@ async function loadSensitiveFieldKeys(
|
|
|
151
106
|
tenantId: string,
|
|
152
107
|
entityName: string,
|
|
153
108
|
): Promise<string[]> {
|
|
154
|
-
const
|
|
155
|
-
SELECT field_key, serialized_field
|
|
156
|
-
FROM read_custom_field_definitions
|
|
157
|
-
WHERE entity_name = ${entityName} AND tenant_id = ${tenantId}
|
|
158
|
-
`);
|
|
159
|
-
const rows: ReadonlyArray<unknown> = Array.isArray(rowsResult) ? rowsResult : [];
|
|
109
|
+
const rows = await selectFieldDefinitionsForEntity(db, entityName, tenantId);
|
|
160
110
|
const keys: string[] = [];
|
|
161
111
|
for (const raw of rows) {
|
|
162
|
-
// skip: see isCustomFieldsHostRow rationale — defense in depth against
|
|
163
|
-
// driver shape drift.
|
|
164
112
|
if (!isFieldDefinitionRow(raw)) continue;
|
|
165
113
|
const parsed = parseSerializedField(raw.serialized_field);
|
|
166
114
|
if (parsed?.sensitive === true) keys.push(raw.field_key);
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
// ob Boot-Validation oder Entity-Definition gebrochen ist — pre-S2.D2b
|
|
7
7
|
// Sicherheitsnetz.
|
|
8
8
|
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
9
10
|
import {
|
|
10
11
|
setupTestStack,
|
|
11
12
|
type TestStack,
|
|
12
13
|
unsafeCreateEntityTable,
|
|
13
14
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
14
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
15
15
|
import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../feature";
|
|
16
16
|
|
|
17
17
|
let stack: TestStack;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
1
2
|
import { ensureTemporalPolyfill, getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
2
|
-
import { beforeAll, describe, expect, test } from "vitest";
|
|
3
3
|
import { computeCutoff, InvalidKeepForError, isPastCutoff } from "../keep-for";
|
|
4
4
|
|
|
5
5
|
beforeAll(async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Tests fuer retentionOverrideSchema (S2.D2.5 M2+M3) — strict-Zod
|
|
2
2
|
// faengt Sub-Level-Tippfehler + Strategy-Enum-Drift + keepFor-Format-Drift.
|
|
3
3
|
|
|
4
|
-
import { describe, expect, test } from "
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
5
5
|
import { retentionOverrideSchema } from "../override-schema";
|
|
6
6
|
|
|
7
7
|
describe("retentionOverrideSchema — accept-Faelle", () => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// API für Forget-Flow + Cleanup-Job. Round-trip: Override in DB seeden,
|
|
3
3
|
// Query rufen, verify dass resolver Override greift.
|
|
4
4
|
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
5
6
|
import {
|
|
6
7
|
createEventStoreExecutor,
|
|
7
8
|
createTenantDb,
|
|
@@ -15,7 +16,6 @@ import {
|
|
|
15
16
|
testTenantId,
|
|
16
17
|
unsafeCreateEntityTable,
|
|
17
18
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
18
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
19
19
|
import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../feature";
|
|
20
20
|
import { tenantRetentionOverrideTable } from "../schema/tenant-retention-override";
|
|
21
21
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Unit-Tests für resolveRetentionPolicy — pure function, keine
|
|
2
2
|
// Test-Stack-Abhängigkeit.
|
|
3
3
|
|
|
4
|
+
import { describe, expect, test } from "bun:test";
|
|
4
5
|
import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
-
import { describe, expect, test } from "vitest";
|
|
6
6
|
import { resolveRetentionPolicy } from "../resolver";
|
|
7
7
|
|
|
8
8
|
describe("resolveRetentionPolicy — Layer-Resolution", () => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { eq } from "drizzle-orm";
|
|
4
3
|
import { z } from "zod";
|
|
5
4
|
import { parseRetentionOverrideOrNull } from "../_internal/parse-override";
|
|
6
5
|
import { type EffectiveRetentionPolicy, resolveRetentionPolicy } from "../resolver";
|
|
@@ -28,12 +27,10 @@ export const policyForQuery = defineQueryHandler({
|
|
|
28
27
|
const entityName = query.payload.entityName;
|
|
29
28
|
|
|
30
29
|
// Layer 3: Tenant-Override aus DB laden (UNIQUE(tenantId, entityName))
|
|
31
|
-
const overrideRow = (await fetchOne(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
eq(tenantRetentionOverrideTable["entityName"], entityName),
|
|
36
|
-
)) as { config: string | null } | null; // @cast-boundary db-runner
|
|
30
|
+
const overrideRow = (await fetchOne(ctx.db, tenantRetentionOverrideTable, {
|
|
31
|
+
tenantId: query.user.tenantId,
|
|
32
|
+
entityName,
|
|
33
|
+
})) as { config: string | null } | null; // @cast-boundary db-runner
|
|
37
34
|
|
|
38
35
|
const tenantOverride = parseRetentionOverrideOrNull(
|
|
39
36
|
overrideRow?.config ?? null,
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
// HandlerContext. Beide Pfade nutzen denselben `parseRetentionOverrideOrNull`
|
|
7
7
|
// + `resolveRetentionPolicy`, also kein Drift-Risiko.
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
10
|
+
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
10
11
|
import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
11
|
-
import { eq } from "drizzle-orm";
|
|
12
12
|
import { parseRetentionOverrideOrNull } from "./_internal/parse-override";
|
|
13
13
|
import { type EffectiveRetentionPolicy, resolveRetentionPolicy } from "./resolver";
|
|
14
14
|
import { tenantRetentionOverrideTable } from "./schema/tenant-retention-override";
|
|
@@ -23,12 +23,10 @@ export interface ResolveForTenantArgs {
|
|
|
23
23
|
export async function resolveRetentionPolicyForTenant(
|
|
24
24
|
args: ResolveForTenantArgs,
|
|
25
25
|
): Promise<EffectiveRetentionPolicy> {
|
|
26
|
-
const overrideRow = (await fetchOne(
|
|
27
|
-
args.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
eq(tenantRetentionOverrideTable["entityName"], args.entityName),
|
|
31
|
-
)) as { config: string | null } | null; // @cast-boundary db-runner
|
|
26
|
+
const overrideRow = (await fetchOne(args.db, tenantRetentionOverrideTable, {
|
|
27
|
+
tenantId: args.tenantId,
|
|
28
|
+
entityName: args.entityName,
|
|
29
|
+
})) as { config: string | null } | null; // @cast-boundary db-runner
|
|
32
30
|
|
|
33
31
|
const tenantOverride = parseRetentionOverrideOrNull(
|
|
34
32
|
overrideRow?.config ?? null,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import {
|
|
3
3
|
createEntity,
|
|
4
4
|
createLongTextField,
|
|
@@ -41,7 +41,7 @@ export const tenantRetentionOverrideEntity = createEntity({
|
|
|
41
41
|
indexes: [{ unique: true, columns: ["tenantId", "entityName"] }],
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
export const tenantRetentionOverrideTable =
|
|
44
|
+
export const tenantRetentionOverrideTable = buildEntityTable(
|
|
45
45
|
"tenantRetentionOverride",
|
|
46
46
|
tenantRetentionOverrideEntity,
|
|
47
47
|
);
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// aggregateType) fails loudly instead of breaking downstream consumers
|
|
7
7
|
// (MSPs, audit-feature, event-replays) who subscribe by name.
|
|
8
8
|
|
|
9
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
9
11
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
10
12
|
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
11
13
|
import {
|
|
@@ -16,8 +18,7 @@ import {
|
|
|
16
18
|
unsafeCreateEntityTable,
|
|
17
19
|
unsafePushTables,
|
|
18
20
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
19
|
-
import {
|
|
20
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
21
|
+
import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
21
22
|
import { createChannelInAppFeature } from "../../channel-in-app/feature";
|
|
22
23
|
import { inAppMessagesTable } from "../../channel-in-app/tables";
|
|
23
24
|
import { createConfigFeature, createConfigResolver } from "../../config";
|
|
@@ -75,9 +76,7 @@ afterAll(async () => {
|
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
beforeEach(async () => {
|
|
78
|
-
|
|
79
|
-
await db.delete(eventsTable);
|
|
80
|
-
await db.delete(deliveryAttemptsTable);
|
|
79
|
+
await resetTestTables(db, [eventsTable, deliveryAttemptsTable]);
|
|
81
80
|
});
|
|
82
81
|
|
|
83
82
|
describe("delivery event shape", () => {
|
|
@@ -89,10 +88,7 @@ describe("delivery event shape", () => {
|
|
|
89
88
|
admin.tenantId,
|
|
90
89
|
);
|
|
91
90
|
|
|
92
|
-
const events = await db
|
|
93
|
-
.select()
|
|
94
|
-
.from(eventsTable)
|
|
95
|
-
.where(eq(eventsTable.aggregateType, "deliveryAttempt"));
|
|
91
|
+
const events = await selectMany(db, eventsTable, { aggregateType: "deliveryAttempt" });
|
|
96
92
|
|
|
97
93
|
// One channel registered (in-app) → one delivery attempt → one event.
|
|
98
94
|
expect(events).toHaveLength(1);
|
|
@@ -121,10 +117,7 @@ describe("delivery event shape", () => {
|
|
|
121
117
|
admin.tenantId,
|
|
122
118
|
);
|
|
123
119
|
|
|
124
|
-
const [event] = await db
|
|
125
|
-
.select()
|
|
126
|
-
.from(eventsTable)
|
|
127
|
-
.where(eq(eventsTable.aggregateType, "deliveryAttempt"));
|
|
120
|
+
const [event] = await selectMany(db, eventsTable, { aggregateType: "deliveryAttempt" });
|
|
128
121
|
if (!event) throw new Error("expected one event");
|
|
129
122
|
|
|
130
123
|
// The service schema-parses before append (see logDelivery), but we
|
|
@@ -145,10 +138,7 @@ describe("delivery event shape", () => {
|
|
|
145
138
|
admin.tenantId,
|
|
146
139
|
);
|
|
147
140
|
|
|
148
|
-
const [event] = await db
|
|
149
|
-
.select()
|
|
150
|
-
.from(eventsTable)
|
|
151
|
-
.where(eq(eventsTable.aggregateType, "deliveryAttempt"));
|
|
141
|
+
const [event] = await selectMany(db, eventsTable, { aggregateType: "deliveryAttempt" });
|
|
152
142
|
if (!event) throw new Error("expected one event");
|
|
153
143
|
|
|
154
144
|
// PK is unique — a matching row on `id === aggregateId` is already the
|
|
@@ -156,10 +146,7 @@ describe("delivery event shape", () => {
|
|
|
156
146
|
// + tenantSecretsTable: projection-row PK IS the event aggregateId, so
|
|
157
147
|
// a replay of the same event conflicts on the PK rather than
|
|
158
148
|
// duplicating the log row.
|
|
159
|
-
const [row] = await db
|
|
160
|
-
.select()
|
|
161
|
-
.from(deliveryAttemptsTable)
|
|
162
|
-
.where(eq(deliveryAttemptsTable.id, event.aggregateId));
|
|
149
|
+
const [row] = await selectMany(db, deliveryAttemptsTable, { id: event.aggregateId });
|
|
163
150
|
expect(row).toBeDefined();
|
|
164
151
|
expect(row?.notificationType).toBe("example:notify:pk-link");
|
|
165
152
|
});
|