@cosmicdrift/kumiko-bundled-features 0.352.0 → 0.354.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/dist/agent-tools/agent-doc-lint.d.ts +2 -0
- package/dist/agent-tools/agent-doc-lint.js +61 -1
- package/dist/agent-tools/agent-manifest.d.ts +1 -0
- package/dist/agent-tools/agent-manifest.js +17 -5
- package/dist/agent-tools/feature.js +7 -2
- package/dist/agent-tools/index.d.ts +1 -1
- package/dist/agent-tools/index.js +1 -1
- package/dist/agent-tools/manifest-deny-qns.d.ts +3 -0
- package/dist/agent-tools/manifest-deny-qns.js +17 -0
- package/dist/agent-tools/tool-catalog.d.ts +2 -1
- package/dist/agent-tools/tool-catalog.js +4 -1
- package/dist/agent-tools/tool-dispatch.js +1 -1
- package/dist/audit/handlers/list.query.js +13 -8
- package/dist/auth-email-password/handlers/change-password.write.js +1 -0
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +2 -2
- package/dist/auth-email-password/handlers/invite-accept.write.js +2 -2
- package/dist/auth-email-password/handlers/invite-create.write.js +17 -0
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +4 -1
- package/dist/auth-email-password/handlers/login.write.js +1 -0
- package/dist/auth-email-password/handlers/reset-password.write.js +1 -0
- package/dist/auth-email-password/handlers/signup-confirm.write.js +6 -4
- package/dist/auth-email-password/handlers/signup-request.write.js +51 -77
- package/dist/auth-email-password/handlers/token-request-handler.js +1 -0
- package/dist/auth-email-password/handlers/verify-email.write.js +1 -0
- package/dist/auth-email-password/index.d.ts +2 -0
- package/dist/auth-email-password/index.js +2 -0
- package/dist/auth-email-password/seeding.d.ts +2 -0
- package/dist/auth-email-password/seeding.js +30 -8
- package/dist/auth-email-password/signup-activation.d.ts +24 -0
- package/dist/auth-email-password/signup-activation.js +41 -0
- package/dist/auth-email-password/signup-token-store.d.ts +5 -0
- package/dist/auth-email-password/signup-token-store.js +8 -3
- package/dist/auth-email-password/web/auth-gate.d.ts +2 -2
- package/dist/auth-email-password/web/auth-gate.js +5 -5
- package/dist/auth-email-password/web/auth-redirect.d.ts +1 -0
- package/dist/auth-email-password/web/auth-redirect.js +14 -0
- package/dist/auth-email-password/web/login-screen.js +2 -2
- package/dist/auth-email-password/web/user-menu.js +1 -1
- package/dist/auth-mfa/feature.js +5 -0
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -0
- package/dist/auth-mfa/handlers/verify.write.js +1 -0
- package/dist/auth-mfa/schema/user-mfa.d.ts +6 -0
- package/dist/auth-mfa/screens.js +2 -2
- package/dist/auth-mfa/web/mfa-recovery-codes-reveal.js +1 -1
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +3 -3
- package/dist/auth-mfa/web/mfa-verify-screen.d.ts +1 -1
- package/dist/auth-mfa/web/mfa-verify-screen.js +1 -1
- package/dist/billing-foundation/billing-info-query.d.ts +5 -2
- package/dist/billing-foundation/checkout-core.d.ts +7 -0
- package/dist/billing-foundation/checkout-core.js +7 -2
- package/dist/billing-foundation/entities.d.ts +8 -0
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +7 -1
- package/dist/billing-foundation/projection.d.ts +8 -0
- package/dist/cap-counter/entity.d.ts +1 -0
- package/dist/cap-overview/access.js +2 -9
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +2 -2
- package/dist/cap-overview/handlers/tenant-options.query.js +4 -5
- package/dist/channel-in-app/in-app-channel.js +1 -1
- package/dist/channel-in-app/tables.js +4 -2
- package/dist/compliance-profiles/schema/profile-selection.d.ts +10 -8
- package/dist/compliance-profiles-ops/handlers/tenants-missing-profile.query.js +2 -4
- package/dist/config/handlers/values.query.d.ts +3 -1
- package/dist/config/handlers/values.query.js +7 -2
- package/dist/config/table.d.ts +6 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.js +110 -61
- package/dist/custom-fields/entity.d.ts +4 -0
- package/dist/data-retention/schema/tenant-retention-override.d.ts +4 -0
- package/dist/delivery/db/queries/address-opt-outs.d.ts +3 -1
- package/dist/delivery/db/queries/address-opt-outs.js +12 -7
- package/dist/delivery/delivery-service.js +60 -12
- package/dist/delivery/public-names.js +1 -1
- package/dist/delivery/tables.d.ts +6 -0
- package/dist/delivery/unsubscribe.js +66 -14
- package/dist/document-ingest-foundation/entity.d.ts +4 -0
- package/dist/feature-toggles/feature.js +2 -3
- package/dist/feature-toggles/toggle-runtime.d.ts +5 -1
- package/dist/feature-toggles/toggle-runtime.js +30 -6
- package/dist/feature-toggles/toggle-sync-signal.d.ts +3 -2
- package/dist/feature-toggles/toggle-sync-signal.js +29 -10
- package/dist/file-derivatives/handlers/public-variant.query.d.ts +1 -0
- package/dist/file-derivatives/handlers/public-variant.query.js +7 -1
- package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +5 -0
- package/dist/files-tenant-data/hooks.js +61 -24
- package/dist/folders/entity.d.ts +8 -0
- package/dist/folders/feature.js +2 -10
- package/dist/folders/handlers/delete-folder.write.js +31 -4
- package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
- package/dist/form-draft/db/queries/cleanup.js +4 -3
- package/dist/form-draft/entity.d.ts +2 -0
- package/dist/form-draft/handlers/cleanup.job.d.ts +5 -1
- package/dist/form-draft/handlers/cleanup.job.js +11 -5
- package/dist/foundation-shared/index.d.ts +1 -1
- package/dist/foundation-shared/index.js +1 -1
- package/dist/foundation-shared/mail-host-policy.d.ts +4 -0
- package/dist/foundation-shared/mail-host-policy.js +13 -4
- package/dist/inbound-mail-foundation/entities.d.ts +25 -0
- package/dist/inbound-mail-foundation/projection.d.ts +20 -0
- package/dist/inbound-provider-imap/feature.js +2 -0
- package/dist/inbound-provider-imap/imap-client.js +3 -4
- package/dist/jobs/constants.d.ts +1 -0
- package/dist/jobs/constants.js +1 -0
- package/dist/jobs/db/queries/retention.d.ts +1 -0
- package/dist/jobs/db/queries/retention.js +5 -0
- package/dist/jobs/db/queries/stale-run-sweep.d.ts +2 -0
- package/dist/jobs/db/queries/stale-run-sweep.js +13 -2
- package/dist/jobs/feature.js +7 -1
- package/dist/jobs/handlers/list.query.d.ts +2 -1
- package/dist/jobs/handlers/list.query.js +40 -15
- package/dist/jobs/handlers/tenant-failures.query.js +1 -1
- package/dist/jobs/handlers/tenant-runs.query.d.ts +17 -0
- package/dist/jobs/handlers/tenant-runs.query.js +72 -0
- package/dist/jobs/i18n.js +1 -0
- package/dist/jobs/index.d.ts +2 -0
- package/dist/jobs/index.js +1 -0
- package/dist/jobs/job-run-logger.d.ts +2 -2
- package/dist/jobs/job-run-logger.js +98 -9
- package/dist/jobs/job-run-table.d.ts +6 -0
- package/dist/jobs/tenant-job-failure-table.js +15 -2
- package/dist/jobs/tenant-job-run-table.d.ts +4 -0
- package/dist/jobs/tenant-job-run-table.js +34 -0
- package/dist/ledger/entity.d.ts +24 -8
- package/dist/ledger/entity.js +17 -5
- package/dist/ledger/feature.js +3 -0
- package/dist/ledger/handlers/confirm-schedule-period.write.js +9 -3
- package/dist/ledger/handlers/create-transaction.write.js +1 -0
- package/dist/ledger/handlers/reverse-transaction.write.js +1 -0
- package/dist/ledger/schedule-subject-pair.d.ts +2 -0
- package/dist/ledger/schedule-subject-pair.js +24 -0
- package/dist/ledger/schemas.js +8 -2
- package/dist/mail-transport-smtp/feature.d.ts +3 -4
- package/dist/mail-transport-smtp/feature.js +4 -13
- package/dist/managed-pages/feature.js +28 -10
- package/dist/managed-pages/table.d.ts +12 -0
- package/dist/notes-history/entity.d.ts +12 -0
- package/dist/notes-history/entity.js +4 -0
- package/dist/notes-history/feature.js +2 -10
- package/dist/personal-access-tokens/handlers/create.write.js +1 -0
- package/dist/personal-access-tokens/schema/api-token.d.ts +6 -0
- package/dist/personal-access-tokens/screens.js +14 -3
- package/dist/secrets/table.d.ts +1 -0
- package/dist/seo/feature.js +28 -6
- package/dist/sessions/feature.d.ts +1 -0
- package/dist/sessions/feature.js +9 -8
- package/dist/sessions/handlers/detail.query.d.ts +1 -1
- package/dist/sessions/handlers/detail.query.js +2 -2
- package/dist/sessions/handlers/list.query.d.ts +1 -1
- package/dist/sessions/handlers/list.query.js +5 -3
- package/dist/sessions/schema/user-session.d.ts +4 -0
- package/dist/sessions/screens.d.ts +2 -2
- package/dist/sessions/screens.js +6 -7
- package/dist/shared/assert-no-where-rule-in-ownership-write.d.ts +2 -0
- package/dist/shared/assert-no-where-rule-in-ownership-write.js +18 -0
- package/dist/shared/encrypt-for-direct-write.js +10 -2
- package/dist/shared/index.d.ts +4 -3
- package/dist/shared/index.js +4 -3
- package/dist/shared/parent-visibility.d.ts +2 -1
- package/dist/shared/parent-visibility.js +21 -0
- package/dist/shared/rate-limited-text-response.d.ts +2 -0
- package/dist/shared/rate-limited-text-response.js +9 -0
- package/dist/shared/row-bound-grant.js +16 -2
- package/dist/shared/session-locale-field.d.ts +1 -0
- package/dist/shared/session-locale-field.js +6 -0
- package/dist/tags/entity.d.ts +9 -0
- package/dist/tags/feature.js +2 -10
- package/dist/template-resolver/api.d.ts +2 -2
- package/dist/template-resolver/handlers/list.query.d.ts +2 -0
- package/dist/template-resolver/handlers/list.query.js +2 -2
- package/dist/template-resolver/table.d.ts +48 -30
- package/dist/template-resolver/user-content-table.d.ts +34 -20
- package/dist/tenant/decrypt-tenant-names.d.ts +3 -0
- package/dist/tenant/decrypt-tenant-names.js +10 -0
- package/dist/tenant/feature.js +7 -1
- package/dist/tenant/handlers/me.query.js +5 -1
- package/dist/tenant/handlers/memberships.query.js +3 -2
- package/dist/tenant/handlers/tenant-directory.query.js +9 -3
- package/dist/tenant/handlers/toggle-enabled.write.js +2 -0
- package/dist/tenant/handlers/update-member-roles.write.js +1 -0
- package/dist/tenant/index.d.ts +1 -0
- package/dist/tenant/index.js +1 -0
- package/dist/tenant/invitation-table.d.ts +54 -8
- package/dist/tenant/invitation-table.js +6 -1
- package/dist/tenant/invited-membership.d.ts +8 -6
- package/dist/tenant/invited-membership.js +28 -7
- package/dist/tenant/membership-table.d.ts +4 -0
- package/dist/tenant/schema/tenant.d.ts +18 -10
- package/dist/tenant/schema/tenant.js +4 -6
- package/dist/tenant/screens.js +1 -1
- package/dist/tenant-handover/move-entity-graph.js +11 -18
- package/dist/tenant-handover/transfer-graph.d.ts +2 -21
- package/dist/tenant-handover/transfer-graph.js +8 -75
- package/dist/tenant-lifecycle/boot-checks.d.ts +1 -0
- package/dist/tenant-lifecycle/boot-checks.js +13 -0
- package/dist/tenant-lifecycle/constants.d.ts +3 -0
- package/dist/tenant-lifecycle/constants.js +5 -0
- package/dist/tenant-lifecycle/events.d.ts +5 -0
- package/dist/tenant-lifecycle/events.js +5 -0
- package/dist/tenant-lifecycle/feature.js +6 -3
- package/dist/tenant-lifecycle/run-tenant-destroy.d.ts +10 -5
- package/dist/tenant-lifecycle/run-tenant-destroy.js +59 -17
- package/dist/tenant-lifecycle/stages.d.ts +8 -1
- package/dist/tenant-lifecycle/stages.js +33 -4
- package/dist/tenant-lifecycle/testing.d.ts +8 -0
- package/dist/tenant-lifecycle/testing.js +42 -0
- package/dist/tier-engine/entity.d.ts +2 -0
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -0
- package/dist/tier-engine/tier-resolver.d.ts +2 -0
- package/dist/user/handlers/detail.query.js +2 -0
- package/dist/user/handlers/list.query.js +6 -2
- package/dist/user/handlers/update.write.js +1 -0
- package/dist/user/schema/user.d.ts +22 -8
- package/dist/user/seeding.d.ts +1 -0
- package/dist/user/seeding.js +20 -0
- package/dist/user-data-rights/boot-checks.js +5 -0
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.js +2 -2
- package/dist/user-data-rights/handlers/request-deletion.write.js +1 -0
- package/dist/user-data-rights/index.d.ts +1 -0
- package/dist/user-data-rights/index.js +1 -0
- package/dist/user-data-rights/schema/download-attempt.d.ts +14 -0
- package/dist/user-data-rights/schema/download-attempt.js +17 -9
- package/dist/user-data-rights/schema/download-token.d.ts +8 -0
- package/dist/user-data-rights/schema/export-job.d.ts +16 -8
- package/dist/user-profile/handlers/change-email.write.js +8 -2
- package/dist/waitlist/entity.d.ts +22 -6
- package/dist/workflow-runner/event-trigger.d.ts +1 -0
- package/dist/workflow-runner/event-trigger.js +63 -15
- package/dist/workflow-runner/handlers/resume-run.write.js +8 -2
- package/dist/workflow-runner/runner.d.ts +2 -1
- package/dist/workflow-runner/runner.js +5 -1
- package/package.json +35 -10
- package/src/agent-tools/changes.json +12 -0
- package/src/audit/changes.json +6 -0
- package/src/auth-email-password/changes.json +24 -0
- package/src/auth-mfa/changes.json +7 -0
- package/src/billing-foundation/changes.json +6 -0
- package/src/channel-in-app/changes.json +8 -1
- package/src/crypto-shredding/changes.json +12 -0
- package/src/delivery/changes.json +6 -0
- package/src/files/changes.json +24 -0
- package/src/files-tenant-data/changes.json +5 -0
- package/src/folders/changes.json +6 -0
- package/src/form-draft/changes.json +7 -1
- package/src/inbound-provider-imap/changes.json +6 -0
- package/src/jobs/changes.json +30 -0
- package/src/ledger/changes.json +23 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/notes-history/changes.json +13 -0
- package/src/seo/changes.json +5 -0
- package/src/template-resolver/changes.json +5 -0
- package/src/tenant/changes.json +19 -0
- package/src/tenant-handover/changes.json +12 -0
- package/src/tenant-lifecycle/changes.json +22 -0
- package/src/tier-engine/changes.json +5 -0
- package/src/user/changes.json +15 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/workflow-runner/changes.json +5 -0
- package/dist/shared/has-where-rule.d.ts +0 -2
- package/dist/shared/has-where-rule.js +0 -10
|
@@ -2,6 +2,7 @@ import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
2
2
|
import { access, definePagedQueryHandler, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import * as z from "zod";
|
|
5
|
+
import { decryptTenantNames } from "../decrypt-tenant-names.js";
|
|
5
6
|
import { tenantTable } from "../schema/tenant.js";
|
|
6
7
|
import { isSystemAdmin } from "./is-system-admin.js";
|
|
7
8
|
// Label source behind every `tenant:tenant` reference column (fw#3142). The
|
|
@@ -38,7 +39,7 @@ export const tenantDirectoryQuery = definePagedQueryHandler({
|
|
|
38
39
|
tenants = await selectMany(db, tenantTable, undefined, {
|
|
39
40
|
limit: scanLimit,
|
|
40
41
|
orderBy: [
|
|
41
|
-
{ col: "
|
|
42
|
+
{ col: "key", direction: "asc" },
|
|
42
43
|
{ col: "id", direction: "asc" },
|
|
43
44
|
],
|
|
44
45
|
});
|
|
@@ -48,10 +49,15 @@ export const tenantDirectoryQuery = definePagedQueryHandler({
|
|
|
48
49
|
const row = await fetchOne(db, tenantTable, { id: query.user.tenantId });
|
|
49
50
|
tenants = row ? [row] : [];
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
+
// name is ciphertext at rest, so the SQL page is keyed by the plaintext
|
|
53
|
+
// `key`; label order and search happen in memory after decrypting.
|
|
54
|
+
const decrypted = await decryptTenantNames(tenants, "tenant:tenant-directory");
|
|
55
|
+
const labeled = decrypted
|
|
56
|
+
.map((tenant) => {
|
|
52
57
|
const id = String(tenant.id);
|
|
53
58
|
return { id, label: typeof tenant.name === "string" ? tenant.name : id };
|
|
54
|
-
})
|
|
59
|
+
})
|
|
60
|
+
.sort((a, b) => a.label.localeCompare(b.label) || a.id.localeCompare(b.id));
|
|
55
61
|
const needle = search?.toLowerCase();
|
|
56
62
|
const rows = needle === undefined
|
|
57
63
|
? labeled
|
|
@@ -12,6 +12,8 @@ function createToggleTenantHandler(enable) {
|
|
|
12
12
|
name: verbName,
|
|
13
13
|
schema: z.object({ id: z.uuid() }),
|
|
14
14
|
access: { roles: ["SystemAdmin"] },
|
|
15
|
+
// Disabling locks every member out; enabling is its undo.
|
|
16
|
+
...(enable ? {} : { agent: { risk: "high" } }),
|
|
15
17
|
description: enable
|
|
16
18
|
? "Re-enables a disabled tenant so its members can sign in and switch to it again; this is the undo for tenant:write:disable."
|
|
17
19
|
: "Marks a tenant disabled so it stops counting as a membership — its members can no longer sign in to it or switch to it — while leaving all of its data in place.",
|
|
@@ -41,6 +41,7 @@ export const updateMemberRolesWrite = defineWriteHandler({
|
|
|
41
41
|
// are session-scoped (event.user.tenantId).
|
|
42
42
|
access: { roles: ["system", ...access.admin] },
|
|
43
43
|
description: "Sets the roles a user holds in a tenant, rejecting reserved role names and any grant that would raise the target above the caller; roles the caller could not grant (platform roles such as DataProtectionOfficer or TenantOwner, undeclared or higher-tier app roles) are kept, only the system user replaces the full list; admins act on their own tenant, SystemAdmins on any.",
|
|
44
|
+
agent: { risk: "high" },
|
|
44
45
|
handler: async (event, ctx) => {
|
|
45
46
|
if (!ctx.systemDb) {
|
|
46
47
|
throw new InternalError({
|
package/dist/tenant/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { cancelPendingInvitation } from "./cancel-pending-invitation.js";
|
|
2
2
|
export { TenantCommandSchemas } from "./command-schemas.js";
|
|
3
3
|
export { OWNER_INVITE_ROLE_OPTIONS, TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries, } from "./constants.js";
|
|
4
|
+
export { decryptTenantNames } from "./decrypt-tenant-names.js";
|
|
4
5
|
export { collectAssignableAppRoles, createTenantFeature } from "./feature.js";
|
|
5
6
|
export type { InvitationStatus } from "./invitation-table.js";
|
|
6
7
|
export { INVITATION_STATUS, INVITATION_STATUSES, tenantInvitationEntity, tenantInvitationsTable, } from "./invitation-table.js";
|
package/dist/tenant/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { cancelPendingInvitation } from "./cancel-pending-invitation.js";
|
|
2
2
|
export { TenantCommandSchemas } from "./command-schemas.js";
|
|
3
3
|
export { OWNER_INVITE_ROLE_OPTIONS, TENANT_FEATURE, TenantErrors, TenantHandlers, TenantQueries, } from "./constants.js";
|
|
4
|
+
export { decryptTenantNames } from "./decrypt-tenant-names.js";
|
|
4
5
|
export { collectAssignableAppRoles, createTenantFeature } from "./feature.js";
|
|
5
6
|
export { INVITATION_STATUS, INVITATION_STATUSES, tenantInvitationEntity, tenantInvitationsTable, } from "./invitation-table.js";
|
|
6
7
|
export { isTenantServingPublicContent } from "./is-tenant-serving-public-content.js";
|
|
@@ -11,6 +11,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
|
|
|
11
11
|
readonly type: "text";
|
|
12
12
|
readonly description?: string;
|
|
13
13
|
readonly maxLength?: number;
|
|
14
|
+
readonly minLength?: number;
|
|
14
15
|
readonly required?: boolean;
|
|
15
16
|
readonly searchable?: boolean;
|
|
16
17
|
readonly sortable?: boolean;
|
|
@@ -32,6 +33,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
|
|
|
32
33
|
readonly type: "text";
|
|
33
34
|
readonly description?: string;
|
|
34
35
|
readonly maxLength?: number;
|
|
36
|
+
readonly minLength?: number;
|
|
35
37
|
readonly required?: boolean;
|
|
36
38
|
readonly searchable?: boolean;
|
|
37
39
|
readonly sortable?: boolean;
|
|
@@ -62,14 +64,15 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
|
|
|
62
64
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
63
65
|
readonly display?: "radio" | "dropdown";
|
|
64
66
|
readonly optionTones?: {
|
|
65
|
-
readonly accepted?:
|
|
66
|
-
readonly cancelled?:
|
|
67
|
-
readonly expired?:
|
|
68
|
-
readonly pending?:
|
|
67
|
+
readonly accepted?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
68
|
+
readonly cancelled?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
69
|
+
readonly expired?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
70
|
+
readonly pending?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
69
71
|
} | undefined;
|
|
70
72
|
readonly optionsQuery?: string;
|
|
71
73
|
readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
|
|
72
74
|
readonly optionsAvailabilityQuery?: string;
|
|
75
|
+
readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"accepted" | "cancelled" | "expired" | "pending">[] | undefined;
|
|
73
76
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
74
77
|
required: true;
|
|
75
78
|
};
|
|
@@ -77,6 +80,7 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
|
|
|
77
80
|
readonly type: "text";
|
|
78
81
|
readonly description?: string;
|
|
79
82
|
readonly maxLength?: number;
|
|
83
|
+
readonly minLength?: number;
|
|
80
84
|
readonly required?: boolean;
|
|
81
85
|
readonly searchable?: boolean;
|
|
82
86
|
readonly sortable?: boolean;
|
|
@@ -94,6 +98,25 @@ export declare const tenantInvitationEntity: import("@cosmicdrift/kumiko-types/f
|
|
|
94
98
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
95
99
|
required: true;
|
|
96
100
|
};
|
|
101
|
+
membershipVersion: {
|
|
102
|
+
readonly type: "number";
|
|
103
|
+
readonly description?: string;
|
|
104
|
+
readonly required?: boolean;
|
|
105
|
+
readonly sortable?: boolean;
|
|
106
|
+
readonly filterable?: boolean;
|
|
107
|
+
readonly sensitive?: boolean;
|
|
108
|
+
readonly default?: number;
|
|
109
|
+
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
110
|
+
readonly min?: number;
|
|
111
|
+
readonly max?: number;
|
|
112
|
+
readonly integer?: boolean;
|
|
113
|
+
readonly unit?: string | {
|
|
114
|
+
readonly field: string;
|
|
115
|
+
};
|
|
116
|
+
readonly grouping?: boolean;
|
|
117
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
118
|
+
required: false;
|
|
119
|
+
};
|
|
97
120
|
expiresAt: {
|
|
98
121
|
readonly type: "timestamp";
|
|
99
122
|
readonly description?: string;
|
|
@@ -115,6 +138,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
|
|
|
115
138
|
readonly type: "text";
|
|
116
139
|
readonly description?: string;
|
|
117
140
|
readonly maxLength?: number;
|
|
141
|
+
readonly minLength?: number;
|
|
118
142
|
readonly required?: boolean;
|
|
119
143
|
readonly searchable?: boolean;
|
|
120
144
|
readonly sortable?: boolean;
|
|
@@ -136,6 +160,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
|
|
|
136
160
|
readonly type: "text";
|
|
137
161
|
readonly description?: string;
|
|
138
162
|
readonly maxLength?: number;
|
|
163
|
+
readonly minLength?: number;
|
|
139
164
|
readonly required?: boolean;
|
|
140
165
|
readonly searchable?: boolean;
|
|
141
166
|
readonly sortable?: boolean;
|
|
@@ -166,14 +191,15 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
|
|
|
166
191
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
167
192
|
readonly display?: "radio" | "dropdown";
|
|
168
193
|
readonly optionTones?: {
|
|
169
|
-
readonly accepted?:
|
|
170
|
-
readonly cancelled?:
|
|
171
|
-
readonly expired?:
|
|
172
|
-
readonly pending?:
|
|
194
|
+
readonly accepted?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
195
|
+
readonly cancelled?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
196
|
+
readonly expired?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
197
|
+
readonly pending?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
173
198
|
} | undefined;
|
|
174
199
|
readonly optionsQuery?: string;
|
|
175
200
|
readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
|
|
176
201
|
readonly optionsAvailabilityQuery?: string;
|
|
202
|
+
readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"accepted" | "cancelled" | "expired" | "pending">[] | undefined;
|
|
177
203
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
178
204
|
required: true;
|
|
179
205
|
};
|
|
@@ -181,6 +207,7 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
|
|
|
181
207
|
readonly type: "text";
|
|
182
208
|
readonly description?: string;
|
|
183
209
|
readonly maxLength?: number;
|
|
210
|
+
readonly minLength?: number;
|
|
184
211
|
readonly required?: boolean;
|
|
185
212
|
readonly searchable?: boolean;
|
|
186
213
|
readonly sortable?: boolean;
|
|
@@ -198,6 +225,25 @@ export declare const tenantInvitationsTable: import("@cosmicdrift/kumiko-framewo
|
|
|
198
225
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
199
226
|
required: true;
|
|
200
227
|
};
|
|
228
|
+
membershipVersion: {
|
|
229
|
+
readonly type: "number";
|
|
230
|
+
readonly description?: string;
|
|
231
|
+
readonly required?: boolean;
|
|
232
|
+
readonly sortable?: boolean;
|
|
233
|
+
readonly filterable?: boolean;
|
|
234
|
+
readonly sensitive?: boolean;
|
|
235
|
+
readonly default?: number;
|
|
236
|
+
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
237
|
+
readonly min?: number;
|
|
238
|
+
readonly max?: number;
|
|
239
|
+
readonly integer?: boolean;
|
|
240
|
+
readonly unit?: string | {
|
|
241
|
+
readonly field: string;
|
|
242
|
+
};
|
|
243
|
+
readonly grouping?: boolean;
|
|
244
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
245
|
+
required: false;
|
|
246
|
+
};
|
|
201
247
|
expiresAt: {
|
|
202
248
|
readonly type: "timestamp";
|
|
203
249
|
readonly description?: string;
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// pending → re-use existing row + refresh Redis-token + send mail
|
|
23
23
|
// (analog zu signup-Resend).
|
|
24
24
|
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
25
|
-
import { createEntity, createMultiSelectField, createSelectField, createTextField, createTimestampField, } from "@cosmicdrift/kumiko-framework/engine";
|
|
25
|
+
import { createEntity, createMultiSelectField, createNumberField, createSelectField, createTextField, createTimestampField, } from "@cosmicdrift/kumiko-framework/engine";
|
|
26
26
|
// kumiko-lint-ignore cross-feature-import global-role options are owned by the user feature
|
|
27
27
|
import { GLOBAL_ROLE_OPTIONS } from "../user/constants.js";
|
|
28
28
|
// Status-const-Object damit Handler-Code keine Magic-Strings nutzt.
|
|
@@ -83,6 +83,11 @@ export const tenantInvitationEntity = createEntity({
|
|
|
83
83
|
personal: { of: "invitedBy" },
|
|
84
84
|
find: "exact",
|
|
85
85
|
}),
|
|
86
|
+
// Version of the invitee's membership row when the invitation was issued
|
|
87
|
+
// (0 = no membership yet). Accept compares it against the current version
|
|
88
|
+
// to detect a membership decision made after the invite. NULL only on rows
|
|
89
|
+
// issued before this field existed.
|
|
90
|
+
membershipVersion: createNumberField({ integer: true, min: 0 }),
|
|
86
91
|
// UI-Anzeige — Wahrheit liegt in Redis-TTL.
|
|
87
92
|
expiresAt: createTimestampField({ required: true }),
|
|
88
93
|
},
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { type DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { type DbRunner, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
4
4
|
export type InvitedMembershipOptions = {
|
|
5
5
|
readonly userId: string;
|
|
6
6
|
readonly tenantId: TenantId;
|
|
7
7
|
readonly role: string;
|
|
8
|
-
readonly
|
|
8
|
+
readonly invitation: InvitationIssuance;
|
|
9
9
|
};
|
|
10
|
+
export type InvitationIssuance = {
|
|
11
|
+
readonly membershipVersion: number | null;
|
|
12
|
+
readonly insertedAt: Temporal.Instant;
|
|
13
|
+
readonly modifiedAt: Temporal.Instant | null;
|
|
14
|
+
};
|
|
15
|
+
export declare function membershipVersionOf(db: Pick<TenantDb, "fetchOne">, tenantId: string, userId: string): Promise<number>;
|
|
10
16
|
export declare function grantInvitedMembershipRole(db: DbRunner, options: InvitedMembershipOptions): Promise<import("@cosmicdrift/kumiko-types/write-error-info-types").WriteFailure | {
|
|
11
17
|
readonly isSuccess: true;
|
|
12
18
|
readonly data: {
|
|
@@ -14,7 +20,3 @@ export declare function grantInvitedMembershipRole(db: DbRunner, options: Invite
|
|
|
14
20
|
readonly roles: readonly string[];
|
|
15
21
|
};
|
|
16
22
|
}>;
|
|
17
|
-
export declare function invitationIssuedAt(invitation: {
|
|
18
|
-
readonly insertedAt: Temporal.Instant;
|
|
19
|
-
readonly modifiedAt: Temporal.Instant | null;
|
|
20
|
-
}): Temporal.Instant;
|
|
@@ -9,6 +9,15 @@ import { findForbiddenMembershipRole, reservedMembershipRoleError } from "./memb
|
|
|
9
9
|
import { tenantMembershipEntity, tenantMembershipsTable } from "./membership-table.js";
|
|
10
10
|
import { seedTenantMembership } from "./seeding.js";
|
|
11
11
|
const membershipExecutor = createEventStoreExecutor(tenantMembershipsTable, tenantMembershipEntity, { entityName: "tenant-membership" });
|
|
12
|
+
const MEMBERSHIP_CREATED_VERSION = 1;
|
|
13
|
+
// 0 = the user has no membership in the tenant yet.
|
|
14
|
+
export async function membershipVersionOf(db, tenantId, userId) {
|
|
15
|
+
const membership = await db.fetchOne(tenantMembershipsTable, {
|
|
16
|
+
tenantId,
|
|
17
|
+
userId,
|
|
18
|
+
});
|
|
19
|
+
return membership?.version ?? 0;
|
|
20
|
+
}
|
|
12
21
|
// Add-only, so unlike updateMemberRoles no session revoke or last-TenantAdmin check.
|
|
13
22
|
// Read unfiltered: tenant:query:memberships hides disabled tenants.
|
|
14
23
|
export async function grantInvitedMembershipRole(db, options) {
|
|
@@ -28,7 +37,7 @@ export async function grantInvitedMembershipRole(db, options) {
|
|
|
28
37
|
return grantedMembership(false, [options.role]);
|
|
29
38
|
}
|
|
30
39
|
// A membership decision made after the invitation was issued wins over it.
|
|
31
|
-
if (
|
|
40
|
+
if (isSupersededByMembership(existing, options.invitation)) {
|
|
32
41
|
return writeFailure(invitationSupersededError());
|
|
33
42
|
}
|
|
34
43
|
const currentRoles = parseRoles(existing.roles);
|
|
@@ -40,13 +49,25 @@ export async function grantInvitedMembershipRole(db, options) {
|
|
|
40
49
|
return updateResult;
|
|
41
50
|
return grantedMembership(true, roles);
|
|
42
51
|
}
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
// The pinned version is the membership's version when the invitation was issued
|
|
53
|
+
// (0 = none yet). A different version now means the membership was decided after
|
|
54
|
+
// the invite. Pin 0: a membership that appeared afterwards is an additive grant
|
|
55
|
+
// only while it is untouched since its creation (version 1). A role change bumps
|
|
56
|
+
// the version above 1 and counts as a decision. A removal in between hard-deletes
|
|
57
|
+
// the row and cancels the invitation (remove-member), so it never reaches this check.
|
|
58
|
+
// Rows issued before the pin existed (null) fall back to wall-clock timestamps,
|
|
59
|
+
// which can misjudge changes within the same millisecond but is all they carry.
|
|
60
|
+
function isSupersededByMembership(membership, invitation) {
|
|
61
|
+
if (invitation.membershipVersion !== null) {
|
|
62
|
+
if (invitation.membershipVersion === 0) {
|
|
63
|
+
return membership.version > MEMBERSHIP_CREATED_VERSION;
|
|
64
|
+
}
|
|
65
|
+
return membership.version !== invitation.membershipVersion;
|
|
66
|
+
}
|
|
67
|
+
// Resend reuses the invitation row, so its last modification is the latest issuance.
|
|
68
|
+
const issuedAt = invitation.modifiedAt ?? invitation.insertedAt;
|
|
48
69
|
const lastChangedAt = membership.modifiedAt ?? membership.insertedAt;
|
|
49
|
-
return Temporal.Instant.compare(lastChangedAt,
|
|
70
|
+
return Temporal.Instant.compare(lastChangedAt, issuedAt) > 0;
|
|
50
71
|
}
|
|
51
72
|
function invitationSupersededError() {
|
|
52
73
|
return new ConflictError({
|
|
@@ -3,6 +3,7 @@ export declare const tenantMembershipEntity: import("@cosmicdrift/kumiko-types/f
|
|
|
3
3
|
readonly type: "text";
|
|
4
4
|
readonly description?: string;
|
|
5
5
|
readonly maxLength?: number;
|
|
6
|
+
readonly minLength?: number;
|
|
6
7
|
readonly required?: boolean;
|
|
7
8
|
readonly searchable?: boolean;
|
|
8
9
|
readonly sortable?: boolean;
|
|
@@ -24,6 +25,7 @@ export declare const tenantMembershipEntity: import("@cosmicdrift/kumiko-types/f
|
|
|
24
25
|
readonly type: "text";
|
|
25
26
|
readonly description?: string;
|
|
26
27
|
readonly maxLength?: number;
|
|
28
|
+
readonly minLength?: number;
|
|
27
29
|
readonly required?: boolean;
|
|
28
30
|
readonly searchable?: boolean;
|
|
29
31
|
readonly sortable?: boolean;
|
|
@@ -47,6 +49,7 @@ export declare const tenantMembershipsTable: import("@cosmicdrift/kumiko-framewo
|
|
|
47
49
|
readonly type: "text";
|
|
48
50
|
readonly description?: string;
|
|
49
51
|
readonly maxLength?: number;
|
|
52
|
+
readonly minLength?: number;
|
|
50
53
|
readonly required?: boolean;
|
|
51
54
|
readonly searchable?: boolean;
|
|
52
55
|
readonly sortable?: boolean;
|
|
@@ -68,6 +71,7 @@ export declare const tenantMembershipsTable: import("@cosmicdrift/kumiko-framewo
|
|
|
68
71
|
readonly type: "text";
|
|
69
72
|
readonly description?: string;
|
|
70
73
|
readonly maxLength?: number;
|
|
74
|
+
readonly minLength?: number;
|
|
71
75
|
readonly required?: boolean;
|
|
72
76
|
readonly searchable?: boolean;
|
|
73
77
|
readonly sortable?: boolean;
|
|
@@ -5,6 +5,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
|
|
|
5
5
|
readonly type: "text";
|
|
6
6
|
readonly description?: string;
|
|
7
7
|
readonly maxLength?: number;
|
|
8
|
+
readonly minLength?: number;
|
|
8
9
|
readonly required?: boolean;
|
|
9
10
|
readonly searchable?: boolean;
|
|
10
11
|
readonly sortable?: boolean;
|
|
@@ -26,6 +27,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
|
|
|
26
27
|
readonly type: "text";
|
|
27
28
|
readonly description?: string;
|
|
28
29
|
readonly maxLength?: number;
|
|
30
|
+
readonly minLength?: number;
|
|
29
31
|
readonly required?: boolean;
|
|
30
32
|
readonly searchable?: boolean;
|
|
31
33
|
readonly sortable?: boolean;
|
|
@@ -58,15 +60,16 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
|
|
|
58
60
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
59
61
|
readonly display?: "radio" | "dropdown";
|
|
60
62
|
readonly optionTones?: {
|
|
61
|
-
readonly active?:
|
|
62
|
-
readonly destroyFailed?:
|
|
63
|
-
readonly destroyRequested?:
|
|
64
|
-
readonly destroyed?:
|
|
65
|
-
readonly destroying?:
|
|
63
|
+
readonly active?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
64
|
+
readonly destroyFailed?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
65
|
+
readonly destroyRequested?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
66
|
+
readonly destroyed?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
67
|
+
readonly destroying?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
66
68
|
} | undefined;
|
|
67
69
|
readonly optionsQuery?: string;
|
|
68
70
|
readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
|
|
69
71
|
readonly optionsAvailabilityQuery?: string;
|
|
72
|
+
readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"active" | "destroyFailed" | "destroyRequested" | "destroyed" | "destroying">[] | undefined;
|
|
70
73
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
71
74
|
required: true;
|
|
72
75
|
};
|
|
@@ -89,6 +92,7 @@ export declare const tenantEntity: import("@cosmicdrift/kumiko-types/fields").En
|
|
|
89
92
|
readonly type: "text";
|
|
90
93
|
readonly description?: string;
|
|
91
94
|
readonly maxLength?: number;
|
|
95
|
+
readonly minLength?: number;
|
|
92
96
|
readonly required?: boolean;
|
|
93
97
|
readonly searchable?: boolean;
|
|
94
98
|
readonly sortable?: boolean;
|
|
@@ -157,6 +161,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
|
|
|
157
161
|
readonly type: "text";
|
|
158
162
|
readonly description?: string;
|
|
159
163
|
readonly maxLength?: number;
|
|
164
|
+
readonly minLength?: number;
|
|
160
165
|
readonly required?: boolean;
|
|
161
166
|
readonly searchable?: boolean;
|
|
162
167
|
readonly sortable?: boolean;
|
|
@@ -178,6 +183,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
|
|
|
178
183
|
readonly type: "text";
|
|
179
184
|
readonly description?: string;
|
|
180
185
|
readonly maxLength?: number;
|
|
186
|
+
readonly minLength?: number;
|
|
181
187
|
readonly required?: boolean;
|
|
182
188
|
readonly searchable?: boolean;
|
|
183
189
|
readonly sortable?: boolean;
|
|
@@ -210,15 +216,16 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
|
|
|
210
216
|
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
211
217
|
readonly display?: "radio" | "dropdown";
|
|
212
218
|
readonly optionTones?: {
|
|
213
|
-
readonly active?:
|
|
214
|
-
readonly destroyFailed?:
|
|
215
|
-
readonly destroyRequested?:
|
|
216
|
-
readonly destroyed?:
|
|
217
|
-
readonly destroying?:
|
|
219
|
+
readonly active?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
220
|
+
readonly destroyFailed?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
221
|
+
readonly destroyRequested?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
222
|
+
readonly destroyed?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
223
|
+
readonly destroying?: "bad" | "neutral" | "ok" | "warn" | undefined;
|
|
218
224
|
} | undefined;
|
|
219
225
|
readonly optionsQuery?: string;
|
|
220
226
|
readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
|
|
221
227
|
readonly optionsAvailabilityQuery?: string;
|
|
228
|
+
readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"active" | "destroyFailed" | "destroyRequested" | "destroyed" | "destroying">[] | undefined;
|
|
222
229
|
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
223
230
|
required: true;
|
|
224
231
|
};
|
|
@@ -241,6 +248,7 @@ export declare const tenantTable: import("@cosmicdrift/kumiko-framework/db").Ent
|
|
|
241
248
|
readonly type: "text";
|
|
242
249
|
readonly description?: string;
|
|
243
250
|
readonly maxLength?: number;
|
|
251
|
+
readonly minLength?: number;
|
|
244
252
|
readonly required?: boolean;
|
|
245
253
|
readonly searchable?: boolean;
|
|
246
254
|
readonly sortable?: boolean;
|
|
@@ -17,19 +17,17 @@ export const tenantEntity = createEntity({
|
|
|
17
17
|
key: createTextField({
|
|
18
18
|
required: true,
|
|
19
19
|
maxLength: 50,
|
|
20
|
+
sortable: true,
|
|
20
21
|
personal: false,
|
|
21
22
|
reason: "technical_reference",
|
|
22
23
|
}),
|
|
23
|
-
//
|
|
24
|
-
// same class as the `company.legalName` example in the boot-validator's
|
|
25
|
-
// own PII-heuristic doc-comment.
|
|
24
|
+
// Subject is the tenant record itself: tenant events live in the writer's (system) tenant stream, so `personal: "tenant"` would not resolve to this tenant.
|
|
26
25
|
name: createTextField({
|
|
27
26
|
required: true,
|
|
28
27
|
maxLength: 200,
|
|
29
28
|
searchable: true,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
reason: "is_business_data",
|
|
29
|
+
personal: { of: "id" },
|
|
30
|
+
find: "none",
|
|
33
31
|
}),
|
|
34
32
|
isEnabled: createBooleanField({ default: true, filterable: true }),
|
|
35
33
|
// Tenant-destroy lifecycle (tenant-lifecycle feature). Defaults keep
|
package/dist/tenant/screens.js
CHANGED
|
@@ -23,7 +23,7 @@ export const tenantListScreen = {
|
|
|
23
23
|
entityId: "id",
|
|
24
24
|
},
|
|
25
25
|
],
|
|
26
|
-
defaultSort: { field: "
|
|
26
|
+
defaultSort: { field: "key", dir: "asc" },
|
|
27
27
|
searchable: true,
|
|
28
28
|
access: { roles: ["SystemAdmin"] },
|
|
29
29
|
// Inert until an app navs it (see file header) — no nav area to resolve
|
|
@@ -174,7 +174,7 @@ export async function moveTransferGraph(args) {
|
|
|
174
174
|
let pending = [
|
|
175
175
|
{ entityName: rootEntityName, rowIds: [rootRowId] },
|
|
176
176
|
];
|
|
177
|
-
for (let round = 0; round
|
|
177
|
+
for (let round = 0; round <= MAX_TRANSFER_DEPTH && pending.length > 0; round++) {
|
|
178
178
|
// Merged per type so an edge leaving a type runs once per round, not once
|
|
179
179
|
// per edge that reached it.
|
|
180
180
|
const discovered = new Map();
|
|
@@ -231,24 +231,17 @@ export async function moveTransferGraph(args) {
|
|
|
231
231
|
}));
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
+
// The extra round past the limit is a probe: it only exists to find out
|
|
235
|
+
// whether rows still hang below the last allowed hop. Its moves are rolled
|
|
236
|
+
// back with the transaction when this throws.
|
|
237
|
+
const [strandedEntityName] = discovered.keys();
|
|
238
|
+
if (round === MAX_TRANSFER_DEPTH && strandedEntityName !== undefined) {
|
|
239
|
+
throw new UnprocessableError("transfer_graph_too_deep", {
|
|
240
|
+
i18nKey: "errors.tenantHandover.transferGraphTooDeep",
|
|
241
|
+
details: { entityName: strandedEntityName },
|
|
242
|
+
});
|
|
243
|
+
}
|
|
234
244
|
pending = [...discovered].map(([entityName, rowIds]) => ({ entityName, rowIds }));
|
|
235
245
|
}
|
|
236
|
-
// The depth limit is a safety net, not a licence to move part of a graph: if
|
|
237
|
-
// rounds ran out while rows remain whose type still leads somewhere, the
|
|
238
|
-
// declaration reaches further than the mover does, and returning quietly
|
|
239
|
-
// would leave exactly the orphaned rows #3088 is about.
|
|
240
|
-
//
|
|
241
|
-
// Rounds count hops of both edge kinds, while the boot validator measures
|
|
242
|
-
// reference chains between transferable entities only. A graph that fills the
|
|
243
|
-
// limit with reference hops and then adds a parentRef child therefore boots
|
|
244
|
-
// clean and fails here instead — the residual the validator cannot see, and
|
|
245
|
-
// the reason this check has to exist rather than trusting boot alone.
|
|
246
|
-
const stranded = pending.find(({ entityName }) => (adjacency.get(entityName) ?? []).length > 0);
|
|
247
|
-
if (stranded !== undefined) {
|
|
248
|
-
throw new UnprocessableError("transfer_graph_too_deep", {
|
|
249
|
-
i18nKey: "errors.tenantHandover.transferGraphTooDeep",
|
|
250
|
-
details: { entityName: stranded.entityName },
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
246
|
return movedCounts;
|
|
254
247
|
}
|
|
@@ -1,23 +1,4 @@
|
|
|
1
|
-
import { type EntityDefinition, type Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
export type TransferEdgeLink
|
|
3
|
-
readonly kind: "parentRef";
|
|
4
|
-
readonly typeField: string;
|
|
5
|
-
readonly idField: string;
|
|
6
|
-
} | {
|
|
7
|
-
readonly kind: "reference";
|
|
8
|
-
readonly field: string;
|
|
9
|
-
};
|
|
10
|
-
export type TransferEdge = {
|
|
11
|
-
readonly entityName: string;
|
|
12
|
-
readonly entity: EntityDefinition;
|
|
13
|
-
/** The already-resolved type this edge hangs off — its moved row ids are
|
|
14
|
-
* the parent ids the mover matches this edge's rows against. */
|
|
15
|
-
readonly parentEntityName: string;
|
|
16
|
-
readonly link: TransferEdgeLink;
|
|
17
|
-
};
|
|
1
|
+
import { type EntityDefinition, type Registry, type TransferAdjacency } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
export type { TransferAdjacency, TransferEdge, TransferEdgeLink, } from "@cosmicdrift/kumiko-framework/engine";
|
|
18
3
|
export declare function resolveTransferableRoot(registry: Registry, entityType: string): EntityDefinition | undefined;
|
|
19
|
-
/** Outgoing edges per type: keyed by the type an edge hangs off, so the mover
|
|
20
|
-
* can ask "what hangs off the rows I just moved" for any type it reaches, at
|
|
21
|
-
* any depth, as often as it reaches it. */
|
|
22
|
-
export type TransferAdjacency = ReadonlyMap<string, readonly TransferEdge[]>;
|
|
23
4
|
export declare function resolveTransferAdjacency(registry: Registry, rootEntityType: string): TransferAdjacency;
|