@cosmicdrift/kumiko-bundled-features 0.377.0 → 0.378.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.
Files changed (76) hide show
  1. package/dist/cap-counter/stock-cap-guard.js +8 -0
  2. package/dist/config/web/i18n.js +1 -0
  3. package/dist/inbound-mail-foundation/handlers/list-accounts.query.d.ts +1 -1
  4. package/dist/inbound-mail-foundation/handlers/list-accounts.query.js +11 -3
  5. package/dist/mail-foundation/i18n.d.ts +5 -2
  6. package/dist/mail-foundation/i18n.js +3 -2
  7. package/dist/mail-transport-smtp/i18n.d.ts +1 -1
  8. package/dist/mail-transport-smtp/i18n.js +1 -1
  9. package/dist/template-resolver/compile-document-body.d.ts +3 -0
  10. package/dist/template-resolver/compile-document-body.js +50 -3
  11. package/dist/template-resolver/constants.d.ts +3 -0
  12. package/dist/template-resolver/constants.js +2 -0
  13. package/dist/template-resolver/document-blocks.d.ts +2 -2
  14. package/dist/template-resolver/document-blocks.js +1 -0
  15. package/dist/template-resolver/document-images.d.ts +2 -0
  16. package/dist/template-resolver/document-images.js +28 -0
  17. package/dist/template-resolver/document-sanitizer.d.ts +1 -1
  18. package/dist/template-resolver/document-sanitizer.js +20 -0
  19. package/dist/template-resolver/document-schema.d.ts +2 -1
  20. package/dist/template-resolver/document-schema.js +4 -0
  21. package/dist/template-resolver/feature.d.ts +9 -0
  22. package/dist/template-resolver/feature.js +58 -33
  23. package/dist/template-resolver/handlers/collection-item.query.js +2 -1
  24. package/dist/template-resolver/handlers/collection-list.query.js +2 -1
  25. package/dist/template-resolver/handlers/collection-set.write.js +2 -2
  26. package/dist/template-resolver/handlers/collection-shared.d.ts +1 -2
  27. package/dist/template-resolver/handlers/collection-shared.js +0 -4
  28. package/dist/template-resolver/handlers/copy-locale.write.d.ts +2 -1
  29. package/dist/template-resolver/handlers/copy-locale.write.js +67 -63
  30. package/dist/template-resolver/handlers/discard-draft.write.d.ts +3 -2
  31. package/dist/template-resolver/handlers/discard-draft.write.js +18 -6
  32. package/dist/template-resolver/handlers/import-bundle.write.d.ts +2 -1
  33. package/dist/template-resolver/handlers/import-bundle.write.js +30 -25
  34. package/dist/template-resolver/handlers/overview.query.d.ts +2 -1
  35. package/dist/template-resolver/handlers/overview.query.js +46 -42
  36. package/dist/template-resolver/handlers/patch-draft.write.d.ts +2 -1
  37. package/dist/template-resolver/handlers/patch-draft.write.js +92 -86
  38. package/dist/template-resolver/handlers/reset-to-system.write.d.ts +2 -1
  39. package/dist/template-resolver/handlers/reset-to-system.write.js +51 -44
  40. package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +2 -1
  41. package/dist/template-resolver/handlers/resource-link-pdf.write.js +34 -28
  42. package/dist/template-resolver/handlers/resource-remove.write.d.ts +2 -1
  43. package/dist/template-resolver/handlers/resource-remove.write.js +39 -33
  44. package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -1
  45. package/dist/template-resolver/handlers/resource-upload.write.js +104 -96
  46. package/dist/template-resolver/handlers/send-test-shared.d.ts +5 -0
  47. package/dist/template-resolver/handlers/send-test-shared.js +19 -0
  48. package/dist/template-resolver/handlers/send-test-status.query.d.ts +9 -0
  49. package/dist/template-resolver/handlers/send-test-status.query.js +18 -0
  50. package/dist/template-resolver/handlers/send-test.write.d.ts +2 -1
  51. package/dist/template-resolver/handlers/send-test.write.js +87 -89
  52. package/dist/template-resolver/handlers/set.write.d.ts +2 -1
  53. package/dist/template-resolver/handlers/set.write.js +94 -90
  54. package/dist/template-resolver/handlers/toggle-status.write.d.ts +6 -7
  55. package/dist/template-resolver/handlers/toggle-status.write.js +28 -10
  56. package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +2 -1
  57. package/dist/template-resolver/handlers/upsert-tenant.write.js +30 -24
  58. package/dist/template-resolver/index.d.ts +4 -2
  59. package/dist/template-resolver/index.js +3 -1
  60. package/dist/template-resolver/qualified-names.d.ts +2 -0
  61. package/dist/template-resolver/qualified-names.js +2 -0
  62. package/dist/template-resolver/template-edit-policy.d.ts +22 -0
  63. package/dist/template-resolver/template-edit-policy.js +45 -0
  64. package/dist/template-resolver/template-preview.js +7 -2
  65. package/dist/template-resolver/template-validation.js +33 -13
  66. package/dist/template-resolver/web/document-editor-extensions.d.ts +1 -0
  67. package/dist/template-resolver/web/document-editor-extensions.js +21 -1
  68. package/dist/template-resolver/web/document-tiptap-editor.d.ts +1 -1
  69. package/dist/template-resolver/web/document-tiptap-editor.js +22 -6
  70. package/dist/template-resolver/web/document-toolbar-blocks.d.ts +2 -0
  71. package/dist/template-resolver/web/i18n.js +4 -0
  72. package/package.json +8 -8
  73. package/src/cap-counter/changes.json +12 -0
  74. package/src/inbound-mail-foundation/changes.json +6 -0
  75. package/src/mail-foundation/changes.json +6 -0
  76. package/src/template-resolver/changes.json +20 -0
@@ -3,9 +3,9 @@ import { defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
4
  import * as z from "zod";
5
5
  import { requireForTenant } from "../../shared/index.js";
6
- import { isCollectionRefusedKind } from "../constants.js";
6
+ import { DEFAULT_COLLECTION_ACCESS, isCollectionRefusedKind } from "../constants.js";
7
7
  import { documentViolationsToFieldIssues, sanitizeTemplateBody } from "../document-sanitizer.js";
8
- import { collectionStore, DEFAULT_COLLECTION_ACCESS, } from "./collection-shared.js";
8
+ import { collectionStore } from "./collection-shared.js";
9
9
  import { contentFormatSchema, folderSchema, localeSchema, slugSchema } from "./shared.js";
10
10
  // Upsert inside one collection. Mirrors set.write for text-blocks, but the
11
11
  // kind comes from the declaration and the access rule is the collection's own,
@@ -1,9 +1,8 @@
1
1
  import type { EventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
- import type { AccessRule, ContentCollectionDefinition, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { ContentCollectionDefinition, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
4
  import { templateResourcesTable } from "../table.ts";
5
5
  import { userContentEntriesTable } from "../user-content-table.ts";
6
- export declare const DEFAULT_COLLECTION_ACCESS: AccessRule;
7
6
  export type CollectionEntryRow = {
8
7
  readonly id: string | number;
9
8
  readonly version: number;
@@ -2,10 +2,6 @@ import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import { templateResourceEntity, templateResourcesTable } from "../table.js";
4
4
  import { userContentEntriesTable, userContentEntryEntity } from "../user-content-table.js";
5
- // Applies when an app mounts a collection without saying who may reach it.
6
- // Deliberately narrow: a collection whose access nobody decided should be
7
- // invisible to normal users rather than open by default.
8
- export const DEFAULT_COLLECTION_ACCESS = { roles: ["TenantAdmin", "SystemAdmin"] };
9
5
  const templateExecutor = createEventStoreExecutor(templateResourcesTable, templateResourceEntity, {
10
6
  entityName: "template-resource",
11
7
  });
@@ -1,5 +1,6 @@
1
1
  import * as z from "zod";
2
- export declare const copyLocaleWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"copy-locale", z.ZodObject<{
2
+ import { type TemplateEditPolicy } from "../template-edit-policy.ts";
3
+ export declare function makeCopyLocaleWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"copy-locale", z.ZodObject<{
3
4
  slug: z.ZodString;
4
5
  kind: z.ZodEnum<{
5
6
  "ai-prompt": "ai-prompt";
@@ -10,6 +10,7 @@ import { TemplateResolverErrors } from "../qualified-names.js";
10
10
  import { createLinkedResourceCopies, prepareLinkedResourceCopiesBeforeTransaction, } from "../store-template-resource.js";
11
11
  import { templateResourcesTable } from "../table.js";
12
12
  import { toWorkingRow } from "../template-draft.js";
13
+ import { refuseTemplateEdit, templateEditAccess, } from "../template-edit-policy.js";
13
14
  import { attachTemplateResources } from "../template-resources.js";
14
15
  import { resolveTemplateVariables } from "../template-validation.js";
15
16
  import { targetExistsIssue } from "./bundle-import.js";
@@ -50,7 +51,7 @@ const copyLocaleSchema = z.object({
50
51
  scope: z.enum(["tenant", "system"]).default("tenant"),
51
52
  });
52
53
  // Runs before the upload (reject early, store nothing) and again in the transaction on the fresh rows.
53
- async function checkCopyRequest(ctx, event) {
54
+ async function checkCopyRequest(ctx, policy, event) {
54
55
  const { slug, kind, fromLocale, toLocale, overwrite, scope } = event.payload;
55
56
  if (scope === "system" && !event.user.roles.includes("SystemAdmin")) {
56
57
  return writeFailure(new AccessDeniedError({
@@ -59,7 +60,8 @@ async function checkCopyRequest(ctx, event) {
59
60
  }));
60
61
  }
61
62
  const tenantId = scope === "system" ? SYSTEM_TENANT_ID : event.user.tenantId;
62
- const refused = refuseTenantScopeUnderSystemTenant(scope, tenantId) ??
63
+ const refused = (scope === "tenant" ? refuseTemplateEdit(policy, event.user, { slug, kind }) : null) ??
64
+ refuseTenantScopeUnderSystemTenant(scope, tenantId) ??
63
65
  refuseSystemOnlyTenantOverride(ctx.registry, scope, slug, kind);
64
66
  if (refused !== null)
65
67
  return refused;
@@ -99,65 +101,67 @@ async function checkCopyRequest(ctx, event) {
99
101
  },
100
102
  };
101
103
  }
102
- export const copyLocaleWrite = defineWriteHandler({
103
- name: "copy-locale",
104
- schema: copyLocaleSchema,
105
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
106
- description: "Copies the text of one locale of a template into another locale as a draft override of the caller's tenant, taking the tenant's own row or else the system default as the source; use it as the starting point for a translation. It refuses an existing target unless overwrite is set. With scope system (SystemAdmin only) both source and target are system-default rows and the target lands as a system draft; any other caller gets a 403.",
107
- beforeTransaction: async (event, ctx) => {
108
- const request = await checkCopyRequest(ctx, event);
109
- if (!request.isSuccess)
110
- return request;
111
- if (!request.data.sourceIsForeign)
112
- return { isSuccess: true, data: [] };
113
- return prepareLinkedResourceCopiesBeforeTransaction({
114
- ctx,
115
- user: event.user,
116
- tenantId: request.data.tenantId,
117
- resources: request.data.sourceResources,
118
- });
119
- },
120
- handler: async (event, ctx) => {
121
- const { slug, kind, toLocale } = event.payload;
122
- const request = await checkCopyRequest(ctx, event);
123
- if (!request.isSuccess)
124
- return request;
125
- const { scope, tenantId, source, contentFormat, sourceResources, sourceIsForeign } = request.data;
126
- const variableSchema = storedRecord(source.variableSchema);
127
- const written = await writeTemplateRow({
128
- ctx,
129
- user: event.user,
130
- tenantId,
131
- scope,
132
- status: "draft",
133
- variables: resolveTemplateVariables(ctx.registry, slug, variableSchema),
134
- payload: {
135
- slug,
136
- kind,
137
- locale: toLocale,
138
- content: source.content,
139
- contentFormat,
140
- variableSchema,
141
- meta: parseStoredJson(source.meta),
142
- title: source.title,
143
- folder: source.folder,
144
- },
145
- });
146
- if (!written.isSuccess)
104
+ export function makeCopyLocaleWrite(policy) {
105
+ return defineWriteHandler({
106
+ name: "copy-locale",
107
+ schema: copyLocaleSchema,
108
+ access: templateEditAccess(policy.tenantEditable),
109
+ description: "Copies the text of one locale of a template into another locale as a draft override of the caller's tenant, taking the tenant's own row or else the system default as the source; use it as the starting point for a translation. It refuses an existing target unless overwrite is set. With scope system (SystemAdmin only) both source and target are system-default rows and the target lands as a system draft; any other caller gets a 403. A TenantAdmin may copy only templates the app lists in tenantEditable, otherwise SystemAdmin only.",
110
+ beforeTransaction: async (event, ctx) => {
111
+ const request = await checkCopyRequest(ctx, policy, event);
112
+ if (!request.isSuccess)
113
+ return request;
114
+ if (!request.data.sourceIsForeign)
115
+ return { isSuccess: true, data: [] };
116
+ return prepareLinkedResourceCopiesBeforeTransaction({
117
+ ctx,
118
+ user: event.user,
119
+ tenantId: request.data.tenantId,
120
+ resources: request.data.sourceResources,
121
+ });
122
+ },
123
+ handler: async (event, ctx) => {
124
+ const { slug, kind, toLocale } = event.payload;
125
+ const request = await checkCopyRequest(ctx, policy, event);
126
+ if (!request.isSuccess)
127
+ return request;
128
+ const { scope, tenantId, source, contentFormat, sourceResources, sourceIsForeign } = request.data;
129
+ const variableSchema = storedRecord(source.variableSchema);
130
+ const written = await writeTemplateRow({
131
+ ctx,
132
+ user: event.user,
133
+ tenantId,
134
+ scope,
135
+ status: "draft",
136
+ variables: resolveTemplateVariables(ctx.registry, slug, variableSchema),
137
+ payload: {
138
+ slug,
139
+ kind,
140
+ locale: toLocale,
141
+ content: source.content,
142
+ contentFormat,
143
+ variableSchema,
144
+ meta: parseStoredJson(source.meta),
145
+ title: source.title,
146
+ folder: source.folder,
147
+ },
148
+ });
149
+ if (!written.isSuccess)
150
+ return written;
151
+ const resources = sourceIsForeign
152
+ ? await createLinkedResourceCopies({ ctx, user: event.user, tenantId }, event.prepared)
153
+ : { isSuccess: true, data: sourceResources };
154
+ if (!resources.isSuccess)
155
+ return resources;
156
+ const attached = await attachTemplateResources(ctx, {
157
+ user: event.user,
158
+ tenantId,
159
+ templateId: written.data.id,
160
+ resources: resources.data,
161
+ });
162
+ if (!attached.isSuccess)
163
+ return attached;
147
164
  return written;
148
- const resources = sourceIsForeign
149
- ? await createLinkedResourceCopies({ ctx, user: event.user, tenantId }, event.prepared)
150
- : { isSuccess: true, data: sourceResources };
151
- if (!resources.isSuccess)
152
- return resources;
153
- const attached = await attachTemplateResources(ctx, {
154
- user: event.user,
155
- tenantId,
156
- templateId: written.data.id,
157
- resources: resources.data,
158
- });
159
- if (!attached.isSuccess)
160
- return attached;
161
- return written;
162
- },
163
- });
165
+ },
166
+ });
167
+ }
@@ -1,11 +1,12 @@
1
1
  import * as z from "zod";
2
- export declare const discardDraftWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
2
+ import { type TemplateEditPolicy } from "../template-edit-policy.ts";
3
+ export declare function makeDiscardDraftWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
3
4
  id: z.ZodString;
4
5
  }, z.core.$strip>, {
5
6
  id: string;
6
7
  discarded: boolean;
7
8
  }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
8
- export declare const discardDraftSystemWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
9
+ export declare function makeDiscardDraftSystemWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<string, z.ZodObject<{
9
10
  id: z.ZodString;
10
11
  }, z.core.$strip>, {
11
12
  id: string;
@@ -2,19 +2,27 @@ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { NotFoundError, ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
3
  import * as z from "zod";
4
4
  import { TemplateResolverErrors } from "../qualified-names.js";
5
+ import { refuseMissingTemplate, refuseTemplateEdit, } from "../template-edit-policy.js";
5
6
  import { executor } from "./shared.js";
6
7
  import { loadTargetRow, writeAccessFor } from "./toggle-status.write.js";
7
- function createDiscardDraftHandler(name, target, description) {
8
+ function createDiscardDraftHandler(policy, name, target, description) {
8
9
  return defineWriteHandler({
9
10
  name,
10
11
  schema: z.object({ id: z.string().min(1) }),
11
- access: writeAccessFor(target),
12
+ access: writeAccessFor(target, policy),
12
13
  ...(target === "system" ? { agent: { risk: "high" } } : {}),
13
14
  description,
14
15
  handler: async (event, ctx) => {
15
16
  const { db, scoped, row } = await loadTargetRow(ctx, target, event.user, event.payload.id);
16
- if (!row)
17
- return writeFailure(new NotFoundError("template-resource", event.payload.id));
17
+ if (!row) {
18
+ const refused = target === "own" ? refuseMissingTemplate(policy, event.user) : null;
19
+ return refused ?? writeFailure(new NotFoundError("template-resource", event.payload.id));
20
+ }
21
+ if (target === "own") {
22
+ const refused = refuseTemplateEdit(policy, event.user, row);
23
+ if (refused !== null)
24
+ return refused;
25
+ }
18
26
  if (row.draft === null) {
19
27
  return writeFailure(new ValidationError({
20
28
  fields: [
@@ -33,5 +41,9 @@ function createDiscardDraftHandler(name, target, description) {
33
41
  },
34
42
  });
35
43
  }
36
- export const discardDraftWrite = createDiscardDraftHandler("discard-draft", "own", "Throws away the open draft of one template resource of the caller's tenant so the editor shows the active version again; the served version is untouched. Refused with no_draft when there is no draft.");
37
- export const discardDraftSystemWrite = createDiscardDraftHandler("discard-draft-system", "system", "Throws away the open draft of one system-default template; the served version stays. SystemAdmin only, refused with no_draft when there is no draft.");
44
+ export function makeDiscardDraftWrite(policy) {
45
+ return createDiscardDraftHandler(policy, "discard-draft", "own", "Throws away the open draft of one template resource of the caller's tenant so the editor shows the active version again; the served version is untouched. Refused with no_draft when there is no draft. Outside the templates the app lists in tenantEditable only a SystemAdmin may use it.");
46
+ }
47
+ export function makeDiscardDraftSystemWrite(policy) {
48
+ return createDiscardDraftHandler(policy, "discard-draft-system", "system", "Throws away the open draft of one system-default template; the served version stays. SystemAdmin only, refused with no_draft when there is no draft.");
49
+ }
@@ -1,5 +1,6 @@
1
1
  import * as z from "zod";
2
- export declare const importBundleWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"import-bundle", z.ZodObject<{
2
+ import { type TemplateEditPolicy } from "../template-edit-policy.ts";
3
+ export declare function makeImportBundleWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"import-bundle", z.ZodObject<{
3
4
  bundle: z.ZodObject<{
4
5
  slug: z.ZodString;
5
6
  kind: z.ZodEnum<{
@@ -1,29 +1,34 @@
1
1
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import * as z from "zod";
3
3
  import { templateBundleSchema } from "../template-bundle.js";
4
+ import { refuseTemplateEdit, templateEditAccess, } from "../template-edit-policy.js";
4
5
  import { importTemplateBundle, prepareTemplateBundleBeforeTransaction } from "./bundle-import.js";
5
- export const importBundleWrite = defineWriteHandler({
6
- name: "import-bundle",
7
- schema: z.object({ bundle: templateBundleSchema, overwrite: z.boolean().default(false) }),
8
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
9
- description: "Imports a template bundle (all locales plus resource files, as produced by export-bundle) into the caller's tenant as drafts; it refuses a locale that already has an override unless overwrite is set, and publish makes the drafts live.",
10
- beforeTransaction: (event, ctx) => prepareTemplateBundleBeforeTransaction({
11
- ctx,
12
- user: event.user,
13
- tenantId: event.user.tenantId,
14
- scope: "tenant",
15
- status: "draft",
16
- bundle: event.payload.bundle,
17
- overwrite: event.payload.overwrite,
18
- }),
19
- handler: (event, ctx) => importTemplateBundle({
20
- ctx,
21
- user: event.user,
22
- tenantId: event.user.tenantId,
23
- scope: "tenant",
24
- status: "draft",
25
- bundle: event.payload.bundle,
26
- overwrite: event.payload.overwrite,
27
- prepared: event.prepared,
28
- }),
29
- });
6
+ export function makeImportBundleWrite(policy) {
7
+ return defineWriteHandler({
8
+ name: "import-bundle",
9
+ schema: z.object({ bundle: templateBundleSchema, overwrite: z.boolean().default(false) }),
10
+ access: templateEditAccess(policy.tenantEditable),
11
+ description: "Imports a template bundle (all locales plus resource files, as produced by export-bundle) into the caller's tenant as drafts; it refuses a locale that already has an override unless overwrite is set, and publish makes the drafts live. Outside the templates the app lists in tenantEditable only a SystemAdmin may use it.",
12
+ beforeTransaction: async (event, ctx) => refuseTemplateEdit(policy, event.user, event.payload.bundle) ??
13
+ prepareTemplateBundleBeforeTransaction({
14
+ ctx,
15
+ user: event.user,
16
+ tenantId: event.user.tenantId,
17
+ scope: "tenant",
18
+ status: "draft",
19
+ bundle: event.payload.bundle,
20
+ overwrite: event.payload.overwrite,
21
+ }),
22
+ handler: async (event, ctx) => refuseTemplateEdit(policy, event.user, event.payload.bundle) ??
23
+ importTemplateBundle({
24
+ ctx,
25
+ user: event.user,
26
+ tenantId: event.user.tenantId,
27
+ scope: "tenant",
28
+ status: "draft",
29
+ bundle: event.payload.bundle,
30
+ overwrite: event.payload.overwrite,
31
+ prepared: event.prepared,
32
+ }),
33
+ });
34
+ }
@@ -1,6 +1,7 @@
1
1
  import * as z from "zod";
2
2
  import { type TemplateStatus } from "../constants.ts";
3
3
  import { type TemplateResourceRow } from "../table.ts";
4
+ import { type TemplateEditPolicy } from "../template-edit-policy.ts";
4
5
  import { type EffectiveMailLayoutSlugs } from "../template-usage.ts";
5
6
  declare const OVERVIEW_KINDS: readonly ["mail", "document-pdf", "mail-layout", "pdf-layout"];
6
7
  type OverviewKind = (typeof OVERVIEW_KINDS)[number];
@@ -38,7 +39,7 @@ export type OverviewResponse = {
38
39
  readonly truncated: boolean;
39
40
  };
40
41
  export declare function effectiveMailLayoutSlugs(rows: readonly TemplateResourceRow[]): EffectiveMailLayoutSlugs;
41
- export declare const overviewQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"overview", z.ZodObject<{
42
+ export declare function makeOverviewQuery(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"overview", z.ZodObject<{
42
43
  search: z.ZodOptional<z.ZodString>;
43
44
  filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
44
45
  field: z.ZodEnum<{
@@ -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 { mayEditTemplate } from "../template-edit-policy.js";
7
8
  import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.js";
8
9
  import { collectTemplateUsage } from "../template-usage.js";
9
10
  import { storedRecord } from "./template-gates.js";
@@ -171,45 +172,48 @@ const filterSchema = z.object({
171
172
  op: z.literal("in"),
172
173
  value: z.array(z.string()),
173
174
  });
174
- export const overviewQuery = defineQueryHandler({
175
- name: "overview",
176
- schema: z.object({
177
- search: z.string().optional(),
178
- filters: z.array(filterSchema).optional(),
179
- layoutSlug: z.string().optional(),
180
- sort: z.string().optional(),
181
- sortDirection: z.enum(["asc", "desc"]).optional(),
182
- limit: z.number().int().min(1).max(200).default(50),
183
- offset: z.number().int().min(0).optional(),
184
- totalCount: z.boolean().optional(),
185
- }),
186
- access: { roles: ["TenantAdmin", "SystemAdmin"] },
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.",
188
- handler: async (query, ctx) => {
189
- const payload = query.payload;
190
- const fetchedRows = await selectMany(ctx.db, templateResourcesTable, { kind: { in: [...OVERVIEW_KINDS] } }, { orderBy: { col: "id", direction: "asc" }, limit: OVERVIEW_ROW_LIMIT + 1 });
191
- const truncated = fetchedRows.length > OVERVIEW_ROW_LIMIT;
192
- const rows = truncated ? fetchedRows.slice(0, OVERVIEW_ROW_LIMIT) : fetchedRows;
193
- if (truncated) {
194
- ctx.log?.warn("template overview: row limit reached, result is incomplete", {
195
- limit: OVERVIEW_ROW_LIMIT,
196
- });
197
- }
198
- const visibleRows = rows.filter((row) => !isHiddenSystemDraft(query.user, row) &&
199
- !isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, row));
200
- const { drafts, layoutSlugByDocument } = buildDrafts(groupBySlugAndKind(visibleRows), query.user);
201
- const effectiveLayouts = effectiveMailLayoutSlugs(visibleRows);
202
- const overviewRows = drafts.map((draft) => withUsage(draft, layoutSlugByDocument, ctx.registry, effectiveLayouts));
203
- const filtered = filterRows(overviewRows, payload.search, payload.filters ?? [], payload.layoutSlug);
204
- const sortField = payload.sort !== undefined && isSortField(payload.sort) ? payload.sort : "modifiedAt";
205
- const direction = (payload.sortDirection ?? "desc") === "desc" ? -1 : 1;
206
- filtered.sort((a, b) => direction * compareBy(sortField, a, b) || a.id.localeCompare(b.id));
207
- const offset = payload.offset ?? 0;
208
- return {
209
- rows: filtered.slice(offset, offset + payload.limit),
210
- nextCursor: null,
211
- total: filtered.length,
212
- truncated,
213
- };
214
- },
215
- });
175
+ export function makeOverviewQuery(policy) {
176
+ return defineQueryHandler({
177
+ name: "overview",
178
+ schema: z.object({
179
+ search: z.string().optional(),
180
+ filters: z.array(filterSchema).optional(),
181
+ layoutSlug: z.string().optional(),
182
+ sort: z.string().optional(),
183
+ sortDirection: z.enum(["asc", "desc"]).optional(),
184
+ limit: z.number().int().min(1).max(200).default(50),
185
+ offset: z.number().int().min(0).optional(),
186
+ totalCount: z.boolean().optional(),
187
+ }),
188
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
189
+ 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, and a TenantAdmin sees only the templates the app lists in tenantEditable; use it to browse and filter all templates at a glance.",
190
+ handler: async (query, ctx) => {
191
+ const payload = query.payload;
192
+ const fetchedRows = await selectMany(ctx.db, templateResourcesTable, { kind: { in: [...OVERVIEW_KINDS] } }, { orderBy: { col: "id", direction: "asc" }, limit: OVERVIEW_ROW_LIMIT + 1 });
193
+ const truncated = fetchedRows.length > OVERVIEW_ROW_LIMIT;
194
+ const rows = truncated ? fetchedRows.slice(0, OVERVIEW_ROW_LIMIT) : fetchedRows;
195
+ if (truncated) {
196
+ ctx.log?.warn("template overview: row limit reached, result is incomplete", {
197
+ limit: OVERVIEW_ROW_LIMIT,
198
+ });
199
+ }
200
+ const visibleRows = rows.filter((row) => !isHiddenSystemDraft(query.user, row) &&
201
+ !isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, row) &&
202
+ mayEditTemplate(policy, query.user, row));
203
+ const { drafts, layoutSlugByDocument } = buildDrafts(groupBySlugAndKind(visibleRows), query.user);
204
+ const effectiveLayouts = effectiveMailLayoutSlugs(visibleRows);
205
+ const overviewRows = drafts.map((draft) => withUsage(draft, layoutSlugByDocument, ctx.registry, effectiveLayouts));
206
+ const filtered = filterRows(overviewRows, payload.search, payload.filters ?? [], payload.layoutSlug);
207
+ const sortField = payload.sort !== undefined && isSortField(payload.sort) ? payload.sort : "modifiedAt";
208
+ const direction = (payload.sortDirection ?? "desc") === "desc" ? -1 : 1;
209
+ filtered.sort((a, b) => direction * compareBy(sortField, a, b) || a.id.localeCompare(b.id));
210
+ const offset = payload.offset ?? 0;
211
+ return {
212
+ rows: filtered.slice(offset, offset + payload.limit),
213
+ nextCursor: null,
214
+ total: filtered.length,
215
+ truncated,
216
+ };
217
+ },
218
+ });
219
+ }
@@ -1,5 +1,6 @@
1
1
  import * as z from "zod";
2
- export declare const patchDraftWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"patch-draft", z.ZodObject<{
2
+ import { type TemplateEditPolicy } from "../template-edit-policy.ts";
3
+ export declare function makePatchDraftWrite(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"patch-draft", z.ZodObject<{
3
4
  slug: z.ZodString;
4
5
  kind: z.ZodEnum<{
5
6
  "document-pdf": "document-pdf";