5
5
import chalk from 'chalk' ;
6
6
import jsonpath from 'jsonpath' ;
7
7
import got from 'got' ;
8
+ import qs from 'qs' ;
8
9
9
10
import ContentSource , { SourceOptions } from './content-source.js' ;
10
11
import ContentResult , { MediaDownload } from './content-result.js' ;
11
12
import Credentials from '../credentials.js' ;
12
13
import { Logger } from '@bluecadet/launchpad-utils' ;
13
14
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
+
14
21
/**
15
22
* Options for StrapiSource
16
23
*/
@@ -30,8 +37,8 @@ export class StrapiOptions extends SourceOptions {
30
37
super ( rest ) ;
31
38
32
39
/**
33
- * Only version `'3'` is supported currently .
34
- * @type {string }
40
+ * Versions `3` and `4` are supported .
41
+ * @type {'3'|'4' }
35
42
* @default '3'
36
43
*/
37
44
this . version = version ;
@@ -43,8 +50,10 @@ export class StrapiOptions extends SourceOptions {
43
50
this . baseUrl = baseUrl ;
44
51
45
52
/**
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> }
48
57
* @default []
49
58
*/
50
59
this . queries = queries ;
@@ -90,18 +99,215 @@ export class StrapiOptions extends SourceOptions {
90
99
}
91
100
}
92
101
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
+
93
286
class StrapiSource extends ContentSource {
287
+ /**
288
+ * @type {StrapiVersionUtils }
289
+ * @private
290
+ */
291
+ _versionUtils ;
292
+
94
293
/**
95
294
*
96
295
* @param {* } config
97
296
* @param {Logger } logger
98
297
*/
99
298
constructor ( config , logger ) {
100
299
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 } '` ) ;
104
309
}
310
+
105
311
if ( ! this . config . queries || ! this . config . queries . length ) {
106
312
throw new Error ( 'No content queries defined' ) ;
107
313
}
@@ -130,55 +336,56 @@ class StrapiSource extends ContentSource {
130
336
/**
131
337
* Recursively fetches content using the Strapi client.
132
338
*
133
- * @param {string } query
339
+ * @param {string | StrapiObjectQuery } query
134
340
* @param {string } jwt The JSON web token generated by Strapi
135
341
* @param {ContentResult } result
136
- * @param {Object } params
342
+ * @param {StrapiPagination } pagination
137
343
* @returns {Promise<Object> } Object with an 'entries' and an 'assets' array.
138
344
*/
139
345
async _fetchPages (
140
346
query ,
141
347
jwt ,
142
348
result ,
143
- params = { start : 0 , limit : 100 }
349
+ pagination = { start : 0 , limit : 100 }
144
350
) {
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 ) ;
153
353
}
354
+
355
+ const pageNum = pagination . start / pagination . limit ;
154
356
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` ;
159
358
160
- return got ( url . toString ( ) , {
359
+ this . logger . debug ( `Fetching page ${ pageNum } of ${ query . contentType } ` ) ;
360
+
361
+ return got ( this . _versionUtils . buildUrl ( query , pagination ) , {
161
362
headers : {
162
363
Authorization : `Bearer ${ jwt } `
163
364
}
164
365
} )
165
366
. json ( )
166
367
. then ( ( content ) => {
167
- if ( ! content || ! content . length ) {
368
+ const transformedContent = this . _versionUtils . transformResult ( content ) ;
369
+ if ( ! transformedContent || ! transformedContent . length ) {
168
370
// Empty result or no more pages left
169
371
return Promise . resolve ( result ) ;
170
372
}
171
373
172
- result . addDataFile ( fileName , content ) ;
374
+ result . addDataFile ( fileName , transformedContent ) ;
375
+
173
376
result . addMediaDownloads (
174
- this . _getMediaUrls ( content ) . map ( url => new MediaDownload ( { url } ) )
377
+ this . _getMediaUrls ( transformedContent ) . map ( url => new MediaDownload ( { url } ) )
175
378
) ;
176
379
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
+ ) {
178
385
// 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 ) ;
182
389
} else {
183
390
// Return combined entries + assets
184
391
return Promise . resolve ( result ) ;
0 commit comments