Skip to content

Commit 6502ded

Browse files
authored
add support for strapi v4 (#111)
* add support for strapi v4 * cleanup strapi docs
1 parent d361d3e commit 6502ded

File tree

3 files changed

+243
-30
lines changed

3 files changed

+243
-30
lines changed

.changeset/tender-guests-add.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bluecadet/launchpad-content": minor
3+
---
4+
5+
add support for strapi v4

packages/content/lib/content-sources/strapi-source.js

Lines changed: 237 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@
55
import chalk from 'chalk';
66
import jsonpath from 'jsonpath';
77
import got from 'got';
8+
import qs from 'qs';
89

910
import ContentSource, { SourceOptions } from './content-source.js';
1011
import ContentResult, { MediaDownload } from './content-result.js';
1112
import Credentials from '../credentials.js';
1213
import { Logger } from '@bluecadet/launchpad-utils';
1314

15+
/**
16+
* @typedef {Object} StrapiObjectQuery
17+
* @property {string} contentType The content type to query
18+
* @property {Object} params Query parameters. Uses `qs` library to stringify.
19+
*/
20+
1421
/**
1522
* Options for StrapiSource
1623
*/
@@ -30,8 +37,8 @@ export class StrapiOptions extends SourceOptions {
3037
super(rest);
3138

3239
/**
33-
* Only version `'3'` is supported currently.
34-
* @type {string}
40+
* Versions `3` and `4` are supported.
41+
* @type {'3'|'4'}
3542
* @default '3'
3643
*/
3744
this.version = version;
@@ -43,8 +50,10 @@ export class StrapiOptions extends SourceOptions {
4350
this.baseUrl = baseUrl;
4451

4552
/**
46-
* Queries for each type of content you want to save. One per content type. Content will be stored as numbered, paginated JSONs. You can include all query parameters supported by Strapi: https://docs-v3.strapi.io/developer-docs/latest/developer-resources/content-api/content-api.html#api-parameters
47-
* @type {Array.<string>}
53+
* Queries for each type of content you want to save. One per content type. Content will be stored as numbered, paginated JSONs.
54+
* You can include all query parameters supported by Strapi.
55+
* You can also pass an object with a `contentType` and `params` property, where `params` is an object of query parameters.
56+
* @type {Array.<string | StrapiObjectQuery>}
4857
* @default []
4958
*/
5059
this.queries = queries;
@@ -90,18 +99,215 @@ export class StrapiOptions extends SourceOptions {
9099
}
91100
}
92101

102+
/**
103+
* @typedef {Object} StrapiPagination
104+
* @property {number} start The index of the first item to fetch
105+
* @property {number} limit The number of items to fetch
106+
*/
107+
108+
class StrapiVersionUtils {
109+
/**
110+
* @type {StrapiOptions}
111+
* @private
112+
*/
113+
config;
114+
115+
/**
116+
* @type {Logger}
117+
* @private
118+
*/
119+
logger;
120+
121+
/**
122+
* @param {StrapiOptions}
123+
* @param {Logger}
124+
*/
125+
constructor(config, logger) {
126+
this.config = config;
127+
this.logger = logger;
128+
}
129+
130+
/**
131+
* @param {StrapiObjectQuery} query
132+
* @returns {string}
133+
*/
134+
buildUrl(query) {
135+
throw new Error('Not implemented');
136+
}
137+
138+
/**
139+
* @param {StrapiObjectQuery} query
140+
* @returns {boolean}
141+
*/
142+
hasPaginationParams(query) {
143+
throw new Error('Not implemented');
144+
}
145+
146+
/**
147+
* @param {object} result
148+
* @returns {object}
149+
*/
150+
transformResult(result) {
151+
return result;
152+
}
153+
154+
/**
155+
* @param {object} result
156+
* @returns {boolean}
157+
*/
158+
canFetchMore(result) {
159+
throw new Error('Not implemented');
160+
}
161+
162+
/**
163+
* @param {string} string
164+
* @returns {StrapiObjectQuery}
165+
*/
166+
parseQuery(string) {
167+
const url = new URL(string, this.config.baseUrl);
168+
const params = qs.parse(url.search.slice(1));
169+
const contentType = url.pathname.split('/').pop();
170+
return { contentType, params };
171+
}
172+
}
173+
174+
class StrapiV4 extends StrapiVersionUtils {
175+
/**
176+
* @param {StrapiObjectQuery} query
177+
* @param {StrapiPagination} [pagination]
178+
* @returns {string}
179+
*/
180+
buildUrl(query, pagination) {
181+
const url = new URL(query.contentType, this.config.baseUrl);
182+
183+
let params = query.params;
184+
185+
// only add pagination params if they arent't specified in the query object
186+
if (!this.hasPaginationParams(query)) {
187+
params = {
188+
...params,
189+
pagination: {
190+
page: (pagination.start / pagination.limit) + 1,
191+
pageSize: pagination.limit,
192+
...params?.pagination
193+
}
194+
};
195+
}
196+
197+
const search = qs.stringify(params, {
198+
encodeValuesOnly: true, // prettify url
199+
addQueryPrefix: true // add ? to beginning
200+
});
201+
202+
url.search = search;
203+
204+
return url.toString();
205+
}
206+
207+
/**
208+
* @param {StrapiObjectQuery} query
209+
* @returns {boolean}
210+
*/
211+
hasPaginationParams(query) {
212+
return query?.params?.pagination?.page !== undefined || query?.params?.pagination?.pageSize !== undefined;
213+
}
214+
215+
/**
216+
* @param {object} result
217+
* @returns {object}
218+
*/
219+
transformResult(result) {
220+
return result.data;
221+
}
222+
223+
/**
224+
* @param {object} result
225+
* @returns {boolean}
226+
*/
227+
canFetchMore(result) {
228+
if (result?.meta?.pagination) {
229+
const { page, pageCount } = result.meta.pagination;
230+
return page < pageCount;
231+
}
232+
233+
return false;
234+
}
235+
}
236+
237+
class StrapiV3 extends StrapiVersionUtils {
238+
/**
239+
* @param {StrapiObjectQuery} query
240+
* @param {StrapiPagination} [pagination]
241+
* @returns {string}
242+
*/
243+
buildUrl(query, pagination) {
244+
const url = new URL(query.contentType, this.config.baseUrl);
245+
246+
let params = query.params;
247+
248+
// only add pagination params if they arent't specified in the query object
249+
if (!this.hasPaginationParams(query)) {
250+
params = {
251+
_start: pagination.start,
252+
_limit: pagination.limit,
253+
...params
254+
};
255+
}
256+
257+
const search = qs.stringify(params, {
258+
encodeValuesOnly: true, // prettify url
259+
addQueryPrefix: true // add ? to beginning
260+
});
261+
262+
url.search = search;
263+
264+
return url.toString();
265+
}
266+
267+
/**
268+
* @param {StrapiObjectQuery} query
269+
* @returns {boolean}
270+
*/
271+
hasPaginationParams(query) {
272+
return query?.params?._start !== undefined || query?.params?._limit !== undefined;
273+
}
274+
275+
/**
276+
* @param {object} result
277+
* @returns {boolean}
278+
*/
279+
canFetchMore(result) {
280+
// strapi v3 doesn't have any pagination info in the response,
281+
// so we can't know if there are more results
282+
return true;
283+
}
284+
}
285+
93286
class StrapiSource extends ContentSource {
287+
/**
288+
* @type {StrapiVersionUtils}
289+
* @private
290+
*/
291+
_versionUtils;
292+
94293
/**
95294
*
96295
* @param {*} config
97296
* @param {Logger} logger
98297
*/
99298
constructor(config, logger) {
100299
super(StrapiSource._assembleConfig(config), logger);
101-
102-
if (!this.config.version || parseInt(this.config.version) !== 3) {
103-
throw new Error(`Strapi content source only supports Strapi v3 (requested version '${this.config.version}')`);
300+
301+
if (!this.config.version) {
302+
throw new Error('Strapi version not specified');
303+
} else if (parseInt(this.config.version) === 3) {
304+
this._versionUtils = new StrapiV3(this.config, this.logger);
305+
} else if (parseInt(this.config.version) === 4) {
306+
this._versionUtils = new StrapiV4(this.config, this.logger);
307+
} else {
308+
throw new Error(`Unsupported strapi version '${this.config.version}'`);
104309
}
310+
105311
if (!this.config.queries || !this.config.queries.length) {
106312
throw new Error('No content queries defined');
107313
}
@@ -130,55 +336,56 @@ class StrapiSource extends ContentSource {
130336
/**
131337
* Recursively fetches content using the Strapi client.
132338
*
133-
* @param {string} query
339+
* @param {string | StrapiObjectQuery} query
134340
* @param {string} jwt The JSON web token generated by Strapi
135341
* @param {ContentResult} result
136-
* @param {Object} params
342+
* @param {StrapiPagination} pagination
137343
* @returns {Promise<Object>} Object with an 'entries' and an 'assets' array.
138344
*/
139345
async _fetchPages(
140346
query,
141347
jwt,
142348
result,
143-
params = { start: 0, limit: 100 }
349+
pagination = { start: 0, limit: 100 }
144350
) {
145-
const pageNum = params.start / params.limit;
146-
const url = new URL(query, this.config.baseUrl);
147-
148-
if (!url.searchParams.has('_start')) {
149-
url.searchParams.append('_start', params.start);
150-
}
151-
if (!url.searchParams.has('_limit')) {
152-
url.searchParams.append('_limit', params.limit);
351+
if (typeof query === 'string') {
352+
query = this._versionUtils.parseQuery(query);
153353
}
354+
355+
const pageNum = pagination.start / pagination.limit;
154356

155-
const contentType = url.pathname;
156-
const fileName = `${contentType}-${pageNum.toString().padStart(this.config.pageNumZeroPad, '0')}.json`;
157-
158-
this.logger.debug(`Fetching page ${pageNum} of ${contentType}`);
357+
const fileName = `${query.contentType}-${pageNum.toString().padStart(this.config.pageNumZeroPad, '0')}.json`;
159358

160-
return got(url.toString(), {
359+
this.logger.debug(`Fetching page ${pageNum} of ${query.contentType}`);
360+
361+
return got(this._versionUtils.buildUrl(query, pagination), {
161362
headers: {
162363
Authorization: `Bearer ${jwt}`
163364
}
164365
})
165366
.json()
166367
.then((content) => {
167-
if (!content || !content.length) {
368+
const transformedContent = this._versionUtils.transformResult(content);
369+
if (!transformedContent || !transformedContent.length) {
168370
// Empty result or no more pages left
169371
return Promise.resolve(result);
170372
}
171373

172-
result.addDataFile(fileName, content);
374+
result.addDataFile(fileName, transformedContent);
375+
173376
result.addMediaDownloads(
174-
this._getMediaUrls(content).map(url => new MediaDownload({ url }))
377+
this._getMediaUrls(transformedContent).map(url => new MediaDownload({ url }))
175378
);
176379

177-
if (this.config.maxNumPages < 0 || pageNum < this.config.maxNumPages - 1) {
380+
if (
381+
!this._versionUtils.hasPaginationParams(query) &&
382+
(this.config.maxNumPages < 0 || pageNum < this.config.maxNumPages - 1) &&
383+
this._versionUtils.canFetchMore(content)
384+
) {
178385
// Fetch next page
179-
params.start = params.start || 0;
180-
params.start += params.limit;
181-
return this._fetchPages(query, jwt, result, params);
386+
pagination.start = pagination.start || 0;
387+
pagination.start += pagination.limit;
388+
return this._fetchPages(query, jwt, result, pagination);
182389
} else {
183390
// Return combined entries + assets
184391
return Promise.resolve(result);

packages/content/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"jsonpath": "^1.1.1",
3939
"markdown-it": "^12.2.0",
4040
"p-queue": "^7.1.0",
41+
"qs": "^6.11.1",
4142
"rimraf": "^3.0.2",
4243
"sanitize-filename": "^1.6.3",
4344
"sanitize-html": "^2.5.1",

0 commit comments

Comments
 (0)