@strapi/core 5.58.0 → 5.59.0

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.
Files changed (98) hide show
  1. package/dist/Strapi.d.ts.map +1 -1
  2. package/dist/Strapi.js +1 -0
  3. package/dist/Strapi.js.map +1 -1
  4. package/dist/Strapi.mjs +1 -0
  5. package/dist/Strapi.mjs.map +1 -1
  6. package/dist/ee/entitlements.d.ts.map +1 -1
  7. package/dist/ee/entitlements.js.map +1 -1
  8. package/dist/ee/entitlements.mjs.map +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1 -1
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/loaders/apis.d.ts.map +1 -1
  15. package/dist/loaders/apis.js.map +1 -1
  16. package/dist/loaders/apis.mjs.map +1 -1
  17. package/dist/loaders/plugins/get-enabled-plugins.js +2 -2
  18. package/dist/loaders/plugins/get-enabled-plugins.js.map +1 -1
  19. package/dist/loaders/plugins/get-enabled-plugins.mjs +2 -2
  20. package/dist/loaders/plugins/get-enabled-plugins.mjs.map +1 -1
  21. package/dist/middlewares/cors.d.ts.map +1 -1
  22. package/dist/middlewares/cors.js.map +1 -1
  23. package/dist/middlewares/cors.mjs.map +1 -1
  24. package/dist/migrations/database/5.0.0-discard-drafts.js +1 -1
  25. package/dist/migrations/database/5.0.0-discard-drafts.js.map +1 -1
  26. package/dist/migrations/database/5.0.0-discard-drafts.mjs +1 -1
  27. package/dist/migrations/database/5.0.0-discard-drafts.mjs.map +1 -1
  28. package/dist/migrations/first-published-at.js +1 -1
  29. package/dist/migrations/first-published-at.js.map +1 -1
  30. package/dist/migrations/first-published-at.mjs +1 -1
  31. package/dist/migrations/first-published-at.mjs.map +1 -1
  32. package/dist/package.json.js +15 -14
  33. package/dist/package.json.js.map +1 -1
  34. package/dist/package.json.mjs +15 -14
  35. package/dist/package.json.mjs.map +1 -1
  36. package/dist/registries/controllers.d.ts.map +1 -1
  37. package/dist/registries/controllers.js.map +1 -1
  38. package/dist/registries/controllers.mjs.map +1 -1
  39. package/dist/services/content-source-maps.js +6 -6
  40. package/dist/services/content-source-maps.js.map +1 -1
  41. package/dist/services/content-source-maps.mjs +6 -6
  42. package/dist/services/content-source-maps.mjs.map +1 -1
  43. package/dist/services/document-service/components.d.ts +13 -11
  44. package/dist/services/document-service/components.d.ts.map +1 -1
  45. package/dist/services/document-service/components.js +115 -66
  46. package/dist/services/document-service/components.js.map +1 -1
  47. package/dist/services/document-service/components.mjs +116 -67
  48. package/dist/services/document-service/components.mjs.map +1 -1
  49. package/dist/services/document-service/events.d.ts.map +1 -1
  50. package/dist/services/document-service/events.js +5 -1
  51. package/dist/services/document-service/events.js.map +1 -1
  52. package/dist/services/document-service/events.mjs +5 -1
  53. package/dist/services/document-service/events.mjs.map +1 -1
  54. package/dist/services/document-service/repository.d.ts.map +1 -1
  55. package/dist/services/document-service/repository.js +26 -7
  56. package/dist/services/document-service/repository.js.map +1 -1
  57. package/dist/services/document-service/repository.mjs +27 -8
  58. package/dist/services/document-service/repository.mjs.map +1 -1
  59. package/dist/services/document-service/transform/data.d.ts.map +1 -1
  60. package/dist/services/document-service/transform/data.js.map +1 -1
  61. package/dist/services/document-service/transform/data.mjs.map +1 -1
  62. package/dist/services/document-service/transform/relations/transform/default-locale.d.ts +4 -1
  63. package/dist/services/document-service/transform/relations/transform/default-locale.d.ts.map +1 -1
  64. package/dist/services/document-service/transform/relations/transform/default-locale.js +12 -9
  65. package/dist/services/document-service/transform/relations/transform/default-locale.js.map +1 -1
  66. package/dist/services/document-service/transform/relations/transform/default-locale.mjs +12 -10
  67. package/dist/services/document-service/transform/relations/transform/default-locale.mjs.map +1 -1
  68. package/dist/services/document-service/utils/clone-relations.d.ts +21 -4
  69. package/dist/services/document-service/utils/clone-relations.d.ts.map +1 -1
  70. package/dist/services/document-service/utils/clone-relations.js +619 -34
  71. package/dist/services/document-service/utils/clone-relations.js.map +1 -1
  72. package/dist/services/document-service/utils/clone-relations.mjs +619 -36
  73. package/dist/services/document-service/utils/clone-relations.mjs.map +1 -1
  74. package/dist/services/document-service/utils/unidirectional-relations.d.ts +4 -2
  75. package/dist/services/document-service/utils/unidirectional-relations.d.ts.map +1 -1
  76. package/dist/services/document-service/utils/unidirectional-relations.js +1 -10
  77. package/dist/services/document-service/utils/unidirectional-relations.js.map +1 -1
  78. package/dist/services/document-service/utils/unidirectional-relations.mjs +1 -10
  79. package/dist/services/document-service/utils/unidirectional-relations.mjs.map +1 -1
  80. package/dist/services/event-hub.d.ts.map +1 -1
  81. package/dist/services/event-hub.js +23 -5
  82. package/dist/services/event-hub.js.map +1 -1
  83. package/dist/services/event-hub.mjs +23 -5
  84. package/dist/services/event-hub.mjs.map +1 -1
  85. package/dist/services/webhook-store.js +2 -2
  86. package/dist/services/webhook-store.js.map +1 -1
  87. package/dist/services/webhook-store.mjs +2 -2
  88. package/dist/services/webhook-store.mjs.map +1 -1
  89. package/dist/utils/fetch.d.ts.map +1 -1
  90. package/dist/utils/fetch.js +44 -8
  91. package/dist/utils/fetch.js.map +1 -1
  92. package/dist/utils/fetch.mjs +37 -8
  93. package/dist/utils/fetch.mjs.map +1 -1
  94. package/dist/utils/transform-content-types-to-models.js +5 -5
  95. package/dist/utils/transform-content-types-to-models.js.map +1 -1
  96. package/dist/utils/transform-content-types-to-models.mjs +5 -5
  97. package/dist/utils/transform-content-types-to-models.mjs.map +1 -1
  98. package/package.json +15 -14
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var _ = require('lodash');
4
+ var strapiUtils = require('@strapi/utils');
4
5
  var mapRelation = require('../transform/relations/utils/map-relation.js');
6
+ var i18n = require('../transform/relations/utils/i18n.js');
7
+ var dp = require('../transform/relations/utils/dp.js');
8
+ var defaultLocale = require('../transform/relations/transform/default-locale.js');
5
9
 
6
10
  const RELATION_OPERATIONS = [
7
11
  'connect',
@@ -29,8 +33,8 @@ const hasMeaningfulRelationOperations = (value)=>{
29
33
  return Array.isArray(operationValue) ? operationValue.length > 0 : operationValue != null;
30
34
  });
31
35
  };
32
- const canTransformRelationOperationPayload = (attribute)=>{
33
- return attribute.useJoinTable !== false && attribute.relation !== 'morphToOne';
36
+ const usesJoinTable = (attribute)=>{
37
+ return attribute.useJoinTable !== false;
34
38
  };
35
39
  const isBidirectionalOneToOne = (attribute)=>{
36
40
  return attribute.relation === 'oneToOne' && [
@@ -39,9 +43,71 @@ const isBidirectionalOneToOne = (attribute)=>{
39
43
  attribute.targetAttribute
40
44
  ].some((inverseAttribute)=>inverseAttribute != null);
41
45
  };
46
+ const isMorphToOneAttribute = (attribute)=>{
47
+ return attribute.relation === 'morphToOne';
48
+ };
49
+ const isBidirectionalOneToManyMappedBy = (attribute)=>{
50
+ return attribute.relation === 'oneToMany' && attribute.mappedBy != null;
51
+ };
42
52
  const hasPopulatedRelation = (value)=>{
43
53
  return isRecord(value) && ('id' in value || 'documentId' in value);
44
54
  };
55
+ const hasPopulatedRelationList = (value)=>{
56
+ return Array.isArray(value) && value.length > 0 && value.every((item)=>isRecord(item) && ('id' in item || 'documentId' in item));
57
+ };
58
+ const getSubmittedValueAtPath = (submitted, path)=>{
59
+ return _.get(submitted, path);
60
+ };
61
+ const wasRelationSubmittedAtPath = (submitted, path)=>{
62
+ return _.has(submitted, path);
63
+ };
64
+ const isRelationUnchangedAtPath = (submitted, path, submittedValue)=>{
65
+ if (!wasRelationSubmittedAtPath(submitted, path)) {
66
+ return true;
67
+ }
68
+ const isOperationPayload = isRelationOperationPayload(submittedValue);
69
+ const hasMeaningfulOperations = isOperationPayload && hasMeaningfulRelationOperations(submittedValue);
70
+ return isOperationPayload && !hasMeaningfulOperations;
71
+ };
72
+ /**
73
+ * The nested collector walks the source entry, so a relation under a component
74
+ * the clone payload set to null still looks omitted. That copy has no owner on
75
+ * the clone and must not be queued. Missing paths are not removal: those owners
76
+ * still need their deferred copy, and a later resolution failure must still throw.
77
+ */ const isOwnerExplicitlyRemoved = (submitted, ownerPath)=>{
78
+ const segments = ownerPath.split('.');
79
+ let current = submitted;
80
+ for (const segment of segments){
81
+ if (Array.isArray(current)) {
82
+ const index = Number(segment);
83
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) {
84
+ return false;
85
+ }
86
+ current = current[index];
87
+ } else if (isRecord(current) && Object.prototype.hasOwnProperty.call(current, segment)) {
88
+ current = current[segment];
89
+ } else {
90
+ return false;
91
+ }
92
+ if (current === null) {
93
+ return true;
94
+ }
95
+ }
96
+ return false;
97
+ };
98
+ /**
99
+ * Like `traverseEntityRelations`, but also visits inline storage (`useJoinTable: false`
100
+ * and `morphToOne`). Clone follow-up must see those relations; the shared transform
101
+ * helper intentionally skips them because they are handled in processData.
102
+ */ const traverseAllEntityRelations = async (visitor, options, data)=>{
103
+ return strapiUtils.traverseEntity(async (visitorOptions, utils)=>{
104
+ const { attribute } = visitorOptions;
105
+ if (!attribute || attribute.type !== 'relation') {
106
+ return;
107
+ }
108
+ return visitor(visitorOptions, utils);
109
+ }, options, data);
110
+ };
45
111
  const collectRelationOperationOverrides = async (submittedData, contentType, getModel)=>{
46
112
  const overrides = new Map();
47
113
  await mapRelation.traverseEntityRelations(({ path, value })=>{
@@ -56,69 +122,588 @@ const collectRelationOperationOverrides = async (submittedData, contentType, get
56
122
  ...overrides.entries()
57
123
  ].sort(([leftPath], [rightPath])=>leftPath.localeCompare(rightPath));
58
124
  };
125
+ const collectNestedCloneRelationAdjustments = async (originalData, submitted, contentType, getModel, data)=>{
126
+ const adjustedData = data;
127
+ const deferredCopies = [];
128
+ const postCloneUpdates = [];
129
+ await traverseAllEntityRelations(({ attribute, key, path, schema, value })=>{
130
+ if (!attribute || !isRelationAttribute(attribute)) {
131
+ return;
132
+ }
133
+ const relationPath = path.rawWithIndices;
134
+ // Owner is the parent of the relation path (preserve indices for
135
+ // repeatable components / DZs, e.g. `items.0.tag` → `items.0`).
136
+ // Top-level relations have no parent segment → null (root entry).
137
+ const lastDot = relationPath.lastIndexOf('.');
138
+ const ownerPath = lastDot === -1 ? null : relationPath.slice(0, lastDot);
139
+ if (ownerPath == null) {
140
+ return;
141
+ }
142
+ if (isOwnerExplicitlyRemoved(submitted, ownerPath)) {
143
+ return;
144
+ }
145
+ const submittedValue = getSubmittedValueAtPath(submitted, relationPath);
146
+ const relationIsUnchanged = isRelationUnchangedAtPath(submitted, relationPath, submittedValue);
147
+ if (relationIsUnchanged && isBidirectionalOneToOne(attribute) && usesJoinTable(attribute) && hasPopulatedRelation(value)) {
148
+ _.unset(adjustedData, relationPath);
149
+ deferredCopies.push({
150
+ schemaUid: schema.uid,
151
+ attributeName: key,
152
+ kind: 'joinTable',
153
+ ownerPath
154
+ });
155
+ return;
156
+ }
157
+ if (relationIsUnchanged && isBidirectionalOneToOne(attribute) && !usesJoinTable(attribute) && hasPopulatedRelation(value)) {
158
+ _.unset(adjustedData, relationPath);
159
+ deferredCopies.push({
160
+ schemaUid: schema.uid,
161
+ attributeName: key,
162
+ kind: 'fkColumn',
163
+ ownerPath
164
+ });
165
+ return;
166
+ }
167
+ // Always defer morphToOne: populate can be null even when morph columns are set.
168
+ if (relationIsUnchanged && isMorphToOneAttribute(attribute)) {
169
+ _.unset(adjustedData, relationPath);
170
+ deferredCopies.push({
171
+ schemaUid: schema.uid,
172
+ attributeName: key,
173
+ kind: 'morphToOne',
174
+ ownerPath
175
+ });
176
+ return;
177
+ }
178
+ if (relationIsUnchanged && isBidirectionalOneToManyMappedBy(attribute) && hasPopulatedRelationList(value)) {
179
+ _.unset(adjustedData, relationPath);
180
+ return;
181
+ }
182
+ if (!relationIsUnchanged && isRelationOperationPayload(submittedValue) && hasMeaningfulRelationOperations(submittedValue) && (!usesJoinTable(attribute) || isMorphToOneAttribute(attribute))) {
183
+ _.unset(adjustedData, relationPath);
184
+ postCloneUpdates.push({
185
+ dataPath: relationPath,
186
+ value: submittedValue,
187
+ schemaUid: schema.uid,
188
+ ownerPath
189
+ });
190
+ }
191
+ }, {
192
+ schema: contentType,
193
+ getModel
194
+ }, originalData);
195
+ return {
196
+ data: adjustedData,
197
+ deferredCopies,
198
+ postCloneUpdates
199
+ };
200
+ };
59
201
  const prepareCloneData = async (originalData, submittedData, contentType, getModel)=>{
60
202
  const submitted = submittedData ?? {};
61
203
  const relationOperationOverrides = await collectRelationOperationOverrides(submitted, contentType, getModel);
62
- const data = _.merge({}, originalData, submitted);
63
- const relationsToCopy = [];
204
+ let data = _.merge({}, originalData, submitted);
205
+ const deferredCopies = [];
206
+ const postCloneUpdates = [];
64
207
  for (const [attributeName, attribute] of Object.entries(contentType.attributes)){
65
- if (!isRelationAttribute(attribute) || !canTransformRelationOperationPayload(attribute)) {
208
+ if (!isRelationAttribute(attribute)) {
66
209
  continue;
67
210
  }
68
- const relationWasSubmitted = Object.prototype.hasOwnProperty.call(submitted, attributeName);
69
211
  const submittedValue = submitted[attributeName];
212
+ const relationWasSubmitted = Object.prototype.hasOwnProperty.call(submitted, attributeName);
70
213
  const isOperationPayload = isRelationOperationPayload(submittedValue);
71
214
  const hasMeaningfulOperations = isOperationPayload && hasMeaningfulRelationOperations(submittedValue);
72
215
  const relationIsUnchanged = !relationWasSubmitted || isOperationPayload && !hasMeaningfulOperations;
73
- if (relationIsUnchanged && isBidirectionalOneToOne(attribute) && hasPopulatedRelation(originalData[attributeName])) {
74
- // Normal relation attachment enforces oneToOne ownership by removing the source link.
75
- // Leave unchanged relations out of create and copy their join row after the clone exists.
216
+ if (relationIsUnchanged && isBidirectionalOneToOne(attribute) && usesJoinTable(attribute) && hasPopulatedRelation(originalData[attributeName])) {
217
+ delete data[attributeName];
218
+ deferredCopies.push({
219
+ schemaUid: contentType.uid,
220
+ attributeName,
221
+ kind: 'joinTable',
222
+ ownerPath: null
223
+ });
224
+ continue;
225
+ }
226
+ if (relationIsUnchanged && isBidirectionalOneToOne(attribute) && !usesJoinTable(attribute) && hasPopulatedRelation(originalData[attributeName])) {
227
+ delete data[attributeName];
228
+ deferredCopies.push({
229
+ schemaUid: contentType.uid,
230
+ attributeName,
231
+ kind: 'fkColumn',
232
+ ownerPath: null
233
+ });
234
+ continue;
235
+ }
236
+ // Always defer morphToOne: populate can be null even when morph columns are set.
237
+ if (relationIsUnchanged && isMorphToOneAttribute(attribute)) {
238
+ delete data[attributeName];
239
+ deferredCopies.push({
240
+ schemaUid: contentType.uid,
241
+ attributeName,
242
+ kind: 'morphToOne',
243
+ ownerPath: null
244
+ });
245
+ continue;
246
+ }
247
+ if (relationIsUnchanged && isBidirectionalOneToManyMappedBy(attribute) && hasPopulatedRelationList(originalData[attributeName])) {
248
+ delete data[attributeName];
249
+ continue;
250
+ }
251
+ if (!relationIsUnchanged && isRelationOperationPayload(submittedValue) && hasMeaningfulOperations && (!usesJoinTable(attribute) || isMorphToOneAttribute(attribute))) {
76
252
  delete data[attributeName];
77
- relationsToCopy.push(attributeName);
253
+ postCloneUpdates.push({
254
+ dataPath: attributeName,
255
+ value: submittedValue,
256
+ schemaUid: contentType.uid,
257
+ ownerPath: null
258
+ });
78
259
  }
79
260
  }
261
+ const nested = await collectNestedCloneRelationAdjustments(originalData, submitted, contentType, getModel, data);
262
+ data = nested.data;
263
+ deferredCopies.push(...nested.deferredCopies);
264
+ postCloneUpdates.push(...nested.postCloneUpdates);
80
265
  for (const [path, value] of relationOperationOverrides){
81
266
  _.set(data, path, value);
82
267
  }
83
268
  return {
84
269
  data,
85
- relationsToCopy
270
+ deferredCopies,
271
+ postCloneUpdates
86
272
  };
87
273
  };
88
- const copyCloneRelationRows = async (strapi, uid, sourceEntryId, targetEntryId, attributeNames)=>{
89
- if (attributeNames.length === 0) {
90
- return;
274
+ const resolveOwnerEntryId = (entryData, ownerPath, fallbackId)=>{
275
+ if (ownerPath == null) {
276
+ return fallbackId;
277
+ }
278
+ const owner = _.get(entryData, ownerPath);
279
+ const ownerId = owner?.id;
280
+ return typeof ownerId === 'number' ? ownerId : undefined;
281
+ };
282
+ const ownerComponentUid = (owner)=>{
283
+ if (!isRecord(owner) || typeof owner.__component !== 'string') {
284
+ return undefined;
285
+ }
286
+ return owner.__component;
287
+ };
288
+ /**
289
+ * A dynamic-zone block can be replaced by another component at the same index.
290
+ * Component ids are allocated per table, so a deferred write that still uses the
291
+ * source component UID and the new row id updates an unrelated row.
292
+ */ const ownerComponentWasReplaced = (originalData, clonedData, ownerPath)=>{
293
+ if (ownerPath == null) {
294
+ return false;
91
295
  }
296
+ const sourceComponent = ownerComponentUid(_.get(originalData, ownerPath));
297
+ const cloneComponent = ownerComponentUid(_.get(clonedData, ownerPath));
298
+ return sourceComponent != null && cloneComponent != null && sourceComponent !== cloneComponent;
299
+ };
300
+ const copyCloneRelationRows = async (strapi, uid, attributeName, sourceEntryId, targetEntryId)=>{
92
301
  const { attributes } = strapi.db.metadata.get(uid);
302
+ const attribute = attributes[attributeName];
303
+ if (attribute?.type !== 'relation' || !('joinTable' in attribute) || !attribute.joinTable) {
304
+ return;
305
+ }
93
306
  const idColumn = strapi.db.metadata.identifiers.ID_COLUMN;
94
307
  const batchSize = strapi.db.dialect.getBatchInsertSize();
308
+ const { joinTable } = attribute;
309
+ const { joinColumn } = joinTable;
95
310
  await strapi.db.transaction(async ({ trx })=>{
96
- for (const attributeName of attributeNames){
97
- const attribute = attributes[attributeName];
98
- if (attribute?.type !== 'relation' || !('joinTable' in attribute) || !attribute.joinTable) {
99
- continue;
311
+ const rows = await strapi.db.getConnection(joinTable.name).where({
312
+ [joinColumn.name]: sourceEntryId,
313
+ ...'on' in joinTable && joinTable.on
314
+ }).select('*').transacting(trx);
315
+ const rowsToCopy = rows.map((row)=>{
316
+ const copiedRow = {
317
+ ...row,
318
+ [joinColumn.name]: targetEntryId
319
+ };
320
+ delete copiedRow[idColumn];
321
+ return copiedRow;
322
+ });
323
+ for(let index = 0; index < rowsToCopy.length; index += batchSize){
324
+ await strapi.db.getConnection(joinTable.name).insert(rowsToCopy.slice(index, index + batchSize)).transacting(trx);
325
+ }
326
+ });
327
+ };
328
+ const copyFkColumnRelation = async (strapi, targetUid, attributeName, sourceEntryId, targetEntryId, sourceUid = targetUid)=>{
329
+ const meta = strapi.db.metadata.get(sourceUid);
330
+ const attribute = meta.attributes[attributeName];
331
+ if (attribute?.type !== 'relation' || !('joinColumn' in attribute) || !attribute.joinColumn) {
332
+ return;
333
+ }
334
+ // Inverse/non-owning joinColumn.name is this table's `id`, not an FK. Copying it
335
+ // attaches an unrelated row whose numeric id happens to match the source id.
336
+ if (!('owner' in attribute) || attribute.owner !== true) {
337
+ return;
338
+ }
339
+ const joinColumnName = attribute.joinColumn.name;
340
+ await strapi.db.transaction(async ({ trx })=>{
341
+ const sourceRow = await strapi.db.getConnection(meta.tableName).where({
342
+ id: sourceEntryId
343
+ }).select([
344
+ joinColumnName
345
+ ]).transacting(trx).first();
346
+ if (!sourceRow) {
347
+ return;
348
+ }
349
+ const targetAttribute = strapi.db.metadata.get(targetUid).attributes[attributeName];
350
+ if (targetAttribute?.type !== 'relation' || targetAttribute.relation !== 'oneToOne') {
351
+ return;
352
+ }
353
+ // processData only accepts attribute names, not raw join-column names
354
+ await strapi.db.query(targetUid).update({
355
+ where: {
356
+ id: targetEntryId
357
+ },
358
+ data: {
359
+ [attributeName]: sourceRow[joinColumnName] ?? null
100
360
  }
101
- const { joinTable } = attribute;
102
- const { joinColumn } = joinTable;
103
- const rows = await strapi.db.connection(joinTable.name).where({
104
- [joinColumn.name]: sourceEntryId,
105
- ...'on' in joinTable && joinTable.on || undefined
106
- }).select('*').transacting(trx);
107
- const rowsToCopy = rows.map((row)=>{
108
- const copiedRow = {
109
- ...row,
110
- [joinColumn.name]: targetEntryId
111
- };
112
- delete copiedRow[idColumn];
113
- return copiedRow;
114
- });
115
- for(let index = 0; index < rowsToCopy.length; index += batchSize){
116
- await strapi.db.connection(joinTable.name).insert(rowsToCopy.slice(index, index + batchSize)).transacting(trx);
361
+ });
362
+ });
363
+ };
364
+ const copyMorphToOneRelation = async (strapi, targetUid, attributeName, sourceEntryId, targetEntryId, sourceUid = targetUid)=>{
365
+ const sourceAttribute = strapi.db.metadata.get(sourceUid).attributes[attributeName];
366
+ const targetAttribute = strapi.db.metadata.get(targetUid).attributes[attributeName];
367
+ if (sourceAttribute?.type !== 'relation' || sourceAttribute.relation !== 'morphToOne' || !sourceAttribute.morphColumn || targetAttribute?.type !== 'relation' || targetAttribute.relation !== 'morphToOne') {
368
+ return;
369
+ }
370
+ const { idColumn, typeColumn } = sourceAttribute.morphColumn;
371
+ const typeField = targetAttribute.morphColumn?.typeField ?? '__type';
372
+ const sourceMeta = strapi.db.metadata.get(sourceUid);
373
+ await strapi.db.transaction(async ({ trx })=>{
374
+ const sourceRow = await strapi.db.getConnection(sourceMeta.tableName).where({
375
+ id: sourceEntryId
376
+ }).select([
377
+ idColumn.name,
378
+ typeColumn.name
379
+ ]).transacting(trx).first();
380
+ if (!sourceRow) {
381
+ return;
382
+ }
383
+ const morphId = sourceRow[idColumn.name];
384
+ const morphType = sourceRow[typeColumn.name];
385
+ // processData only accepts the morph attribute ({ id, __type }), not raw column names
386
+ await strapi.db.query(targetUid).update({
387
+ where: {
388
+ id: targetEntryId
389
+ },
390
+ data: {
391
+ [attributeName]: morphId != null && morphType != null ? {
392
+ id: morphId,
393
+ [typeField]: morphType
394
+ } : null
117
395
  }
396
+ });
397
+ });
398
+ };
399
+ const applyDeferredCloneRelationCopies = async (strapi, rootUid, sourceRootId, targetRootId, originalData, clonedData, deferredCopies)=>{
400
+ if (deferredCopies.length === 0) {
401
+ return;
402
+ }
403
+ for (const task of deferredCopies){
404
+ // Root owners (ownerPath === null) resolve via the fallback root IDs.
405
+ // Nested owners must resolve explicitly — falling back to the root ID would
406
+ // copy component-schema relation rows using an unrelated document ID.
407
+ const sourceEntryId = resolveOwnerEntryId(originalData, task.ownerPath, sourceRootId);
408
+ const targetEntryId = resolveOwnerEntryId(clonedData, task.ownerPath, targetRootId);
409
+ if (sourceEntryId == null || targetEntryId == null) {
410
+ throw new Error(`Unable to resolve clone relation owner for "${task.attributeName}" at path "${task.ownerPath ?? '<root>'}"`);
411
+ }
412
+ if (ownerComponentWasReplaced(originalData, clonedData, task.ownerPath)) {
413
+ continue;
414
+ }
415
+ if (sourceEntryId === targetEntryId) {
416
+ continue;
417
+ }
418
+ if (task.kind === 'joinTable') {
419
+ await copyCloneRelationRows(strapi, task.schemaUid, task.attributeName, sourceEntryId, targetEntryId);
420
+ } else if (task.kind === 'fkColumn') {
421
+ await copyFkColumnRelation(strapi, task.schemaUid, task.attributeName, sourceEntryId, targetEntryId);
422
+ } else if (task.kind === 'morphToOne') {
423
+ await copyMorphToOneRelation(strapi, task.schemaUid, task.attributeName, sourceEntryId, targetEntryId);
424
+ }
425
+ }
426
+ };
427
+ const toRelationRef = (value)=>{
428
+ if (isRecord(value)) {
429
+ return value;
430
+ }
431
+ if (typeof value === 'number') {
432
+ return {
433
+ id: value
434
+ };
435
+ }
436
+ // Document Service shorthand: a string is a documentId, matching mapRelation.
437
+ if (typeof value === 'string') {
438
+ return {
439
+ documentId: value
440
+ };
441
+ }
442
+ return undefined;
443
+ };
444
+ const relationOperationList = (value)=>{
445
+ if (Array.isArray(value)) {
446
+ return value;
447
+ }
448
+ if (value == null) {
449
+ return [];
450
+ }
451
+ return [
452
+ value
453
+ ];
454
+ };
455
+ const pickInlineRelationTargetRef = (value)=>{
456
+ if (Object.prototype.hasOwnProperty.call(value, 'set')) {
457
+ const setValue = value.set;
458
+ if (setValue == null) {
459
+ return null;
118
460
  }
461
+ const list = relationOperationList(setValue);
462
+ if (list.length === 0) {
463
+ return null;
464
+ }
465
+ return toRelationRef(list[list.length - 1]);
466
+ }
467
+ const connect = value.connect;
468
+ if (connect == null) {
469
+ return undefined;
470
+ }
471
+ const list = relationOperationList(connect);
472
+ if (list.length === 0) {
473
+ return undefined;
474
+ }
475
+ return toRelationRef(list[list.length - 1]);
476
+ };
477
+ const toRelationStatus = (value)=>{
478
+ return value === 'draft' || value === 'published' ? value : undefined;
479
+ };
480
+ const resolveEntryIdsByRef = async (strapi, sourceUid, targetUid, ref, opts)=>{
481
+ if (typeof ref.id === 'number') {
482
+ return [
483
+ ref.id
484
+ ];
485
+ }
486
+ if (typeof ref.documentId !== 'string') {
487
+ return [];
488
+ }
489
+ let relation = {
490
+ documentId: ref.documentId,
491
+ locale: typeof ref.locale === 'string' ? ref.locale : undefined,
492
+ status: toRelationStatus(ref.status)
493
+ };
494
+ if (relation.locale == null && !i18n.isLocalizedContentType(sourceUid) && i18n.isLocalizedContentType(targetUid)) {
495
+ relation = defaultLocale.setDefaultLocaleToRelation(relation, await i18n.getDefaultLocale());
496
+ }
497
+ const targetLocale = i18n.getRelationTargetLocale(relation, {
498
+ targetUid,
499
+ sourceUid,
500
+ sourceLocale: opts.locale
119
501
  });
502
+ const sourceHasDP = strapiUtils.contentTypes.hasDraftAndPublish(strapi.getModel(sourceUid));
503
+ const statuses = dp.getRelationTargetStatus(relation, {
504
+ targetUid,
505
+ sourceUid,
506
+ sourceStatus: sourceHasDP ? 'draft' : undefined
507
+ });
508
+ const model = strapi.getModel(targetUid);
509
+ const targetHasDP = strapiUtils.contentTypes.hasDraftAndPublish(model);
510
+ const resolvedIds = [];
511
+ for (const status of statuses){
512
+ const where = {
513
+ documentId: ref.documentId
514
+ };
515
+ if (targetHasDP) {
516
+ where.publishedAt = status === 'draft' ? null : {
517
+ $ne: null
518
+ };
519
+ }
520
+ if (targetLocale) {
521
+ where.locale = targetLocale;
522
+ }
523
+ const row = await strapi.db.query(targetUid).findOne({
524
+ where,
525
+ select: [
526
+ 'id'
527
+ ]
528
+ });
529
+ if (typeof row?.id === 'number' && !resolvedIds.includes(row.id)) {
530
+ resolvedIds.push(row.id);
531
+ }
532
+ }
533
+ return resolvedIds;
534
+ };
535
+ const resolveEntryIdByRef = async (strapi, sourceUid, targetUid, ref, opts)=>{
536
+ const resolvedIds = await resolveEntryIdsByRef(strapi, sourceUid, targetUid, ref, opts);
537
+ // Single-column FK/morph storage can keep only one id; match entity-manager and
538
+ // keep the last resolved status when the standard resolver returns both.
539
+ return resolvedIds.at(-1);
540
+ };
541
+ const getMorphTypeField = (attribute)=>{
542
+ const morphColumn = 'morphColumn' in attribute ? attribute.morphColumn : undefined;
543
+ return morphColumn?.typeField ?? '__type';
544
+ };
545
+ const hasRelationTarget = (attribute)=>{
546
+ return 'target' in attribute && typeof attribute.target === 'string';
547
+ };
548
+ const currentAssociationId = (current)=>{
549
+ if (typeof current === 'number') {
550
+ return current;
551
+ }
552
+ if (isRecord(current) && typeof current.id === 'number') {
553
+ return current.id;
554
+ }
555
+ return undefined;
556
+ };
557
+ const disconnectMatchesCurrent = async (strapi, sourceUid, attribute, disconnect, current, opts)=>{
558
+ const currentId = currentAssociationId(current);
559
+ if (currentId == null) {
560
+ return false;
561
+ }
562
+ const typeField = getMorphTypeField(attribute);
563
+ const currentType = isMorphToOneAttribute(attribute) && isRecord(current) && typeof current[typeField] === 'string' ? current[typeField] : undefined;
564
+ for (const item of disconnect){
565
+ let ref = null;
566
+ if (isRecord(item)) {
567
+ ref = item;
568
+ } else if (typeof item === 'number' || typeof item === 'string') {
569
+ ref = {
570
+ id: item
571
+ };
572
+ }
573
+ if (!ref) {
574
+ continue;
575
+ }
576
+ if (isMorphToOneAttribute(attribute)) {
577
+ const targetUid = ref[typeField];
578
+ if (typeof targetUid !== 'string') {
579
+ continue;
580
+ }
581
+ if (currentType != null && currentType !== targetUid) {
582
+ continue;
583
+ }
584
+ const ids = await resolveEntryIdsByRef(strapi, sourceUid, targetUid, ref, opts);
585
+ if (ids.includes(currentId)) {
586
+ return true;
587
+ }
588
+ continue;
589
+ }
590
+ if (!hasRelationTarget(attribute)) {
591
+ continue;
592
+ }
593
+ const ids = await resolveEntryIdsByRef(strapi, sourceUid, attribute.target, ref, opts);
594
+ if (ids.includes(currentId)) {
595
+ return true;
596
+ }
597
+ }
598
+ return false;
599
+ };
600
+ const resolveInlineRelationAssignment = async (strapi, attribute, value, opts)=>{
601
+ const targetRef = pickInlineRelationTargetRef(value);
602
+ if (targetRef === undefined) {
603
+ const disconnect = relationOperationList(value.disconnect);
604
+ if (disconnect.length === 0) {
605
+ return undefined;
606
+ }
607
+ const matches = await disconnectMatchesCurrent(strapi, opts.sourceUid, attribute, disconnect, opts.originalValue, {
608
+ locale: opts.locale
609
+ });
610
+ if (matches) {
611
+ return null;
612
+ }
613
+ // Unmatched disconnect must keep the original association. The clone was
614
+ // created without the relation (stripped from create data), so copy it now.
615
+ if (opts.sourceOwnerId == null) {
616
+ throw new Error(`Unable to restore clone relation "${opts.attributeName}" after unmatched disconnect`);
617
+ }
618
+ const readUid = opts.sourceSchemaUid ?? opts.ownerUid;
619
+ if (isMorphToOneAttribute(attribute)) {
620
+ await copyMorphToOneRelation(strapi, opts.ownerUid, opts.attributeName, opts.sourceOwnerId, opts.targetOwnerId, readUid);
621
+ } else {
622
+ await copyFkColumnRelation(strapi, opts.ownerUid, opts.attributeName, opts.sourceOwnerId, opts.targetOwnerId, readUid);
623
+ }
624
+ return undefined;
625
+ }
626
+ if (targetRef === null) {
627
+ return null;
628
+ }
629
+ if (isMorphToOneAttribute(attribute)) {
630
+ const typeField = getMorphTypeField(attribute);
631
+ const targetUid = targetRef[typeField];
632
+ if (typeof targetUid !== 'string') {
633
+ throw new Error(`Inline morphToOne clone update requires ${typeField}`);
634
+ }
635
+ const id = await resolveEntryIdByRef(strapi, opts.sourceUid, targetUid, targetRef, opts);
636
+ if (id == null) {
637
+ throw new Error(`Unable to resolve morphToOne target for clone relation update`);
638
+ }
639
+ return {
640
+ id,
641
+ [typeField]: targetUid
642
+ };
643
+ }
644
+ if (!hasRelationTarget(attribute)) {
645
+ throw new Error('Inline FK clone update requires a relation target');
646
+ }
647
+ const id = await resolveEntryIdByRef(strapi, opts.sourceUid, attribute.target, targetRef, opts);
648
+ if (id == null) {
649
+ throw new Error(`Unable to resolve relation target for clone relation update`);
650
+ }
651
+ return id;
652
+ };
653
+ const applyPostCloneRelationUpdates = async (strapi, rootUid, sourceRootId, clonedEntryId, clonedData, originalData, postCloneUpdates)=>{
654
+ if (postCloneUpdates.length === 0) {
655
+ return;
656
+ }
657
+ const locale = clonedData.locale;
658
+ for (const update of postCloneUpdates){
659
+ const attributeName = update.dataPath.split('.').pop();
660
+ const ownerEntryId = resolveOwnerEntryId(clonedData, update.ownerPath, clonedEntryId);
661
+ const sourceOwnerId = resolveOwnerEntryId(originalData, update.ownerPath, sourceRootId);
662
+ if (ownerEntryId == null) {
663
+ throw new Error(`Unable to resolve clone relation owner for "${attributeName}" at path "${update.ownerPath ?? '<root>'}"`);
664
+ }
665
+ let schemaUid = update.schemaUid;
666
+ if (ownerComponentWasReplaced(originalData, clonedData, update.ownerPath)) {
667
+ const cloneComponent = ownerComponentUid(_.get(clonedData, update.ownerPath));
668
+ const cloneAttribute = cloneComponent ? strapi.db.metadata.get(cloneComponent).attributes[attributeName] : undefined;
669
+ // The submitted operation belongs to the new block. Do not apply it with the
670
+ // source component UID, or the new row id will match an unrelated row.
671
+ if (!cloneComponent || cloneAttribute?.type !== 'relation') {
672
+ continue;
673
+ }
674
+ schemaUid = cloneComponent;
675
+ }
676
+ const attribute = strapi.db.metadata.get(schemaUid).attributes[attributeName];
677
+ if (!attribute || attribute.type !== 'relation') {
678
+ throw new Error(`Unable to resolve clone relation attribute "${attributeName}" on "${schemaUid}"`);
679
+ }
680
+ const assignment = await resolveInlineRelationAssignment(strapi, attribute, update.value, {
681
+ locale,
682
+ ownerUid: schemaUid,
683
+ sourceSchemaUid: update.schemaUid,
684
+ sourceUid: rootUid,
685
+ originalValue: _.get(originalData, update.dataPath),
686
+ sourceOwnerId,
687
+ targetOwnerId: ownerEntryId,
688
+ attributeName
689
+ });
690
+ if (assignment === undefined) {
691
+ continue;
692
+ }
693
+ // processData accepts attribute names (FK id / morph { id, __type }), not operation payloads.
694
+ await strapi.db.query(schemaUid).update({
695
+ where: {
696
+ id: ownerEntryId
697
+ },
698
+ data: {
699
+ [attributeName]: assignment
700
+ }
701
+ });
702
+ }
120
703
  };
121
704
 
705
+ exports.applyDeferredCloneRelationCopies = applyDeferredCloneRelationCopies;
706
+ exports.applyPostCloneRelationUpdates = applyPostCloneRelationUpdates;
122
707
  exports.copyCloneRelationRows = copyCloneRelationRows;
123
708
  exports.prepareCloneData = prepareCloneData;
124
709
  //# sourceMappingURL=clone-relations.js.map