@cooperco/cooper-component-library 0.1.127 → 0.1.128

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.
@@ -0,0 +1,566 @@
1
+ /**
2
+ * Inline tabModule.bodyCopy: replace the Entry Link to the bodyCopy content
3
+ * type with a RichText field directly on tabModule.
4
+ *
5
+ * Pattern mirrors 0054 (create → transformEntries → deleteField) and 0044
6
+ * (makeRequest to resolve linked entry data). changeFieldId renames the temp
7
+ * RichText field back to `bodyCopy` after the Link field is removed.
8
+ *
9
+ * Source RichText validations match 0047-create-body-copy.cjs so existing
10
+ * documents (including highlightedText embeds) remain valid.
11
+ *
12
+ * Content-type cleanup (delete leftover bodyCopy entries + content type) is
13
+ * intentionally in 0096 — makeRequest at migration start only works after this
14
+ * migration has already dropped the Link field (see 0090 → 0091 split).
15
+ *
16
+ * Sandbox note: some tabModules retain Link fields pointing at Body Copy
17
+ * entries that no longer exist (orphaned references). Missing links are
18
+ * skipped with a log so the migration can finish; those tabs end up with an
19
+ * empty inline bodyCopy and need content re-authored if still used.
20
+ *
21
+ * down() reverses the data migration: for each tabModule it recreates a
22
+ * bodyCopy entry from the inline RichText, publishes it, and restores the
23
+ * Link field. Requires 0096 down to have recreated the bodyCopy content type.
24
+ */
25
+
26
+ const BODY_COPY_RICH_TEXT_VALIDATIONS = [
27
+ {
28
+ enabledMarks: ['bold', 'italic', 'underline'],
29
+ message: 'Only bold, italic, and underline marks are allowed',
30
+ },
31
+ {
32
+ enabledNodeTypes: [
33
+ 'ordered-list',
34
+ 'unordered-list',
35
+ 'hr',
36
+ 'hyperlink',
37
+ 'embedded-entry-inline',
38
+ ],
39
+ },
40
+ {
41
+ nodes: {
42
+ 'embedded-entry-inline': [
43
+ {
44
+ linkContentType: ['highlightedText'],
45
+ message: 'You can only embedd Higlighted Text.',
46
+ },
47
+ ],
48
+ },
49
+ },
50
+ ]
51
+
52
+ /**
53
+ * contentful-sdk-core's errorHandler builds errors as:
54
+ * error.name = data.sys.id // e.g. "NotFound"
55
+ * error.message = JSON.stringify({ status, statusText, message, details, request, requestId })
56
+ * so status/sys are NOT top-level properties — they live in the JSON message
57
+ * (and name). Plain Axios / CMA shapes may still expose status/sys/response.
58
+ *
59
+ * @param {unknown} error
60
+ * @returns {Record<string, unknown>}
61
+ */
62
+ const summarizeErrorShape = (error) => {
63
+ if (!error || typeof error !== 'object') {
64
+ return { typeof: typeof error, value: String(error) }
65
+ }
66
+
67
+ const err = /** @type {Record<string, unknown>} */ (error)
68
+ let parsedMessage = null
69
+ if (typeof err.message === 'string') {
70
+ try {
71
+ parsedMessage = JSON.parse(err.message)
72
+ } catch {
73
+ parsedMessage = null
74
+ }
75
+ }
76
+
77
+ return {
78
+ name: err.name,
79
+ status: err.status,
80
+ statusCode: err.statusCode,
81
+ sys: err.sys,
82
+ responseStatus:
83
+ err.response && typeof err.response === 'object'
84
+ ? /** @type {Record<string, unknown>} */ (err.response).status
85
+ : undefined,
86
+ responseSys:
87
+ err.response && typeof err.response === 'object'
88
+ ? /** @type {Record<string, unknown>} */ (
89
+ /** @type {Record<string, unknown>} */ (err.response).data || {}
90
+ ).sys
91
+ : undefined,
92
+ messagePreview:
93
+ typeof err.message === 'string' ? err.message.slice(0, 500) : err.message,
94
+ parsedMessageStatus:
95
+ parsedMessage && typeof parsedMessage === 'object'
96
+ ? /** @type {Record<string, unknown>} */ (parsedMessage).status
97
+ : undefined,
98
+ parsedMessageSys:
99
+ parsedMessage && typeof parsedMessage === 'object'
100
+ ? /** @type {Record<string, unknown>} */ (parsedMessage).sys
101
+ : undefined,
102
+ keys: Object.keys(err),
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @param {unknown} error
108
+ * @returns {boolean}
109
+ */
110
+ const isNotFoundError = (error) => {
111
+ if (!error || typeof error !== 'object') {
112
+ return false
113
+ }
114
+
115
+ const err = /** @type {Record<string, unknown>} */ (error)
116
+
117
+ // contentful-sdk-core: error.name = data.sys.id ("NotFound")
118
+ if (err.name === 'NotFound') {
119
+ return true
120
+ }
121
+
122
+ if (err.status === 404 || err.statusCode === 404) {
123
+ return true
124
+ }
125
+
126
+ if (
127
+ err.response &&
128
+ typeof err.response === 'object' &&
129
+ /** @type {Record<string, unknown>} */ (err.response).status === 404
130
+ ) {
131
+ return true
132
+ }
133
+
134
+ const topSys = err.sys
135
+ if (
136
+ topSys &&
137
+ typeof topSys === 'object' &&
138
+ /** @type {Record<string, unknown>} */ (topSys).id === 'NotFound'
139
+ ) {
140
+ return true
141
+ }
142
+
143
+ if (err.response && typeof err.response === 'object') {
144
+ const data = /** @type {Record<string, unknown>} */ (err.response).data
145
+ if (
146
+ data &&
147
+ typeof data === 'object' &&
148
+ /** @type {Record<string, unknown>} */ (data).sys &&
149
+ typeof /** @type {Record<string, unknown>} */ (data).sys === 'object' &&
150
+ /** @type {Record<string, unknown>} */ (
151
+ /** @type {Record<string, unknown>} */ (data).sys
152
+ ).id === 'NotFound'
153
+ ) {
154
+ return true
155
+ }
156
+ }
157
+
158
+ // contentful-sdk-core packs status into error.message JSON
159
+ if (typeof err.message === 'string') {
160
+ try {
161
+ const parsed = JSON.parse(err.message)
162
+ if (parsed && typeof parsed === 'object') {
163
+ if (parsed.status === 404) {
164
+ return true
165
+ }
166
+ if (parsed.sys && parsed.sys.id === 'NotFound') {
167
+ return true
168
+ }
169
+ }
170
+ } catch {
171
+ // message is not JSON — ignore
172
+ }
173
+ if (/\bNotFound\b/.test(err.message) && /\b404\b/.test(err.message)) {
174
+ return true
175
+ }
176
+ }
177
+
178
+ return false
179
+ }
180
+
181
+ /**
182
+ * @param {unknown} value
183
+ * @returns {boolean}
184
+ */
185
+ const isEntryLink = (value) =>
186
+ !!value &&
187
+ typeof value === 'object' &&
188
+ /** @type {Record<string, unknown>} */ (value).sys &&
189
+ typeof /** @type {Record<string, unknown>} */ (value).sys === 'object' &&
190
+ /** @type {Record<string, unknown>} */ (
191
+ /** @type {Record<string, unknown>} */ (value).sys
192
+ ).type === 'Link' &&
193
+ !!/** @type {Record<string, unknown>} */ (
194
+ /** @type {Record<string, unknown>} */ (value).sys
195
+ ).id
196
+
197
+ /**
198
+ * @param {unknown} value
199
+ * @returns {boolean}
200
+ */
201
+ const isRichTextDocument = (value) =>
202
+ !!value &&
203
+ typeof value === 'object' &&
204
+ /** @type {Record<string, unknown>} */ (value).nodeType === 'document'
205
+
206
+ /**
207
+ * @param {Function} makeRequest
208
+ * @param {string} entryId
209
+ * @returns {Promise<Record<string, unknown>|null>}
210
+ */
211
+ const fetchEntry = async (makeRequest, entryId) => {
212
+ try {
213
+ return await makeRequest({
214
+ method: 'GET',
215
+ url: `/entries/${entryId}`,
216
+ })
217
+ } catch (error) {
218
+ process.stdout.write(
219
+ `[0095] makeRequest error fetching entry ${entryId}: ` +
220
+ `${JSON.stringify(summarizeErrorShape(error))}\n`
221
+ )
222
+ if (isNotFoundError(error)) {
223
+ return null
224
+ }
225
+ throw error
226
+ }
227
+ }
228
+
229
+ /**
230
+ * @param {Function} makeRequest
231
+ * @param {Record<string, unknown>} entry
232
+ * @returns {Promise<Record<string, unknown>>}
233
+ */
234
+ const publishEntry = async (makeRequest, entry) => {
235
+ const entryId = /** @type {Record<string, unknown>} */ (entry.sys).id
236
+ const version = /** @type {Record<string, unknown>} */ (entry.sys).version
237
+ return await makeRequest({
238
+ method: 'PUT',
239
+ url: `/entries/${entryId}/published`,
240
+ headers: {
241
+ 'X-Contentful-Version': String(version),
242
+ },
243
+ })
244
+ }
245
+
246
+ /**
247
+ * Create or update a bodyCopy entry for one tabModule locale, then publish.
248
+ * One bodyCopy entry is reused across locales for the same tabModule (cached).
249
+ *
250
+ * @param {object} params
251
+ * @param {Function} params.makeRequest
252
+ * @param {Map<string, string>} params.bodyCopyIdByTabId
253
+ * @param {string} params.tabModuleId
254
+ * @param {string} params.currentLocale
255
+ * @param {unknown} params.richText
256
+ * @param {string} params.entryName
257
+ * @param {string|undefined} params.existingBodyCopyId
258
+ * @returns {Promise<string|null>} bodyCopy entry id, or null if skipped
259
+ */
260
+ const upsertBodyCopyEntry = async ({
261
+ makeRequest,
262
+ bodyCopyIdByTabId,
263
+ tabModuleId,
264
+ currentLocale,
265
+ richText,
266
+ entryName,
267
+ existingBodyCopyId,
268
+ }) => {
269
+ const cachedId = bodyCopyIdByTabId.get(tabModuleId)
270
+ const targetId = existingBodyCopyId || cachedId
271
+
272
+ if (targetId) {
273
+ const existing = await fetchEntry(makeRequest, targetId)
274
+ if (existing) {
275
+ const fields = {
276
+ ...(existing.fields || {}),
277
+ entryName: {
278
+ ...((existing.fields && existing.fields.entryName) || {}),
279
+ [currentLocale]: entryName,
280
+ },
281
+ bodyCopy: {
282
+ ...((existing.fields && existing.fields.bodyCopy) || {}),
283
+ [currentLocale]: richText,
284
+ },
285
+ }
286
+
287
+ const updated = await makeRequest({
288
+ method: 'PUT',
289
+ url: `/entries/${targetId}`,
290
+ headers: {
291
+ 'X-Contentful-Content-Type': 'bodyCopy',
292
+ 'X-Contentful-Version': String(existing.sys.version),
293
+ },
294
+ data: { fields },
295
+ })
296
+
297
+ await publishEntry(makeRequest, updated)
298
+ bodyCopyIdByTabId.set(tabModuleId, targetId)
299
+ process.stdout.write(
300
+ `[0095 down] Updated bodyCopy ${targetId} for tabModule ${tabModuleId} ` +
301
+ `(locale ${currentLocale})\n`
302
+ )
303
+ return targetId
304
+ }
305
+
306
+ process.stdout.write(
307
+ `[0095 down] Existing bodyCopy ${targetId} missing; creating a new entry ` +
308
+ `for tabModule ${tabModuleId} (locale ${currentLocale})\n`
309
+ )
310
+ }
311
+
312
+ const created = await makeRequest({
313
+ method: 'POST',
314
+ url: `/entries`,
315
+ headers: {
316
+ 'X-Contentful-Content-Type': 'bodyCopy',
317
+ },
318
+ data: {
319
+ fields: {
320
+ entryName: {
321
+ [currentLocale]: entryName,
322
+ },
323
+ bodyCopy: {
324
+ [currentLocale]: richText,
325
+ },
326
+ },
327
+ },
328
+ })
329
+
330
+ const published = await publishEntry(makeRequest, created)
331
+ const newId = published.sys.id
332
+ bodyCopyIdByTabId.set(tabModuleId, newId)
333
+ process.stdout.write(
334
+ `[0095 down] Created bodyCopy ${newId} for tabModule ${tabModuleId} ` +
335
+ `(locale ${currentLocale})\n`
336
+ )
337
+ return newId
338
+ }
339
+
340
+ module.exports = {
341
+ // @ts-check
342
+ /** @type { import('contentful-migration').MigrationFunction } */
343
+ up: function (migration, { makeRequest }) {
344
+ const tabModule = migration.editContentType('tabModule')
345
+ /** @type {Set<string>} */
346
+ const skippedBodyCopyIds = new Set()
347
+
348
+ // 1. Temp RichText field (bodyCopy is still the Link field).
349
+ tabModule
350
+ .createField('bodyCopyRichText')
351
+ .name('Body Copy')
352
+ .type('RichText')
353
+ .required(false)
354
+ .validations(BODY_COPY_RICH_TEXT_VALIDATIONS)
355
+
356
+ // Keep editor order consistent with the old reference field (before cta).
357
+ tabModule.moveField('bodyCopyRichText').beforeField('cta')
358
+
359
+ // 2. Copy RichText from each linked bodyCopy entry into the temp field.
360
+ migration.transformEntries({
361
+ contentType: 'tabModule',
362
+ from: ['bodyCopy'],
363
+ to: ['bodyCopyRichText'],
364
+ transformEntryForLocale: async (fromFields, currentLocale, { id }) => {
365
+ const link = fromFields.bodyCopy?.[currentLocale]
366
+ if (!link?.sys?.id) {
367
+ return
368
+ }
369
+
370
+ const linkedBodyCopyId = link.sys.id
371
+ let linkedEntry
372
+ try {
373
+ linkedEntry = await makeRequest({
374
+ method: 'GET',
375
+ url: `/entries/${linkedBodyCopyId}`,
376
+ })
377
+ } catch (error) {
378
+ process.stdout.write(
379
+ `[0095] makeRequest error for bodyCopy ${linkedBodyCopyId} ` +
380
+ `(tabModule ${id}, locale ${currentLocale}): ` +
381
+ `${JSON.stringify(summarizeErrorShape(error))}\n`
382
+ )
383
+
384
+ if (isNotFoundError(error)) {
385
+ skippedBodyCopyIds.add(linkedBodyCopyId)
386
+ process.stdout.write(
387
+ `[0095] Skipping missing bodyCopy entry ${linkedBodyCopyId} ` +
388
+ `(referenced by tabModule ${id}, locale ${currentLocale})\n`
389
+ )
390
+ return
391
+ }
392
+ throw error
393
+ }
394
+
395
+ const richText =
396
+ linkedEntry.fields?.bodyCopy?.[currentLocale] ??
397
+ linkedEntry.fields?.bodyCopy?.['en-US']
398
+
399
+ if (!richText) {
400
+ process.stdout.write(
401
+ `[0095] Skipping bodyCopy entry ${linkedBodyCopyId} with empty RichText ` +
402
+ `(referenced by tabModule ${id}, locale ${currentLocale})\n`
403
+ )
404
+ return
405
+ }
406
+
407
+ return {
408
+ bodyCopyRichText: richText,
409
+ }
410
+ },
411
+ })
412
+
413
+ // 3. Drop the old Entry Link field.
414
+ tabModule.deleteField('bodyCopy')
415
+
416
+ // 4. Rename temp RichText → bodyCopy (preserves migrated values).
417
+ tabModule.changeFieldId('bodyCopyRichText', 'bodyCopy')
418
+ },
419
+
420
+ /** @type { import('contentful-migration').MigrationFunction } */
421
+ down: async function (migration, { makeRequest }) {
422
+ // Idempotent: if bodyCopy is already a Link, a prior down completed.
423
+ const contentType = await makeRequest({
424
+ method: 'GET',
425
+ url: '/content_types/tabModule',
426
+ })
427
+ const bodyCopyField = (contentType.fields || []).find(
428
+ (field) => field.id === 'bodyCopy'
429
+ )
430
+ if (bodyCopyField?.type === 'Link') {
431
+ process.stdout.write(
432
+ '[0095 down] tabModule.bodyCopy is already a Link; skipping rollback\n'
433
+ )
434
+ return
435
+ }
436
+ if (bodyCopyField?.type !== 'RichText') {
437
+ process.stdout.write(
438
+ `[0095 down] Unexpected tabModule.bodyCopy type ` +
439
+ `"${bodyCopyField?.type ?? 'missing'}"; skipping rollback\n`
440
+ )
441
+ return
442
+ }
443
+
444
+ const tabModule = migration.editContentType('tabModule')
445
+ /** @type {Map<string, string>} */
446
+ const bodyCopyIdByTabId = new Map()
447
+
448
+ // Temp Link field while bodyCopy is still the inline RichText.
449
+ tabModule
450
+ .createField('bodyCopyLink')
451
+ .name('Body Copy')
452
+ .type('Link')
453
+ .linkType('Entry')
454
+ .required(false)
455
+ .validations([{ linkContentType: ['bodyCopy'] }])
456
+
457
+ tabModule.moveField('bodyCopyLink').beforeField('cta')
458
+
459
+ migration.transformEntries({
460
+ contentType: 'tabModule',
461
+ from: ['bodyCopy', 'bodyCopyLink', 'entryName', 'title'],
462
+ to: ['bodyCopyLink'],
463
+ transformEntryForLocale: async (fromFields, currentLocale, { id }) => {
464
+ const existingLink = fromFields.bodyCopyLink?.[currentLocale]
465
+ const inlineBodyCopy = fromFields.bodyCopy?.[currentLocale]
466
+
467
+ // Already restored to a Link for this locale — reuse / refresh.
468
+ if (isEntryLink(existingLink)) {
469
+ const existingId = existingLink.sys.id
470
+ if (isRichTextDocument(inlineBodyCopy)) {
471
+ const entryName =
472
+ fromFields.entryName?.[currentLocale] ||
473
+ fromFields.title?.[currentLocale] ||
474
+ `Tab Module ${id}`
475
+ await upsertBodyCopyEntry({
476
+ makeRequest,
477
+ bodyCopyIdByTabId,
478
+ tabModuleId: id,
479
+ currentLocale,
480
+ richText: inlineBodyCopy,
481
+ entryName,
482
+ existingBodyCopyId: existingId,
483
+ })
484
+ } else {
485
+ bodyCopyIdByTabId.set(id, existingId)
486
+ process.stdout.write(
487
+ `[0095 down] Reusing existing bodyCopyLink ${existingId} ` +
488
+ `for tabModule ${id} (locale ${currentLocale})\n`
489
+ )
490
+ }
491
+ return {
492
+ bodyCopyLink: {
493
+ sys: {
494
+ type: 'Link',
495
+ linkType: 'Entry',
496
+ id: existingId,
497
+ },
498
+ },
499
+ }
500
+ }
501
+
502
+ // No inline RichText for this locale — nothing to restore.
503
+ if (!isRichTextDocument(inlineBodyCopy)) {
504
+ if (bodyCopyIdByTabId.has(id)) {
505
+ return {
506
+ bodyCopyLink: {
507
+ sys: {
508
+ type: 'Link',
509
+ linkType: 'Entry',
510
+ id: bodyCopyIdByTabId.get(id),
511
+ },
512
+ },
513
+ }
514
+ }
515
+ process.stdout.write(
516
+ `[0095 down] Skipping tabModule ${id} locale ${currentLocale}: ` +
517
+ `no inline RichText bodyCopy to restore\n`
518
+ )
519
+ return
520
+ }
521
+
522
+ const entryName =
523
+ fromFields.entryName?.[currentLocale] ||
524
+ fromFields.title?.[currentLocale] ||
525
+ `Tab Module ${id}`
526
+
527
+ let bodyCopyId
528
+ try {
529
+ bodyCopyId = await upsertBodyCopyEntry({
530
+ makeRequest,
531
+ bodyCopyIdByTabId,
532
+ tabModuleId: id,
533
+ currentLocale,
534
+ richText: inlineBodyCopy,
535
+ entryName,
536
+ existingBodyCopyId: bodyCopyIdByTabId.get(id),
537
+ })
538
+ } catch (error) {
539
+ process.stdout.write(
540
+ `[0095 down] Failed restoring bodyCopy for tabModule ${id} ` +
541
+ `(locale ${currentLocale}): ` +
542
+ `${JSON.stringify(summarizeErrorShape(error))}\n`
543
+ )
544
+ throw error
545
+ }
546
+
547
+ if (!bodyCopyId) {
548
+ return
549
+ }
550
+
551
+ return {
552
+ bodyCopyLink: {
553
+ sys: {
554
+ type: 'Link',
555
+ linkType: 'Entry',
556
+ id: bodyCopyId,
557
+ },
558
+ },
559
+ }
560
+ },
561
+ })
562
+
563
+ tabModule.deleteField('bodyCopy')
564
+ tabModule.changeFieldId('bodyCopyLink', 'bodyCopy')
565
+ },
566
+ }
@@ -0,0 +1,151 @@
1
+ const addEntryNameField = require('../helpers/addEntryNameField.cjs')
2
+
3
+ /**
4
+ * Delete the obsolete bodyCopy content type after 0095 inlined its RichText
5
+ * onto tabModule.
6
+ *
7
+ * Only tabModule referenced bodyCopy (0048-create-tab-module.cjs). After 0095
8
+ * removed that Link field, nothing else links to it — safe to wipe entries and
9
+ * delete the content type.
10
+ *
11
+ * Entry deletion uses makeRequest at migration start (execution of 0096), which
12
+ * is valid here because 0095 has already been applied to the environment.
13
+ */
14
+
15
+ const BODY_COPY_RICH_TEXT_VALIDATIONS = [
16
+ {
17
+ enabledMarks: ['bold', 'italic', 'underline'],
18
+ message: 'Only bold, italic, and underline marks are allowed',
19
+ },
20
+ {
21
+ enabledNodeTypes: [
22
+ 'ordered-list',
23
+ 'unordered-list',
24
+ 'hr',
25
+ 'hyperlink',
26
+ 'embedded-entry-inline',
27
+ ],
28
+ },
29
+ {
30
+ nodes: {
31
+ 'embedded-entry-inline': [
32
+ {
33
+ linkContentType: ['highlightedText'],
34
+ message: 'You can only embedd Higlighted Text.',
35
+ },
36
+ ],
37
+ },
38
+ },
39
+ ]
40
+
41
+ /**
42
+ * Unpublish (if needed) and delete a single entry via the CMA.
43
+ * Ignores 404 so retries / races are safe.
44
+ *
45
+ * @param {Function} makeRequest
46
+ * @param {string} entryId
47
+ */
48
+ async function deleteEntry(makeRequest, entryId) {
49
+ let entry
50
+ try {
51
+ entry = await makeRequest({
52
+ method: 'GET',
53
+ url: `/entries/${entryId}`,
54
+ })
55
+ } catch (error) {
56
+ if (error?.status === 404 || error?.response?.status === 404) {
57
+ return
58
+ }
59
+ throw error
60
+ }
61
+
62
+ if (entry.sys.publishedVersion) {
63
+ try {
64
+ await makeRequest({
65
+ method: 'DELETE',
66
+ url: `/entries/${entryId}/published`,
67
+ headers: {
68
+ 'X-Contentful-Version': String(entry.sys.version),
69
+ },
70
+ })
71
+ entry = await makeRequest({
72
+ method: 'GET',
73
+ url: `/entries/${entryId}`,
74
+ })
75
+ } catch (error) {
76
+ if (error?.status !== 404 && error?.response?.status !== 404) {
77
+ throw error
78
+ }
79
+ return
80
+ }
81
+ }
82
+
83
+ try {
84
+ await makeRequest({
85
+ method: 'DELETE',
86
+ url: `/entries/${entryId}`,
87
+ headers: {
88
+ 'X-Contentful-Version': String(entry.sys.version),
89
+ },
90
+ })
91
+ } catch (error) {
92
+ if (error?.status !== 404 && error?.response?.status !== 404) {
93
+ throw error
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Delete every entry of the given content type (paginated).
100
+ *
101
+ * @param {Function} makeRequest
102
+ * @param {string} contentTypeId
103
+ */
104
+ async function deleteAllEntriesOfContentType(makeRequest, contentTypeId) {
105
+ const limit = 100
106
+
107
+ // Re-query from skip 0 after each page of deletes — indexes shift as
108
+ // entries drop, so a single ascending skip walk would miss rows.
109
+ for (;;) {
110
+ const page = await makeRequest({
111
+ method: 'GET',
112
+ url: `/entries?content_type=${contentTypeId}&limit=${limit}&skip=0&sys.archivedAt[exists]=false`,
113
+ })
114
+
115
+ const items = page.items ?? []
116
+ if (items.length === 0) {
117
+ break
118
+ }
119
+
120
+ for (const item of items) {
121
+ await deleteEntry(makeRequest, item.sys.id)
122
+ }
123
+ }
124
+ }
125
+
126
+ module.exports = {
127
+ // @ts-check
128
+ /** @type { import('contentful-migration').MigrationFunction } */
129
+ up: async function (migration, { makeRequest }) {
130
+ await deleteAllEntriesOfContentType(makeRequest, 'bodyCopy')
131
+ migration.deleteContentType('bodyCopy')
132
+ },
133
+
134
+ /** @type { import('contentful-migration').MigrationFunction } */
135
+ down: function (migration) {
136
+ const bodyCopy = migration.createContentType('bodyCopy', {
137
+ name: 'Body Copy',
138
+ displayField: 'entryName',
139
+ description: 'Manage the body copy module',
140
+ })
141
+
142
+ addEntryNameField(bodyCopy)
143
+
144
+ bodyCopy
145
+ .createField('bodyCopy')
146
+ .name('Body Copy')
147
+ .type('RichText')
148
+ .required(true)
149
+ .validations(BODY_COPY_RICH_TEXT_VALIDATIONS)
150
+ },
151
+ }