@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
package/src/delivery/tables.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
boolean,
|
|
3
3
|
buildBaseColumns,
|
|
4
|
+
defineUnmanagedTable,
|
|
5
|
+
type EntityTableMeta,
|
|
4
6
|
instant,
|
|
5
7
|
table as pgTable,
|
|
8
|
+
sql,
|
|
6
9
|
text,
|
|
7
10
|
uniqueIndex,
|
|
8
11
|
uuid,
|
|
@@ -12,7 +15,6 @@ import {
|
|
|
12
15
|
createEntity,
|
|
13
16
|
createTextField,
|
|
14
17
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
15
|
-
import { sql } from "drizzle-orm";
|
|
16
18
|
|
|
17
19
|
// Delivery-log is an append-only stream of per-attempt records. The stream
|
|
18
20
|
// of truth lives in the events-Tabelle (one aggregate per attempt, event
|
|
@@ -39,6 +41,29 @@ export const deliveryAttemptsTable = pgTable("read_delivery_attempts", {
|
|
|
39
41
|
createdAt: instant("created_at").default(sql`now()`).notNull(),
|
|
40
42
|
});
|
|
41
43
|
|
|
44
|
+
// **Unmanaged table** — bewusst KEIN createEntity. Begründung:
|
|
45
|
+
// - id kommt aus dem Aggregate-Stream (kein gen_random_uuid()-DEFAULT)
|
|
46
|
+
// - kein version/inserted_by/modified_by/modified_at — keine in-place-
|
|
47
|
+
// Edits, keine Audit-Spalten nötig (idempotent-on-replay via PK-Konflikt)
|
|
48
|
+
// - created_at statt inserted_at — historischer Naming-Drift, kein Bug
|
|
49
|
+
// App trägt Verantwortung für tenant-scoping in Queries + replay-idempotency.
|
|
50
|
+
// pgTable bleibt source-of-truth für Query-API; Phase 4 leitet das pgTable
|
|
51
|
+
// aus dieser Meta ab.
|
|
52
|
+
export const deliveryAttemptsTableMeta: EntityTableMeta = defineUnmanagedTable({
|
|
53
|
+
tableName: "read_delivery_attempts",
|
|
54
|
+
columns: [
|
|
55
|
+
{ name: "id", pgType: "uuid", notNull: true, primaryKey: true },
|
|
56
|
+
{ name: "tenant_id", pgType: "uuid", notNull: true },
|
|
57
|
+
{ name: "notification_type", pgType: "text", notNull: true },
|
|
58
|
+
{ name: "channel", pgType: "text", notNull: true },
|
|
59
|
+
{ name: "recipient_id", pgType: "text", notNull: false },
|
|
60
|
+
{ name: "recipient_address", pgType: "text", notNull: false },
|
|
61
|
+
{ name: "status", pgType: "text", notNull: true },
|
|
62
|
+
{ name: "error", pgType: "text", notNull: false },
|
|
63
|
+
{ name: "created_at", pgType: "timestamptz", notNull: true, defaultSql: "now()" },
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
|
|
42
67
|
// User-scoped opt-in/opt-out for (notificationType, channel) pairs. Post-ES
|
|
43
68
|
// refactor: each row is a notificationPreference aggregate with
|
|
44
69
|
// `.created / .updated / .deleted` lifecycle events written via the
|
|
@@ -10,13 +10,9 @@
|
|
|
10
10
|
// through to update. Worst case: one extra roundtrip for the loser of
|
|
11
11
|
// the race. Happy path: same number of queries as the pre-ES upsert.
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
fetchOne,
|
|
16
|
-
type TenantDb,
|
|
17
|
-
} from "@cosmicdrift/kumiko-framework/db";
|
|
13
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
14
|
+
import { createEventStoreExecutor, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
18
15
|
import type { SessionUser, TenantId, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
19
|
-
import { eq } from "drizzle-orm";
|
|
20
16
|
import { notificationPreferenceEntity, notificationPreferencesTable } from "./tables";
|
|
21
17
|
|
|
22
18
|
const executor = createEventStoreExecutor(
|
|
@@ -38,14 +34,12 @@ async function lookup(
|
|
|
38
34
|
notificationType: string,
|
|
39
35
|
channel: string,
|
|
40
36
|
): Promise<PreferenceLookupRow | undefined> {
|
|
41
|
-
return fetchOne<PreferenceLookupRow>(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
eq(notificationPreferencesTable.channel, channel),
|
|
48
|
-
);
|
|
37
|
+
return fetchOne<PreferenceLookupRow>(db, notificationPreferencesTable, {
|
|
38
|
+
tenantId,
|
|
39
|
+
userId,
|
|
40
|
+
notificationType,
|
|
41
|
+
channel,
|
|
42
|
+
});
|
|
49
43
|
}
|
|
50
44
|
|
|
51
45
|
export type UpsertPreferenceInput = {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { asRawClient, insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
3
|
import {
|
|
2
|
-
|
|
4
|
+
buildEntityTable,
|
|
3
5
|
createEventStoreExecutor,
|
|
4
6
|
entityEventName,
|
|
5
7
|
integer,
|
|
@@ -24,9 +26,7 @@ import {
|
|
|
24
26
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
25
27
|
import { createLateBoundHolder } from "@cosmicdrift/kumiko-framework/testing";
|
|
26
28
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
27
|
-
import { sql } from "drizzle-orm";
|
|
28
29
|
import { Temporal } from "temporal-polyfill";
|
|
29
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
30
30
|
import { z } from "zod";
|
|
31
31
|
import { FEATURE_TOGGLE_SET_EVENT_NAME } from "../constants";
|
|
32
32
|
import { createFeatureTogglesFeature } from "../feature";
|
|
@@ -44,7 +44,7 @@ const widgetEntity = createEntity({
|
|
|
44
44
|
active: createBooleanField({ default: true }),
|
|
45
45
|
},
|
|
46
46
|
});
|
|
47
|
-
const widgetTable =
|
|
47
|
+
const widgetTable = buildEntityTable("widget", widgetEntity);
|
|
48
48
|
|
|
49
49
|
const widgetCrud = createEventStoreExecutor(widgetTable, widgetEntity, {
|
|
50
50
|
entityName: "widget",
|
|
@@ -79,7 +79,7 @@ const widgetAuditEntity = createEntity({
|
|
|
79
79
|
widgetName: createTextField({ required: true, maxLength: 100 }),
|
|
80
80
|
},
|
|
81
81
|
});
|
|
82
|
-
const widgetAuditTable =
|
|
82
|
+
const widgetAuditTable = buildEntityTable("widget-audit", widgetAuditEntity);
|
|
83
83
|
|
|
84
84
|
function widgetAuditFeature(): FeatureDefinition {
|
|
85
85
|
return defineFeature("widget-audit", (r) => {
|
|
@@ -92,7 +92,7 @@ function widgetAuditFeature(): FeatureDefinition {
|
|
|
92
92
|
if (!ctx.db) return;
|
|
93
93
|
const name = result.changes!["name"] as string | undefined;
|
|
94
94
|
if (!name) return;
|
|
95
|
-
await ctx.db
|
|
95
|
+
await insertOne(ctx.db, widgetAuditTable, {
|
|
96
96
|
id: generateId(),
|
|
97
97
|
widgetName: name,
|
|
98
98
|
version: 1,
|
|
@@ -128,13 +128,10 @@ function widgetTrackerFeature(): FeatureDefinition {
|
|
|
128
128
|
table: widgetTrackerTable,
|
|
129
129
|
apply: {
|
|
130
130
|
[entityEventName("widget", "created")]: async (event, tx) => {
|
|
131
|
-
await tx
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
target: widgetTrackerTable.tenantId,
|
|
136
|
-
set: { count: sql`${widgetTrackerTable.count} + 1` },
|
|
137
|
-
});
|
|
131
|
+
await asRawClient(tx).unsafe(
|
|
132
|
+
`INSERT INTO "widget_tracker" (tenant_id, count) VALUES ($1::uuid, 1) ON CONFLICT (tenant_id) DO UPDATE SET count = widget_tracker.count + 1`,
|
|
133
|
+
[event.tenantId],
|
|
134
|
+
);
|
|
138
135
|
},
|
|
139
136
|
},
|
|
140
137
|
});
|
|
@@ -183,15 +180,17 @@ afterAll(async () => {
|
|
|
183
180
|
});
|
|
184
181
|
|
|
185
182
|
beforeEach(async () => {
|
|
186
|
-
await stack.db.
|
|
187
|
-
await stack.db.
|
|
188
|
-
await stack.db.
|
|
189
|
-
await stack.db.
|
|
183
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${widgetAuditTable.tableName}"`);
|
|
184
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${widgetTable.tableName}"`);
|
|
185
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${widgetTrackerTable.tableName}"`);
|
|
186
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM "${globalFeatureStateTable.tableName}"`);
|
|
190
187
|
// Wipe the event log + reset every consumer cursor so each test starts
|
|
191
188
|
// from event-id 0. Tests that drain via eventDispatcher.runOnce() need
|
|
192
189
|
// this or they drain a shared backlog and see false-positive counters.
|
|
193
|
-
await stack.db.
|
|
194
|
-
await stack.db.
|
|
190
|
+
await asRawClient(stack.db).unsafe(`DELETE FROM kumiko_events`);
|
|
191
|
+
await asRawClient(stack.db).unsafe(
|
|
192
|
+
`UPDATE kumiko_event_consumers SET last_processed_event_id = 0`,
|
|
193
|
+
);
|
|
195
194
|
await runtime.refresh();
|
|
196
195
|
});
|
|
197
196
|
|
|
@@ -212,17 +211,17 @@ async function createWidget(name: string) {
|
|
|
212
211
|
}
|
|
213
212
|
|
|
214
213
|
async function countWidgets(): Promise<number> {
|
|
215
|
-
const rows = await stack.db
|
|
214
|
+
const rows = await selectMany(stack.db, widgetTable);
|
|
216
215
|
return rows.length;
|
|
217
216
|
}
|
|
218
217
|
|
|
219
218
|
async function countAuditRows(): Promise<number> {
|
|
220
|
-
const rows = await stack.db
|
|
219
|
+
const rows = await selectMany(stack.db, widgetAuditTable);
|
|
221
220
|
return rows.length;
|
|
222
221
|
}
|
|
223
222
|
|
|
224
223
|
async function trackerCount(): Promise<number> {
|
|
225
|
-
const rows = await stack.db
|
|
224
|
+
const rows = await selectMany(stack.db, widgetTrackerTable);
|
|
226
225
|
return rows[0]?.count ?? 0;
|
|
227
226
|
}
|
|
228
227
|
|
|
@@ -231,15 +230,16 @@ async function trackerCount(): Promise<number> {
|
|
|
231
230
|
// explicit shape — typed access everywhere else.
|
|
232
231
|
type ConsumerCursorRow = { last_processed_event_id: number | string };
|
|
233
232
|
async function trackerCursor(): Promise<number> {
|
|
234
|
-
const rows = (await stack.db.
|
|
235
|
-
|
|
233
|
+
const rows = (await asRawClient(stack.db).unsafe(
|
|
234
|
+
`SELECT last_processed_event_id FROM kumiko_event_consumers WHERE name LIKE '%tracker%' LIMIT 1`,
|
|
236
235
|
)) as unknown as readonly ConsumerCursorRow[];
|
|
237
236
|
return Number(rows[0]?.last_processed_event_id ?? 0);
|
|
238
237
|
}
|
|
239
238
|
|
|
240
239
|
async function setTrackerCursor(value: number): Promise<void> {
|
|
241
|
-
await stack.db.
|
|
242
|
-
|
|
240
|
+
await asRawClient(stack.db).unsafe(
|
|
241
|
+
`UPDATE kumiko_event_consumers SET last_processed_event_id = $1 WHERE name LIKE '%tracker%'`,
|
|
242
|
+
[value],
|
|
243
243
|
);
|
|
244
244
|
}
|
|
245
245
|
|
|
@@ -252,7 +252,7 @@ describe("feature-toggles runtime cache", () => {
|
|
|
252
252
|
});
|
|
253
253
|
|
|
254
254
|
test("refresh() re-reads the DB snapshot", async () => {
|
|
255
|
-
await stack.db
|
|
255
|
+
await insertOne(stack.db, globalFeatureStateTable, {
|
|
256
256
|
featureName: "widget",
|
|
257
257
|
enabled: false,
|
|
258
258
|
version: 1,
|
|
@@ -330,7 +330,7 @@ describe("runtime on/off/on — the user's scenario", () => {
|
|
|
330
330
|
expect(body.data?.previousEnabled).toBeNull();
|
|
331
331
|
|
|
332
332
|
// Row persisted.
|
|
333
|
-
const rows = await stack.db
|
|
333
|
+
const rows = await selectMany(stack.db, globalFeatureStateTable);
|
|
334
334
|
expect(rows).toHaveLength(1);
|
|
335
335
|
|
|
336
336
|
// Snapshot updated — widget:create now 403s.
|
|
@@ -517,8 +517,8 @@ describe("feature-toggles queries + audit automation", () => {
|
|
|
517
517
|
admin,
|
|
518
518
|
);
|
|
519
519
|
|
|
520
|
-
const events = (await stack.db.
|
|
521
|
-
|
|
520
|
+
const events = (await asRawClient(stack.db).unsafe(
|
|
521
|
+
`SELECT type, payload FROM kumiko_events WHERE type = 'feature-toggles:event:toggle-set'`,
|
|
522
522
|
)) as unknown as readonly {
|
|
523
523
|
type: string;
|
|
524
524
|
payload: Record<string, unknown>;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// durchläuft. Die Convention-Pin ist die einzige Aussage des tests —
|
|
12
12
|
// echtes integration-Verhalten deckt feature-toggles.integration.ts ab.
|
|
13
13
|
|
|
14
|
+
import { describe, expect, test } from "bun:test";
|
|
14
15
|
import {
|
|
15
16
|
createEntity,
|
|
16
17
|
createRegistry,
|
|
@@ -21,7 +22,6 @@ import {
|
|
|
21
22
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
22
23
|
import { createDispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
23
24
|
import { createTestUser } from "@cosmicdrift/kumiko-framework/stack";
|
|
24
|
-
import { describe, expect, test } from "vitest";
|
|
25
25
|
import { createFeatureTogglesFeature } from "../feature";
|
|
26
26
|
import type { GlobalFeatureToggleRuntime } from "../toggle-runtime";
|
|
27
27
|
|
|
@@ -43,12 +43,13 @@ describe("Sprint 8a: registered.query SYSTEM_TENANT_ID convention", () => {
|
|
|
43
43
|
|
|
44
44
|
const registry = createRegistry([dummy, featureToggles]);
|
|
45
45
|
|
|
46
|
-
// Mock ctx.db
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// und
|
|
46
|
+
// Mock ctx.db via bun-db's asRawClient surface: handler ruft
|
|
47
|
+
// selectMany(ctx.db.raw, ...) → asRawClient(raw).unsafe(...). Wir
|
|
48
|
+
// liefern leere overrides damit der handler durch den DB-Pfad kommt
|
|
49
|
+
// und dann ctx.effectiveFeatures aufruft (das observable hier).
|
|
50
50
|
const mockDb = {
|
|
51
|
-
|
|
51
|
+
unsafe: async () => [] as unknown[],
|
|
52
|
+
begin: async () => undefined,
|
|
52
53
|
} as unknown as Parameters<typeof createDispatcher>[1]["db"];
|
|
53
54
|
|
|
54
55
|
const callerTenant = "00000000-0000-4000-8000-0000000000c1" as TenantId;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { Temporal } from "temporal-polyfill";
|
|
4
|
+
|
|
5
|
+
export async function updateFeatureToggleOptimistic(
|
|
6
|
+
db: DbRunner,
|
|
7
|
+
params: {
|
|
8
|
+
readonly enabled: boolean;
|
|
9
|
+
readonly updatedBy: string;
|
|
10
|
+
readonly updatedAt: Temporal.Instant;
|
|
11
|
+
readonly featureName: string;
|
|
12
|
+
readonly expectedVersion: number;
|
|
13
|
+
},
|
|
14
|
+
): Promise<readonly unknown[]> {
|
|
15
|
+
return asRawClient(db).unsafe(
|
|
16
|
+
'UPDATE "read_global_feature_state" SET enabled = $1, version = version + 1, updated_by = $2, updated_at = $3 WHERE feature_name = $4 AND version = $5 RETURNING *',
|
|
17
|
+
[
|
|
18
|
+
params.enabled,
|
|
19
|
+
params.updatedBy,
|
|
20
|
+
params.updatedAt,
|
|
21
|
+
params.featureName,
|
|
22
|
+
params.expectedVersion,
|
|
23
|
+
],
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -24,10 +24,18 @@ export type FeatureTogglesOptions = {
|
|
|
24
24
|
//
|
|
25
25
|
// Production setup: resolve the runtime after buildServer returns, then
|
|
26
26
|
// pass `() => runtime`. For tests, use createLateBoundHolder + .get().
|
|
27
|
-
|
|
27
|
+
//
|
|
28
|
+
// **Optional** — boot-mode (KUMIKO_DRY_RUN_ENV=boot) wires the feature
|
|
29
|
+
// up without ever calling the set-handler, so a runtime-stub is then
|
|
30
|
+
// pure cargo-cult. Omit `getRuntime` and the handler throws lazily on
|
|
31
|
+
// first call with an actionable message. App-authors who DO route to
|
|
32
|
+
// the set-handler at runtime MUST supply the accessor.
|
|
33
|
+
readonly getRuntime?: () => GlobalFeatureToggleRuntime;
|
|
28
34
|
};
|
|
29
35
|
|
|
30
|
-
export function createFeatureTogglesFeature(
|
|
36
|
+
export function createFeatureTogglesFeature(
|
|
37
|
+
options: FeatureTogglesOptions = {},
|
|
38
|
+
): FeatureDefinition {
|
|
31
39
|
return defineFeature("feature-toggles", (r) => {
|
|
32
40
|
r.systemScope();
|
|
33
41
|
|
|
@@ -79,6 +87,12 @@ export function createFeatureTogglesFeature(options: FeatureTogglesOptions): Fea
|
|
|
79
87
|
// than re-parsing — the payload round-trips through JSON and is
|
|
80
88
|
// fixed at the source.
|
|
81
89
|
const payload = event.payload as { featureName: string; enabled: boolean }; // @cast-boundary engine-payload
|
|
90
|
+
if (!options.getRuntime) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"[feature-toggles] toggle-cache-sync MSP fired but createFeatureTogglesFeature " +
|
|
93
|
+
"was wired up without `getRuntime`. Wire the accessor in your app-config.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
82
96
|
options.getRuntime().apply(payload.featureName, payload.enabled);
|
|
83
97
|
},
|
|
84
98
|
},
|
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
instant,
|
|
4
4
|
integer,
|
|
5
5
|
table as pgTable,
|
|
6
|
+
sql,
|
|
6
7
|
text,
|
|
7
8
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
8
|
-
import { sql } from "drizzle-orm";
|
|
9
9
|
|
|
10
10
|
// Global feature-toggle override state. One row per feature that has ever
|
|
11
11
|
// been explicitly flipped by an operator. Missing row = "no override,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { globalFeatureStateTable } from "../global-feature-state-table";
|
|
@@ -11,8 +12,14 @@ export const listQuery = defineQueryHandler({
|
|
|
11
12
|
schema: z.object({}),
|
|
12
13
|
access: { roles: ["SystemAdmin", "Admin"] },
|
|
13
14
|
handler: async (_event, ctx) => {
|
|
14
|
-
type Row =
|
|
15
|
-
|
|
15
|
+
type Row = {
|
|
16
|
+
featureName: string;
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
version: number;
|
|
19
|
+
updatedAt: Temporal.Instant;
|
|
20
|
+
updatedBy: string;
|
|
21
|
+
};
|
|
22
|
+
const rows = await selectMany<Row>(ctx.db.raw, globalFeatureStateTable);
|
|
16
23
|
return {
|
|
17
24
|
items: rows.map((r) => ({
|
|
18
25
|
featureName: r.featureName,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { globalFeatureStateTable } from "../global-feature-state-table";
|
|
@@ -15,13 +16,8 @@ export const registeredQuery = defineQueryHandler({
|
|
|
15
16
|
schema: z.object({}),
|
|
16
17
|
access: { roles: ["SystemAdmin", "Admin"] },
|
|
17
18
|
handler: async (_event, ctx) => {
|
|
18
|
-
type OverrideRow =
|
|
19
|
-
const overrideRows =
|
|
20
|
-
.select({
|
|
21
|
-
featureName: globalFeatureStateTable.featureName,
|
|
22
|
-
enabled: globalFeatureStateTable.enabled,
|
|
23
|
-
})
|
|
24
|
-
.from(globalFeatureStateTable)) as OverrideRow[]; // @cast-boundary db-row
|
|
19
|
+
type OverrideRow = { featureName: string; enabled: boolean };
|
|
20
|
+
const overrideRows = await selectMany<OverrideRow>(ctx.db.raw, globalFeatureStateTable);
|
|
25
21
|
const overrides = new Map(overrideRows.map((r) => [r.featureName, r.enabled]));
|
|
26
22
|
|
|
27
23
|
// SystemAdmin operator-tooling: das listing soll die PLATTFORM-truth
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
|
|
3
3
|
import {
|
|
4
4
|
UnprocessableError,
|
|
5
5
|
VersionConflictError,
|
|
6
6
|
writeFailure,
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
8
|
-
import { and, eq, sql } from "drizzle-orm";
|
|
9
8
|
import { Temporal } from "temporal-polyfill";
|
|
10
9
|
import { z } from "zod";
|
|
11
10
|
import {
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
FEATURE_TOGGLE_SET_EVENT_NAME,
|
|
14
13
|
FeatureToggleErrors,
|
|
15
14
|
} from "../constants";
|
|
15
|
+
import { updateFeatureToggleOptimistic } from "../db/queries/toggle-state";
|
|
16
16
|
import { globalFeatureStateTable } from "../global-feature-state-table";
|
|
17
17
|
import type { GlobalFeatureToggleRuntime } from "../toggle-runtime";
|
|
18
18
|
|
|
@@ -25,7 +25,13 @@ import type { GlobalFeatureToggleRuntime } from "../toggle-runtime";
|
|
|
25
25
|
// flow: tests + setupTestStack construct the feature definition BEFORE the
|
|
26
26
|
// runtime exists (the runtime needs the registry, which setupTestStack
|
|
27
27
|
// builds from the features). The accessor is resolved lazily, at call time.
|
|
28
|
-
|
|
28
|
+
//
|
|
29
|
+
// `undefined` accessor is legitimate at registration-time for boot-mode
|
|
30
|
+
// smoke-apps (`KUMIKO_DRY_RUN_ENV=boot`) that never dispatch a set-call.
|
|
31
|
+
// We throw lazily on first call with an actionable message — `as
|
|
32
|
+
// GlobalFeatureToggleRuntime`-casts at the registration site are no
|
|
33
|
+
// longer needed.
|
|
34
|
+
export function createSetWriteHandler(getRuntime: (() => GlobalFeatureToggleRuntime) | undefined) {
|
|
29
35
|
return defineWriteHandler({
|
|
30
36
|
name: "set",
|
|
31
37
|
schema: z.object({
|
|
@@ -66,18 +72,25 @@ export function createSetWriteHandler(getRuntime: () => GlobalFeatureToggleRunti
|
|
|
66
72
|
// Read current state for event payload + optimistic-lock version.
|
|
67
73
|
// `$inferSelect` narrows the result shape to the real table schema —
|
|
68
74
|
// no hand-rolled cast, no drift if a column is added later.
|
|
69
|
-
type StateRow =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
+
type StateRow = {
|
|
76
|
+
featureName: string;
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
version: number;
|
|
79
|
+
updatedAt: Temporal.Instant;
|
|
80
|
+
updatedBy: string;
|
|
81
|
+
};
|
|
82
|
+
const [existing] = await selectMany<StateRow>(
|
|
83
|
+
ctx.db,
|
|
84
|
+
globalFeatureStateTable,
|
|
85
|
+
{ featureName },
|
|
86
|
+
{ limit: 1 },
|
|
87
|
+
);
|
|
75
88
|
|
|
76
89
|
const previousEnabled = existing?.enabled ?? null;
|
|
77
90
|
|
|
78
91
|
if (!existing) {
|
|
79
92
|
// First-time override: insert.
|
|
80
|
-
await ctx.db
|
|
93
|
+
await insertOne(ctx.db, globalFeatureStateTable, {
|
|
81
94
|
featureName,
|
|
82
95
|
enabled,
|
|
83
96
|
version: 1,
|
|
@@ -88,21 +101,13 @@ export function createSetWriteHandler(getRuntime: () => GlobalFeatureToggleRunti
|
|
|
88
101
|
// Upsert with optimistic lock. Two operators flipping the same
|
|
89
102
|
// toggle simultaneously is rare but possible — the version-WHERE
|
|
90
103
|
// ensures only one wins; the loser sees VersionConflictError.
|
|
91
|
-
const updated = await ctx.db
|
|
92
|
-
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
})
|
|
99
|
-
.where(
|
|
100
|
-
and(
|
|
101
|
-
eq(globalFeatureStateTable.featureName, featureName),
|
|
102
|
-
eq(globalFeatureStateTable.version, existing.version),
|
|
103
|
-
),
|
|
104
|
-
)
|
|
105
|
-
.returning();
|
|
104
|
+
const updated = await updateFeatureToggleOptimistic(ctx.db, {
|
|
105
|
+
enabled,
|
|
106
|
+
updatedBy: event.user.id,
|
|
107
|
+
updatedAt: Temporal.Now.instant(),
|
|
108
|
+
featureName,
|
|
109
|
+
expectedVersion: existing.version,
|
|
110
|
+
});
|
|
106
111
|
|
|
107
112
|
if (updated.length === 0) {
|
|
108
113
|
return writeFailure(
|
|
@@ -147,6 +152,13 @@ export function createSetWriteHandler(getRuntime: () => GlobalFeatureToggleRunti
|
|
|
147
152
|
// the `toggle-cache-sync` MSP (see feature-toggles-feature.ts). Both
|
|
148
153
|
// paths are idempotent — Map.set is last-write-wins and the DB is
|
|
149
154
|
// the source of truth after boot-time initialize().
|
|
155
|
+
if (!getRuntime) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
"[feature-toggles] set-handler called but createFeatureTogglesFeature " +
|
|
158
|
+
"was wired up without `getRuntime`. Wire the accessor in your app-config " +
|
|
159
|
+
"(production: `() => runtime` after buildServer; tests: createLateBoundHolder.get).",
|
|
160
|
+
);
|
|
161
|
+
}
|
|
150
162
|
getRuntime().apply(featureName, enabled);
|
|
151
163
|
|
|
152
164
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import {
|
|
3
4
|
computeEffectiveFeatures,
|
|
@@ -25,12 +26,8 @@ export class GlobalFeatureToggleRuntime {
|
|
|
25
26
|
) {}
|
|
26
27
|
|
|
27
28
|
async initialize(): Promise<void> {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
featureName: globalFeatureStateTable.featureName,
|
|
31
|
-
enabled: globalFeatureStateTable.enabled,
|
|
32
|
-
})
|
|
33
|
-
.from(globalFeatureStateTable);
|
|
29
|
+
type Row = { featureName: string; enabled: boolean };
|
|
30
|
+
const rows = await selectMany<Row>(this.db, globalFeatureStateTable);
|
|
34
31
|
this.snapshot = new Map(rows.map((r) => [r.featureName, r.enabled]));
|
|
35
32
|
}
|
|
36
33
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Plugin-API-shaped file-foundation. Provider-specific configs/secrets
|
|
3
3
|
// are tested in their own provider-feature (file-provider-s3/__tests__).
|
|
4
4
|
|
|
5
|
-
import { describe, expect, test } from "
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
6
|
import { fileFoundationFeature } from "../feature";
|
|
7
7
|
|
|
8
8
|
describe("fileFoundationFeature — shape", () => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// provider-factory through the dispatcher so the real config-resolver
|
|
3
3
|
// + secrets-context + tenant-scoped reads are exercised.
|
|
4
4
|
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
5
6
|
import { randomBytes } from "node:crypto";
|
|
6
7
|
import { createEncryptionProvider, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
7
8
|
import { defineFeature, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -19,7 +20,6 @@ import {
|
|
|
19
20
|
createMutableMasterKeyProvider,
|
|
20
21
|
type MutableMasterKeyProvider,
|
|
21
22
|
} from "@cosmicdrift/kumiko-framework/testing";
|
|
22
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
23
23
|
import { z } from "zod";
|
|
24
24
|
import { createConfigFeature } from "../../config";
|
|
25
25
|
import { ConfigHandlers } from "../../config/constants";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// feature.ts contract tests for file-provider-inmemory.
|
|
2
2
|
|
|
3
|
-
import { describe, expect, test } from "
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
4
|
import { clearStorage, fileProviderInMemoryFeature, listKeys } from "../feature";
|
|
5
5
|
|
|
6
6
|
describe("fileProviderInMemoryFeature — shape", () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// feature.ts contract tests for file-provider-s3.
|
|
2
2
|
|
|
3
|
-
import { describe, expect, test } from "
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
4
|
import { fileProviderS3Feature, S3_SECRET_ACCESS_KEY } from "../feature";
|
|
5
5
|
|
|
6
6
|
describe("fileProviderS3Feature — shape", () => {
|
|
@@ -12,8 +12,14 @@
|
|
|
12
12
|
import { defineFeature, EXT_USER_DATA } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
13
|
import { FILE_UPLOADED_EVENT_TYPE, fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
|
|
14
14
|
import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
|
|
16
|
+
// Native dialect exposes column metadata on the `columns` array (EntityTableMeta)
|
|
17
|
+
// and on the Symbol.for("kumiko:schema:Columns") map (compat shape). Tests use the
|
|
18
|
+
// Symbol map because keys are JS field-names (camelCase), matching what
|
|
19
|
+
// feature-entity definitions declare.
|
|
20
|
+
const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
|
|
21
|
+
|
|
22
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
17
23
|
import { createFilesFeature, fileRefEntity } from "../feature";
|
|
18
24
|
|
|
19
25
|
let stack: TestStack;
|
|
@@ -101,11 +107,16 @@ describe("files :: cross-feature behavior (F1, S1.7)", () => {
|
|
|
101
107
|
});
|
|
102
108
|
|
|
103
109
|
describe("files :: DDL-Konsistenz (M3, S1.7)", () => {
|
|
104
|
-
//
|
|
105
|
-
// Symbol
|
|
106
|
-
//
|
|
110
|
+
// The native dialect's SchemaTable exposes its column map via the
|
|
111
|
+
// Symbol.for("kumiko:schema:Columns") metadata (kept for back-compat with
|
|
112
|
+
// anything that previously introspected pgTable that way). Keys are
|
|
113
|
+
// JS field-names — exactly the level feature-entity declarations live at.
|
|
107
114
|
function pgColumnNames(): Set<string> {
|
|
108
|
-
|
|
115
|
+
const cols = (fileRefsTable as unknown as Record<symbol, unknown>)[KUMIKO_COLUMNS_SYMBOL];
|
|
116
|
+
if (typeof cols !== "object" || cols === null) {
|
|
117
|
+
throw new Error("files.integration: fileRefsTable has no kumiko:schema:Columns symbol");
|
|
118
|
+
}
|
|
119
|
+
return new Set(Object.keys(cols as Record<string, unknown>));
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
test("Feature-Entity-Felder matchen die Framework-pgTable column-set", () => {
|
|
@@ -117,7 +128,7 @@ describe("files :: DDL-Konsistenz (M3, S1.7)", () => {
|
|
|
117
128
|
// Vergleich: alle Feature-Felder muessen als Spalten in der pgTable
|
|
118
129
|
// existieren (umgekehrt darf pgTable framework-managed Spalten haben
|
|
119
130
|
// wie tenantId/createdAt/updatedAt/deletedAt — die deklariert das
|
|
120
|
-
// Framework automatisch beim
|
|
131
|
+
// Framework automatisch beim buildEntityTable-Mapping).
|
|
121
132
|
const pgColumns = pgColumnNames();
|
|
122
133
|
const featureFields = Object.keys(fileRefEntity.fields);
|
|
123
134
|
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
// in Sprint 5 — Tenant-Lifecycle löscht alle FileRefs.
|
|
26
26
|
// 3. Boot-Validation für PII-Annotations greift (fileName, originalName).
|
|
27
27
|
//
|
|
28
|
-
// Kein
|
|
28
|
+
// Kein buildEntityTable hier — die Mapping-Tabelle existiert schon im
|
|
29
29
|
// Framework. Drizzle-Reads in den Sprint-2+-Hooks gehen direkt über
|
|
30
30
|
// `fileRefsTable` aus `@cosmicdrift/kumiko-framework/files`.
|
|
31
31
|
//
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { parseS3EnvConfig } from "../env-helper";
|
|
3
3
|
|
|
4
4
|
// Tests run against real process.env — we snapshot + restore per-test so
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
1
2
|
import type { FileStorageProvider } from "@cosmicdrift/kumiko-framework/files";
|
|
2
3
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
3
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
4
4
|
import { createS3ProviderFromEnv } from "../env-helper";
|
|
5
5
|
import { createS3Provider } from "../s3-provider";
|
|
6
6
|
|