@enonic/nextjs-adapter 3.0.0-B2 → 3.0.0-B4
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/LICENSE +201 -0
- package/baseMappings.js.map +1 -1
- package/client.js.map +1 -1
- package/common/ComponentRegistry.js.map +1 -1
- package/common/UrlProcessor.js.map +1 -1
- package/common/constants.js.map +1 -1
- package/common/env.js +14 -13
- package/common/env.js.map +1 -1
- package/guillotine/applyProcessors.js.map +1 -1
- package/guillotine/buildComponentTree.js.map +1 -1
- package/guillotine/buildErrorResponse.js.map +1 -1
- package/guillotine/buildPage.js.map +1 -1
- package/guillotine/collectComponentDescriptors.js.map +1 -1
- package/guillotine/combineMultipleQueries.js.map +1 -1
- package/guillotine/createMetaData.js.map +1 -1
- package/guillotine/fetchContent.d.ts +2 -2
- package/guillotine/fetchContent.js +172 -170
- package/guillotine/fetchContent.js.map +1 -1
- package/guillotine/fetchContentData.js.map +1 -1
- package/guillotine/fetchContentPathsForAllLocales.js.map +1 -1
- package/guillotine/fetchContentPathsForLocale.js.map +1 -1
- package/guillotine/fetchFromApi.d.ts +1 -1
- package/guillotine/fetchFromApi.js +56 -54
- package/guillotine/fetchFromApi.js.map +1 -1
- package/guillotine/fetchGuillotine.d.ts +1 -1
- package/guillotine/fetchGuillotine.js +38 -36
- package/guillotine/fetchGuillotine.js.map +1 -1
- package/guillotine/fetchMetaData.js.map +1 -1
- package/guillotine/getCleanContentPathArrayOrThrow400.js.map +1 -1
- package/guillotine/getComponentConfig.js.map +1 -1
- package/guillotine/getContentAndComponentPaths.js.map +1 -1
- package/guillotine/getMetaData.js.map +1 -1
- package/guillotine/getParentRegion.js.map +1 -1
- package/guillotine/getQueryAndVariables.js.map +1 -1
- package/guillotine/metadata/configQuery.js.map +1 -1
- package/guillotine/metadata/richTextQuery.js.map +1 -1
- package/guillotine/parseComponentPath.js.map +1 -1
- package/guillotine/prefixLayoutPath.js.map +1 -1
- package/guillotine/processComponentConfig.js.map +1 -1
- package/guillotine/restrictComponentsToPath.js.map +1 -1
- package/guillotine/validateData.js.map +1 -1
- package/guillotine/validateNotFound.js.map +1 -1
- package/guillotine/validateShortcut.js.map +1 -1
- package/i18n/LocaleContext.jsx.map +1 -1
- package/i18n/i18n.js.map +1 -1
- package/index.d.ts +1 -5
- package/index.js +1 -5
- package/index.js.map +1 -1
- package/package.json +15 -16
- package/query/Shortcut.js.map +1 -1
- package/server.d.ts +5 -0
- package/server.js +6 -0
- package/server.js.map +1 -0
- package/src/baseMappings.ts +53 -0
- package/src/client.ts +2 -0
- package/src/common/ComponentRegistry.ts +162 -0
- package/src/common/UrlProcessor.ts +142 -0
- package/src/common/constants.ts +85 -0
- package/src/common/env.ts +39 -0
- package/src/guillotine/applyProcessors.ts +38 -0
- package/src/guillotine/buildComponentTree.ts +63 -0
- package/src/guillotine/buildErrorResponse.ts +39 -0
- package/src/guillotine/buildPage.ts +15 -0
- package/src/guillotine/collectComponentDescriptors.ts +63 -0
- package/src/guillotine/combineMultipleQueries.ts +75 -0
- package/src/guillotine/createMetaData.ts +73 -0
- package/src/guillotine/fetchContent.ts +250 -0
- package/src/guillotine/fetchContentData.ts +33 -0
- package/src/guillotine/fetchContentPathsForAllLocales.ts +21 -0
- package/src/guillotine/fetchContentPathsForLocale.ts +42 -0
- package/src/guillotine/fetchFromApi.ts +69 -0
- package/src/guillotine/fetchGuillotine.ts +61 -0
- package/src/guillotine/fetchMetaData.ts +32 -0
- package/src/guillotine/getCleanContentPathArrayOrThrow400.ts +22 -0
- package/src/guillotine/getComponentConfig.ts +7 -0
- package/src/guillotine/getContentAndComponentPaths.ts +11 -0
- package/src/guillotine/getMetaData.ts +73 -0
- package/src/guillotine/getParentRegion.ts +59 -0
- package/src/guillotine/getQueryAndVariables.ts +52 -0
- package/src/guillotine/metadata/configQuery.ts +46 -0
- package/src/guillotine/metadata/richTextQuery.ts +34 -0
- package/src/guillotine/parseComponentPath.ts +29 -0
- package/src/guillotine/prefixLayoutPath.ts +16 -0
- package/src/guillotine/processComponentConfig.ts +38 -0
- package/src/guillotine/restrictComponentsToPath.ts +45 -0
- package/src/guillotine/validateData.ts +11 -0
- package/src/guillotine/validateNotFound.ts +20 -0
- package/src/guillotine/validateShortcut.ts +37 -0
- package/src/i18n/LocaleContext.tsx +54 -0
- package/src/i18n/i18n.ts +54 -0
- package/src/index.ts +42 -0
- package/src/query/Shortcut.ts +14 -0
- package/src/server.ts +5 -0
- package/src/types/component.ts +147 -0
- package/src/types/componentProps.ts +142 -0
- package/src/types/guillotine.ts +426 -0
- package/src/types/i18n.ts +8 -0
- package/src/types/index.ts +58 -0
- package/src/types/next.ts +7 -0
- package/src/types/util.ts +5 -0
- package/src/utils/addLineNumbers.ts +3 -0
- package/src/utils/buildGuillotineRequestHeaders.ts +32 -0
- package/src/utils/commonChars.ts +16 -0
- package/src/utils/fixDoubleSlashes.ts +3 -0
- package/src/utils/getContentApiUrl.ts +20 -0
- package/src/utils/getProjectLocaleConfig.ts +24 -0
- package/src/utils/getProjectLocaleConfigById.ts +21 -0
- package/src/utils/getProjectLocaleConfigByLocale.ts +19 -0
- package/src/utils/getProjectLocaleConfigs.ts +36 -0
- package/src/utils/getProjectLocales.ts +6 -0
- package/src/utils/getRenderMode.ts +14 -0
- package/src/utils/getRequestLocaleInfo.ts +29 -0
- package/src/utils/getXpBaseUrl.ts +12 -0
- package/src/utils/indent.ts +4 -0
- package/src/utils/sanitizeGraphqlName.ts +11 -0
- package/src/views/BaseComponent.tsx +153 -0
- package/src/views/BaseContent.tsx +47 -0
- package/src/views/BaseLayout.tsx +33 -0
- package/src/views/BaseMacro.tsx +94 -0
- package/src/views/BasePage.tsx +42 -0
- package/src/views/BasePart.tsx +34 -0
- package/src/views/Empty.tsx +5 -0
- package/src/views/Fragment.tsx +41 -0
- package/src/views/MainView.tsx +22 -0
- package/src/views/PropsView.tsx +38 -0
- package/src/views/Region.tsx +76 -0
- package/src/views/RichTextView.tsx +75 -0
- package/src/views/SingleComponent.tsx +19 -0
- package/src/views/StaticContent.tsx +39 -0
- package/src/views/Text.tsx +19 -0
- package/src/views/getClientHydratedHtml.tsx +12 -0
- package/src/views/macros/DefaultMacro.tsx +9 -0
- package/src/views/macros/DisableMacro.tsx +8 -0
- package/types/component.js.map +1 -1
- package/types/componentProps.js.map +1 -1
- package/types/guillotine.js.map +1 -1
- package/types/i18n.js.map +1 -1
- package/types/index.js.map +1 -1
- package/types/next.js.map +1 -1
- package/types/util.js.map +1 -1
- package/utils/addLineNumbers.js.map +1 -1
- package/utils/buildGuillotineRequestHeaders.js.map +1 -1
- package/utils/commonChars.js.map +1 -1
- package/utils/fixDoubleSlashes.js.map +1 -1
- package/utils/getContentApiUrl.js.map +1 -1
- package/utils/getProjectLocaleConfig.js.map +1 -1
- package/utils/getProjectLocaleConfigById.js.map +1 -1
- package/utils/getProjectLocaleConfigByLocale.js.map +1 -1
- package/utils/getProjectLocaleConfigs.js.map +1 -1
- package/utils/getProjectLocales.js.map +1 -1
- package/utils/getRenderMode.js.map +1 -1
- package/utils/getRequestLocaleInfo.js.map +1 -1
- package/utils/getXpBaseUrl.js.map +1 -1
- package/utils/indent.js.map +1 -1
- package/utils/sanitizeGraphqlName.js.map +1 -1
- package/views/BaseComponent.jsx.map +1 -1
- package/views/BaseContent.jsx.map +1 -1
- package/views/BaseLayout.jsx.map +1 -1
- package/views/BaseMacro.jsx.map +1 -1
- package/views/BasePage.jsx.map +1 -1
- package/views/BasePart.jsx.map +1 -1
- package/views/Empty.jsx.map +1 -1
- package/views/Fragment.jsx.map +1 -1
- package/views/MainView.jsx.map +1 -1
- package/views/PropsView.jsx.map +1 -1
- package/views/Region.jsx.map +1 -1
- package/views/RichTextView.jsx.map +1 -1
- package/views/SingleComponent.jsx.map +1 -1
- package/views/StaticContent.jsx.map +1 -1
- package/views/Text.jsx.map +1 -1
- package/views/getClientHydratedHtml.jsx.map +1 -1
- package/views/macros/DefaultMacro.jsx.map +1 -1
- package/views/macros/DisableMacro.jsx.map +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type {ComponentDescriptor, Context, FetchContentResult} from '../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {headers} from 'next/headers';
|
|
5
|
+
import {ComponentRegistry} from '../common/ComponentRegistry';
|
|
6
|
+
import {UrlProcessor} from '../common/UrlProcessor';
|
|
7
|
+
import {
|
|
8
|
+
FRAGMENT_CONTENTTYPE_NAME,
|
|
9
|
+
PAGE_TEMPLATE_CONTENTTYPE_NAME,
|
|
10
|
+
PAGE_TEMPLATE_FOLDER,
|
|
11
|
+
RENDER_MODE,
|
|
12
|
+
XP_COMPONENT_TYPE,
|
|
13
|
+
XP_REQUEST_TYPE,
|
|
14
|
+
} from '../common/constants';
|
|
15
|
+
import {APP_NAME, APP_NAME_DASHED, IS_DEV_MODE} from '../common/env';
|
|
16
|
+
import {getContentApiUrl} from '../utils/getContentApiUrl';
|
|
17
|
+
import {getProjectLocaleConfig} from '../utils/getProjectLocaleConfig';
|
|
18
|
+
import {getRenderMode} from '../utils/getRenderMode';
|
|
19
|
+
import {getXpBaseUrl} from '../utils/getXpBaseUrl';
|
|
20
|
+
import {getRequestLocaleInfo} from '../utils/getRequestLocaleInfo';
|
|
21
|
+
import {buildGuillotineRequestHeaders} from '../utils/buildGuillotineRequestHeaders';
|
|
22
|
+
|
|
23
|
+
import {buildPage} from './buildPage';
|
|
24
|
+
import {fetchMetaData} from './fetchMetaData';
|
|
25
|
+
import {getCleanContentPathArrayOrThrow400} from './getCleanContentPathArrayOrThrow400';
|
|
26
|
+
import {buildErrorResponse} from './buildErrorResponse';
|
|
27
|
+
import {getContentAndComponentPaths} from './getContentAndComponentPaths';
|
|
28
|
+
import {restrictComponentsToPath} from './restrictComponentsToPath';
|
|
29
|
+
import {processComponentConfig} from './processComponentConfig';
|
|
30
|
+
import {getQueryAndVariables} from './getQueryAndVariables';
|
|
31
|
+
import {collectComponentDescriptors} from './collectComponentDescriptors';
|
|
32
|
+
import {combineMultipleQueries} from './combineMultipleQueries';
|
|
33
|
+
import {fetchContentData} from './fetchContentData';
|
|
34
|
+
import {applyProcessors} from './applyProcessors';
|
|
35
|
+
import {createMetaData} from './createMetaData';
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Default fetchContent function, built with params from imports.
|
|
40
|
+
* It runs custom content-type-specific guillotine calls against an XP guillotine endpoint, returns content data, error and some meta data
|
|
41
|
+
* 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.
|
|
42
|
+
* @param context object from Next, contains .query info
|
|
43
|
+
* @returns FetchContentResult object: {data?: T, error?: {code, message}}
|
|
44
|
+
*/
|
|
45
|
+
export async function fetchContent(context: Context): Promise<FetchContentResult> {
|
|
46
|
+
const {locale, locales, defaultLocale} = getRequestLocaleInfo(context);
|
|
47
|
+
|
|
48
|
+
// ideally we only want to set headers in draft mode,
|
|
49
|
+
// but draft mode token is not passed in non-static mode
|
|
50
|
+
// so try to add them whenever they are absent
|
|
51
|
+
if (!context.headers) {
|
|
52
|
+
// TODO does this break routing or dynamic server-side rendering?
|
|
53
|
+
// I guess hydration will fix it anyway?
|
|
54
|
+
const allHeaders = headers();
|
|
55
|
+
|
|
56
|
+
context.headers = new Headers();
|
|
57
|
+
allHeaders.forEach((value, key) => {
|
|
58
|
+
context.headers.set(key, value);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const outHeaders = buildGuillotineRequestHeaders({
|
|
63
|
+
context,
|
|
64
|
+
defaultLocale,
|
|
65
|
+
locale,
|
|
66
|
+
locales,
|
|
67
|
+
});
|
|
68
|
+
const xpBaseUrl = getXpBaseUrl(context);
|
|
69
|
+
const contentApiUrl = getContentApiUrl(context);
|
|
70
|
+
const projectConfig = getProjectLocaleConfig(context);
|
|
71
|
+
const renderMode = getRenderMode(context);
|
|
72
|
+
let requestType = XP_REQUEST_TYPE.TYPE;
|
|
73
|
+
const requestContentPath = getCleanContentPathArrayOrThrow400(context.contentPath);
|
|
74
|
+
const errorResponse = buildErrorResponse(requestType, renderMode, contentApiUrl, xpBaseUrl, locale, defaultLocale, requestContentPath);
|
|
75
|
+
|
|
76
|
+
UrlProcessor.setSiteKey(projectConfig.site);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const [siteRelativeContentPath, componentPath] = getContentAndComponentPaths(requestContentPath);
|
|
80
|
+
if (componentPath) {
|
|
81
|
+
// set component request type because url contains component path
|
|
82
|
+
requestType = XP_REQUEST_TYPE.COMPONENT;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ///////////// FIRST GUILLOTINE CALL FOR METADATA /////////////////
|
|
86
|
+
const metaResult = await fetchMetaData(contentApiUrl, '${site}/' + siteRelativeContentPath, projectConfig, outHeaders);
|
|
87
|
+
// ///////////////////////////////////////////////////////////////////////
|
|
88
|
+
|
|
89
|
+
const {_path, type} = metaResult.meta || {};
|
|
90
|
+
const contentPath = _path || siteRelativeContentPath;
|
|
91
|
+
|
|
92
|
+
if (metaResult.error) {
|
|
93
|
+
console.error(metaResult.error);
|
|
94
|
+
return errorResponse(metaResult.error.code, metaResult.error.message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!metaResult.meta) {
|
|
98
|
+
return errorResponse('404', 'No meta data found for content, most likely content does not exist');
|
|
99
|
+
} else if (!type) {
|
|
100
|
+
return errorResponse('500', "Server responded with incomplete meta data: missing content 'type' attribute.");
|
|
101
|
+
|
|
102
|
+
} else if (
|
|
103
|
+
renderMode === RENDER_MODE.NEXT
|
|
104
|
+
&& !IS_DEV_MODE
|
|
105
|
+
&& (
|
|
106
|
+
type === FRAGMENT_CONTENTTYPE_NAME ||
|
|
107
|
+
type === PAGE_TEMPLATE_CONTENTTYPE_NAME ||
|
|
108
|
+
type === PAGE_TEMPLATE_FOLDER
|
|
109
|
+
)) {
|
|
110
|
+
return errorResponse('404', `Content type [${type}] is not accessible in ${renderMode} mode`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const components = restrictComponentsToPath(type, metaResult.meta.components, componentPath);
|
|
114
|
+
if (componentPath && !components.length) {
|
|
115
|
+
// component was not found
|
|
116
|
+
return errorResponse('404', `Component ${componentPath} was not found`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (requestType !== XP_REQUEST_TYPE.COMPONENT && components.length > 0) {
|
|
120
|
+
requestType = XP_REQUEST_TYPE.PAGE;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ////////////////////////////////////////////////// Content type established. Proceed to data call:
|
|
124
|
+
|
|
125
|
+
const allDescriptors: ComponentDescriptor[] = [];
|
|
126
|
+
|
|
127
|
+
// Add the content type query at all cases
|
|
128
|
+
const contentTypeDef = ComponentRegistry.getContentType(type);
|
|
129
|
+
const pageCmp = (components || []).find(cmp => cmp.type === XP_COMPONENT_TYPE.PAGE);
|
|
130
|
+
if (pageCmp) {
|
|
131
|
+
processComponentConfig(APP_NAME, APP_NAME_DASHED, pageCmp);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const contentQueryAndVars = getQueryAndVariables(type, contentPath, context, contentTypeDef?.query, pageCmp?.page?.config);
|
|
135
|
+
if (contentQueryAndVars) {
|
|
136
|
+
allDescriptors.push({
|
|
137
|
+
type: contentTypeDef,
|
|
138
|
+
queryAndVariables: contentQueryAndVars,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const commonQueryAndVars = getQueryAndVariables(type, contentPath, context, ComponentRegistry.getCommonQuery(),
|
|
143
|
+
pageCmp?.page?.config);
|
|
144
|
+
if (commonQueryAndVars) {
|
|
145
|
+
allDescriptors.push({
|
|
146
|
+
type: contentTypeDef,
|
|
147
|
+
queryAndVariables: commonQueryAndVars,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (components?.length && ComponentRegistry) {
|
|
152
|
+
for (const cmp of (components || [])) {
|
|
153
|
+
processComponentConfig(APP_NAME, APP_NAME_DASHED, cmp);
|
|
154
|
+
}
|
|
155
|
+
// Collect component queries if defined
|
|
156
|
+
const componentDescriptors = collectComponentDescriptors({
|
|
157
|
+
components,
|
|
158
|
+
xpContentPath: contentPath,
|
|
159
|
+
context,
|
|
160
|
+
});
|
|
161
|
+
if (componentDescriptors.length) {
|
|
162
|
+
allDescriptors.push(...componentDescriptors);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const {query, variables} = combineMultipleQueries(allDescriptors);
|
|
167
|
+
if (!query.trim()) {
|
|
168
|
+
return errorResponse('400', `Missing or empty query override for content type ${type}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// /////////////// SECOND GUILLOTINE CALL FOR DATA //////////////////////
|
|
172
|
+
const contentResults = await fetchContentData(contentApiUrl, contentPath, projectConfig, query, variables, outHeaders);
|
|
173
|
+
// ///////////////////////////////////////////////////////////////////////////
|
|
174
|
+
|
|
175
|
+
if (contentResults.error) {
|
|
176
|
+
console.error(contentResults.error);
|
|
177
|
+
return errorResponse(contentResults.error.code, contentResults.error.message);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Apply processors to every component
|
|
181
|
+
const datas = await applyProcessors(allDescriptors, contentResults, context);
|
|
182
|
+
|
|
183
|
+
// Unwind the data back to components
|
|
184
|
+
|
|
185
|
+
let contentData = null, common = null;
|
|
186
|
+
let startFrom = 0;
|
|
187
|
+
if (contentQueryAndVars) {
|
|
188
|
+
const item = datas[startFrom];
|
|
189
|
+
contentData = item.status === 'fulfilled' ? item.value : item.reason;
|
|
190
|
+
startFrom++;
|
|
191
|
+
}
|
|
192
|
+
if (commonQueryAndVars) {
|
|
193
|
+
const item = datas[startFrom];
|
|
194
|
+
common = item.status === 'fulfilled' ? item.value : item.reason;
|
|
195
|
+
startFrom++;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (let i = startFrom; i < datas.length; i++) {
|
|
199
|
+
// component descriptors hold references to components
|
|
200
|
+
// that will later be used for creating page regions
|
|
201
|
+
const datum = datas[i];
|
|
202
|
+
if (datum.status === 'rejected') {
|
|
203
|
+
let reason = datum.reason;
|
|
204
|
+
if (reason instanceof Error) {
|
|
205
|
+
reason = reason.message;
|
|
206
|
+
} else if (typeof reason !== 'string') {
|
|
207
|
+
reason = String(reason);
|
|
208
|
+
}
|
|
209
|
+
allDescriptors[i].component.error = reason;
|
|
210
|
+
} else {
|
|
211
|
+
allDescriptors[i].component.data = datum.value;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const page = buildPage(type, components);
|
|
216
|
+
const meta = createMetaData({
|
|
217
|
+
apiUrl: contentApiUrl,
|
|
218
|
+
baseUrl: xpBaseUrl,
|
|
219
|
+
components,
|
|
220
|
+
contentPath: siteRelativeContentPath,
|
|
221
|
+
contentType: type,
|
|
222
|
+
defaultLocale,
|
|
223
|
+
locale,
|
|
224
|
+
pageCmp: page,
|
|
225
|
+
renderMode,
|
|
226
|
+
requestedComponentPath: componentPath,
|
|
227
|
+
requestType,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
data: contentData,
|
|
232
|
+
common,
|
|
233
|
+
meta,
|
|
234
|
+
page,
|
|
235
|
+
};
|
|
236
|
+
} catch (e: any) {
|
|
237
|
+
console.error(e);
|
|
238
|
+
|
|
239
|
+
let error;
|
|
240
|
+
try {
|
|
241
|
+
error = JSON.parse(e.message);
|
|
242
|
+
} catch (e2) {
|
|
243
|
+
error = {
|
|
244
|
+
code: 'Local',
|
|
245
|
+
message: e.message,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return errorResponse(error.code, error.message);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContentApiBaseBody,
|
|
3
|
+
ContentResult,
|
|
4
|
+
ProjectLocaleConfig,
|
|
5
|
+
} from '../types';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import {fetchGuillotine} from './fetchGuillotine';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export const fetchContentData = async <Content = Record<string, unknown>>(
|
|
12
|
+
contentApiUrl: string,
|
|
13
|
+
xpContentPath: string,
|
|
14
|
+
projectConfig: ProjectLocaleConfig,
|
|
15
|
+
query: string,
|
|
16
|
+
variables?: {},
|
|
17
|
+
headers?: {},
|
|
18
|
+
): Promise<ContentResult> => {
|
|
19
|
+
const body: ContentApiBaseBody = {query};
|
|
20
|
+
if (variables && Object.keys(variables).length > 0) {
|
|
21
|
+
body.variables = variables;
|
|
22
|
+
}
|
|
23
|
+
const contentResults = await fetchGuillotine<ContentResult<Content>>(contentApiUrl, body, projectConfig, headers);
|
|
24
|
+
|
|
25
|
+
if (contentResults.error) {
|
|
26
|
+
return contentResults;
|
|
27
|
+
} else {
|
|
28
|
+
return {
|
|
29
|
+
// omit the aliases and return values
|
|
30
|
+
contents: Object.values(contentResults),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {ContentPathItem} from '../types';
|
|
2
|
+
|
|
3
|
+
import {GET_STATIC_PATHS_QUERY} from '../common/constants';
|
|
4
|
+
import {getProjectLocaleConfigs} from '../utils/getProjectLocaleConfigs';
|
|
5
|
+
import {fetchContentPathsForLocale} from './fetchContentPathsForLocale';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export async function fetchContentPathsForAllLocales(
|
|
9
|
+
path: string,
|
|
10
|
+
query: string = GET_STATIC_PATHS_QUERY,
|
|
11
|
+
countPerLocale = 999,
|
|
12
|
+
): Promise<ContentPathItem[]> {
|
|
13
|
+
const promises = Object.values(getProjectLocaleConfigs()).map(
|
|
14
|
+
config => fetchContentPathsForLocale(
|
|
15
|
+
path, config, query, countPerLocale,
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
return Promise.all(promises).then(results => {
|
|
19
|
+
return results.reduce((all, localePaths) => all.concat(localePaths), []);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type {ContentApiBaseBody, ContentPathItem, GuillotineResult, ProjectLocaleConfig} from '../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {GET_STATIC_PATHS_QUERY} from '../common/constants';
|
|
5
|
+
import {getContentApiUrl} from '../utils/getContentApiUrl';
|
|
6
|
+
import {fetchGuillotine} from './fetchGuillotine';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export async function fetchContentPathsForLocale(
|
|
10
|
+
path: string,
|
|
11
|
+
config: ProjectLocaleConfig,
|
|
12
|
+
query: string = GET_STATIC_PATHS_QUERY,
|
|
13
|
+
count = 999,
|
|
14
|
+
): Promise<ContentPathItem[]> {
|
|
15
|
+
const contentApiUrl = getContentApiUrl({
|
|
16
|
+
contentPath: path,
|
|
17
|
+
locale: config.locale,
|
|
18
|
+
});
|
|
19
|
+
const body: ContentApiBaseBody = {
|
|
20
|
+
query,
|
|
21
|
+
variables: {
|
|
22
|
+
path,
|
|
23
|
+
count,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
return fetchGuillotine<ContentPathItem[]>(contentApiUrl, body, config).then((
|
|
27
|
+
results: GuillotineResult,
|
|
28
|
+
) => {
|
|
29
|
+
return results.guillotine.queryDsl.reduce((
|
|
30
|
+
prev: ContentPathItem[],
|
|
31
|
+
child: any,
|
|
32
|
+
) => {
|
|
33
|
+
const regexp = new RegExp(`^/${child.site?._name ? `${child.site._name}/?` : ''}`);
|
|
34
|
+
const contentPath = child._path.replace(regexp, '');
|
|
35
|
+
prev.push({
|
|
36
|
+
contentPath: contentPath.split('/'),
|
|
37
|
+
locale: config.locale,
|
|
38
|
+
});
|
|
39
|
+
return prev;
|
|
40
|
+
}, []);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type {ContentApiBaseBody, GuillotineResponse, GuillotineResponseJson, ProjectLocaleConfig} from '../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/** Generic fetch */
|
|
5
|
+
export async function fetchFromApi<Data = Record<string, unknown>>(
|
|
6
|
+
apiUrl: string,
|
|
7
|
+
body: ContentApiBaseBody,
|
|
8
|
+
projectConfig: ProjectLocaleConfig,
|
|
9
|
+
headers?: {},
|
|
10
|
+
method = 'POST',
|
|
11
|
+
) {
|
|
12
|
+
const options = {
|
|
13
|
+
method,
|
|
14
|
+
headers: {
|
|
15
|
+
'Accept': 'application/json',
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
'X-Guillotine-SiteKey': projectConfig.site,
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify(body),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
if (headers) {
|
|
23
|
+
Object.assign(options.headers, headers);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let res: GuillotineResponse<Data>;
|
|
27
|
+
try {
|
|
28
|
+
res = await fetch(apiUrl, options);
|
|
29
|
+
} catch (e: any) {
|
|
30
|
+
console.warn(apiUrl, e);
|
|
31
|
+
throw new Error(JSON.stringify({
|
|
32
|
+
code: 'API',
|
|
33
|
+
message: e.message,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
throw new Error(JSON.stringify({
|
|
39
|
+
code: res.status,
|
|
40
|
+
message: `Data fetching failed (message: '${await res.text()}')`,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let json: GuillotineResponseJson<Data>;
|
|
45
|
+
try {
|
|
46
|
+
json = await res.json();
|
|
47
|
+
} catch (e) {
|
|
48
|
+
throw new Error(JSON.stringify({
|
|
49
|
+
code: 500,
|
|
50
|
+
message: `API call completed but with non-JSON data: ${JSON.stringify(await res.text())}`,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!json) {
|
|
55
|
+
throw new Error(JSON.stringify({
|
|
56
|
+
code: 500,
|
|
57
|
+
message: `API call completed but with unexpectedly empty data: ${JSON.stringify(await res.text())}`,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(json)) {
|
|
62
|
+
throw new Error(JSON.stringify({
|
|
63
|
+
code: 500,
|
|
64
|
+
message: `API call completed but with unexpected array data: ${JSON.stringify(json)}`,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return json;
|
|
69
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type {ContentApiBaseBody, GuillotineResult, ProjectLocaleConfig} from '../types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {fetchFromApi} from './fetchFromApi';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** Guillotine-specialized fetch, using the generic fetch above */
|
|
8
|
+
export async function fetchGuillotine<Data = Record<string, unknown>>(
|
|
9
|
+
contentApiUrl: string,
|
|
10
|
+
body: ContentApiBaseBody,
|
|
11
|
+
projectConfig: ProjectLocaleConfig,
|
|
12
|
+
headers?: {},
|
|
13
|
+
): Promise<GuillotineResult> {
|
|
14
|
+
if (typeof body.query !== 'string' || !body.query.trim()) {
|
|
15
|
+
return {
|
|
16
|
+
error: {
|
|
17
|
+
code: '400',
|
|
18
|
+
message: `Invalid or missing query. JSON.stringify(query) = ${JSON.stringify(body.query)}`,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const path = body.variables?.path;
|
|
23
|
+
const pathMessage = path ? JSON.stringify(path) : '';
|
|
24
|
+
return await fetchFromApi<Data>(
|
|
25
|
+
contentApiUrl,
|
|
26
|
+
body,
|
|
27
|
+
projectConfig,
|
|
28
|
+
headers,
|
|
29
|
+
)
|
|
30
|
+
.then(json => {
|
|
31
|
+
let errors: any[] = json.errors; // fetchFromApi ensures json is an object
|
|
32
|
+
|
|
33
|
+
if (errors) {
|
|
34
|
+
if (!Array.isArray(errors)) {
|
|
35
|
+
errors = [errors];
|
|
36
|
+
}
|
|
37
|
+
console.error(`${errors.length} error(s) when fetching data from: ${contentApiUrl}`);
|
|
38
|
+
console.error(`headers: ${JSON.stringify(headers, null, 2)} \nvariables: ${JSON.stringify(body.variables, null, 2)}`);
|
|
39
|
+
errors.forEach(error => {
|
|
40
|
+
console.error(error);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
error: {
|
|
45
|
+
code: '500',
|
|
46
|
+
message: `Server responded with ${errors.length} error(s), probably from guillotine - see log.`,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return json.data;
|
|
52
|
+
})
|
|
53
|
+
.catch((err) => {
|
|
54
|
+
console.warn(`Client-side error when trying to fetch data ${pathMessage}`, err);
|
|
55
|
+
try {
|
|
56
|
+
return {error: JSON.parse(err.message)};
|
|
57
|
+
} catch (e2) {
|
|
58
|
+
return {error: {code: 'Client-side error', message: err.message}};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContentApiBaseBody,
|
|
3
|
+
MetaResult,
|
|
4
|
+
ProjectLocaleConfig,
|
|
5
|
+
} from '../types';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import {getMetaQuery, pageFragmentQuery} from './getMetaData';
|
|
9
|
+
import {fetchGuillotine} from './fetchGuillotine';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export const fetchMetaData = async (
|
|
13
|
+
contentApiUrl: string,
|
|
14
|
+
xpContentPath: string,
|
|
15
|
+
projectConfig: ProjectLocaleConfig,
|
|
16
|
+
headers?: {},
|
|
17
|
+
): Promise<MetaResult> => {
|
|
18
|
+
const body: ContentApiBaseBody = {
|
|
19
|
+
query: getMetaQuery(pageFragmentQuery()),
|
|
20
|
+
variables: {
|
|
21
|
+
path: xpContentPath,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const metaResult = await fetchGuillotine<MetaResult>(contentApiUrl, body, projectConfig, headers);
|
|
25
|
+
if (metaResult.error) {
|
|
26
|
+
return metaResult;
|
|
27
|
+
} else {
|
|
28
|
+
return {
|
|
29
|
+
meta: metaResult?.guillotine?.get,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** 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). */
|
|
2
|
+
export const getCleanContentPathArrayOrThrow400 = (contentPath: string | string[] | undefined): string => {
|
|
3
|
+
if (contentPath === undefined) {
|
|
4
|
+
return '';
|
|
5
|
+
}
|
|
6
|
+
const isArray = Array.isArray(contentPath);
|
|
7
|
+
|
|
8
|
+
if (!isArray) {
|
|
9
|
+
if (typeof contentPath !== 'string') {
|
|
10
|
+
throw Error(JSON.stringify({
|
|
11
|
+
code: 400,
|
|
12
|
+
message: `Unexpected target content _path: contentPath must be a string or pure string array (contentPath=${JSON.stringify(
|
|
13
|
+
contentPath)})`,
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return contentPath;
|
|
18
|
+
|
|
19
|
+
} else {
|
|
20
|
+
return (contentPath).join('/');
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const COMPONENT_PATH_KEY = /\/?_\/component/; // first slash is optional for root components
|
|
2
|
+
|
|
3
|
+
export const getContentAndComponentPaths = (requestPath: string): string[] => {
|
|
4
|
+
let contentPath, componentPath;
|
|
5
|
+
if (COMPONENT_PATH_KEY.test(requestPath)) {
|
|
6
|
+
[contentPath, componentPath] = requestPath.split(COMPONENT_PATH_KEY);
|
|
7
|
+
} else {
|
|
8
|
+
contentPath = requestPath;
|
|
9
|
+
}
|
|
10
|
+
return [contentPath, componentPath];
|
|
11
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {ComponentRegistry} from '../common/ComponentRegistry';
|
|
2
|
+
import {configQuery} from './metadata/configQuery';
|
|
3
|
+
import {richTextQuery} from './metadata/richTextQuery';
|
|
4
|
+
import {indent} from '../utils/indent';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const partConfigQuery = (): string => {
|
|
8
|
+
return configQuery(ComponentRegistry.getParts());
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const layoutConfigQuery = (): string => {
|
|
12
|
+
return configQuery(ComponentRegistry.getLayouts());
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const pageConfigQuery = (): string => {
|
|
16
|
+
return configQuery(ComponentRegistry.getPages());
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
IMPORTANT: make sure to transform your queries into functions too when using this function inside them !!!
|
|
22
|
+
*/
|
|
23
|
+
const componentsQuery = () => `type
|
|
24
|
+
path
|
|
25
|
+
page {
|
|
26
|
+
descriptor
|
|
27
|
+
${indent(pageConfigQuery(), 4)}
|
|
28
|
+
template {
|
|
29
|
+
_path
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
layout {
|
|
33
|
+
descriptor
|
|
34
|
+
${indent(layoutConfigQuery(), 4)}
|
|
35
|
+
}
|
|
36
|
+
text {
|
|
37
|
+
${indent(richTextQuery('value'), 4)}
|
|
38
|
+
}
|
|
39
|
+
part {
|
|
40
|
+
descriptor
|
|
41
|
+
${indent(partConfigQuery(), 4)}
|
|
42
|
+
}
|
|
43
|
+
image {
|
|
44
|
+
caption
|
|
45
|
+
image {
|
|
46
|
+
imageUrl (type:absolute, scale: "width-768")
|
|
47
|
+
}
|
|
48
|
+
}`;
|
|
49
|
+
|
|
50
|
+
// THIS QUERY DOES NOT SUPPORT NESTED FRAGMENTS
|
|
51
|
+
export const pageFragmentQuery = () => `components(resolveFragment: false, resolveTemplate: true) {
|
|
52
|
+
fragment {
|
|
53
|
+
id
|
|
54
|
+
fragment {
|
|
55
|
+
components {
|
|
56
|
+
${indent(componentsQuery(),16)}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
${indent(componentsQuery(),4)}
|
|
61
|
+
}`;
|
|
62
|
+
|
|
63
|
+
export function getMetaQuery(pageFragment?: string): string {
|
|
64
|
+
return `query($path:ID!) {
|
|
65
|
+
guillotine {
|
|
66
|
+
get(key:$path) {
|
|
67
|
+
_path
|
|
68
|
+
type
|
|
69
|
+
${indent(pageFragment || '', 12)}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}`;
|
|
73
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PageComponent,
|
|
3
|
+
PageRegion,
|
|
4
|
+
PathFragment,
|
|
5
|
+
RegionTree,
|
|
6
|
+
} from '../types';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import {XP_COMPONENT_TYPE} from '../common/constants';
|
|
10
|
+
import {prefixLayoutPath} from './prefixLayoutPath';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export function getParentRegion(
|
|
14
|
+
source: RegionTree,
|
|
15
|
+
contentType: string,
|
|
16
|
+
cmpPath: PathFragment[],
|
|
17
|
+
components: PageComponent[] = [],
|
|
18
|
+
createMissing ? : boolean,
|
|
19
|
+
): PageRegion | undefined {
|
|
20
|
+
let currentTree: RegionTree = source;
|
|
21
|
+
let currentRegion: PageRegion | undefined;
|
|
22
|
+
let parentPath = '';
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < cmpPath.length; i++) {
|
|
25
|
+
const pathFragment = cmpPath[i];
|
|
26
|
+
const regionName = pathFragment.region;
|
|
27
|
+
parentPath += `/${pathFragment.region}/${pathFragment.index}`;
|
|
28
|
+
currentRegion = currentTree[regionName];
|
|
29
|
+
|
|
30
|
+
if (!currentRegion) {
|
|
31
|
+
if (createMissing) {
|
|
32
|
+
currentRegion = {
|
|
33
|
+
name: regionName,
|
|
34
|
+
components: [],
|
|
35
|
+
};
|
|
36
|
+
currentTree[regionName] = currentRegion;
|
|
37
|
+
} else {
|
|
38
|
+
throw `Region [${regionName}] was not found`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (i < cmpPath.length - 1) {
|
|
43
|
+
// look for layouts inside if this is not the last path fragment
|
|
44
|
+
|
|
45
|
+
const layout = components.find((cmp: PageComponent) => {
|
|
46
|
+
return cmp.type === XP_COMPONENT_TYPE.LAYOUT && prefixLayoutPath(contentType, cmp.path) === parentPath;
|
|
47
|
+
});
|
|
48
|
+
if (!layout) {
|
|
49
|
+
throw `Layout [${parentPath}] not found among components, but needed for component [${JSON.stringify(cmpPath, null, 2)}]`;
|
|
50
|
+
}
|
|
51
|
+
if (!layout.regions) {
|
|
52
|
+
layout.regions = {};
|
|
53
|
+
}
|
|
54
|
+
currentTree = layout.regions;
|
|
55
|
+
}
|
|
56
|
+
} // for
|
|
57
|
+
|
|
58
|
+
return currentRegion;
|
|
59
|
+
}
|