@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
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// Pattern follows cap-counter.integration.ts: probe-feature mit own entity,
|
|
11
11
|
// wired via wireCustomFieldsFor.
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
14
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
15
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
14
16
|
import {
|
|
15
17
|
createEntity,
|
|
16
18
|
createEntityExecutor,
|
|
@@ -25,8 +27,6 @@ import {
|
|
|
25
27
|
type TestStack,
|
|
26
28
|
unsafeCreateEntityTable,
|
|
27
29
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
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";
|
|
@@ -41,7 +41,7 @@ const propertyEntity = createEntity({
|
|
|
41
41
|
customFields: customFieldsField(),
|
|
42
42
|
},
|
|
43
43
|
});
|
|
44
|
-
const propertyTable =
|
|
44
|
+
const propertyTable = buildEntityTable("property", propertyEntity);
|
|
45
45
|
|
|
46
46
|
const propertyFeature = defineFeature("property-test", (r) => {
|
|
47
47
|
r.entity("property", propertyEntity);
|
|
@@ -91,9 +91,9 @@ afterAll(async () => {
|
|
|
91
91
|
|
|
92
92
|
beforeEach(async () => {
|
|
93
93
|
// Clean slate per test — event-log + entity-rows.
|
|
94
|
-
await stack.db.
|
|
95
|
-
await stack.db.
|
|
96
|
-
await stack.db.
|
|
94
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM kumiko_events`);
|
|
95
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_t1_properties`);
|
|
96
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_custom_field_definitions`);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
// --- Helpers ---
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, test } from "
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { fieldDefinitionAggregateId } from "../aggregate-id";
|
|
3
3
|
import { SUPPORTED_FIELD_TYPES } from "../constants";
|
|
4
4
|
import { createCustomFieldsFeature } from "../feature";
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
// exactly as in B2 (no extra gate) — the existing roundtrip-test suite
|
|
10
10
|
// stays green, and we add an explicit covers-the-no-op-path test here too.
|
|
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,
|
|
@@ -24,8 +26,6 @@ import {
|
|
|
24
26
|
type TestStack,
|
|
25
27
|
unsafeCreateEntityTable,
|
|
26
28
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
27
|
-
import { sql } from "drizzle-orm";
|
|
28
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
29
29
|
import { z } from "zod";
|
|
30
30
|
import { fieldDefinitionEntity } from "../entity";
|
|
31
31
|
import { createCustomFieldsFeature } from "../feature";
|
|
@@ -38,7 +38,7 @@ const propertyEntity = createEntity({
|
|
|
38
38
|
customFields: customFieldsField(),
|
|
39
39
|
},
|
|
40
40
|
});
|
|
41
|
-
const propertyTable =
|
|
41
|
+
const propertyTable = buildEntityTable("property", propertyEntity);
|
|
42
42
|
|
|
43
43
|
const propertyFeature = defineFeature("property-t15b", (r) => {
|
|
44
44
|
r.entity("property", propertyEntity);
|
|
@@ -84,8 +84,8 @@ afterAll(async () => {
|
|
|
84
84
|
|
|
85
85
|
beforeEach(async () => {
|
|
86
86
|
await resetEventStore(stack);
|
|
87
|
-
await stack.db.
|
|
88
|
-
await stack.db.
|
|
87
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_t15b_properties`);
|
|
88
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_custom_field_definitions`);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
async function defineField(fieldKey: string, serializedField: Record<string, unknown>) {
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
// rejects with `unprocessable` + reason `cap_exceeded` when the
|
|
6
6
|
// tenant already has >= N definitions.
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
10
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
9
11
|
import {
|
|
10
12
|
createEntity,
|
|
11
13
|
createEntityExecutor,
|
|
@@ -20,8 +22,6 @@ import {
|
|
|
20
22
|
type TestStack,
|
|
21
23
|
unsafeCreateEntityTable,
|
|
22
24
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
23
|
-
import { sql } from "drizzle-orm";
|
|
24
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
25
25
|
import { z } from "zod";
|
|
26
26
|
import { fieldDefinitionEntity } from "../entity";
|
|
27
27
|
import { createCustomFieldsFeature } from "../feature";
|
|
@@ -34,7 +34,7 @@ const propertyEntity = createEntity({
|
|
|
34
34
|
customFields: customFieldsField(),
|
|
35
35
|
},
|
|
36
36
|
});
|
|
37
|
-
const propertyTable =
|
|
37
|
+
const propertyTable = buildEntityTable("property", propertyEntity);
|
|
38
38
|
|
|
39
39
|
const propertyFeature = defineFeature("property-t15e", (r) => {
|
|
40
40
|
r.entity("property", propertyEntity);
|
|
@@ -74,8 +74,8 @@ afterAll(async () => {
|
|
|
74
74
|
|
|
75
75
|
beforeEach(async () => {
|
|
76
76
|
await resetEventStore(stack);
|
|
77
|
-
await stack.db.
|
|
78
|
-
await stack.db.
|
|
77
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_t15e_properties`);
|
|
78
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM read_custom_field_definitions`);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
async function defineField(fieldKey: string) {
|
|
@@ -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";
|