@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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler, SYSTEM_ROLE } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
3
|
-
import { eq } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { tenantMembershipsTable } from "../membership-table";
|
|
6
6
|
|
|
@@ -11,10 +11,7 @@ export const membershipsQuery = defineQueryHandler({
|
|
|
11
11
|
// directly by tenant admins managing memberships in the admin UI.
|
|
12
12
|
access: { roles: [SYSTEM_ROLE, "SystemAdmin"] },
|
|
13
13
|
handler: async (query, ctx) => {
|
|
14
|
-
const rows = await ctx.db
|
|
15
|
-
?.select()
|
|
16
|
-
.from(tenantMembershipsTable)
|
|
17
|
-
.where(eq(tenantMembershipsTable.userId, query.payload.userId));
|
|
14
|
+
const rows = await selectMany(ctx.db, tenantMembershipsTable, { userId: query.payload.userId });
|
|
18
15
|
|
|
19
16
|
return rows.map((row) => ({
|
|
20
17
|
...row,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { createEventStoreExecutor, type DbRow } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import { defineWriteHandler, withResponseData } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
4
|
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
-
import { eq } from "drizzle-orm";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { tenantMembershipEntity, tenantMembershipsTable } from "../membership-table";
|
|
7
7
|
|
|
@@ -15,12 +15,10 @@ export const removeMemberWrite = defineWriteHandler({
|
|
|
15
15
|
access: { roles: ["SystemAdmin"] },
|
|
16
16
|
handler: async (event, ctx) => {
|
|
17
17
|
const db = ctx.db;
|
|
18
|
-
const existing = await fetchOne(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
eq(tenantMembershipsTable.tenantId, event.payload.tenantId),
|
|
23
|
-
);
|
|
18
|
+
const existing = await fetchOne(db, tenantMembershipsTable, {
|
|
19
|
+
userId: event.payload.userId,
|
|
20
|
+
tenantId: event.payload.tenantId,
|
|
21
|
+
});
|
|
24
22
|
if (!existing) {
|
|
25
23
|
return writeFailure(
|
|
26
24
|
new NotFoundError("membership", undefined, {
|
|
@@ -1,40 +1,30 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler, SYSTEM_ROLE } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { eq } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { tenantMembershipsTable } from "../membership-table";
|
|
5
5
|
|
|
6
6
|
// Cross-feature query: resolve user IDs by tenantId or userId.
|
|
7
7
|
// Other features (delivery, jobs, etc.) use this to get user lists
|
|
8
8
|
// without knowing about membership internals.
|
|
9
|
-
//
|
|
10
|
-
// Examples:
|
|
11
|
-
// { tenantId: 1 } → all user IDs in tenant 1
|
|
12
|
-
// { userId: 5 } → [5] if member of any tenant, [] if not
|
|
13
9
|
export const resolveUserIdsQuery = defineQueryHandler({
|
|
14
10
|
name: "resolveUserIds",
|
|
15
11
|
schema: z.object({
|
|
16
12
|
tenantId: z.string().optional(),
|
|
17
13
|
userId: z.string().optional(),
|
|
18
14
|
}),
|
|
19
|
-
// System-internal: invoked by other features (delivery, jobs) through queryAs(systemUser, ...).
|
|
20
|
-
// Never called directly by an end-user request.
|
|
21
15
|
access: { roles: [SYSTEM_ROLE] },
|
|
22
16
|
handler: async (query, ctx) => {
|
|
23
17
|
const { tenantId, userId } = query.payload;
|
|
24
18
|
|
|
25
19
|
if (tenantId !== undefined) {
|
|
26
|
-
const rows = await ctx.db
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return rows.map((r) => r["userId"] as number); // @cast-boundary db-row
|
|
20
|
+
const rows = await selectMany<{ userId: number }>(ctx.db, tenantMembershipsTable, {
|
|
21
|
+
tenantId,
|
|
22
|
+
});
|
|
23
|
+
return rows.map((r) => r.userId);
|
|
31
24
|
}
|
|
32
25
|
|
|
33
26
|
if (userId !== undefined) {
|
|
34
|
-
const rows = await ctx.db
|
|
35
|
-
.select({ userId: tenantMembershipsTable.userId })
|
|
36
|
-
.from(tenantMembershipsTable)
|
|
37
|
-
.where(eq(tenantMembershipsTable.userId, userId));
|
|
27
|
+
const rows = await selectMany(ctx.db, tenantMembershipsTable, { userId });
|
|
38
28
|
return rows.length > 0 ? [userId] : [];
|
|
39
29
|
}
|
|
40
30
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { createEventStoreExecutor, type DbRow } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import { defineWriteHandler, withResponseData } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
4
|
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
-
import { eq } from "drizzle-orm";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { tenantMembershipEntity, tenantMembershipsTable } from "../membership-table";
|
|
7
7
|
|
|
@@ -23,12 +23,10 @@ export const updateMemberRolesWrite = defineWriteHandler({
|
|
|
23
23
|
access: { roles: ["system", "SystemAdmin"] },
|
|
24
24
|
handler: async (event, ctx) => {
|
|
25
25
|
const db = ctx.db;
|
|
26
|
-
const existing = await fetchOne(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
eq(tenantMembershipsTable.tenantId, event.payload.tenantId),
|
|
31
|
-
);
|
|
26
|
+
const existing = await fetchOne(db, tenantMembershipsTable, {
|
|
27
|
+
userId: event.payload.userId,
|
|
28
|
+
tenantId: event.payload.tenantId,
|
|
29
|
+
});
|
|
32
30
|
if (!existing) {
|
|
33
31
|
return writeFailure(
|
|
34
32
|
new NotFoundError("membership", undefined, {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// pending → re-use existing row + refresh Redis-token + send mail
|
|
23
23
|
// (analog zu signup-Resend).
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
26
26
|
import {
|
|
27
27
|
createEntity,
|
|
28
28
|
createSelectField,
|
|
@@ -88,7 +88,4 @@ export const tenantInvitationEntity = createEntity({
|
|
|
88
88
|
],
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
export const tenantInvitationsTable =
|
|
92
|
-
"tenant-invitation",
|
|
93
|
-
tenantInvitationEntity,
|
|
94
|
-
);
|
|
91
|
+
export const tenantInvitationsTable = buildEntityTable("tenant-invitation", tenantInvitationEntity);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
|
|
4
4
|
// Membership is event-sourced. Each (userId, tenantId) pair is its own
|
|
@@ -13,7 +13,7 @@ import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/eng
|
|
|
13
13
|
// database level independent of the handler lookup.
|
|
14
14
|
//
|
|
15
15
|
// Single-Source-of-Truth: `tenantMembershipEntity`. Die DB-Tabelle wird
|
|
16
|
-
// aus der EntityDefinition über
|
|
16
|
+
// aus der EntityDefinition über buildEntityTable abgeleitet, der
|
|
17
17
|
// unique-Index ist via entity.indexes deklariert.
|
|
18
18
|
export const tenantMembershipEntity = createEntity({
|
|
19
19
|
table: "read_tenant_memberships",
|
|
@@ -29,7 +29,4 @@ export const tenantMembershipEntity = createEntity({
|
|
|
29
29
|
],
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
export const tenantMembershipsTable =
|
|
33
|
-
"tenant-membership",
|
|
34
|
-
tenantMembershipEntity,
|
|
35
|
-
);
|
|
32
|
+
export const tenantMembershipsTable = buildEntityTable("tenant-membership", tenantMembershipEntity);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import {
|
|
3
3
|
createBooleanField,
|
|
4
4
|
createEntity,
|
|
@@ -24,4 +24,4 @@ export const tenantEntity = createEntity({
|
|
|
24
24
|
indexes: [{ unique: true, columns: ["key"] }],
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
export const tenantTable =
|
|
27
|
+
export const tenantTable = buildEntityTable("tenant", tenantEntity);
|
package/src/tenant/seeding.ts
CHANGED
|
@@ -31,16 +31,15 @@
|
|
|
31
31
|
// Fixture-seeding prioritises "make the state exist" over "detect duplicate
|
|
32
32
|
// seeding", which is usually a test-author bug we don't need to surface.
|
|
33
33
|
|
|
34
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
34
35
|
import {
|
|
35
36
|
createEventStoreExecutor,
|
|
36
37
|
createTenantDb,
|
|
37
|
-
type
|
|
38
|
-
fetchOne,
|
|
38
|
+
type DbRunner,
|
|
39
39
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
40
40
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
41
|
-
import {
|
|
41
|
+
import { getAggregateStreamMaxVersion } from "@cosmicdrift/kumiko-framework/event-store";
|
|
42
42
|
import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
43
|
-
import { eq, max as maxFn } from "drizzle-orm";
|
|
44
43
|
import { tenantMembershipEntity, tenantMembershipsTable } from "./membership-table";
|
|
45
44
|
import { tenantEntity, tenantTable } from "./schema/tenant";
|
|
46
45
|
|
|
@@ -83,7 +82,7 @@ export type SeedTenantOptions = {
|
|
|
83
82
|
* `TenantHandlers.create`, minus the SystemAdmin-access-check and minus
|
|
84
83
|
* ConflictError-on-duplicate.
|
|
85
84
|
*/
|
|
86
|
-
export async function seedTenant(db:
|
|
85
|
+
export async function seedTenant(db: DbRunner, options: SeedTenantOptions): Promise<TenantId> {
|
|
87
86
|
const by = options.by ?? TestUsers.systemAdmin;
|
|
88
87
|
// executor.create erwartet eine TenantDb (mit .insert()-API), nicht
|
|
89
88
|
// die rohe DbConnection. Auch wenn das Tenant-Aggregat selbst NICHT
|
|
@@ -92,18 +91,15 @@ export async function seedTenant(db: DbConnection, options: SeedTenantOptions):
|
|
|
92
91
|
// seedTenantMembership nötig.
|
|
93
92
|
const tdb = createTenantDb(db, by.tenantId, "system");
|
|
94
93
|
|
|
95
|
-
const existing = await fetchOne(db, tenantTable,
|
|
94
|
+
const existing = await fetchOne(db, tenantTable, { id: options.id });
|
|
96
95
|
if (existing) return options.id;
|
|
97
96
|
|
|
98
97
|
// Idempotenz: Aggregate kann im Event-Store existieren ohne Projection-Row
|
|
99
98
|
// (Projection-Drift nach rebuild, manuellem DELETE, oder async-lag). Wenn
|
|
100
99
|
// Stream-Version > 0 → kein create() — wäre version_conflict. Caller
|
|
101
100
|
// bekommt die ID, Projection wird beim nächsten Dispatcher-Cycle aufgebaut.
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
.from(eventsTable)
|
|
105
|
-
.where(eq(eventsTable.aggregateId, options.id));
|
|
106
|
-
if ((streamRow?.v ?? 0) > 0) return options.id;
|
|
101
|
+
const streamVersion = await getAggregateStreamMaxVersion(db, options.id);
|
|
102
|
+
if (streamVersion > 0) return options.id;
|
|
107
103
|
|
|
108
104
|
const result = await tenantExecutor.create(
|
|
109
105
|
{ id: options.id, key: options.key, name: options.name },
|
|
@@ -126,7 +122,7 @@ export async function seedTenant(db: DbConnection, options: SeedTenantOptions):
|
|
|
126
122
|
* ConflictError on duplicates (duplicate calls no-op).
|
|
127
123
|
*/
|
|
128
124
|
export async function seedTenantMembership(
|
|
129
|
-
db:
|
|
125
|
+
db: DbRunner,
|
|
130
126
|
options: SeedTenantMembershipOptions,
|
|
131
127
|
): Promise<void> {
|
|
132
128
|
const by = options.by ?? TestUsers.systemAdmin;
|
|
@@ -138,12 +134,10 @@ export async function seedTenantMembership(
|
|
|
138
134
|
// only certain tables get truncated. A plain executor.create would trip
|
|
139
135
|
// the (user_id, tenant_id) unique index; the fixture call-site would then
|
|
140
136
|
// have to juggle try/catch. Lookup-first keeps call-sites clean.
|
|
141
|
-
const existing = await fetchOne(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
eq(tenantMembershipsTable.tenantId, options.tenantId),
|
|
146
|
-
);
|
|
137
|
+
const existing = await fetchOne(db, tenantMembershipsTable, {
|
|
138
|
+
userId: options.userId,
|
|
139
|
+
tenantId: options.tenantId,
|
|
140
|
+
});
|
|
147
141
|
// skip: idempotent no-op — duplicate seed is expected across beforeEach-
|
|
148
142
|
// resets that don't truncate this table. Cheaper than try/catch on the
|
|
149
143
|
// unique-index, and documented in the function JSDoc above.
|
|
@@ -180,7 +180,7 @@ convenience routes (`/legal/impressum`, `/legal/datenschutz`,
|
|
|
180
180
|
## Architecture
|
|
181
181
|
|
|
182
182
|
- **Single source of truth:** `textBlockEntity` in `table.ts`.
|
|
183
|
-
The Drizzle table is derived via `
|
|
183
|
+
The Drizzle table is derived via `buildEntityTable("text-block",
|
|
184
184
|
textBlockEntity)`, the unique index on `(tenantId, slug, lang)` is
|
|
185
185
|
declared via `entity.indexes`.
|
|
186
186
|
- **Event-sourced:** the write path goes through
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
1
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
3
4
|
import {
|
|
@@ -8,7 +9,6 @@ import {
|
|
|
8
9
|
unsafeCreateEntityTable,
|
|
9
10
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
10
11
|
import { expectErrorIncludes } from "@cosmicdrift/kumiko-framework/testing";
|
|
11
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
12
12
|
import { TextContentHandlers, TextContentQueries } from "../constants";
|
|
13
13
|
import { createTextContentFeature } from "../feature";
|
|
14
14
|
import { seedTextBlock } from "../seeding";
|
|
@@ -389,7 +389,7 @@ describe("text-content :: edge-cases", () => {
|
|
|
389
389
|
{ slug: "race-test", lang: "de" },
|
|
390
390
|
tenantAdmin,
|
|
391
391
|
);
|
|
392
|
-
const finalBody = fetched!["body"];
|
|
392
|
+
const finalBody = fetched!["body"] as string;
|
|
393
393
|
expect(["from-a", "from-b", "v1"]).toContain(finalBody);
|
|
394
394
|
});
|
|
395
395
|
});
|
package/src/text-content/api.ts
CHANGED
|
@@ -8,11 +8,10 @@
|
|
|
8
8
|
// bleiben Features durch Refactorings entkoppelt — wer textBlocksTable
|
|
9
9
|
// umzieht oder die Query-Signatur ändert, muss nur die Factory anpassen.
|
|
10
10
|
|
|
11
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
11
12
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
12
|
-
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
13
13
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
14
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
15
|
-
import { eq } from "drizzle-orm";
|
|
16
15
|
import { type TextBlockRow, textBlocksTable } from "./table";
|
|
17
16
|
|
|
18
17
|
export type TextBlock = {
|
|
@@ -40,13 +39,7 @@ export type TextContentApi = {
|
|
|
40
39
|
export function createTextContentApi(db: DbConnection): TextContentApi {
|
|
41
40
|
return {
|
|
42
41
|
getBlock: async ({ tenantId, slug, lang }) => {
|
|
43
|
-
const row = await fetchOne<TextBlockRow>(
|
|
44
|
-
db,
|
|
45
|
-
textBlocksTable,
|
|
46
|
-
eq(textBlocksTable["tenantId"], tenantId),
|
|
47
|
-
eq(textBlocksTable["slug"], slug),
|
|
48
|
-
eq(textBlocksTable["lang"], lang),
|
|
49
|
-
);
|
|
42
|
+
const row = await fetchOne<TextBlockRow>(db, textBlocksTable, { tenantId, slug, lang });
|
|
50
43
|
if (!row) return null;
|
|
51
44
|
return {
|
|
52
45
|
slug: row.slug,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { AccessDeniedError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
-
import { eq } from "drizzle-orm";
|
|
5
4
|
import { z } from "zod";
|
|
6
5
|
import { type TextBlockRow, textBlocksTable } from "../table";
|
|
7
6
|
|
|
@@ -35,13 +34,11 @@ export const bySlugQuery = defineQueryHandler({
|
|
|
35
34
|
});
|
|
36
35
|
}
|
|
37
36
|
const tenantId = override ?? query.user.tenantId;
|
|
38
|
-
const row = await fetchOne<TextBlockRow>(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
eq(textBlocksTable["lang"], query.payload.lang),
|
|
44
|
-
);
|
|
37
|
+
const row = await fetchOne<TextBlockRow>(ctx.db, textBlocksTable, {
|
|
38
|
+
tenantId,
|
|
39
|
+
slug: query.payload.slug,
|
|
40
|
+
lang: query.payload.lang,
|
|
41
|
+
});
|
|
45
42
|
|
|
46
43
|
if (!row) return null;
|
|
47
44
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { castTenantRows } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
4
|
import { AccessDeniedError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
-
import { eq } from "drizzle-orm";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { type TextBlockRow, textBlocksTable } from "../table";
|
|
7
7
|
|
|
@@ -42,7 +42,7 @@ export const byTenantQuery = defineQueryHandler({
|
|
|
42
42
|
}
|
|
43
43
|
const tenantId = override ?? query.user.tenantId;
|
|
44
44
|
const rows = castTenantRows<TextBlockRow>(
|
|
45
|
-
await ctx.db
|
|
45
|
+
await selectMany(ctx.db, textBlocksTable, { tenantId: tenantId }),
|
|
46
46
|
);
|
|
47
47
|
return {
|
|
48
48
|
blocks: rows.map((row) => ({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import { defineWriteHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
4
|
import { AccessDeniedError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
-
import { eq } from "drizzle-orm";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { type TextBlockRow, textBlockEntity, textBlocksTable } from "../table";
|
|
7
7
|
|
|
@@ -86,13 +86,11 @@ export const setWrite = defineWriteHandler({
|
|
|
86
86
|
const executorUser =
|
|
87
87
|
override !== undefined ? { ...event.user, tenantId: override as TenantId } : event.user; // @cast-boundary engine-bridge
|
|
88
88
|
|
|
89
|
-
const existing = await fetchOne<TextBlockRow>(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
eq(textBlocksTable["lang"], event.payload.lang),
|
|
95
|
-
);
|
|
89
|
+
const existing = await fetchOne<TextBlockRow>(db, textBlocksTable, {
|
|
90
|
+
tenantId,
|
|
91
|
+
slug: event.payload.slug,
|
|
92
|
+
lang: event.payload.lang,
|
|
93
|
+
});
|
|
96
94
|
|
|
97
95
|
// V.1.4 folder: optional + null erlaubt (root-node). Optional-Chain
|
|
98
96
|
// mapped undefined → null damit drizzle nullable-column konsistent
|
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
// aber ohne Access-Check. Idempotent: zweiter Call mit gleichem
|
|
4
4
|
// (tenantId, slug, lang) updated den existing Block.
|
|
5
5
|
|
|
6
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
6
7
|
import {
|
|
7
8
|
createEventStoreExecutor,
|
|
8
9
|
createTenantDb,
|
|
9
10
|
type DbConnection,
|
|
10
|
-
fetchOne,
|
|
11
11
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
12
12
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
13
|
import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
14
|
-
import { eq } from "drizzle-orm";
|
|
15
14
|
import { type TextBlockRow, textBlockEntity, textBlocksTable } from "./table";
|
|
16
15
|
|
|
17
16
|
const executor = createEventStoreExecutor(textBlocksTable, textBlockEntity, {
|
|
@@ -47,13 +46,11 @@ export async function seedTextBlock(
|
|
|
47
46
|
// checks (tenant-scope-validation) greifen.
|
|
48
47
|
const tdb = createTenantDb(db, opts.tenantId, "system");
|
|
49
48
|
|
|
50
|
-
const existing = await fetchOne<TextBlockRow>(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
eq(textBlocksTable["lang"], opts.lang),
|
|
56
|
-
);
|
|
49
|
+
const existing = await fetchOne<TextBlockRow>(db, textBlocksTable, {
|
|
50
|
+
tenantId: opts.tenantId,
|
|
51
|
+
slug: opts.slug,
|
|
52
|
+
lang: opts.lang,
|
|
53
|
+
});
|
|
57
54
|
|
|
58
55
|
const folder = opts.folder ?? null;
|
|
59
56
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
|
|
4
4
|
// TextBlock — generischer Container für statische Texte (legal pages,
|
|
@@ -29,7 +29,7 @@ export const textBlockEntity = createEntity({
|
|
|
29
29
|
],
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
export const textBlocksTable =
|
|
32
|
+
export const textBlocksTable = buildEntityTable("text-block", textBlockEntity);
|
|
33
33
|
|
|
34
34
|
// Concrete Row-Type — single-source dafür dass die unknown-Werte die
|
|
35
35
|
// Drizzle aus `Record<string, unknown>` liefert genau einmal benannt
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
// mock.module eretzt imports für alle Konsumenten — statische imports
|
|
3
|
+
// vor mock.module sehen die gemockte Version weil Bun am Loader-Level
|
|
4
|
+
// intercepted. useShellUser ist hier ein Mock-Objekt.
|
|
5
|
+
import { useShellUser } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";
|
|
3
6
|
import {
|
|
4
7
|
createStaticLocaleResolver,
|
|
5
8
|
LocaleProvider,
|
|
@@ -8,37 +11,26 @@ import {
|
|
|
8
11
|
import { defaultPrimitives } from "@cosmicdrift/kumiko-renderer-web";
|
|
9
12
|
import { render, screen } from "@testing-library/react";
|
|
10
13
|
import type { ReactNode } from "react";
|
|
11
|
-
import { describe, expect, test, vi } from "vitest";
|
|
12
14
|
import { textContentClient } from "../client-plugin";
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// Antworten. Memory `[Keine Fake-Tests]`: wir testen echtes Rendering,
|
|
17
|
-
// nicht nur die canWrite-Bedingung.
|
|
18
|
-
vi.mock("@cosmicdrift/kumiko-bundled-features/auth-email-password/web", () => ({
|
|
19
|
-
useShellUser: vi.fn(),
|
|
16
|
+
mock.module("@cosmicdrift/kumiko-bundled-features/auth-email-password/web", () => ({
|
|
17
|
+
useShellUser: mock(),
|
|
20
18
|
}));
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
refetch: vi.fn(),
|
|
37
|
-
})),
|
|
38
|
-
};
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
import { useShellUser } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";
|
|
20
|
+
const actual_renderer = await import("@cosmicdrift/kumiko-renderer");
|
|
21
|
+
mock.module("@cosmicdrift/kumiko-renderer", () => ({
|
|
22
|
+
...actual_renderer,
|
|
23
|
+
useDispatcher: mock(() => ({
|
|
24
|
+
write: mock(),
|
|
25
|
+
query: mock(),
|
|
26
|
+
})),
|
|
27
|
+
useQuery: mock(() => ({
|
|
28
|
+
data: { slug: "imprint", lang: "de", title: "Impressum", body: "Inhalt" },
|
|
29
|
+
loading: false,
|
|
30
|
+
error: null,
|
|
31
|
+
refetch: mock(),
|
|
32
|
+
})),
|
|
33
|
+
}));
|
|
42
34
|
|
|
43
35
|
const TARGET = {
|
|
44
36
|
featureId: "text-content",
|
|
@@ -65,21 +57,21 @@ function Wrapper({ children }: { readonly children: ReactNode }): ReactNode {
|
|
|
65
57
|
|
|
66
58
|
describe("TextContentEditor — role-based write-access", () => {
|
|
67
59
|
test("TenantAdmin sieht Save-Button + editable inputs", () => {
|
|
68
|
-
|
|
60
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun mock function
|
|
61
|
+
(useShellUser as any).mockReturnValue({ id: "u1", roles: ["TenantAdmin"] });
|
|
69
62
|
const Editor = getEditor();
|
|
70
63
|
render(<Editor target={TARGET} onClose={() => {}} />, { wrapper: Wrapper });
|
|
71
64
|
|
|
72
|
-
// Save-Button gerendert (canWrite=true → Button.type=submit)
|
|
73
65
|
const saveButton = screen.getByRole("button", { name: /speichern/i });
|
|
74
66
|
expect(saveButton).toBeTruthy();
|
|
75
67
|
expect(saveButton.hasAttribute("disabled")).toBe(false);
|
|
76
68
|
|
|
77
|
-
// Read-only-Banner darf NICHT erscheinen
|
|
78
69
|
expect(screen.queryByText(/Read-only/)).toBeNull();
|
|
79
70
|
});
|
|
80
71
|
|
|
81
72
|
test("SystemAdmin sieht Save-Button (alternative write-role)", () => {
|
|
82
|
-
|
|
73
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun mock function
|
|
74
|
+
(useShellUser as any).mockReturnValue({ id: "u1", roles: ["SystemAdmin"] });
|
|
83
75
|
const Editor = getEditor();
|
|
84
76
|
render(<Editor target={TARGET} onClose={() => {}} />, { wrapper: Wrapper });
|
|
85
77
|
|
|
@@ -88,11 +80,8 @@ describe("TextContentEditor — role-based write-access", () => {
|
|
|
88
80
|
});
|
|
89
81
|
|
|
90
82
|
test("Editor-Role sieht Read-only-Banner + KEIN Save-Button", () => {
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
// by-slug-query (read), aber NICHT auf set.write. UI muss das
|
|
94
|
-
// explizit kommunizieren statt 403 erst beim save zu zeigen.
|
|
95
|
-
vi.mocked(useShellUser).mockReturnValue({ id: "u1", roles: ["Editor"] });
|
|
83
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun mock function
|
|
84
|
+
(useShellUser as any).mockReturnValue({ id: "u1", roles: ["Editor"] });
|
|
96
85
|
const Editor = getEditor();
|
|
97
86
|
render(<Editor target={TARGET} onClose={() => {}} />, { wrapper: Wrapper });
|
|
98
87
|
|
|
@@ -101,12 +90,8 @@ describe("TextContentEditor — role-based write-access", () => {
|
|
|
101
90
|
});
|
|
102
91
|
|
|
103
92
|
test("Admin-Role (publicstatus-Convention, ohne TenantAdmin-dual-Tag) sieht Read-only-Banner", () => {
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
// wer "Admin" allein hat (Memory `[Role-Naming-Drift]`), muss
|
|
107
|
-
// explizit auch "TenantAdmin" im JWT tragen damit der Editor
|
|
108
|
-
// schreiben darf.
|
|
109
|
-
vi.mocked(useShellUser).mockReturnValue({ id: "u1", roles: ["Admin"] });
|
|
93
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun mock function
|
|
94
|
+
(useShellUser as any).mockReturnValue({ id: "u1", roles: ["Admin"] });
|
|
110
95
|
const Editor = getEditor();
|
|
111
96
|
render(<Editor target={TARGET} onClose={() => {}} />, { wrapper: Wrapper });
|
|
112
97
|
|
|
@@ -115,7 +100,8 @@ describe("TextContentEditor — role-based write-access", () => {
|
|
|
115
100
|
});
|
|
116
101
|
|
|
117
102
|
test("Logged-out (useShellUser=undefined) sieht Read-only-Banner", () => {
|
|
118
|
-
|
|
103
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun mock function
|
|
104
|
+
(useShellUser as any).mockReturnValue(undefined);
|
|
119
105
|
const Editor = getEditor();
|
|
120
106
|
render(<Editor target={TARGET} onClose={() => {}} />, { wrapper: Wrapper });
|
|
121
107
|
|