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