@enonic/nextjs-adapter 2.0.0 → 2.0.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/.eslint-childrc.js +17 -0
- package/.eslintrc.js +15 -0
- package/.github/dependabot.yml +20 -0
- package/.github/pull-merger.yml +1 -0
- package/.github/workflows/release.yml +39 -0
- package/LICENSE +201 -0
- package/RELEASE.md +75 -0
- package/package.json +3 -2
- package/server/.eslintrc.js +11 -0
- package/server/ClearableFileCache.ts +410 -0
- package/server/cache.ts +50 -0
- package/server/index.ts +74 -0
- package/server/tsconfig.json +23 -0
- package/setversion.js +8 -0
- package/src/.eslintrc.js +11 -0
- package/src/ComponentRegistry.ts +189 -0
- package/{UrlProcessor.js → src/UrlProcessor.ts} +54 -36
- package/{baseMappings.js → src/baseMappings.ts} +13 -3
- package/{guillotine/fetchContent.js → src/guillotine/fetchContent.ts} +493 -243
- package/src/guillotine/getMetaData.ts +246 -0
- package/{index.d.ts → src/index.ts} +1 -1
- package/src/tsconfig.json +20 -0
- package/{utils.js → src/utils.ts} +158 -68
- package/{views/BaseComponent.jsx → src/views/BaseComponent.tsx} +73 -39
- package/{views/BaseContent.jsx → src/views/BaseContent.tsx} +24 -17
- package/src/views/BaseLayout.tsx +45 -0
- package/{views/BaseMacro.jsx → src/views/BaseMacro.tsx} +45 -29
- package/src/views/BasePage.tsx +56 -0
- package/src/views/BasePart.tsx +50 -0
- package/{views/Empty.jsx → src/views/Empty.tsx} +2 -0
- package/src/views/Fragment.tsx +45 -0
- package/src/views/MainView.tsx +21 -0
- package/src/views/Region.tsx +89 -0
- package/{views/RichTextView.jsx → src/views/RichTextView.tsx} +31 -10
- package/{views/SingleComponent.jsx → src/views/SingleComponent.tsx} +8 -2
- package/src/views/StaticContent.tsx +40 -0
- package/src/views/Text.tsx +15 -0
- package/src/views/macros/DefaultMacro.tsx +9 -0
- package/src/views/macros/DisableMacro.tsx +8 -0
- package/ComponentRegistry.d.ts +0 -64
- package/ComponentRegistry.js +0 -121
- package/UrlProcessor.d.ts +0 -25
- package/baseMappings.d.ts +0 -1
- package/guillotine/fetchContent.d.ts +0 -49
- package/guillotine/getMetaData.d.ts +0 -94
- package/guillotine/getMetaData.js +0 -128
- package/index.js +0 -5
- package/query/Shortcut.d.ts +0 -1
- package/server/ClearableFileCache.d.ts +0 -31
- package/server/ClearableFileCache.js +0 -472
- package/server/cache.d.ts +0 -2
- package/server/cache.js +0 -129
- package/server/index.d.ts +0 -5
- package/server/index.js +0 -132
- package/utils.d.ts +0 -114
- package/views/BaseComponent.d.ts +0 -25
- package/views/BaseContent.d.ts +0 -4
- package/views/BaseLayout.d.ts +0 -17
- package/views/BaseLayout.jsx +0 -24
- package/views/BaseMacro.d.ts +0 -16
- package/views/BasePage.d.ts +0 -19
- package/views/BasePage.jsx +0 -34
- package/views/BasePart.d.ts +0 -19
- package/views/BasePart.jsx +0 -24
- package/views/Empty.d.ts +0 -3
- package/views/Fragment.d.ts +0 -10
- package/views/Fragment.jsx +0 -30
- package/views/MainView.d.ts +0 -4
- package/views/MainView.jsx +0 -14
- package/views/Region.d.ts +0 -20
- package/views/Region.jsx +0 -52
- package/views/RichTextView.d.ts +0 -16
- package/views/SingleComponent.d.ts +0 -5
- package/views/StaticContent.d.ts +0 -15
- package/views/StaticContent.jsx +0 -36
- package/views/Text.d.ts +0 -9
- package/views/Text.jsx +0 -4
- package/views/macros/DefaultMacro.d.ts +0 -4
- package/views/macros/DefaultMacro.jsx +0 -3
- package/views/macros/DisableMacro.d.ts +0 -3
- package/views/macros/DisableMacro.jsx +0 -2
- /package/{query/Shortcut.js → src/query/Shortcut.ts} +0 -0
|
@@ -1,22 +1,121 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import {getMetaQuery, MetaData, PageComponent, PageData, pageFragmentQuery, PageRegion, RegionTree} from './getMetaData';
|
|
2
|
+
|
|
3
|
+
import adapterConstants, {
|
|
4
|
+
addJsessionHeaders,
|
|
5
|
+
addLocaleHeaders,
|
|
6
|
+
APP_NAME,
|
|
7
|
+
APP_NAME_DASHED,
|
|
8
|
+
ContentPathItem,
|
|
9
|
+
Context,
|
|
10
|
+
FRAGMENT_CONTENTTYPE_NAME,
|
|
11
|
+
FRAGMENT_DEFAULT_REGION_NAME,
|
|
12
|
+
GET_STATIC_PATHS_QUERY,
|
|
13
|
+
getContentApiUrl,
|
|
14
|
+
getLocaleProjectConfig,
|
|
15
|
+
getLocaleProjectConfigs,
|
|
16
|
+
getRenderMode,
|
|
17
|
+
getXpBaseUrl,
|
|
18
|
+
IS_DEV_MODE,
|
|
19
|
+
LocaleProjectConfig,
|
|
20
|
+
normalizeLocale,
|
|
21
|
+
PAGE_TEMPLATE_CONTENTTYPE_NAME,
|
|
22
|
+
PAGE_TEMPLATE_FOLDER,
|
|
23
|
+
PURGE_CACHE_URL,
|
|
24
|
+
RENDER_MODE,
|
|
25
|
+
sanitizeGraphqlName,
|
|
26
|
+
XP_COMPONENT_TYPE,
|
|
27
|
+
XP_REQUEST_TYPE,
|
|
28
|
+
} from '../utils';
|
|
29
|
+
import {ComponentDefinition, ComponentRegistry, SelectedQueryMaybeVariablesFunc} from '../ComponentRegistry';
|
|
30
|
+
import {getUrl, UrlProcessor} from '../UrlProcessor';
|
|
31
|
+
import {GetServerSidePropsResult, GetStaticPropsResult} from 'next';
|
|
32
|
+
|
|
33
|
+
type AdapterConstants = {
|
|
34
|
+
APP_NAME: string,
|
|
35
|
+
APP_NAME_DASHED: string,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type Result = {
|
|
39
|
+
error?: {
|
|
40
|
+
code: string,
|
|
41
|
+
message: string
|
|
42
|
+
} | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type GuillotineResult = Result & {
|
|
46
|
+
[dataKey: string]: any;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type MetaResult = Result & {
|
|
50
|
+
meta?: {
|
|
51
|
+
_path: string;
|
|
52
|
+
type: string,
|
|
53
|
+
pageAsJson?: PageData,
|
|
54
|
+
components?: PageComponent[],
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
type ContentResult = Result & {
|
|
59
|
+
contents?: Record<string, any>[];
|
|
9
60
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
61
|
+
|
|
62
|
+
interface ComponentDescriptor {
|
|
63
|
+
type?: ComponentDefinition;
|
|
64
|
+
component?: PageComponent;
|
|
65
|
+
queryAndVariables?: QueryAndVariables;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type FetchContentResult = Result & {
|
|
69
|
+
data: Record<string, any> | null,
|
|
70
|
+
common: Record<string, any> | null,
|
|
71
|
+
meta: MetaData,
|
|
72
|
+
page: PageComponent | null,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
type FetcherConfig<T extends AdapterConstants> = T & {
|
|
77
|
+
componentRegistry: typeof ComponentRegistry
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
interface QueryAndVariables {
|
|
81
|
+
query: string;
|
|
82
|
+
variables?: Record<string, any>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 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.
|
|
87
|
+
* @param contentPath string or string array: pre-split or slash-delimited _path to a content available on the API
|
|
88
|
+
* @returns FetchContentResult object: {data?: T, error?: {code, message}}
|
|
89
|
+
*/
|
|
90
|
+
export type ContentFetcher = (contentPath: string | string[], context?: Context) => Promise<FetchContentResult>;
|
|
91
|
+
|
|
92
|
+
const NO_PROPS_PROCESSOR = async (props: any) => await props ?? {};
|
|
93
|
+
|
|
15
94
|
const ALIAS_PREFIX = 'request';
|
|
95
|
+
|
|
16
96
|
const GUILLOTINE_QUERY_REGEXP = /^\s*query\s*(?:\((.*)*\))?\s*{\s*guillotine\s*{((?:.|\s)+)}\s*}\s*$/;
|
|
97
|
+
|
|
17
98
|
const GRAPHQL_FRAGMENTS_REGEXP = /fragment\s+.+\s+on\s+.+\s*{[\s\w{}().,:"'`]+}/;
|
|
99
|
+
|
|
100
|
+
// /////////////////////////////////////////////////////////////////////////////// Data
|
|
101
|
+
|
|
102
|
+
// Shape of content base-data API body
|
|
103
|
+
export type ContentApiBaseBody = {
|
|
104
|
+
query?: string, // Override the default base-data query
|
|
105
|
+
variables?: { // GraphQL variables inserted into the query
|
|
106
|
+
path?: string, // Full content item _path
|
|
107
|
+
[key: string]: string | number | undefined,
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
18
111
|
/** Generic fetch */
|
|
19
|
-
export const fetchFromApi =
|
|
112
|
+
export const fetchFromApi = async (
|
|
113
|
+
apiUrl: string,
|
|
114
|
+
body: ContentApiBaseBody,
|
|
115
|
+
projectConfig: LocaleProjectConfig,
|
|
116
|
+
headers?: {},
|
|
117
|
+
method = 'POST',
|
|
118
|
+
) => {
|
|
20
119
|
const options = {
|
|
21
120
|
method,
|
|
22
121
|
headers: {
|
|
@@ -26,47 +125,55 @@ export const fetchFromApi = (apiUrl, body, projectConfig, headers, method = 'POS
|
|
|
26
125
|
},
|
|
27
126
|
body: JSON.stringify(body),
|
|
28
127
|
};
|
|
128
|
+
|
|
29
129
|
if (headers) {
|
|
30
130
|
Object.assign(options.headers, headers);
|
|
31
131
|
}
|
|
132
|
+
|
|
32
133
|
let res;
|
|
33
134
|
try {
|
|
34
|
-
res =
|
|
35
|
-
}
|
|
36
|
-
catch (e) {
|
|
135
|
+
res = await fetch(apiUrl, options);
|
|
136
|
+
} catch (e: any) {
|
|
37
137
|
console.warn(apiUrl, e);
|
|
38
138
|
throw new Error(JSON.stringify({
|
|
39
139
|
code: 'API',
|
|
40
140
|
message: e.message,
|
|
41
141
|
}));
|
|
42
142
|
}
|
|
143
|
+
|
|
43
144
|
if (!res.ok) {
|
|
44
145
|
throw new Error(JSON.stringify({
|
|
45
146
|
code: res.status,
|
|
46
|
-
message: `Data fetching failed (message: '${
|
|
147
|
+
message: `Data fetching failed (message: '${await res.text()}')`,
|
|
47
148
|
}));
|
|
48
149
|
}
|
|
150
|
+
|
|
49
151
|
let json;
|
|
50
152
|
try {
|
|
51
|
-
json =
|
|
52
|
-
}
|
|
53
|
-
catch (e) {
|
|
153
|
+
json = await res.json();
|
|
154
|
+
} catch (e) {
|
|
54
155
|
throw new Error(JSON.stringify({
|
|
55
156
|
code: 500,
|
|
56
|
-
message: `API call completed but with non-JSON data: ${JSON.stringify(
|
|
157
|
+
message: `API call completed but with non-JSON data: ${JSON.stringify(await res.text())}`,
|
|
57
158
|
}));
|
|
58
159
|
}
|
|
160
|
+
|
|
59
161
|
if (!json) {
|
|
60
162
|
throw new Error(JSON.stringify({
|
|
61
163
|
code: 500,
|
|
62
|
-
message: `API call completed but with unexpectedly empty data: ${JSON.stringify(
|
|
164
|
+
message: `API call completed but with unexpectedly empty data: ${JSON.stringify(await res.text())}`,
|
|
63
165
|
}));
|
|
64
166
|
}
|
|
167
|
+
|
|
65
168
|
return json;
|
|
66
|
-
}
|
|
169
|
+
};
|
|
170
|
+
|
|
67
171
|
/** Guillotine-specialized fetch, using the generic fetch above */
|
|
68
|
-
export const fetchGuillotine =
|
|
69
|
-
|
|
172
|
+
export const fetchGuillotine = async (
|
|
173
|
+
contentApiUrl: string,
|
|
174
|
+
body: ContentApiBaseBody,
|
|
175
|
+
projectConfig: LocaleProjectConfig,
|
|
176
|
+
headers?: {}): Promise<GuillotineResult> => {
|
|
70
177
|
if (typeof body.query !== 'string' || !body.query.trim()) {
|
|
71
178
|
return {
|
|
72
179
|
error: {
|
|
@@ -75,98 +182,129 @@ export const fetchGuillotine = (contentApiUrl, body, projectConfig, headers) =>
|
|
|
75
182
|
},
|
|
76
183
|
};
|
|
77
184
|
}
|
|
78
|
-
const path =
|
|
185
|
+
const path = body.variables?.path;
|
|
79
186
|
const pathMessage = path ? JSON.stringify(path) : '';
|
|
80
|
-
const result =
|
|
187
|
+
const result = await fetchFromApi(
|
|
188
|
+
contentApiUrl,
|
|
189
|
+
body,
|
|
190
|
+
projectConfig,
|
|
191
|
+
headers,
|
|
192
|
+
)
|
|
81
193
|
.then(json => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
errors
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
194
|
+
let errors: any[] = (json || {}).errors;
|
|
195
|
+
|
|
196
|
+
if (errors) {
|
|
197
|
+
if (!Array.isArray(errors)) {
|
|
198
|
+
errors = [errors];
|
|
199
|
+
}
|
|
200
|
+
console.error(`${errors.length} error(s) when fetching data from: ${contentApiUrl}`);
|
|
201
|
+
console.error(`headers: ${JSON.stringify(headers, null, 2)}
|
|
89
202
|
variables: ${JSON.stringify(body.variables, null, 2)}`);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
203
|
+
errors.forEach(error => {
|
|
204
|
+
console.error(error);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
error: {
|
|
209
|
+
code: '500',
|
|
210
|
+
message: `Server responded with ${errors.length} error(s), probably from guillotine - see log.`,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return json.data;
|
|
216
|
+
})
|
|
102
217
|
.catch((err) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
218
|
+
console.warn(`Client-side error when trying to fetch data ${pathMessage}`, err);
|
|
219
|
+
try {
|
|
220
|
+
return {error: JSON.parse(err.message)};
|
|
221
|
+
} catch (e2) {
|
|
222
|
+
return {error: {code: 'Client-side error', message: err.message}};
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return result as GuillotineResult;
|
|
227
|
+
};
|
|
228
|
+
|
|
113
229
|
// /////////////////////////////////////////////////////////////////////////////// Specific fetch wrappers:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const body = {
|
|
230
|
+
|
|
231
|
+
const fetchMetaData = async (contentApiUrl: string, xpContentPath: string, projectConfig: LocaleProjectConfig, headers?: {}): Promise<MetaResult> => {
|
|
232
|
+
const body: ContentApiBaseBody = {
|
|
117
233
|
query: getMetaQuery(pageFragmentQuery()),
|
|
118
234
|
variables: {
|
|
119
235
|
path: xpContentPath,
|
|
120
236
|
},
|
|
121
237
|
};
|
|
122
|
-
const metaResult =
|
|
238
|
+
const metaResult = await fetchGuillotine(contentApiUrl, body, projectConfig, headers);
|
|
123
239
|
if (metaResult.error) {
|
|
124
240
|
return metaResult;
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
241
|
+
} else {
|
|
127
242
|
return {
|
|
128
|
-
meta:
|
|
243
|
+
meta: metaResult?.guillotine?.get,
|
|
129
244
|
};
|
|
130
245
|
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
const fetchContentData = async <T>(
|
|
250
|
+
contentApiUrl: string,
|
|
251
|
+
xpContentPath: string,
|
|
252
|
+
projectConfig: LocaleProjectConfig,
|
|
253
|
+
query: string,
|
|
254
|
+
variables?: {},
|
|
255
|
+
headers?: {},
|
|
256
|
+
): Promise<ContentResult> => {
|
|
257
|
+
|
|
258
|
+
const body: ContentApiBaseBody = {query};
|
|
134
259
|
if (variables && Object.keys(variables).length > 0) {
|
|
135
260
|
body.variables = variables;
|
|
136
261
|
}
|
|
137
|
-
const contentResults =
|
|
262
|
+
const contentResults = await fetchGuillotine(contentApiUrl, body, projectConfig, headers);
|
|
263
|
+
|
|
138
264
|
if (contentResults.error) {
|
|
139
265
|
return contentResults;
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
266
|
+
} else {
|
|
142
267
|
return {
|
|
143
268
|
// omit the aliases and return values
|
|
144
269
|
contents: Object.values(contentResults),
|
|
145
270
|
};
|
|
146
271
|
}
|
|
147
|
-
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
|
|
148
275
|
// /////////////////////////////////////////////////////////////////////////////// Error checking:
|
|
276
|
+
|
|
149
277
|
/** 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). */
|
|
150
|
-
const getCleanContentPathArrayOrThrow400 = (contentPath) => {
|
|
278
|
+
const getCleanContentPathArrayOrThrow400 = (contentPath: string | string[] | undefined): string => {
|
|
151
279
|
if (contentPath === undefined) {
|
|
152
280
|
return '';
|
|
153
281
|
}
|
|
154
282
|
const isArray = Array.isArray(contentPath);
|
|
283
|
+
|
|
155
284
|
if (!isArray) {
|
|
156
285
|
if (typeof contentPath !== 'string') {
|
|
157
286
|
throw Error(JSON.stringify({
|
|
158
287
|
code: 400,
|
|
159
|
-
message: `Unexpected target content _path: contentPath must be a string or pure string array (contentPath=${JSON.stringify(
|
|
288
|
+
message: `Unexpected target content _path: contentPath must be a string or pure string array (contentPath=${JSON.stringify(
|
|
289
|
+
contentPath)})`,
|
|
160
290
|
}));
|
|
161
291
|
}
|
|
292
|
+
|
|
162
293
|
return contentPath;
|
|
163
|
-
|
|
164
|
-
else {
|
|
294
|
+
|
|
295
|
+
} else {
|
|
165
296
|
return (contentPath).join('/');
|
|
166
297
|
}
|
|
167
298
|
};
|
|
168
|
-
|
|
169
|
-
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
// ------------------------------------------------------------- XP view component data handling
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
type PathFragment = { region: string, index: number };
|
|
305
|
+
|
|
306
|
+
function parseComponentPath(contentType: string, path: string): PathFragment[] {
|
|
307
|
+
const matches: PathFragment[] = [];
|
|
170
308
|
let match;
|
|
171
309
|
const myRegexp = /(?:(\w+)\/(\d+))+/g;
|
|
172
310
|
while ((match = myRegexp.exec(path)) !== null) {
|
|
@@ -185,15 +323,20 @@ function parseComponentPath(contentType, path) {
|
|
|
185
323
|
}
|
|
186
324
|
return matches;
|
|
187
325
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
326
|
+
|
|
327
|
+
function getParentRegion(source: RegionTree, contentType: string, cmpPath: PathFragment[], components: PageComponent[] = [],
|
|
328
|
+
createMissing?: boolean): PageRegion | undefined {
|
|
329
|
+
|
|
330
|
+
let currentTree: RegionTree = source;
|
|
331
|
+
let currentRegion: PageRegion | undefined;
|
|
191
332
|
let parentPath = '';
|
|
333
|
+
|
|
192
334
|
for (let i = 0; i < cmpPath.length; i++) {
|
|
193
335
|
const pathFragment = cmpPath[i];
|
|
194
336
|
const regionName = pathFragment.region;
|
|
195
337
|
parentPath += `/${pathFragment.region}/${pathFragment.index}`;
|
|
196
338
|
currentRegion = currentTree[regionName];
|
|
339
|
+
|
|
197
340
|
if (!currentRegion) {
|
|
198
341
|
if (createMissing) {
|
|
199
342
|
currentRegion = {
|
|
@@ -201,14 +344,15 @@ function getParentRegion(source, contentType, cmpPath, components = [], createMi
|
|
|
201
344
|
components: [],
|
|
202
345
|
};
|
|
203
346
|
currentTree[regionName] = currentRegion;
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
347
|
+
} else {
|
|
206
348
|
throw `Region [${regionName}] was not found`;
|
|
207
349
|
}
|
|
208
350
|
}
|
|
351
|
+
|
|
209
352
|
if (i < cmpPath.length - 1) {
|
|
210
353
|
// look for layouts inside if this is not the last path fragment
|
|
211
|
-
|
|
354
|
+
|
|
355
|
+
const layout = components.find((cmp: PageComponent) => {
|
|
212
356
|
return cmp.type === XP_COMPONENT_TYPE.LAYOUT && prefixLayoutPath(contentType, cmp.path) === parentPath;
|
|
213
357
|
});
|
|
214
358
|
if (!layout) {
|
|
@@ -220,94 +364,107 @@ function getParentRegion(source, contentType, cmpPath, components = [], createMi
|
|
|
220
364
|
currentTree = layout.regions;
|
|
221
365
|
}
|
|
222
366
|
}
|
|
367
|
+
|
|
223
368
|
return currentRegion;
|
|
224
369
|
}
|
|
225
|
-
|
|
370
|
+
|
|
371
|
+
function prefixLayoutPath(contentType: string, path: string): string {
|
|
226
372
|
if (contentType !== FRAGMENT_CONTENTTYPE_NAME) {
|
|
227
373
|
return path;
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
374
|
+
} else {
|
|
230
375
|
// prepend FRAGMENT_DEFAULT_REGION_NAME to path to conform to page structure
|
|
231
376
|
// so that component with path '/' becomes /FRAGMENT_DEFAULT_REGION_NAME/0
|
|
232
377
|
// path /left/1 becomes /FRAGMENT_DEFAULT_REGION_NAME/0/left/1
|
|
233
378
|
return `/${FRAGMENT_DEFAULT_REGION_NAME}/0${path === '/' ? '' : path}`;
|
|
234
379
|
}
|
|
235
380
|
}
|
|
236
|
-
|
|
237
|
-
|
|
381
|
+
|
|
382
|
+
function buildPage(contentType: string, comps: PageComponent[] = []): PageComponent {
|
|
383
|
+
|
|
384
|
+
const page: PageComponent = {
|
|
238
385
|
type: XP_COMPONENT_TYPE.PAGE,
|
|
239
386
|
path: '/',
|
|
240
387
|
};
|
|
388
|
+
|
|
241
389
|
return buildComponentTree(comps, page, contentType);
|
|
242
390
|
}
|
|
243
|
-
|
|
244
|
-
|
|
391
|
+
|
|
392
|
+
function buildComponentTree(comps: PageComponent[], rootComp: PageComponent, contentType?: string): PageComponent {
|
|
393
|
+
const rootTree: RegionTree = {};
|
|
245
394
|
const isFragmentType = contentType === FRAGMENT_CONTENTTYPE_NAME;
|
|
395
|
+
|
|
246
396
|
if (isFragmentType) {
|
|
247
397
|
rootComp.regions = rootTree;
|
|
248
398
|
}
|
|
399
|
+
|
|
249
400
|
(comps || []).forEach(cmp => {
|
|
250
|
-
var _a, _b, _c;
|
|
251
401
|
const cmpPath = parseComponentPath(contentType, cmp.path);
|
|
402
|
+
|
|
252
403
|
if (cmp.path === '/') {
|
|
253
404
|
if (cmp.type === XP_COMPONENT_TYPE.PAGE && rootComp.type === XP_COMPONENT_TYPE.PAGE) {
|
|
254
405
|
// add page values to page object
|
|
255
406
|
rootComp = Object.assign(rootComp, cmp);
|
|
256
407
|
rootComp.page.regions = rootTree;
|
|
257
408
|
return;
|
|
258
|
-
}
|
|
259
|
-
else if (cmp.type === XP_COMPONENT_TYPE.LAYOUT && rootComp.type === XP_COMPONENT_TYPE.FRAGMENT) {
|
|
409
|
+
} else if (cmp.type === XP_COMPONENT_TYPE.LAYOUT && rootComp.type === XP_COMPONENT_TYPE.FRAGMENT) {
|
|
260
410
|
// when rendering fragment as part of the page with layout at root path
|
|
261
411
|
// remove other children from list as they will be included in layout
|
|
262
412
|
rootComp.fragment.fragment.components = [cmp];
|
|
263
413
|
cmp.regions = rootTree;
|
|
264
414
|
}
|
|
265
415
|
}
|
|
416
|
+
|
|
266
417
|
if (cmp.type === XP_COMPONENT_TYPE.FRAGMENT) {
|
|
267
418
|
// build component subtree for fragment
|
|
268
|
-
cmp = buildComponentTree(
|
|
419
|
+
cmp = buildComponentTree(cmp.fragment?.fragment?.components, cmp, contentType);
|
|
269
420
|
}
|
|
421
|
+
|
|
270
422
|
const region = getParentRegion(rootTree, contentType, cmpPath, comps, true);
|
|
423
|
+
|
|
271
424
|
if (region) {
|
|
272
425
|
// getting the index of component from string like '/main/0/left/11'
|
|
273
|
-
const cmpIndex =
|
|
426
|
+
const cmpIndex = cmpPath[cmpPath.length - 1]?.index;
|
|
274
427
|
if (cmpIndex >= 0) {
|
|
275
428
|
region.components.splice(cmpIndex, 0, cmp);
|
|
276
|
-
}
|
|
277
|
-
else {
|
|
429
|
+
} else {
|
|
278
430
|
throw Error(`Could not find [${cmp.type}] component index at ${JSON.stringify(cmpPath, null, 2)}, rendering not possible.`);
|
|
279
431
|
}
|
|
280
432
|
}
|
|
281
433
|
});
|
|
434
|
+
|
|
282
435
|
return rootComp;
|
|
283
436
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
const
|
|
289
|
-
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
function combineMultipleQueries(queriesWithVars: ComponentDescriptor[]): QueryAndVariables {
|
|
440
|
+
const queries: string[] = [];
|
|
441
|
+
const fragments: string[] = [];
|
|
442
|
+
const superVars: { [key: string]: any } = {};
|
|
443
|
+
const superParams: string[] = [];
|
|
444
|
+
|
|
445
|
+
queriesWithVars.forEach((componentDescriptor: ComponentDescriptor, index: number) => {
|
|
290
446
|
const queryAndVars = componentDescriptor.queryAndVariables;
|
|
291
447
|
if (!queryAndVars) {
|
|
292
448
|
return;
|
|
293
449
|
}
|
|
450
|
+
|
|
294
451
|
// Extract fragments first if exist
|
|
295
452
|
let q = queryAndVars.query;
|
|
296
453
|
let match = q.match(GRAPHQL_FRAGMENTS_REGEXP);
|
|
297
|
-
if (
|
|
454
|
+
if (match?.length === 1) {
|
|
298
455
|
// extract a fragment to put it at root level
|
|
299
456
|
fragments.push(match[0]);
|
|
300
457
|
// remove it from query because queries are going to get wrapped
|
|
301
458
|
q = q.replace(match[0], '');
|
|
302
459
|
}
|
|
460
|
+
|
|
303
461
|
// Extract graphql query and its params and add prefixes to exclude collisions with other queries
|
|
304
462
|
match = q.match(GUILLOTINE_QUERY_REGEXP);
|
|
305
463
|
let query = '';
|
|
306
464
|
if (match && match.length === 2) {
|
|
307
465
|
// no params, just query
|
|
308
466
|
query = match[1];
|
|
309
|
-
}
|
|
310
|
-
else if (match && match.length === 3) {
|
|
467
|
+
} else if (match && match.length === 3) {
|
|
311
468
|
// both query and params are present
|
|
312
469
|
query = match[2];
|
|
313
470
|
// process args
|
|
@@ -328,50 +485,62 @@ function combineMultipleQueries(queriesWithVars) {
|
|
|
328
485
|
if (query.length) {
|
|
329
486
|
queries.push(`${ALIAS_PREFIX}${index}:guillotine {${query}}`);
|
|
330
487
|
}
|
|
488
|
+
|
|
331
489
|
// Update variables with the same prefixes
|
|
332
490
|
Object.entries(queryAndVars.variables || {}).forEach(entry => {
|
|
333
491
|
superVars[`${ALIAS_PREFIX}${index}_${entry[0]}`] = entry[1];
|
|
334
492
|
});
|
|
335
493
|
});
|
|
494
|
+
|
|
336
495
|
// Compose the super query
|
|
337
496
|
const superQuery = `query ${superParams.length ? `(${superParams.join(', ')})` : ''} {
|
|
338
497
|
${queries.join('\n')}
|
|
339
498
|
}
|
|
340
499
|
${fragments.join('\n')}
|
|
341
500
|
`;
|
|
501
|
+
|
|
342
502
|
return {
|
|
343
503
|
query: superQuery,
|
|
344
504
|
variables: superVars,
|
|
345
505
|
};
|
|
346
506
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
return
|
|
507
|
+
|
|
508
|
+
async function applyProcessors(componentDescriptors: ComponentDescriptor[], contentResults: ContentResult,
|
|
509
|
+
context?: Context): Promise<PromiseSettledResult<any>[]> {
|
|
510
|
+
|
|
511
|
+
let dataCounter = 0;
|
|
512
|
+
const processorPromises = componentDescriptors.map(async (desc: ComponentDescriptor) => {
|
|
513
|
+
// we're iterating component descriptors here
|
|
514
|
+
// some of them might not have provided graphql requests
|
|
515
|
+
// but we still need to run props processor for them
|
|
516
|
+
// in case they want to fetch their data from elsewhere
|
|
517
|
+
const propsProcessor = desc.type?.processor || NO_PROPS_PROCESSOR;
|
|
518
|
+
let data;
|
|
519
|
+
if (desc.queryAndVariables) {
|
|
520
|
+
// if there is a query then there must be a result for it
|
|
521
|
+
data = contentResults.contents[dataCounter++];
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const config = getComponentConfig(desc.component);
|
|
525
|
+
return await propsProcessor(data, context, config);
|
|
366
526
|
});
|
|
527
|
+
|
|
528
|
+
return Promise.allSettled(processorPromises);
|
|
367
529
|
}
|
|
368
|
-
|
|
530
|
+
|
|
531
|
+
function getComponentConfig(cmp?: PageComponent) {
|
|
369
532
|
const cmpData = cmp && cmp[cmp.type];
|
|
370
533
|
return cmpData && 'config' in cmpData ? cmpData.config : undefined;
|
|
371
534
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
535
|
+
|
|
536
|
+
function collectComponentDescriptors(components: PageComponent[],
|
|
537
|
+
componentRegistry: typeof ComponentRegistry,
|
|
538
|
+
xpContentPath: string,
|
|
539
|
+
context: Context | undefined,
|
|
540
|
+
): ComponentDescriptor[] {
|
|
541
|
+
|
|
542
|
+
const descriptors: ComponentDescriptor[] = [];
|
|
543
|
+
|
|
375
544
|
for (const cmp of (components || [])) {
|
|
376
545
|
processComponentConfig(APP_NAME, APP_NAME_DASHED, cmp);
|
|
377
546
|
// only look for parts
|
|
@@ -390,18 +559,19 @@ function collectComponentDescriptors(components, componentRegistry, xpContentPat
|
|
|
390
559
|
});
|
|
391
560
|
}
|
|
392
561
|
}
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
562
|
+
} else {
|
|
395
563
|
// look for parts inside fragments
|
|
396
|
-
const fragPartDescs = collectComponentDescriptors(
|
|
564
|
+
const fragPartDescs = collectComponentDescriptors(cmp.fragment?.fragment?.components, componentRegistry, xpContentPath, context);
|
|
397
565
|
if (fragPartDescs.length) {
|
|
398
566
|
descriptors.push(...fragPartDescs);
|
|
399
567
|
}
|
|
400
568
|
}
|
|
401
569
|
}
|
|
570
|
+
|
|
402
571
|
return descriptors;
|
|
403
572
|
}
|
|
404
|
-
|
|
573
|
+
|
|
574
|
+
function processComponentConfig(myAppName: string, myAppNameDashed: string, cmp: PageComponent) {
|
|
405
575
|
const cmpData = cmp[cmp.type];
|
|
406
576
|
if (cmpData && 'descriptor' in cmpData && cmpData.descriptor) {
|
|
407
577
|
const [appName, cmpName] = cmpData.descriptor.split(':');
|
|
@@ -409,7 +579,7 @@ function processComponentConfig(myAppName, myAppNameDashed, cmp) {
|
|
|
409
579
|
return;
|
|
410
580
|
}
|
|
411
581
|
let config;
|
|
412
|
-
const configArray = [];
|
|
582
|
+
const configArray: Object[] = [];
|
|
413
583
|
if ('configAsJson' in cmpData && cmpData.configAsJson && cmpData.configAsJson[myAppNameDashed]) {
|
|
414
584
|
config = cmpData.configAsJson[myAppNameDashed][cmpName];
|
|
415
585
|
if (config) {
|
|
@@ -429,86 +599,108 @@ function processComponentConfig(myAppName, myAppNameDashed, cmp) {
|
|
|
429
599
|
}
|
|
430
600
|
}
|
|
431
601
|
}
|
|
432
|
-
|
|
602
|
+
|
|
603
|
+
function getQueryAndVariables(type: string,
|
|
604
|
+
path: string,
|
|
605
|
+
selectedQuery?: SelectedQueryMaybeVariablesFunc,
|
|
606
|
+
context?: Context,
|
|
607
|
+
config?: any): QueryAndVariables | undefined {
|
|
608
|
+
|
|
433
609
|
let query, getVariables;
|
|
610
|
+
|
|
434
611
|
if (typeof selectedQuery === 'string' || typeof selectedQuery === 'function') {
|
|
435
612
|
query = selectedQuery;
|
|
436
|
-
|
|
437
|
-
else if (Array.isArray(selectedQuery)) {
|
|
613
|
+
|
|
614
|
+
} else if (Array.isArray(selectedQuery)) {
|
|
438
615
|
query = selectedQuery[0];
|
|
439
616
|
getVariables = selectedQuery[1];
|
|
440
|
-
|
|
441
|
-
else if (typeof selectedQuery === 'object') {
|
|
617
|
+
|
|
618
|
+
} else if (typeof selectedQuery === 'object') {
|
|
442
619
|
query = selectedQuery.query;
|
|
443
620
|
getVariables = selectedQuery.variables;
|
|
444
621
|
}
|
|
622
|
+
|
|
445
623
|
if (getVariables && typeof getVariables !== 'function') {
|
|
446
624
|
throw Error(`getVariables for content type ${type} should be a function, not: ${typeof getVariables}`);
|
|
447
625
|
}
|
|
626
|
+
|
|
448
627
|
if (query && typeof query !== 'string' && typeof query !== 'function') {
|
|
449
628
|
throw Error(`Query for content type ${type} should be a string or function, not: ${typeof query}`);
|
|
450
629
|
}
|
|
630
|
+
|
|
451
631
|
if (typeof query === 'function') {
|
|
452
632
|
query = query(path, context, config);
|
|
453
633
|
}
|
|
634
|
+
|
|
454
635
|
if (query) {
|
|
455
636
|
return {
|
|
456
637
|
query,
|
|
457
|
-
variables: getVariables ? getVariables(path, context, config) : {
|
|
638
|
+
variables: getVariables ? getVariables(path, context, config) : {path},
|
|
458
639
|
};
|
|
459
640
|
}
|
|
460
641
|
}
|
|
461
|
-
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
function createPageData(contentType: string, components: PageComponent[], componentPath?: string): PageComponent | undefined {
|
|
462
645
|
let page;
|
|
463
646
|
if (components && !componentPath) {
|
|
464
647
|
page = buildPage(contentType, components);
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
648
|
+
} else {
|
|
467
649
|
// Don't build page for single component
|
|
468
650
|
page = null;
|
|
469
651
|
}
|
|
470
|
-
return page;
|
|
652
|
+
return page as PageComponent;
|
|
471
653
|
}
|
|
472
|
-
|
|
473
|
-
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
function createMetaData(contentType: string, contentPath: string,
|
|
657
|
+
requestType: XP_REQUEST_TYPE, renderMode: RENDER_MODE,
|
|
658
|
+
apiUrl: string, baseUrl: string,
|
|
659
|
+
locale: string, defaultLocale: string,
|
|
660
|
+
requestedComponentPath: string | undefined,
|
|
661
|
+
pageCmp?: PageComponent, components: PageComponent[] = []): MetaData {
|
|
474
662
|
// .meta will be visible in final rendered inline props.
|
|
475
663
|
// Only adding some .meta attributes here on certain conditions
|
|
476
664
|
// (instead of always adding them and letting them be visible as false/undefined etc)
|
|
477
|
-
const meta = {
|
|
665
|
+
const meta: MetaData = {
|
|
478
666
|
type: contentType,
|
|
479
667
|
path: contentPath,
|
|
480
668
|
requestType: requestType,
|
|
481
669
|
renderMode: renderMode,
|
|
482
670
|
canRender: false,
|
|
483
|
-
catchAll: false,
|
|
671
|
+
catchAll: false, // catchAll only refers to content type catch-all
|
|
484
672
|
apiUrl,
|
|
485
673
|
baseUrl,
|
|
486
674
|
locale,
|
|
487
675
|
defaultLocale,
|
|
488
676
|
};
|
|
677
|
+
|
|
489
678
|
if (requestedComponentPath) {
|
|
490
679
|
meta.requestedComponent = components.find(cmp => cmp.path === requestedComponentPath);
|
|
491
680
|
}
|
|
492
|
-
|
|
681
|
+
|
|
682
|
+
const pageDesc = pageCmp?.page?.descriptor;
|
|
493
683
|
const typeDef = ComponentRegistry.getContentType(contentType);
|
|
494
|
-
if (
|
|
684
|
+
if (typeDef?.view && !typeDef.catchAll) {
|
|
495
685
|
meta.canRender = true;
|
|
496
|
-
}
|
|
497
|
-
else if (requestType === XP_REQUEST_TYPE.COMPONENT) {
|
|
686
|
+
} else if (requestType === XP_REQUEST_TYPE.COMPONENT) {
|
|
498
687
|
// always render a single component (show missing if not implemented)
|
|
499
688
|
meta.canRender = true;
|
|
500
|
-
}
|
|
501
|
-
else if (pageDesc) {
|
|
689
|
+
} else if (pageDesc) {
|
|
502
690
|
// always render a page if there is a descriptor (show missing if not implemented)
|
|
503
691
|
meta.canRender = true;
|
|
504
|
-
}
|
|
505
|
-
else if (typeDef === null || typeDef === void 0 ? void 0 : typeDef.view) {
|
|
692
|
+
} else if (typeDef?.view) {
|
|
506
693
|
meta.canRender = true;
|
|
507
694
|
meta.catchAll = true;
|
|
508
695
|
}
|
|
696
|
+
|
|
509
697
|
return meta;
|
|
510
698
|
}
|
|
511
|
-
|
|
699
|
+
|
|
700
|
+
function errorResponse(code = '500', message = 'Unknown error',
|
|
701
|
+
requestType: XP_REQUEST_TYPE, renderMode: RENDER_MODE,
|
|
702
|
+
apiUrl: string, baseUrl: string, locale: string, defaultLocale: string,
|
|
703
|
+
contentPath?: string): FetchContentResult {
|
|
512
704
|
return {
|
|
513
705
|
error: {
|
|
514
706
|
code,
|
|
@@ -531,18 +723,22 @@ function errorResponse(code = '500', message = 'Unknown error', requestType, ren
|
|
|
531
723
|
},
|
|
532
724
|
};
|
|
533
725
|
}
|
|
534
|
-
|
|
535
|
-
|
|
726
|
+
|
|
727
|
+
function restrictComponentsToPath(contentType: string, components?: PageComponent[], componentPath?: string) {
|
|
728
|
+
if (!componentPath || !components?.length) {
|
|
536
729
|
return components || [];
|
|
537
730
|
}
|
|
731
|
+
|
|
538
732
|
// filter components to the requested one only
|
|
539
733
|
const component = components.find(cmp => {
|
|
540
734
|
return cmp.path === componentPath;
|
|
541
735
|
});
|
|
736
|
+
|
|
542
737
|
if (!component) {
|
|
543
738
|
return [];
|
|
544
739
|
}
|
|
545
|
-
|
|
740
|
+
|
|
741
|
+
let result: PageComponent[] = [component];
|
|
546
742
|
if (component.type !== XP_COMPONENT_TYPE.LAYOUT) {
|
|
547
743
|
// remember to include all parent layouts too !
|
|
548
744
|
const cmpPath = parseComponentPath(contentType, component.path);
|
|
@@ -553,8 +749,7 @@ function restrictComponentsToPath(contentType, components, componentPath) {
|
|
|
553
749
|
result.unshift(parentCmp);
|
|
554
750
|
}
|
|
555
751
|
}
|
|
556
|
-
}
|
|
557
|
-
else {
|
|
752
|
+
} else {
|
|
558
753
|
// It's a layout, include child components
|
|
559
754
|
const childCmps = components.filter(cmp => cmp.path !== component.path && cmp.path.startsWith(component.path));
|
|
560
755
|
if (childCmps.length) {
|
|
@@ -563,30 +758,40 @@ function restrictComponentsToPath(contentType, components, componentPath) {
|
|
|
563
758
|
}
|
|
564
759
|
return result;
|
|
565
760
|
}
|
|
566
|
-
|
|
567
|
-
const
|
|
761
|
+
|
|
762
|
+
const COMPONENT_PATH_KEY = /\/?_\/component/; // first slash is optional for root components
|
|
763
|
+
|
|
764
|
+
const getContentAndComponentPaths = (requestPath: string, context: Context): string[] => {
|
|
568
765
|
let contentPath, componentPath;
|
|
569
766
|
if (COMPONENT_PATH_KEY.test(requestPath)) {
|
|
570
767
|
[contentPath, componentPath] = requestPath.split(COMPONENT_PATH_KEY);
|
|
571
|
-
}
|
|
572
|
-
else {
|
|
768
|
+
} else {
|
|
573
769
|
contentPath = requestPath;
|
|
574
770
|
}
|
|
575
771
|
return [contentPath, componentPath];
|
|
576
772
|
};
|
|
773
|
+
|
|
577
774
|
// ///////////////////////////// ENTRY 1 - THE BUILDER:
|
|
775
|
+
|
|
578
776
|
/**
|
|
579
777
|
* Configures, builds and returns a general fetchContent function.
|
|
580
778
|
* @param config object containing ComponentRegistry as well as constants imported from enonic-connecion-config.js
|
|
581
779
|
* @returns ContentFetcher
|
|
582
780
|
*/
|
|
583
|
-
const buildContentFetcher = (config) => {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
781
|
+
const buildContentFetcher = <T extends AdapterConstants>(config: FetcherConfig<T>): ContentFetcher => {
|
|
782
|
+
|
|
783
|
+
const {
|
|
784
|
+
APP_NAME,
|
|
785
|
+
APP_NAME_DASHED,
|
|
786
|
+
componentRegistry,
|
|
787
|
+
} = config;
|
|
788
|
+
|
|
789
|
+
return async (contentPathOrArray: string | string[], context: Context): Promise<FetchContentResult> => {
|
|
790
|
+
|
|
587
791
|
if (context.preview) {
|
|
588
792
|
populateEnonicHeaders(context);
|
|
589
793
|
}
|
|
794
|
+
|
|
590
795
|
const headers = {};
|
|
591
796
|
addJsessionHeaders(headers, context);
|
|
592
797
|
addLocaleHeaders(headers, context);
|
|
@@ -595,70 +800,91 @@ const buildContentFetcher = (config) => {
|
|
|
595
800
|
const projectConfig = getLocaleProjectConfig(context);
|
|
596
801
|
const renderMode = getRenderMode(context);
|
|
597
802
|
let requestType = XP_REQUEST_TYPE.TYPE;
|
|
803
|
+
|
|
598
804
|
UrlProcessor.setSiteKey(projectConfig.site);
|
|
805
|
+
|
|
599
806
|
try {
|
|
600
807
|
const requestContentPath = getCleanContentPathArrayOrThrow400(contentPathOrArray);
|
|
808
|
+
|
|
601
809
|
if ('/' + requestContentPath === PURGE_CACHE_URL) {
|
|
602
810
|
// return 404 for /_/cache/enonic/purge because it should've been handled by standalone endpoint
|
|
603
811
|
return errorResponse('404', 'Not found', requestType, renderMode, contentApiUrl, xpBaseUrl, context.locale, context.defaultLocale, requestContentPath);
|
|
604
812
|
}
|
|
813
|
+
|
|
605
814
|
const [siteRelativeContentPath, componentPath] = getContentAndComponentPaths(requestContentPath, context);
|
|
606
815
|
if (componentPath) {
|
|
607
816
|
// set component request type because url contains component path
|
|
608
817
|
requestType = XP_REQUEST_TYPE.COMPONENT;
|
|
609
818
|
}
|
|
819
|
+
|
|
610
820
|
// ///////////// FIRST GUILLOTINE CALL FOR METADATA /////////////////
|
|
611
|
-
const metaResult =
|
|
821
|
+
const metaResult = await fetchMetaData(contentApiUrl, '${site}/' + siteRelativeContentPath, projectConfig, headers);
|
|
612
822
|
// ///////////////////////////////////////////////////////////////////////
|
|
613
|
-
|
|
823
|
+
|
|
824
|
+
const {_path, type} = metaResult.meta || {};
|
|
614
825
|
const contentPath = _path || siteRelativeContentPath;
|
|
826
|
+
|
|
615
827
|
if (metaResult.error) {
|
|
616
828
|
console.error(metaResult.error);
|
|
617
|
-
return errorResponse(metaResult.error.code, metaResult.error.message, requestType, renderMode, contentApiUrl, xpBaseUrl,
|
|
829
|
+
return errorResponse(metaResult.error.code, metaResult.error.message, requestType, renderMode, contentApiUrl, xpBaseUrl,
|
|
830
|
+
context.locale, context.defaultLocale, contentPath);
|
|
618
831
|
}
|
|
832
|
+
|
|
619
833
|
if (!metaResult.meta) {
|
|
620
|
-
return errorResponse('404', 'No meta data found for content, most likely content does not exist', requestType, renderMode,
|
|
621
|
-
|
|
622
|
-
else if (!type) {
|
|
623
|
-
return errorResponse('500', "Server responded with incomplete meta data: missing content 'type' attribute.", requestType,
|
|
624
|
-
|
|
625
|
-
|
|
834
|
+
return errorResponse('404', 'No meta data found for content, most likely content does not exist', requestType, renderMode,
|
|
835
|
+
contentApiUrl, xpBaseUrl, context.locale, context.defaultLocale, contentPath);
|
|
836
|
+
} else if (!type) {
|
|
837
|
+
return errorResponse('500', "Server responded with incomplete meta data: missing content 'type' attribute.", requestType,
|
|
838
|
+
renderMode, contentApiUrl, xpBaseUrl, context.locale, context.defaultLocale, contentPath);
|
|
839
|
+
|
|
840
|
+
} else if (renderMode === RENDER_MODE.NEXT && !IS_DEV_MODE &&
|
|
626
841
|
(type === FRAGMENT_CONTENTTYPE_NAME ||
|
|
627
842
|
type === PAGE_TEMPLATE_CONTENTTYPE_NAME ||
|
|
628
843
|
type === PAGE_TEMPLATE_FOLDER)) {
|
|
629
|
-
return errorResponse('404', `Content type [${type}] is not accessible in ${renderMode} mode`, requestType, renderMode,
|
|
844
|
+
return errorResponse('404', `Content type [${type}] is not accessible in ${renderMode} mode`, requestType, renderMode,
|
|
845
|
+
contentApiUrl, xpBaseUrl, context.locale, context.defaultLocale, contentPath);
|
|
630
846
|
}
|
|
847
|
+
|
|
631
848
|
const components = restrictComponentsToPath(type, metaResult.meta.components, componentPath);
|
|
632
849
|
if (componentPath && !components.length) {
|
|
633
850
|
// component was not found
|
|
634
|
-
return errorResponse('404', `Component ${componentPath} was not found`, requestType, renderMode,
|
|
851
|
+
return errorResponse('404', `Component ${componentPath} was not found`, requestType, renderMode,
|
|
852
|
+
contentApiUrl, xpBaseUrl, context.locale, context.defaultLocale, contentPath);
|
|
635
853
|
}
|
|
854
|
+
|
|
636
855
|
if (requestType !== XP_REQUEST_TYPE.COMPONENT && components.length > 0) {
|
|
637
856
|
requestType = XP_REQUEST_TYPE.PAGE;
|
|
638
857
|
}
|
|
858
|
+
|
|
639
859
|
// ////////////////////////////////////////////////// Content type established. Proceed to data call:
|
|
640
|
-
|
|
860
|
+
|
|
861
|
+
const allDescriptors: ComponentDescriptor[] = [];
|
|
862
|
+
|
|
641
863
|
// Add the content type query at all cases
|
|
642
|
-
const contentTypeDef = componentRegistry
|
|
864
|
+
const contentTypeDef = componentRegistry?.getContentType(type);
|
|
643
865
|
const pageCmp = (components || []).find(cmp => cmp.type === XP_COMPONENT_TYPE.PAGE);
|
|
644
866
|
if (pageCmp) {
|
|
645
867
|
processComponentConfig(APP_NAME, APP_NAME_DASHED, pageCmp);
|
|
646
868
|
}
|
|
647
|
-
|
|
869
|
+
|
|
870
|
+
const contentQueryAndVars = getQueryAndVariables(type, contentPath, contentTypeDef?.query, context, pageCmp?.page?.config);
|
|
648
871
|
if (contentQueryAndVars) {
|
|
649
872
|
allDescriptors.push({
|
|
650
873
|
type: contentTypeDef,
|
|
651
874
|
queryAndVariables: contentQueryAndVars,
|
|
652
875
|
});
|
|
653
876
|
}
|
|
654
|
-
|
|
877
|
+
|
|
878
|
+
const commonQueryAndVars = getQueryAndVariables(type, contentPath, componentRegistry.getCommonQuery(), context,
|
|
879
|
+
pageCmp?.page?.config);
|
|
655
880
|
if (commonQueryAndVars) {
|
|
656
881
|
allDescriptors.push({
|
|
657
882
|
type: contentTypeDef,
|
|
658
883
|
queryAndVariables: commonQueryAndVars,
|
|
659
884
|
});
|
|
660
885
|
}
|
|
661
|
-
|
|
886
|
+
|
|
887
|
+
if (components?.length && componentRegistry) {
|
|
662
888
|
for (const cmp of (components || [])) {
|
|
663
889
|
processComponentConfig(APP_NAME, APP_NAME_DASHED, cmp);
|
|
664
890
|
}
|
|
@@ -668,20 +894,29 @@ const buildContentFetcher = (config) => {
|
|
|
668
894
|
allDescriptors.push(...componentDescriptors);
|
|
669
895
|
}
|
|
670
896
|
}
|
|
671
|
-
|
|
897
|
+
|
|
898
|
+
const {query, variables} = combineMultipleQueries(allDescriptors);
|
|
899
|
+
|
|
672
900
|
if (!query.trim()) {
|
|
673
|
-
return errorResponse('400', `Missing or empty query override for content type ${type}`, requestType, renderMode,
|
|
901
|
+
return errorResponse('400', `Missing or empty query override for content type ${type}`, requestType, renderMode,
|
|
902
|
+
contentApiUrl, xpBaseUrl, context.locale, context.defaultLocale, contentPath);
|
|
674
903
|
}
|
|
904
|
+
|
|
675
905
|
// /////////////// SECOND GUILLOTINE CALL FOR DATA //////////////////////
|
|
676
|
-
const contentResults =
|
|
906
|
+
const contentResults = await fetchContentData(contentApiUrl, contentPath, projectConfig, query, variables, headers);
|
|
677
907
|
// ///////////////////////////////////////////////////////////////////////////
|
|
908
|
+
|
|
678
909
|
if (contentResults.error) {
|
|
679
910
|
console.error(contentResults.error);
|
|
680
|
-
return errorResponse(contentResults.error.code, contentResults.error.message, requestType, renderMode, contentApiUrl,
|
|
911
|
+
return errorResponse(contentResults.error.code, contentResults.error.message, requestType, renderMode, contentApiUrl,
|
|
912
|
+
xpBaseUrl, context.locale, context.defaultLocale, contentPath);
|
|
681
913
|
}
|
|
914
|
+
|
|
682
915
|
// Apply processors to every component
|
|
683
|
-
const datas =
|
|
916
|
+
const datas = await applyProcessors(allDescriptors, contentResults, context);
|
|
917
|
+
|
|
684
918
|
// Unwind the data back to components
|
|
919
|
+
|
|
685
920
|
let contentData = null, common = null;
|
|
686
921
|
let startFrom = 0;
|
|
687
922
|
if (contentQueryAndVars) {
|
|
@@ -694,6 +929,7 @@ const buildContentFetcher = (config) => {
|
|
|
694
929
|
common = item.status === 'fulfilled' ? item.value : item.reason;
|
|
695
930
|
startFrom++;
|
|
696
931
|
}
|
|
932
|
+
|
|
697
933
|
for (let i = startFrom; i < datas.length; i++) {
|
|
698
934
|
// component descriptors hold references to components
|
|
699
935
|
// that will later be used for creating page regions
|
|
@@ -702,42 +938,46 @@ const buildContentFetcher = (config) => {
|
|
|
702
938
|
let reason = datum.reason;
|
|
703
939
|
if (reason instanceof Error) {
|
|
704
940
|
reason = reason.message;
|
|
705
|
-
}
|
|
706
|
-
else if (typeof reason !== 'string') {
|
|
941
|
+
} else if (typeof reason !== 'string') {
|
|
707
942
|
reason = String(reason);
|
|
708
943
|
}
|
|
709
944
|
allDescriptors[i].component.error = reason;
|
|
710
|
-
}
|
|
711
|
-
else {
|
|
945
|
+
} else {
|
|
712
946
|
allDescriptors[i].component.data = datum.value;
|
|
713
947
|
}
|
|
714
948
|
}
|
|
949
|
+
|
|
715
950
|
const page = createPageData(type, components);
|
|
716
951
|
const meta = createMetaData(type, siteRelativeContentPath, requestType, renderMode, contentApiUrl, xpBaseUrl, context.locale, context.defaultLocale, componentPath, page, components);
|
|
952
|
+
|
|
717
953
|
return {
|
|
718
954
|
data: contentData,
|
|
719
955
|
common,
|
|
720
956
|
meta,
|
|
721
957
|
page,
|
|
722
|
-
};
|
|
723
|
-
|
|
724
|
-
catch (e) {
|
|
958
|
+
} as FetchContentResult;
|
|
959
|
+
|
|
960
|
+
} catch (e: any) {
|
|
725
961
|
console.error(e);
|
|
962
|
+
|
|
726
963
|
let error;
|
|
727
964
|
try {
|
|
728
965
|
error = JSON.parse(e.message);
|
|
729
|
-
}
|
|
730
|
-
catch (e2) {
|
|
966
|
+
} catch (e2) {
|
|
731
967
|
error = {
|
|
732
968
|
code: 'Local',
|
|
733
969
|
message: e.message,
|
|
734
970
|
};
|
|
735
971
|
}
|
|
736
|
-
return errorResponse(error.code, error.message, requestType, renderMode, contentApiUrl, xpBaseUrl, context.locale,
|
|
972
|
+
return errorResponse(error.code, error.message, requestType, renderMode, contentApiUrl, xpBaseUrl, context.locale,
|
|
973
|
+
context.defaultLocale, contentPathOrArray.toString());
|
|
737
974
|
}
|
|
738
|
-
}
|
|
975
|
+
};
|
|
739
976
|
};
|
|
977
|
+
|
|
978
|
+
|
|
740
979
|
// //////////////////////////// ENTRY 2: ready-to-use fetchContent function
|
|
980
|
+
|
|
741
981
|
/**
|
|
742
982
|
* Default fetchContent function, built with params from imports.
|
|
743
983
|
* It runs custom content-type-specific guillotine calls against an XP guillotine endpoint, returns content data, error and some meta data
|
|
@@ -746,11 +986,16 @@ const buildContentFetcher = (config) => {
|
|
|
746
986
|
* @param context object from Next, contains .query info
|
|
747
987
|
* @returns FetchContentResult object: {data?: T, error?: {code, message}}
|
|
748
988
|
*/
|
|
749
|
-
export const fetchContent = buildContentFetcher(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
989
|
+
export const fetchContent: ContentFetcher = buildContentFetcher<AdapterConstants>({
|
|
990
|
+
...adapterConstants,
|
|
991
|
+
componentRegistry: ComponentRegistry,
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
export function createResponse(props: FetchContentResult, context?: Context, isStatic?: true): GetStaticPropsResult<FetchContentResult>;
|
|
995
|
+
export function createResponse(props: FetchContentResult, context?: Context, isStatic?: false): GetServerSidePropsResult<FetchContentResult>;
|
|
996
|
+
export function createResponse(props: FetchContentResult, context?: Context, isStatic = true): GetStaticPropsResult<FetchContentResult> | GetServerSidePropsResult<FetchContentResult> {
|
|
997
|
+
const {data, meta, error} = props;
|
|
998
|
+
const pageUrl = data?.get?.data?.target?.pageUrl;
|
|
754
999
|
if (meta.type === 'base:shortcut' && pageUrl) {
|
|
755
1000
|
if (meta.renderMode !== RENDER_MODE.NEXT) {
|
|
756
1001
|
// console.debug(`Returning 404 for shortcut in ${RENDER_MODE.NEXT} mode`);
|
|
@@ -759,6 +1004,7 @@ export function createResponse(props, context, isStatic = true) {
|
|
|
759
1004
|
notFound: true,
|
|
760
1005
|
};
|
|
761
1006
|
}
|
|
1007
|
+
|
|
762
1008
|
const destination = getUrl(pageUrl, meta, true);
|
|
763
1009
|
// console.debug('Redirecting to', destination);
|
|
764
1010
|
return {
|
|
@@ -768,62 +1014,66 @@ export function createResponse(props, context, isStatic = true) {
|
|
|
768
1014
|
},
|
|
769
1015
|
};
|
|
770
1016
|
}
|
|
1017
|
+
|
|
771
1018
|
// we can not set 418 for static paths,
|
|
772
1019
|
// but we can show 404 instead to be handled in CS
|
|
773
1020
|
const canNotRender = meta && !meta.canRender && meta.renderMode !== RENDER_MODE.EDIT;
|
|
774
|
-
|
|
775
|
-
const
|
|
1021
|
+
|
|
1022
|
+
const catchAllInNextProdMode = meta?.renderMode === RENDER_MODE.NEXT && !IS_DEV_MODE && meta?.catchAll;
|
|
1023
|
+
|
|
1024
|
+
const notFound = (error && error.code === '404') || context.res?.statusCode === 404 || canNotRender || catchAllInNextProdMode || undefined;
|
|
1025
|
+
|
|
776
1026
|
// console.debug('Returning view ' + (notFound ? '(not found)' : ''));
|
|
777
|
-
const result = {
|
|
1027
|
+
const result: GetStaticPropsResult<FetchContentResult> | GetServerSidePropsResult<FetchContentResult> = {
|
|
778
1028
|
notFound,
|
|
779
1029
|
props,
|
|
780
1030
|
};
|
|
1031
|
+
|
|
781
1032
|
if (isStatic) {
|
|
782
|
-
result.revalidate = 3600;
|
|
1033
|
+
(<GetStaticPropsResult<FetchContentResult>>result).revalidate = 3600; // In seconds, meaning every hour
|
|
783
1034
|
}
|
|
1035
|
+
|
|
784
1036
|
return result;
|
|
785
1037
|
}
|
|
786
|
-
|
|
1038
|
+
|
|
1039
|
+
function populateEnonicHeaders(context: Context) {
|
|
787
1040
|
const pd = context.previewData;
|
|
788
1041
|
if (!pd) {
|
|
789
1042
|
return;
|
|
790
1043
|
}
|
|
791
|
-
const req = context.req || {};
|
|
1044
|
+
const req = context.req || {} as any;
|
|
792
1045
|
req.headers = Object.assign(req.headers || {}, pd.headers);
|
|
793
1046
|
context.params = Object.assign(context.params || {}, pd.params);
|
|
794
1047
|
context.req = req;
|
|
795
1048
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
});
|
|
1049
|
+
|
|
1050
|
+
export async function fetchContentPathsForAllLocales(path: string, query: string = GET_STATIC_PATHS_QUERY, countPerLocale = 999): Promise<ContentPathItem[]> {
|
|
1051
|
+
const promises = Object.values(getLocaleProjectConfigs()).map(config => fetchContentPathsForLocale(path, config, query, countPerLocale));
|
|
1052
|
+
return Promise.all(promises).then(results => {
|
|
1053
|
+
return results.reduce((all, localePaths) => all.concat(localePaths), []);
|
|
802
1054
|
});
|
|
803
1055
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
}, []);
|
|
827
|
-
});
|
|
1056
|
+
|
|
1057
|
+
export async function fetchContentPathsForLocale(path: string, config: LocaleProjectConfig, query: string = GET_STATIC_PATHS_QUERY, count = 999): Promise<ContentPathItem[]> {
|
|
1058
|
+
const contentApiUrl = getContentApiUrl({locale: config.locale});
|
|
1059
|
+
const body: ContentApiBaseBody = {
|
|
1060
|
+
query,
|
|
1061
|
+
variables: {
|
|
1062
|
+
path,
|
|
1063
|
+
count,
|
|
1064
|
+
},
|
|
1065
|
+
};
|
|
1066
|
+
return fetchGuillotine(contentApiUrl, body, config).then((results: GuillotineResult) => {
|
|
1067
|
+
return results.guillotine.queryDsl.reduce((prev: ContentPathItem[], child: any) => {
|
|
1068
|
+
const regexp = new RegExp(`/${child.site?._name}/?`);
|
|
1069
|
+
const contentPath = child._path.replace(regexp, '');
|
|
1070
|
+
prev.push({
|
|
1071
|
+
params: {
|
|
1072
|
+
contentPath: contentPath.split('/'),
|
|
1073
|
+
},
|
|
1074
|
+
locale: normalizeLocale(config.locale),
|
|
1075
|
+
});
|
|
1076
|
+
return prev;
|
|
1077
|
+
}, []);
|
|
828
1078
|
});
|
|
829
1079
|
}
|