@cosmicdrift/kumiko-renderer-puppeteer-client 0.374.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.
@@ -4,11 +4,13 @@
4
4
  import { sanitizePdfAttachmentFileName } from "@cosmicdrift/kumiko-bundled-features/delivery";
5
5
  import { FileRefTooLargeError, readFileRefContent, } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
6
6
  import { RendererError, } from "@cosmicdrift/kumiko-bundled-features/renderer-foundation";
7
- import { collectDocumentResourceKeys, compileDocumentBodyToHandlebars, createTemplateResolverApi, DEFAULT_PDF_LAYOUT_SLUG, PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, prepareDocumentForPrint, readLayoutRegions, readPdfTemplateMeta, readTemplateResourceMaxBytes, resolveTemplateVariables, sanitizeDocumentHtml, sanitizeLayoutHtml, TemplateNotFoundError, toResourceDataUri, } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
7
+ import { collectDocumentImageVariables, collectDocumentResourceKeys, compileDocumentBodyToHandlebars, createTemplateResolverApi, DEFAULT_PDF_LAYOUT_SLUG, isResourceImageContentType, PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, prepareDocumentForPrint, readLayoutRegions, readPdfTemplateMeta, readTemplateResourceMaxBytes, resolveTemplateVariables, sanitizeDocumentHtml, sanitizeLayoutHtml, TemplateNotFoundError, toResourceDataUri, } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
8
8
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
9
9
  import { collectRegionResourceKeys, compileDocumentSource, createDocumentHandlebars, mergeResourceFileRefs, readBrandingAccentColor, readDocumentFormat, readTenantName, resolveResourceFileRefs, } from "@cosmicdrift/kumiko-renderer-mail-html";
10
10
  const LINE_BREAKS = /[\r\n]+/g;
11
11
  const IMAGE_CONTENT_TYPE_PREFIX = "image/";
12
+ const MAX_RECORD_IMAGES = 24;
13
+ const MAX_RECORD_IMAGES_TOTAL_BYTES = 20 * 1024 * 1024;
12
14
  const PREVIEW_PAGE_NUMBER = "1";
13
15
  const EMPTY_CHROMIUM_REGION = "<span></span>";
14
16
  const PAGE_NUMBER_HTML = '<span class="pageNumber"></span>';
@@ -26,6 +28,9 @@ const PRINT_CSS = [
26
28
  "[data-align=justify]{text-align:justify}",
27
29
  "[data-size=small]{font-size:8pt;color:#555}",
28
30
  "img{max-width:100%}",
31
+ ".kumiko-doc-images{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin:8px 0}",
32
+ ".kumiko-doc-images:empty{display:none}",
33
+ ".kumiko-doc-images img{width:100%;break-inside:avoid;page-break-inside:avoid}",
29
34
  ].join("");
30
35
  const EMPTY_REGIONS = { header: "", signature: "", footer: "" };
31
36
  function describe(e) {
@@ -129,6 +134,67 @@ async function loadResourceDataUris(ctx, fileRefIds) {
129
134
  }
130
135
  return dataUris;
131
136
  }
137
+ function fileRefIdsOf(value) {
138
+ if (typeof value === "string")
139
+ return [value];
140
+ return Array.isArray(value)
141
+ ? value.filter((entry) => typeof entry === "string")
142
+ : [];
143
+ }
144
+ // Own tenant only (no system files): the ids come from the app's record, so a foreign or
145
+ // unreadable id drops silently like a missing image instead of failing the document.
146
+ async function readRecordImage(ctx, filesForTenant, fileRefId, maxBytes) {
147
+ try {
148
+ const found = await readFileRefContent({
149
+ db: ctx.db,
150
+ filesForTenant,
151
+ fileRefId,
152
+ allowedTenantIds: [ctx.tenantId],
153
+ maxBytes,
154
+ });
155
+ if (found === null || !isResourceImageContentType(found.contentType))
156
+ return null;
157
+ return { dataUri: toResourceDataUri(found.contentType, found.content), size: found.size };
158
+ }
159
+ catch (e) {
160
+ if (e instanceof FileRefTooLargeError)
161
+ return null;
162
+ throw e;
163
+ }
164
+ }
165
+ async function loadRecordImageDataUris(ctx, variables, imageVariableNames) {
166
+ const dataUrisByVariable = new Map();
167
+ const { filesForTenant } = ctx;
168
+ if (filesForTenant === undefined || imageVariableNames.length === 0)
169
+ return dataUrisByVariable;
170
+ const maxBytes = await readTemplateResourceMaxBytes(ctx.config, ctx.registry);
171
+ const seenIds = new Set();
172
+ let loadedCount = 0;
173
+ let loadedBytes = 0;
174
+ for (const name of imageVariableNames) {
175
+ const dataUris = [];
176
+ for (const fileRefId of fileRefIdsOf(variables[name])) {
177
+ if (seenIds.has(fileRefId))
178
+ continue;
179
+ seenIds.add(fileRefId);
180
+ if (loadedCount >= MAX_RECORD_IMAGES)
181
+ break;
182
+ const image = await readRecordImage(ctx, filesForTenant, fileRefId, Math.min(maxBytes, MAX_RECORD_IMAGES_TOTAL_BYTES - loadedBytes));
183
+ if (image === null)
184
+ continue;
185
+ loadedCount += 1;
186
+ loadedBytes += image.size;
187
+ dataUris.push(image.dataUri);
188
+ }
189
+ dataUrisByVariable.set(name, dataUris);
190
+ }
191
+ return dataUrisByVariable;
192
+ }
193
+ // Text context without images variables: their value is file ids, which no placeholder,
194
+ // title or file name pattern may print.
195
+ function withoutImageVariables(variables, definitions) {
196
+ return Object.fromEntries(Object.entries(variables).filter(([name]) => !(Object.hasOwn(definitions, name) && definitions[name]?.type === "images")));
197
+ }
132
198
  function renderRegion(hbs, region, context, resourceSrc) {
133
199
  if (region.trim() === "")
134
200
  return "";
@@ -221,7 +287,7 @@ async function findOwnPdfTemplate(tplApi, ctx, slug, locale, scope) {
221
287
  ...(scope === "system" && { scope }),
222
288
  });
223
289
  }
224
- async function assembleDocument(input, ctx, markers) {
290
+ async function assembleDocument(input, ctx, markers, recordImages = "placeholder") {
225
291
  const { slug, locale, variables } = input;
226
292
  const tplApi = createTemplateResolverApi(ctx.db);
227
293
  const template = input.draft === undefined
@@ -237,7 +303,18 @@ async function assembleDocument(input, ctx, markers) {
237
303
  readBrandingAccentColor(ctx),
238
304
  ]);
239
305
  const hbs = createDocumentHandlebars(format);
240
- const context = { ...variables, tenantName, signature: "", imprint: "", page: "", pages: "" };
306
+ const variableDefinitions = resolveTemplateVariables(ctx.registry, slug, template.variableSchema);
307
+ const context = {
308
+ ...withoutImageVariables(variables, variableDefinitions),
309
+ tenantName,
310
+ signature: "",
311
+ imprint: "",
312
+ page: "",
313
+ pages: "",
314
+ };
315
+ const recordImageUris = recordImages === "resolve"
316
+ ? await loadRecordImageDataUris(ctx, variables, collectDocumentImageVariables(template.content))
317
+ : new Map();
241
318
  const bodyKeys = collectDocumentResourceKeys(template.content);
242
319
  const layoutKeys = collectRegionResourceKeys(layout.regions);
243
320
  const bodyRefs = await resolveResourceFileRefs(tplApi, ctx, template, bodyKeys);
@@ -247,7 +324,12 @@ async function assembleDocument(input, ctx, markers) {
247
324
  const dataUris = await loadResourceDataUris(ctx, mergeResourceFileRefs(bodyRefs, layoutRefs));
248
325
  const resourceSrc = (key) => dataUris.get(key);
249
326
  const missingResourceKeys = [...new Set([...bodyKeys, ...layoutKeys])].filter((key) => !dataUris.has(key));
250
- const bodySource = compileDocumentBodyToHandlebars(template.content, resolveTemplateVariables(ctx.registry, slug, template.variableSchema), { resourceSrc, ...(accentColor !== undefined && { stepsColor: accentColor }) });
327
+ const bodySource = compileDocumentBodyToHandlebars(template.content, variableDefinitions, {
328
+ resourceSrc,
329
+ recordImagesSrc: (variable) => recordImageUris.get(variable) ?? [],
330
+ recordImagesPlaceholder: recordImages === "placeholder",
331
+ ...(accentColor !== undefined && { stepsColor: accentColor }),
332
+ });
251
333
  const bodyHtml = prepareDocumentForPrint(compileDocumentSource(hbs, bodySource)(context));
252
334
  const signatureHtml = renderRegion(hbs, layout.regions.signature, context, resourceSrc);
253
335
  const pagedContext = markers === "chromium"
@@ -283,7 +365,7 @@ async function assembleDocument(input, ctx, markers) {
283
365
  }
284
366
  }
285
367
  export async function buildDocumentPdfRequest(req, ctx) {
286
- return toPdfRequest(await assembleDocument(req.payload, ctx, "chromium"), req.payload.locale);
368
+ return toPdfRequest(await assembleDocument(req.payload, ctx, "chromium", "resolve"), req.payload.locale);
287
369
  }
288
370
  // Prints an unsaved draft; the stored template of the slug only supplies images and the variable schema.
289
371
  export async function buildDocumentDraftPdfRequest(req, ctx) {
package/dist/entity.d.ts CHANGED
@@ -14,7 +14,7 @@ export declare const renderedPdfEntity: import("@cosmicdrift/kumiko-types/fields
14
14
  readonly sensitive?: boolean;
15
15
  readonly writeOnly?: boolean;
16
16
  readonly readAsInstruction?: boolean;
17
- readonly format?: "email" | "url" | "phone" | "password";
17
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
18
18
  readonly default?: string;
19
19
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
20
20
  readonly multiline?: boolean | {
@@ -37,7 +37,7 @@ export declare const renderedPdfEntity: import("@cosmicdrift/kumiko-types/fields
37
37
  readonly sensitive?: boolean;
38
38
  readonly writeOnly?: boolean;
39
39
  readonly readAsInstruction?: boolean;
40
- readonly format?: "email" | "url" | "phone" | "password";
40
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
41
41
  readonly default?: string;
42
42
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
43
43
  readonly multiline?: boolean | {
@@ -60,7 +60,7 @@ export declare const renderedPdfEntity: import("@cosmicdrift/kumiko-types/fields
60
60
  readonly sensitive?: boolean;
61
61
  readonly writeOnly?: boolean;
62
62
  readonly readAsInstruction?: boolean;
63
- readonly format?: "email" | "url" | "phone" | "password";
63
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
64
64
  readonly default?: string;
65
65
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
66
66
  readonly multiline?: boolean | {
@@ -83,7 +83,7 @@ export declare const renderedPdfEntity: import("@cosmicdrift/kumiko-types/fields
83
83
  readonly sensitive?: boolean;
84
84
  readonly writeOnly?: boolean;
85
85
  readonly readAsInstruction?: boolean;
86
- readonly format?: "email" | "url" | "phone" | "password";
86
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
87
87
  readonly default?: string;
88
88
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
89
89
  readonly multiline?: boolean | {
@@ -125,7 +125,7 @@ export declare const renderedPdfEntity: import("@cosmicdrift/kumiko-types/fields
125
125
  readonly sensitive?: boolean;
126
126
  readonly writeOnly?: boolean;
127
127
  readonly readAsInstruction?: boolean;
128
- readonly format?: "email" | "url" | "phone" | "password";
128
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
129
129
  readonly default?: string;
130
130
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
131
131
  readonly multiline?: boolean | {
@@ -148,7 +148,7 @@ export declare const renderedPdfEntity: import("@cosmicdrift/kumiko-types/fields
148
148
  readonly sensitive?: boolean;
149
149
  readonly writeOnly?: boolean;
150
150
  readonly readAsInstruction?: boolean;
151
- readonly format?: "email" | "url" | "phone" | "password";
151
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
152
152
  readonly default?: string;
153
153
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
154
154
  readonly multiline?: boolean | {
@@ -211,7 +211,7 @@ export declare const renderedPdfsTable: import("@cosmicdrift/kumiko-framework/db
211
211
  readonly sensitive?: boolean;
212
212
  readonly writeOnly?: boolean;
213
213
  readonly readAsInstruction?: boolean;
214
- readonly format?: "email" | "url" | "phone" | "password";
214
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
215
215
  readonly default?: string;
216
216
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
217
217
  readonly multiline?: boolean | {
@@ -234,7 +234,7 @@ export declare const renderedPdfsTable: import("@cosmicdrift/kumiko-framework/db
234
234
  readonly sensitive?: boolean;
235
235
  readonly writeOnly?: boolean;
236
236
  readonly readAsInstruction?: boolean;
237
- readonly format?: "email" | "url" | "phone" | "password";
237
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
238
238
  readonly default?: string;
239
239
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
240
240
  readonly multiline?: boolean | {
@@ -257,7 +257,7 @@ export declare const renderedPdfsTable: import("@cosmicdrift/kumiko-framework/db
257
257
  readonly sensitive?: boolean;
258
258
  readonly writeOnly?: boolean;
259
259
  readonly readAsInstruction?: boolean;
260
- readonly format?: "email" | "url" | "phone" | "password";
260
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
261
261
  readonly default?: string;
262
262
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
263
263
  readonly multiline?: boolean | {
@@ -280,7 +280,7 @@ export declare const renderedPdfsTable: import("@cosmicdrift/kumiko-framework/db
280
280
  readonly sensitive?: boolean;
281
281
  readonly writeOnly?: boolean;
282
282
  readonly readAsInstruction?: boolean;
283
- readonly format?: "email" | "url" | "phone" | "password";
283
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
284
284
  readonly default?: string;
285
285
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
286
286
  readonly multiline?: boolean | {
@@ -322,7 +322,7 @@ export declare const renderedPdfsTable: import("@cosmicdrift/kumiko-framework/db
322
322
  readonly sensitive?: boolean;
323
323
  readonly writeOnly?: boolean;
324
324
  readonly readAsInstruction?: boolean;
325
- readonly format?: "email" | "url" | "phone" | "password";
325
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
326
326
  readonly default?: string;
327
327
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
328
328
  readonly multiline?: boolean | {
@@ -345,7 +345,7 @@ export declare const renderedPdfsTable: import("@cosmicdrift/kumiko-framework/db
345
345
  readonly sensitive?: boolean;
346
346
  readonly writeOnly?: boolean;
347
347
  readonly readAsInstruction?: boolean;
348
- readonly format?: "email" | "url" | "phone" | "password";
348
+ readonly format?: "email" | "url" | "phone" | "password" | "month";
349
349
  readonly default?: string;
350
350
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
351
351
  readonly multiline?: boolean | {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer-puppeteer-client",
3
- "version": "0.374.0",
3
+ "version": "0.378.0",
4
4
  "description": "Renderer-Foundation plugin: HTTP client for puppeteer-service (PDF + image snapshot via remote browser).",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -19,13 +19,13 @@
19
19
  }
20
20
  },
21
21
  "dependencies": {
22
- "@cosmicdrift/kumiko-bundled-features": "0.374.0",
23
- "@cosmicdrift/kumiko-framework": "0.374.0",
24
- "@cosmicdrift/kumiko-renderer-mail-html": "0.374.0",
25
- "@cosmicdrift/kumiko-types": "0.374.0"
22
+ "@cosmicdrift/kumiko-bundled-features": "0.378.0",
23
+ "@cosmicdrift/kumiko-framework": "0.378.0",
24
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.378.0",
25
+ "@cosmicdrift/kumiko-types": "0.378.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@cosmicdrift/kumiko-testing": "0.374.0"
28
+ "@cosmicdrift/kumiko-testing": "0.378.0"
29
29
  },
30
30
  "repository": {
31
31
  "type": "git",