@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.
- package/dist/Strapi.d.ts.map +1 -1
- package/dist/Strapi.js +1 -0
- package/dist/Strapi.js.map +1 -1
- package/dist/Strapi.mjs +1 -0
- package/dist/Strapi.mjs.map +1 -1
- package/dist/ee/entitlements.d.ts.map +1 -1
- package/dist/ee/entitlements.js.map +1 -1
- package/dist/ee/entitlements.mjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/loaders/apis.d.ts.map +1 -1
- package/dist/loaders/apis.js.map +1 -1
- package/dist/loaders/apis.mjs.map +1 -1
- package/dist/loaders/plugins/get-enabled-plugins.js +2 -2
- package/dist/loaders/plugins/get-enabled-plugins.js.map +1 -1
- package/dist/loaders/plugins/get-enabled-plugins.mjs +2 -2
- package/dist/loaders/plugins/get-enabled-plugins.mjs.map +1 -1
- package/dist/middlewares/cors.d.ts.map +1 -1
- package/dist/middlewares/cors.js.map +1 -1
- package/dist/middlewares/cors.mjs.map +1 -1
- package/dist/migrations/database/5.0.0-discard-drafts.js +1 -1
- package/dist/migrations/database/5.0.0-discard-drafts.js.map +1 -1
- package/dist/migrations/database/5.0.0-discard-drafts.mjs +1 -1
- package/dist/migrations/database/5.0.0-discard-drafts.mjs.map +1 -1
- package/dist/migrations/first-published-at.js +1 -1
- package/dist/migrations/first-published-at.js.map +1 -1
- package/dist/migrations/first-published-at.mjs +1 -1
- package/dist/migrations/first-published-at.mjs.map +1 -1
- package/dist/package.json.js +15 -14
- package/dist/package.json.js.map +1 -1
- package/dist/package.json.mjs +15 -14
- package/dist/package.json.mjs.map +1 -1
- package/dist/registries/controllers.d.ts.map +1 -1
- package/dist/registries/controllers.js.map +1 -1
- package/dist/registries/controllers.mjs.map +1 -1
- package/dist/services/content-source-maps.js +6 -6
- package/dist/services/content-source-maps.js.map +1 -1
- package/dist/services/content-source-maps.mjs +6 -6
- package/dist/services/content-source-maps.mjs.map +1 -1
- package/dist/services/document-service/components.d.ts +13 -11
- package/dist/services/document-service/components.d.ts.map +1 -1
- package/dist/services/document-service/components.js +115 -66
- package/dist/services/document-service/components.js.map +1 -1
- package/dist/services/document-service/components.mjs +116 -67
- package/dist/services/document-service/components.mjs.map +1 -1
- package/dist/services/document-service/events.d.ts.map +1 -1
- package/dist/services/document-service/events.js +5 -1
- package/dist/services/document-service/events.js.map +1 -1
- package/dist/services/document-service/events.mjs +5 -1
- package/dist/services/document-service/events.mjs.map +1 -1
- package/dist/services/document-service/repository.d.ts.map +1 -1
- package/dist/services/document-service/repository.js +26 -7
- package/dist/services/document-service/repository.js.map +1 -1
- package/dist/services/document-service/repository.mjs +27 -8
- package/dist/services/document-service/repository.mjs.map +1 -1
- package/dist/services/document-service/transform/data.d.ts.map +1 -1
- package/dist/services/document-service/transform/data.js.map +1 -1
- package/dist/services/document-service/transform/data.mjs.map +1 -1
- package/dist/services/document-service/transform/relations/transform/default-locale.d.ts +4 -1
- package/dist/services/document-service/transform/relations/transform/default-locale.d.ts.map +1 -1
- package/dist/services/document-service/transform/relations/transform/default-locale.js +12 -9
- package/dist/services/document-service/transform/relations/transform/default-locale.js.map +1 -1
- package/dist/services/document-service/transform/relations/transform/default-locale.mjs +12 -10
- package/dist/services/document-service/transform/relations/transform/default-locale.mjs.map +1 -1
- package/dist/services/document-service/utils/clone-relations.d.ts +21 -4
- package/dist/services/document-service/utils/clone-relations.d.ts.map +1 -1
- package/dist/services/document-service/utils/clone-relations.js +619 -34
- package/dist/services/document-service/utils/clone-relations.js.map +1 -1
- package/dist/services/document-service/utils/clone-relations.mjs +619 -36
- package/dist/services/document-service/utils/clone-relations.mjs.map +1 -1
- package/dist/services/document-service/utils/unidirectional-relations.d.ts +4 -2
- package/dist/services/document-service/utils/unidirectional-relations.d.ts.map +1 -1
- package/dist/services/document-service/utils/unidirectional-relations.js +1 -10
- package/dist/services/document-service/utils/unidirectional-relations.js.map +1 -1
- package/dist/services/document-service/utils/unidirectional-relations.mjs +1 -10
- package/dist/services/document-service/utils/unidirectional-relations.mjs.map +1 -1
- package/dist/services/event-hub.d.ts.map +1 -1
- package/dist/services/event-hub.js +23 -5
- package/dist/services/event-hub.js.map +1 -1
- package/dist/services/event-hub.mjs +23 -5
- package/dist/services/event-hub.mjs.map +1 -1
- package/dist/services/webhook-store.js +2 -2
- package/dist/services/webhook-store.js.map +1 -1
- package/dist/services/webhook-store.mjs +2 -2
- package/dist/services/webhook-store.mjs.map +1 -1
- package/dist/utils/fetch.d.ts.map +1 -1
- package/dist/utils/fetch.js +44 -8
- package/dist/utils/fetch.js.map +1 -1
- package/dist/utils/fetch.mjs +37 -8
- package/dist/utils/fetch.mjs.map +1 -1
- package/dist/utils/transform-content-types-to-models.js +5 -5
- package/dist/utils/transform-content-types-to-models.js.map +1 -1
- package/dist/utils/transform-content-types-to-models.mjs +5 -5
- package/dist/utils/transform-content-types-to-models.mjs.map +1 -1
- 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
|
|
33
|
-
return attribute.useJoinTable !== false
|
|
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
|
-
|
|
63
|
-
const
|
|
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)
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
+
deferredCopies,
|
|
271
|
+
postCloneUpdates
|
|
86
272
|
};
|
|
87
273
|
};
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|