@momentumcms/server-analog 0.5.1 → 0.5.3

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 (3) hide show
  1. package/index.cjs +485 -62
  2. package/index.js +484 -62
  3. package/package.json +7 -2
package/index.cjs CHANGED
@@ -531,6 +531,383 @@ var init_src = __esm({
531
531
  }
532
532
  });
533
533
 
534
+ // libs/email/src/lib/utils/escape-html.ts
535
+ function escapeHtml2(unsafe) {
536
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
537
+ }
538
+ var init_escape_html = __esm({
539
+ "libs/email/src/lib/utils/escape-html.ts"() {
540
+ "use strict";
541
+ }
542
+ });
543
+
544
+ // libs/email/src/lib/utils/css-inliner.ts
545
+ function inlineCss(html) {
546
+ return (0, import_juice.default)(html, {
547
+ removeStyleTags: true,
548
+ preserveMediaQueries: true,
549
+ preserveFontFaces: true,
550
+ insertPreservedExtraCss: true
551
+ });
552
+ }
553
+ var import_juice;
554
+ var init_css_inliner = __esm({
555
+ "libs/email/src/lib/utils/css-inliner.ts"() {
556
+ "use strict";
557
+ import_juice = __toESM(require("juice"));
558
+ }
559
+ });
560
+
561
+ // libs/email/src/lib/utils/replace-variables.ts
562
+ function replaceVariables(text2, variables) {
563
+ return text2.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? "");
564
+ }
565
+ function replaceBlockVariables(blocks2, variables) {
566
+ return blocks2.map((block) => ({
567
+ ...block,
568
+ data: replaceDataVariables(block.data, variables)
569
+ }));
570
+ }
571
+ function replaceDataVariables(data, variables) {
572
+ const result = {};
573
+ for (const [key, value] of Object.entries(data)) {
574
+ if (typeof value === "string") {
575
+ result[key] = replaceVariables(value, variables);
576
+ } else if (Array.isArray(value)) {
577
+ result[key] = value.map((item) => {
578
+ if (typeof item === "object" && item !== null && "blocks" in item) {
579
+ const col = item;
580
+ const nestedBlocks = Array.isArray(col["blocks"]) ? col["blocks"] : [];
581
+ return { ...col, blocks: replaceBlockVariables(nestedBlocks, variables) };
582
+ }
583
+ return item;
584
+ });
585
+ } else {
586
+ result[key] = value;
587
+ }
588
+ }
589
+ return result;
590
+ }
591
+ var init_replace_variables = __esm({
592
+ "libs/email/src/lib/utils/replace-variables.ts"() {
593
+ "use strict";
594
+ }
595
+ });
596
+
597
+ // libs/email/src/lib/utils/sanitize.ts
598
+ function sanitizeAlignment(value) {
599
+ return VALID_ALIGNMENTS.has(value) ? value : "left";
600
+ }
601
+ function sanitizeCssValue(value) {
602
+ return value.replace(/[;{}()"'<>\\]/g, "");
603
+ }
604
+ function sanitizeFontFamily(value) {
605
+ return value.replace(/[;{}()"<>\\]/g, "");
606
+ }
607
+ function sanitizeCssNumber(value, fallback) {
608
+ if (value === null || value === void 0)
609
+ return String(fallback);
610
+ const num = Number(value);
611
+ return Number.isFinite(num) && num >= 0 ? String(num) : String(fallback);
612
+ }
613
+ function sanitizeUrl(url) {
614
+ const trimmed = url.trim();
615
+ if (!trimmed || trimmed === "#")
616
+ return trimmed || "#";
617
+ try {
618
+ const parsed = new URL(trimmed);
619
+ return SAFE_URL_PROTOCOLS.has(parsed.protocol) ? trimmed : "#";
620
+ } catch {
621
+ if (trimmed.startsWith("/") || trimmed.startsWith("#"))
622
+ return trimmed;
623
+ return "#";
624
+ }
625
+ }
626
+ var VALID_ALIGNMENTS, SAFE_URL_PROTOCOLS;
627
+ var init_sanitize = __esm({
628
+ "libs/email/src/lib/utils/sanitize.ts"() {
629
+ "use strict";
630
+ VALID_ALIGNMENTS = /* @__PURE__ */ new Set(["left", "center", "right"]);
631
+ SAFE_URL_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
632
+ }
633
+ });
634
+
635
+ // libs/email/src/types.ts
636
+ var DEFAULT_EMAIL_THEME;
637
+ var init_types = __esm({
638
+ "libs/email/src/types.ts"() {
639
+ "use strict";
640
+ DEFAULT_EMAIL_THEME = {
641
+ primaryColor: "#18181b",
642
+ backgroundColor: "#f4f4f5",
643
+ textColor: "#3f3f46",
644
+ mutedColor: "#71717a",
645
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
646
+ borderRadius: "8px"
647
+ };
648
+ }
649
+ });
650
+
651
+ // libs/email/src/lib/render/render-blocks.ts
652
+ function renderEmailFromBlocks(template, options) {
653
+ const theme = { ...DEFAULT_EMAIL_THEME, ...template.theme };
654
+ const shouldInline = options?.inlineCss ?? true;
655
+ const blocks2 = options?.variables ? replaceBlockVariables(template.blocks, options.variables) : template.blocks;
656
+ const validBlocks = blocks2.filter((block) => {
657
+ if (!isValidBlock(block)) {
658
+ console.warn("[momentum:email] Skipping invalid email block:", block);
659
+ return false;
660
+ }
661
+ return true;
662
+ });
663
+ const blocksHtml = validBlocks.map((block) => renderBlock(block, theme, 0)).join("\n");
664
+ let html = wrapEmailDocument(blocksHtml, theme);
665
+ if (shouldInline) {
666
+ html = inlineCss(html);
667
+ }
668
+ return html;
669
+ }
670
+ function wrapEmailDocument(content, theme) {
671
+ const fontFamily = sanitizeFontFamily(theme.fontFamily);
672
+ const bgColor = sanitizeCssValue(theme.backgroundColor);
673
+ const borderRadius = sanitizeCssValue(theme.borderRadius);
674
+ return `<!DOCTYPE html>
675
+ <html lang="en">
676
+ <head>
677
+ <meta charset="UTF-8">
678
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
679
+ </head>
680
+ <body style="margin: 0; padding: 0; font-family: ${fontFamily}; background-color: ${bgColor}; line-height: 1.6;">
681
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: ${bgColor};">
682
+ <tr>
683
+ <td style="padding: 40px 20px;">
684
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px; margin: 0 auto; background-color: #ffffff; border-radius: ${borderRadius}; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
685
+ <tr>
686
+ <td style="padding: 40px;">
687
+ ${content}
688
+ </td>
689
+ </tr>
690
+ </table>
691
+ </td>
692
+ </tr>
693
+ </table>
694
+ </body>
695
+ </html>`;
696
+ }
697
+ function isValidBlock(block) {
698
+ const rec = block;
699
+ return typeof block === "object" && block !== null && typeof rec["id"] === "string" && rec["id"].length > 0 && typeof rec["type"] === "string" && typeof rec["data"] === "object" && rec["data"] !== null;
700
+ }
701
+ function renderBlock(block, theme, depth) {
702
+ switch (block.type) {
703
+ case "header":
704
+ return renderHeaderBlock(block.data, theme);
705
+ case "text":
706
+ return renderTextBlock(block.data, theme);
707
+ case "button":
708
+ return renderButtonBlock(block.data, theme);
709
+ case "image":
710
+ return renderImageBlock(block.data);
711
+ case "divider":
712
+ return renderDividerBlock(block.data);
713
+ case "spacer":
714
+ return renderSpacerBlock(block.data);
715
+ case "columns":
716
+ if (depth >= MAX_BLOCK_DEPTH) {
717
+ console.warn("[momentum:email] Max nesting depth reached, skipping columns block");
718
+ return "";
719
+ }
720
+ return renderColumnsBlock(block.data, theme, depth);
721
+ case "footer":
722
+ return renderFooterBlock(block.data, theme);
723
+ default:
724
+ return `<!-- unknown block type: ${escapeHtml2(block.type)} -->`;
725
+ }
726
+ }
727
+ function renderHeaderBlock(data, theme) {
728
+ const title = escapeHtml2(String(data["title"] ?? ""));
729
+ const subtitle = data["subtitle"] ? escapeHtml2(String(data["subtitle"])) : "";
730
+ const alignment = sanitizeAlignment(String(data["alignment"] ?? "left"));
731
+ return `<h1 style="margin: 0 0 8px; font-size: 24px; font-weight: 600; color: ${sanitizeCssValue(theme.textColor)}; text-align: ${alignment};">${title}</h1>${subtitle ? `<p style="margin: 0 0 16px; font-size: 16px; color: ${sanitizeCssValue(theme.mutedColor)}; text-align: ${alignment};">${subtitle}</p>` : ""}`;
732
+ }
733
+ function renderTextBlock(data, theme) {
734
+ const content = escapeHtml2(String(data["content"] ?? ""));
735
+ const fontSize = sanitizeCssNumber(data["fontSize"], 16);
736
+ const color = sanitizeCssValue(String(data["color"] ?? theme.textColor));
737
+ const alignment = sanitizeAlignment(String(data["alignment"] ?? "left"));
738
+ return `<p style="margin: 0 0 16px; font-size: ${fontSize}px; color: ${color}; text-align: ${alignment}; line-height: 1.6;">${content}</p>`;
739
+ }
740
+ function renderButtonBlock(data, theme) {
741
+ const label = escapeHtml2(String(data["label"] ?? "Click here"));
742
+ const href = escapeHtml2(sanitizeUrl(String(data["href"] ?? "#")));
743
+ const bgColor = sanitizeCssValue(String(data["backgroundColor"] ?? theme.primaryColor));
744
+ const color = sanitizeCssValue(String(data["color"] ?? "#ffffff"));
745
+ const alignment = sanitizeAlignment(String(data["alignment"] ?? "left"));
746
+ return `<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
747
+ <tr>
748
+ <td style="padding: 0 0 16px;" align="${alignment}">
749
+ <a href="${href}" style="display: inline-block; padding: 12px 24px; background-color: ${bgColor}; color: ${color}; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 16px;">${label}</a>
750
+ </td>
751
+ </tr>
752
+ </table>`;
753
+ }
754
+ function renderImageBlock(data) {
755
+ const rawSrc = String(data["src"] ?? "").trim();
756
+ if (!rawSrc)
757
+ return "<!-- image block: no src configured -->";
758
+ const src = escapeHtml2(sanitizeUrl(rawSrc));
759
+ const alt = escapeHtml2(String(data["alt"] ?? ""));
760
+ const width = sanitizeCssValue(String(data["width"] ?? "100%"));
761
+ const img = `<img src="${src}" alt="${alt}" width="${width}" style="display: block; max-width: 100%; height: auto; border: 0;">`;
762
+ if (data["href"]) {
763
+ const href = escapeHtml2(sanitizeUrl(String(data["href"])));
764
+ return `<a href="${href}" style="display: block;">${img}</a>`;
765
+ }
766
+ return img;
767
+ }
768
+ function renderDividerBlock(data) {
769
+ const color = sanitizeCssValue(String(data["color"] ?? "#e4e4e7"));
770
+ const margin = sanitizeCssValue(String(data["margin"] ?? "24px 0"));
771
+ return `<hr style="border: none; border-top: 1px solid ${color}; margin: ${margin};">`;
772
+ }
773
+ function renderSpacerBlock(data) {
774
+ const height = sanitizeCssNumber(data["height"], 24);
775
+ return `<div style="height: ${height}px; line-height: ${height}px; font-size: 1px;">&nbsp;</div>`;
776
+ }
777
+ function renderColumnsBlock(data, theme, depth) {
778
+ const rawColumns = data["columns"];
779
+ const columns = Array.isArray(rawColumns) ? rawColumns : [];
780
+ const width = Math.floor(100 / (columns.length || 1));
781
+ const tds = columns.map((col) => {
782
+ const colObj = (
783
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- narrowing unknown column objects
784
+ typeof col === "object" && col !== null ? col : {}
785
+ );
786
+ const rawBlocks = colObj["blocks"];
787
+ const colContent = (Array.isArray(rawBlocks) ? rawBlocks : []).filter(isValidBlock).map((b) => renderBlock(b, theme, depth + 1)).join("\n");
788
+ return `<td style="width: ${width}%; vertical-align: top; padding: 0 8px;">${colContent}</td>`;
789
+ }).join("\n");
790
+ return `<table role="presentation" width="100%" cellspacing="0" cellpadding="0"><tr>${tds}</tr></table>`;
791
+ }
792
+ function renderFooterBlock(data, theme) {
793
+ const text2 = escapeHtml2(String(data["text"] ?? ""));
794
+ const color = sanitizeCssValue(String(data["color"] ?? theme.mutedColor));
795
+ return `<p style="margin: 16px 0 0; font-size: 12px; color: ${color}; text-align: center;">${text2}</p>`;
796
+ }
797
+ var MAX_BLOCK_DEPTH;
798
+ var init_render_blocks = __esm({
799
+ "libs/email/src/lib/render/render-blocks.ts"() {
800
+ "use strict";
801
+ init_escape_html();
802
+ init_css_inliner();
803
+ init_replace_variables();
804
+ init_sanitize();
805
+ init_types();
806
+ MAX_BLOCK_DEPTH = 5;
807
+ }
808
+ });
809
+
810
+ // libs/email/src/lib/utils/blocks-to-plain-text.ts
811
+ function blocksToPlainText(blocks2, depth = 0) {
812
+ const lines = [];
813
+ for (const block of blocks2) {
814
+ const text2 = blockToText(block, depth);
815
+ if (text2) {
816
+ lines.push(text2);
817
+ }
818
+ }
819
+ return lines.join("\n\n");
820
+ }
821
+ function blockToText(block, depth) {
822
+ switch (block.type) {
823
+ case "header":
824
+ return headerToText(block.data);
825
+ case "text":
826
+ return String(block.data["content"] ?? "");
827
+ case "button":
828
+ return buttonToText(block.data);
829
+ case "footer":
830
+ return String(block.data["text"] ?? "");
831
+ case "columns":
832
+ if (depth >= MAX_BLOCK_DEPTH2) {
833
+ console.warn("[momentum:email] Max nesting depth reached, skipping columns block");
834
+ return "";
835
+ }
836
+ return columnsToText(block.data, depth);
837
+ case "divider":
838
+ case "spacer":
839
+ case "image":
840
+ return "";
841
+ default:
842
+ return "";
843
+ }
844
+ }
845
+ function headerToText(data) {
846
+ const title = String(data["title"] ?? "");
847
+ const subtitle = data["subtitle"] ? String(data["subtitle"]) : "";
848
+ if (!title && !subtitle)
849
+ return "";
850
+ if (!subtitle)
851
+ return title;
852
+ return `${title}
853
+ ${subtitle}`;
854
+ }
855
+ function buttonToText(data) {
856
+ const label = String(data["label"] ?? "");
857
+ const href = data["href"] ? String(data["href"]) : "";
858
+ if (!label)
859
+ return "";
860
+ if (!href)
861
+ return label;
862
+ return `${label}: ${href}`;
863
+ }
864
+ function columnsToText(data, depth) {
865
+ const columns = data["columns"];
866
+ if (!Array.isArray(columns))
867
+ return "";
868
+ const parts = [];
869
+ for (const col of columns) {
870
+ if (col && typeof col === "object" && Array.isArray(col.blocks)) {
871
+ const colText = blocksToPlainText(col.blocks, depth + 1);
872
+ if (colText) {
873
+ parts.push(colText);
874
+ }
875
+ }
876
+ }
877
+ return parts.join("\n\n");
878
+ }
879
+ var MAX_BLOCK_DEPTH2;
880
+ var init_blocks_to_plain_text = __esm({
881
+ "libs/email/src/lib/utils/blocks-to-plain-text.ts"() {
882
+ "use strict";
883
+ MAX_BLOCK_DEPTH2 = 5;
884
+ }
885
+ });
886
+
887
+ // libs/email/src/server.ts
888
+ var server_exports = {};
889
+ __export(server_exports, {
890
+ DEFAULT_EMAIL_THEME: () => DEFAULT_EMAIL_THEME,
891
+ blocksToPlainText: () => blocksToPlainText,
892
+ escapeHtml: () => escapeHtml2,
893
+ inlineCss: () => inlineCss,
894
+ isValidBlock: () => isValidBlock,
895
+ renderEmailFromBlocks: () => renderEmailFromBlocks,
896
+ replaceBlockVariables: () => replaceBlockVariables,
897
+ replaceVariables: () => replaceVariables
898
+ });
899
+ var init_server = __esm({
900
+ "libs/email/src/server.ts"() {
901
+ "use strict";
902
+ init_render_blocks();
903
+ init_replace_variables();
904
+ init_blocks_to_plain_text();
905
+ init_css_inliner();
906
+ init_escape_html();
907
+ init_types();
908
+ }
909
+ });
910
+
534
911
  // libs/server-analog/src/index.ts
535
912
  var src_exports2 = {};
536
913
  __export(src_exports2, {
@@ -973,6 +1350,31 @@ function validateRowCount(name, label, count, minRows, maxRows, errors) {
973
1350
  }
974
1351
 
975
1352
  // libs/core/src/lib/collections/media.collection.ts
1353
+ var validateFocalPoint = (value) => {
1354
+ if (value === null || value === void 0)
1355
+ return true;
1356
+ if (typeof value !== "object" || Array.isArray(value)) {
1357
+ return "Focal point must be an object with x and y coordinates";
1358
+ }
1359
+ const fp = Object.fromEntries(Object.entries(value));
1360
+ if (!("x" in fp) || !("y" in fp)) {
1361
+ return "Focal point must have both x and y properties";
1362
+ }
1363
+ const { x, y } = fp;
1364
+ if (typeof x !== "number" || !Number.isFinite(x)) {
1365
+ return "Focal point x must be a finite number";
1366
+ }
1367
+ if (typeof y !== "number" || !Number.isFinite(y)) {
1368
+ return "Focal point y must be a finite number";
1369
+ }
1370
+ if (x < 0 || x > 1) {
1371
+ return `Focal point x must be between 0 and 1 (received ${x})`;
1372
+ }
1373
+ if (y < 0 || y > 1) {
1374
+ return `Focal point y must be between 0 and 1 (received ${y})`;
1375
+ }
1376
+ return true;
1377
+ };
976
1378
  var MediaCollection = defineCollection({
977
1379
  slug: "media",
978
1380
  labels: {
@@ -1027,6 +1429,14 @@ var MediaCollection = defineCollection({
1027
1429
  json("focalPoint", {
1028
1430
  label: "Focal Point",
1029
1431
  description: "Focal point coordinates for image cropping",
1432
+ validate: validateFocalPoint,
1433
+ admin: {
1434
+ hidden: true
1435
+ }
1436
+ }),
1437
+ json("sizes", {
1438
+ label: "Image Sizes",
1439
+ description: "Generated image size variants",
1030
1440
  admin: {
1031
1441
  hidden: true
1032
1442
  }
@@ -1796,6 +2206,15 @@ function deepEqual(a, b) {
1796
2206
  (key) => Object.prototype.hasOwnProperty.call(bRec, key) && deepEqual(aRec[key], bRec[key])
1797
2207
  );
1798
2208
  }
2209
+ function stripTransientKeys(data) {
2210
+ const result = {};
2211
+ for (const [key, value] of Object.entries(data)) {
2212
+ if (!key.startsWith("_")) {
2213
+ result[key] = value;
2214
+ }
2215
+ }
2216
+ return result;
2217
+ }
1799
2218
  function flattenWhereClause(where) {
1800
2219
  if (!where)
1801
2220
  return {};
@@ -1959,6 +2378,7 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
1959
2378
  );
1960
2379
  }
1961
2380
  processedData = await this.runHooks("beforeChange", processedData, "create");
2381
+ processedData = stripTransientKeys(processedData);
1962
2382
  const doc = await this.adapter.create(this.slug, processedData);
1963
2383
  await this.runHooks("afterChange", doc, "create");
1964
2384
  if (hasFieldHooks(this.collectionConfig.fields)) {
@@ -2019,6 +2439,7 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
2019
2439
  );
2020
2440
  }
2021
2441
  processedData = await this.runHooks("beforeChange", processedData, "update", originalDoc);
2442
+ processedData = stripTransientKeys(processedData);
2022
2443
  const doc = await this.adapter.update(this.slug, id, processedData);
2023
2444
  await this.runHooks("afterChange", doc, "update", originalDoc);
2024
2445
  if (hasFieldHooks(this.collectionConfig.fields)) {
@@ -3566,7 +3987,8 @@ async function handleUpload(config, request) {
3566
3987
  filesize: file.size,
3567
3988
  path: storedFile.path,
3568
3989
  url: storedFile.url,
3569
- alt: alt ?? ""
3990
+ alt: alt ?? "",
3991
+ _file: file
3570
3992
  };
3571
3993
  const api = getMomentumAPI().setContext({ user });
3572
3994
  const doc = await api.collection(collection).create(mediaData);
@@ -3633,7 +4055,8 @@ async function handleCollectionUpload(globalConfig, request) {
3633
4055
  mimeType: file.mimeType,
3634
4056
  filesize: file.size,
3635
4057
  path: storedFile.path,
3636
- url: storedFile.url
4058
+ url: storedFile.url,
4059
+ _file: file
3637
4060
  };
3638
4061
  const api = getMomentumAPI().setContext({ user });
3639
4062
  const doc = await api.collection(collectionSlug).create(docData);
@@ -3670,6 +4093,7 @@ function getMimeTypeFromPath(path) {
3670
4093
  png: "image/png",
3671
4094
  gif: "image/gif",
3672
4095
  webp: "image/webp",
4096
+ avif: "image/avif",
3673
4097
  svg: "image/svg+xml",
3674
4098
  pdf: "application/pdf",
3675
4099
  json: "application/json",
@@ -4745,13 +5169,12 @@ function getEmailBuilderFieldName(collection) {
4745
5169
  return field?.name;
4746
5170
  }
4747
5171
  async function renderEmailPreviewHTML(doc, blocksFieldName) {
4748
- const emailPkg = "@momentumcms/email";
4749
- const { renderEmailFromBlocks } = await import(emailPkg);
5172
+ const { renderEmailFromBlocks: renderEmailFromBlocks2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4750
5173
  const blocks2 = doc[blocksFieldName];
4751
5174
  if (!Array.isArray(blocks2) || blocks2.length === 0) {
4752
5175
  return '<html><body style="display:flex;align-items:center;justify-content:center;min-height:100vh;color:#666;font-family:sans-serif"><p>No email blocks yet.</p></body></html>';
4753
5176
  }
4754
- return renderEmailFromBlocks({ blocks: blocks2 });
5177
+ return renderEmailFromBlocks2({ blocks: blocks2 });
4755
5178
  }
4756
5179
  function nestBracketParams(flat) {
4757
5180
  const result = {};
@@ -5340,6 +5763,63 @@ function createComprehensiveMomentumHandler(config) {
5340
5763
  }
5341
5764
  }
5342
5765
  }
5766
+ if (seg2 === "preview" && seg1 && (method === "GET" || method === "POST")) {
5767
+ if (!user) {
5768
+ utils.setResponseStatus(event, 401);
5769
+ return { error: "Authentication required to access preview" };
5770
+ }
5771
+ try {
5772
+ const collectionSlug2 = seg0;
5773
+ const docId = seg1;
5774
+ const collectionConfig = config.collections.find((c) => c.slug === collectionSlug2);
5775
+ if (!collectionConfig) {
5776
+ utils.setResponseStatus(event, 404);
5777
+ return { error: "Collection not found" };
5778
+ }
5779
+ const accessFn = collectionConfig.access?.read;
5780
+ if (accessFn) {
5781
+ const allowed = await Promise.resolve(accessFn({ req: { user } }));
5782
+ if (!allowed) {
5783
+ utils.setResponseStatus(event, 403);
5784
+ return { error: "Access denied" };
5785
+ }
5786
+ }
5787
+ let docRecord;
5788
+ if (method === "POST") {
5789
+ const body2 = await safeReadBody(event, utils, method);
5790
+ if (body2["data"] && typeof body2["data"] === "object") {
5791
+ docRecord = body2["data"];
5792
+ } else {
5793
+ utils.setResponseStatus(event, 400);
5794
+ return { error: "POST preview requires { data: ... } body" };
5795
+ }
5796
+ } else {
5797
+ const contextApi = getContextualAPI(user);
5798
+ const doc = await contextApi.collection(collectionSlug2).findById(docId);
5799
+ if (!doc) {
5800
+ utils.setResponseStatus(event, 404);
5801
+ return { error: "Document not found" };
5802
+ }
5803
+ docRecord = doc;
5804
+ }
5805
+ const emailField = getEmailBuilderFieldName(collectionConfig);
5806
+ const html = emailField ? await renderEmailPreviewHTML(docRecord, emailField) : renderPreviewHTML({ doc: docRecord, collection: collectionConfig });
5807
+ utils.setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
5808
+ return utils.send(event, html);
5809
+ } catch (error) {
5810
+ const message = sanitizeErrorMessage(error, "Unknown error");
5811
+ if (message.includes("Access denied")) {
5812
+ utils.setResponseStatus(event, 403);
5813
+ return { error: message };
5814
+ }
5815
+ if (message.includes("not found")) {
5816
+ utils.setResponseStatus(event, 404);
5817
+ return { error: message };
5818
+ }
5819
+ utils.setResponseStatus(event, 500);
5820
+ return { error: "Preview failed", message };
5821
+ }
5822
+ }
5343
5823
  if (seg1 && seg2 && method === "POST") {
5344
5824
  const collectionSlug2 = seg0;
5345
5825
  const docId = seg1;
@@ -5437,63 +5917,6 @@ function createComprehensiveMomentumHandler(config) {
5437
5917
  };
5438
5918
  }
5439
5919
  }
5440
- if (seg2 === "preview" && seg1 && (method === "GET" || method === "POST")) {
5441
- if (!user) {
5442
- utils.setResponseStatus(event, 401);
5443
- return { error: "Authentication required to access preview" };
5444
- }
5445
- try {
5446
- const collectionSlug2 = seg0;
5447
- const docId = seg1;
5448
- const collectionConfig = config.collections.find((c) => c.slug === collectionSlug2);
5449
- if (!collectionConfig) {
5450
- utils.setResponseStatus(event, 404);
5451
- return { error: "Collection not found" };
5452
- }
5453
- const accessFn = collectionConfig.access?.read;
5454
- if (accessFn) {
5455
- const allowed = await Promise.resolve(accessFn({ req: { user } }));
5456
- if (!allowed) {
5457
- utils.setResponseStatus(event, 403);
5458
- return { error: "Access denied" };
5459
- }
5460
- }
5461
- let docRecord;
5462
- if (method === "POST") {
5463
- const body2 = await safeReadBody(event, utils, method);
5464
- if (body2["data"] && typeof body2["data"] === "object") {
5465
- docRecord = body2["data"];
5466
- } else {
5467
- utils.setResponseStatus(event, 400);
5468
- return { error: "POST preview requires { data: ... } body" };
5469
- }
5470
- } else {
5471
- const contextApi = getContextualAPI(user);
5472
- const doc = await contextApi.collection(collectionSlug2).findById(docId);
5473
- if (!doc) {
5474
- utils.setResponseStatus(event, 404);
5475
- return { error: "Document not found" };
5476
- }
5477
- docRecord = doc;
5478
- }
5479
- const emailField = getEmailBuilderFieldName(collectionConfig);
5480
- const html = emailField ? await renderEmailPreviewHTML(docRecord, emailField) : renderPreviewHTML({ doc: docRecord, collection: collectionConfig });
5481
- utils.setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
5482
- return utils.send(event, html);
5483
- } catch (error) {
5484
- const message = sanitizeErrorMessage(error, "Unknown error");
5485
- if (message.includes("Access denied")) {
5486
- utils.setResponseStatus(event, 403);
5487
- return { error: message };
5488
- }
5489
- if (message.includes("not found")) {
5490
- utils.setResponseStatus(event, 404);
5491
- return { error: message };
5492
- }
5493
- utils.setResponseStatus(event, 500);
5494
- return { error: "Preview failed", message };
5495
- }
5496
- }
5497
5920
  if (seg0 && seg1 && !seg2) {
5498
5921
  const customKey = `${method}:${seg0}/${seg1}`;
5499
5922
  const customEntry = customEndpointMap.get(customKey);
package/index.js CHANGED
@@ -508,6 +508,382 @@ var init_src = __esm({
508
508
  }
509
509
  });
510
510
 
511
+ // libs/email/src/lib/utils/escape-html.ts
512
+ function escapeHtml2(unsafe) {
513
+ return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
514
+ }
515
+ var init_escape_html = __esm({
516
+ "libs/email/src/lib/utils/escape-html.ts"() {
517
+ "use strict";
518
+ }
519
+ });
520
+
521
+ // libs/email/src/lib/utils/css-inliner.ts
522
+ import juice from "juice";
523
+ function inlineCss(html) {
524
+ return juice(html, {
525
+ removeStyleTags: true,
526
+ preserveMediaQueries: true,
527
+ preserveFontFaces: true,
528
+ insertPreservedExtraCss: true
529
+ });
530
+ }
531
+ var init_css_inliner = __esm({
532
+ "libs/email/src/lib/utils/css-inliner.ts"() {
533
+ "use strict";
534
+ }
535
+ });
536
+
537
+ // libs/email/src/lib/utils/replace-variables.ts
538
+ function replaceVariables(text2, variables) {
539
+ return text2.replace(/\{\{(\w+)\}\}/g, (_, key) => variables[key] ?? "");
540
+ }
541
+ function replaceBlockVariables(blocks2, variables) {
542
+ return blocks2.map((block) => ({
543
+ ...block,
544
+ data: replaceDataVariables(block.data, variables)
545
+ }));
546
+ }
547
+ function replaceDataVariables(data, variables) {
548
+ const result = {};
549
+ for (const [key, value] of Object.entries(data)) {
550
+ if (typeof value === "string") {
551
+ result[key] = replaceVariables(value, variables);
552
+ } else if (Array.isArray(value)) {
553
+ result[key] = value.map((item) => {
554
+ if (typeof item === "object" && item !== null && "blocks" in item) {
555
+ const col = item;
556
+ const nestedBlocks = Array.isArray(col["blocks"]) ? col["blocks"] : [];
557
+ return { ...col, blocks: replaceBlockVariables(nestedBlocks, variables) };
558
+ }
559
+ return item;
560
+ });
561
+ } else {
562
+ result[key] = value;
563
+ }
564
+ }
565
+ return result;
566
+ }
567
+ var init_replace_variables = __esm({
568
+ "libs/email/src/lib/utils/replace-variables.ts"() {
569
+ "use strict";
570
+ }
571
+ });
572
+
573
+ // libs/email/src/lib/utils/sanitize.ts
574
+ function sanitizeAlignment(value) {
575
+ return VALID_ALIGNMENTS.has(value) ? value : "left";
576
+ }
577
+ function sanitizeCssValue(value) {
578
+ return value.replace(/[;{}()"'<>\\]/g, "");
579
+ }
580
+ function sanitizeFontFamily(value) {
581
+ return value.replace(/[;{}()"<>\\]/g, "");
582
+ }
583
+ function sanitizeCssNumber(value, fallback) {
584
+ if (value === null || value === void 0)
585
+ return String(fallback);
586
+ const num = Number(value);
587
+ return Number.isFinite(num) && num >= 0 ? String(num) : String(fallback);
588
+ }
589
+ function sanitizeUrl(url) {
590
+ const trimmed = url.trim();
591
+ if (!trimmed || trimmed === "#")
592
+ return trimmed || "#";
593
+ try {
594
+ const parsed = new URL(trimmed);
595
+ return SAFE_URL_PROTOCOLS.has(parsed.protocol) ? trimmed : "#";
596
+ } catch {
597
+ if (trimmed.startsWith("/") || trimmed.startsWith("#"))
598
+ return trimmed;
599
+ return "#";
600
+ }
601
+ }
602
+ var VALID_ALIGNMENTS, SAFE_URL_PROTOCOLS;
603
+ var init_sanitize = __esm({
604
+ "libs/email/src/lib/utils/sanitize.ts"() {
605
+ "use strict";
606
+ VALID_ALIGNMENTS = /* @__PURE__ */ new Set(["left", "center", "right"]);
607
+ SAFE_URL_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:"]);
608
+ }
609
+ });
610
+
611
+ // libs/email/src/types.ts
612
+ var DEFAULT_EMAIL_THEME;
613
+ var init_types = __esm({
614
+ "libs/email/src/types.ts"() {
615
+ "use strict";
616
+ DEFAULT_EMAIL_THEME = {
617
+ primaryColor: "#18181b",
618
+ backgroundColor: "#f4f4f5",
619
+ textColor: "#3f3f46",
620
+ mutedColor: "#71717a",
621
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
622
+ borderRadius: "8px"
623
+ };
624
+ }
625
+ });
626
+
627
+ // libs/email/src/lib/render/render-blocks.ts
628
+ function renderEmailFromBlocks(template, options) {
629
+ const theme = { ...DEFAULT_EMAIL_THEME, ...template.theme };
630
+ const shouldInline = options?.inlineCss ?? true;
631
+ const blocks2 = options?.variables ? replaceBlockVariables(template.blocks, options.variables) : template.blocks;
632
+ const validBlocks = blocks2.filter((block) => {
633
+ if (!isValidBlock(block)) {
634
+ console.warn("[momentum:email] Skipping invalid email block:", block);
635
+ return false;
636
+ }
637
+ return true;
638
+ });
639
+ const blocksHtml = validBlocks.map((block) => renderBlock(block, theme, 0)).join("\n");
640
+ let html = wrapEmailDocument(blocksHtml, theme);
641
+ if (shouldInline) {
642
+ html = inlineCss(html);
643
+ }
644
+ return html;
645
+ }
646
+ function wrapEmailDocument(content, theme) {
647
+ const fontFamily = sanitizeFontFamily(theme.fontFamily);
648
+ const bgColor = sanitizeCssValue(theme.backgroundColor);
649
+ const borderRadius = sanitizeCssValue(theme.borderRadius);
650
+ return `<!DOCTYPE html>
651
+ <html lang="en">
652
+ <head>
653
+ <meta charset="UTF-8">
654
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
655
+ </head>
656
+ <body style="margin: 0; padding: 0; font-family: ${fontFamily}; background-color: ${bgColor}; line-height: 1.6;">
657
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: ${bgColor};">
658
+ <tr>
659
+ <td style="padding: 40px 20px;">
660
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px; margin: 0 auto; background-color: #ffffff; border-radius: ${borderRadius}; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
661
+ <tr>
662
+ <td style="padding: 40px;">
663
+ ${content}
664
+ </td>
665
+ </tr>
666
+ </table>
667
+ </td>
668
+ </tr>
669
+ </table>
670
+ </body>
671
+ </html>`;
672
+ }
673
+ function isValidBlock(block) {
674
+ const rec = block;
675
+ return typeof block === "object" && block !== null && typeof rec["id"] === "string" && rec["id"].length > 0 && typeof rec["type"] === "string" && typeof rec["data"] === "object" && rec["data"] !== null;
676
+ }
677
+ function renderBlock(block, theme, depth) {
678
+ switch (block.type) {
679
+ case "header":
680
+ return renderHeaderBlock(block.data, theme);
681
+ case "text":
682
+ return renderTextBlock(block.data, theme);
683
+ case "button":
684
+ return renderButtonBlock(block.data, theme);
685
+ case "image":
686
+ return renderImageBlock(block.data);
687
+ case "divider":
688
+ return renderDividerBlock(block.data);
689
+ case "spacer":
690
+ return renderSpacerBlock(block.data);
691
+ case "columns":
692
+ if (depth >= MAX_BLOCK_DEPTH) {
693
+ console.warn("[momentum:email] Max nesting depth reached, skipping columns block");
694
+ return "";
695
+ }
696
+ return renderColumnsBlock(block.data, theme, depth);
697
+ case "footer":
698
+ return renderFooterBlock(block.data, theme);
699
+ default:
700
+ return `<!-- unknown block type: ${escapeHtml2(block.type)} -->`;
701
+ }
702
+ }
703
+ function renderHeaderBlock(data, theme) {
704
+ const title = escapeHtml2(String(data["title"] ?? ""));
705
+ const subtitle = data["subtitle"] ? escapeHtml2(String(data["subtitle"])) : "";
706
+ const alignment = sanitizeAlignment(String(data["alignment"] ?? "left"));
707
+ return `<h1 style="margin: 0 0 8px; font-size: 24px; font-weight: 600; color: ${sanitizeCssValue(theme.textColor)}; text-align: ${alignment};">${title}</h1>${subtitle ? `<p style="margin: 0 0 16px; font-size: 16px; color: ${sanitizeCssValue(theme.mutedColor)}; text-align: ${alignment};">${subtitle}</p>` : ""}`;
708
+ }
709
+ function renderTextBlock(data, theme) {
710
+ const content = escapeHtml2(String(data["content"] ?? ""));
711
+ const fontSize = sanitizeCssNumber(data["fontSize"], 16);
712
+ const color = sanitizeCssValue(String(data["color"] ?? theme.textColor));
713
+ const alignment = sanitizeAlignment(String(data["alignment"] ?? "left"));
714
+ return `<p style="margin: 0 0 16px; font-size: ${fontSize}px; color: ${color}; text-align: ${alignment}; line-height: 1.6;">${content}</p>`;
715
+ }
716
+ function renderButtonBlock(data, theme) {
717
+ const label = escapeHtml2(String(data["label"] ?? "Click here"));
718
+ const href = escapeHtml2(sanitizeUrl(String(data["href"] ?? "#")));
719
+ const bgColor = sanitizeCssValue(String(data["backgroundColor"] ?? theme.primaryColor));
720
+ const color = sanitizeCssValue(String(data["color"] ?? "#ffffff"));
721
+ const alignment = sanitizeAlignment(String(data["alignment"] ?? "left"));
722
+ return `<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
723
+ <tr>
724
+ <td style="padding: 0 0 16px;" align="${alignment}">
725
+ <a href="${href}" style="display: inline-block; padding: 12px 24px; background-color: ${bgColor}; color: ${color}; text-decoration: none; border-radius: 6px; font-weight: 500; font-size: 16px;">${label}</a>
726
+ </td>
727
+ </tr>
728
+ </table>`;
729
+ }
730
+ function renderImageBlock(data) {
731
+ const rawSrc = String(data["src"] ?? "").trim();
732
+ if (!rawSrc)
733
+ return "<!-- image block: no src configured -->";
734
+ const src = escapeHtml2(sanitizeUrl(rawSrc));
735
+ const alt = escapeHtml2(String(data["alt"] ?? ""));
736
+ const width = sanitizeCssValue(String(data["width"] ?? "100%"));
737
+ const img = `<img src="${src}" alt="${alt}" width="${width}" style="display: block; max-width: 100%; height: auto; border: 0;">`;
738
+ if (data["href"]) {
739
+ const href = escapeHtml2(sanitizeUrl(String(data["href"])));
740
+ return `<a href="${href}" style="display: block;">${img}</a>`;
741
+ }
742
+ return img;
743
+ }
744
+ function renderDividerBlock(data) {
745
+ const color = sanitizeCssValue(String(data["color"] ?? "#e4e4e7"));
746
+ const margin = sanitizeCssValue(String(data["margin"] ?? "24px 0"));
747
+ return `<hr style="border: none; border-top: 1px solid ${color}; margin: ${margin};">`;
748
+ }
749
+ function renderSpacerBlock(data) {
750
+ const height = sanitizeCssNumber(data["height"], 24);
751
+ return `<div style="height: ${height}px; line-height: ${height}px; font-size: 1px;">&nbsp;</div>`;
752
+ }
753
+ function renderColumnsBlock(data, theme, depth) {
754
+ const rawColumns = data["columns"];
755
+ const columns = Array.isArray(rawColumns) ? rawColumns : [];
756
+ const width = Math.floor(100 / (columns.length || 1));
757
+ const tds = columns.map((col) => {
758
+ const colObj = (
759
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- narrowing unknown column objects
760
+ typeof col === "object" && col !== null ? col : {}
761
+ );
762
+ const rawBlocks = colObj["blocks"];
763
+ const colContent = (Array.isArray(rawBlocks) ? rawBlocks : []).filter(isValidBlock).map((b) => renderBlock(b, theme, depth + 1)).join("\n");
764
+ return `<td style="width: ${width}%; vertical-align: top; padding: 0 8px;">${colContent}</td>`;
765
+ }).join("\n");
766
+ return `<table role="presentation" width="100%" cellspacing="0" cellpadding="0"><tr>${tds}</tr></table>`;
767
+ }
768
+ function renderFooterBlock(data, theme) {
769
+ const text2 = escapeHtml2(String(data["text"] ?? ""));
770
+ const color = sanitizeCssValue(String(data["color"] ?? theme.mutedColor));
771
+ return `<p style="margin: 16px 0 0; font-size: 12px; color: ${color}; text-align: center;">${text2}</p>`;
772
+ }
773
+ var MAX_BLOCK_DEPTH;
774
+ var init_render_blocks = __esm({
775
+ "libs/email/src/lib/render/render-blocks.ts"() {
776
+ "use strict";
777
+ init_escape_html();
778
+ init_css_inliner();
779
+ init_replace_variables();
780
+ init_sanitize();
781
+ init_types();
782
+ MAX_BLOCK_DEPTH = 5;
783
+ }
784
+ });
785
+
786
+ // libs/email/src/lib/utils/blocks-to-plain-text.ts
787
+ function blocksToPlainText(blocks2, depth = 0) {
788
+ const lines = [];
789
+ for (const block of blocks2) {
790
+ const text2 = blockToText(block, depth);
791
+ if (text2) {
792
+ lines.push(text2);
793
+ }
794
+ }
795
+ return lines.join("\n\n");
796
+ }
797
+ function blockToText(block, depth) {
798
+ switch (block.type) {
799
+ case "header":
800
+ return headerToText(block.data);
801
+ case "text":
802
+ return String(block.data["content"] ?? "");
803
+ case "button":
804
+ return buttonToText(block.data);
805
+ case "footer":
806
+ return String(block.data["text"] ?? "");
807
+ case "columns":
808
+ if (depth >= MAX_BLOCK_DEPTH2) {
809
+ console.warn("[momentum:email] Max nesting depth reached, skipping columns block");
810
+ return "";
811
+ }
812
+ return columnsToText(block.data, depth);
813
+ case "divider":
814
+ case "spacer":
815
+ case "image":
816
+ return "";
817
+ default:
818
+ return "";
819
+ }
820
+ }
821
+ function headerToText(data) {
822
+ const title = String(data["title"] ?? "");
823
+ const subtitle = data["subtitle"] ? String(data["subtitle"]) : "";
824
+ if (!title && !subtitle)
825
+ return "";
826
+ if (!subtitle)
827
+ return title;
828
+ return `${title}
829
+ ${subtitle}`;
830
+ }
831
+ function buttonToText(data) {
832
+ const label = String(data["label"] ?? "");
833
+ const href = data["href"] ? String(data["href"]) : "";
834
+ if (!label)
835
+ return "";
836
+ if (!href)
837
+ return label;
838
+ return `${label}: ${href}`;
839
+ }
840
+ function columnsToText(data, depth) {
841
+ const columns = data["columns"];
842
+ if (!Array.isArray(columns))
843
+ return "";
844
+ const parts = [];
845
+ for (const col of columns) {
846
+ if (col && typeof col === "object" && Array.isArray(col.blocks)) {
847
+ const colText = blocksToPlainText(col.blocks, depth + 1);
848
+ if (colText) {
849
+ parts.push(colText);
850
+ }
851
+ }
852
+ }
853
+ return parts.join("\n\n");
854
+ }
855
+ var MAX_BLOCK_DEPTH2;
856
+ var init_blocks_to_plain_text = __esm({
857
+ "libs/email/src/lib/utils/blocks-to-plain-text.ts"() {
858
+ "use strict";
859
+ MAX_BLOCK_DEPTH2 = 5;
860
+ }
861
+ });
862
+
863
+ // libs/email/src/server.ts
864
+ var server_exports = {};
865
+ __export(server_exports, {
866
+ DEFAULT_EMAIL_THEME: () => DEFAULT_EMAIL_THEME,
867
+ blocksToPlainText: () => blocksToPlainText,
868
+ escapeHtml: () => escapeHtml2,
869
+ inlineCss: () => inlineCss,
870
+ isValidBlock: () => isValidBlock,
871
+ renderEmailFromBlocks: () => renderEmailFromBlocks,
872
+ replaceBlockVariables: () => replaceBlockVariables,
873
+ replaceVariables: () => replaceVariables
874
+ });
875
+ var init_server = __esm({
876
+ "libs/email/src/server.ts"() {
877
+ "use strict";
878
+ init_render_blocks();
879
+ init_replace_variables();
880
+ init_blocks_to_plain_text();
881
+ init_css_inliner();
882
+ init_escape_html();
883
+ init_types();
884
+ }
885
+ });
886
+
511
887
  // libs/logger/src/lib/log-level.ts
512
888
  var LOG_LEVEL_VALUES = {
513
889
  debug: 0,
@@ -941,6 +1317,31 @@ function validateRowCount(name, label, count, minRows, maxRows, errors) {
941
1317
  }
942
1318
 
943
1319
  // libs/core/src/lib/collections/media.collection.ts
1320
+ var validateFocalPoint = (value) => {
1321
+ if (value === null || value === void 0)
1322
+ return true;
1323
+ if (typeof value !== "object" || Array.isArray(value)) {
1324
+ return "Focal point must be an object with x and y coordinates";
1325
+ }
1326
+ const fp = Object.fromEntries(Object.entries(value));
1327
+ if (!("x" in fp) || !("y" in fp)) {
1328
+ return "Focal point must have both x and y properties";
1329
+ }
1330
+ const { x, y } = fp;
1331
+ if (typeof x !== "number" || !Number.isFinite(x)) {
1332
+ return "Focal point x must be a finite number";
1333
+ }
1334
+ if (typeof y !== "number" || !Number.isFinite(y)) {
1335
+ return "Focal point y must be a finite number";
1336
+ }
1337
+ if (x < 0 || x > 1) {
1338
+ return `Focal point x must be between 0 and 1 (received ${x})`;
1339
+ }
1340
+ if (y < 0 || y > 1) {
1341
+ return `Focal point y must be between 0 and 1 (received ${y})`;
1342
+ }
1343
+ return true;
1344
+ };
944
1345
  var MediaCollection = defineCollection({
945
1346
  slug: "media",
946
1347
  labels: {
@@ -995,6 +1396,14 @@ var MediaCollection = defineCollection({
995
1396
  json("focalPoint", {
996
1397
  label: "Focal Point",
997
1398
  description: "Focal point coordinates for image cropping",
1399
+ validate: validateFocalPoint,
1400
+ admin: {
1401
+ hidden: true
1402
+ }
1403
+ }),
1404
+ json("sizes", {
1405
+ label: "Image Sizes",
1406
+ description: "Generated image size variants",
998
1407
  admin: {
999
1408
  hidden: true
1000
1409
  }
@@ -1764,6 +2173,15 @@ function deepEqual(a, b) {
1764
2173
  (key) => Object.prototype.hasOwnProperty.call(bRec, key) && deepEqual(aRec[key], bRec[key])
1765
2174
  );
1766
2175
  }
2176
+ function stripTransientKeys(data) {
2177
+ const result = {};
2178
+ for (const [key, value] of Object.entries(data)) {
2179
+ if (!key.startsWith("_")) {
2180
+ result[key] = value;
2181
+ }
2182
+ }
2183
+ return result;
2184
+ }
1767
2185
  function flattenWhereClause(where) {
1768
2186
  if (!where)
1769
2187
  return {};
@@ -1927,6 +2345,7 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
1927
2345
  );
1928
2346
  }
1929
2347
  processedData = await this.runHooks("beforeChange", processedData, "create");
2348
+ processedData = stripTransientKeys(processedData);
1930
2349
  const doc = await this.adapter.create(this.slug, processedData);
1931
2350
  await this.runHooks("afterChange", doc, "create");
1932
2351
  if (hasFieldHooks(this.collectionConfig.fields)) {
@@ -1987,6 +2406,7 @@ var CollectionOperationsImpl = class _CollectionOperationsImpl {
1987
2406
  );
1988
2407
  }
1989
2408
  processedData = await this.runHooks("beforeChange", processedData, "update", originalDoc);
2409
+ processedData = stripTransientKeys(processedData);
1990
2410
  const doc = await this.adapter.update(this.slug, id, processedData);
1991
2411
  await this.runHooks("afterChange", doc, "update", originalDoc);
1992
2412
  if (hasFieldHooks(this.collectionConfig.fields)) {
@@ -3547,7 +3967,8 @@ async function handleUpload(config, request) {
3547
3967
  filesize: file.size,
3548
3968
  path: storedFile.path,
3549
3969
  url: storedFile.url,
3550
- alt: alt ?? ""
3970
+ alt: alt ?? "",
3971
+ _file: file
3551
3972
  };
3552
3973
  const api = getMomentumAPI().setContext({ user });
3553
3974
  const doc = await api.collection(collection).create(mediaData);
@@ -3614,7 +4035,8 @@ async function handleCollectionUpload(globalConfig, request) {
3614
4035
  mimeType: file.mimeType,
3615
4036
  filesize: file.size,
3616
4037
  path: storedFile.path,
3617
- url: storedFile.url
4038
+ url: storedFile.url,
4039
+ _file: file
3618
4040
  };
3619
4041
  const api = getMomentumAPI().setContext({ user });
3620
4042
  const doc = await api.collection(collectionSlug).create(docData);
@@ -3651,6 +4073,7 @@ function getMimeTypeFromPath(path) {
3651
4073
  png: "image/png",
3652
4074
  gif: "image/gif",
3653
4075
  webp: "image/webp",
4076
+ avif: "image/avif",
3654
4077
  svg: "image/svg+xml",
3655
4078
  pdf: "application/pdf",
3656
4079
  json: "application/json",
@@ -4726,13 +5149,12 @@ function getEmailBuilderFieldName(collection) {
4726
5149
  return field?.name;
4727
5150
  }
4728
5151
  async function renderEmailPreviewHTML(doc, blocksFieldName) {
4729
- const emailPkg = "@momentumcms/email";
4730
- const { renderEmailFromBlocks } = await import(emailPkg);
5152
+ const { renderEmailFromBlocks: renderEmailFromBlocks2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4731
5153
  const blocks2 = doc[blocksFieldName];
4732
5154
  if (!Array.isArray(blocks2) || blocks2.length === 0) {
4733
5155
  return '<html><body style="display:flex;align-items:center;justify-content:center;min-height:100vh;color:#666;font-family:sans-serif"><p>No email blocks yet.</p></body></html>';
4734
5156
  }
4735
- return renderEmailFromBlocks({ blocks: blocks2 });
5157
+ return renderEmailFromBlocks2({ blocks: blocks2 });
4736
5158
  }
4737
5159
  function nestBracketParams(flat) {
4738
5160
  const result = {};
@@ -5321,6 +5743,63 @@ function createComprehensiveMomentumHandler(config) {
5321
5743
  }
5322
5744
  }
5323
5745
  }
5746
+ if (seg2 === "preview" && seg1 && (method === "GET" || method === "POST")) {
5747
+ if (!user) {
5748
+ utils.setResponseStatus(event, 401);
5749
+ return { error: "Authentication required to access preview" };
5750
+ }
5751
+ try {
5752
+ const collectionSlug2 = seg0;
5753
+ const docId = seg1;
5754
+ const collectionConfig = config.collections.find((c) => c.slug === collectionSlug2);
5755
+ if (!collectionConfig) {
5756
+ utils.setResponseStatus(event, 404);
5757
+ return { error: "Collection not found" };
5758
+ }
5759
+ const accessFn = collectionConfig.access?.read;
5760
+ if (accessFn) {
5761
+ const allowed = await Promise.resolve(accessFn({ req: { user } }));
5762
+ if (!allowed) {
5763
+ utils.setResponseStatus(event, 403);
5764
+ return { error: "Access denied" };
5765
+ }
5766
+ }
5767
+ let docRecord;
5768
+ if (method === "POST") {
5769
+ const body2 = await safeReadBody(event, utils, method);
5770
+ if (body2["data"] && typeof body2["data"] === "object") {
5771
+ docRecord = body2["data"];
5772
+ } else {
5773
+ utils.setResponseStatus(event, 400);
5774
+ return { error: "POST preview requires { data: ... } body" };
5775
+ }
5776
+ } else {
5777
+ const contextApi = getContextualAPI(user);
5778
+ const doc = await contextApi.collection(collectionSlug2).findById(docId);
5779
+ if (!doc) {
5780
+ utils.setResponseStatus(event, 404);
5781
+ return { error: "Document not found" };
5782
+ }
5783
+ docRecord = doc;
5784
+ }
5785
+ const emailField = getEmailBuilderFieldName(collectionConfig);
5786
+ const html = emailField ? await renderEmailPreviewHTML(docRecord, emailField) : renderPreviewHTML({ doc: docRecord, collection: collectionConfig });
5787
+ utils.setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
5788
+ return utils.send(event, html);
5789
+ } catch (error) {
5790
+ const message = sanitizeErrorMessage(error, "Unknown error");
5791
+ if (message.includes("Access denied")) {
5792
+ utils.setResponseStatus(event, 403);
5793
+ return { error: message };
5794
+ }
5795
+ if (message.includes("not found")) {
5796
+ utils.setResponseStatus(event, 404);
5797
+ return { error: message };
5798
+ }
5799
+ utils.setResponseStatus(event, 500);
5800
+ return { error: "Preview failed", message };
5801
+ }
5802
+ }
5324
5803
  if (seg1 && seg2 && method === "POST") {
5325
5804
  const collectionSlug2 = seg0;
5326
5805
  const docId = seg1;
@@ -5418,63 +5897,6 @@ function createComprehensiveMomentumHandler(config) {
5418
5897
  };
5419
5898
  }
5420
5899
  }
5421
- if (seg2 === "preview" && seg1 && (method === "GET" || method === "POST")) {
5422
- if (!user) {
5423
- utils.setResponseStatus(event, 401);
5424
- return { error: "Authentication required to access preview" };
5425
- }
5426
- try {
5427
- const collectionSlug2 = seg0;
5428
- const docId = seg1;
5429
- const collectionConfig = config.collections.find((c) => c.slug === collectionSlug2);
5430
- if (!collectionConfig) {
5431
- utils.setResponseStatus(event, 404);
5432
- return { error: "Collection not found" };
5433
- }
5434
- const accessFn = collectionConfig.access?.read;
5435
- if (accessFn) {
5436
- const allowed = await Promise.resolve(accessFn({ req: { user } }));
5437
- if (!allowed) {
5438
- utils.setResponseStatus(event, 403);
5439
- return { error: "Access denied" };
5440
- }
5441
- }
5442
- let docRecord;
5443
- if (method === "POST") {
5444
- const body2 = await safeReadBody(event, utils, method);
5445
- if (body2["data"] && typeof body2["data"] === "object") {
5446
- docRecord = body2["data"];
5447
- } else {
5448
- utils.setResponseStatus(event, 400);
5449
- return { error: "POST preview requires { data: ... } body" };
5450
- }
5451
- } else {
5452
- const contextApi = getContextualAPI(user);
5453
- const doc = await contextApi.collection(collectionSlug2).findById(docId);
5454
- if (!doc) {
5455
- utils.setResponseStatus(event, 404);
5456
- return { error: "Document not found" };
5457
- }
5458
- docRecord = doc;
5459
- }
5460
- const emailField = getEmailBuilderFieldName(collectionConfig);
5461
- const html = emailField ? await renderEmailPreviewHTML(docRecord, emailField) : renderPreviewHTML({ doc: docRecord, collection: collectionConfig });
5462
- utils.setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
5463
- return utils.send(event, html);
5464
- } catch (error) {
5465
- const message = sanitizeErrorMessage(error, "Unknown error");
5466
- if (message.includes("Access denied")) {
5467
- utils.setResponseStatus(event, 403);
5468
- return { error: message };
5469
- }
5470
- if (message.includes("not found")) {
5471
- utils.setResponseStatus(event, 404);
5472
- return { error: message };
5473
- }
5474
- utils.setResponseStatus(event, 500);
5475
- return { error: "Preview failed", message };
5476
- }
5477
- }
5478
5900
  if (seg0 && seg1 && !seg2) {
5479
5901
  const customKey = `${method}:${seg0}/${seg1}`;
5480
5902
  const customEntry = customEndpointMap.get(customKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momentumcms/server-analog",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Nitro/h3 adapter for Momentum CMS with Analog.js support",
5
5
  "license": "MIT",
6
6
  "author": "Momentum CMS Contributors",
@@ -32,9 +32,14 @@
32
32
  "@momentumcms/storage": ">=0.0.1"
33
33
  },
34
34
  "dependencies": {
35
+ "@angular/compiler": "~21.2.0",
36
+ "@angular/core": "~21.2.0",
37
+ "@angular/platform-browser": "~21.2.0",
38
+ "@angular/platform-server": "~21.2.0",
35
39
  "@aws-sdk/client-s3": "^3.983.0",
36
40
  "@aws-sdk/s3-request-presigner": "^3.983.0",
37
- "graphql": "^16.12.0"
41
+ "graphql": "^16.12.0",
42
+ "juice": "^11.1.1"
38
43
  },
39
44
  "module": "./index.js"
40
45
  }