@enonic/nextjs-adapter 0.0.1-alpha.2 → 0.0.1-alpha.4
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/ComponentRegistry.d.ts +61 -0
- package/ComponentRegistry.js +115 -0
- package/UrlProcessor.d.ts +21 -0
- package/UrlProcessor.js +54 -0
- package/baseMappings.d.ts +1 -0
- package/baseMappings.js +38 -0
- package/guillotine/fetchContent.d.ts +82 -0
- package/guillotine/fetchContent.js +644 -0
- package/guillotine/getMetaData.d.ts +91 -0
- package/guillotine/getMetaData.js +128 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/utils.d.ts +64 -0
- package/utils.js +146 -0
- package/views/BaseComponent.d.ts +25 -0
- package/views/BaseComponent.jsx +114 -0
- package/views/BaseContent.d.ts +4 -0
- package/views/BaseContent.jsx +41 -0
- package/views/BaseLayout.d.ts +17 -0
- package/views/BaseLayout.jsx +24 -0
- package/views/BaseMacro.d.ts +16 -0
- package/views/BaseMacro.jsx +85 -0
- package/views/BasePage.d.ts +19 -0
- package/views/BasePage.jsx +34 -0
- package/views/BasePart.d.ts +19 -0
- package/views/BasePart.jsx +24 -0
- package/views/Empty.d.ts +3 -0
- package/views/Empty.jsx +3 -0
- package/views/Fragment.d.ts +10 -0
- package/views/Fragment.jsx +25 -0
- package/views/MainView.d.ts +4 -0
- package/views/MainView.jsx +14 -0
- package/views/Region.d.ts +20 -0
- package/views/Region.jsx +57 -0
- package/views/RichTextView.d.ts +15 -0
- package/views/RichTextView.jsx +55 -0
- package/views/SingleComponent.d.ts +5 -0
- package/views/SingleComponent.jsx +11 -0
- package/views/Text.d.ts +9 -0
- package/views/Text.jsx +4 -0
- package/views/macros/DefaultMacro.d.ts +4 -0
- package/views/macros/DefaultMacro.jsx +3 -0
- package/views/macros/DisableMacro.d.ts +3 -0
- package/views/macros/DisableMacro.jsx +2 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { getMetaQuery, pageFragmentQuery, } from './getMetaData';
|
|
11
|
+
import adapterConstants, { APP_NAME, APP_NAME_DASHED, FRAGMENT_CONTENTTYPE_NAME, FRAGMENT_DEFAULT_REGION_NAME, getContentApiUrl, getXpBaseUrl, IS_DEV_MODE, PAGE_TEMPLATE_CONTENTTYPE_NAME, PAGE_TEMPLATE_FOLDER, RENDER_MODE, sanitizeGraphqlName, SITE_KEY, XP_COMPONENT_TYPE, XP_REQUEST_TYPE, } from '../utils';
|
|
12
|
+
import { ComponentRegistry } from '../ComponentRegistry';
|
|
13
|
+
const NO_PROPS_PROCESSOR = (props) => __awaiter(void 0, void 0, void 0, function* () { var _a; return (_a = yield props) !== null && _a !== void 0 ? _a : {}; });
|
|
14
|
+
const ALIAS_PREFIX = 'request';
|
|
15
|
+
const GUILLOTINE_QUERY_REGEXP = /^\s*query\s*(?:\((.*)*\))?\s*{\s*guillotine\s*{((?:.|\s)+)}\s*}\s*$/;
|
|
16
|
+
const GRAPHQL_FRAGMENTS_REGEXP = /fragment\s+.+\s+on\s+.+\s*{[\s\w{}().,:"'`]+}/;
|
|
17
|
+
/** Generic fetch */
|
|
18
|
+
export const fetchFromApi = (apiUrl, body, method = 'POST') => __awaiter(void 0, void 0, void 0, function* () {
|
|
19
|
+
const options = {
|
|
20
|
+
method,
|
|
21
|
+
headers: {
|
|
22
|
+
'Accept': 'application/json',
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'X-Guillotine-SiteKey': SITE_KEY,
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify(body),
|
|
27
|
+
};
|
|
28
|
+
let res;
|
|
29
|
+
try {
|
|
30
|
+
res = yield fetch(apiUrl, options);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.warn(apiUrl, e);
|
|
34
|
+
throw new Error(JSON.stringify({
|
|
35
|
+
code: 'API',
|
|
36
|
+
message: e.message,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error(JSON.stringify({
|
|
41
|
+
code: res.status,
|
|
42
|
+
message: `Data fetching failed (message: '${yield res.text()}')`,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
let json;
|
|
46
|
+
try {
|
|
47
|
+
json = yield res.json();
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
throw new Error(JSON.stringify({
|
|
51
|
+
code: 500,
|
|
52
|
+
message: `API call completed but with non-JSON data: ${JSON.stringify(yield res.text())}`,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
if (!json) {
|
|
56
|
+
throw new Error(JSON.stringify({
|
|
57
|
+
code: 500,
|
|
58
|
+
message: `API call completed but with unexpectedly empty data: ${JSON.stringify(yield res.text())}`,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
return json;
|
|
62
|
+
});
|
|
63
|
+
/** Guillotine-specialized fetch, using the generic fetch above */
|
|
64
|
+
export const fetchGuillotine = (contentApiUrl, body, xpContentPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
if (typeof body.query !== 'string' || !body.query.trim()) {
|
|
66
|
+
return {
|
|
67
|
+
error: {
|
|
68
|
+
code: '400',
|
|
69
|
+
message: `Invalid or missing query. JSON.stringify(query) = ${JSON.stringify(body.query)}`,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const result = yield fetchFromApi(contentApiUrl, body)
|
|
74
|
+
.then(json => {
|
|
75
|
+
let errors = (json || {}).errors;
|
|
76
|
+
if (errors) {
|
|
77
|
+
if (!Array.isArray(errors)) {
|
|
78
|
+
errors = [errors];
|
|
79
|
+
}
|
|
80
|
+
console.warn(`${errors.length} error(s) when trying to fetch data (path = ${JSON.stringify(xpContentPath)}):`);
|
|
81
|
+
errors.forEach(error => {
|
|
82
|
+
console.error(error);
|
|
83
|
+
});
|
|
84
|
+
console.warn(`Query:\n${body.query}`);
|
|
85
|
+
console.warn(`Variables: ${JSON.stringify(body.variables, null, 2)}`);
|
|
86
|
+
return {
|
|
87
|
+
error: {
|
|
88
|
+
code: '500',
|
|
89
|
+
message: `Server responded with ${errors.length} error(s), probably from guillotine - see log.`,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return json.data;
|
|
94
|
+
})
|
|
95
|
+
.catch((err) => {
|
|
96
|
+
console.warn(`Client-side error when trying to fetch data (path = ${JSON.stringify(xpContentPath)})`, err);
|
|
97
|
+
try {
|
|
98
|
+
return { error: JSON.parse(err.message) };
|
|
99
|
+
}
|
|
100
|
+
catch (e2) {
|
|
101
|
+
return { error: { code: 'Client-side error', message: err.message } };
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return result;
|
|
105
|
+
});
|
|
106
|
+
// /////////////////////////////////////////////////////////////////////////////// Specific fetch wrappers:
|
|
107
|
+
const fetchMetaData = (contentApiUrl, xpContentPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
108
|
+
var _b;
|
|
109
|
+
const body = {
|
|
110
|
+
query: getMetaQuery(pageFragmentQuery()),
|
|
111
|
+
variables: {
|
|
112
|
+
path: xpContentPath,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
const metaResult = yield fetchGuillotine(contentApiUrl, body, xpContentPath);
|
|
116
|
+
if (metaResult.error) {
|
|
117
|
+
return metaResult;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
return {
|
|
121
|
+
meta: (_b = metaResult === null || metaResult === void 0 ? void 0 : metaResult.guillotine) === null || _b === void 0 ? void 0 : _b.get,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const fetchContentData = (contentApiUrl, xpContentPath, query, variables) => __awaiter(void 0, void 0, void 0, function* () {
|
|
126
|
+
const body = { query };
|
|
127
|
+
if (variables && Object.keys(variables).length > 0) {
|
|
128
|
+
body.variables = variables;
|
|
129
|
+
}
|
|
130
|
+
const contentResults = yield fetchGuillotine(contentApiUrl, body, xpContentPath);
|
|
131
|
+
if (contentResults.error) {
|
|
132
|
+
return contentResults;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
return {
|
|
136
|
+
// omit the aliases and return values
|
|
137
|
+
contents: Object.values(contentResults),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// /////////////////////////////////////////////////////////////////////////////// Error checking:
|
|
142
|
+
/** Checks a site-relative contentPath as a slash-delimited string or a string array, and returns a pure site-relative path string (no double slashes, starts with a slash, does not end with one). */
|
|
143
|
+
const getCleanContentPathArrayOrThrow400 = (contentPath) => {
|
|
144
|
+
if (contentPath === undefined) {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
const isArray = Array.isArray(contentPath);
|
|
148
|
+
if (!isArray) {
|
|
149
|
+
if (typeof contentPath !== 'string') {
|
|
150
|
+
throw Error(JSON.stringify({
|
|
151
|
+
code: 400,
|
|
152
|
+
message: `Unexpected target content _path: contentPath must be a string or pure string array (contentPath=${JSON.stringify(contentPath)})`,
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
return contentPath;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
return (contentPath).join('/');
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
function parseComponentPath(contentType, path) {
|
|
162
|
+
const matches = [];
|
|
163
|
+
let match;
|
|
164
|
+
const myRegexp = /(?:(\w+)\/(\d+))+/g;
|
|
165
|
+
while ((match = myRegexp.exec(path)) !== null) {
|
|
166
|
+
matches.push({
|
|
167
|
+
region: match[1],
|
|
168
|
+
index: +match[2],
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (contentType === FRAGMENT_CONTENTTYPE_NAME) {
|
|
172
|
+
// there is no main region in fragment content and the root component has '/' path
|
|
173
|
+
// prepend FRAGMENT_DEFAULT_REGION_NAME to path to conform to page structure
|
|
174
|
+
matches.unshift({
|
|
175
|
+
region: FRAGMENT_DEFAULT_REGION_NAME,
|
|
176
|
+
index: 0,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return matches;
|
|
180
|
+
}
|
|
181
|
+
function getParentRegion(source, contentType, cmpPath, components = [], createMissing) {
|
|
182
|
+
let currentTree = source;
|
|
183
|
+
let currentRegion;
|
|
184
|
+
let parentPath = '';
|
|
185
|
+
for (let i = 0; i < cmpPath.length; i++) {
|
|
186
|
+
const pathFragment = cmpPath[i];
|
|
187
|
+
const regionName = pathFragment.region;
|
|
188
|
+
parentPath += `/${pathFragment.region}/${pathFragment.index}`;
|
|
189
|
+
currentRegion = currentTree[regionName];
|
|
190
|
+
if (!currentRegion) {
|
|
191
|
+
if (createMissing) {
|
|
192
|
+
currentRegion = {
|
|
193
|
+
name: regionName,
|
|
194
|
+
components: [],
|
|
195
|
+
};
|
|
196
|
+
currentTree[regionName] = currentRegion;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
throw `Region [${regionName}] was not found`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (i < cmpPath.length - 1) {
|
|
203
|
+
// look for layouts inside if this is not the last path fragment
|
|
204
|
+
const layout = components.find((cmp) => {
|
|
205
|
+
return cmp.type === XP_COMPONENT_TYPE.LAYOUT && prefixLayoutPath(contentType, cmp.path) === parentPath;
|
|
206
|
+
});
|
|
207
|
+
if (!layout) {
|
|
208
|
+
throw `Layout [${parentPath}] not found among components, but needed for component [${JSON.stringify(cmpPath, null, 2)}]`;
|
|
209
|
+
}
|
|
210
|
+
if (!layout.regions) {
|
|
211
|
+
layout.regions = {};
|
|
212
|
+
}
|
|
213
|
+
currentTree = layout.regions;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return currentRegion;
|
|
217
|
+
}
|
|
218
|
+
function prefixLayoutPath(contentType, path) {
|
|
219
|
+
if (contentType !== FRAGMENT_CONTENTTYPE_NAME) {
|
|
220
|
+
return path;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
// prepend FRAGMENT_DEFAULT_REGION_NAME to path to conform to page structure
|
|
224
|
+
// so that component with path '/' becomes /FRAGMENT_DEFAULT_REGION_NAME/0
|
|
225
|
+
// path /left/1 becomes /FRAGMENT_DEFAULT_REGION_NAME/0/left/1
|
|
226
|
+
return `/${FRAGMENT_DEFAULT_REGION_NAME}/0${path === '/' ? '' : path}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function buildPage(contentType, comps = []) {
|
|
230
|
+
let page = {
|
|
231
|
+
type: XP_COMPONENT_TYPE.PAGE,
|
|
232
|
+
path: '/',
|
|
233
|
+
};
|
|
234
|
+
const tree = {};
|
|
235
|
+
if (contentType === FRAGMENT_CONTENTTYPE_NAME) {
|
|
236
|
+
page.regions = tree;
|
|
237
|
+
}
|
|
238
|
+
comps.forEach(cmp => {
|
|
239
|
+
var _a;
|
|
240
|
+
const cmpPath = parseComponentPath(contentType, cmp.path);
|
|
241
|
+
let region;
|
|
242
|
+
if (cmp.path === '/' && cmp.type === XP_COMPONENT_TYPE.PAGE) {
|
|
243
|
+
// add page values to page object
|
|
244
|
+
page = Object.assign(page, cmp);
|
|
245
|
+
page.page.regions = tree;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
region = getParentRegion(tree, contentType, cmpPath, comps, true);
|
|
250
|
+
}
|
|
251
|
+
if (region) {
|
|
252
|
+
// getting the index of component from string like '/main/0/left/11'
|
|
253
|
+
const cmpIndex = (_a = cmpPath[cmpPath.length - 1]) === null || _a === void 0 ? void 0 : _a.index;
|
|
254
|
+
if (cmpIndex >= 0) {
|
|
255
|
+
region.components.splice(cmpIndex, 0, cmp);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
throw Error(`Could not find [${cmp.type}] component index at ${JSON.stringify(cmpPath, null, 2)}, rendering not possible.`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
return page;
|
|
263
|
+
}
|
|
264
|
+
function combineMultipleQueries(queriesWithVars) {
|
|
265
|
+
const queries = [];
|
|
266
|
+
const fragments = [];
|
|
267
|
+
const superVars = {};
|
|
268
|
+
const superParams = [];
|
|
269
|
+
queriesWithVars.forEach((componentDescriptor, index) => {
|
|
270
|
+
const queryAndVars = componentDescriptor.queryAndVariables;
|
|
271
|
+
if (!queryAndVars) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Extract fragments first if exist
|
|
275
|
+
let q = queryAndVars.query;
|
|
276
|
+
let match = q.match(GRAPHQL_FRAGMENTS_REGEXP);
|
|
277
|
+
if ((match === null || match === void 0 ? void 0 : match.length) === 1) {
|
|
278
|
+
// extract a fragment to put it at root level
|
|
279
|
+
fragments.push(match[0]);
|
|
280
|
+
// remove it from query because queries are going to get wrapped
|
|
281
|
+
q = q.replace(match[0], '');
|
|
282
|
+
}
|
|
283
|
+
// Extract graphql query and its params and add prefixes to exclude collisions with other queries
|
|
284
|
+
match = q.match(GUILLOTINE_QUERY_REGEXP);
|
|
285
|
+
let query = '';
|
|
286
|
+
if (match && match.length === 2) {
|
|
287
|
+
// no params, just query
|
|
288
|
+
query = match[1];
|
|
289
|
+
}
|
|
290
|
+
else if (match && match.length === 3) {
|
|
291
|
+
// both query and params are present
|
|
292
|
+
query = match[2];
|
|
293
|
+
// process args
|
|
294
|
+
const args = match[1];
|
|
295
|
+
if (args) {
|
|
296
|
+
args.split(',').forEach(originalParamString => {
|
|
297
|
+
const [originalKey, originalVal] = originalParamString.trim().split(':');
|
|
298
|
+
const [prefixedKey, prefixedVal] = [`$${ALIAS_PREFIX}${index}_${originalKey.substr(1)}`, originalVal];
|
|
299
|
+
superParams.push(`${prefixedKey}:${prefixedVal}`);
|
|
300
|
+
// also update param references in query itself !
|
|
301
|
+
// query = query.replaceAll(originalKey, prefixedKey);
|
|
302
|
+
// replaceAll is not supported in older nodejs versions
|
|
303
|
+
const origKeyPattern = new RegExp(originalKey.replace(/\$/g, '\\$'), 'g');
|
|
304
|
+
query = query.replace(origKeyPattern, prefixedKey);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (query.length) {
|
|
309
|
+
queries.push(`${ALIAS_PREFIX}${index}:guillotine {${query}}`);
|
|
310
|
+
}
|
|
311
|
+
// Update variables with the same prefixes
|
|
312
|
+
Object.entries(queryAndVars.variables || {}).forEach(entry => {
|
|
313
|
+
superVars[`${ALIAS_PREFIX}${index}_${entry[0]}`] = entry[1];
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
// Compose the super query
|
|
317
|
+
const superQuery = `query ${superParams.length ? `(${superParams.join(', ')})` : ''} {
|
|
318
|
+
${queries.join('\n')}
|
|
319
|
+
}
|
|
320
|
+
${fragments.join('\n')}
|
|
321
|
+
`;
|
|
322
|
+
return {
|
|
323
|
+
query: superQuery,
|
|
324
|
+
variables: superVars,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function applyProcessors(componentDescriptors, contentResults, context) {
|
|
328
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
329
|
+
let dataCounter = 0;
|
|
330
|
+
const processorPromises = componentDescriptors.map((desc) => __awaiter(this, void 0, void 0, function* () {
|
|
331
|
+
var _a;
|
|
332
|
+
// we're iterating component descriptors here
|
|
333
|
+
// some of them might not have provided graphql requests
|
|
334
|
+
// but we still need to run props processor for them
|
|
335
|
+
// in case they want to fetch their data from elsewhere
|
|
336
|
+
const propsProcessor = ((_a = desc.type) === null || _a === void 0 ? void 0 : _a.processor) || NO_PROPS_PROCESSOR;
|
|
337
|
+
let data;
|
|
338
|
+
if (desc.queryAndVariables) {
|
|
339
|
+
// if there is a query then there must be a result for it
|
|
340
|
+
data = contentResults.contents[dataCounter++];
|
|
341
|
+
}
|
|
342
|
+
return yield propsProcessor(data, context);
|
|
343
|
+
}));
|
|
344
|
+
return Promise.allSettled(processorPromises);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function collectComponentDescriptors(components, componentRegistry, requestedComponentPath, xpContentPath, context) {
|
|
348
|
+
const descriptors = [];
|
|
349
|
+
for (const cmp of (components || [])) {
|
|
350
|
+
processComponentConfig(APP_NAME, APP_NAME_DASHED, cmp);
|
|
351
|
+
// only look for parts
|
|
352
|
+
// look for single part if it is a single component request
|
|
353
|
+
if (XP_COMPONENT_TYPE.FRAGMENT !== cmp.type) {
|
|
354
|
+
const cmpDef = ComponentRegistry.getByComponent(cmp);
|
|
355
|
+
if (cmpDef) {
|
|
356
|
+
// const partPath = `${xpContentPath}/_component${cmp.path}`;
|
|
357
|
+
const cmpData = cmp[cmp.type];
|
|
358
|
+
const config = cmpData && 'config' in cmpData ? cmpData.config : undefined;
|
|
359
|
+
const queryAndVariables = getQueryAndVariables(cmp.type, xpContentPath, cmpDef.query, context, config);
|
|
360
|
+
if (queryAndVariables) {
|
|
361
|
+
descriptors.push({
|
|
362
|
+
component: cmp,
|
|
363
|
+
type: cmpDef,
|
|
364
|
+
queryAndVariables: queryAndVariables,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
// look for parts inside fragments
|
|
371
|
+
const fragPartDescs = collectComponentDescriptors(cmp.fragment.fragment.components, componentRegistry, requestedComponentPath, xpContentPath, context);
|
|
372
|
+
if (fragPartDescs.length) {
|
|
373
|
+
descriptors.push(...fragPartDescs);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return descriptors;
|
|
378
|
+
}
|
|
379
|
+
function processComponentConfig(myAppName, myAppNameDashed, cmp) {
|
|
380
|
+
const cmpData = cmp[cmp.type];
|
|
381
|
+
if (cmpData && 'descriptor' in cmpData && cmpData.descriptor) {
|
|
382
|
+
const [appName, cmpName] = cmpData.descriptor.split(':');
|
|
383
|
+
if (appName !== myAppName) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
let config;
|
|
387
|
+
const configArray = [];
|
|
388
|
+
if ('configAsJson' in cmpData && cmpData.configAsJson && cmpData.configAsJson[myAppNameDashed]) {
|
|
389
|
+
config = cmpData.configAsJson[myAppNameDashed][cmpName];
|
|
390
|
+
if (config) {
|
|
391
|
+
configArray.push(config);
|
|
392
|
+
delete cmpData.configAsJson;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const sanitizedAppName = sanitizeGraphqlName(appName);
|
|
396
|
+
if ('config' in cmpData && cmpData.config && cmpData.config[sanitizedAppName]) {
|
|
397
|
+
config = cmpData.config[sanitizedAppName][sanitizeGraphqlName(cmpName)];
|
|
398
|
+
if (config) {
|
|
399
|
+
configArray.push(config);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (configArray.length) {
|
|
403
|
+
cmpData.config = Object.assign({}, ...configArray);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function getQueryAndVariables(type, path, selectedQuery, context, config) {
|
|
408
|
+
let query, getVariables;
|
|
409
|
+
if (typeof selectedQuery === 'string') {
|
|
410
|
+
query = selectedQuery;
|
|
411
|
+
}
|
|
412
|
+
else if (Array.isArray(selectedQuery)) {
|
|
413
|
+
query = selectedQuery[0];
|
|
414
|
+
getVariables = selectedQuery[1];
|
|
415
|
+
}
|
|
416
|
+
else if (typeof selectedQuery === 'object') {
|
|
417
|
+
query = selectedQuery.query;
|
|
418
|
+
getVariables = selectedQuery.variables;
|
|
419
|
+
}
|
|
420
|
+
if (getVariables && typeof getVariables !== 'function') {
|
|
421
|
+
throw Error(`getVariables for content type ${type} should be a function, not: ${typeof getVariables}`);
|
|
422
|
+
}
|
|
423
|
+
if (query && typeof query !== 'string') {
|
|
424
|
+
throw Error(`Query for content type ${type} should be a string, not: ${typeof query}`);
|
|
425
|
+
}
|
|
426
|
+
if (query) {
|
|
427
|
+
return {
|
|
428
|
+
query: query,
|
|
429
|
+
variables: getVariables ? getVariables(path, context, config) : { path },
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function createPageData(contentType, components) {
|
|
434
|
+
let page;
|
|
435
|
+
if (components) {
|
|
436
|
+
page = buildPage(contentType, components);
|
|
437
|
+
}
|
|
438
|
+
return page;
|
|
439
|
+
}
|
|
440
|
+
function createMetaData(contentType, contentPath, requestType, renderMode, apiUrl, baseUrl, requestedComponentPath, pageCmp, components = []) {
|
|
441
|
+
var _a;
|
|
442
|
+
// .meta will be visible in final rendered inline props.
|
|
443
|
+
// Only adding some .meta attributes here on certain conditions
|
|
444
|
+
// (instead of always adding them and letting them be visible as false/undefined etc)
|
|
445
|
+
const meta = {
|
|
446
|
+
type: contentType,
|
|
447
|
+
path: contentPath,
|
|
448
|
+
requestType: requestType,
|
|
449
|
+
renderMode: renderMode,
|
|
450
|
+
canRender: false,
|
|
451
|
+
catchAll: false,
|
|
452
|
+
apiUrl,
|
|
453
|
+
baseUrl,
|
|
454
|
+
};
|
|
455
|
+
if (requestedComponentPath) {
|
|
456
|
+
meta.requestedComponent = components.find(cmp => cmp.path === requestedComponentPath);
|
|
457
|
+
}
|
|
458
|
+
const pageDesc = (_a = pageCmp === null || pageCmp === void 0 ? void 0 : pageCmp.page) === null || _a === void 0 ? void 0 : _a.descriptor;
|
|
459
|
+
const typeDef = ComponentRegistry.getContentType(contentType);
|
|
460
|
+
if ((typeDef === null || typeDef === void 0 ? void 0 : typeDef.view) && !typeDef.catchAll) {
|
|
461
|
+
meta.canRender = true;
|
|
462
|
+
}
|
|
463
|
+
else if (pageDesc) {
|
|
464
|
+
// always render a page if there is a descriptor (show missing in case it's not implemented)
|
|
465
|
+
meta.canRender = true;
|
|
466
|
+
meta.catchAll = false; // catchAll only refers to content type catch-all
|
|
467
|
+
}
|
|
468
|
+
else if (typeDef === null || typeDef === void 0 ? void 0 : typeDef.view) {
|
|
469
|
+
meta.canRender = true;
|
|
470
|
+
meta.catchAll = true;
|
|
471
|
+
}
|
|
472
|
+
return meta;
|
|
473
|
+
}
|
|
474
|
+
function errorResponse(code = '500', message = 'Unknown error', requestType, renderMode, apiUrl, baseUrl, contentPath) {
|
|
475
|
+
return {
|
|
476
|
+
error: {
|
|
477
|
+
code,
|
|
478
|
+
message,
|
|
479
|
+
},
|
|
480
|
+
page: null,
|
|
481
|
+
common: null,
|
|
482
|
+
data: null,
|
|
483
|
+
meta: {
|
|
484
|
+
type: '',
|
|
485
|
+
requestType: requestType,
|
|
486
|
+
renderMode: renderMode,
|
|
487
|
+
path: contentPath || '',
|
|
488
|
+
canRender: false,
|
|
489
|
+
catchAll: false,
|
|
490
|
+
apiUrl,
|
|
491
|
+
baseUrl,
|
|
492
|
+
},
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
// ///////////////////////////// ENTRY 1 - THE BUILDER:
|
|
496
|
+
/**
|
|
497
|
+
* Configures, builds and returns a general fetchContent function.
|
|
498
|
+
* @param adapterConstants Object containing attributes imported from enonic-connecion-config.js: constants and function concerned with connection to the XP backend. Easiest: caller imports enonic-connection-config and just passes that entire object here as adapterConstants.
|
|
499
|
+
* @param componentRegistry ComponentRegistry object from ComponentRegistry.ts, holding user type mappings that are set in typesRegistration.ts file
|
|
500
|
+
* @returns ContentFetcher
|
|
501
|
+
*/
|
|
502
|
+
export const buildContentFetcher = (config) => {
|
|
503
|
+
const { APP_NAME, APP_NAME_DASHED, getXPRequestType, getRenderMode, getSingleComponentPath, componentRegistry, } = config;
|
|
504
|
+
return (contentPathOrArray, context) => __awaiter(void 0, void 0, void 0, function* () {
|
|
505
|
+
var _a, _b;
|
|
506
|
+
const xpBaseUrl = getXpBaseUrl(context);
|
|
507
|
+
const contentApiUrl = getContentApiUrl(context);
|
|
508
|
+
const requestType = getXPRequestType(context);
|
|
509
|
+
const renderMode = getRenderMode(context);
|
|
510
|
+
let contentPath;
|
|
511
|
+
try {
|
|
512
|
+
const siteRelativeContentPath = getCleanContentPathArrayOrThrow400(contentPathOrArray);
|
|
513
|
+
let requestedComponentPath;
|
|
514
|
+
if (requestType === XP_REQUEST_TYPE.COMPONENT) {
|
|
515
|
+
requestedComponentPath = getSingleComponentPath(context);
|
|
516
|
+
}
|
|
517
|
+
// ///////////// FIRST GUILLOTINE CALL FOR METADATA /////////////////
|
|
518
|
+
const metaResult = yield fetchMetaData(contentApiUrl, '${site}/' + siteRelativeContentPath);
|
|
519
|
+
// ///////////////////////////////////////////////////////////////////////
|
|
520
|
+
const { type, components, _path } = metaResult.meta || {};
|
|
521
|
+
contentPath = _path || '';
|
|
522
|
+
if (metaResult.error) {
|
|
523
|
+
return errorResponse(metaResult.error.code, metaResult.error.message, requestType, renderMode, contentApiUrl, xpBaseUrl, contentPath);
|
|
524
|
+
}
|
|
525
|
+
if (!metaResult.meta) {
|
|
526
|
+
return errorResponse('404', 'No meta data found for content, most likely content does not exist', requestType, renderMode, contentApiUrl, xpBaseUrl, contentPath);
|
|
527
|
+
}
|
|
528
|
+
else if (!type) {
|
|
529
|
+
return errorResponse('500', "Server responded with incomplete meta data: missing content 'type' attribute.", requestType, renderMode, contentApiUrl, xpBaseUrl, contentPath);
|
|
530
|
+
}
|
|
531
|
+
else if (renderMode === RENDER_MODE.NEXT && !IS_DEV_MODE &&
|
|
532
|
+
(type === FRAGMENT_CONTENTTYPE_NAME ||
|
|
533
|
+
type === PAGE_TEMPLATE_CONTENTTYPE_NAME ||
|
|
534
|
+
type === PAGE_TEMPLATE_FOLDER)) {
|
|
535
|
+
return errorResponse('404', `Content type [${type}] is not accessible in ${renderMode} mode`, requestType, renderMode, contentApiUrl, xpBaseUrl, contentPath);
|
|
536
|
+
}
|
|
537
|
+
// ////////////////////////////////////////////////// Content type established. Proceed to data call:
|
|
538
|
+
const allDescriptors = [];
|
|
539
|
+
// Add the content type query at all cases
|
|
540
|
+
const contentTypeDef = componentRegistry === null || componentRegistry === void 0 ? void 0 : componentRegistry.getContentType(type);
|
|
541
|
+
const pageCmp = (components || []).find(cmp => cmp.type === XP_COMPONENT_TYPE.PAGE);
|
|
542
|
+
if (pageCmp) {
|
|
543
|
+
processComponentConfig(APP_NAME, APP_NAME_DASHED, pageCmp);
|
|
544
|
+
}
|
|
545
|
+
const contentQueryAndVars = getQueryAndVariables(type, contentPath, contentTypeDef === null || contentTypeDef === void 0 ? void 0 : contentTypeDef.query, context, (_a = pageCmp === null || pageCmp === void 0 ? void 0 : pageCmp.page) === null || _a === void 0 ? void 0 : _a.config);
|
|
546
|
+
if (contentQueryAndVars) {
|
|
547
|
+
allDescriptors.push({
|
|
548
|
+
type: contentTypeDef,
|
|
549
|
+
queryAndVariables: contentQueryAndVars,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const commonQueryAndVars = getQueryAndVariables(type, contentPath, componentRegistry.getCommonQuery(), context, (_b = pageCmp === null || pageCmp === void 0 ? void 0 : pageCmp.page) === null || _b === void 0 ? void 0 : _b.config);
|
|
553
|
+
if (commonQueryAndVars) {
|
|
554
|
+
allDescriptors.push({
|
|
555
|
+
type: contentTypeDef,
|
|
556
|
+
queryAndVariables: commonQueryAndVars,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
if ((components === null || components === void 0 ? void 0 : components.length) && componentRegistry) {
|
|
560
|
+
for (const cmp of (components || [])) {
|
|
561
|
+
processComponentConfig(APP_NAME, APP_NAME_DASHED, cmp);
|
|
562
|
+
}
|
|
563
|
+
// Collect component queries if defined
|
|
564
|
+
const componentDescriptors = collectComponentDescriptors(components, componentRegistry, requestedComponentPath, contentPath, context);
|
|
565
|
+
if (componentDescriptors.length) {
|
|
566
|
+
allDescriptors.push(...componentDescriptors);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const { query, variables } = combineMultipleQueries(allDescriptors);
|
|
570
|
+
if (!query.trim()) {
|
|
571
|
+
return errorResponse('400', `Missing or empty query override for content type ${type}`, requestType, renderMode, contentApiUrl, xpBaseUrl, contentPath);
|
|
572
|
+
}
|
|
573
|
+
// /////////////// SECOND GUILLOTINE CALL FOR DATA //////////////////////
|
|
574
|
+
const contentResults = yield fetchContentData(contentApiUrl, contentPath, query, variables);
|
|
575
|
+
// ///////////////////////////////////////////////////////////////////////////
|
|
576
|
+
// Apply processors to every component
|
|
577
|
+
const datas = yield applyProcessors(allDescriptors, contentResults, context);
|
|
578
|
+
// Unwind the data back to components
|
|
579
|
+
let contentData = null, common = null;
|
|
580
|
+
let startFrom = 0;
|
|
581
|
+
if (contentQueryAndVars) {
|
|
582
|
+
const item = datas[startFrom];
|
|
583
|
+
contentData = item.status === 'fulfilled' ? item.value : item.reason;
|
|
584
|
+
startFrom++;
|
|
585
|
+
}
|
|
586
|
+
if (commonQueryAndVars) {
|
|
587
|
+
const item = datas[startFrom];
|
|
588
|
+
common = item.status === 'fulfilled' ? item.value : item.reason;
|
|
589
|
+
startFrom++;
|
|
590
|
+
}
|
|
591
|
+
for (let i = startFrom; i < datas.length; i++) {
|
|
592
|
+
// component descriptors hold references to components
|
|
593
|
+
// that will later be used for creating page regions
|
|
594
|
+
const datum = datas[i];
|
|
595
|
+
if (datum.status === 'rejected') {
|
|
596
|
+
let reason = datum.reason;
|
|
597
|
+
if (reason instanceof Error) {
|
|
598
|
+
reason = reason.message;
|
|
599
|
+
}
|
|
600
|
+
else if (typeof reason !== 'string') {
|
|
601
|
+
reason = String(reason);
|
|
602
|
+
}
|
|
603
|
+
allDescriptors[i].component.error = reason;
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
allDescriptors[i].component.data = datum.value;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const page = createPageData(type, components);
|
|
610
|
+
const meta = createMetaData(type, siteRelativeContentPath, requestType, renderMode, contentApiUrl, xpBaseUrl, requestedComponentPath, page, components);
|
|
611
|
+
return {
|
|
612
|
+
data: contentData,
|
|
613
|
+
common,
|
|
614
|
+
meta,
|
|
615
|
+
page: page || null,
|
|
616
|
+
};
|
|
617
|
+
// /////////////////////////////////////////////////////////// Catch
|
|
618
|
+
}
|
|
619
|
+
catch (e) {
|
|
620
|
+
console.error(e);
|
|
621
|
+
let error;
|
|
622
|
+
try {
|
|
623
|
+
error = JSON.parse(e.message);
|
|
624
|
+
}
|
|
625
|
+
catch (e2) {
|
|
626
|
+
error = {
|
|
627
|
+
code: 'Local',
|
|
628
|
+
message: e.message,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return errorResponse(error.code, error.message, requestType, renderMode, contentApiUrl, xpBaseUrl, contentPath);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
};
|
|
635
|
+
// //////////////////////////// ENTRY 2: ready-to-use fetchContent function
|
|
636
|
+
/**
|
|
637
|
+
* Default fetchContent function, built with params from imports.
|
|
638
|
+
* It runs custom content-type-specific guillotine calls against an XP guillotine endpoint, returns content data, error and some meta data
|
|
639
|
+
* Sends one query to the guillotine API and asks for content type, then uses the type to select a second query and variables, which is sent to the API and fetches content data.
|
|
640
|
+
* @param contentPath string or string array: local (site-relative) path to a content available on the API (by XP _path - obtainable by running contentPath through getXpPath). Pre-split into string array, or already a slash-delimited string.
|
|
641
|
+
* @param context object from Next, contains .query info
|
|
642
|
+
* @returns FetchContentResult object: {data?: T, error?: {code, message}}
|
|
643
|
+
*/
|
|
644
|
+
export const fetchContent = buildContentFetcher(Object.assign(Object.assign({}, adapterConstants), { componentRegistry: ComponentRegistry }));
|