@cooperco/cooper-component-library 0.1.126 → 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.
- package/dist/cms/0094-add-anchor-to-page-modules.cjs +114 -0
- package/dist/cms/0095-tabmodule-inline-body-copy.cjs +566 -0
- package/dist/cms/0096-delete-body-copy-content-type.cjs +151 -0
- package/dist/cms/carousel.query.ts +1 -0
- package/dist/cms/chartModule.query.ts +1 -0
- package/dist/cms/containerCollectionModule.query.ts +1 -0
- package/dist/cms/containerModule.query.ts +1 -0
- package/dist/cms/contentModule.query.ts +1 -0
- package/dist/cms/contentful/migrations/scripts/0094-add-anchor-to-page-modules.cjs +114 -0
- package/dist/cms/contentful/migrations/scripts/0095-tabmodule-inline-body-copy.cjs +566 -0
- package/dist/cms/contentful/migrations/scripts/0096-delete-body-copy-content-type.cjs +151 -0
- package/dist/cms/contentful/queries/carousel.query.js +1 -0
- package/dist/cms/contentful/queries/carousel.query.ts +1 -0
- package/dist/cms/contentful/queries/chartModule.query.js +1 -0
- package/dist/cms/contentful/queries/chartModule.query.ts +1 -0
- package/dist/cms/contentful/queries/containerCollectionModule.query.js +1 -0
- package/dist/cms/contentful/queries/containerCollectionModule.query.ts +1 -0
- package/dist/cms/contentful/queries/containerModule.query.js +1 -0
- package/dist/cms/contentful/queries/containerModule.query.ts +1 -0
- package/dist/cms/contentful/queries/contentModule.query.js +1 -0
- package/dist/cms/contentful/queries/contentModule.query.ts +1 -0
- package/dist/cms/contentful/queries/logoCollection.query.js +1 -0
- package/dist/cms/contentful/queries/logoCollection.query.ts +1 -0
- package/dist/cms/contentful/queries/page.query.js +11 -0
- package/dist/cms/contentful/queries/page.query.ts +11 -0
- package/dist/cms/contentful/queries/placeholderModule.query.js +2 -0
- package/dist/cms/contentful/queries/placeholderModule.query.ts +2 -0
- package/dist/cms/contentful/queries/productModule.query.js +14 -3
- package/dist/cms/contentful/queries/productModule.query.ts +14 -3
- package/dist/cms/contentful/queries/splitModule.query.js +2 -0
- package/dist/cms/contentful/queries/splitModule.query.ts +2 -0
- package/dist/cms/contentful/queries/testimonial.query.js +1 -0
- package/dist/cms/contentful/queries/testimonial.query.ts +1 -0
- package/dist/cms/contentful/queries/tileCollection.query.js +1 -0
- package/dist/cms/contentful/queries/tileCollection.query.ts +1 -0
- package/dist/cms/logoCollection.query.ts +1 -0
- package/dist/cms/migrations/scripts/0094-add-anchor-to-page-modules.cjs +114 -0
- package/dist/cms/migrations/scripts/0095-tabmodule-inline-body-copy.cjs +566 -0
- package/dist/cms/migrations/scripts/0096-delete-body-copy-content-type.cjs +151 -0
- package/dist/cms/page.query.ts +11 -0
- package/dist/cms/placeholderModule.query.ts +2 -0
- package/dist/cms/productModule.query.ts +14 -3
- package/dist/cms/queries/carousel.query.ts +1 -0
- package/dist/cms/queries/chartModule.query.ts +1 -0
- package/dist/cms/queries/containerCollectionModule.query.ts +1 -0
- package/dist/cms/queries/containerModule.query.ts +1 -0
- package/dist/cms/queries/contentModule.query.ts +1 -0
- package/dist/cms/queries/logoCollection.query.ts +1 -0
- package/dist/cms/queries/page.query.ts +11 -0
- package/dist/cms/queries/placeholderModule.query.ts +2 -0
- package/dist/cms/queries/productModule.query.ts +14 -3
- package/dist/cms/queries/splitModule.query.ts +2 -0
- package/dist/cms/queries/testimonial.query.ts +1 -0
- package/dist/cms/queries/tileCollection.query.ts +1 -0
- package/dist/cms/scripts/0094-add-anchor-to-page-modules.cjs +114 -0
- package/dist/cms/scripts/0095-tabmodule-inline-body-copy.cjs +566 -0
- package/dist/cms/scripts/0096-delete-body-copy-content-type.cjs +151 -0
- package/dist/cms/splitModule.query.ts +2 -0
- package/dist/cms/testimonial.query.ts +1 -0
- package/dist/cms/tileCollection.query.ts +1 -0
- package/dist/lib/component-lib.js +3886 -3848
- package/dist/lib/component-lib.umd.cjs +23 -23
- package/dist/lib/style.css +1 -1
- package/dist/types/src/index.d.ts +2 -1
- package/dist/types/src/types.d.ts +1 -0
- package/dist/types/src/utils/normalizeAnchor.d.ts +12 -0
- package/package.json +1 -9
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
/** @type { import('contentful-migration').MigrationFunction } */
|
|
3
|
+
up: function (migration) {
|
|
4
|
+
const carouselModule = migration.editContentType('carouselModule')
|
|
5
|
+
carouselModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
6
|
+
carouselModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
7
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const chartModule = migration.editContentType('chartModule')
|
|
11
|
+
chartModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
12
|
+
chartModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
13
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const containerCollectionModule = migration.editContentType('containerCollectionModule')
|
|
17
|
+
containerCollectionModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
18
|
+
containerCollectionModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
19
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const containerModule = migration.editContentType('containerModule')
|
|
23
|
+
containerModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
24
|
+
containerModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
25
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const contentModule = migration.editContentType('contentModule')
|
|
29
|
+
contentModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
30
|
+
contentModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
31
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const logoCollectionModule = migration.editContentType('logoCollectionModule')
|
|
35
|
+
logoCollectionModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
36
|
+
logoCollectionModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
37
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const placeholderModule = migration.editContentType('placeholderModule')
|
|
41
|
+
placeholderModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
42
|
+
placeholderModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
43
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const productModule = migration.editContentType('productModule')
|
|
47
|
+
productModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
48
|
+
productModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
49
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const splitModule = migration.editContentType('splitModule')
|
|
53
|
+
splitModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
54
|
+
splitModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
55
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const tabModule = migration.editContentType('tabModule')
|
|
59
|
+
tabModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
60
|
+
tabModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
61
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const testimonialModule = migration.editContentType('testimonialModule')
|
|
65
|
+
testimonialModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
66
|
+
testimonialModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
67
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const tileCollectionModule = migration.editContentType('tileCollectionModule')
|
|
71
|
+
tileCollectionModule.createField('anchor', { name: 'Anchor', type: 'Symbol', localized: true, })
|
|
72
|
+
tileCollectionModule.changeFieldControl('anchor', 'builtin', 'singleLine', {
|
|
73
|
+
helpText: 'Set the anchor (#) for the page module.',
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
down: function (migration) {
|
|
78
|
+
const carouselModule = migration.editContentType('carouselModule')
|
|
79
|
+
carouselModule.deleteField('anchor')
|
|
80
|
+
|
|
81
|
+
const chartModule = migration.editContentType('chartModule')
|
|
82
|
+
chartModule.deleteField('anchor')
|
|
83
|
+
|
|
84
|
+
const containerCollectionModule = migration.editContentType('containerCollectionModule')
|
|
85
|
+
containerCollectionModule.deleteField('anchor')
|
|
86
|
+
|
|
87
|
+
const containerModule = migration.editContentType('containerModule')
|
|
88
|
+
containerModule.deleteField('anchor')
|
|
89
|
+
|
|
90
|
+
const contentModule = migration.editContentType('contentModule')
|
|
91
|
+
contentModule.deleteField('anchor')
|
|
92
|
+
|
|
93
|
+
const logoCollectionModule = migration.editContentType('logoCollectionModule')
|
|
94
|
+
logoCollectionModule.deleteField('anchor')
|
|
95
|
+
|
|
96
|
+
const placeholderModule = migration.editContentType('placeholderModule')
|
|
97
|
+
placeholderModule.deleteField('anchor')
|
|
98
|
+
|
|
99
|
+
const productModule = migration.editContentType('productModule')
|
|
100
|
+
productModule.deleteField('anchor')
|
|
101
|
+
|
|
102
|
+
const splitModule = migration.editContentType('splitModule')
|
|
103
|
+
splitModule.deleteField('anchor')
|
|
104
|
+
|
|
105
|
+
const tabModule = migration.editContentType('tabModule')
|
|
106
|
+
tabModule.deleteField('anchor')
|
|
107
|
+
|
|
108
|
+
const testimonialModule = migration.editContentType('testimonialModule')
|
|
109
|
+
testimonialModule.deleteField('anchor')
|
|
110
|
+
|
|
111
|
+
const tileCollectionModule = migration.editContentType('tileCollectionModule')
|
|
112
|
+
tileCollectionModule.deleteField('anchor')
|
|
113
|
+
},
|
|
114
|
+
}
|
|
@@ -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
|
+
}
|