@momentumcms/server-analog 0.5.0 → 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/index.cjs CHANGED
@@ -317,7 +317,7 @@ function detectMimeType(buffer) {
317
317
  if (match) {
318
318
  if (sig.bytes[0] === 82 && sig.bytes[1] === 73) {
319
319
  if (buffer.length >= 12) {
320
- const formatId = buffer.slice(8, 12).toString("ascii");
320
+ const formatId = String.fromCharCode(...buffer.subarray(8, 12));
321
321
  if (formatId === "WEBP") {
322
322
  return "image/webp";
323
323
  }
@@ -330,7 +330,7 @@ function detectMimeType(buffer) {
330
330
  }
331
331
  }
332
332
  if (sig.mimeType === "video/mp4" && buffer.length >= 8) {
333
- const boxType = buffer.slice(4, 8).toString("ascii");
333
+ const boxType = String.fromCharCode(...buffer.subarray(4, 8));
334
334
  if (boxType === "ftyp") {
335
335
  return "video/mp4";
336
336
  }
@@ -339,7 +339,7 @@ function detectMimeType(buffer) {
339
339
  }
340
340
  }
341
341
  if (isTextContent(buffer)) {
342
- 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)));
343
343
  if (text2.trim().startsWith("{") || text2.trim().startsWith("[")) {
344
344
  return "application/json";
345
345
  }
@@ -4335,11 +4335,11 @@ SwaggerUIBundle({
4335
4335
 
4336
4336
  // libs/server-core/src/lib/preview-renderer.ts
4337
4337
  function renderPreviewHTML(options) {
4338
- const { doc, collection } = options;
4338
+ const { doc, collection, customFieldRenderers } = options;
4339
4339
  const titleField = collection.admin?.useAsTitle ?? "id";
4340
4340
  const title = escapeHtml(String(doc[titleField] ?? doc["id"] ?? "Untitled"));
4341
4341
  const fields = collection.fields ?? [];
4342
- 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");
4343
4343
  const richTextFields = fields.filter((f) => f.type === "richText").map((f) => f.name);
4344
4344
  return `<!DOCTYPE html>
4345
4345
  <html lang="en">
@@ -4373,6 +4373,7 @@ ${fieldHtml}
4373
4373
  (function(){
4374
4374
  var richTextFields=${JSON.stringify(richTextFields)};
4375
4375
  window.addEventListener('message',function(e){
4376
+ if(e.origin!==window.location.origin)return;
4376
4377
  if(!e.data||e.data.type!=='momentum-preview-update')return;
4377
4378
  var d=e.data.data;if(!d)return;
4378
4379
  document.querySelectorAll('[data-field]').forEach(function(el){
@@ -4393,16 +4394,20 @@ if(titleEl){var tf=titleEl.getAttribute('data-field');if(d[tf]!==undefined)title
4393
4394
  </body>
4394
4395
  </html>`;
4395
4396
  }
4396
- function renderField(field, doc) {
4397
+ function renderField(field, doc, customRenderers) {
4397
4398
  const value = doc[field.name];
4398
4399
  if (value === void 0 || value === null) {
4399
4400
  return renderFieldWrapper(field, "");
4400
4401
  }
4402
+ const editorKey = field.admin?.editor;
4403
+ if (editorKey && customRenderers?.[editorKey]) {
4404
+ return renderFieldWrapper(field, customRenderers[editorKey](value, field));
4405
+ }
4401
4406
  switch (field.type) {
4402
4407
  case "richText":
4403
4408
  return renderFieldWrapper(
4404
4409
  field,
4405
- `<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>`
4406
4411
  );
4407
4412
  case "checkbox":
4408
4413
  return renderFieldWrapper(
@@ -4733,6 +4738,21 @@ function coerceCsvValue(value, fieldType) {
4733
4738
  }
4734
4739
 
4735
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
+ }
4736
4756
  function nestBracketParams(flat) {
4737
4757
  const result = {};
4738
4758
  for (const [key, value] of Object.entries(flat)) {
@@ -5417,26 +5437,47 @@ function createComprehensiveMomentumHandler(config) {
5417
5437
  };
5418
5438
  }
5419
5439
  }
5420
- 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
+ }
5421
5445
  try {
5422
5446
  const collectionSlug2 = seg0;
5423
5447
  const docId = seg1;
5424
- const contextApi = getContextualAPI(user);
5425
- const doc = await contextApi.collection(collectionSlug2).findById(docId);
5426
- if (!doc) {
5427
- utils.setResponseStatus(event, 404);
5428
- return { error: "Document not found" };
5429
- }
5430
5448
  const collectionConfig = config.collections.find((c) => c.slug === collectionSlug2);
5431
5449
  if (!collectionConfig) {
5432
5450
  utils.setResponseStatus(event, 404);
5433
5451
  return { error: "Collection not found" };
5434
5452
  }
5435
- const html = renderPreviewHTML({
5436
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- doc type from API
5437
- doc,
5438
- collection: collectionConfig
5439
- });
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 });
5440
5481
  utils.setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
5441
5482
  return utils.send(event, html);
5442
5483
  } catch (error) {
@@ -5705,12 +5746,13 @@ function createComprehensiveMomentumHandler(config) {
5705
5746
  fields[field.name] = field.data.toString("utf-8");
5706
5747
  }
5707
5748
  }
5749
+ const collectionUpload = postUploadCol.upload ?? {};
5708
5750
  const uploadRequest = {
5709
5751
  file,
5710
5752
  user,
5711
5753
  fields,
5712
5754
  collectionSlug: seg0,
5713
- collectionUpload: postUploadCol.upload
5755
+ collectionUpload
5714
5756
  };
5715
5757
  const response2 = await handleCollectionUpload(uploadConfig, uploadRequest);
5716
5758
  utils.setResponseStatus(event, response2.status);
package/index.js CHANGED
@@ -294,7 +294,7 @@ function detectMimeType(buffer) {
294
294
  if (match) {
295
295
  if (sig.bytes[0] === 82 && sig.bytes[1] === 73) {
296
296
  if (buffer.length >= 12) {
297
- const formatId = buffer.slice(8, 12).toString("ascii");
297
+ const formatId = String.fromCharCode(...buffer.subarray(8, 12));
298
298
  if (formatId === "WEBP") {
299
299
  return "image/webp";
300
300
  }
@@ -307,7 +307,7 @@ function detectMimeType(buffer) {
307
307
  }
308
308
  }
309
309
  if (sig.mimeType === "video/mp4" && buffer.length >= 8) {
310
- const boxType = buffer.slice(4, 8).toString("ascii");
310
+ const boxType = String.fromCharCode(...buffer.subarray(4, 8));
311
311
  if (boxType === "ftyp") {
312
312
  return "video/mp4";
313
313
  }
@@ -316,7 +316,7 @@ function detectMimeType(buffer) {
316
316
  }
317
317
  }
318
318
  if (isTextContent(buffer)) {
319
- 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)));
320
320
  if (text2.trim().startsWith("{") || text2.trim().startsWith("[")) {
321
321
  return "application/json";
322
322
  }
@@ -4316,11 +4316,11 @@ SwaggerUIBundle({
4316
4316
 
4317
4317
  // libs/server-core/src/lib/preview-renderer.ts
4318
4318
  function renderPreviewHTML(options) {
4319
- const { doc, collection } = options;
4319
+ const { doc, collection, customFieldRenderers } = options;
4320
4320
  const titleField = collection.admin?.useAsTitle ?? "id";
4321
4321
  const title = escapeHtml(String(doc[titleField] ?? doc["id"] ?? "Untitled"));
4322
4322
  const fields = collection.fields ?? [];
4323
- 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");
4324
4324
  const richTextFields = fields.filter((f) => f.type === "richText").map((f) => f.name);
4325
4325
  return `<!DOCTYPE html>
4326
4326
  <html lang="en">
@@ -4354,6 +4354,7 @@ ${fieldHtml}
4354
4354
  (function(){
4355
4355
  var richTextFields=${JSON.stringify(richTextFields)};
4356
4356
  window.addEventListener('message',function(e){
4357
+ if(e.origin!==window.location.origin)return;
4357
4358
  if(!e.data||e.data.type!=='momentum-preview-update')return;
4358
4359
  var d=e.data.data;if(!d)return;
4359
4360
  document.querySelectorAll('[data-field]').forEach(function(el){
@@ -4374,16 +4375,20 @@ if(titleEl){var tf=titleEl.getAttribute('data-field');if(d[tf]!==undefined)title
4374
4375
  </body>
4375
4376
  </html>`;
4376
4377
  }
4377
- function renderField(field, doc) {
4378
+ function renderField(field, doc, customRenderers) {
4378
4379
  const value = doc[field.name];
4379
4380
  if (value === void 0 || value === null) {
4380
4381
  return renderFieldWrapper(field, "");
4381
4382
  }
4383
+ const editorKey = field.admin?.editor;
4384
+ if (editorKey && customRenderers?.[editorKey]) {
4385
+ return renderFieldWrapper(field, customRenderers[editorKey](value, field));
4386
+ }
4382
4387
  switch (field.type) {
4383
4388
  case "richText":
4384
4389
  return renderFieldWrapper(
4385
4390
  field,
4386
- `<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>`
4387
4392
  );
4388
4393
  case "checkbox":
4389
4394
  return renderFieldWrapper(
@@ -4714,6 +4719,21 @@ function coerceCsvValue(value, fieldType) {
4714
4719
  }
4715
4720
 
4716
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
+ }
4717
4737
  function nestBracketParams(flat) {
4718
4738
  const result = {};
4719
4739
  for (const [key, value] of Object.entries(flat)) {
@@ -5398,26 +5418,47 @@ function createComprehensiveMomentumHandler(config) {
5398
5418
  };
5399
5419
  }
5400
5420
  }
5401
- 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
+ }
5402
5426
  try {
5403
5427
  const collectionSlug2 = seg0;
5404
5428
  const docId = seg1;
5405
- const contextApi = getContextualAPI(user);
5406
- const doc = await contextApi.collection(collectionSlug2).findById(docId);
5407
- if (!doc) {
5408
- utils.setResponseStatus(event, 404);
5409
- return { error: "Document not found" };
5410
- }
5411
5429
  const collectionConfig = config.collections.find((c) => c.slug === collectionSlug2);
5412
5430
  if (!collectionConfig) {
5413
5431
  utils.setResponseStatus(event, 404);
5414
5432
  return { error: "Collection not found" };
5415
5433
  }
5416
- const html = renderPreviewHTML({
5417
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- doc type from API
5418
- doc,
5419
- collection: collectionConfig
5420
- });
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 });
5421
5462
  utils.setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
5422
5463
  return utils.send(event, html);
5423
5464
  } catch (error) {
@@ -5686,12 +5727,13 @@ function createComprehensiveMomentumHandler(config) {
5686
5727
  fields[field.name] = field.data.toString("utf-8");
5687
5728
  }
5688
5729
  }
5730
+ const collectionUpload = postUploadCol.upload ?? {};
5689
5731
  const uploadRequest = {
5690
5732
  file,
5691
5733
  user,
5692
5734
  fields,
5693
5735
  collectionSlug: seg0,
5694
- collectionUpload: postUploadCol.upload
5736
+ collectionUpload
5695
5737
  };
5696
5738
  const response2 = await handleCollectionUpload(uploadConfig, uploadRequest);
5697
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.5.0",
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.