@nitrogenbuilder/connector-payload 0.1.43 → 0.1.44

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.
@@ -3,8 +3,14 @@
3
3
  *
4
4
  * The plugin registers collections at init time via `registerCollection()`.
5
5
  */
6
+ type RegisteredCollection = {
7
+ collectionSlug: string;
8
+ routePattern?: string;
9
+ };
6
10
  export declare function registerCollection(alias: string, collectionSlug: string, routePattern?: string): void;
7
11
  export declare function resolveCollection(postType: string): string;
8
12
  export declare function resolveCollectionRoutePattern(postType: string): string | undefined;
13
+ export declare function getRegisteredCollections(): RegisteredCollection[];
9
14
  export declare function buildRelativePermalink(postType: string, slug: string): string;
10
15
  export declare function buildPermalink(postType: string, slug: string, frontendUrl?: string): string;
16
+ export {};
@@ -33,6 +33,16 @@ export function resolveCollection(postType) {
33
33
  export function resolveCollectionRoutePattern(postType) {
34
34
  return registeredCollections.get(postType)?.routePattern;
35
35
  }
36
+ export function getRegisteredCollections() {
37
+ const collectionsBySlug = new Map();
38
+ for (const collection of registeredCollections.values()) {
39
+ const existing = collectionsBySlug.get(collection.collectionSlug);
40
+ if (!existing || (!existing.routePattern && collection.routePattern)) {
41
+ collectionsBySlug.set(collection.collectionSlug, collection);
42
+ }
43
+ }
44
+ return Array.from(collectionsBySlug.values());
45
+ }
36
46
  export function buildRelativePermalink(postType, slug) {
37
47
  return buildRelativePath(resolveCollectionRoutePattern(postType), slug);
38
48
  }
@@ -1,4 +1,4 @@
1
- import { getNitrogenSettings, buildDynamicData, buildPageResponse, buildListItemResponse, getTemplateForType, requireAuth, } from './helpers.js';
1
+ import { getNitrogenSettings, buildDynamicData, buildResolvedPageResponse, buildResolvedListItemResponse, getTemplateForType, requireAuth, } from './helpers.js';
2
2
  /**
3
3
  * Creates a complete set of CRUD endpoints for a given Payload collection.
4
4
  *
@@ -28,7 +28,7 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
28
28
  limit: 0,
29
29
  depth: 1,
30
30
  });
31
- const items = result.docs.map((doc) => buildListItemResponse(doc, settings, collectionSlug));
31
+ const items = await Promise.all(result.docs.map((doc) => buildResolvedListItemResponse(payload, doc, settings, collectionSlug)));
32
32
  return Response.json(items);
33
33
  },
34
34
  },
@@ -75,8 +75,9 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
75
75
  getTemplateForType(payload, collectionSlug),
76
76
  ]);
77
77
  const dynamicData = buildDynamicData(doc, settings);
78
+ const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData, collectionSlug);
78
79
  return Response.json({
79
- ...buildPageResponse(doc, settings, dynamicData, collectionSlug),
80
+ ...response,
80
81
  template: pageTemplate,
81
82
  headerTemplate,
82
83
  footerTemplate,
@@ -190,8 +191,9 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
190
191
  }
191
192
  const doc = result.docs[0];
192
193
  const dynamicData = buildDynamicData(doc, settings);
194
+ const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData, collectionSlug);
193
195
  return Response.json({
194
- ...buildPageResponse(doc, settings, dynamicData, collectionSlug),
196
+ ...response,
195
197
  template: pageTemplate,
196
198
  headerTemplate,
197
199
  footerTemplate,
@@ -217,8 +219,9 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
217
219
  }
218
220
  const doc = result.docs[0];
219
221
  const dynamicData = buildDynamicData(doc, settings);
222
+ const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData, collectionSlug);
220
223
  return Response.json({
221
- ...buildPageResponse(doc, settings, dynamicData, collectionSlug),
224
+ ...response,
222
225
  template: pageTemplate,
223
226
  headerTemplate,
224
227
  footerTemplate,
@@ -0,0 +1,3 @@
1
+ import type { Payload } from 'payload';
2
+ import type { NitrogenSettingsGlobal } from '../types.js';
3
+ export declare function resolveCtrlLinksInNitrogenData<T>(payload: Payload, value: T, settings: NitrogenSettingsGlobal): Promise<T>;
@@ -0,0 +1,228 @@
1
+ import { buildRelativePermalink, getRegisteredCollections, } from '../collection-registry.js';
2
+ function isRecord(value) {
3
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
4
+ }
5
+ function getString(value) {
6
+ return typeof value === 'string' ? value : '';
7
+ }
8
+ function getId(value) {
9
+ if (typeof value === 'string' || typeof value === 'number')
10
+ return value;
11
+ return null;
12
+ }
13
+ function cloneJson(value) {
14
+ if (value === undefined)
15
+ return value;
16
+ return JSON.parse(JSON.stringify(value));
17
+ }
18
+ function normalizeRelativePath(value) {
19
+ const trimmed = value.trim();
20
+ if (!trimmed || trimmed === '/home')
21
+ return '/';
22
+ try {
23
+ const parsed = new URL(trimmed);
24
+ return normalizeRelativePath(`${parsed.pathname}${parsed.search}${parsed.hash}`);
25
+ }
26
+ catch {
27
+ // Treat as already relative.
28
+ }
29
+ const [path = '', suffix = ''] = trimmed.split(/([?#].*)/, 2);
30
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`;
31
+ const withoutTrailingSlash = normalizedPath.length > 1 ? normalizedPath.replace(/\/+$/, '') : normalizedPath;
32
+ return `${withoutTrailingSlash}${suffix}`;
33
+ }
34
+ function getPathname(value) {
35
+ const normalized = normalizeRelativePath(value);
36
+ return normalized.split(/[?#]/)[0] || '/';
37
+ }
38
+ function matchRoutePattern(pathname, routePattern) {
39
+ if (!routePattern)
40
+ return 1;
41
+ const pattern = getPathname(routePattern);
42
+ const placeholderMatch = pattern.match(/\[(?:\.\.\.)?slug\]/);
43
+ if (placeholderMatch) {
44
+ const base = pattern.slice(0, placeholderMatch.index).replace(/\/+$/, '');
45
+ if (!base) {
46
+ return pathname === '/' ? 0 : 20;
47
+ }
48
+ return pathname.startsWith(`${base}/`) ? 100 + base.length : 0;
49
+ }
50
+ if (pathname === pattern)
51
+ return 90 + pattern.length;
52
+ return pathname.startsWith(`${pattern}/`) ? 50 + pattern.length : 0;
53
+ }
54
+ function getCollectionCandidates(link, internalRef) {
55
+ const explicitCollection = getString(internalRef.collection) ||
56
+ getString(internalRef.collectionSlug) ||
57
+ getString(link.collection) ||
58
+ getString(link.collectionSlug);
59
+ const savedPath = getString(internalRef.relativePermalink) ||
60
+ getString(link.url) ||
61
+ getString(internalRef.slug);
62
+ const pathname = getPathname(savedPath);
63
+ const candidates = new Map();
64
+ if (explicitCollection) {
65
+ candidates.set(explicitCollection, Number.MAX_SAFE_INTEGER);
66
+ }
67
+ for (const collection of getRegisteredCollections()) {
68
+ const score = matchRoutePattern(pathname, collection.routePattern);
69
+ if (score <= 0)
70
+ continue;
71
+ candidates.set(collection.collectionSlug, Math.max(candidates.get(collection.collectionSlug) || 0, score));
72
+ }
73
+ if (!candidates.has('pages')) {
74
+ candidates.set('pages', 1);
75
+ }
76
+ return Array.from(candidates.entries())
77
+ .sort((a, b) => b[1] - a[1])
78
+ .map(([collection]) => collection);
79
+ }
80
+ function getDocumentRelativePermalink(doc, collectionSlug) {
81
+ const breadcrumbs = doc.breadcrumbs;
82
+ const lastUrl = Array.isArray(breadcrumbs)
83
+ ? breadcrumbs.at(-1)?.url
84
+ : undefined;
85
+ if (typeof lastUrl === 'string' && lastUrl.trim()) {
86
+ return normalizeRelativePath(lastUrl);
87
+ }
88
+ return buildRelativePermalink(collectionSlug, String(doc.slug || ''));
89
+ }
90
+ function getDocumentPermalink(doc, settings, collectionSlug) {
91
+ const relativePermalink = getDocumentRelativePermalink(doc, collectionSlug);
92
+ const baseUrl = (settings.frontendUrl || '').replace(/\/$/, '');
93
+ return baseUrl ? `${baseUrl}${relativePermalink}` : relativePermalink;
94
+ }
95
+ function getPayloadBaseUrl(settings) {
96
+ return (process.env.NEXT_PUBLIC_PAYLOAD_API_URL ||
97
+ process.env.NEXT_PUBLIC_SERVER_URL ||
98
+ settings.nitrogenEditorUrl ||
99
+ settings.instanceUrl ||
100
+ '').replace(/\/$/, '');
101
+ }
102
+ function resolveMediaUrl(url, settings) {
103
+ if (!url || /^https?:\/\//i.test(url))
104
+ return url;
105
+ const baseUrl = getPayloadBaseUrl(settings);
106
+ const relativeUrl = url.startsWith('/') ? url : `/${url}`;
107
+ return baseUrl ? `${baseUrl}${relativeUrl}` : relativeUrl;
108
+ }
109
+ async function findInternalTarget(context, link, internalRef) {
110
+ const entryId = getId(internalRef.entryId);
111
+ if (entryId === null)
112
+ return null;
113
+ const candidates = getCollectionCandidates(link, internalRef);
114
+ for (const collection of candidates) {
115
+ try {
116
+ const doc = (await context.payload.findByID({
117
+ collection: collection,
118
+ id: entryId,
119
+ depth: 1,
120
+ disableErrors: true,
121
+ }));
122
+ if (!doc)
123
+ continue;
124
+ return {
125
+ slug: String(doc.slug || internalRef.slug || ''),
126
+ relativePermalink: getDocumentRelativePermalink(doc, collection),
127
+ url: getDocumentPermalink(doc, context.settings, collection),
128
+ };
129
+ }
130
+ catch {
131
+ // Try the next candidate collection.
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+ async function resolveInternalLink(context, link, internalRef) {
137
+ const entryId = getId(internalRef.entryId);
138
+ if (entryId === null)
139
+ return null;
140
+ const key = [
141
+ entryId,
142
+ getString(internalRef.relativePermalink),
143
+ getString(link.url),
144
+ getString(internalRef.collection),
145
+ getString(link.collection),
146
+ ].join(':');
147
+ if (!context.internalLinks.has(key)) {
148
+ context.internalLinks.set(key, findInternalTarget(context, link, internalRef));
149
+ }
150
+ return context.internalLinks.get(key);
151
+ }
152
+ async function findMediaTarget(context, mediaRef) {
153
+ const mediaId = getId(mediaRef.mediaId);
154
+ if (mediaId === null)
155
+ return null;
156
+ try {
157
+ const doc = (await context.payload.findByID({
158
+ collection: 'media',
159
+ id: mediaId,
160
+ depth: 0,
161
+ disableErrors: true,
162
+ }));
163
+ const url = doc?.url || '';
164
+ if (!doc || !url)
165
+ return null;
166
+ return {
167
+ filename: doc.filename || getString(mediaRef.filename),
168
+ mimeType: doc.mimeType || getString(mediaRef.mimeType),
169
+ url: resolveMediaUrl(url, context.settings),
170
+ };
171
+ }
172
+ catch {
173
+ return null;
174
+ }
175
+ }
176
+ async function resolveMediaLink(context, mediaRef) {
177
+ const mediaId = getId(mediaRef.mediaId);
178
+ if (mediaId === null)
179
+ return null;
180
+ const key = String(mediaId);
181
+ if (!context.mediaLinks.has(key)) {
182
+ context.mediaLinks.set(key, findMediaTarget(context, mediaRef));
183
+ }
184
+ return context.mediaLinks.get(key);
185
+ }
186
+ async function resolveLinkRecord(context, link) {
187
+ if (link.type === 'internal' && isRecord(link.internalRef)) {
188
+ const resolved = await resolveInternalLink(context, link, link.internalRef);
189
+ if (!resolved)
190
+ return;
191
+ link.url = resolved.url;
192
+ link.internalRef.slug = resolved.slug;
193
+ link.internalRef.relativePermalink = resolved.relativePermalink;
194
+ return;
195
+ }
196
+ if (link.type === 'media' && isRecord(link.mediaRef)) {
197
+ const resolved = await resolveMediaLink(context, link.mediaRef);
198
+ if (!resolved)
199
+ return;
200
+ link.url = resolved.url;
201
+ link.mediaRef.url = resolved.url;
202
+ link.mediaRef.filename = resolved.filename;
203
+ link.mediaRef.mimeType = resolved.mimeType;
204
+ }
205
+ }
206
+ async function walk(context, value) {
207
+ if (Array.isArray(value)) {
208
+ await Promise.all(value.map((item) => walk(context, item)));
209
+ return;
210
+ }
211
+ if (!isRecord(value))
212
+ return;
213
+ await resolveLinkRecord(context, value);
214
+ await Promise.all(Object.values(value).map((item) => walk(context, item)));
215
+ }
216
+ export async function resolveCtrlLinksInNitrogenData(payload, value, settings) {
217
+ if (!value)
218
+ return value;
219
+ const cloned = cloneJson(value);
220
+ const context = {
221
+ internalLinks: new Map(),
222
+ mediaLinks: new Map(),
223
+ payload,
224
+ settings,
225
+ };
226
+ await walk(context, cloned);
227
+ return cloned;
228
+ }
@@ -31,6 +31,29 @@ export declare function buildPageResponse(doc: NitrogenPageDoc | NitrogenTemplat
31
31
  wysiwygColors: {};
32
32
  };
33
33
  };
34
+ export declare function buildResolvedPageResponse(payload: Payload, doc: NitrogenPageDoc | NitrogenTemplateDoc, settings: NitrogenSettingsGlobal, dynamicData: JsonObject, collectionSlug?: string): Promise<{
35
+ id: string | number;
36
+ token: string;
37
+ title: string;
38
+ author: string | number;
39
+ slug: string;
40
+ permalink: string;
41
+ relative_permalink: string;
42
+ content: string;
43
+ dynamic_data: JsonObject;
44
+ settings: JsonObject;
45
+ nitrogen_settings: {
46
+ variables?: Record<string, {
47
+ type?: string;
48
+ value?: string | boolean;
49
+ }> | Array<{
50
+ name: string;
51
+ value?: string;
52
+ }>;
53
+ urlMaps: never[];
54
+ wysiwygColors: {};
55
+ };
56
+ }>;
34
57
  export declare function buildListItemResponse(doc: NitrogenPageDoc | NitrogenTemplateDoc, settings: NitrogenSettingsGlobal, collectionSlug?: string): {
35
58
  id: string | number;
36
59
  title: string;
@@ -51,6 +74,26 @@ export declare function buildListItemResponse(doc: NitrogenPageDoc | NitrogenTem
51
74
  wysiwygColors: {};
52
75
  };
53
76
  };
77
+ export declare function buildResolvedListItemResponse(payload: Payload, doc: NitrogenPageDoc | NitrogenTemplateDoc, settings: NitrogenSettingsGlobal, collectionSlug?: string): Promise<{
78
+ id: string | number;
79
+ title: string;
80
+ slug: string;
81
+ permalink: string;
82
+ relative_permalink: string;
83
+ content: string;
84
+ settings: JsonObject;
85
+ nitrogen_settings: {
86
+ variables?: Record<string, {
87
+ type?: string;
88
+ value?: string | boolean;
89
+ }> | Array<{
90
+ name: string;
91
+ value?: string;
92
+ }>;
93
+ urlMaps: never[];
94
+ wysiwygColors: {};
95
+ };
96
+ }>;
54
97
  export declare function buildMediaItemResponse(doc: MediaDoc): {
55
98
  id: string | number;
56
99
  filename: string;
@@ -1,4 +1,5 @@
1
1
  import { buildRelativePermalink } from '../collection-registry.js';
2
+ import { resolveCtrlLinksInNitrogenData } from './ctrlLinkResolver.js';
2
3
  function normalizeRelativePath(value) {
3
4
  const trimmed = value.trim();
4
5
  if (!trimmed || trimmed === '/home')
@@ -95,6 +96,13 @@ export function buildPageResponse(doc, settings, dynamicData, collectionSlug = '
95
96
  },
96
97
  };
97
98
  }
99
+ export async function buildResolvedPageResponse(payload, doc, settings, dynamicData, collectionSlug = 'pages') {
100
+ const response = buildPageResponse(doc, settings, dynamicData, collectionSlug);
101
+ if (doc.nitrogenData) {
102
+ response.content = JSON.stringify(await resolveCtrlLinksInNitrogenData(payload, doc.nitrogenData, settings));
103
+ }
104
+ return response;
105
+ }
98
106
  export function buildListItemResponse(doc, settings, collectionSlug = 'pages') {
99
107
  const pageSettings = 'pageSettings' in doc ? doc.pageSettings : undefined;
100
108
  const templateSettings = 'templateSettings' in doc ? doc.templateSettings : undefined;
@@ -114,6 +122,13 @@ export function buildListItemResponse(doc, settings, collectionSlug = 'pages') {
114
122
  },
115
123
  };
116
124
  }
125
+ export async function buildResolvedListItemResponse(payload, doc, settings, collectionSlug = 'pages') {
126
+ const response = buildListItemResponse(doc, settings, collectionSlug);
127
+ if (doc.nitrogenData) {
128
+ response.content = JSON.stringify(await resolveCtrlLinksInNitrogenData(payload, doc.nitrogenData, settings));
129
+ }
130
+ return response;
131
+ }
117
132
  export function buildMediaItemResponse(doc) {
118
133
  const filename = doc.filename || '';
119
134
  const ext = filename.includes('.') ? `.${filename.split('.').pop()}` : '';
@@ -197,9 +212,13 @@ export async function getTemplateForType(payload, type) {
197
212
  const doc = result.docs[0];
198
213
  if (!doc)
199
214
  return null;
215
+ const settings = await getNitrogenSettings(payload);
216
+ const resolvedNitrogenData = doc.nitrogenData
217
+ ? await resolveCtrlLinksInNitrogenData(payload, doc.nitrogenData, settings)
218
+ : null;
200
219
  return {
201
220
  ID: doc.id,
202
- content: doc.nitrogenData ? JSON.stringify(doc.nitrogenData) : '[]',
221
+ content: resolvedNitrogenData ? JSON.stringify(resolvedNitrogenData) : '[]',
203
222
  };
204
223
  }
205
224
  catch {
@@ -1,4 +1,4 @@
1
- import { getNitrogenSettings, buildDynamicData, buildPageResponse, buildListItemResponse, getDocumentPermalink, getDocumentRelativePermalink, getTemplateForType, requireAuth, } from './helpers.js';
1
+ import { getNitrogenSettings, buildDynamicData, buildResolvedPageResponse, buildResolvedListItemResponse, getDocumentPermalink, getDocumentRelativePermalink, getTemplateForType, requireAuth, } from './helpers.js';
2
2
  function isContentTemplateCollection(collection) {
3
3
  return !!collection && collection !== 'header' && collection !== 'footer';
4
4
  }
@@ -60,7 +60,7 @@ export const templatesEndpoints = [
60
60
  // Collection may not exist — use template's own data
61
61
  }
62
62
  }
63
- const response = buildPageResponse(doc, settings, dynamicData);
63
+ const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData);
64
64
  const previewTarget = getTemplatePreviewTarget(doc, settings, associatedCollection, associatedDoc);
65
65
  return Response.json({
66
66
  ...response,
@@ -99,11 +99,14 @@ export const templatesEndpoints = [
99
99
  limit: 0,
100
100
  depth: 1,
101
101
  });
102
- const items = result.docs.map((doc) => ({
103
- ...buildListItemResponse(doc, settings),
104
- permalink: `${(settings.frontendUrl || '').replace(/\/$/, '')}/nitrogen-templates/${doc.slug}`,
105
- relative_permalink: `/nitrogen-templates/${doc.slug}`,
106
- postType: doc.associatedCollection || '',
102
+ const items = await Promise.all(result.docs.map(async (doc) => {
103
+ const response = await buildResolvedListItemResponse(payload, doc, settings);
104
+ return {
105
+ ...response,
106
+ permalink: `${(settings.frontendUrl || '').replace(/\/$/, '')}/nitrogen-templates/${doc.slug}`,
107
+ relative_permalink: `/nitrogen-templates/${doc.slug}`,
108
+ postType: doc.associatedCollection || '',
109
+ };
107
110
  }));
108
111
  return Response.json(items);
109
112
  },
@@ -147,7 +150,7 @@ export const templatesEndpoints = [
147
150
  // Collection may not exist — use template's own data
148
151
  }
149
152
  }
150
- const response = buildPageResponse(doc, settings, dynamicData);
153
+ const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData);
151
154
  const previewTarget = getTemplatePreviewTarget(doc, settings, associatedCollection, associatedDoc);
152
155
  return Response.json({
153
156
  ...response,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitrogenbuilder/connector-payload",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
4
4
  "description": "Nitrogen page builder connector plugin for Payload CMS 3.x",
5
5
  "author": "Leonardo Dentzien <leo@torchmedia.ca>",
6
6
  "type": "module",