@momentumcms/server-analog 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.5.0 (2026-02-23)
2
+
3
+ This was a version bump only for server-analog to align it with other projects, there were no code changes.
4
+
5
+ ## 0.4.1 (2026-02-22)
6
+
7
+ This was a version bump only for server-analog to align it with other projects, there were no code changes.
8
+
1
9
  ## 0.4.0 (2026-02-22)
2
10
 
3
11
  ### 🚀 Features
package/index.cjs CHANGED
@@ -37,6 +37,35 @@ var init_storage_types = __esm({
37
37
  }
38
38
  });
39
39
 
40
+ // libs/storage/src/lib/storage-utils.ts
41
+ function getExtensionFromMimeType(mimeType) {
42
+ return MIME_TO_EXT[mimeType] ?? "";
43
+ }
44
+ var MIME_TO_EXT;
45
+ var init_storage_utils = __esm({
46
+ "libs/storage/src/lib/storage-utils.ts"() {
47
+ "use strict";
48
+ MIME_TO_EXT = {
49
+ "image/jpeg": ".jpg",
50
+ "image/png": ".png",
51
+ "image/gif": ".gif",
52
+ "image/webp": ".webp",
53
+ "image/svg+xml": ".svg",
54
+ "application/pdf": ".pdf",
55
+ "application/json": ".json",
56
+ "text/plain": ".txt",
57
+ "text/html": ".html",
58
+ "text/css": ".css",
59
+ "application/javascript": ".js",
60
+ "video/mp4": ".mp4",
61
+ "video/webm": ".webm",
62
+ "audio/mpeg": ".mp3",
63
+ "audio/wav": ".wav",
64
+ "application/zip": ".zip"
65
+ };
66
+ }
67
+ });
68
+
40
69
  // libs/storage/src/lib/storage-local.ts
41
70
  function localStorageAdapter(options) {
42
71
  const { directory, baseUrl } = options;
@@ -103,27 +132,6 @@ function localStorageAdapter(options) {
103
132
  }
104
133
  };
105
134
  }
106
- function getExtensionFromMimeType(mimeType) {
107
- const mimeToExt = {
108
- "image/jpeg": ".jpg",
109
- "image/png": ".png",
110
- "image/gif": ".gif",
111
- "image/webp": ".webp",
112
- "image/svg+xml": ".svg",
113
- "application/pdf": ".pdf",
114
- "application/json": ".json",
115
- "text/plain": ".txt",
116
- "text/html": ".html",
117
- "text/css": ".css",
118
- "application/javascript": ".js",
119
- "video/mp4": ".mp4",
120
- "video/webm": ".webm",
121
- "audio/mpeg": ".mp3",
122
- "audio/wav": ".wav",
123
- "application/zip": ".zip"
124
- };
125
- return mimeToExt[mimeType] ?? "";
126
- }
127
135
  var import_node_fs, import_node_path, import_node_crypto2;
128
136
  var init_storage_local = __esm({
129
137
  "libs/storage/src/lib/storage-local.ts"() {
@@ -131,6 +139,7 @@ var init_storage_local = __esm({
131
139
  import_node_fs = require("node:fs");
132
140
  import_node_path = require("node:path");
133
141
  import_node_crypto2 = require("node:crypto");
142
+ init_storage_utils();
134
143
  }
135
144
  });
136
145
 
@@ -192,7 +201,7 @@ function s3StorageAdapter(options) {
192
201
  return {
193
202
  async upload(file, uploadOptions) {
194
203
  const s3 = await getClient();
195
- const ext = (0, import_node_path2.extname)(file.originalName) || getExtensionFromMimeType2(file.mimeType);
204
+ const ext = (0, import_node_path2.extname)(file.originalName) || getExtensionFromMimeType(file.mimeType);
196
205
  const filename = uploadOptions?.filename ? `${uploadOptions.filename}${ext}` : `${(0, import_node_crypto3.randomUUID)()}${ext}`;
197
206
  const key = uploadOptions?.directory ? `${uploadOptions.directory}/${filename}` : filename;
198
207
  await s3.send(
@@ -281,33 +290,13 @@ function s3StorageAdapter(options) {
281
290
  }
282
291
  };
283
292
  }
284
- function getExtensionFromMimeType2(mimeType) {
285
- const mimeToExt = {
286
- "image/jpeg": ".jpg",
287
- "image/png": ".png",
288
- "image/gif": ".gif",
289
- "image/webp": ".webp",
290
- "image/svg+xml": ".svg",
291
- "application/pdf": ".pdf",
292
- "application/json": ".json",
293
- "text/plain": ".txt",
294
- "text/html": ".html",
295
- "text/css": ".css",
296
- "application/javascript": ".js",
297
- "video/mp4": ".mp4",
298
- "video/webm": ".webm",
299
- "audio/mpeg": ".mp3",
300
- "audio/wav": ".wav",
301
- "application/zip": ".zip"
302
- };
303
- return mimeToExt[mimeType] ?? "";
304
- }
305
293
  var import_node_crypto3, import_node_path2, S3Client, PutObjectCommand, DeleteObjectCommand, HeadObjectCommand, GetObjectCommand, getSignedUrl;
306
294
  var init_storage_s3 = __esm({
307
295
  "libs/storage/src/lib/storage-s3.ts"() {
308
296
  "use strict";
309
297
  import_node_crypto3 = require("node:crypto");
310
298
  import_node_path2 = require("node:path");
299
+ init_storage_utils();
311
300
  }
312
301
  });
313
302
 
@@ -328,7 +317,7 @@ function detectMimeType(buffer) {
328
317
  if (match) {
329
318
  if (sig.bytes[0] === 82 && sig.bytes[1] === 73) {
330
319
  if (buffer.length >= 12) {
331
- const formatId = buffer.slice(8, 12).toString("ascii");
320
+ const formatId = String.fromCharCode(...buffer.subarray(8, 12));
332
321
  if (formatId === "WEBP") {
333
322
  return "image/webp";
334
323
  }
@@ -341,7 +330,7 @@ function detectMimeType(buffer) {
341
330
  }
342
331
  }
343
332
  if (sig.mimeType === "video/mp4" && buffer.length >= 8) {
344
- const boxType = buffer.slice(4, 8).toString("ascii");
333
+ const boxType = String.fromCharCode(...buffer.subarray(4, 8));
345
334
  if (boxType === "ftyp") {
346
335
  return "video/mp4";
347
336
  }
@@ -350,7 +339,7 @@ function detectMimeType(buffer) {
350
339
  }
351
340
  }
352
341
  if (isTextContent(buffer)) {
353
- const text2 = buffer.toString("utf8", 0, Math.min(buffer.length, 1e3));
342
+ const text2 = new TextDecoder().decode(buffer.subarray(0, Math.min(buffer.length, 1e3)));
354
343
  if (text2.trim().startsWith("{") || text2.trim().startsWith("[")) {
355
344
  return "application/json";
356
345
  }
@@ -524,6 +513,7 @@ var init_mime_validator = __esm({
524
513
  var src_exports = {};
525
514
  __export(src_exports, {
526
515
  detectMimeType: () => detectMimeType,
516
+ getExtensionFromMimeType: () => getExtensionFromMimeType,
527
517
  isMimeTypeAllowed: () => isMimeTypeAllowed,
528
518
  localStorageAdapter: () => localStorageAdapter,
529
519
  mimeTypeMatches: () => mimeTypeMatches,
@@ -536,6 +526,7 @@ var init_src = __esm({
536
526
  init_storage_types();
537
527
  init_storage_local();
538
528
  init_storage_s3();
529
+ init_storage_utils();
539
530
  init_mime_validator();
540
531
  }
541
532
  });
@@ -4344,11 +4335,11 @@ SwaggerUIBundle({
4344
4335
 
4345
4336
  // libs/server-core/src/lib/preview-renderer.ts
4346
4337
  function renderPreviewHTML(options) {
4347
- const { doc, collection } = options;
4338
+ const { doc, collection, customFieldRenderers } = options;
4348
4339
  const titleField = collection.admin?.useAsTitle ?? "id";
4349
4340
  const title = escapeHtml(String(doc[titleField] ?? doc["id"] ?? "Untitled"));
4350
4341
  const fields = collection.fields ?? [];
4351
- const fieldHtml = fields.filter((f) => !isHiddenField(f) && !isLayoutField(f) && f.name !== titleField).map((f) => renderField(f, doc)).filter(Boolean).join("\n");
4342
+ const fieldHtml = fields.filter((f) => !isHiddenField(f) && !isLayoutField(f) && f.name !== titleField).map((f) => renderField(f, doc, customFieldRenderers)).filter(Boolean).join("\n");
4352
4343
  const richTextFields = fields.filter((f) => f.type === "richText").map((f) => f.name);
4353
4344
  return `<!DOCTYPE html>
4354
4345
  <html lang="en">
@@ -4382,6 +4373,7 @@ ${fieldHtml}
4382
4373
  (function(){
4383
4374
  var richTextFields=${JSON.stringify(richTextFields)};
4384
4375
  window.addEventListener('message',function(e){
4376
+ if(e.origin!==window.location.origin)return;
4385
4377
  if(!e.data||e.data.type!=='momentum-preview-update')return;
4386
4378
  var d=e.data.data;if(!d)return;
4387
4379
  document.querySelectorAll('[data-field]').forEach(function(el){
@@ -4402,16 +4394,20 @@ if(titleEl){var tf=titleEl.getAttribute('data-field');if(d[tf]!==undefined)title
4402
4394
  </body>
4403
4395
  </html>`;
4404
4396
  }
4405
- function renderField(field, doc) {
4397
+ function renderField(field, doc, customRenderers) {
4406
4398
  const value = doc[field.name];
4407
4399
  if (value === void 0 || value === null) {
4408
4400
  return renderFieldWrapper(field, "");
4409
4401
  }
4402
+ const editorKey = field.admin?.editor;
4403
+ if (editorKey && customRenderers?.[editorKey]) {
4404
+ return renderFieldWrapper(field, customRenderers[editorKey](value, field));
4405
+ }
4410
4406
  switch (field.type) {
4411
4407
  case "richText":
4412
4408
  return renderFieldWrapper(
4413
4409
  field,
4414
- `<div class="field-value rich-text" data-field="${escapeHtml(field.name)}">${String(value)}</div>`
4410
+ `<div class="field-value rich-text" data-field="${escapeHtml(field.name)}">${escapeHtml(String(value))}</div>`
4415
4411
  );
4416
4412
  case "checkbox":
4417
4413
  return renderFieldWrapper(
@@ -4742,6 +4738,21 @@ function coerceCsvValue(value, fieldType) {
4742
4738
  }
4743
4739
 
4744
4740
  // libs/server-analog/src/lib/server-analog.ts
4741
+ function getEmailBuilderFieldName(collection) {
4742
+ const field = collection.fields.find(
4743
+ (f) => f.type === "json" && f.admin?.editor === "email-builder"
4744
+ );
4745
+ return field?.name;
4746
+ }
4747
+ async function renderEmailPreviewHTML(doc, blocksFieldName) {
4748
+ const emailPkg = "@momentumcms/email";
4749
+ const { renderEmailFromBlocks } = await import(emailPkg);
4750
+ const blocks2 = doc[blocksFieldName];
4751
+ if (!Array.isArray(blocks2) || blocks2.length === 0) {
4752
+ 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
+ }
4754
+ return renderEmailFromBlocks({ blocks: blocks2 });
4755
+ }
4745
4756
  function nestBracketParams(flat) {
4746
4757
  const result = {};
4747
4758
  for (const [key, value] of Object.entries(flat)) {
@@ -5426,26 +5437,47 @@ function createComprehensiveMomentumHandler(config) {
5426
5437
  };
5427
5438
  }
5428
5439
  }
5429
- if (seg2 === "preview" && seg1 && method === "GET") {
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
+ }
5430
5445
  try {
5431
5446
  const collectionSlug2 = seg0;
5432
5447
  const docId = seg1;
5433
- const contextApi = getContextualAPI(user);
5434
- const doc = await contextApi.collection(collectionSlug2).findById(docId);
5435
- if (!doc) {
5436
- utils.setResponseStatus(event, 404);
5437
- return { error: "Document not found" };
5438
- }
5439
5448
  const collectionConfig = config.collections.find((c) => c.slug === collectionSlug2);
5440
5449
  if (!collectionConfig) {
5441
5450
  utils.setResponseStatus(event, 404);
5442
5451
  return { error: "Collection not found" };
5443
5452
  }
5444
- const html = renderPreviewHTML({
5445
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- doc type from API
5446
- doc,
5447
- collection: collectionConfig
5448
- });
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 });
5449
5481
  utils.setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
5450
5482
  return utils.send(event, html);
5451
5483
  } catch (error) {
@@ -5714,12 +5746,13 @@ function createComprehensiveMomentumHandler(config) {
5714
5746
  fields[field.name] = field.data.toString("utf-8");
5715
5747
  }
5716
5748
  }
5749
+ const collectionUpload = postUploadCol.upload ?? {};
5717
5750
  const uploadRequest = {
5718
5751
  file,
5719
5752
  user,
5720
5753
  fields,
5721
5754
  collectionSlug: seg0,
5722
- collectionUpload: postUploadCol.upload
5755
+ collectionUpload
5723
5756
  };
5724
5757
  const response2 = await handleCollectionUpload(uploadConfig, uploadRequest);
5725
5758
  utils.setResponseStatus(event, response2.status);
package/index.js CHANGED
@@ -15,6 +15,35 @@ var init_storage_types = __esm({
15
15
  }
16
16
  });
17
17
 
18
+ // libs/storage/src/lib/storage-utils.ts
19
+ function getExtensionFromMimeType(mimeType) {
20
+ return MIME_TO_EXT[mimeType] ?? "";
21
+ }
22
+ var MIME_TO_EXT;
23
+ var init_storage_utils = __esm({
24
+ "libs/storage/src/lib/storage-utils.ts"() {
25
+ "use strict";
26
+ MIME_TO_EXT = {
27
+ "image/jpeg": ".jpg",
28
+ "image/png": ".png",
29
+ "image/gif": ".gif",
30
+ "image/webp": ".webp",
31
+ "image/svg+xml": ".svg",
32
+ "application/pdf": ".pdf",
33
+ "application/json": ".json",
34
+ "text/plain": ".txt",
35
+ "text/html": ".html",
36
+ "text/css": ".css",
37
+ "application/javascript": ".js",
38
+ "video/mp4": ".mp4",
39
+ "video/webm": ".webm",
40
+ "audio/mpeg": ".mp3",
41
+ "audio/wav": ".wav",
42
+ "application/zip": ".zip"
43
+ };
44
+ }
45
+ });
46
+
18
47
  // libs/storage/src/lib/storage-local.ts
19
48
  import { existsSync, mkdirSync, writeFileSync, unlinkSync, readFileSync, lstatSync } from "node:fs";
20
49
  import { join, extname, resolve, normalize } from "node:path";
@@ -84,30 +113,10 @@ function localStorageAdapter(options) {
84
113
  }
85
114
  };
86
115
  }
87
- function getExtensionFromMimeType(mimeType) {
88
- const mimeToExt = {
89
- "image/jpeg": ".jpg",
90
- "image/png": ".png",
91
- "image/gif": ".gif",
92
- "image/webp": ".webp",
93
- "image/svg+xml": ".svg",
94
- "application/pdf": ".pdf",
95
- "application/json": ".json",
96
- "text/plain": ".txt",
97
- "text/html": ".html",
98
- "text/css": ".css",
99
- "application/javascript": ".js",
100
- "video/mp4": ".mp4",
101
- "video/webm": ".webm",
102
- "audio/mpeg": ".mp3",
103
- "audio/wav": ".wav",
104
- "application/zip": ".zip"
105
- };
106
- return mimeToExt[mimeType] ?? "";
107
- }
108
116
  var init_storage_local = __esm({
109
117
  "libs/storage/src/lib/storage-local.ts"() {
110
118
  "use strict";
119
+ init_storage_utils();
111
120
  }
112
121
  });
113
122
 
@@ -171,7 +180,7 @@ function s3StorageAdapter(options) {
171
180
  return {
172
181
  async upload(file, uploadOptions) {
173
182
  const s3 = await getClient();
174
- const ext = extname2(file.originalName) || getExtensionFromMimeType2(file.mimeType);
183
+ const ext = extname2(file.originalName) || getExtensionFromMimeType(file.mimeType);
175
184
  const filename = uploadOptions?.filename ? `${uploadOptions.filename}${ext}` : `${randomUUID2()}${ext}`;
176
185
  const key = uploadOptions?.directory ? `${uploadOptions.directory}/${filename}` : filename;
177
186
  await s3.send(
@@ -260,31 +269,11 @@ function s3StorageAdapter(options) {
260
269
  }
261
270
  };
262
271
  }
263
- function getExtensionFromMimeType2(mimeType) {
264
- const mimeToExt = {
265
- "image/jpeg": ".jpg",
266
- "image/png": ".png",
267
- "image/gif": ".gif",
268
- "image/webp": ".webp",
269
- "image/svg+xml": ".svg",
270
- "application/pdf": ".pdf",
271
- "application/json": ".json",
272
- "text/plain": ".txt",
273
- "text/html": ".html",
274
- "text/css": ".css",
275
- "application/javascript": ".js",
276
- "video/mp4": ".mp4",
277
- "video/webm": ".webm",
278
- "audio/mpeg": ".mp3",
279
- "audio/wav": ".wav",
280
- "application/zip": ".zip"
281
- };
282
- return mimeToExt[mimeType] ?? "";
283
- }
284
272
  var S3Client, PutObjectCommand, DeleteObjectCommand, HeadObjectCommand, GetObjectCommand, getSignedUrl;
285
273
  var init_storage_s3 = __esm({
286
274
  "libs/storage/src/lib/storage-s3.ts"() {
287
275
  "use strict";
276
+ init_storage_utils();
288
277
  }
289
278
  });
290
279
 
@@ -305,7 +294,7 @@ function detectMimeType(buffer) {
305
294
  if (match) {
306
295
  if (sig.bytes[0] === 82 && sig.bytes[1] === 73) {
307
296
  if (buffer.length >= 12) {
308
- const formatId = buffer.slice(8, 12).toString("ascii");
297
+ const formatId = String.fromCharCode(...buffer.subarray(8, 12));
309
298
  if (formatId === "WEBP") {
310
299
  return "image/webp";
311
300
  }
@@ -318,7 +307,7 @@ function detectMimeType(buffer) {
318
307
  }
319
308
  }
320
309
  if (sig.mimeType === "video/mp4" && buffer.length >= 8) {
321
- const boxType = buffer.slice(4, 8).toString("ascii");
310
+ const boxType = String.fromCharCode(...buffer.subarray(4, 8));
322
311
  if (boxType === "ftyp") {
323
312
  return "video/mp4";
324
313
  }
@@ -327,7 +316,7 @@ function detectMimeType(buffer) {
327
316
  }
328
317
  }
329
318
  if (isTextContent(buffer)) {
330
- const text2 = buffer.toString("utf8", 0, Math.min(buffer.length, 1e3));
319
+ const text2 = new TextDecoder().decode(buffer.subarray(0, Math.min(buffer.length, 1e3)));
331
320
  if (text2.trim().startsWith("{") || text2.trim().startsWith("[")) {
332
321
  return "application/json";
333
322
  }
@@ -501,6 +490,7 @@ var init_mime_validator = __esm({
501
490
  var src_exports = {};
502
491
  __export(src_exports, {
503
492
  detectMimeType: () => detectMimeType,
493
+ getExtensionFromMimeType: () => getExtensionFromMimeType,
504
494
  isMimeTypeAllowed: () => isMimeTypeAllowed,
505
495
  localStorageAdapter: () => localStorageAdapter,
506
496
  mimeTypeMatches: () => mimeTypeMatches,
@@ -513,6 +503,7 @@ var init_src = __esm({
513
503
  init_storage_types();
514
504
  init_storage_local();
515
505
  init_storage_s3();
506
+ init_storage_utils();
516
507
  init_mime_validator();
517
508
  }
518
509
  });
@@ -4325,11 +4316,11 @@ SwaggerUIBundle({
4325
4316
 
4326
4317
  // libs/server-core/src/lib/preview-renderer.ts
4327
4318
  function renderPreviewHTML(options) {
4328
- const { doc, collection } = options;
4319
+ const { doc, collection, customFieldRenderers } = options;
4329
4320
  const titleField = collection.admin?.useAsTitle ?? "id";
4330
4321
  const title = escapeHtml(String(doc[titleField] ?? doc["id"] ?? "Untitled"));
4331
4322
  const fields = collection.fields ?? [];
4332
- const fieldHtml = fields.filter((f) => !isHiddenField(f) && !isLayoutField(f) && f.name !== titleField).map((f) => renderField(f, doc)).filter(Boolean).join("\n");
4323
+ const fieldHtml = fields.filter((f) => !isHiddenField(f) && !isLayoutField(f) && f.name !== titleField).map((f) => renderField(f, doc, customFieldRenderers)).filter(Boolean).join("\n");
4333
4324
  const richTextFields = fields.filter((f) => f.type === "richText").map((f) => f.name);
4334
4325
  return `<!DOCTYPE html>
4335
4326
  <html lang="en">
@@ -4363,6 +4354,7 @@ ${fieldHtml}
4363
4354
  (function(){
4364
4355
  var richTextFields=${JSON.stringify(richTextFields)};
4365
4356
  window.addEventListener('message',function(e){
4357
+ if(e.origin!==window.location.origin)return;
4366
4358
  if(!e.data||e.data.type!=='momentum-preview-update')return;
4367
4359
  var d=e.data.data;if(!d)return;
4368
4360
  document.querySelectorAll('[data-field]').forEach(function(el){
@@ -4383,16 +4375,20 @@ if(titleEl){var tf=titleEl.getAttribute('data-field');if(d[tf]!==undefined)title
4383
4375
  </body>
4384
4376
  </html>`;
4385
4377
  }
4386
- function renderField(field, doc) {
4378
+ function renderField(field, doc, customRenderers) {
4387
4379
  const value = doc[field.name];
4388
4380
  if (value === void 0 || value === null) {
4389
4381
  return renderFieldWrapper(field, "");
4390
4382
  }
4383
+ const editorKey = field.admin?.editor;
4384
+ if (editorKey && customRenderers?.[editorKey]) {
4385
+ return renderFieldWrapper(field, customRenderers[editorKey](value, field));
4386
+ }
4391
4387
  switch (field.type) {
4392
4388
  case "richText":
4393
4389
  return renderFieldWrapper(
4394
4390
  field,
4395
- `<div class="field-value rich-text" data-field="${escapeHtml(field.name)}">${String(value)}</div>`
4391
+ `<div class="field-value rich-text" data-field="${escapeHtml(field.name)}">${escapeHtml(String(value))}</div>`
4396
4392
  );
4397
4393
  case "checkbox":
4398
4394
  return renderFieldWrapper(
@@ -4723,6 +4719,21 @@ function coerceCsvValue(value, fieldType) {
4723
4719
  }
4724
4720
 
4725
4721
  // libs/server-analog/src/lib/server-analog.ts
4722
+ function getEmailBuilderFieldName(collection) {
4723
+ const field = collection.fields.find(
4724
+ (f) => f.type === "json" && f.admin?.editor === "email-builder"
4725
+ );
4726
+ return field?.name;
4727
+ }
4728
+ async function renderEmailPreviewHTML(doc, blocksFieldName) {
4729
+ const emailPkg = "@momentumcms/email";
4730
+ const { renderEmailFromBlocks } = await import(emailPkg);
4731
+ const blocks2 = doc[blocksFieldName];
4732
+ if (!Array.isArray(blocks2) || blocks2.length === 0) {
4733
+ 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
+ }
4735
+ return renderEmailFromBlocks({ blocks: blocks2 });
4736
+ }
4726
4737
  function nestBracketParams(flat) {
4727
4738
  const result = {};
4728
4739
  for (const [key, value] of Object.entries(flat)) {
@@ -5407,26 +5418,47 @@ function createComprehensiveMomentumHandler(config) {
5407
5418
  };
5408
5419
  }
5409
5420
  }
5410
- if (seg2 === "preview" && seg1 && method === "GET") {
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
+ }
5411
5426
  try {
5412
5427
  const collectionSlug2 = seg0;
5413
5428
  const docId = seg1;
5414
- const contextApi = getContextualAPI(user);
5415
- const doc = await contextApi.collection(collectionSlug2).findById(docId);
5416
- if (!doc) {
5417
- utils.setResponseStatus(event, 404);
5418
- return { error: "Document not found" };
5419
- }
5420
5429
  const collectionConfig = config.collections.find((c) => c.slug === collectionSlug2);
5421
5430
  if (!collectionConfig) {
5422
5431
  utils.setResponseStatus(event, 404);
5423
5432
  return { error: "Collection not found" };
5424
5433
  }
5425
- const html = renderPreviewHTML({
5426
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- doc type from API
5427
- doc,
5428
- collection: collectionConfig
5429
- });
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 });
5430
5462
  utils.setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
5431
5463
  return utils.send(event, html);
5432
5464
  } catch (error) {
@@ -5695,12 +5727,13 @@ function createComprehensiveMomentumHandler(config) {
5695
5727
  fields[field.name] = field.data.toString("utf-8");
5696
5728
  }
5697
5729
  }
5730
+ const collectionUpload = postUploadCol.upload ?? {};
5698
5731
  const uploadRequest = {
5699
5732
  file,
5700
5733
  user,
5701
5734
  fields,
5702
5735
  collectionSlug: seg0,
5703
- collectionUpload: postUploadCol.upload
5736
+ collectionUpload
5704
5737
  };
5705
5738
  const response2 = await handleCollectionUpload(uploadConfig, uploadRequest);
5706
5739
  utils.setResponseStatus(event, response2.status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momentumcms/server-analog",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Nitro/h3 adapter for Momentum CMS with Analog.js support",
5
5
  "license": "MIT",
6
6
  "author": "Momentum CMS Contributors",
@@ -28,7 +28,8 @@
28
28
  "types": "./src/index.d.ts",
29
29
  "peerDependencies": {
30
30
  "@momentumcms/core": ">=0.0.1",
31
- "@momentumcms/server-core": ">=0.0.1"
31
+ "@momentumcms/server-core": ">=0.0.1",
32
+ "@momentumcms/storage": ">=0.0.1"
32
33
  },
33
34
  "dependencies": {
34
35
  "@aws-sdk/client-s3": "^3.983.0",
@@ -43,7 +43,7 @@ export type ReadMultipartFormDataFn = (event: H3Event) => Promise<Array<{
43
43
  /**
44
44
  * Type for send function from h3.
45
45
  */
46
- export type SendFn = (event: H3Event, data: Buffer | string, type?: string) => unknown;
46
+ export type SendFn = (event: H3Event, data: Buffer | Uint8Array | string, type?: string) => unknown;
47
47
  /**
48
48
  * Extended h3 utilities for comprehensive API handling.
49
49
  */
@@ -59,7 +59,7 @@ export interface MomentumH3Utils {
59
59
  type?: string;
60
60
  data: Buffer;
61
61
  }> | undefined>;
62
- send(event: H3Event, data: Buffer | string, type?: string): unknown;
62
+ send(event: H3Event, data: Buffer | Uint8Array | string, type?: string): unknown;
63
63
  }
64
64
  /**
65
65
  * Creates an h3 event handler for Momentum CMS API.