@nitrogenbuilder/connector-payload 0.1.43 → 0.1.50
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/dist/collection-registry.d.ts +6 -0
- package/dist/collection-registry.js +10 -0
- package/dist/components/NitrogenAgentCredential.d.ts +1 -0
- package/dist/components/NitrogenAgentCredential.js +6 -0
- package/dist/components/NitrogenAgentCredentialRuntime.d.ts +10 -0
- package/dist/components/NitrogenAgentCredentialRuntime.js +136 -0
- package/dist/editor/NitrogenEditorPage.d.ts +0 -1
- package/dist/editor/NitrogenEditorPage.js +10 -30
- package/dist/endpoints/agent-auth.d.ts +31 -0
- package/dist/endpoints/agent-auth.js +210 -0
- package/dist/endpoints/batch.js +33 -1
- package/dist/endpoints/collection-endpoints.js +24 -11
- package/dist/endpoints/ctrlLinkResolver.d.ts +3 -0
- package/dist/endpoints/ctrlLinkResolver.js +228 -0
- package/dist/endpoints/helpers.d.ts +56 -0
- package/dist/endpoints/helpers.js +66 -1
- package/dist/endpoints/sitemap.d.ts +14 -0
- package/dist/endpoints/sitemap.js +109 -0
- package/dist/endpoints/templateConditions.d.ts +52 -0
- package/dist/endpoints/templateConditions.js +297 -0
- package/dist/endpoints/templates.js +11 -8
- package/dist/globals/NitrogenSettings.js +8 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +58 -0
- package/package.json +6 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getNitrogenSettings, buildDynamicData,
|
|
1
|
+
import { getNitrogenSettings, buildDynamicData, buildResolvedPageResponse, buildResolvedListItemResponse, getTemplateForType, requireAuth, canServeDocument, } from './helpers.js';
|
|
2
|
+
import { resolveTemplateForDocument } from './templateConditions.js';
|
|
2
3
|
/**
|
|
3
4
|
* Creates a complete set of CRUD endpoints for a given Payload collection.
|
|
4
5
|
*
|
|
@@ -28,7 +29,7 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
28
29
|
limit: 0,
|
|
29
30
|
depth: 1,
|
|
30
31
|
});
|
|
31
|
-
const items = result.docs.map((doc) =>
|
|
32
|
+
const items = await Promise.all(result.docs.map((doc) => buildResolvedListItemResponse(payload, doc, settings, collectionSlug)));
|
|
32
33
|
return Response.json(items);
|
|
33
34
|
},
|
|
34
35
|
},
|
|
@@ -67,16 +68,20 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
67
68
|
const { payload, routeParams } = req;
|
|
68
69
|
const id = routeParams?.id;
|
|
69
70
|
try {
|
|
70
|
-
const [doc, settings, headerTemplate, footerTemplate
|
|
71
|
+
const [doc, settings, headerTemplate, footerTemplate] = await Promise.all([
|
|
71
72
|
payload.findByID({ collection, id, depth: 1 }),
|
|
72
73
|
getNitrogenSettings(payload),
|
|
73
74
|
getTemplateForType(payload, 'header'),
|
|
74
75
|
getTemplateForType(payload, 'footer'),
|
|
75
|
-
getTemplateForType(payload, collectionSlug),
|
|
76
76
|
]);
|
|
77
|
+
if (!canServeDocument(req, doc, settings)) {
|
|
78
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
79
|
+
}
|
|
80
|
+
const pageTemplate = await resolveTemplateForDocument(payload, doc, collectionSlug, settings);
|
|
77
81
|
const dynamicData = buildDynamicData(doc, settings);
|
|
82
|
+
const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData, collectionSlug);
|
|
78
83
|
return Response.json({
|
|
79
|
-
...
|
|
84
|
+
...response,
|
|
80
85
|
template: pageTemplate,
|
|
81
86
|
headerTemplate,
|
|
82
87
|
footerTemplate,
|
|
@@ -178,20 +183,24 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
178
183
|
if (!slug) {
|
|
179
184
|
return Response.json({ error: 'Slug is required' }, { status: 400 });
|
|
180
185
|
}
|
|
181
|
-
const [result, settings, headerTemplate, footerTemplate
|
|
186
|
+
const [result, settings, headerTemplate, footerTemplate] = await Promise.all([
|
|
182
187
|
payload.find({ collection, where: { slug: { equals: slug } }, limit: 1, depth: 1 }),
|
|
183
188
|
getNitrogenSettings(payload),
|
|
184
189
|
getTemplateForType(payload, 'header'),
|
|
185
190
|
getTemplateForType(payload, 'footer'),
|
|
186
|
-
getTemplateForType(payload, collectionSlug),
|
|
187
191
|
]);
|
|
188
192
|
if (!result.docs.length) {
|
|
189
193
|
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
190
194
|
}
|
|
191
195
|
const doc = result.docs[0];
|
|
196
|
+
if (!canServeDocument(req, doc, settings)) {
|
|
197
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
198
|
+
}
|
|
199
|
+
const pageTemplate = await resolveTemplateForDocument(payload, doc, collectionSlug, settings);
|
|
192
200
|
const dynamicData = buildDynamicData(doc, settings);
|
|
201
|
+
const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData, collectionSlug);
|
|
193
202
|
return Response.json({
|
|
194
|
-
...
|
|
203
|
+
...response,
|
|
195
204
|
template: pageTemplate,
|
|
196
205
|
headerTemplate,
|
|
197
206
|
footerTemplate,
|
|
@@ -205,20 +214,24 @@ export function createCollectionEndpoints(collectionSlug, endpointPrefix) {
|
|
|
205
214
|
handler: async (req) => {
|
|
206
215
|
const { payload, routeParams } = req;
|
|
207
216
|
const slug = routeParams?.slug;
|
|
208
|
-
const [result, settings, headerTemplate, footerTemplate
|
|
217
|
+
const [result, settings, headerTemplate, footerTemplate] = await Promise.all([
|
|
209
218
|
payload.find({ collection, where: { slug: { equals: slug } }, limit: 1, depth: 1 }),
|
|
210
219
|
getNitrogenSettings(payload),
|
|
211
220
|
getTemplateForType(payload, 'header'),
|
|
212
221
|
getTemplateForType(payload, 'footer'),
|
|
213
|
-
getTemplateForType(payload, collectionSlug),
|
|
214
222
|
]);
|
|
215
223
|
if (!result.docs.length) {
|
|
216
224
|
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
217
225
|
}
|
|
218
226
|
const doc = result.docs[0];
|
|
227
|
+
if (!canServeDocument(req, doc, settings)) {
|
|
228
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
229
|
+
}
|
|
230
|
+
const pageTemplate = await resolveTemplateForDocument(payload, doc, collectionSlug, settings);
|
|
219
231
|
const dynamicData = buildDynamicData(doc, settings);
|
|
232
|
+
const response = await buildResolvedPageResponse(payload, doc, settings, dynamicData, collectionSlug);
|
|
220
233
|
return Response.json({
|
|
221
|
-
...
|
|
234
|
+
...response,
|
|
222
235
|
template: pageTemplate,
|
|
223
236
|
headerTemplate,
|
|
224
237
|
footerTemplate,
|
|
@@ -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;
|
|
@@ -98,3 +141,16 @@ export declare function getNitrogenSettings(payload: Payload): Promise<NitrogenS
|
|
|
98
141
|
*/
|
|
99
142
|
export declare function getTemplateForType(payload: Payload, type: string): Promise<TemplateRef | null>;
|
|
100
143
|
export declare function requireAuth(req: PayloadRequest): Response | null;
|
|
144
|
+
/**
|
|
145
|
+
* True when the request carries the connector token (the `x-nitrogen-token`
|
|
146
|
+
* header, or `nitrogen-token` query param for iframe URLs) matching the stored
|
|
147
|
+
* `connectorToken` setting. Lets the staging frontend / editor preview draft &
|
|
148
|
+
* private docs without a logged-in session. Mirrors WP `preview_token_valid`.
|
|
149
|
+
*/
|
|
150
|
+
export declare function previewTokenValid(req: PayloadRequest, settings: NitrogenSettingsGlobal): boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Whether a document may be served for a given request. Published docs are
|
|
153
|
+
* public; non-published (draft/pending/private) docs require either a logged-in
|
|
154
|
+
* user or a valid preview token. Mirrors the WP status gate in `get_item`.
|
|
155
|
+
*/
|
|
156
|
+
export declare function canServeDocument(req: PayloadRequest, doc: Record<string, unknown> | null | undefined, settings: NitrogenSettingsGlobal): boolean;
|
|
@@ -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:
|
|
221
|
+
content: resolvedNitrogenData ? JSON.stringify(resolvedNitrogenData) : '[]',
|
|
203
222
|
};
|
|
204
223
|
}
|
|
205
224
|
catch {
|
|
@@ -212,3 +231,49 @@ export function requireAuth(req) {
|
|
|
212
231
|
}
|
|
213
232
|
return null;
|
|
214
233
|
}
|
|
234
|
+
// Constant-time string compare in pure JS — deliberately avoids importing a
|
|
235
|
+
// node builtin. Payload's tsx-based `migrate` bin transpiles the config graph
|
|
236
|
+
// and chokes when esbuild normalizes a builtin import to `node:crypto`
|
|
237
|
+
// (ENOENT on `node:crypto?tsx-namespace=…`), so nothing reachable from the
|
|
238
|
+
// config at load time may statically import `crypto`.
|
|
239
|
+
function timingSafeStringEqual(a, b) {
|
|
240
|
+
if (a.length !== b.length)
|
|
241
|
+
return false;
|
|
242
|
+
let mismatch = 0;
|
|
243
|
+
for (let i = 0; i < a.length; i++) {
|
|
244
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
245
|
+
}
|
|
246
|
+
return mismatch === 0;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* True when the request carries the connector token (the `x-nitrogen-token`
|
|
250
|
+
* header, or `nitrogen-token` query param for iframe URLs) matching the stored
|
|
251
|
+
* `connectorToken` setting. Lets the staging frontend / editor preview draft &
|
|
252
|
+
* private docs without a logged-in session. Mirrors WP `preview_token_valid`.
|
|
253
|
+
*/
|
|
254
|
+
export function previewTokenValid(req, settings) {
|
|
255
|
+
const expected = settings.connectorToken;
|
|
256
|
+
if (!expected)
|
|
257
|
+
return false;
|
|
258
|
+
let token = req.headers?.get?.('x-nitrogen-token') || '';
|
|
259
|
+
if (!token) {
|
|
260
|
+
const url = new URL(req.url || '', 'http://localhost');
|
|
261
|
+
token = url.searchParams.get('nitrogen-token') || '';
|
|
262
|
+
}
|
|
263
|
+
if (!token)
|
|
264
|
+
return false;
|
|
265
|
+
return timingSafeStringEqual(String(expected), token);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Whether a document may be served for a given request. Published docs are
|
|
269
|
+
* public; non-published (draft/pending/private) docs require either a logged-in
|
|
270
|
+
* user or a valid preview token. Mirrors the WP status gate in `get_item`.
|
|
271
|
+
*/
|
|
272
|
+
export function canServeDocument(req, doc, settings) {
|
|
273
|
+
if (!doc)
|
|
274
|
+
return false;
|
|
275
|
+
const status = String(doc._status ?? doc.status ?? 'published');
|
|
276
|
+
if (status === 'published')
|
|
277
|
+
return true;
|
|
278
|
+
return !!req.user || previewTokenValid(req, settings);
|
|
279
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap endpoints — ported from the WordPress connector
|
|
3
|
+
* (rest-routes.php: get_sitemap_index, get_sitemap_for_post_type,
|
|
4
|
+
* build_sitemap_urls_for_post_type, resolve_sitemap_locs_for_post).
|
|
5
|
+
*
|
|
6
|
+
* PORTING NOTE: in WP a single post could emit MULTIPLE `loc`s — one per
|
|
7
|
+
* nitrogen_template matching the post, each contributing its own `url_prefix`
|
|
8
|
+
* (so a post served at both canonical and prefixed URLs appeared multiple
|
|
9
|
+
* times). The TARGET (Payload) model has exactly one `routePattern` per
|
|
10
|
+
* registered collection, so we emit ONE canonical URL per document via
|
|
11
|
+
* `buildRelativePermalink`. Per-template prefix multiplexing is deferred.
|
|
12
|
+
*/
|
|
13
|
+
import type { Endpoint } from 'payload';
|
|
14
|
+
export declare const sitemapEndpoints: Endpoint[];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap endpoints — ported from the WordPress connector
|
|
3
|
+
* (rest-routes.php: get_sitemap_index, get_sitemap_for_post_type,
|
|
4
|
+
* build_sitemap_urls_for_post_type, resolve_sitemap_locs_for_post).
|
|
5
|
+
*
|
|
6
|
+
* PORTING NOTE: in WP a single post could emit MULTIPLE `loc`s — one per
|
|
7
|
+
* nitrogen_template matching the post, each contributing its own `url_prefix`
|
|
8
|
+
* (so a post served at both canonical and prefixed URLs appeared multiple
|
|
9
|
+
* times). The TARGET (Payload) model has exactly one `routePattern` per
|
|
10
|
+
* registered collection, so we emit ONE canonical URL per document via
|
|
11
|
+
* `buildRelativePermalink`. Per-template prefix multiplexing is deferred.
|
|
12
|
+
*/
|
|
13
|
+
import { buildRelativePermalink, getRegisteredCollections, resolveCollection, } from '../collection-registry.js';
|
|
14
|
+
import { findAllDocs } from '../inventory/indexing.js';
|
|
15
|
+
/** Internal Nitrogen collections that must never appear in the sitemap. */
|
|
16
|
+
const EXCLUDED_COLLECTIONS = new Set([
|
|
17
|
+
'nitrogen-templates',
|
|
18
|
+
'nitrogen-component-catalog',
|
|
19
|
+
'nitrogen-component-usage',
|
|
20
|
+
]);
|
|
21
|
+
/**
|
|
22
|
+
* Fetch published docs for a collection selecting only `slug` and `updatedAt`.
|
|
23
|
+
*
|
|
24
|
+
* Payload collections may or may not have drafts enabled (the `_status`
|
|
25
|
+
* field). We first try filtering by `_status: 'published'`; if that throws
|
|
26
|
+
* (collection has no `_status`), we fall back to fetching all docs.
|
|
27
|
+
*/
|
|
28
|
+
async function fetchPublishedSitemapDocs(payload, collection) {
|
|
29
|
+
const select = { slug: true, updatedAt: true };
|
|
30
|
+
try {
|
|
31
|
+
return await findAllDocs(payload, collection, {
|
|
32
|
+
where: { _status: { equals: 'published' } },
|
|
33
|
+
select,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return findAllDocs(payload, collection, { select });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Coerce a doc's updatedAt into an ISO-8601 string, or null when absent. */
|
|
41
|
+
function toIsoString(value) {
|
|
42
|
+
if (!value)
|
|
43
|
+
return null;
|
|
44
|
+
const date = new Date(value);
|
|
45
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
46
|
+
}
|
|
47
|
+
export const sitemapEndpoints = [
|
|
48
|
+
// GET /nitrogen/v1/sitemap — sitemap index (mirror get_sitemap_index)
|
|
49
|
+
{
|
|
50
|
+
path: '/nitrogen/v1/sitemap',
|
|
51
|
+
method: 'get',
|
|
52
|
+
handler: async (req) => {
|
|
53
|
+
const { payload } = req;
|
|
54
|
+
const postTypes = [];
|
|
55
|
+
for (const { collectionSlug } of getRegisteredCollections()) {
|
|
56
|
+
if (EXCLUDED_COLLECTIONS.has(collectionSlug))
|
|
57
|
+
continue;
|
|
58
|
+
let docs;
|
|
59
|
+
try {
|
|
60
|
+
docs = await fetchPublishedSitemapDocs(payload, collectionSlug);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// One bad collection shouldn't 500 the whole index — skip it.
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (docs.length === 0)
|
|
67
|
+
continue;
|
|
68
|
+
let lastmod = null;
|
|
69
|
+
for (const doc of docs) {
|
|
70
|
+
const iso = toIsoString(doc.updatedAt);
|
|
71
|
+
if (iso !== null && (lastmod === null || iso > lastmod)) {
|
|
72
|
+
lastmod = iso;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
postTypes.push({ post_type: collectionSlug, lastmod });
|
|
76
|
+
}
|
|
77
|
+
return Response.json({ post_types: postTypes });
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
// GET /nitrogen/v1/sitemap/:post_type — URLs for one post type
|
|
81
|
+
// (mirror get_sitemap_for_post_type)
|
|
82
|
+
{
|
|
83
|
+
path: '/nitrogen/v1/sitemap/:post_type',
|
|
84
|
+
method: 'get',
|
|
85
|
+
handler: async (req) => {
|
|
86
|
+
const { payload, routeParams } = req;
|
|
87
|
+
const postType = String(routeParams?.post_type || '');
|
|
88
|
+
const collection = resolveCollection(postType);
|
|
89
|
+
if (EXCLUDED_COLLECTIONS.has(collection)) {
|
|
90
|
+
return Response.json({ error: 'Post type not eligible for sitemap' }, { status: 404 });
|
|
91
|
+
}
|
|
92
|
+
let docs;
|
|
93
|
+
try {
|
|
94
|
+
docs = await fetchPublishedSitemapDocs(payload, collection);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return Response.json({ error: 'No URLs for this post type' }, { status: 404 });
|
|
98
|
+
}
|
|
99
|
+
const urls = docs.map((doc) => ({
|
|
100
|
+
loc: buildRelativePermalink(postType, String(doc.slug || '')),
|
|
101
|
+
lastmod: toIsoString(doc.updatedAt),
|
|
102
|
+
}));
|
|
103
|
+
if (urls.length === 0) {
|
|
104
|
+
return Response.json({ error: 'No URLs for this post type' }, { status: 404 });
|
|
105
|
+
}
|
|
106
|
+
return Response.json({ urls });
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template Conditions — TypeScript port of the WordPress
|
|
3
|
+
* `Nitrogen\TemplateConditions` scoring engine
|
|
4
|
+
* (nitrogen-connector/includes/template-conditions.php).
|
|
5
|
+
*
|
|
6
|
+
* Resolves the best-matching nitrogen-template for a document by scoring
|
|
7
|
+
* editor-authored condition groups (OR'd groups, AND'd conditions within a
|
|
8
|
+
* group) and falling back to a legacy associatedCollection match.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is pure/typed and defensive: the evaluators never throw —
|
|
11
|
+
* malformed input simply yields a non-match (false).
|
|
12
|
+
*/
|
|
13
|
+
import type { Payload } from 'payload';
|
|
14
|
+
import type { JsonObject, NitrogenPageDoc, NitrogenTemplateDoc, NitrogenSettingsGlobal } from '../types.js';
|
|
15
|
+
import { type TemplateRef } from './helpers.js';
|
|
16
|
+
/**
|
|
17
|
+
* Evaluation context for a single document.
|
|
18
|
+
*
|
|
19
|
+
* `dynamic` holds the buildDynamicData() namespace; `post` is the raw document.
|
|
20
|
+
* The dynamic_data evaluator resolves its dot-path against both (dynamic first,
|
|
21
|
+
* then post) so editor-authored paths match leniently.
|
|
22
|
+
*/
|
|
23
|
+
export interface TemplateContext {
|
|
24
|
+
post: NitrogenPageDoc | NitrogenTemplateDoc | Record<string, unknown>;
|
|
25
|
+
postType: string;
|
|
26
|
+
postId: string;
|
|
27
|
+
dynamic: JsonObject;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Evaluate authored conditions against a context (php:45-100).
|
|
31
|
+
*
|
|
32
|
+
* Groups are OR'd; conditions within a group are AND'd. An `exclude` flag
|
|
33
|
+
* inverts a condition's result. An unknown condition type fails the whole group.
|
|
34
|
+
*
|
|
35
|
+
* Returns the matched-condition count (score) of the first matching group, or
|
|
36
|
+
* false. If there are no conditionGroups, returns false (the caller handles the
|
|
37
|
+
* legacy fallback).
|
|
38
|
+
*/
|
|
39
|
+
export declare function evaluateConditions(templateConditions: unknown, context: TemplateContext): number | false;
|
|
40
|
+
/**
|
|
41
|
+
* Build the evaluation context for a document (php:187-216, Payload-adapted).
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildTemplateContext(doc: NitrogenPageDoc | NitrogenTemplateDoc, collectionSlug: string, settings: NitrogenSettingsGlobal): TemplateContext;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the best-matching template for a document
|
|
46
|
+
* (php:140-182 resolve_template + php:106-132 evaluate_legacy).
|
|
47
|
+
*
|
|
48
|
+
* Returns a { ID, content } ref with ctrl-links resolved and content
|
|
49
|
+
* JSON-stringified — the same shape getTemplateForType() returns — or null if
|
|
50
|
+
* no template matches.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveTemplateForDocument(payload: Payload, doc: NitrogenPageDoc | NitrogenTemplateDoc, collectionSlug: string, settings: NitrogenSettingsGlobal): Promise<TemplateRef | null>;
|