@cosmicdrift/kumiko-bundled-features 0.14.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 +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/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 -689
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
// The reference timestamp is the host row's `modified_at`, not a per-key
|
|
10
10
|
// timestamp — see run-retention.ts header for the rationale.
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
14
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
13
15
|
import {
|
|
14
16
|
createEntity,
|
|
15
17
|
createEntityExecutor,
|
|
@@ -25,8 +27,6 @@ import {
|
|
|
25
27
|
unsafeCreateEntityTable,
|
|
26
28
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
27
29
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
28
|
-
import { sql } from "drizzle-orm";
|
|
29
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
30
30
|
import { z } from "zod";
|
|
31
31
|
import { fieldDefinitionEntity } from "../entity";
|
|
32
32
|
import { createCustomFieldsFeature } from "../feature";
|
|
@@ -40,7 +40,7 @@ const propertyEntity = createEntity({
|
|
|
40
40
|
customFields: customFieldsField(),
|
|
41
41
|
},
|
|
42
42
|
});
|
|
43
|
-
const propertyTable =
|
|
43
|
+
const propertyTable = buildEntityTable("property", propertyEntity);
|
|
44
44
|
|
|
45
45
|
const propertyFeature = defineFeature("property-t15d", (r) => {
|
|
46
46
|
r.entity("property", propertyEntity);
|
|
@@ -81,8 +81,8 @@ afterAll(async () => {
|
|
|
81
81
|
|
|
82
82
|
beforeEach(async () => {
|
|
83
83
|
await resetEventStore(stack);
|
|
84
|
-
await stack.db.
|
|
85
|
-
await stack.db.
|
|
84
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_t15d_properties`);
|
|
85
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_custom_field_definitions`);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
async function defineField(fieldKey: string, serializedField: Record<string, unknown>) {
|
|
@@ -116,14 +116,16 @@ async function setField(entityId: string, fieldKey: string, value: unknown) {
|
|
|
116
116
|
// older than the retention cutoff. Faster than waiting `keepFor` real
|
|
117
117
|
// time and the cleanest way to drive the cron under test.
|
|
118
118
|
async function backdateRow(id: string, isoOlderThan: string) {
|
|
119
|
-
await stack.db.
|
|
120
|
-
|
|
119
|
+
await asRawClient(stack.db).unsafe(
|
|
120
|
+
`UPDATE read_t15d_properties SET modified_at = $1::timestamptz WHERE id = $2`,
|
|
121
|
+
[isoOlderThan, id],
|
|
121
122
|
);
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
async function readRow(id: string): Promise<Record<string, unknown> | undefined> {
|
|
125
|
-
const rows = await stack.db.
|
|
126
|
-
|
|
126
|
+
const rows = await asRawClient(stack.db).unsafe(
|
|
127
|
+
`SELECT id, custom_fields FROM read_t15d_properties WHERE id = $1`,
|
|
128
|
+
[id],
|
|
127
129
|
);
|
|
128
130
|
return (rows as ReadonlyArray<Record<string, unknown>>)[0];
|
|
129
131
|
}
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
// * Forget strategy=delete: no-op — the host entity's own user-data-
|
|
15
15
|
// rights hook handles the row delete, jsonb travels with the row.
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
18
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
19
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
18
20
|
import {
|
|
19
21
|
createEntity,
|
|
20
22
|
createEntityExecutor,
|
|
@@ -32,8 +34,6 @@ import {
|
|
|
32
34
|
type TestStack,
|
|
33
35
|
unsafeCreateEntityTable,
|
|
34
36
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
35
|
-
import { sql } from "drizzle-orm";
|
|
36
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
37
37
|
import { z } from "zod";
|
|
38
38
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
39
39
|
import { createDataRetentionFeature } from "../../data-retention";
|
|
@@ -52,17 +52,20 @@ const propertyEntity = createEntity({
|
|
|
52
52
|
customFields: customFieldsField(),
|
|
53
53
|
},
|
|
54
54
|
});
|
|
55
|
-
const propertyTable =
|
|
55
|
+
const propertyTable = buildEntityTable("property", propertyEntity);
|
|
56
56
|
|
|
57
57
|
// Host entity gets its own EXT_USER_DATA-registration too — that's the
|
|
58
58
|
// canonical setup (host bundle handles row-anonymize/delete, custom-fields
|
|
59
59
|
// adds its strip-sensitive-jsonb layer on top). Both hooks fire in the
|
|
60
60
|
// same cleanup-run.
|
|
61
61
|
const hostExportHook: UserDataExportHook = async (ctx) => {
|
|
62
|
-
const rows = await ctx.db.
|
|
62
|
+
const rows = await asRawClient(ctx.db).unsafe(
|
|
63
|
+
`
|
|
63
64
|
SELECT id, name FROM read_t15c_properties
|
|
64
|
-
WHERE inserted_by_id = $
|
|
65
|
-
|
|
65
|
+
WHERE inserted_by_id = $1 AND tenant_id = $2
|
|
66
|
+
`,
|
|
67
|
+
[ctx.userId, ctx.tenantId],
|
|
68
|
+
);
|
|
66
69
|
const list = rows as ReadonlyArray<Record<string, unknown>>;
|
|
67
70
|
if (list.length === 0) return null;
|
|
68
71
|
return {
|
|
@@ -73,16 +76,22 @@ const hostExportHook: UserDataExportHook = async (ctx) => {
|
|
|
73
76
|
|
|
74
77
|
const hostDeleteHook: UserDataDeleteHook = async (ctx, strategy) => {
|
|
75
78
|
if (strategy === "delete") {
|
|
76
|
-
await ctx.db.
|
|
79
|
+
await asRawClient(ctx.db).unsafe(
|
|
80
|
+
`
|
|
77
81
|
DELETE FROM read_t15c_properties
|
|
78
|
-
WHERE inserted_by_id = $
|
|
79
|
-
|
|
82
|
+
WHERE inserted_by_id = $1 AND tenant_id = $2
|
|
83
|
+
`,
|
|
84
|
+
[ctx.userId, ctx.tenantId],
|
|
85
|
+
);
|
|
80
86
|
} else {
|
|
81
87
|
// anonymize: clear owner, keep row + non-sensitive customFields
|
|
82
|
-
await ctx.db.
|
|
88
|
+
await asRawClient(ctx.db).unsafe(
|
|
89
|
+
`
|
|
83
90
|
UPDATE read_t15c_properties SET inserted_by_id = NULL
|
|
84
|
-
WHERE inserted_by_id = $
|
|
85
|
-
|
|
91
|
+
WHERE inserted_by_id = $1 AND tenant_id = $2
|
|
92
|
+
`,
|
|
93
|
+
[ctx.userId, ctx.tenantId],
|
|
94
|
+
);
|
|
86
95
|
}
|
|
87
96
|
};
|
|
88
97
|
|
|
@@ -143,8 +152,8 @@ afterAll(async () => {
|
|
|
143
152
|
|
|
144
153
|
beforeEach(async () => {
|
|
145
154
|
await resetEventStore(stack);
|
|
146
|
-
await stack.db.
|
|
147
|
-
await stack.db.
|
|
155
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_t15c_properties`);
|
|
156
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_custom_field_definitions`);
|
|
148
157
|
});
|
|
149
158
|
|
|
150
159
|
async function defineField(fieldKey: string, serializedField: Record<string, unknown>) {
|
|
@@ -175,8 +184,9 @@ async function setField(entityId: string, fieldKey: string, value: unknown) {
|
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
async function readRow(id: string): Promise<Record<string, unknown> | undefined> {
|
|
178
|
-
const rows = await stack.db.
|
|
179
|
-
|
|
187
|
+
const rows = await asRawClient(stack.db).unsafe(
|
|
188
|
+
`SELECT id, custom_fields FROM read_t15c_properties WHERE id = $1`,
|
|
189
|
+
[id],
|
|
180
190
|
);
|
|
181
191
|
const list = rows as ReadonlyArray<Record<string, unknown>>;
|
|
182
192
|
return list[0];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
-
import { describe, expect, test } from "vitest";
|
|
4
4
|
import { customFieldsField, wireCustomFieldsFor } from "../wire-for-entity";
|
|
5
5
|
|
|
6
6
|
// B2 wireCustomFieldsFor: einziger Aufruf registriert MSP + postQuery-hook +
|
|
@@ -15,7 +15,7 @@ const propertyEntity = createEntity({
|
|
|
15
15
|
},
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
const propertyTable =
|
|
18
|
+
const propertyTable = buildEntityTable("property", propertyEntity);
|
|
19
19
|
|
|
20
20
|
describe("wireCustomFieldsFor", () => {
|
|
21
21
|
test("registers useExtension + MSP + postQuery-entity-hook + search-payload-extension", () => {
|
|
@@ -43,7 +43,7 @@ describe("wireCustomFieldsFor", () => {
|
|
|
43
43
|
expect(feature.entityHooks.postQuery["property"]).toHaveLength(1);
|
|
44
44
|
|
|
45
45
|
// 4. search-payload-extension on "property"
|
|
46
|
-
expect(feature.searchPayloadExtensions["property"]).toHaveLength(1);
|
|
46
|
+
expect(feature.searchPayloadExtensions!["property"]).toHaveLength(1);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
test("postQuery-hook flattens row.customFields onto root", async () => {
|
|
@@ -107,7 +107,7 @@ describe("wireCustomFieldsFor", () => {
|
|
|
107
107
|
wireCustomFieldsFor(r, "property", propertyTable);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
const contributor = feature.searchPayloadExtensions["property"]?.[0]?.fn;
|
|
110
|
+
const contributor = feature.searchPayloadExtensions!["property"]?.[0]?.fn;
|
|
111
111
|
expect(contributor).toBeDefined();
|
|
112
112
|
const result = await contributor?.({
|
|
113
113
|
entityName: "property",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
|
|
4
|
+
export async function selectSerializedFieldDefinition(
|
|
5
|
+
db: TenantDb,
|
|
6
|
+
tenantId: string,
|
|
7
|
+
entityName: string,
|
|
8
|
+
fieldKey: string,
|
|
9
|
+
): Promise<unknown | null> {
|
|
10
|
+
const rows = await asRawClient(db.raw).unsafe(
|
|
11
|
+
"SELECT serialized_field FROM read_custom_field_definitions WHERE entity_name = $1 AND field_key = $2 AND tenant_id = $3 LIMIT 1",
|
|
12
|
+
[entityName, fieldKey, tenantId],
|
|
13
|
+
);
|
|
14
|
+
const first = (rows as ReadonlyArray<Record<string, unknown>>)[0];
|
|
15
|
+
return first ? (first["serialized_field"] ?? null) : null;
|
|
16
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
|
|
4
|
+
function quoteTable(tableName: string): string {
|
|
5
|
+
return `"${tableName.replace(/"/g, '""')}"`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function setCustomFieldValue(
|
|
9
|
+
db: DbRunner,
|
|
10
|
+
tableName: string,
|
|
11
|
+
fieldKey: string,
|
|
12
|
+
valueJson: string,
|
|
13
|
+
aggregateId: string,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const tbl = quoteTable(tableName);
|
|
16
|
+
const escapedKey = fieldKey.replace(/'/g, "''");
|
|
17
|
+
await asRawClient(db).unsafe(
|
|
18
|
+
`UPDATE ${tbl} SET custom_fields = jsonb_set(custom_fields, '{${escapedKey}}', $1::jsonb, true) WHERE id = $2`,
|
|
19
|
+
[valueJson, aggregateId],
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function clearCustomFieldKey(
|
|
24
|
+
db: DbRunner,
|
|
25
|
+
tableName: string,
|
|
26
|
+
fieldKey: string,
|
|
27
|
+
aggregateId: string,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
const tbl = quoteTable(tableName);
|
|
30
|
+
await asRawClient(db).unsafe(
|
|
31
|
+
`UPDATE ${tbl} SET custom_fields = custom_fields - $1 WHERE id = $2`,
|
|
32
|
+
[fieldKey, aggregateId],
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function removeCustomFieldKeyFromAllRows(
|
|
37
|
+
db: DbRunner,
|
|
38
|
+
tableName: string,
|
|
39
|
+
fieldKey: string,
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const tbl = quoteTable(tableName);
|
|
42
|
+
await asRawClient(db).unsafe(`UPDATE ${tbl} SET custom_fields = custom_fields - $1`, [fieldKey]);
|
|
43
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
|
|
4
|
+
export async function countTenantFieldDefinitions(db: TenantDb, tenantId: string): Promise<number> {
|
|
5
|
+
const rowsResult = await asRawClient(db.raw).unsafe(
|
|
6
|
+
"SELECT COUNT(*)::int AS n FROM read_custom_field_definitions WHERE tenant_id = $1",
|
|
7
|
+
[tenantId],
|
|
8
|
+
);
|
|
9
|
+
const rows = rowsResult as ReadonlyArray<Record<string, unknown>>;
|
|
10
|
+
const first = rows[0];
|
|
11
|
+
if (!first) return 0;
|
|
12
|
+
const n = first["n"];
|
|
13
|
+
return typeof n === "number" ? n : Number.parseInt(String(n ?? 0), 10);
|
|
14
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
|
|
4
|
+
export async function selectFieldDefinitionsWithSerialized(
|
|
5
|
+
db: DbRunner,
|
|
6
|
+
entityName: string,
|
|
7
|
+
tenantId: string,
|
|
8
|
+
): Promise<readonly { field_key: string; serialized_field: unknown }[]> {
|
|
9
|
+
return asRawClient(db).unsafe(
|
|
10
|
+
"SELECT field_key, serialized_field FROM read_custom_field_definitions WHERE entity_name = $1 AND tenant_id = $2",
|
|
11
|
+
[entityName, tenantId],
|
|
12
|
+
) as Promise<readonly { field_key: string; serialized_field: unknown }[]>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function selectHostRowsWithCustomFields(
|
|
16
|
+
db: DbRunner,
|
|
17
|
+
tableName: string,
|
|
18
|
+
tenantId: string,
|
|
19
|
+
): Promise<readonly unknown[]> {
|
|
20
|
+
const quoted = `"${tableName.replace(/"/g, '""')}"`;
|
|
21
|
+
const rowsResult = await asRawClient(db).unsafe(
|
|
22
|
+
`SELECT id, modified_at, custom_fields FROM ${quoted} WHERE tenant_id = $1 AND custom_fields IS NOT NULL`,
|
|
23
|
+
[tenantId],
|
|
24
|
+
);
|
|
25
|
+
return Array.isArray(rowsResult) ? rowsResult : [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function updateHostRowCustomFields(
|
|
29
|
+
db: DbRunner,
|
|
30
|
+
tableName: string,
|
|
31
|
+
customFieldsJson: string,
|
|
32
|
+
rowId: string,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
const quoted = `"${tableName.replace(/"/g, '""')}"`;
|
|
35
|
+
await asRawClient(db).unsafe(`UPDATE ${quoted} SET custom_fields = $1::jsonb WHERE id = $2`, [
|
|
36
|
+
customFieldsJson,
|
|
37
|
+
rowId,
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
|
|
4
|
+
function quoteTable(tableName: string): string {
|
|
5
|
+
return `"${tableName.replace(/"/g, '""')}"`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function quoteColumn(columnName: string): string {
|
|
9
|
+
return `"${columnName.replace(/"/g, '""')}"`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function selectCustomFieldsHostRows(
|
|
13
|
+
db: DbRunner,
|
|
14
|
+
tableName: string,
|
|
15
|
+
userIdColumn: string,
|
|
16
|
+
userId: string,
|
|
17
|
+
tenantId: string,
|
|
18
|
+
): Promise<readonly unknown[]> {
|
|
19
|
+
const tbl = quoteTable(tableName);
|
|
20
|
+
const userCol = quoteColumn(userIdColumn);
|
|
21
|
+
const rowsResult = await asRawClient(db).unsafe(
|
|
22
|
+
`SELECT id, custom_fields FROM ${tbl} WHERE ${userCol} = $1 AND tenant_id = $2`,
|
|
23
|
+
[userId, tenantId],
|
|
24
|
+
);
|
|
25
|
+
return Array.isArray(rowsResult) ? rowsResult : [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function stripSensitiveCustomFieldKeys(
|
|
29
|
+
db: DbRunner,
|
|
30
|
+
tableName: string,
|
|
31
|
+
userIdColumn: string,
|
|
32
|
+
sensitiveKeys: readonly string[],
|
|
33
|
+
userId: string,
|
|
34
|
+
tenantId: string,
|
|
35
|
+
): Promise<void> {
|
|
36
|
+
const tbl = quoteTable(tableName);
|
|
37
|
+
const userCol = quoteColumn(userIdColumn);
|
|
38
|
+
const placeholders = sensitiveKeys.map((_, i) => `$${i + 1}`).join(" - ");
|
|
39
|
+
await asRawClient(db).unsafe(
|
|
40
|
+
`UPDATE ${tbl} SET custom_fields = custom_fields - ${placeholders} WHERE ${userCol} = $${sensitiveKeys.length + 1} AND tenant_id = $${sensitiveKeys.length + 2}`,
|
|
41
|
+
[...sensitiveKeys, userId, tenantId],
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function selectFieldDefinitionsForEntity(
|
|
46
|
+
db: DbRunner,
|
|
47
|
+
entityName: string,
|
|
48
|
+
tenantId: string,
|
|
49
|
+
): Promise<readonly { field_key: string; serialized_field: unknown }[]> {
|
|
50
|
+
return asRawClient(db).unsafe(
|
|
51
|
+
"SELECT field_key, serialized_field FROM read_custom_field_definitions WHERE entity_name = $1 AND tenant_id = $2",
|
|
52
|
+
[entityName, tenantId],
|
|
53
|
+
) as Promise<readonly { field_key: string; serialized_field: unknown }[]>;
|
|
54
|
+
}
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
// T1.5b — per-field write access-check for the set/clear handlers.
|
|
2
|
-
//
|
|
3
|
-
// Loads a fieldDefinition by (tenantId, entityName, fieldKey), reads its
|
|
4
|
-
// `serializedField.fieldAccess.write` array, and verifies the calling user
|
|
5
|
-
// holds at least one of the listed roles. When `fieldAccess.write` is
|
|
6
|
-
// absent or empty the handler-level RBAC is the only gate.
|
|
7
2
|
|
|
8
3
|
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
9
|
-
import {
|
|
4
|
+
import { selectSerializedFieldDefinition } from "../db/queries/field-access";
|
|
10
5
|
import { parseSerializedField } from "./parse-serialized-field";
|
|
11
6
|
|
|
12
7
|
export type FieldAccessCheckResult =
|
|
@@ -17,36 +12,6 @@ export type FieldAccessCheckResult =
|
|
|
17
12
|
requiredRoles?: ReadonlyArray<string>;
|
|
18
13
|
};
|
|
19
14
|
|
|
20
|
-
// Resolution mirrors the Plan-Doc v2 system+tenant UNION: the active
|
|
21
|
-
// definition for a fieldKey on an entity is either system-scope or
|
|
22
|
-
// tenant-scope, never both (B1 conflict-rule). The tenant-scoped row sits
|
|
23
|
-
// in the caller's tenantId; system-scoped rows would sit under
|
|
24
|
-
// SYSTEM_TENANT_ID. B1 only ships the tenant-scoped pipeline, so we only
|
|
25
|
-
// query the caller's tenant — system-scope lookup will land in B2.
|
|
26
|
-
async function loadSerializedField(
|
|
27
|
-
db: TenantDb,
|
|
28
|
-
tenantId: string,
|
|
29
|
-
entityName: string,
|
|
30
|
-
fieldKey: string,
|
|
31
|
-
): Promise<unknown | null> {
|
|
32
|
-
// TenantDb's tenant-filtered API doesn't expose raw SQL — for this
|
|
33
|
-
// single-row lookup we drop down to the underlying DbRunner. tenantId
|
|
34
|
-
// is still pinned in the WHERE clause so we don't lose isolation.
|
|
35
|
-
const rows = await db.raw.execute(sql`
|
|
36
|
-
SELECT serialized_field
|
|
37
|
-
FROM read_custom_field_definitions
|
|
38
|
-
WHERE entity_name = ${entityName}
|
|
39
|
-
AND field_key = ${fieldKey}
|
|
40
|
-
AND tenant_id = ${tenantId}
|
|
41
|
-
LIMIT 1
|
|
42
|
-
`);
|
|
43
|
-
const first = (rows as ReadonlyArray<Record<string, unknown>>)[0]; // @cast-boundary db-row
|
|
44
|
-
return first ? (first["serialized_field"] ?? null) : null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Per Plan-Doc T1.5b: an empty / undefined `write` array means the field
|
|
48
|
-
// inherits the handler-level RBAC unchanged. Only an explicit non-empty
|
|
49
|
-
// list constrains. Intersection is role-name equality (case-sensitive).
|
|
50
15
|
export async function checkFieldAccessForWrite(
|
|
51
16
|
db: TenantDb,
|
|
52
17
|
tenantId: string,
|
|
@@ -54,16 +19,12 @@ export async function checkFieldAccessForWrite(
|
|
|
54
19
|
fieldKey: string,
|
|
55
20
|
userRoles: ReadonlyArray<string>,
|
|
56
21
|
): Promise<FieldAccessCheckResult> {
|
|
57
|
-
const serialized = await
|
|
22
|
+
const serialized = await selectSerializedFieldDefinition(db, tenantId, entityName, fieldKey);
|
|
58
23
|
if (serialized === null) {
|
|
59
24
|
return { ok: false, reason: "field_definition_not_found" };
|
|
60
25
|
}
|
|
61
26
|
|
|
62
27
|
const parsed = parseSerializedField(serialized);
|
|
63
|
-
// skip: corrupt serialized_field on disk → treat as no-access-restriction
|
|
64
|
-
// rather than 500. Loader already returned null on missing row, so a
|
|
65
|
-
// null here means parse-failure on a present row; behave like an open
|
|
66
|
-
// field (next gate is the handler-level RBAC).
|
|
67
28
|
if (!parsed) return { ok: true };
|
|
68
29
|
|
|
69
30
|
const required = parsed.fieldAccess?.write;
|
|
@@ -1,28 +1,5 @@
|
|
|
1
1
|
// T1.5e — per-tenant fieldDefinition quota.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// `read_custom_field_definitions` scoped to the caller's tenant. The
|
|
5
|
-
// `define-tenant-field` handler consults this before insert and rejects
|
|
6
|
-
// with `cap_exceeded` once a configurable per-tenant ceiling is reached.
|
|
7
|
-
//
|
|
8
|
-
// This is a simple projection-count rather than a `cap-counter`-bundle
|
|
9
|
-
// counter, because the read-projection is the authoritative source
|
|
10
|
-
// (soft-deleted rows already drop out) and we don't need rolling-window
|
|
11
|
-
// semantics. A future iteration can swap to `cap-counter` if pricing
|
|
12
|
-
// wants e.g. monthly-roll definition allowances.
|
|
13
|
-
|
|
14
|
-
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
15
|
-
import { sql } from "drizzle-orm";
|
|
3
|
+
// Re-exports from db/queries/quota.ts for backward-compatible import paths.
|
|
16
4
|
|
|
17
|
-
export
|
|
18
|
-
const rowsResult = await db.raw.execute(sql`
|
|
19
|
-
SELECT COUNT(*)::int AS n
|
|
20
|
-
FROM read_custom_field_definitions
|
|
21
|
-
WHERE tenant_id = ${tenantId}
|
|
22
|
-
`);
|
|
23
|
-
const rows = rowsResult as ReadonlyArray<Record<string, unknown>>; // @cast-boundary db-row
|
|
24
|
-
const first = rows[0];
|
|
25
|
-
if (!first) return 0;
|
|
26
|
-
const n = first["n"];
|
|
27
|
-
return typeof n === "number" ? n : Number.parseInt(String(n ?? 0), 10);
|
|
28
|
-
}
|
|
5
|
+
export { countTenantFieldDefinitions } from "../db/queries/quota";
|
|
@@ -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
|
});
|