@cosmicdrift/kumiko-bundled-features 0.14.0 → 0.16.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 +2 -2
- 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/_internal/parse-override.ts +19 -0
- package/src/compliance-profiles/handlers/for-tenant.query.ts +10 -32
- 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 +11 -27
- 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 +10 -2
- 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 +26 -15
- 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 -689
|
@@ -17,10 +17,22 @@
|
|
|
17
17
|
|
|
18
18
|
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
19
19
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
20
|
-
import {
|
|
21
|
-
|
|
20
|
+
import {
|
|
21
|
+
selectFieldDefinitionsWithSerialized,
|
|
22
|
+
selectHostRowsWithCustomFields,
|
|
23
|
+
updateHostRowCustomFields,
|
|
24
|
+
} from "./db/queries/retention";
|
|
22
25
|
import { parseSerializedField } from "./lib/parse-serialized-field";
|
|
23
26
|
|
|
27
|
+
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
28
|
+
function getTableName(table: unknown): string {
|
|
29
|
+
if (typeof table === "object" && table !== null) {
|
|
30
|
+
const sym = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
|
|
31
|
+
if (typeof sym === "string") return sym;
|
|
32
|
+
}
|
|
33
|
+
throw new Error("custom-fields/run-retention: table missing kumiko:schema:Name symbol");
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
|
|
25
37
|
|
|
26
38
|
// Lifted from data-retention/keep-for.ts because the helper isn't re-exported
|
|
@@ -48,7 +60,7 @@ export interface RunCustomFieldsRetentionOptions {
|
|
|
48
60
|
readonly db: DbRunner;
|
|
49
61
|
readonly tenantId: string;
|
|
50
62
|
readonly entityName: string;
|
|
51
|
-
readonly entityTable:
|
|
63
|
+
readonly entityTable: unknown;
|
|
52
64
|
/** Current time, injected for time-travel-tests. */
|
|
53
65
|
readonly now: Instant;
|
|
54
66
|
}
|
|
@@ -75,18 +87,13 @@ export async function runCustomFieldsRetention(
|
|
|
75
87
|
return { rowsScanned: 0, rowsUpdated: 0, removalsByFieldKey: {} };
|
|
76
88
|
}
|
|
77
89
|
|
|
78
|
-
const tableName =
|
|
79
|
-
const
|
|
80
|
-
SELECT id, modified_at, custom_fields
|
|
81
|
-
FROM ${tableName}
|
|
82
|
-
WHERE tenant_id = ${opts.tenantId} AND custom_fields IS NOT NULL
|
|
83
|
-
`);
|
|
90
|
+
const tableName = getTableName(opts.entityTable);
|
|
91
|
+
const rows = await selectHostRowsWithCustomFields(opts.db, tableName, opts.tenantId);
|
|
84
92
|
|
|
85
93
|
const removalsByFieldKey: Record<string, number> = {};
|
|
86
94
|
let rowsUpdated = 0;
|
|
87
95
|
let rowsScanned = 0;
|
|
88
96
|
|
|
89
|
-
const rows: ReadonlyArray<unknown> = Array.isArray(rowsResult) ? rowsResult : [];
|
|
90
97
|
for (const raw of rows) {
|
|
91
98
|
rowsScanned++;
|
|
92
99
|
const row = asHostRow(raw);
|
|
@@ -125,11 +132,7 @@ export async function runCustomFieldsRetention(
|
|
|
125
132
|
removalsByFieldKey[key] = (removalsByFieldKey[key] ?? 0) + 1;
|
|
126
133
|
}
|
|
127
134
|
|
|
128
|
-
await opts.db.
|
|
129
|
-
UPDATE ${tableName}
|
|
130
|
-
SET custom_fields = ${JSON.stringify(mutated)}::jsonb
|
|
131
|
-
WHERE id = ${row.id}
|
|
132
|
-
`);
|
|
135
|
+
await updateHostRowCustomFields(opts.db, tableName, JSON.stringify(mutated), row.id);
|
|
133
136
|
rowsUpdated++;
|
|
134
137
|
}
|
|
135
138
|
|
|
@@ -171,12 +174,7 @@ async function loadRetentionPolicies(
|
|
|
171
174
|
tenantId: string,
|
|
172
175
|
entityName: string,
|
|
173
176
|
): Promise<Map<string, RetentionPolicy>> {
|
|
174
|
-
const
|
|
175
|
-
SELECT field_key, serialized_field
|
|
176
|
-
FROM read_custom_field_definitions
|
|
177
|
-
WHERE entity_name = ${entityName} AND tenant_id = ${tenantId}
|
|
178
|
-
`);
|
|
179
|
-
const rows: ReadonlyArray<unknown> = Array.isArray(rowsResult) ? rowsResult : [];
|
|
177
|
+
const rows = await selectFieldDefinitionsWithSerialized(db, entityName, tenantId);
|
|
180
178
|
const out = new Map<string, RetentionPolicy>();
|
|
181
179
|
for (const raw of rows) {
|
|
182
180
|
// skip: see asHostRow rationale.
|
|
@@ -3,10 +3,22 @@ import {
|
|
|
3
3
|
type FeatureRegistrar,
|
|
4
4
|
type JsonbFieldDef,
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
-
import type { AnyColumn } from "drizzle-orm";
|
|
7
|
-
import { eq, sql } from "drizzle-orm";
|
|
8
|
-
import type { PgTable } from "drizzle-orm/pg-core";
|
|
9
6
|
import { CUSTOM_FIELDS_EXTENSION } from "./constants";
|
|
7
|
+
import {
|
|
8
|
+
clearCustomFieldKey,
|
|
9
|
+
removeCustomFieldKeyFromAllRows,
|
|
10
|
+
setCustomFieldValue,
|
|
11
|
+
} from "./db/queries/projection";
|
|
12
|
+
|
|
13
|
+
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
14
|
+
function getTableName(table: unknown): string {
|
|
15
|
+
if (typeof table === "object" && table !== null) {
|
|
16
|
+
const sym = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
|
|
17
|
+
if (typeof sym === "string") return sym;
|
|
18
|
+
}
|
|
19
|
+
throw new Error("wire-for-entity: table missing kumiko:schema:Name symbol");
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
import type { CustomFieldClearedPayload, CustomFieldSetPayload } from "./events";
|
|
11
23
|
import { customFieldsFeature } from "./feature";
|
|
12
24
|
|
|
@@ -40,7 +52,7 @@ export function customFieldsField(): JsonbFieldDef {
|
|
|
40
52
|
// });
|
|
41
53
|
//
|
|
42
54
|
// Der `entityTable`-Parameter ist die Drizzle-Table-Instance (typically
|
|
43
|
-
// `
|
|
55
|
+
// `buildEntityTable(name, entity)`-Output). Die Closure über `entityTable`
|
|
44
56
|
// erspart der MSP-apply-fn einen runtime-table-lookup über die Registry.
|
|
45
57
|
//
|
|
46
58
|
// **Was registriert wird**:
|
|
@@ -67,7 +79,7 @@ export function customFieldsField(): JsonbFieldDef {
|
|
|
67
79
|
export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
|
|
68
80
|
r: TReg,
|
|
69
81
|
entityName: string,
|
|
70
|
-
entityTable:
|
|
82
|
+
entityTable: unknown,
|
|
71
83
|
): void {
|
|
72
84
|
// biome-ignore lint/correctness/useHookAtTopLevel: r.useExtension is a registrar-API method, not a React hook — false positive on the "use"-prefix heuristic.
|
|
73
85
|
r.useExtension(CUSTOM_FIELDS_EXTENSION, entityName);
|
|
@@ -91,14 +103,15 @@ export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
|
|
|
91
103
|
|
|
92
104
|
// jsonb_set: setze key auf value. Wenn key noch nicht existiert →
|
|
93
105
|
// wird angelegt (create_missing=true ist default). value muss als
|
|
94
|
-
// jsonb-literal kommen
|
|
95
|
-
const
|
|
96
|
-
await
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
// jsonb-literal kommen.
|
|
107
|
+
const tableName = getTableName(entityTable);
|
|
108
|
+
await setCustomFieldValue(
|
|
109
|
+
tx,
|
|
110
|
+
tableName,
|
|
111
|
+
payload.fieldKey,
|
|
112
|
+
JSON.stringify(payload.value),
|
|
113
|
+
event.aggregateId,
|
|
114
|
+
);
|
|
102
115
|
},
|
|
103
116
|
[clearedEventType]: async (event, tx) => {
|
|
104
117
|
// skip: MSP feuert für alle aggregate-types — nur unsere host-entity
|
|
@@ -107,13 +120,8 @@ export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
|
|
|
107
120
|
const payload = event.payload as CustomFieldClearedPayload; // @cast-boundary engine-payload
|
|
108
121
|
|
|
109
122
|
// jsonb minus operator (`-`) entfernt key aus jsonb-object.
|
|
110
|
-
const
|
|
111
|
-
await tx
|
|
112
|
-
.update(entityTable)
|
|
113
|
-
.set({
|
|
114
|
-
customFields: sql`${sql.identifier("custom_fields")} - ${payload.fieldKey}`,
|
|
115
|
-
})
|
|
116
|
-
.where(eq(idCol, event.aggregateId));
|
|
123
|
+
const tableName = getTableName(entityTable);
|
|
124
|
+
await clearCustomFieldKey(tx, tableName, payload.fieldKey, event.aggregateId);
|
|
117
125
|
},
|
|
118
126
|
[fieldDefDeletedType]: async (event, tx) => {
|
|
119
127
|
// fieldDefinition.deleted fires nur einmal pro fieldDef-delete
|
|
@@ -125,9 +133,8 @@ export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
|
|
|
125
133
|
// ihre Rows.
|
|
126
134
|
if (payload.entityName !== entityName) return;
|
|
127
135
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
});
|
|
136
|
+
const tableName = getTableName(entityTable);
|
|
137
|
+
await removeCustomFieldKeyFromAllRows(tx, tableName, payload.fieldKey);
|
|
131
138
|
},
|
|
132
139
|
},
|
|
133
140
|
});
|
|
@@ -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
|
});
|