@strapi/core 5.36.1 → 5.37.1
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/core-api/controller/index.d.ts.map +1 -1
- package/dist/core-api/controller/index.js +17 -16
- package/dist/core-api/controller/index.js.map +1 -1
- package/dist/core-api/controller/index.mjs +17 -16
- package/dist/core-api/controller/index.mjs.map +1 -1
- package/dist/core-api/routes/index.js +15 -2
- package/dist/core-api/routes/index.js.map +1 -1
- package/dist/core-api/routes/index.mjs +15 -2
- package/dist/core-api/routes/index.mjs.map +1 -1
- package/dist/core-api/routes/validation/content-type.d.ts +5 -1
- package/dist/core-api/routes/validation/content-type.d.ts.map +1 -1
- package/dist/core-api/routes/validation/content-type.js +10 -0
- package/dist/core-api/routes/validation/content-type.js.map +1 -1
- package/dist/core-api/routes/validation/content-type.mjs +10 -0
- package/dist/core-api/routes/validation/content-type.mjs.map +1 -1
- package/dist/migrations/database/5.0.0-discard-drafts.d.ts.map +1 -1
- package/dist/migrations/database/5.0.0-discard-drafts.js +508 -3
- package/dist/migrations/database/5.0.0-discard-drafts.js.map +1 -1
- package/dist/migrations/database/5.0.0-discard-drafts.mjs +509 -4
- package/dist/migrations/database/5.0.0-discard-drafts.mjs.map +1 -1
- package/dist/package.json.js +14 -14
- package/dist/package.json.mjs +14 -14
- package/dist/services/content-api/index.d.ts +6 -3
- package/dist/services/content-api/index.d.ts.map +1 -1
- package/dist/services/content-api/index.js +165 -3
- package/dist/services/content-api/index.js.map +1 -1
- package/dist/services/content-api/index.mjs +147 -4
- package/dist/services/content-api/index.mjs.map +1 -1
- package/dist/services/document-service/draft-and-publish.d.ts +16 -2
- package/dist/services/document-service/draft-and-publish.d.ts.map +1 -1
- package/dist/services/document-service/draft-and-publish.js +53 -0
- package/dist/services/document-service/draft-and-publish.js.map +1 -1
- package/dist/services/document-service/draft-and-publish.mjs +53 -2
- package/dist/services/document-service/draft-and-publish.mjs.map +1 -1
- package/dist/services/document-service/params.d.ts +24 -0
- package/dist/services/document-service/params.d.ts.map +1 -1
- package/dist/services/document-service/params.js +33 -0
- package/dist/services/document-service/params.js.map +1 -1
- package/dist/services/document-service/params.mjs +31 -1
- package/dist/services/document-service/params.mjs.map +1 -1
- package/dist/services/document-service/repository.d.ts.map +1 -1
- package/dist/services/document-service/repository.js +165 -4
- package/dist/services/document-service/repository.js.map +1 -1
- package/dist/services/document-service/repository.mjs +167 -6
- package/dist/services/document-service/repository.mjs.map +1 -1
- package/dist/services/document-service/transform/query.d.ts.map +1 -1
- package/dist/services/document-service/transform/query.js +39 -3
- package/dist/services/document-service/transform/query.js.map +1 -1
- package/dist/services/document-service/transform/query.mjs +37 -1
- package/dist/services/document-service/transform/query.mjs.map +1 -1
- package/dist/services/server/register-routes.js +3 -0
- package/dist/services/server/register-routes.js.map +1 -1
- package/dist/services/server/register-routes.mjs +3 -0
- package/dist/services/server/register-routes.mjs.map +1 -1
- package/package.json +14 -14
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
import { createId } from '@paralleldrive/cuid2';
|
|
2
2
|
import { contentTypes } from '@strapi/utils';
|
|
3
3
|
import createDebug from 'debug';
|
|
4
|
-
import { getComponentJoinTableName, getComponentJoinColumnEntityName, getComponentJoinColumnInverseName, getComponentTypeColumn } from '../../utils/transform-content-types-to-models.mjs';
|
|
4
|
+
import { getComponentJoinTableName, getComponentJoinColumnEntityName, getComponentJoinColumnInverseName, getComponentTypeColumn, getDzJoinTableName } from '../../utils/transform-content-types-to-models.mjs';
|
|
5
5
|
|
|
6
|
+
const resolveComponentMetadataUidCandidates = (componentUid)=>{
|
|
7
|
+
if (componentUid.startsWith('component::')) {
|
|
8
|
+
return [
|
|
9
|
+
componentUid,
|
|
10
|
+
componentUid.slice('component::'.length)
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
return [
|
|
14
|
+
componentUid,
|
|
15
|
+
`component::${componentUid}`
|
|
16
|
+
];
|
|
17
|
+
};
|
|
18
|
+
const getComponentMetadataByUid = (componentUid)=>{
|
|
19
|
+
for (const candidate of resolveComponentMetadataUidCandidates(componentUid)){
|
|
20
|
+
try {
|
|
21
|
+
const meta = strapi.db.metadata.get(candidate);
|
|
22
|
+
if (meta) {
|
|
23
|
+
return meta;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
};
|
|
6
31
|
const DEFAULT_PRIMARY_KEY_COLUMN = 'id';
|
|
7
32
|
/**
|
|
8
33
|
* Determines the primary-key column name for a schema, handling the various shapes
|
|
@@ -138,6 +163,219 @@ const DEFAULT_PRIMARY_KEY_COLUMN = 'id';
|
|
|
138
163
|
uid,
|
|
139
164
|
publishedToDraftMap
|
|
140
165
|
});
|
|
166
|
+
// (1) Media: one shared table owned by upload plugin; we are the "related" side (related_id).
|
|
167
|
+
// Copy rows so draft entity ids get the same file links as their published pair.
|
|
168
|
+
await copyMediaMorphToDraftsForContentType({
|
|
169
|
+
trx,
|
|
170
|
+
uid,
|
|
171
|
+
publishedToDraftMap
|
|
172
|
+
});
|
|
173
|
+
// (2) Other morph: per-attribute tables where we are the "source" side (join column = our entity id).
|
|
174
|
+
// Copy rows so draft entity ids get the same polymorphic links. Skips upload table by name.
|
|
175
|
+
await copySourceSideMorphRelationsForContentType({
|
|
176
|
+
trx,
|
|
177
|
+
uid,
|
|
178
|
+
publishedToDraftMap
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Returns the upload plugin's morph join table info (e.g. files_related_morphs) if present.
|
|
183
|
+
* This is the single table used by all media fields across content types and components.
|
|
184
|
+
*/ function getUploadMorphTableInfo() {
|
|
185
|
+
const fileMeta = strapi.db.metadata.get('plugin::upload.file');
|
|
186
|
+
if (!fileMeta?.attributes) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const relatedAttr = fileMeta.attributes.related;
|
|
190
|
+
const joinTable = relatedAttr?.joinTable;
|
|
191
|
+
const morphColumn = joinTable?.morphColumn ?? relatedAttr?.morphColumn;
|
|
192
|
+
if (!joinTable?.name || !joinTable?.joinColumn?.name || !morphColumn) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
tableName: joinTable.name,
|
|
197
|
+
joinColumnName: joinTable.joinColumn.name,
|
|
198
|
+
relatedIdColumnName: morphColumn.idColumn.name,
|
|
199
|
+
relatedTypeColumnName: morphColumn.typeColumn.name
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Unified morph copy: duplicate rows in a morph join table, rewriting one column from original ids to draft ids.
|
|
204
|
+
* Used for both (1) media: upload table, we are "related" side, filter by related_type; (2) other morph: we are "source" side, no filter.
|
|
205
|
+
* @param columnToRewrite - the column that holds our entity id (related_id in upload table, entity_id in source-side tables).
|
|
206
|
+
* @param filter - optional: only copy rows where filter.column = filter.value (e.g. related_type = uid for media).
|
|
207
|
+
*/ async function copyMorphRowsByIdMap({ trx, tableName, columnToRewrite, idMap, filter, contextReason }) {
|
|
208
|
+
if (idMap.size === 0) return;
|
|
209
|
+
const hasTable = await ensureTableExists(trx, tableName);
|
|
210
|
+
if (!hasTable) return;
|
|
211
|
+
const originalIds = Array.from(idMap.keys());
|
|
212
|
+
const chunks = chunkArray(originalIds, getBatchSize(trx, 500));
|
|
213
|
+
for (const chunk of chunks){
|
|
214
|
+
let q = trx(tableName).select('*').whereIn(columnToRewrite, chunk);
|
|
215
|
+
if (filter) {
|
|
216
|
+
q = q.where(filter.column, filter.value);
|
|
217
|
+
}
|
|
218
|
+
const rows = await q;
|
|
219
|
+
if (rows.length === 0) continue;
|
|
220
|
+
const toInsert = [];
|
|
221
|
+
for (const row of rows){
|
|
222
|
+
const originalId = normalizeId(row[columnToRewrite]);
|
|
223
|
+
if (originalId == null) continue;
|
|
224
|
+
const newId = idMap.get(originalId);
|
|
225
|
+
if (newId == null) continue;
|
|
226
|
+
const { id, ...rest } = row;
|
|
227
|
+
toInsert.push({
|
|
228
|
+
...rest,
|
|
229
|
+
[columnToRewrite]: newId
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (toInsert.length > 0) {
|
|
233
|
+
await insertRelationsWithDuplicateHandling({
|
|
234
|
+
trx,
|
|
235
|
+
tableName,
|
|
236
|
+
relations: toInsert,
|
|
237
|
+
context: {
|
|
238
|
+
reason: contextReason,
|
|
239
|
+
tableName
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Same as copyMorphRowsByIdMap but for (originalId, draftId) pairs when one original can map to multiple drafts (e.g. cloned components).
|
|
247
|
+
*/ async function copyMorphRowsByPairs({ trx, tableName, columnToRewrite, pairs, filter, contextReason }) {
|
|
248
|
+
if (pairs.length === 0) return;
|
|
249
|
+
const hasTable = await ensureTableExists(trx, tableName);
|
|
250
|
+
if (!hasTable) return;
|
|
251
|
+
const originalIds = [
|
|
252
|
+
...new Set(pairs.map((p)=>p.originalId))
|
|
253
|
+
];
|
|
254
|
+
const chunks = chunkArray(originalIds, getBatchSize(trx, 500));
|
|
255
|
+
for (const chunk of chunks){
|
|
256
|
+
let q = trx(tableName).select('*').whereIn(columnToRewrite, chunk);
|
|
257
|
+
if (filter) {
|
|
258
|
+
q = q.where(filter.column, filter.value);
|
|
259
|
+
}
|
|
260
|
+
const rows = await q;
|
|
261
|
+
if (rows.length === 0) continue;
|
|
262
|
+
const toInsert = [];
|
|
263
|
+
for (const row of rows){
|
|
264
|
+
const originalId = normalizeId(row[columnToRewrite]);
|
|
265
|
+
if (originalId == null) continue;
|
|
266
|
+
const draftIds = pairs.filter((p)=>p.originalId === originalId).map((p)=>p.draftId);
|
|
267
|
+
for (const draftId of draftIds){
|
|
268
|
+
const { id, ...rest } = row;
|
|
269
|
+
toInsert.push({
|
|
270
|
+
...rest,
|
|
271
|
+
[columnToRewrite]: draftId
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (toInsert.length > 0) {
|
|
276
|
+
await insertRelationsWithDuplicateHandling({
|
|
277
|
+
trx,
|
|
278
|
+
tableName,
|
|
279
|
+
relations: toInsert,
|
|
280
|
+
context: {
|
|
281
|
+
reason: contextReason,
|
|
282
|
+
tableName
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const recordClonedComponentPair = (clonedComponentPairsCache, componentUid, originalId, draftId)=>{
|
|
289
|
+
if (!clonedComponentPairsCache) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
let pairMap = clonedComponentPairsCache.get(componentUid);
|
|
293
|
+
if (!pairMap) {
|
|
294
|
+
pairMap = new Map();
|
|
295
|
+
clonedComponentPairsCache.set(componentUid, pairMap);
|
|
296
|
+
}
|
|
297
|
+
pairMap.set(`${originalId}:${draftId}`, draftId);
|
|
298
|
+
};
|
|
299
|
+
/** Copy media morph rows for this content type (upload table; we are the "related" side). */ async function copyMediaMorphToDraftsForContentType({ trx, uid, publishedToDraftMap }) {
|
|
300
|
+
const morphInfo = getUploadMorphTableInfo();
|
|
301
|
+
if (!morphInfo) return;
|
|
302
|
+
await copyMorphRowsByIdMap({
|
|
303
|
+
trx,
|
|
304
|
+
tableName: morphInfo.tableName,
|
|
305
|
+
columnToRewrite: morphInfo.relatedIdColumnName,
|
|
306
|
+
idMap: publishedToDraftMap,
|
|
307
|
+
filter: {
|
|
308
|
+
column: morphInfo.relatedTypeColumnName,
|
|
309
|
+
value: uid
|
|
310
|
+
},
|
|
311
|
+
contextReason: 'media-morph-draft'
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/** Copy source-side morph rows for this content type (per-attribute tables; we are the "source" side). */ async function copySourceSideMorphRelationsForContentType({ trx, uid, publishedToDraftMap }) {
|
|
315
|
+
const meta = strapi.db.metadata.get(uid);
|
|
316
|
+
if (!meta) return;
|
|
317
|
+
const uploadMorph = getUploadMorphTableInfo();
|
|
318
|
+
const uploadTableName = uploadMorph?.tableName ?? null;
|
|
319
|
+
for (const attribute of Object.values(meta.attributes)){
|
|
320
|
+
if (attribute.type !== 'relation' || !attribute.joinTable?.morphColumn) continue;
|
|
321
|
+
const joinTable = attribute.joinTable;
|
|
322
|
+
if (joinTable.name === uploadTableName) continue;
|
|
323
|
+
// Dynamic zones are represented as morph relations in DB metadata, but their rows live in
|
|
324
|
+
// the shared component join table (`*_cmps`) and are already handled by copyComponentRelations.
|
|
325
|
+
if (joinTable.name.includes('_cmps')) continue;
|
|
326
|
+
const sourceColumnName = joinTable.joinColumn?.name;
|
|
327
|
+
if (!sourceColumnName) continue;
|
|
328
|
+
await copyMorphRowsByIdMap({
|
|
329
|
+
trx,
|
|
330
|
+
tableName: joinTable.name,
|
|
331
|
+
columnToRewrite: sourceColumnName,
|
|
332
|
+
idMap: publishedToDraftMap,
|
|
333
|
+
contextReason: 'source-side-morph-draft'
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Copy source-side morph relations for cloned components (each component type's morph attributes).
|
|
339
|
+
*/ async function copySourceSideMorphRelationsForClonedComponents({ trx, clonedComponentPairsCache }) {
|
|
340
|
+
const uploadMorph = getUploadMorphTableInfo();
|
|
341
|
+
const uploadTableName = uploadMorph?.tableName ?? null;
|
|
342
|
+
for (const [componentType, cloneMap] of clonedComponentPairsCache.entries()){
|
|
343
|
+
let componentMeta;
|
|
344
|
+
try {
|
|
345
|
+
componentMeta = getComponentMetadataByUid(componentType);
|
|
346
|
+
} catch {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
for (const attribute of Object.values(componentMeta.attributes || {})){
|
|
350
|
+
if (attribute.type !== 'relation' || !attribute.joinTable?.morphColumn) continue;
|
|
351
|
+
const joinTable = attribute.joinTable;
|
|
352
|
+
if (joinTable.name === uploadTableName) continue;
|
|
353
|
+
// Component dynamic zones also use `*_cmps` join tables and are cloned recursively.
|
|
354
|
+
// Copying them here would reinsert original component ids and create shared draft/published rows.
|
|
355
|
+
if (joinTable.name.includes('_cmps')) continue;
|
|
356
|
+
const sourceColumnName = joinTable.joinColumn?.name;
|
|
357
|
+
if (!sourceColumnName) continue;
|
|
358
|
+
const pairs = [];
|
|
359
|
+
for (const [key, newComponentId] of cloneMap.entries()){
|
|
360
|
+
const originalId = Number(key.split(':')[0]);
|
|
361
|
+
if (!Number.isNaN(originalId)) {
|
|
362
|
+
pairs.push({
|
|
363
|
+
originalId,
|
|
364
|
+
draftId: newComponentId
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (pairs.length > 0) {
|
|
369
|
+
await copyMorphRowsByPairs({
|
|
370
|
+
trx,
|
|
371
|
+
tableName: joinTable.name,
|
|
372
|
+
columnToRewrite: sourceColumnName,
|
|
373
|
+
pairs,
|
|
374
|
+
contextReason: 'source-side-morph-draft-pairs'
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
141
379
|
}
|
|
142
380
|
/**
|
|
143
381
|
* Splits large input arrays into smaller batches so we can run SQL queries without
|
|
@@ -651,7 +889,7 @@ const debug = createDebug('strapi::migration::discard-drafts');
|
|
|
651
889
|
* cloning the same component type thousands of times.
|
|
652
890
|
*/ const getComponentMeta = (componentUid)=>{
|
|
653
891
|
if (!componentMetaCache.has(componentUid)) {
|
|
654
|
-
const meta =
|
|
892
|
+
const meta = getComponentMetadataByUid(componentUid);
|
|
655
893
|
componentMetaCache.set(componentUid, meta ?? null);
|
|
656
894
|
}
|
|
657
895
|
return componentMetaCache.get(componentUid);
|
|
@@ -842,6 +1080,12 @@ const debug = createDebug('strapi::migration::discard-drafts');
|
|
|
842
1080
|
if (attribute.type !== 'relation' || !attribute.joinTable) {
|
|
843
1081
|
continue;
|
|
844
1082
|
}
|
|
1083
|
+
const rawComponentAttribute = strapi.components[componentUid]?.attributes?.[attributeName];
|
|
1084
|
+
// Component and dynamic zone links need deep cloning of target component rows.
|
|
1085
|
+
// Skip the generic join-table copier so the recursive clone path can handle them.
|
|
1086
|
+
if (rawComponentAttribute?.type === 'component' || rawComponentAttribute?.type === 'dynamiczone') {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
845
1089
|
const joinTable = attribute.joinTable;
|
|
846
1090
|
const sourceColumnName = joinTable.joinColumn.name;
|
|
847
1091
|
const targetColumnName = joinTable.inverseJoinColumn.name;
|
|
@@ -908,7 +1152,7 @@ const debug = createDebug('strapi::migration::discard-drafts');
|
|
|
908
1152
|
/**
|
|
909
1153
|
* Clones a component row (including nested relations) so the newly created draft entity
|
|
910
1154
|
* owns its own copy, matching what the document service would have produced.
|
|
911
|
-
*/ async function cloneComponentInstance({ trx, componentUid, componentId, parentUid, parentPublishedToDraftMap, draftMapCache, isForDraftEntity = true, reverseMapCache }) {
|
|
1155
|
+
*/ async function cloneComponentInstance({ trx, componentUid, componentId, parentUid, parentPublishedToDraftMap, draftMapCache, clonedComponentPairsCache, isForDraftEntity = true, reverseMapCache }) {
|
|
912
1156
|
const componentMeta = getComponentMeta(componentUid);
|
|
913
1157
|
if (!componentMeta) {
|
|
914
1158
|
return componentId;
|
|
@@ -975,7 +1219,74 @@ const debug = createDebug('strapi::migration::discard-drafts');
|
|
|
975
1219
|
if (Number.isNaN(newComponentId)) {
|
|
976
1220
|
throw new Error(`Invalid cloned component identifier for ${componentUid} (id: ${componentId})`);
|
|
977
1221
|
}
|
|
1222
|
+
recordClonedComponentPair(clonedComponentPairsCache, componentUid, Number(componentPrimaryKey), newComponentId);
|
|
978
1223
|
await cloneComponentRelationJoinTables(trx, componentMeta, componentUid, Number(componentPrimaryKey), newComponentId, parentUid, parentPublishedToDraftMap, draftMapCache, isForDraftEntity, reverseMapCache);
|
|
1224
|
+
// Clone nested components (component and dynamiczone attributes) so draft has its own copy
|
|
1225
|
+
const componentSchema = strapi.components[componentUid];
|
|
1226
|
+
const collectionName = componentSchema?.collectionName;
|
|
1227
|
+
if (collectionName && componentSchema?.attributes) {
|
|
1228
|
+
const identifiers = strapi.db.metadata.identifiers;
|
|
1229
|
+
const entityIdCol = getComponentJoinColumnEntityName(identifiers);
|
|
1230
|
+
const componentIdCol = getComponentJoinColumnInverseName(identifiers);
|
|
1231
|
+
const componentTypeCol = getComponentTypeColumn(identifiers);
|
|
1232
|
+
const fieldCol = identifiers.FIELD_COLUMN;
|
|
1233
|
+
// Use the raw component schema here: DB metadata transforms component/DZ attrs into relations,
|
|
1234
|
+
// so their original `type` values are only available on the schema definition.
|
|
1235
|
+
for (const [attrName, attr] of Object.entries(componentSchema.attributes || {})){
|
|
1236
|
+
if (attr.type === 'component') {
|
|
1237
|
+
const nestedJoinTableName = getComponentJoinTableName(collectionName, identifiers);
|
|
1238
|
+
if (!await ensureTableExists(trx, nestedJoinTableName)) continue;
|
|
1239
|
+
const nestedRows = await trx(nestedJoinTableName).select('*').where(entityIdCol, componentPrimaryKey).where(componentTypeCol, attr.component).where(fieldCol, attrName);
|
|
1240
|
+
for (const row of nestedRows){
|
|
1241
|
+
const nestedId = Number(row[componentIdCol]);
|
|
1242
|
+
if (Number.isNaN(nestedId)) continue;
|
|
1243
|
+
const newNestedId = await cloneComponentInstance({
|
|
1244
|
+
trx,
|
|
1245
|
+
componentUid: attr.component,
|
|
1246
|
+
componentId: nestedId,
|
|
1247
|
+
parentUid: componentUid,
|
|
1248
|
+
parentPublishedToDraftMap,
|
|
1249
|
+
draftMapCache,
|
|
1250
|
+
clonedComponentPairsCache,
|
|
1251
|
+
isForDraftEntity,
|
|
1252
|
+
reverseMapCache
|
|
1253
|
+
});
|
|
1254
|
+
const { id, ...rest } = row;
|
|
1255
|
+
await insertRowWithDuplicateHandling(trx, nestedJoinTableName, {
|
|
1256
|
+
...rest,
|
|
1257
|
+
[entityIdCol]: newComponentId,
|
|
1258
|
+
[componentIdCol]: newNestedId
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
} else if (attr.type === 'dynamiczone' && Array.isArray(attr.components)) {
|
|
1262
|
+
const dzJoinTableName = getDzJoinTableName(collectionName, identifiers);
|
|
1263
|
+
if (!await ensureTableExists(trx, dzJoinTableName)) continue;
|
|
1264
|
+
const dzRows = await trx(dzJoinTableName).select('*').where(entityIdCol, componentPrimaryKey).where(fieldCol, attrName);
|
|
1265
|
+
for (const row of dzRows){
|
|
1266
|
+
const nestedType = row[componentTypeCol];
|
|
1267
|
+
const nestedId = Number(row[componentIdCol]);
|
|
1268
|
+
if (!nestedType || Number.isNaN(nestedId)) continue;
|
|
1269
|
+
const newNestedId = await cloneComponentInstance({
|
|
1270
|
+
trx,
|
|
1271
|
+
componentUid: nestedType,
|
|
1272
|
+
componentId: nestedId,
|
|
1273
|
+
parentUid: componentUid,
|
|
1274
|
+
parentPublishedToDraftMap,
|
|
1275
|
+
draftMapCache,
|
|
1276
|
+
clonedComponentPairsCache,
|
|
1277
|
+
isForDraftEntity,
|
|
1278
|
+
reverseMapCache
|
|
1279
|
+
});
|
|
1280
|
+
const { id, ...rest } = row;
|
|
1281
|
+
await insertRowWithDuplicateHandling(trx, dzJoinTableName, {
|
|
1282
|
+
...rest,
|
|
1283
|
+
[entityIdCol]: newComponentId,
|
|
1284
|
+
[componentIdCol]: newNestedId
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
979
1290
|
return newComponentId;
|
|
980
1291
|
}
|
|
981
1292
|
/**
|
|
@@ -1611,7 +1922,7 @@ const debug = createDebug('strapi::migration::discard-drafts');
|
|
|
1611
1922
|
];
|
|
1612
1923
|
const draftMapCache = new Map();
|
|
1613
1924
|
for (const componentType of componentTypes){
|
|
1614
|
-
const componentMeta =
|
|
1925
|
+
const componentMeta = getComponentMetadataByUid(componentType);
|
|
1615
1926
|
if (!componentMeta) continue;
|
|
1616
1927
|
for (const [, attr] of Object.entries(componentMeta.attributes || {})){
|
|
1617
1928
|
if (attr.type !== 'relation' || !attr.joinTable) continue;
|
|
@@ -1714,6 +2025,152 @@ const debug = createDebug('strapi::migration::discard-drafts');
|
|
|
1714
2025
|
}
|
|
1715
2026
|
}
|
|
1716
2027
|
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Fix published entities' component relations so they point to published targets (not draft).
|
|
2030
|
+
* After duplicating nested components, the published tree keeps the original components;
|
|
2031
|
+
* their relation _lnk rows must point to published targets so the published view resolves them.
|
|
2032
|
+
*/ async function fixPublishedComponentRelationTargets({ trx, uid }) {
|
|
2033
|
+
const meta = strapi.db.metadata.get(uid);
|
|
2034
|
+
if (!meta) return;
|
|
2035
|
+
const contentType = strapi.contentTypes[uid];
|
|
2036
|
+
const collectionName = contentType?.collectionName;
|
|
2037
|
+
if (!collectionName) return;
|
|
2038
|
+
const identifiers = strapi.db.metadata.identifiers;
|
|
2039
|
+
const joinTableName = getComponentJoinTableName(collectionName, identifiers);
|
|
2040
|
+
const entityIdColumn = getComponentJoinColumnEntityName(identifiers);
|
|
2041
|
+
const componentIdColumn = getComponentJoinColumnInverseName(identifiers);
|
|
2042
|
+
const componentTypeColumn = getComponentTypeColumn(identifiers);
|
|
2043
|
+
if (!await ensureTableExists(trx, joinTableName)) return;
|
|
2044
|
+
const publishedIds = (await trx(meta.tableName).select('id').whereNotNull('published_at')).map((r)=>Number(r.id));
|
|
2045
|
+
if (publishedIds.length === 0) return;
|
|
2046
|
+
const reverseMapCache = new Map();
|
|
2047
|
+
const typeToIds = new Map();
|
|
2048
|
+
let currentLevelByType = new Map();
|
|
2049
|
+
currentLevelByType.set(uid, publishedIds);
|
|
2050
|
+
const maxLevels = 10;
|
|
2051
|
+
for(let level = 0; level < maxLevels; level += 1){
|
|
2052
|
+
const nextLevelByType = new Map();
|
|
2053
|
+
if (level === 0) {
|
|
2054
|
+
const chunks = chunkArray(publishedIds, getBatchSize(trx, 1000));
|
|
2055
|
+
for (const chunk of chunks){
|
|
2056
|
+
const rows = await trx(joinTableName).select(componentIdColumn, componentTypeColumn).whereIn(entityIdColumn, chunk);
|
|
2057
|
+
for (const row of rows){
|
|
2058
|
+
const type = row[componentTypeColumn];
|
|
2059
|
+
const id = Number(row[componentIdColumn]);
|
|
2060
|
+
if (!type || Number.isNaN(id)) continue;
|
|
2061
|
+
if (!typeToIds.has(type)) typeToIds.set(type, new Set());
|
|
2062
|
+
typeToIds.get(type).add(id);
|
|
2063
|
+
if (!nextLevelByType.has(type)) nextLevelByType.set(type, []);
|
|
2064
|
+
nextLevelByType.get(type).push(id);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
} else {
|
|
2068
|
+
for (const [compType, ids] of currentLevelByType.entries()){
|
|
2069
|
+
if (compType === uid) continue;
|
|
2070
|
+
const compSchema = strapi.components[compType];
|
|
2071
|
+
if (!compSchema?.collectionName) continue;
|
|
2072
|
+
const nestedTable = getComponentJoinTableName(compSchema.collectionName, identifiers);
|
|
2073
|
+
if (!await ensureTableExists(trx, nestedTable)) continue;
|
|
2074
|
+
const idChunks = chunkArray(ids, getBatchSize(trx, 1000));
|
|
2075
|
+
for (const idChunk of idChunks){
|
|
2076
|
+
const rows = await trx(nestedTable).select(componentIdColumn, componentTypeColumn).whereIn(entityIdColumn, idChunk);
|
|
2077
|
+
for (const row of rows){
|
|
2078
|
+
const type = row[componentTypeColumn];
|
|
2079
|
+
const id = Number(row[componentIdColumn]);
|
|
2080
|
+
if (!type || Number.isNaN(id)) continue;
|
|
2081
|
+
if (!typeToIds.has(type)) typeToIds.set(type, new Set());
|
|
2082
|
+
typeToIds.get(type).add(id);
|
|
2083
|
+
if (!nextLevelByType.has(type)) nextLevelByType.set(type, []);
|
|
2084
|
+
nextLevelByType.get(type).push(id);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
if (nextLevelByType.size === 0) break;
|
|
2090
|
+
currentLevelByType = nextLevelByType;
|
|
2091
|
+
}
|
|
2092
|
+
for (const [componentType, componentIds] of typeToIds.entries()){
|
|
2093
|
+
const componentMeta = getComponentMetadataByUid(componentType);
|
|
2094
|
+
if (!componentMeta) continue;
|
|
2095
|
+
const ids = Array.from(componentIds);
|
|
2096
|
+
if (ids.length === 0) continue;
|
|
2097
|
+
for (const [, attr] of Object.entries(componentMeta.attributes || {})){
|
|
2098
|
+
if (attr.type !== 'relation' || !attr.joinTable) continue;
|
|
2099
|
+
const targetUid = attr.target;
|
|
2100
|
+
if (!targetUid) continue;
|
|
2101
|
+
const targetContentType = strapi.contentTypes[targetUid];
|
|
2102
|
+
if (!targetContentType?.options?.draftAndPublish) continue;
|
|
2103
|
+
const relationJoinTable = attr.joinTable.name;
|
|
2104
|
+
const sourceColumn = attr.joinTable.joinColumn.name;
|
|
2105
|
+
const targetColumn = attr.joinTable.inverseJoinColumn.name;
|
|
2106
|
+
if (!await ensureTableExists(trx, relationJoinTable)) continue;
|
|
2107
|
+
const relations = await trx(relationJoinTable).whereIn(sourceColumn, ids).select('id', sourceColumn, targetColumn);
|
|
2108
|
+
if (relations.length === 0) continue;
|
|
2109
|
+
const targetMeta = strapi.db.metadata.get(targetUid);
|
|
2110
|
+
if (!targetMeta) continue;
|
|
2111
|
+
const targetIdList = [
|
|
2112
|
+
...new Set(relations.map((r)=>r[targetColumn]).filter(Boolean))
|
|
2113
|
+
];
|
|
2114
|
+
if (targetIdList.length === 0) continue;
|
|
2115
|
+
const targets = await trx(targetMeta.tableName).whereIn('id', targetIdList).select('id', 'published_at');
|
|
2116
|
+
const targetState = new Map(targets.map((t)=>[
|
|
2117
|
+
Number(t.id),
|
|
2118
|
+
t.published_at !== null ? 'published' : 'draft'
|
|
2119
|
+
]));
|
|
2120
|
+
const draftToPublished = await getDraftToPublishedMap(trx, targetUid, reverseMapCache);
|
|
2121
|
+
if (!draftToPublished || draftToPublished.size === 0) continue;
|
|
2122
|
+
const relationsToUpdate = [];
|
|
2123
|
+
for (const relation of relations){
|
|
2124
|
+
const targetId = Number(relation[targetColumn]);
|
|
2125
|
+
if (targetState.get(targetId) !== 'draft') continue;
|
|
2126
|
+
const publishedId = draftToPublished.get(targetId);
|
|
2127
|
+
if (publishedId == null) continue;
|
|
2128
|
+
relationsToUpdate.push({
|
|
2129
|
+
relationId: relation.id,
|
|
2130
|
+
sourceId: Number(relation[sourceColumn]),
|
|
2131
|
+
oldTargetId: targetId,
|
|
2132
|
+
newTargetId: publishedId
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
if (relationsToUpdate.length > 0) {
|
|
2136
|
+
const updateChunks = chunkArray(relationsToUpdate, getBatchSize(trx, 100));
|
|
2137
|
+
for (const updateChunk of updateChunks){
|
|
2138
|
+
const existingRelationMap = await buildExistingRelationMap({
|
|
2139
|
+
trx,
|
|
2140
|
+
tableName: relationJoinTable,
|
|
2141
|
+
sourceColumnName: sourceColumn,
|
|
2142
|
+
targetColumnName: targetColumn,
|
|
2143
|
+
updates: updateChunk.map((u)=>({
|
|
2144
|
+
sourceId: u.sourceId,
|
|
2145
|
+
newTargetId: u.newTargetId
|
|
2146
|
+
})),
|
|
2147
|
+
batchSize: getBatchSize(trx, 100)
|
|
2148
|
+
});
|
|
2149
|
+
for (const update of updateChunk){
|
|
2150
|
+
const key = `${update.sourceId}_${update.newTargetId}`;
|
|
2151
|
+
const existingRelationId = existingRelationMap.get(key);
|
|
2152
|
+
if (existingRelationId != null && existingRelationId !== update.relationId) {
|
|
2153
|
+
await trx(relationJoinTable).where('id', update.relationId).delete();
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
2156
|
+
try {
|
|
2157
|
+
await trx(relationJoinTable).where('id', update.relationId).update({
|
|
2158
|
+
[targetColumn]: update.newTargetId
|
|
2159
|
+
});
|
|
2160
|
+
existingRelationMap.set(key, update.relationId);
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
if (isDuplicateEntryError(error)) {
|
|
2163
|
+
await trx(relationJoinTable).where('id', update.relationId).delete();
|
|
2164
|
+
} else {
|
|
2165
|
+
throw error;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
1717
2174
|
/**
|
|
1718
2175
|
* Copy component relations from published entries to draft entries
|
|
1719
2176
|
*/ async function copyComponentRelations({ trx, uid, publishedToDraftMap }) {
|
|
@@ -1748,6 +2205,7 @@ const debug = createDebug('strapi::migration::discard-drafts');
|
|
|
1748
2205
|
continue;
|
|
1749
2206
|
}
|
|
1750
2207
|
const componentCloneCache = new Map();
|
|
2208
|
+
const clonedComponentPairsCache = new Map();
|
|
1751
2209
|
const componentTargetDraftMapCache = new Map();
|
|
1752
2210
|
const componentTargetReverseMapCache = new Map();
|
|
1753
2211
|
const componentHierarchyCaches = {
|
|
@@ -1819,6 +2277,7 @@ const debug = createDebug('strapi::migration::discard-drafts');
|
|
|
1819
2277
|
parentUid: uid,
|
|
1820
2278
|
parentPublishedToDraftMap: publishedToDraftMap,
|
|
1821
2279
|
draftMapCache: componentTargetDraftMapCache,
|
|
2280
|
+
clonedComponentPairsCache,
|
|
1822
2281
|
isForDraftEntity: true,
|
|
1823
2282
|
reverseMapCache: componentTargetReverseMapCache
|
|
1824
2283
|
});
|
|
@@ -1909,6 +2368,40 @@ const debug = createDebug('strapi::migration::discard-drafts');
|
|
|
1909
2368
|
});
|
|
1910
2369
|
}
|
|
1911
2370
|
}
|
|
2371
|
+
// Copy media morph rows for cloned components so draft components have the same media as published
|
|
2372
|
+
const morphInfo = getUploadMorphTableInfo();
|
|
2373
|
+
if (morphInfo) {
|
|
2374
|
+
for (const [componentType, cloneMap] of clonedComponentPairsCache.entries()){
|
|
2375
|
+
const pairs = [];
|
|
2376
|
+
for (const [key, newComponentId] of cloneMap.entries()){
|
|
2377
|
+
const originalId = Number(key.split(':')[0]);
|
|
2378
|
+
if (!Number.isNaN(originalId)) {
|
|
2379
|
+
pairs.push({
|
|
2380
|
+
originalId,
|
|
2381
|
+
draftId: newComponentId
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (pairs.length > 0) {
|
|
2386
|
+
await copyMorphRowsByPairs({
|
|
2387
|
+
trx,
|
|
2388
|
+
tableName: morphInfo.tableName,
|
|
2389
|
+
columnToRewrite: morphInfo.relatedIdColumnName,
|
|
2390
|
+
pairs,
|
|
2391
|
+
filter: {
|
|
2392
|
+
column: morphInfo.relatedTypeColumnName,
|
|
2393
|
+
value: componentType
|
|
2394
|
+
},
|
|
2395
|
+
contextReason: 'media-morph-draft-components'
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
// Copy any other morph relations where cloned components are the source (morphToOne/morphToMany)
|
|
2401
|
+
await copySourceSideMorphRelationsForClonedComponents({
|
|
2402
|
+
trx,
|
|
2403
|
+
clonedComponentPairsCache
|
|
2404
|
+
});
|
|
1912
2405
|
}
|
|
1913
2406
|
}
|
|
1914
2407
|
/**
|
|
@@ -1977,6 +2470,18 @@ const debug = createDebug('strapi::migration::discard-drafts');
|
|
|
1977
2470
|
}
|
|
1978
2471
|
strapi.log.info('[discard-drafts] Stage 4/5 complete');
|
|
1979
2472
|
/**
|
|
2473
|
+
* Fix published entities' component relations to point to published targets (not draft).
|
|
2474
|
+
* Ensures the published view can resolve nested component relations after duplication.
|
|
2475
|
+
*/ strapi.log.info('[discard-drafts] Stage 4b/5 – fixing published component relations to published targets');
|
|
2476
|
+
for (const model of dpModels){
|
|
2477
|
+
debug(` • fixing published component relations for ${model.uid}`);
|
|
2478
|
+
await fixPublishedComponentRelationTargets({
|
|
2479
|
+
trx,
|
|
2480
|
+
uid: model.uid
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
strapi.log.info('[discard-drafts] Stage 4b/5 complete');
|
|
2484
|
+
/**
|
|
1980
2485
|
* Update JoinColumn relations (foreign keys) to point to draft versions
|
|
1981
2486
|
* This matches discard() behavior: drafts relate to drafts
|
|
1982
2487
|
*/ strapi.log.info('[discard-drafts] Stage 5/5 – updating foreign key references to draft targets');
|