@cosmicdrift/kumiko-bundled-features 0.16.0 → 0.19.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 +1 -1
- package/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
- package/src/cap-counter/__tests__/{cap-counter.integration.ts → cap-counter.integration.test.ts} +14 -3
- package/src/cap-counter/__tests__/enforce-cap.test.ts +8 -4
- package/src/cap-counter/__tests__/{with-cap-enforcement.integration.ts → with-cap-enforcement.integration.test.ts} +14 -3
- package/src/cap-counter/enforce-cap.ts +2 -4
- package/src/cap-counter/handlers/get-counter.query.ts +1 -3
- package/src/cap-counter/handlers/increment.write.ts +1 -2
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +1 -2
- package/src/channel-in-app/in-app-channel.ts +1 -3
- package/src/custom-fields/__tests__/cross-tenant-field-delete.integration.test.ts +177 -0
- package/src/custom-fields/__tests__/{custom-fields.integration.ts → custom-fields.integration.test.ts} +105 -0
- package/src/custom-fields/db/queries/projection.ts +33 -4
- package/src/custom-fields/db/queries/retention.ts +2 -2
- package/src/custom-fields/db/queries/user-data-rights.ts +6 -3
- package/src/custom-fields/feature.ts +10 -4
- package/src/custom-fields/handlers/delete-system-field.write.ts +5 -1
- package/src/custom-fields/handlers/delete-tenant-field.write.ts +1 -1
- package/src/custom-fields/handlers/set-custom-field.write.ts +33 -17
- package/src/custom-fields/lib/field-access.ts +39 -14
- package/src/custom-fields/lib/value-schema.ts +45 -0
- package/src/custom-fields/run-retention.ts +1 -1
- package/src/custom-fields/wire-for-entity.ts +22 -4
- package/src/custom-fields/wire-user-data-rights.ts +3 -2
- package/src/delivery/delivery-service.ts +1 -1
- package/src/delivery/types.ts +2 -2
- package/src/feature-toggles/__tests__/{feature-toggles.integration.ts → feature-toggles.integration.test.ts} +6 -6
- package/src/feature-toggles/handlers/set.write.ts +10 -8
- package/src/subscription-stripe/__tests__/{stripe-foundation.integration.ts → stripe-foundation.integration.test.ts} +7 -10
- package/src/tier-engine/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +4 -3
- package/src/user-data-rights/__tests__/{audit-log.integration.ts → audit-log.integration.test.ts} +12 -5
- package/src/user-data-rights/__tests__/{cross-data-matrix.integration.ts → cross-data-matrix.integration.test.ts} +29 -12
- package/src/user-data-rights/__tests__/{download.integration.ts → download.integration.test.ts} +15 -7
- package/src/user-data-rights/__tests__/{export-job-idempotency.integration.ts → export-job-idempotency.integration.test.ts} +13 -11
- package/src/user-data-rights/__tests__/{request-cancel-deletion.integration.ts → request-cancel-deletion.integration.test.ts} +8 -7
- package/src/user-data-rights/__tests__/{request-deletion-callback.integration.ts → request-deletion-callback.integration.test.ts} +8 -5
- package/src/user-data-rights/__tests__/{request-export.integration.ts → request-export.integration.test.ts} +6 -3
- package/src/user-data-rights/__tests__/{restriction-flow.integration.ts → restriction-flow.integration.test.ts} +11 -8
- package/src/user-data-rights/__tests__/{run-export-jobs.integration.ts → run-export-jobs.integration.test.ts} +25 -13
- package/src/user-data-rights/__tests__/{run-forget-cleanup.integration.ts → run-forget-cleanup.integration.test.ts} +6 -3
- package/src/user-data-rights/__tests__/{run-user-export.integration.ts → run-user-export.integration.test.ts} +6 -3
- package/src/user-data-rights/__tests__/{user-data-rights.integration.ts → user-data-rights.integration.test.ts} +3 -1
- package/src/user-data-rights/db/queries/export-jobs.ts +6 -5
- package/src/user-data-rights/db/queries/forget-cleanup.ts +11 -6
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +5 -10
- package/src/user-data-rights/handlers/export-status.query.ts +12 -12
- package/src/user-data-rights/run-export-jobs.ts +2 -5
- package/src/user-data-rights/run-forget-cleanup.ts +0 -1
- package/src/user-data-rights-defaults/__tests__/{user-data-rights-defaults.integration.ts → user-data-rights-defaults.integration.test.ts} +2 -0
- /package/src/__tests__/{es-ops-e2e.integration.ts → es-ops-e2e.integration.test.ts} +0 -0
- /package/src/audit/__tests__/{audit.integration.ts → audit.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{account-lockout-no-redis.integration.ts → account-lockout-no-redis.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{account-lockout.integration.ts → account-lockout.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{auth-claims.integration.ts → auth-claims.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{auth.integration.ts → auth.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{email-verification.integration.ts → email-verification.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{identity-v3-login.integration.ts → identity-v3-login.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{invite-flow.integration.ts → invite-flow.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{multi-roles.integration.ts → multi-roles.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{password-reset.integration.ts → password-reset.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{public-routes-rate-limit.integration.ts → public-routes-rate-limit.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{seed-admin.integration.ts → seed-admin.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{session-callbacks.integration.ts → session-callbacks.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{session-strict-mode.integration.ts → session-strict-mode.integration.test.ts} +0 -0
- /package/src/auth-email-password/__tests__/{signup-flow.integration.ts → signup-flow.integration.test.ts} +0 -0
- /package/src/billing-foundation/__tests__/{billing-foundation.integration.ts → billing-foundation.integration.test.ts} +0 -0
- /package/src/compliance-profiles/__tests__/{compliance-profiles.integration.ts → compliance-profiles.integration.test.ts} +0 -0
- /package/src/compliance-profiles/__tests__/{seeding.integration.ts → seeding.integration.test.ts} +0 -0
- /package/src/config/__tests__/{cascade.integration.ts → cascade.integration.test.ts} +0 -0
- /package/src/config/__tests__/{config.integration.ts → config.integration.test.ts} +0 -0
- /package/src/custom-fields/__tests__/{audit-integration.integration.ts → audit-integration.integration.test.ts} +0 -0
- /package/src/custom-fields/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +0 -0
- /package/src/custom-fields/__tests__/{quota.integration.ts → quota.integration.test.ts} +0 -0
- /package/src/custom-fields/__tests__/{retention.integration.ts → retention.integration.test.ts} +0 -0
- /package/src/custom-fields/__tests__/{user-data-rights.integration.ts → user-data-rights.integration.test.ts} +0 -0
- /package/src/data-retention/__tests__/{data-retention.integration.ts → data-retention.integration.test.ts} +0 -0
- /package/src/data-retention/__tests__/{policy-for.integration.ts → policy-for.integration.test.ts} +0 -0
- /package/src/delivery/__tests__/{delivery-events.integration.ts → delivery-events.integration.test.ts} +0 -0
- /package/src/delivery/__tests__/{delivery.integration.ts → delivery.integration.test.ts} +0 -0
- /package/src/file-foundation/__tests__/{file-foundation.integration.ts → file-foundation.integration.test.ts} +0 -0
- /package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +0 -0
- /package/src/files-provider-s3/__tests__/{s3-provider.integration.ts → s3-provider.integration.test.ts} +0 -0
- /package/src/jobs/__tests__/{job-system-user.integration.ts → job-system-user.integration.test.ts} +0 -0
- /package/src/jobs/__tests__/{jobs-events.integration.ts → jobs-events.integration.test.ts} +0 -0
- /package/src/jobs/__tests__/{jobs-feature.integration.ts → jobs-feature.integration.test.ts} +0 -0
- /package/src/legal-pages/__tests__/{legal-pages.integration.ts → legal-pages.integration.test.ts} +0 -0
- /package/src/mail-foundation/__tests__/{mail-foundation.integration.ts → mail-foundation.integration.test.ts} +0 -0
- /package/src/rate-limiting/__tests__/{rate-limiting.integration.ts → rate-limiting.integration.test.ts} +0 -0
- /package/src/renderer-foundation/__tests__/{collect-plugins.integration.ts → collect-plugins.integration.test.ts} +0 -0
- /package/src/secrets/__tests__/{rotate.integration.ts → rotate.integration.test.ts} +0 -0
- /package/src/secrets/__tests__/{secrets-events.integration.ts → secrets-events.integration.test.ts} +0 -0
- /package/src/secrets/__tests__/{secrets.integration.ts → secrets.integration.test.ts} +0 -0
- /package/src/sessions/__tests__/{cleanup.integration.ts → cleanup.integration.test.ts} +0 -0
- /package/src/sessions/__tests__/{password-auto-revoke.integration.ts → password-auto-revoke.integration.test.ts} +0 -0
- /package/src/sessions/__tests__/{sessions.integration.ts → sessions.integration.test.ts} +0 -0
- /package/src/subscription-mollie/__tests__/{mollie-foundation.integration.ts → mollie-foundation.integration.test.ts} +0 -0
- /package/src/template-resolver/__tests__/{handlers.integration.ts → handlers.integration.test.ts} +0 -0
- /package/src/template-resolver/__tests__/{template-resolver.integration.ts → template-resolver.integration.test.ts} +0 -0
- /package/src/tenant/__tests__/{multi-tenant.integration.ts → multi-tenant.integration.test.ts} +0 -0
- /package/src/tenant/__tests__/{seed-testing.integration.ts → seed-testing.integration.test.ts} +0 -0
- /package/src/tenant/__tests__/{tenant.integration.ts → tenant.integration.test.ts} +0 -0
- /package/src/text-content/__tests__/{text-content.integration.ts → text-content.integration.test.ts} +0 -0
- /package/src/tier-engine/__tests__/{auto-default-tier.integration.ts → auto-default-tier.integration.test.ts} +0 -0
- /package/src/tier-engine/__tests__/{tier-engine.integration.ts → tier-engine.integration.test.ts} +0 -0
- /package/src/user/__tests__/{seed-testing.integration.ts → seed-testing.integration.test.ts} +0 -0
- /package/src/user/__tests__/{user.integration.ts → user.integration.test.ts} +0 -0
|
@@ -2,7 +2,8 @@ import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
2
2
|
import { failNotFound, failUnprocessable } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { customFieldsFeature } from "../feature";
|
|
5
|
-
import {
|
|
5
|
+
import { fieldWriteAccessDeniedRoles, loadFieldDefinition } from "../lib/field-access";
|
|
6
|
+
import { buildCustomFieldValueSchema } from "../lib/value-schema";
|
|
6
7
|
|
|
7
8
|
export const setCustomFieldPayloadSchema = z.object({
|
|
8
9
|
entityName: z.string().min(1).max(64),
|
|
@@ -24,16 +25,18 @@ export type SetCustomFieldPayload = z.infer<typeof setCustomFieldPayloadSchema>;
|
|
|
24
25
|
// concurrent writes auf gleiches Field gehen beide durch (Plan-Doc v2
|
|
25
26
|
// Concurrency-Tabelle).
|
|
26
27
|
//
|
|
27
|
-
// **
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
28
|
+
// **Write-Pfad (Single-Fetch)** — eine fieldDefinition-Ladung, drei Gates:
|
|
29
|
+
// 1. Definition fehlt → 404.
|
|
30
|
+
// 2. field-access (T1.5b): fieldAccess.write-Rollen müssen intersecten, sonst
|
|
31
|
+
// 403/422. Handler-level RBAC (TenantAdmin/Member) bleibt zusätzlich.
|
|
32
|
+
// 3. Value-Validation (Builder-Reuse): der Wert wird gegen das aus
|
|
33
|
+
// serializedField rehydrierte fieldToZod-Schema geparst. Type-Mismatch →
|
|
34
|
+
// 422, KEIN Event entsteht (Projection bleibt typed — Plan-Doc
|
|
35
|
+
// Stammfeld-Identität). `value: null` auf einem typisierten Feld ist ein
|
|
36
|
+
// Type-Mismatch → 422; zum Entfernen eines Werts dient clear-custom-field.
|
|
32
37
|
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// gesetzt ist, muss der calling user mindestens eine der Rollen halten.
|
|
36
|
-
// Handler-level RBAC (TenantAdmin/Member) bleibt zusätzlich.
|
|
38
|
+
// Scope: NUR Type-Validation. Required-on-set + Default-Application sind
|
|
39
|
+
// out-of-scope (Plan-Doc "Stammfeld-Identität" listet sie als eigene Zeilen).
|
|
37
40
|
export const setCustomFieldHandler: WriteHandlerDef = {
|
|
38
41
|
name: "set-custom-field",
|
|
39
42
|
schema: setCustomFieldPayloadSchema,
|
|
@@ -41,23 +44,36 @@ export const setCustomFieldHandler: WriteHandlerDef = {
|
|
|
41
44
|
handler: async (event, ctx) => {
|
|
42
45
|
const payload = event.payload as SetCustomFieldPayload; // @cast-boundary engine-payload
|
|
43
46
|
|
|
44
|
-
const
|
|
47
|
+
const loaded = await loadFieldDefinition(
|
|
45
48
|
ctx.db,
|
|
46
49
|
event.user.tenantId,
|
|
47
50
|
payload.entityName,
|
|
48
51
|
payload.fieldKey,
|
|
49
|
-
event.user.roles,
|
|
50
52
|
);
|
|
51
|
-
if (!
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
if (!loaded.found) {
|
|
54
|
+
return failNotFound("fieldDefinition", payload.fieldKey);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const deniedRoles = fieldWriteAccessDeniedRoles(loaded.field, event.user.roles);
|
|
58
|
+
if (deniedRoles) {
|
|
55
59
|
return failUnprocessable("field_access_denied", {
|
|
56
60
|
fieldKey: payload.fieldKey,
|
|
57
|
-
requiredRoles:
|
|
61
|
+
requiredRoles: deniedRoles,
|
|
58
62
|
});
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
const valueSchema = buildCustomFieldValueSchema(loaded.field);
|
|
66
|
+
if (valueSchema) {
|
|
67
|
+
const parsed = valueSchema.safeParse(payload.value);
|
|
68
|
+
if (!parsed.success) {
|
|
69
|
+
return failUnprocessable("custom_field_value_invalid", {
|
|
70
|
+
fieldKey: payload.fieldKey,
|
|
71
|
+
fieldType: loaded.field?.type,
|
|
72
|
+
issues: parsed.error.issues.map((i) => i.message),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
// Emit customField.set on host-aggregate stream. unsafeAppendEvent
|
|
62
78
|
// (statt strict appendEvent) weil event-type-map keine cross-feature-
|
|
63
79
|
// augmentation für diesen event-typ hat — wir nutzen den qualified
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// T1.5b — per-field write access-check for the set/clear handlers.
|
|
2
|
+
// Plus single-fetch loader so set-custom-field can run access-check AND
|
|
3
|
+
// value-validation off one DB read (no double fetch).
|
|
2
4
|
|
|
3
5
|
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
4
6
|
import { selectSerializedFieldDefinition } from "../db/queries/field-access";
|
|
5
|
-
import { parseSerializedField } from "./parse-serialized-field";
|
|
7
|
+
import { parseSerializedField, type SerializedFieldShape } from "./parse-serialized-field";
|
|
6
8
|
|
|
7
9
|
export type FieldAccessCheckResult =
|
|
8
10
|
| { ok: true }
|
|
@@ -12,6 +14,37 @@ export type FieldAccessCheckResult =
|
|
|
12
14
|
requiredRoles?: ReadonlyArray<string>;
|
|
13
15
|
};
|
|
14
16
|
|
|
17
|
+
export type LoadedFieldDefinition =
|
|
18
|
+
| { found: false }
|
|
19
|
+
// `field` is null when the row exists but its serialized_field is corrupt —
|
|
20
|
+
// callers treat that as "no restriction / no schema" (lenient), distinct
|
|
21
|
+
// from `found: false` (no definition → 404).
|
|
22
|
+
| { found: true; field: SerializedFieldShape | null };
|
|
23
|
+
|
|
24
|
+
export async function loadFieldDefinition(
|
|
25
|
+
db: TenantDb,
|
|
26
|
+
tenantId: string,
|
|
27
|
+
entityName: string,
|
|
28
|
+
fieldKey: string,
|
|
29
|
+
): Promise<LoadedFieldDefinition> {
|
|
30
|
+
const serialized = await selectSerializedFieldDefinition(db, tenantId, entityName, fieldKey);
|
|
31
|
+
if (serialized === null) return { found: false };
|
|
32
|
+
return { found: true, field: parseSerializedField(serialized) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Pure access-check on an already-loaded definition. Returns the required
|
|
36
|
+
// roles when the caller is denied, or `null` when access is allowed.
|
|
37
|
+
export function fieldWriteAccessDeniedRoles(
|
|
38
|
+
field: SerializedFieldShape | null,
|
|
39
|
+
userRoles: ReadonlyArray<string>,
|
|
40
|
+
): ReadonlyArray<string> | null {
|
|
41
|
+
const required = field?.fieldAccess?.write;
|
|
42
|
+
if (!required || required.length === 0) return null;
|
|
43
|
+
return userRoles.some((role) => required.includes(role)) ? null : required;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Convenience wrapper retained for clear-custom-field (no value-validation
|
|
47
|
+
// needed there) — does the load + access-check in one call.
|
|
15
48
|
export async function checkFieldAccessForWrite(
|
|
16
49
|
db: TenantDb,
|
|
17
50
|
tenantId: string,
|
|
@@ -19,18 +52,10 @@ export async function checkFieldAccessForWrite(
|
|
|
19
52
|
fieldKey: string,
|
|
20
53
|
userRoles: ReadonlyArray<string>,
|
|
21
54
|
): Promise<FieldAccessCheckResult> {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
return { ok: false, reason: "field_definition_not_found" };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const parsed = parseSerializedField(serialized);
|
|
28
|
-
if (!parsed) return { ok: true };
|
|
55
|
+
const loaded = await loadFieldDefinition(db, tenantId, entityName, fieldKey);
|
|
56
|
+
if (!loaded.found) return { ok: false, reason: "field_definition_not_found" };
|
|
29
57
|
|
|
30
|
-
const
|
|
31
|
-
if (!
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
const hit = userRoles.some((role) => required.includes(role));
|
|
35
|
-
return hit ? { ok: true } : { ok: false, reason: "field_access_denied", requiredRoles: required };
|
|
58
|
+
const deniedRoles = fieldWriteAccessDeniedRoles(loaded.field, userRoles);
|
|
59
|
+
if (!deniedRoles) return { ok: true };
|
|
60
|
+
return { ok: false, reason: "field_access_denied", requiredRoles: deniedRoles };
|
|
36
61
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CURRENCIES,
|
|
3
|
+
type FieldDefinition,
|
|
4
|
+
fieldToZod,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import type { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// Builds a Zod schema that validates a custom-field VALUE against its
|
|
9
|
+
// fieldDefinition. Reuses the framework's `fieldToZod` (Builder-Reuse /
|
|
10
|
+
// Stammfeld-Identität, Plan-Doc) — one field-type-schema source, no drift
|
|
11
|
+
// between Stammfeld- and Custom-Field-validation.
|
|
12
|
+
//
|
|
13
|
+
// Vocabulary bridge: custom-fields expose `enum` (Plan + `r.field.enum([...])`)
|
|
14
|
+
// where the framework's FieldDefinition calls the equivalent type `select`
|
|
15
|
+
// with an `options` array. This single boundary translates it; everything
|
|
16
|
+
// else is already FieldDefinition-shaped (the serialized dehydrated builder).
|
|
17
|
+
//
|
|
18
|
+
// Returns `null` when the serialized field is unparseable or names a type
|
|
19
|
+
// `fieldToZod` cannot interpret — callers then skip value-validation rather
|
|
20
|
+
// than hard-rejecting a field they cannot understand.
|
|
21
|
+
export function buildCustomFieldValueSchema(parsedField: unknown): z.ZodTypeAny | null {
|
|
22
|
+
if (!parsedField || typeof parsedField !== "object") return null;
|
|
23
|
+
const obj = parsedField as Record<string, unknown>;
|
|
24
|
+
if (typeof obj["type"] !== "string") return null;
|
|
25
|
+
|
|
26
|
+
const fieldDef =
|
|
27
|
+
obj["type"] === "enum"
|
|
28
|
+
? { ...obj, type: "select", options: obj["values"] ?? obj["options"] ?? [] }
|
|
29
|
+
: obj;
|
|
30
|
+
|
|
31
|
+
// fieldToZod's money case validates `currency` against the passed list, not
|
|
32
|
+
// a field-level key — so surface the field's own currency when it declares
|
|
33
|
+
// one, else fall back to the framework defaults.
|
|
34
|
+
const currencies =
|
|
35
|
+
typeof obj["currency"] === "string" ? [obj["currency"] as string] : DEFAULT_CURRENCIES;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// @cast-boundary serialized-field is the dehydrated r.field.X() output =
|
|
39
|
+
// a FieldDefinition; fieldToZod reads only its type-specific keys (the
|
|
40
|
+
// extra fieldAccess/sensitive/retention/label keys are ignored).
|
|
41
|
+
return fieldToZod(fieldDef as unknown as FieldDefinition, currencies);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -132,7 +132,7 @@ export async function runCustomFieldsRetention(
|
|
|
132
132
|
removalsByFieldKey[key] = (removalsByFieldKey[key] ?? 0) + 1;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
await updateHostRowCustomFields(opts.db, tableName,
|
|
135
|
+
await updateHostRowCustomFields(opts.db, tableName, mutated, row.id);
|
|
136
136
|
rowsUpdated++;
|
|
137
137
|
}
|
|
138
138
|
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createJsonbField,
|
|
3
3
|
type FeatureRegistrar,
|
|
4
|
+
isSystemTenant,
|
|
4
5
|
type JsonbFieldDef,
|
|
6
|
+
type TenantId,
|
|
5
7
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
8
|
import { CUSTOM_FIELDS_EXTENSION } from "./constants";
|
|
7
9
|
import {
|
|
8
10
|
clearCustomFieldKey,
|
|
9
|
-
|
|
11
|
+
removeCustomFieldKeyForTenant,
|
|
12
|
+
removeCustomFieldKeyFromAllTenants,
|
|
10
13
|
setCustomFieldValue,
|
|
11
14
|
} from "./db/queries/projection";
|
|
12
15
|
|
|
@@ -109,7 +112,7 @@ export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
|
|
|
109
112
|
tx,
|
|
110
113
|
tableName,
|
|
111
114
|
payload.fieldKey,
|
|
112
|
-
|
|
115
|
+
payload.value,
|
|
113
116
|
event.aggregateId,
|
|
114
117
|
);
|
|
115
118
|
},
|
|
@@ -127,14 +130,29 @@ export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
|
|
|
127
130
|
// fieldDefinition.deleted fires nur einmal pro fieldDef-delete
|
|
128
131
|
// (NICHT per-entity). Wir entfernen den key aus ALLEN rows der host-
|
|
129
132
|
// entity falls die deleted-fieldDef für diese entity galt.
|
|
130
|
-
const payload = event.payload as {
|
|
133
|
+
const payload = event.payload as {
|
|
134
|
+
entityName: string;
|
|
135
|
+
fieldKey: string;
|
|
136
|
+
tenantId?: TenantId;
|
|
137
|
+
}; // @cast-boundary engine-payload
|
|
131
138
|
// skip: fieldDefinition.deleted feuert für ALLE fieldDefs cross-entity;
|
|
132
139
|
// nur wenn die deleted-fieldDef diese host-entity betraf, cleanen wir
|
|
133
140
|
// ihre Rows.
|
|
134
141
|
if (payload.entityName !== entityName) return;
|
|
135
142
|
|
|
136
143
|
const tableName = getTableName(entityTable);
|
|
137
|
-
|
|
144
|
+
// Scope cleanup to the deleted definition's owning tenant. System-scope
|
|
145
|
+
// definitions apply to every tenant → cascade across all rows; tenant-
|
|
146
|
+
// scope deletions must only touch that tenant's rows, else deleting one
|
|
147
|
+
// tenant's field strips the same kebab key from every tenant (data loss).
|
|
148
|
+
// Fallback to the event's stream tenantId for events appended before the
|
|
149
|
+
// payload carried tenantId.
|
|
150
|
+
const defTenantId = payload.tenantId ?? event.tenantId;
|
|
151
|
+
if (isSystemTenant(defTenantId)) {
|
|
152
|
+
await removeCustomFieldKeyFromAllTenants(tx, tableName, payload.fieldKey);
|
|
153
|
+
} else {
|
|
154
|
+
await removeCustomFieldKeyForTenant(tx, tableName, payload.fieldKey, defTenantId);
|
|
155
|
+
}
|
|
138
156
|
},
|
|
139
157
|
},
|
|
140
158
|
});
|
|
@@ -32,8 +32,9 @@ interface CustomFieldsHostRow {
|
|
|
32
32
|
function asCustomFieldsHostRow(value: unknown): CustomFieldsHostRow | null {
|
|
33
33
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
34
34
|
if (!("id" in value) || typeof value.id !== "string") return null;
|
|
35
|
-
|
|
36
|
-
const cf =
|
|
35
|
+
const record = value as Record<string, unknown>;
|
|
36
|
+
const cf = record["custom_fields"] ?? record["customFields"];
|
|
37
|
+
if (cf === undefined) return null;
|
|
37
38
|
if (cf === null) return { id: value.id, customFields: null };
|
|
38
39
|
if (!cf || typeof cf !== "object" || Array.isArray(cf)) return null;
|
|
39
40
|
return { id: value.id, customFields: Object.fromEntries(Object.entries(cf)) };
|
|
@@ -153,7 +153,7 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
function buildChannelContext(tenantId: TenantId): ChannelContext {
|
|
156
|
-
return { db, registry, sseBroker, tenantId };
|
|
156
|
+
return { db: createTenantDb(db, tenantId), registry, sseBroker, tenantId };
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
async function logDelivery(entry: DeliveryLogEntry): Promise<void> {
|
package/src/delivery/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SseBroker } from "@cosmicdrift/kumiko-framework/api";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import type {
|
|
4
4
|
NotifyOptions,
|
|
5
5
|
Registry,
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
// --- Channel Interface ---
|
|
11
11
|
|
|
12
12
|
export type ChannelContext = {
|
|
13
|
-
readonly db:
|
|
13
|
+
readonly db: TenantDb;
|
|
14
14
|
readonly registry: Registry;
|
|
15
15
|
readonly sseBroker: SseBroker | undefined;
|
|
16
16
|
readonly tenantId: TenantId;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type FeatureDefinition,
|
|
17
17
|
SYSTEM_TENANT_ID,
|
|
18
18
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
19
|
+
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
19
20
|
import { createEventDispatcher, type EventConsumer } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
20
21
|
import {
|
|
21
22
|
createTestUser,
|
|
@@ -517,12 +518,11 @@ describe("feature-toggles queries + audit automation", () => {
|
|
|
517
518
|
admin,
|
|
518
519
|
);
|
|
519
520
|
|
|
520
|
-
const events =
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
type:
|
|
524
|
-
|
|
525
|
-
}[];
|
|
521
|
+
const events = await selectMany<{ type: string; payload: Record<string, unknown> }>(
|
|
522
|
+
stack.db,
|
|
523
|
+
eventsTable,
|
|
524
|
+
{ type: "feature-toggles:event:toggle-set" },
|
|
525
|
+
);
|
|
526
526
|
|
|
527
527
|
expect(events).toHaveLength(1);
|
|
528
528
|
expect(events[0]?.payload).toMatchObject({
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
FEATURE_TOGGLE_SET_EVENT_NAME,
|
|
13
13
|
FeatureToggleErrors,
|
|
14
14
|
} from "../constants";
|
|
15
|
-
import { updateFeatureToggleOptimistic } from "../db/queries/toggle-state";
|
|
16
15
|
import { globalFeatureStateTable } from "../global-feature-state-table";
|
|
17
16
|
import type { GlobalFeatureToggleRuntime } from "../toggle-runtime";
|
|
18
17
|
|
|
@@ -101,13 +100,16 @@ export function createSetWriteHandler(getRuntime: (() => GlobalFeatureToggleRunt
|
|
|
101
100
|
// Upsert with optimistic lock. Two operators flipping the same
|
|
102
101
|
// toggle simultaneously is rare but possible — the version-WHERE
|
|
103
102
|
// ensures only one wins; the loser sees VersionConflictError.
|
|
104
|
-
const updated = await
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
const updated = await ctx.db.updateMany(
|
|
104
|
+
globalFeatureStateTable,
|
|
105
|
+
{
|
|
106
|
+
enabled,
|
|
107
|
+
updatedBy: event.user.id,
|
|
108
|
+
updatedAt: Temporal.Now.instant(),
|
|
109
|
+
version: existing.version + 1,
|
|
110
|
+
},
|
|
111
|
+
{ featureName, version: existing.version },
|
|
112
|
+
);
|
|
111
113
|
|
|
112
114
|
if (updated.length === 0) {
|
|
113
115
|
return writeFailure(
|
|
@@ -142,10 +142,10 @@ function buildStripeSubscriptionEvent(overrides: {
|
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function signEvent(payload: string): string {
|
|
146
|
-
return stripeForFixtures.webhooks.
|
|
145
|
+
async function signEvent(payload: string, secret = TEST_SECRET): Promise<string> {
|
|
146
|
+
return stripeForFixtures.webhooks.generateTestHeaderStringAsync({
|
|
147
147
|
payload,
|
|
148
|
-
secret
|
|
148
|
+
secret,
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -172,7 +172,7 @@ describe("scenario 1: Stripe-event → DB happy path", () => {
|
|
|
172
172
|
priceId: "price_business_yearly",
|
|
173
173
|
});
|
|
174
174
|
const payload = JSON.stringify(stripeEvent);
|
|
175
|
-
const sig = signEvent(payload);
|
|
175
|
+
const sig = await signEvent(payload);
|
|
176
176
|
|
|
177
177
|
const res = await postStripeWebhook(payload, sig);
|
|
178
178
|
expect(res.status).toBe(200);
|
|
@@ -225,10 +225,7 @@ describe("scenario 2: invalid sig → 401, kein DB-write", () => {
|
|
|
225
225
|
});
|
|
226
226
|
const payload = JSON.stringify(stripeEvent);
|
|
227
227
|
// Wrong secret = invalid sig.
|
|
228
|
-
const wrongSig =
|
|
229
|
-
payload,
|
|
230
|
-
secret: "whsec_wrong_secret",
|
|
231
|
-
});
|
|
228
|
+
const wrongSig = await signEvent(payload, "whsec_wrong_secret");
|
|
232
229
|
|
|
233
230
|
const res = await postStripeWebhook(payload, wrongSig);
|
|
234
231
|
expect(res.status).toBe(401);
|
|
@@ -260,7 +257,7 @@ describe("scenario 3: idempotency via Stripe-retry", () => {
|
|
|
260
257
|
subscriptionId: "sub_4003",
|
|
261
258
|
});
|
|
262
259
|
const payload = JSON.stringify(stripeEvent);
|
|
263
|
-
const sig = signEvent(payload);
|
|
260
|
+
const sig = await signEvent(payload);
|
|
264
261
|
|
|
265
262
|
const res1 = await postStripeWebhook(payload, sig);
|
|
266
263
|
expect(res1.status).toBe(200);
|
|
@@ -292,7 +289,7 @@ describe("scenario 4: ignored event-types pass through", () => {
|
|
|
292
289
|
tenantId: tenantStringId,
|
|
293
290
|
});
|
|
294
291
|
const payload = JSON.stringify(stripeEvent);
|
|
295
|
-
const sig = signEvent(payload);
|
|
292
|
+
const sig = await signEvent(payload);
|
|
296
293
|
|
|
297
294
|
const res = await postStripeWebhook(payload, sig);
|
|
298
295
|
expect(res.status).toBe(200);
|
|
@@ -52,6 +52,7 @@ const TEST_TIER_MAP: TierMap<TestCaps> = {
|
|
|
52
52
|
// Toy-feature mit einem tenantadmin-only-handler. Wenn das feature im
|
|
53
53
|
// effective-Set ist, dispatcher passt durch — wenn nicht, 403 feature_disabled.
|
|
54
54
|
const featProFeature = defineFeature("feat-pro", (r) => {
|
|
55
|
+
r.toggleable({ default: false });
|
|
55
56
|
r.queryHandler(
|
|
56
57
|
"ping",
|
|
57
58
|
{
|
|
@@ -174,10 +175,10 @@ describe("createTierEngineFeature — per-tenant resolver", () => {
|
|
|
174
175
|
const resolver = await plugin.build({ db: stack.db, registry: stack.registry });
|
|
175
176
|
const systemSet = resolver(SYSTEM_TENANT_ID);
|
|
176
177
|
|
|
177
|
-
// Union
|
|
178
|
+
// Union of tier-map features (feat-pro in pro+business, feat-business in business).
|
|
178
179
|
expect(systemSet.has("feat-pro")).toBe(true);
|
|
179
180
|
expect(systemSet.has("feat-business")).toBe(true);
|
|
180
|
-
//
|
|
181
|
-
expect(systemSet.size).
|
|
181
|
+
// SYSTEM tenant also receives always-on non-toggleable features from includeBundled.
|
|
182
|
+
expect(systemSet.size).toBeGreaterThanOrEqual(2);
|
|
182
183
|
});
|
|
183
184
|
});
|
package/src/user-data-rights/__tests__/{audit-log.integration.ts → audit-log.integration.test.ts}
RENAMED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
4
4
|
import { asRawClient, insertMany, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
5
|
-
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
5
|
+
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
6
6
|
import {
|
|
7
7
|
createTestUser,
|
|
8
8
|
setupTestStack,
|
|
@@ -10,11 +10,14 @@ import {
|
|
|
10
10
|
testTenantId,
|
|
11
11
|
unsafeCreateEntityTable,
|
|
12
12
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
13
|
+
import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
13
14
|
import {
|
|
14
15
|
createComplianceProfilesFeature,
|
|
15
16
|
tenantComplianceProfileEntity,
|
|
17
|
+
tenantComplianceProfileTable,
|
|
16
18
|
} from "../../compliance-profiles";
|
|
17
19
|
import { createDataRetentionFeature } from "../../data-retention";
|
|
20
|
+
import { createSessionsFeature } from "../../sessions";
|
|
18
21
|
import { USER_STATUS, userEntity, userTable } from "../../user";
|
|
19
22
|
import { createUserFeature } from "../../user/feature";
|
|
20
23
|
import { createUserDataRightsFeature } from "../feature";
|
|
@@ -36,6 +39,8 @@ beforeAll(async () => {
|
|
|
36
39
|
createUserFeature(),
|
|
37
40
|
createDataRetentionFeature(),
|
|
38
41
|
createComplianceProfilesFeature(),
|
|
42
|
+
createSessionsFeature(),
|
|
43
|
+
|
|
39
44
|
createUserDataRightsFeature(),
|
|
40
45
|
],
|
|
41
46
|
});
|
|
@@ -50,10 +55,12 @@ afterAll(async () => {
|
|
|
50
55
|
});
|
|
51
56
|
|
|
52
57
|
beforeEach(async () => {
|
|
53
|
-
await
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
await resetTestTables(stack.db, [
|
|
59
|
+
userTable,
|
|
60
|
+
tenantComplianceProfileTable,
|
|
61
|
+
downloadAttemptsTable,
|
|
62
|
+
eventsTable,
|
|
63
|
+
]);
|
|
57
64
|
});
|
|
58
65
|
|
|
59
66
|
async function seedUser(u: typeof alice, email: string): Promise<void> {
|
|
@@ -13,18 +13,20 @@
|
|
|
13
13
|
// Alices Forget; Bobs Daten landen NICHT in Alices Export-Bundle.
|
|
14
14
|
|
|
15
15
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
16
|
-
import { asRawClient, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
16
|
+
import { asRawClient, deleteMany, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
17
17
|
import {
|
|
18
18
|
defineFeature,
|
|
19
19
|
EXT_USER_DATA,
|
|
20
20
|
type UserDataDeleteHook,
|
|
21
21
|
type UserDataExportHook,
|
|
22
22
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
23
|
+
import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
|
|
23
24
|
import {
|
|
24
25
|
setupTestStack,
|
|
25
26
|
type TestStack,
|
|
26
27
|
unsafeCreateEntityTable,
|
|
27
28
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
29
|
+
import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
28
30
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
29
31
|
import {
|
|
30
32
|
createComplianceProfilesFeature,
|
|
@@ -32,6 +34,8 @@ import {
|
|
|
32
34
|
} from "../../compliance-profiles";
|
|
33
35
|
import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../../data-retention";
|
|
34
36
|
import { createFilesFeature } from "../../files";
|
|
37
|
+
import { createSessionsFeature } from "../../sessions";
|
|
38
|
+
import { tenantMembershipsTable } from "../../tenant";
|
|
35
39
|
import { createUserFeature, USER_STATUS, userEntity, userTable } from "../../user";
|
|
36
40
|
import { createUserDataRightsDefaultsFeature } from "../../user-data-rights-defaults";
|
|
37
41
|
import { createUserDataRightsFeature } from "../feature";
|
|
@@ -55,6 +59,18 @@ type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
|
|
|
55
59
|
const NOW = (): Instant => getTemporal().Now.instant();
|
|
56
60
|
const PAST = (): Instant => getTemporal().Instant.fromEpochMilliseconds(Date.now() - 60_000);
|
|
57
61
|
|
|
62
|
+
const KUMIKO_NAME = Symbol.for("kumiko:schema:Name");
|
|
63
|
+
const KUMIKO_COLUMNS = Symbol.for("kumiko:schema:Columns");
|
|
64
|
+
|
|
65
|
+
/** Minimal bun-db table descriptor for the synthetic test_notes table. */
|
|
66
|
+
const testNotesTable = {
|
|
67
|
+
[KUMIKO_NAME]: "test_notes",
|
|
68
|
+
[KUMIKO_COLUMNS]: {
|
|
69
|
+
tenantId: { name: "tenant_id", getSQLType: () => "uuid" },
|
|
70
|
+
authorId: { name: "author_id", getSQLType: () => "text" },
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
58
74
|
// Synthetic third-party Domain-Feature: "note" mit export- + delete-Hook.
|
|
59
75
|
// Stellvertretend fuer App-spezifische Entities (Chat-Message, Blog-Post
|
|
60
76
|
// etc.), die ueber EXT_USER_DATA sauber in die Pipeline integrieren.
|
|
@@ -81,13 +97,10 @@ const exportNotes: UserDataExportHook = async (ctx) => {
|
|
|
81
97
|
};
|
|
82
98
|
|
|
83
99
|
const deleteNotes: UserDataDeleteHook = async (ctx, _strategy) => {
|
|
84
|
-
await
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
`,
|
|
89
|
-
[ctx.tenantId, ctx.userId],
|
|
90
|
-
);
|
|
100
|
+
await deleteMany(ctx.db, testNotesTable, {
|
|
101
|
+
tenantId: ctx.tenantId,
|
|
102
|
+
authorId: ctx.userId,
|
|
103
|
+
});
|
|
91
104
|
};
|
|
92
105
|
|
|
93
106
|
const testNotesFeature = defineFeature("test-notes", (r) => {
|
|
@@ -104,6 +117,8 @@ beforeAll(async () => {
|
|
|
104
117
|
createFilesFeature(),
|
|
105
118
|
createDataRetentionFeature(),
|
|
106
119
|
createComplianceProfilesFeature(),
|
|
120
|
+
createSessionsFeature(),
|
|
121
|
+
|
|
107
122
|
createUserDataRightsFeature(),
|
|
108
123
|
createUserDataRightsDefaultsFeature(),
|
|
109
124
|
testNotesFeature,
|
|
@@ -161,10 +176,12 @@ afterAll(async () => {
|
|
|
161
176
|
});
|
|
162
177
|
|
|
163
178
|
beforeEach(async () => {
|
|
164
|
-
await
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
179
|
+
await resetTestTables(stack.db, [
|
|
180
|
+
userTable,
|
|
181
|
+
tenantMembershipsTable,
|
|
182
|
+
fileRefsTable,
|
|
183
|
+
testNotesTable,
|
|
184
|
+
]);
|
|
168
185
|
});
|
|
169
186
|
|
|
170
187
|
async function seedUser(
|
package/src/user-data-rights/__tests__/{download.integration.ts → download.integration.test.ts}
RENAMED
|
@@ -12,7 +12,7 @@ import { randomBytes } from "node:crypto";
|
|
|
12
12
|
import { asRawClient, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
13
13
|
import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
|
|
14
14
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
15
|
-
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
15
|
+
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
16
16
|
import {
|
|
17
17
|
createInMemoryFileProvider,
|
|
18
18
|
type FileStorageProvider,
|
|
@@ -25,10 +25,12 @@ import {
|
|
|
25
25
|
unsafeCreateEntityTable,
|
|
26
26
|
unsafePushTables,
|
|
27
27
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
28
|
+
import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
28
29
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
29
30
|
import {
|
|
30
31
|
createComplianceProfilesFeature,
|
|
31
32
|
tenantComplianceProfileEntity,
|
|
33
|
+
tenantComplianceProfileTable,
|
|
32
34
|
} from "../../compliance-profiles";
|
|
33
35
|
import { createConfigFeature } from "../../config";
|
|
34
36
|
import { ConfigHandlers } from "../../config/constants";
|
|
@@ -38,6 +40,8 @@ import { configValuesTable } from "../../config/table";
|
|
|
38
40
|
import { createDataRetentionFeature } from "../../data-retention";
|
|
39
41
|
import { fileFoundationFeature } from "../../file-foundation";
|
|
40
42
|
import { fileProviderInMemoryFeature } from "../../file-provider-inmemory";
|
|
43
|
+
import { createSessionsFeature } from "../../sessions";
|
|
44
|
+
import { tenantMembershipsTable } from "../../tenant";
|
|
41
45
|
import { createUserFeature } from "../../user";
|
|
42
46
|
import { createUserDataRightsFeature } from "../feature";
|
|
43
47
|
import { runExportJobs } from "../run-export-jobs";
|
|
@@ -108,6 +112,8 @@ beforeAll(async () => {
|
|
|
108
112
|
fileFoundationFeature,
|
|
109
113
|
fileProviderInMemoryFeature,
|
|
110
114
|
noSignedUrlProviderFeature,
|
|
115
|
+
createSessionsFeature(),
|
|
116
|
+
|
|
111
117
|
createUserDataRightsFeature(),
|
|
112
118
|
],
|
|
113
119
|
extraContext: ({ registry }) => ({
|
|
@@ -151,12 +157,14 @@ afterAll(async () => {
|
|
|
151
157
|
});
|
|
152
158
|
|
|
153
159
|
beforeEach(async () => {
|
|
154
|
-
await
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
await resetTestTables(stack.db, [
|
|
161
|
+
exportDownloadTokensTable,
|
|
162
|
+
exportJobsTable,
|
|
163
|
+
eventsTable,
|
|
164
|
+
tenantComplianceProfileTable,
|
|
165
|
+
tenantMembershipsTable,
|
|
166
|
+
configValuesTable,
|
|
167
|
+
]);
|
|
160
168
|
providerPerTenant = new Map();
|
|
161
169
|
|
|
162
170
|
// Setup file-foundation provider="inmemory" pro Tenant.
|