@cosmicdrift/kumiko-bundled-features 0.365.1 → 0.367.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/admin-shell/feature.js +9 -9
- package/dist/agent-tools/tool-dispatch.d.ts +9 -1
- package/dist/agent-tools/tool-dispatch.js +55 -5
- package/dist/audit/feature.js +1 -1
- package/dist/auth-email-password/constants.d.ts +0 -1
- package/dist/auth-email-password/constants.js +0 -2
- package/dist/auth-email-password/feature.js +3 -3
- package/dist/auth-email-password/handlers/invite-create.write.js +1 -0
- package/dist/auth-mfa/feature.js +4 -4
- package/dist/auth-mfa-user-data/index.js +1 -1
- package/dist/billing-foundation/consumer-protection/register-consent.js +1 -1
- package/dist/billing-foundation/feature.js +1 -1
- package/dist/cap-counter/cap-reservation.js +73 -25
- package/dist/cap-counter/constants.d.ts +1 -0
- package/dist/cap-counter/constants.js +2 -0
- package/dist/cap-counter/feature.js +2 -2
- package/dist/cap-counter/tables.js +5 -2
- package/dist/cap-overview/feature.js +3 -3
- package/dist/channel-discord/feature.js +2 -2
- package/dist/channel-email/email-channel.d.ts +5 -1
- package/dist/channel-email/email-channel.js +8 -2
- package/dist/channel-email/feature.js +2 -2
- package/dist/channel-in-app/feature.js +1 -1
- package/dist/channel-push/feature.js +1 -1
- package/dist/channel-slack/feature.js +2 -2
- package/dist/channel-teams/feature.js +2 -2
- package/dist/channel-telegram/feature.js +2 -2
- package/dist/compliance-profiles/feature.js +1 -1
- package/dist/compliance-profiles-ops/index.js +3 -3
- package/dist/config/feature.js +1 -1
- package/dist/custom-fields/feature.js +2 -2
- package/dist/custom-fields/wire-for-entity.js +17 -13
- package/dist/data-retention/feature.js +1 -1
- package/dist/data-retention/presets.d.ts +4 -4
- package/dist/delivery/channel-context.js +1 -0
- package/dist/delivery/db/queries/address-opt-outs.d.ts +1 -1
- package/dist/delivery/db/queries/address-opt-outs.js +6 -2
- package/dist/delivery/db/queries/preferences.d.ts +1 -1
- package/dist/delivery/db/queries/preferences.js +4 -4
- package/dist/delivery/delivery-service.d.ts +2 -0
- package/dist/delivery/delivery-service.js +39 -6
- package/dist/delivery/feature.js +10 -6
- package/dist/delivery/handlers/resubscribe-address.write.js +13 -9
- package/dist/delivery/handlers/resubscribe-user.write.js +3 -1
- package/dist/delivery/handlers/set-preference.write.js +3 -1
- package/dist/delivery/handlers/unsubscribe-address.write.d.ts +6 -0
- package/dist/delivery/handlers/unsubscribe-address.write.js +11 -6
- package/dist/delivery/handlers/unsubscribe-user.write.js +3 -1
- package/dist/delivery/index.d.ts +3 -0
- package/dist/delivery/index.js +2 -0
- package/dist/delivery/notification-type-aliases.d.ts +3 -0
- package/dist/delivery/notification-type-aliases.js +11 -0
- package/dist/delivery/testing.d.ts +2 -0
- package/dist/delivery/types.d.ts +2 -1
- package/dist/delivery/unsubscribe-link.d.ts +25 -0
- package/dist/delivery/unsubscribe-link.js +40 -0
- package/dist/delivery/unsubscribe-page-strings.d.ts +11 -0
- package/dist/delivery/unsubscribe-page-strings.js +15 -0
- package/dist/delivery/unsubscribe.d.ts +3 -0
- package/dist/delivery/unsubscribe.js +65 -29
- package/dist/derivatives-sharp/feature.js +1 -1
- package/dist/document-ingest-foundation/feature.js +2 -2
- package/dist/file-derivatives/feature.js +3 -3
- package/dist/file-foundation/feature.js +1 -1
- package/dist/file-provider-inmemory/feature.js +2 -2
- package/dist/file-provider-s3/feature.js +3 -3
- package/dist/file-provider-s3-env/feature.js +2 -2
- package/dist/files-tenant-data/index.js +1 -1
- package/dist/folders/feature.js +2 -2
- package/dist/folders-user-data/index.js +1 -1
- package/dist/form-draft/feature.js +2 -2
- package/dist/form-draft-user-data/index.js +1 -1
- package/dist/inbound-mail-foundation/connect-routes.d.ts +3 -0
- package/dist/inbound-mail-foundation/connect-routes.js +6 -1
- package/dist/inbound-mail-foundation/feature.js +1 -1
- package/dist/inbound-mail-foundation/types.d.ts +17 -7
- package/dist/inbound-mail-foundation/watch-supervisor.js +16 -2
- package/dist/inbound-provider-imap/feature.js +2 -2
- package/dist/inbound-provider-inmemory/feature.js +1 -1
- package/dist/ledger/feature.js +5 -5
- package/dist/legal-pages/feature.js +1 -1
- package/dist/mail-foundation/feature.d.ts +16 -1
- package/dist/mail-foundation/feature.js +30 -9
- package/dist/mail-foundation/index.d.ts +1 -1
- package/dist/mail-foundation/index.js +1 -1
- package/dist/mail-transport-inmemory/feature.js +2 -2
- package/dist/mail-transport-smtp/feature.js +3 -3
- package/dist/managed-pages/css-gate.js +1 -1
- package/dist/managed-pages/feature.js +2 -2
- package/dist/metrics/feature.js +2 -2
- package/dist/notes-history/feature.js +2 -2
- package/dist/notes-history-user-data/index.js +1 -1
- package/dist/personal-access-tokens/feature.js +17 -13
- package/dist/readiness/feature.js +2 -2
- package/dist/renderer-foundation/feature.js +1 -1
- package/dist/renderer-pdf/feature.js +1 -1
- package/dist/secrets/feature.js +1 -1
- package/dist/seo/feature.js +1 -1
- package/dist/sessions/feature.js +20 -16
- package/dist/step-dispatcher/feature.js +1 -1
- package/dist/subscription-mollie/feature.js +1 -1
- package/dist/subscription-stripe/feature.js +3 -3
- package/dist/tags/feature.js +2 -2
- package/dist/template-resolver/compile-document-body.js +47 -11
- package/dist/template-resolver/document-sanitizer.d.ts +2 -0
- package/dist/template-resolver/document-sanitizer.js +16 -1
- package/dist/template-resolver/document-schema.d.ts +2 -0
- package/dist/template-resolver/document-schema.js +31 -0
- package/dist/template-resolver/feature.js +2 -2
- package/dist/template-resolver/handlers/bundle-import.js +3 -2
- package/dist/template-resolver/handlers/copy-locale.write.js +3 -2
- package/dist/template-resolver/handlers/find-by-id.query.js +5 -1
- package/dist/template-resolver/handlers/list.query.js +3 -1
- package/dist/template-resolver/handlers/overview.query.js +4 -2
- package/dist/template-resolver/handlers/template-write.d.ts +2 -1
- package/dist/template-resolver/handlers/template-write.js +12 -1
- package/dist/template-resolver/index.d.ts +3 -2
- package/dist/template-resolver/index.js +3 -2
- package/dist/template-resolver/qualified-names.d.ts +1 -0
- package/dist/template-resolver/qualified-names.js +1 -0
- package/dist/template-resolver/seeding.d.ts +4 -0
- package/dist/template-resolver/seeding.js +40 -5
- package/dist/template-resolver/template-preview.js +2 -0
- package/dist/template-resolver/template-system-only.d.ts +8 -0
- package/dist/template-resolver/template-system-only.js +11 -0
- package/dist/template-resolver/template-validation.js +42 -28
- package/dist/template-resolver/web/i18n.js +1 -0
- package/dist/template-resolver-user-data/index.js +1 -1
- package/dist/tenant/feature.js +4 -4
- package/dist/tenant/seeding.js +1 -1
- package/dist/tenant-lifecycle/feature.js +2 -3
- package/dist/tenant-settings/feature.js +1 -1
- package/dist/tier-engine/feature.js +86 -72
- package/dist/user/feature.js +1 -1
- package/dist/user-data-rights/feature.js +4 -4
- package/dist/user-data-rights-defaults/feature.js +2 -2
- package/dist/user-profile/feature.js +4 -4
- package/dist/waitlist/feature.js +2 -2
- package/package.json +10 -10
- package/src/agent-tools/changes.json +6 -0
- package/src/auth-email-password/changes.json +13 -0
- package/src/cap-counter/changes.json +7 -0
- package/src/channel-email/changes.json +6 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/delivery/changes.json +18 -0
- package/src/inbound-mail-foundation/changes.json +13 -0
- package/src/template-resolver/changes.json +25 -0
- package/src/user-data-rights/changes.json +6 -0
|
@@ -284,6 +284,36 @@ export const DocumentTextSize = Extension.create({
|
|
|
284
284
|
];
|
|
285
285
|
},
|
|
286
286
|
});
|
|
287
|
+
// A block shown only when the named variable has a value (data-if, the
|
|
288
|
+
// conditional counterpart of data-each). Table rows qualify so an invoice can
|
|
289
|
+
// drop a discount line; tiptap merges the attribute into paragraph, heading
|
|
290
|
+
// and row markup only.
|
|
291
|
+
export const DOCUMENT_CONDITION_ATTRIBUTE = "data-if";
|
|
292
|
+
export const DocumentCondition = Extension.create({
|
|
293
|
+
name: "documentCondition",
|
|
294
|
+
addGlobalAttributes() {
|
|
295
|
+
return [
|
|
296
|
+
{
|
|
297
|
+
types: ["paragraph", "heading", "tableRow"],
|
|
298
|
+
attributes: {
|
|
299
|
+
showIf: {
|
|
300
|
+
default: null,
|
|
301
|
+
parseHTML: (element) => {
|
|
302
|
+
const name = element.getAttribute(DOCUMENT_CONDITION_ATTRIBUTE);
|
|
303
|
+
return name !== null && isDocumentReferenceName(name) ? name : null;
|
|
304
|
+
},
|
|
305
|
+
renderHTML: (attributes) => {
|
|
306
|
+
const name = attributes["showIf"];
|
|
307
|
+
return typeof name === "string" && isDocumentReferenceName(name)
|
|
308
|
+
? { [DOCUMENT_CONDITION_ATTRIBUTE]: name }
|
|
309
|
+
: {};
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
},
|
|
316
|
+
});
|
|
287
317
|
export const documentExtensions = [
|
|
288
318
|
StarterKit.configure({
|
|
289
319
|
blockquote: false,
|
|
@@ -318,4 +348,5 @@ export const documentExtensions = [
|
|
|
318
348
|
DocumentSteps,
|
|
319
349
|
DocumentTextAlign,
|
|
320
350
|
DocumentTextSize,
|
|
351
|
+
DocumentCondition,
|
|
321
352
|
];
|
|
@@ -63,13 +63,13 @@ export function createTemplateResolverFeature(opts = {}) {
|
|
|
63
63
|
resourceMaxBytes: createSystemConfig("number", { default: 2_097_152, bounds: { min: 1 } }),
|
|
64
64
|
},
|
|
65
65
|
});
|
|
66
|
-
r.entity("template-resource", templateResourceEntity);
|
|
66
|
+
r.entity({ name: "template-resource", ...templateResourceEntity });
|
|
67
67
|
// Only when the app actually mounts a user-owned collection: the entity's
|
|
68
68
|
// `content` is `userOwned`, which makes it subject data — the boot guard
|
|
69
69
|
// then requires an EXT_USER_DATA hook (mount `template-resolver-user-data`).
|
|
70
70
|
// Registering it unconditionally would impose that on every app.
|
|
71
71
|
if (hasUserOwned)
|
|
72
|
-
r.entity("user-content-entry", userContentEntryEntity);
|
|
72
|
+
r.entity({ name: "user-content-entry", ...userContentEntryEntity });
|
|
73
73
|
const handlers = {
|
|
74
74
|
upsertSystem: r.writeHandler(upsertSystemWrite),
|
|
75
75
|
upsertTenant: r.writeHandler(upsertTenantWrite),
|
|
@@ -7,7 +7,7 @@ import { attachTemplateResources } from "../template-resources.js";
|
|
|
7
7
|
import { resolveTemplateVariables } from "../template-validation.js";
|
|
8
8
|
import { MAX_FILE_ATTACHMENTS_PER_TEMPLATE } from "./resource-shared.js";
|
|
9
9
|
import { evaluateTemplateWrite } from "./template-gates.js";
|
|
10
|
-
import { refuseTenantScopeUnderSystemTenant, writeTemplateRow, } from "./template-write.js";
|
|
10
|
+
import { refuseSystemOnlyTenantOverride, refuseTenantScopeUnderSystemTenant, writeTemplateRow, } from "./template-write.js";
|
|
11
11
|
export function targetExistsIssue(locale) {
|
|
12
12
|
return {
|
|
13
13
|
path: "locale",
|
|
@@ -61,7 +61,8 @@ function checkBundleAttachments(bundle) {
|
|
|
61
61
|
// beforeTransaction half: validates the bundle and uploads every resource file to the target
|
|
62
62
|
// tenant's storage. A failed or throwing upload releases the ones before it.
|
|
63
63
|
export async function prepareTemplateBundleBeforeTransaction(args) {
|
|
64
|
-
const refused = refuseTenantScopeUnderSystemTenant(args.scope, args.tenantId)
|
|
64
|
+
const refused = refuseTenantScopeUnderSystemTenant(args.scope, args.tenantId) ??
|
|
65
|
+
refuseSystemOnlyTenantOverride(args.ctx.registry, args.scope, args.bundle.slug, args.bundle.kind);
|
|
65
66
|
if (refused !== null)
|
|
66
67
|
return refused;
|
|
67
68
|
const issues = await collectBundleIssues(args, args.status);
|
|
@@ -15,7 +15,7 @@ import { resolveTemplateVariables } from "../template-validation.js";
|
|
|
15
15
|
import { targetExistsIssue } from "./bundle-import.js";
|
|
16
16
|
import { kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
|
|
17
17
|
import { parseStoredJson, storedRecord } from "./template-gates.js";
|
|
18
|
-
import { refuseTenantScopeUnderSystemTenant, writeTemplateRow } from "./template-write.js";
|
|
18
|
+
import { refuseSystemOnlyTenantOverride, refuseTenantScopeUnderSystemTenant, writeTemplateRow, } from "./template-write.js";
|
|
19
19
|
function readDbFor(ctx, scope) {
|
|
20
20
|
return scope === "system" ? requireForTenant(ctx, SYSTEM_TENANT_ID).db : ctx.db;
|
|
21
21
|
}
|
|
@@ -59,7 +59,8 @@ async function checkCopyRequest(ctx, event) {
|
|
|
59
59
|
}));
|
|
60
60
|
}
|
|
61
61
|
const tenantId = scope === "system" ? SYSTEM_TENANT_ID : event.user.tenantId;
|
|
62
|
-
const refused = refuseTenantScopeUnderSystemTenant(scope, tenantId)
|
|
62
|
+
const refused = refuseTenantScopeUnderSystemTenant(scope, tenantId) ??
|
|
63
|
+
refuseSystemOnlyTenantOverride(ctx.registry, scope, slug, kind);
|
|
63
64
|
if (refused !== null)
|
|
64
65
|
return refused;
|
|
65
66
|
const readDb = readDbFor(ctx, scope);
|
|
@@ -3,6 +3,7 @@ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { templateResourcesTable } from "../table.js";
|
|
5
5
|
import { draftViewerTenantId, isHiddenSystemDraft, toWorkingRow } from "../template-draft.js";
|
|
6
|
+
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.js";
|
|
6
7
|
// Admin-Lookup für UI-Edit-Flow. Returnt das raw Template inkl. draft/
|
|
7
8
|
// archived Status. Tenant-Isolation: User sieht nur Templates des
|
|
8
9
|
// eigenen Tenants oder SYSTEM_TENANT (system-defaults sind public-
|
|
@@ -17,8 +18,11 @@ export const findByIdQuery = defineQueryHandler({
|
|
|
17
18
|
const stored = await fetchOne(ctx.db, templateResourcesTable, {
|
|
18
19
|
id: query.payload.id,
|
|
19
20
|
});
|
|
20
|
-
if (!stored ||
|
|
21
|
+
if (!stored ||
|
|
22
|
+
isHiddenSystemDraft(query.user, stored) ||
|
|
23
|
+
isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, stored)) {
|
|
21
24
|
return null;
|
|
25
|
+
}
|
|
22
26
|
const row = toWorkingRow(stored, draftViewerTenantId(query.user, stored));
|
|
23
27
|
const isOwnTenant = row.tenantId === query.user.tenantId;
|
|
24
28
|
const isSystemTemplate = row.scope === "system";
|
|
@@ -4,6 +4,7 @@ import * as z from "zod";
|
|
|
4
4
|
import { TEMPLATE_KINDS, TEMPLATE_STATUSES } from "../constants.js";
|
|
5
5
|
import { templateResourcesTable } from "../table.js";
|
|
6
6
|
import { isHiddenSystemDraft } from "../template-draft.js";
|
|
7
|
+
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.js";
|
|
7
8
|
// ctx.db is tenant-scoped even for SystemAdmin: every caller sees its own
|
|
8
9
|
// tenant's rows plus the system defaults, never another tenant's rows.
|
|
9
10
|
export const listQuery = defineQueryHandler({
|
|
@@ -39,7 +40,8 @@ export const listQuery = defineQueryHandler({
|
|
|
39
40
|
limit: 500,
|
|
40
41
|
}));
|
|
41
42
|
return rows
|
|
42
|
-
.filter((row) => !isHiddenSystemDraft(query.user, row)
|
|
43
|
+
.filter((row) => !isHiddenSystemDraft(query.user, row) &&
|
|
44
|
+
!isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, row))
|
|
43
45
|
.map((row) => ({
|
|
44
46
|
id: String(row.id),
|
|
45
47
|
tenantId: row.tenantId,
|
|
@@ -4,6 +4,7 @@ import * as z from "zod";
|
|
|
4
4
|
import { DEFAULT_MAIL_LAYOUT_SLUG, FALLBACK_LOCALE } from "../constants.js";
|
|
5
5
|
import { templateResourcesTable } from "../table.js";
|
|
6
6
|
import { activeSinceOf, activeVersionOf, draftViewerTenantId, isHiddenSystemDraft, parseTemplateDraft, servedVersionOf, } from "../template-draft.js";
|
|
7
|
+
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.js";
|
|
7
8
|
import { collectTemplateUsage } from "../template-usage.js";
|
|
8
9
|
import { storedRecord } from "./template-gates.js";
|
|
9
10
|
const OVERVIEW_KINDS = ["mail", "document-pdf", "mail-layout", "pdf-layout"];
|
|
@@ -183,7 +184,7 @@ export const overviewQuery = defineQueryHandler({
|
|
|
183
184
|
totalCount: z.boolean().optional(),
|
|
184
185
|
}),
|
|
185
186
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
186
|
-
description: "Lists mail, PDF and layout templates as one row per slug and kind for the caller's tenant (optionally only the mails and PDFs using one layout slug): where the effective version comes from (own override or system), its locales, status and version (the served one; `draftVersion` is set when an unpublished draft waits beside it, `activeSince` says since when the served version applies), the system version it overrides, the layout it uses and which notifications or templates use it; use it to browse and filter all templates at a glance.",
|
|
187
|
+
description: "Lists mail, PDF and layout templates as one row per slug and kind for the caller's tenant (optionally only the mails and PDFs using one layout slug): where the effective version comes from (own override or system), its locales, status and version (the served one; `draftVersion` is set when an unpublished draft waits beside it, `activeSince` says since when the served version applies), the system version it overrides, the layout it uses and which notifications or templates use it; templates of notifications registered with `systemOnly` are listed for SystemAdmins only; use it to browse and filter all templates at a glance.",
|
|
187
188
|
handler: async (query, ctx) => {
|
|
188
189
|
const payload = query.payload;
|
|
189
190
|
const fetchedRows = await selectMany(ctx.db, templateResourcesTable, { kind: { in: [...OVERVIEW_KINDS] } }, { orderBy: { col: "id", direction: "asc" }, limit: OVERVIEW_ROW_LIMIT + 1 });
|
|
@@ -194,7 +195,8 @@ export const overviewQuery = defineQueryHandler({
|
|
|
194
195
|
limit: OVERVIEW_ROW_LIMIT,
|
|
195
196
|
});
|
|
196
197
|
}
|
|
197
|
-
const visibleRows = rows.filter((row) => !isHiddenSystemDraft(query.user, row)
|
|
198
|
+
const visibleRows = rows.filter((row) => !isHiddenSystemDraft(query.user, row) &&
|
|
199
|
+
!isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, row));
|
|
198
200
|
const { drafts, layoutSlugByDocument } = buildDrafts(groupBySlugAndKind(visibleRows), query.user);
|
|
199
201
|
const effectiveLayouts = effectiveMailLayoutSlugs(visibleRows);
|
|
200
202
|
const overviewRows = drafts.map((draft) => withUsage(draft, layoutSlugByDocument, ctx.registry, effectiveLayouts));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type HandlerContext, type SessionUser, type TemplateVariables, type TenantId, type WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
1
|
+
import { type HandlerContext, type Registry, type SessionUser, type TemplateVariables, type TenantId, type WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { type WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import type { ContentFormat, TemplateKind } from "../constants.js";
|
|
4
4
|
import type { TemplateWriteScope } from "../layout-regions.js";
|
|
@@ -32,6 +32,7 @@ export type TemplateRowWrite = {
|
|
|
32
32
|
readonly payload: TemplateRowPayload;
|
|
33
33
|
};
|
|
34
34
|
export declare function refuseTenantScopeUnderSystemTenant(scope: TemplateWriteScope, tenantId: string): WriteResult<never> | null;
|
|
35
|
+
export declare function refuseSystemOnlyTenantOverride(registry: Registry, scope: TemplateWriteScope, slug: string, kind: string): WriteFailure | null;
|
|
35
36
|
export type GatedTemplateWrite = {
|
|
36
37
|
readonly ok: true;
|
|
37
38
|
readonly evaluated: Extract<TemplateWriteResult, {
|
|
@@ -2,8 +2,10 @@ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
2
2
|
import { SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { AccessDeniedError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import { requireForTenant } from "../../shared/index.js";
|
|
5
|
+
import { TemplateResolverErrors } from "../qualified-names.js";
|
|
5
6
|
import { templateResourcesTable } from "../table.js";
|
|
6
7
|
import { activationChanges, serializeTemplateDraft } from "../template-draft.js";
|
|
8
|
+
import { isSystemOnlyTemplate } from "../template-system-only.js";
|
|
7
9
|
import { executor } from "./shared.js";
|
|
8
10
|
import { evaluateTemplateWrite, } from "./template-gates.js";
|
|
9
11
|
// The tenant-scope-under-system-tenant ban lives here so no write path can
|
|
@@ -16,6 +18,14 @@ export function refuseTenantScopeUnderSystemTenant(scope, tenantId) {
|
|
|
16
18
|
details: { reason: "system_tenant_override_not_allowed_use_upsert_system" },
|
|
17
19
|
}));
|
|
18
20
|
}
|
|
21
|
+
export function refuseSystemOnlyTenantOverride(registry, scope, slug, kind) {
|
|
22
|
+
if (scope !== "tenant" || !isSystemOnlyTemplate(registry, slug, kind))
|
|
23
|
+
return null;
|
|
24
|
+
return writeFailure(new AccessDeniedError({
|
|
25
|
+
i18nKey: "templateResolver.errors.systemOnlyTemplate",
|
|
26
|
+
details: { reason: TemplateResolverErrors.systemOnlyTemplate },
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
19
29
|
// Evaluation plus the publish rule: a row that goes active must not carry
|
|
20
30
|
// publish issues, a draft may.
|
|
21
31
|
export function gateTemplateWrite(input, status) {
|
|
@@ -63,7 +73,8 @@ function updateChanges(existing, status, fields) {
|
|
|
63
73
|
// separately.
|
|
64
74
|
export async function writeTemplateRow(args) {
|
|
65
75
|
const { ctx, user, tenantId, scope, status, payload } = args;
|
|
66
|
-
const refused = refuseTenantScopeUnderSystemTenant(scope, tenantId)
|
|
76
|
+
const refused = refuseTenantScopeUnderSystemTenant(scope, tenantId) ??
|
|
77
|
+
refuseSystemOnlyTenantOverride(ctx.registry, scope, payload.slug, payload.kind);
|
|
67
78
|
if (refused !== null)
|
|
68
79
|
return refused;
|
|
69
80
|
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { createTemplateResolverApi, type ResolveRequest, requireTemplateResolver, TemplateNotFoundError, type TemplateResolverApi, type TemplateResource, } from "./api.js";
|
|
2
2
|
export { type CompileDocumentBodyOptions, collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.js";
|
|
3
3
|
export { CONTENT_FORMATS, type ContentFormat, DEFAULT_MAIL_LAYOUT_SLUG, DEFAULT_PDF_LAYOUT_SLUG, DOCUMENT_HTML_KINDS, type DocumentHtmlKind, FALLBACK_LOCALE, NON_RENDER_KINDS, type NonRenderKind, PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, type PdfOrientation, type PdfPageFormat, RENDER_KINDS, type RenderKind, SYSTEM_TENANT_ID, TEMPLATE_KINDS, TEMPLATE_SCOPES, TEMPLATE_STATUSES, TEXT_BLOCK_KIND, type TemplateKind, type TemplateScope, type TemplateStatus, UPSERT_KINDS, type UpsertKind, } from "./constants.js";
|
|
4
|
-
export { type DocumentViolation, type DocumentViolationCode, documentViolationsToFieldIssues, type SanitizeBodyResult, type SanitizeHtmlResult, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.js";
|
|
5
|
-
export { DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, type DocumentTextAlignment, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
4
|
+
export { type DocumentViolation, type DocumentViolationCode, documentViolationsToFieldIssues, flattenParagraphsToLineBreaks, type SanitizeBodyResult, type SanitizeHtmlResult, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.js";
|
|
5
|
+
export { DOCUMENT_CONDITION_ATTRIBUTE, DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentCondition, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, type DocumentTextAlignment, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
6
6
|
export { createTemplateResolverFeature } from "./feature.js";
|
|
7
7
|
export type { LayoutSourceResponse, LayoutSourceRow } from "./handlers/layout-source.query.js";
|
|
8
8
|
export type { OverviewKindGroup, OverviewOrigin, OverviewResponse, OverviewRow, OverviewUsage, } from "./handlers/overview.query.js";
|
|
@@ -19,6 +19,7 @@ export { type TemplateBundle, templateBundleSchema } from "./template-bundle.js"
|
|
|
19
19
|
export { isMailTemplateMeta, type LayoutTemplateMeta, MAIL_CATEGORIES, MAIL_TONES, type MailCategory, type MailLayoutMeta, type MailTemplateMeta, type MailTone, type PdfTemplateMeta, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readMailLayoutMeta, readPdfTemplateMeta, type TemplateMeta, type TemplateMetaIssue, type TemplateMetaResult, templateSlugSchema, } from "./template-meta.js";
|
|
20
20
|
export { type DocumentDraftOutcome, evaluateDocumentDraft } from "./template-preview.js";
|
|
21
21
|
export { readTemplateResource, type TemplateResourceFile } from "./template-resources.js";
|
|
22
|
+
export { isSystemOnlyTemplate } from "./template-system-only.js";
|
|
22
23
|
export { collectTemplateUsage, type TemplateUsageEntry } from "./template-usage.js";
|
|
23
24
|
export { hasPlaceholderProblems, resolveTemplateVariables, SYSTEM_PLACEHOLDERS, systemPlaceholdersFor, type TemplateValidation, validateTemplatePlaceholders, } from "./template-validation.js";
|
|
24
25
|
export { type UserContentEntryRow, userContentEntriesTable, userContentEntryEntity, } from "./user-content-table.js";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { createTemplateResolverApi, requireTemplateResolver, TemplateNotFoundError, } from "./api.js";
|
|
2
2
|
export { collectDocumentResourceKeys, compileDocumentBodyToHandlebars, toResourceDataUri, } from "./compile-document-body.js";
|
|
3
3
|
export { CONTENT_FORMATS, DEFAULT_MAIL_LAYOUT_SLUG, DEFAULT_PDF_LAYOUT_SLUG, DOCUMENT_HTML_KINDS, FALLBACK_LOCALE, NON_RENDER_KINDS, PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, RENDER_KINDS, SYSTEM_TENANT_ID, TEMPLATE_KINDS, TEMPLATE_SCOPES, TEMPLATE_STATUSES, TEXT_BLOCK_KIND, UPSERT_KINDS, } from "./constants.js";
|
|
4
|
-
export { documentViolationsToFieldIssues, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.js";
|
|
5
|
-
export { DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
4
|
+
export { documentViolationsToFieldIssues, flattenParagraphsToLineBreaks, sanitizeDocumentHtml, sanitizeTemplateBody, } from "./document-sanitizer.js";
|
|
5
|
+
export { DOCUMENT_CONDITION_ATTRIBUTE, DOCUMENT_NODE_NAMES, DOCUMENT_TEXT_ALIGNMENTS, DocumentButton, DocumentCondition, DocumentDivider, DocumentLink, DocumentPageBreak, DocumentResource, DocumentTable, DocumentTableCell, DocumentTableHeader, DocumentTableRow, DocumentTextAlign, DocumentVariable, documentExtensions, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "./document-schema.js";
|
|
6
6
|
export { createTemplateResolverFeature } from "./feature.js";
|
|
7
7
|
export { localeSchema } from "./handlers/shared.js";
|
|
8
8
|
export { isHexColor, mailLayoutAppearance, } from "./layout-appearance.js";
|
|
@@ -17,6 +17,7 @@ export { templateBundleSchema } from "./template-bundle.js";
|
|
|
17
17
|
export { isMailTemplateMeta, MAIL_CATEGORIES, MAIL_TONES, parseTemplateMeta, pdfAttachmentFileNamePatternSchema, readMailLayoutMeta, readPdfTemplateMeta, templateSlugSchema, } from "./template-meta.js";
|
|
18
18
|
export { evaluateDocumentDraft } from "./template-preview.js";
|
|
19
19
|
export { readTemplateResource } from "./template-resources.js";
|
|
20
|
+
export { isSystemOnlyTemplate } from "./template-system-only.js";
|
|
20
21
|
export { collectTemplateUsage } from "./template-usage.js";
|
|
21
22
|
export { hasPlaceholderProblems, resolveTemplateVariables, SYSTEM_PLACEHOLDERS, systemPlaceholdersFor, validateTemplatePlaceholders, } from "./template-validation.js";
|
|
22
23
|
export { userContentEntriesTable, userContentEntryEntity, } from "./user-content-table.js";
|
|
@@ -44,6 +44,7 @@ export declare const TemplateResolverErrors: {
|
|
|
44
44
|
readonly alreadyArchived: "template_already_archived";
|
|
45
45
|
readonly targetExists: "target_exists";
|
|
46
46
|
readonly systemResourcesNeedSystemTenant: "system_resources_need_system_tenant";
|
|
47
|
+
readonly systemOnlyTemplate: "system_only_template";
|
|
47
48
|
readonly bundleLocalesDiffer: "bundle_locales_differ";
|
|
48
49
|
readonly resourceTooLarge: "template_resource_too_large";
|
|
49
50
|
readonly resourceUnsupportedImage: "template_resource_unsupported_image";
|
|
@@ -55,6 +55,7 @@ export const TemplateResolverErrors = {
|
|
|
55
55
|
alreadyArchived: "template_already_archived",
|
|
56
56
|
targetExists: "target_exists",
|
|
57
57
|
systemResourcesNeedSystemTenant: "system_resources_need_system_tenant",
|
|
58
|
+
systemOnlyTemplate: "system_only_template",
|
|
58
59
|
bundleLocalesDiffer: "bundle_locales_differ",
|
|
59
60
|
resourceTooLarge: "template_resource_too_large",
|
|
60
61
|
resourceUnsupportedImage: "template_resource_unsupported_image",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { type Registry, type SessionUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import type { Dispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
3
4
|
import { type SeedIfExists } from "@cosmicdrift/kumiko-framework/seeding";
|
|
4
5
|
import { type ContentFormat, type UpsertKind } from "./constants.js";
|
|
5
6
|
import type { TemplateBundle } from "./template-bundle.js";
|
|
@@ -24,6 +25,9 @@ export declare function seedSystemTemplate(db: DbConnection, opts: SeedSystemTem
|
|
|
24
25
|
export type SeedSystemTemplateBundleOptions = {
|
|
25
26
|
readonly by?: SessionUser;
|
|
26
27
|
readonly ifExists?: SeedIfExists;
|
|
28
|
+
/** Required for bundles with resources (e.g. a `logo`): the files go through the
|
|
29
|
+
* import-bundle-system write, which needs the file provider of the system tenant. */
|
|
30
|
+
readonly dispatcher?: Dispatcher;
|
|
27
31
|
};
|
|
28
32
|
export declare function seedSystemTemplateBundle(db: DbConnection, bundle: TemplateBundle, opts?: SeedSystemTemplateBundleOptions): Promise<{
|
|
29
33
|
ids: Record<string, string>;
|
|
@@ -13,10 +13,11 @@ import { createTenantDb, deleteMany, } from "@cosmicdrift/kumiko-framework/db";
|
|
|
13
13
|
import { createSystemUser, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
14
|
import { getStreamVersion, getUnscopedStreamMaxVersionForSeed, } from "@cosmicdrift/kumiko-framework/event-store";
|
|
15
15
|
import { runEventStoreSeed } from "@cosmicdrift/kumiko-framework/seeding";
|
|
16
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
16
17
|
import { TEXT_BLOCK_KIND } from "./constants.js";
|
|
17
18
|
import { executor } from "./handlers/shared.js";
|
|
18
19
|
import { evaluateTemplateWrite } from "./handlers/template-gates.js";
|
|
19
|
-
import { TEMPLATE_RESOLVER_FEATURE } from "./qualified-names.js";
|
|
20
|
+
import { TEMPLATE_RESOLVER_FEATURE, TemplateResolverHandlers } from "./qualified-names.js";
|
|
20
21
|
import { templateResourcesTable } from "./table.js";
|
|
21
22
|
import { activeStampChanges, servedStampChanges } from "./template-draft.js";
|
|
22
23
|
import { templateSlugSchema } from "./template-meta.js";
|
|
@@ -154,12 +155,12 @@ export async function seedSystemTemplate(db, opts) {
|
|
|
154
155
|
update: (row) => applyUpdate(row, existingRow),
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
|
-
// Seeds one system row per bundle locale. Bundled resources need a file
|
|
158
|
-
//
|
|
159
|
-
//
|
|
158
|
+
// Seeds one system row per bundle locale. Bundled resources need a file provider and
|
|
159
|
+
// the system tenant's file space, so those bundles are written through the
|
|
160
|
+
// import-bundle-system handler on the seed dispatcher instead of directly.
|
|
160
161
|
export async function seedSystemTemplateBundle(db, bundle, opts = {}) {
|
|
161
162
|
if (Object.keys(bundle.resources).length > 0) {
|
|
162
|
-
|
|
163
|
+
return seedSystemTemplateBundleWithResources(db, bundle, opts);
|
|
163
164
|
}
|
|
164
165
|
const ids = {};
|
|
165
166
|
for (const [locale, entry] of Object.entries(bundle.locales)) {
|
|
@@ -178,6 +179,40 @@ export async function seedSystemTemplateBundle(db, bundle, opts = {}) {
|
|
|
178
179
|
}
|
|
179
180
|
return { ids };
|
|
180
181
|
}
|
|
182
|
+
async function seedSystemTemplateBundleWithResources(db, bundle, opts) {
|
|
183
|
+
const label = `seedSystemTemplateBundle ${bundle.slug}/${bundle.kind}`;
|
|
184
|
+
const { dispatcher } = opts;
|
|
185
|
+
if (!dispatcher) {
|
|
186
|
+
throw new Error(`${label}: bundles with resources need a file provider; pass opts.dispatcher (the dispatcher of the seed deps) so the files are stored through the import-bundle-system write`);
|
|
187
|
+
}
|
|
188
|
+
if ((opts.ifExists ?? "skip") === "skip") {
|
|
189
|
+
const existingIds = {};
|
|
190
|
+
for (const locale of Object.keys(bundle.locales)) {
|
|
191
|
+
const row = await fetchOne(db, templateResourcesTable, {
|
|
192
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
193
|
+
slug: bundle.slug,
|
|
194
|
+
kind: bundle.kind,
|
|
195
|
+
locale,
|
|
196
|
+
});
|
|
197
|
+
if (row)
|
|
198
|
+
existingIds[locale] = String(row.id);
|
|
199
|
+
}
|
|
200
|
+
if (Object.keys(existingIds).length === Object.keys(bundle.locales).length) {
|
|
201
|
+
return { ids: existingIds };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const result = await dispatcher.write(TemplateResolverHandlers.importBundleSystem, { bundle }, opts.by ?? createSystemUser(SYSTEM_TENANT_ID, ["SystemAdmin"]));
|
|
205
|
+
if (!result.isSuccess) {
|
|
206
|
+
throw new Error(`${label} failed: ${result.error.code}: ${result.error.message}`);
|
|
207
|
+
}
|
|
208
|
+
return { ids: importedIds(result.data) };
|
|
209
|
+
}
|
|
210
|
+
function importedIds(data) {
|
|
211
|
+
const ids = isPlainObject(data) ? data["ids"] : undefined;
|
|
212
|
+
if (!isPlainObject(ids))
|
|
213
|
+
throw new Error("seedSystemTemplateBundle: import returned no ids");
|
|
214
|
+
return Object.fromEntries(Object.entries(ids).map(([locale, id]) => [locale, String(id)]));
|
|
215
|
+
}
|
|
181
216
|
export function hasNotificationTemplates(registry) {
|
|
182
217
|
return Array.from(registry.getAllNotifications().values()).some((notification) => notification.template !== undefined);
|
|
183
218
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
export declare function isSystemOnlyTemplate(registry: Registry, slug: string, kind: string): boolean;
|
|
3
|
+
export declare function isSystemOnlyTemplateHiddenFrom(user: {
|
|
4
|
+
readonly roles: readonly string[];
|
|
5
|
+
}, registry: Registry, row: {
|
|
6
|
+
readonly slug: string;
|
|
7
|
+
readonly kind: string;
|
|
8
|
+
}): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isSystemAdmin } from "./template-draft.js";
|
|
2
|
+
const NOTIFICATION_TEMPLATE_KIND = "mail";
|
|
3
|
+
// A notification registered with systemOnly keeps its mail template as the system default:
|
|
4
|
+
// tenants neither list nor override it, and overrides stored before the flag are ignored.
|
|
5
|
+
export function isSystemOnlyTemplate(registry, slug, kind) {
|
|
6
|
+
return (kind === NOTIFICATION_TEMPLATE_KIND &&
|
|
7
|
+
registry.getAllNotifications().get(slug)?.systemOnly === true);
|
|
8
|
+
}
|
|
9
|
+
export function isSystemOnlyTemplateHiddenFrom(user, registry, row) {
|
|
10
|
+
return !isSystemAdmin(user) && isSystemOnlyTemplate(registry, row.slug, row.kind);
|
|
11
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
import { withParsedDocument } from "./document-sanitizer.js";
|
|
3
|
-
import { isDocumentPlaceholderHref, isDocumentReferenceName } from "./document-schema.js";
|
|
2
|
+
import { conditionScopeElement, withParsedDocument } from "./document-sanitizer.js";
|
|
3
|
+
import { DOCUMENT_CONDITION_ATTRIBUTE, isDocumentPlaceholderHref, isDocumentReferenceName, } from "./document-schema.js";
|
|
4
4
|
export const SYSTEM_PLACEHOLDERS = new Set([
|
|
5
5
|
"tenantName",
|
|
6
6
|
"signature",
|
|
@@ -34,6 +34,7 @@ const scalarVariableSchema = z.discriminatedUnion("type", [
|
|
|
34
34
|
.optional(),
|
|
35
35
|
}),
|
|
36
36
|
z.object({ ...variableCommon, type: z.literal("date"), example: textLikeExample }),
|
|
37
|
+
z.object({ ...variableCommon, type: z.literal("percent"), example: z.number().optional() }),
|
|
37
38
|
]);
|
|
38
39
|
const variableDefSchema = z.union([
|
|
39
40
|
scalarVariableSchema,
|
|
@@ -99,34 +100,47 @@ const NAME_ATTRIBUTE_BY_TAG = {
|
|
|
99
100
|
span: "data-variable",
|
|
100
101
|
div: "data-steps",
|
|
101
102
|
};
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
function scanRepeatRows(root, scope) {
|
|
104
|
+
for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
|
|
105
|
+
const listName = element.getAttribute("data-each") ?? "";
|
|
106
|
+
const def = Object.hasOwn(scope.variables, listName) ? scope.variables[listName] : undefined;
|
|
107
|
+
if (def?.type === "list")
|
|
108
|
+
scope.used.add(listName);
|
|
109
|
+
else
|
|
110
|
+
scope.unknown.add(listName);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function scanConditions(root, scope) {
|
|
114
|
+
for (const element of Array.from(root.querySelectorAll(`[${DOCUMENT_CONDITION_ATTRIBUTE}]`))) {
|
|
115
|
+
const itemFields = listItemFields(scope.variables, enclosingEachName(conditionScopeElement(element)));
|
|
116
|
+
recordName(element.getAttribute(DOCUMENT_CONDITION_ATTRIBUTE) ?? "", itemFields, scope);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function scanReferences(root, scope) {
|
|
120
|
+
for (const element of Array.from(root.querySelectorAll("span[data-variable], div[data-steps], a[href], img[data-resource]"))) {
|
|
121
|
+
const itemFields = listItemFields(scope.variables, enclosingEachName(element));
|
|
122
|
+
const tag = element.tagName.toLowerCase();
|
|
123
|
+
const nameAttribute = NAME_ATTRIBUTE_BY_TAG[tag];
|
|
124
|
+
if (nameAttribute !== undefined) {
|
|
125
|
+
recordName(element.getAttribute(nameAttribute) ?? "", itemFields, scope);
|
|
126
|
+
}
|
|
127
|
+
else if (tag === "a") {
|
|
128
|
+
const href = element.getAttribute("href") ?? "";
|
|
129
|
+
if (isDocumentPlaceholderHref(href))
|
|
130
|
+
recordName(href.slice(2, -2), itemFields, scope);
|
|
111
131
|
}
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (nameAttribute !== undefined) {
|
|
117
|
-
recordName(element.getAttribute(nameAttribute) ?? "", itemFields, scope);
|
|
118
|
-
}
|
|
119
|
-
else if (tag === "a") {
|
|
120
|
-
const href = element.getAttribute("href") ?? "";
|
|
121
|
-
if (isDocumentPlaceholderHref(href))
|
|
122
|
-
recordName(href.slice(2, -2), itemFields, scope);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
const key = element.getAttribute("data-resource") ?? "";
|
|
126
|
-
if (!scope.resourceKeys.has(key))
|
|
127
|
-
scope.unknownResources.add(key);
|
|
128
|
-
}
|
|
132
|
+
else {
|
|
133
|
+
const key = element.getAttribute("data-resource") ?? "";
|
|
134
|
+
if (!scope.resourceKeys.has(key))
|
|
135
|
+
scope.unknownResources.add(key);
|
|
129
136
|
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function scanBodyPlaceholders(body, scope) {
|
|
140
|
+
withParsedDocument(body, (root) => {
|
|
141
|
+
scanRepeatRows(root, scope);
|
|
142
|
+
scanConditions(root, scope);
|
|
143
|
+
scanReferences(root, scope);
|
|
130
144
|
});
|
|
131
145
|
}
|
|
132
146
|
const HANDLEBARS_EXPRESSION = /\{\{([^{}]*)\}\}/g;
|
|
@@ -76,6 +76,7 @@ export const defaultTranslations = {
|
|
|
76
76
|
"templateResolver.errors.collection.kindNotAllowed": "“{detail}” templates are written through their own editor, not through a collection.",
|
|
77
77
|
"templateResolver.errors.noSystemTemplate": "There is no active system template to reset to.",
|
|
78
78
|
"templateResolver.errors.noDraft": "There is no draft to discard.",
|
|
79
|
+
"templateResolver.errors.systemOnlyTemplate": "Only the platform maintains this template; it cannot be overridden per tenant.",
|
|
79
80
|
"templateResolver.errors.template.invalidMeta": "The template settings are invalid.",
|
|
80
81
|
"templateResolver.errors.template.meta.subject.required": "The subject is missing.",
|
|
81
82
|
"templateResolver.errors.template.meta.subject.invalid": "The subject is invalid (at most 200 characters, no line breaks).",
|
|
@@ -12,7 +12,7 @@ export const templateResolverUserDataFeature = defineFeature("template-resolver-
|
|
|
12
12
|
// template-resolver is optional for the same reason as in
|
|
13
13
|
// notes-history-user-data: a toggleable mount would otherwise trip the
|
|
14
14
|
// "effectively disabled" boot warning even though the entity is there.
|
|
15
|
-
r.requires("user-data-rights");
|
|
15
|
+
r.requires({ features: ["user-data-rights"] });
|
|
16
16
|
r.optionalRequires("template-resolver");
|
|
17
17
|
r.useExtension(EXT_USER_DATA, "user-content-entry", {
|
|
18
18
|
export: userContentExportHook,
|
package/dist/tenant/feature.js
CHANGED
|
@@ -38,7 +38,7 @@ export function createTenantFeature(options) {
|
|
|
38
38
|
recommended: true,
|
|
39
39
|
});
|
|
40
40
|
r.systemScope();
|
|
41
|
-
r.requires("config");
|
|
41
|
+
r.requires({ features: ["config"] });
|
|
42
42
|
r.extendsRegistrar(EXT_ASSIGNABLE_ROLE, {});
|
|
43
43
|
// The elevation guards read the registry, the screens only these options: a role the
|
|
44
44
|
// server accepts but the screen omits is silently stripped on the next member-roles save.
|
|
@@ -49,9 +49,9 @@ export function createTenantFeature(options) {
|
|
|
49
49
|
throw new Error(`[tenant] assignable app role "${role}" (assignableFrom "${assignableFrom}") is declared but missing from the tenant screens' options; pass assignableAppRoles: collectAssignableAppRoles(appFeatures) to createTenantFeature.`);
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
-
r.entity("tenant", tenantEntity);
|
|
53
|
-
r.entity("tenant-membership", tenantMembershipEntity);
|
|
54
|
-
r.entity("tenant-invitation", tenantInvitationEntity);
|
|
52
|
+
r.entity({ name: "tenant", ...tenantEntity });
|
|
53
|
+
r.entity({ name: "tenant-membership", ...tenantMembershipEntity });
|
|
54
|
+
r.entity({ name: "tenant-invitation", ...tenantInvitationEntity });
|
|
55
55
|
r.config({
|
|
56
56
|
keys: {
|
|
57
57
|
// Stammdaten-Settings — Tenant-Admin darf ändern, alle dürfen lesen.
|
package/dist/tenant/seeding.js
CHANGED
|
@@ -185,7 +185,7 @@ export async function seedTenant(db, options, hooks) {
|
|
|
185
185
|
}
|
|
186
186
|
// "tenant:seed" matches no handler-scoped hook, only entity-scoped ones
|
|
187
187
|
// (keyed by result.entityName === "tenant") — that's exactly what
|
|
188
|
-
// tier-engine's `r.hook("postSave", { allOf: "tenant" }, ...)` needs.
|
|
188
|
+
// tier-engine's `r.hook({ type: "postSave", target: { allOf: "tenant" }, ... })` needs.
|
|
189
189
|
await fireEntityPostSave(hooks, "tenant:seed", result.data, { tenantId: options.id, db });
|
|
190
190
|
return { id: options.id };
|
|
191
191
|
}
|
|
@@ -19,7 +19,7 @@ export function createTenantLifecycleFeature() {
|
|
|
19
19
|
category: "compliance",
|
|
20
20
|
recommended: false,
|
|
21
21
|
});
|
|
22
|
-
r.requires("tenant", "compliance-profiles");
|
|
22
|
+
r.requires({ features: ["tenant", "compliance-profiles"] });
|
|
23
23
|
r.optionalRequires("sessions");
|
|
24
24
|
r.usesApi("compliance.forTenant");
|
|
25
25
|
r.extendsRegistrar(EXT_TENANT_DATA, {});
|
|
@@ -74,8 +74,7 @@ export function createTenantLifecycleFeature() {
|
|
|
74
74
|
if (!ctx.registry) {
|
|
75
75
|
throw new Error("run-tenant-destruction: ctx.registry required (JobContext incomplete)");
|
|
76
76
|
}
|
|
77
|
-
const
|
|
78
|
-
const now = T.Now.instant();
|
|
77
|
+
const now = ctx.tz.now();
|
|
79
78
|
// Without crossTenantReads (undeclared hatch) fall through to the sweep.
|
|
80
79
|
if (ctx.crossTenantReads && !(await hasTenantDestructionWork(ctx.crossTenantReads, now))) {
|
|
81
80
|
// skip: no tenant is due, so the sweep would not touch anything
|
|
@@ -16,7 +16,7 @@ import { buildTenantSettingsKeys } from "./config.js";
|
|
|
16
16
|
import { TENANT_SETTINGS_FEATURE_NAME } from "./constants.js";
|
|
17
17
|
export function createTenantSettingsFeature(opts = {}) {
|
|
18
18
|
return defineFeature(TENANT_SETTINGS_FEATURE_NAME, (r) => {
|
|
19
|
-
r.requires("config");
|
|
19
|
+
r.requires({ features: ["config"] });
|
|
20
20
|
r.describe("Per-tenant Currency + Locale defaults, exposed as two config keys (tenant-settings:config:currency, tenant-settings:config:locale) that surface in the self-populating Settings-Hub via `mask`. Pair with defineCreateWithTenantDefaults to auto-fill a money field's currency or a locale field on create instead of hard-coding a literal per entity.");
|
|
21
21
|
r.config({ keys: buildTenantSettingsKeys(opts) });
|
|
22
22
|
r.translations({
|