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