@zachariaz/strapi-plugin-content-variants 0.1.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/README.md +600 -0
- package/dist/_chunks/Segments-BREqC60L.js +330 -0
- package/dist/_chunks/Segments-BgxnvvtR.mjs +330 -0
- package/dist/_chunks/en-Bnfrhhim.js +62 -0
- package/dist/_chunks/en-e_966kWj.mjs +62 -0
- package/dist/_chunks/index-DVoZM8JU.js +1036 -0
- package/dist/_chunks/index-Dj2sexmk.mjs +1020 -0
- package/dist/admin/index.js +3 -0
- package/dist/admin/index.mjs +4 -0
- package/dist/admin/src/components/Initializer.d.ts +5 -0
- package/dist/admin/src/components/SegmentPickerAction.d.ts +7 -0
- package/dist/admin/src/components/VariantInfoAction.d.ts +8 -0
- package/dist/admin/src/components/VariantPanel.d.ts +13 -0
- package/dist/admin/src/components/VariantPickerAction.d.ts +20 -0
- package/dist/admin/src/contentManagerHooks/editView.d.ts +6 -0
- package/dist/admin/src/contentManagerHooks/listView.d.ts +22 -0
- package/dist/admin/src/hooks/useSegments.d.ts +17 -0
- package/dist/admin/src/hooks/useVariantFamily.d.ts +19 -0
- package/dist/admin/src/hooks/useVariantLinks.d.ts +44 -0
- package/dist/admin/src/index.d.ts +11 -0
- package/dist/admin/src/pages/Settings/Segments.d.ts +2 -0
- package/dist/admin/src/pluginId.d.ts +2 -0
- package/dist/admin/src/utils/batchLinkFetcher.d.ts +11 -0
- package/dist/admin/src/utils/variants.d.ts +13 -0
- package/dist/server/index.js +895 -0
- package/dist/server/index.mjs +896 -0
- package/dist/server/src/bootstrap.d.ts +17 -0
- package/dist/server/src/config/index.d.ts +5 -0
- package/dist/server/src/content-types/index.d.ts +121 -0
- package/dist/server/src/controllers/index.d.ts +24 -0
- package/dist/server/src/controllers/segment.d.ts +11 -0
- package/dist/server/src/controllers/variant-link.d.ts +18 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/index.d.ts +244 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/admin.d.ts +12 -0
- package/dist/server/src/routes/content-api.d.ts +19 -0
- package/dist/server/src/routes/index.d.ts +25 -0
- package/dist/server/src/services/index.d.ts +62 -0
- package/dist/server/src/services/segment.d.ts +14 -0
- package/dist/server/src/services/variant-link.d.ts +60 -0
- package/dist/server/src/services/variant-resolver.d.ts +23 -0
- package/package.json +104 -0
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const register = ({ strapi }) => {
|
|
3
|
+
};
|
|
4
|
+
const PLUGIN_ID$1 = "content-variants";
|
|
5
|
+
const VARIANT_LINK_UID$1 = "plugin::content-variants.variant-link";
|
|
6
|
+
const bootstrap = async ({ strapi }) => {
|
|
7
|
+
strapi.server.use(async (ctx, next) => {
|
|
8
|
+
const segment = ctx.query?.segment;
|
|
9
|
+
const includeVariants = ctx.query?.includeVariants;
|
|
10
|
+
if (segment) ctx.state.segment = segment;
|
|
11
|
+
if (includeVariants) ctx.state.includeVariants = includeVariants;
|
|
12
|
+
await next();
|
|
13
|
+
if (segment) {
|
|
14
|
+
ctx.set("Vary", "segment");
|
|
15
|
+
ctx.set("Cache-Control", "public, max-age=60, s-maxage=300");
|
|
16
|
+
} else if (includeVariants === "true") {
|
|
17
|
+
ctx.set("Vary", "includeVariants");
|
|
18
|
+
ctx.set("Cache-Control", "public, max-age=300, s-maxage=600");
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
strapi.documents.use(async (ctx, next) => {
|
|
22
|
+
const result = await next();
|
|
23
|
+
const action = ctx.action;
|
|
24
|
+
if (action !== "findMany" && action !== "findOne") {
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
const contentTypeUid = ctx.uid;
|
|
28
|
+
if (!contentTypeUid) return result;
|
|
29
|
+
if (typeof contentTypeUid === "string" && contentTypeUid.startsWith("plugin::content-variants.")) {
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
const params = ctx.params || {};
|
|
33
|
+
const segment = params.segment || params.filters?.segment || ctx.state?.segment;
|
|
34
|
+
const includeVariants = params.includeVariants || ctx.state?.includeVariants;
|
|
35
|
+
const isSegmentMode = typeof segment === "string" && segment.length > 0;
|
|
36
|
+
const isEnrichedMode = includeVariants === "true" && !isSegmentMode;
|
|
37
|
+
if (!isSegmentMode && !isEnrichedMode) {
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
const resolver = strapi.plugin(PLUGIN_ID$1).service("variant-resolver");
|
|
41
|
+
const variantLinkService2 = strapi.plugin(PLUGIN_ID$1).service("variant-link");
|
|
42
|
+
const isVariantEnabledUid = (uid) => {
|
|
43
|
+
try {
|
|
44
|
+
const ct = strapi.contentType(uid);
|
|
45
|
+
return !!ct?.pluginOptions?.[PLUGIN_ID$1]?.enabled;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const topLevelVariantEnabled = isVariantEnabledUid(contentTypeUid);
|
|
51
|
+
const getVariantIds = async () => {
|
|
52
|
+
return variantLinkService2.findVariantDocumentIds(contentTypeUid);
|
|
53
|
+
};
|
|
54
|
+
const resolveDoc = async (base, targetUid) => {
|
|
55
|
+
if (!base || !base.documentId) return base;
|
|
56
|
+
const links = await strapi.documents(VARIANT_LINK_UID$1).findMany({
|
|
57
|
+
filters: {
|
|
58
|
+
$and: [
|
|
59
|
+
{ baseContentType: { $eq: targetUid } },
|
|
60
|
+
{ baseDocumentId: { $eq: base.documentId } }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
populate: ["assignments.segment"]
|
|
64
|
+
});
|
|
65
|
+
if (!Array.isArray(links) || links.length === 0) {
|
|
66
|
+
return base;
|
|
67
|
+
}
|
|
68
|
+
let bestLink = null;
|
|
69
|
+
let bestPriority = Infinity;
|
|
70
|
+
for (const link of links) {
|
|
71
|
+
for (const assignment of link.assignments || []) {
|
|
72
|
+
if (assignment.segment?.slug === segment) {
|
|
73
|
+
const p = assignment.priority ?? 0;
|
|
74
|
+
if (p < bestPriority) {
|
|
75
|
+
bestPriority = p;
|
|
76
|
+
bestLink = link;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!bestLink || !bestLink.variantDocumentId) {
|
|
82
|
+
return base;
|
|
83
|
+
}
|
|
84
|
+
const variant = await strapi.documents(targetUid).findOne({
|
|
85
|
+
documentId: bestLink.variantDocumentId,
|
|
86
|
+
...params.locale ? { locale: params.locale } : {},
|
|
87
|
+
...params.status ? { status: params.status } : {}
|
|
88
|
+
});
|
|
89
|
+
if (!variant) {
|
|
90
|
+
return base;
|
|
91
|
+
}
|
|
92
|
+
return resolver.resolveDocument(base, variant, {
|
|
93
|
+
segmentSlug: segment,
|
|
94
|
+
contentTypeUid: targetUid
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
const enrichDoc = async (base, targetUid) => {
|
|
98
|
+
if (!base || !base.documentId) return base;
|
|
99
|
+
const links = await strapi.documents(VARIANT_LINK_UID$1).findMany({
|
|
100
|
+
filters: {
|
|
101
|
+
$and: [
|
|
102
|
+
{ baseContentType: { $eq: targetUid } },
|
|
103
|
+
{ baseDocumentId: { $eq: base.documentId } }
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
populate: ["assignments.segment"]
|
|
107
|
+
});
|
|
108
|
+
if (!Array.isArray(links) || links.length === 0) {
|
|
109
|
+
return { ...base, _variants: [] };
|
|
110
|
+
}
|
|
111
|
+
const variants = [];
|
|
112
|
+
for (const link of links) {
|
|
113
|
+
const variantDoc = await strapi.documents(targetUid).findOne({
|
|
114
|
+
documentId: link.variantDocumentId,
|
|
115
|
+
...params.locale ? { locale: params.locale } : {},
|
|
116
|
+
...params.status ? { status: params.status } : {}
|
|
117
|
+
});
|
|
118
|
+
if (!variantDoc) continue;
|
|
119
|
+
const fields = resolver.extractVariantFields(variantDoc, targetUid);
|
|
120
|
+
const segments = (link.assignments || []).filter((a) => a.segment).map((a) => ({
|
|
121
|
+
name: a.segment.name,
|
|
122
|
+
slug: a.segment.slug
|
|
123
|
+
}));
|
|
124
|
+
variants.push({
|
|
125
|
+
documentId: link.variantDocumentId,
|
|
126
|
+
segments,
|
|
127
|
+
fields
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return { ...base, _variants: variants };
|
|
131
|
+
};
|
|
132
|
+
const resolvePopulated = async (doc, ownerUid) => {
|
|
133
|
+
if (!doc || typeof doc !== "object") return;
|
|
134
|
+
let contentType;
|
|
135
|
+
try {
|
|
136
|
+
contentType = strapi.contentType(ownerUid);
|
|
137
|
+
} catch {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const attributes2 = contentType?.attributes || {};
|
|
141
|
+
for (const [fieldName, attr] of Object.entries(attributes2)) {
|
|
142
|
+
const attribute = attr;
|
|
143
|
+
if (attribute.type === "relation" && attribute.target) {
|
|
144
|
+
const populated = doc[fieldName];
|
|
145
|
+
if (!populated || typeof populated !== "object") continue;
|
|
146
|
+
const targetUid = attribute.target;
|
|
147
|
+
if (targetUid.startsWith("plugin::content-variants.")) continue;
|
|
148
|
+
const targetVariantEnabled = isVariantEnabledUid(targetUid);
|
|
149
|
+
const isArray = Array.isArray(populated);
|
|
150
|
+
const items = isArray ? populated : [populated];
|
|
151
|
+
const processed = [];
|
|
152
|
+
for (const item of items) {
|
|
153
|
+
let resolved = item;
|
|
154
|
+
if (targetVariantEnabled && item?.documentId) {
|
|
155
|
+
if (isSegmentMode) {
|
|
156
|
+
resolved = await resolveDoc(item, targetUid);
|
|
157
|
+
} else if (isEnrichedMode) {
|
|
158
|
+
resolved = await enrichDoc(item, targetUid);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await resolvePopulated(resolved, targetUid);
|
|
162
|
+
processed.push(resolved);
|
|
163
|
+
}
|
|
164
|
+
doc[fieldName] = isArray ? processed : processed[0];
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (attribute.type === "component" && attribute.component) {
|
|
168
|
+
const populated = doc[fieldName];
|
|
169
|
+
if (!populated) continue;
|
|
170
|
+
const componentUid = attribute.component;
|
|
171
|
+
if (attribute.repeatable && Array.isArray(populated)) {
|
|
172
|
+
for (const item of populated) {
|
|
173
|
+
await resolvePopulated(item, componentUid);
|
|
174
|
+
}
|
|
175
|
+
} else if (typeof populated === "object") {
|
|
176
|
+
await resolvePopulated(populated, componentUid);
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (attribute.type === "dynamiczone") {
|
|
181
|
+
const populated = doc[fieldName];
|
|
182
|
+
if (!Array.isArray(populated)) continue;
|
|
183
|
+
for (const item of populated) {
|
|
184
|
+
if (item?.__component) {
|
|
185
|
+
await resolvePopulated(item, item.__component);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const filterVariants = async (docs) => {
|
|
192
|
+
const variantIds = await getVariantIds();
|
|
193
|
+
if (variantIds.size === 0) return docs;
|
|
194
|
+
return docs.filter((doc) => !variantIds.has(doc.documentId));
|
|
195
|
+
};
|
|
196
|
+
const processDocArray = async (docs) => {
|
|
197
|
+
const filtered = topLevelVariantEnabled ? await filterVariants(docs) : docs;
|
|
198
|
+
const processed = [];
|
|
199
|
+
for (const item of filtered) {
|
|
200
|
+
let doc = item;
|
|
201
|
+
if (topLevelVariantEnabled) {
|
|
202
|
+
if (isSegmentMode) {
|
|
203
|
+
doc = await resolveDoc(item, contentTypeUid);
|
|
204
|
+
} else if (isEnrichedMode) {
|
|
205
|
+
doc = await enrichDoc(item, contentTypeUid);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
await resolvePopulated(doc, contentTypeUid);
|
|
209
|
+
processed.push(doc);
|
|
210
|
+
}
|
|
211
|
+
return processed;
|
|
212
|
+
};
|
|
213
|
+
if (action === "findOne" && result && typeof result === "object" && !Array.isArray(result)) {
|
|
214
|
+
let doc = result;
|
|
215
|
+
if (topLevelVariantEnabled) {
|
|
216
|
+
if (isSegmentMode) {
|
|
217
|
+
doc = await resolveDoc(result, contentTypeUid);
|
|
218
|
+
} else if (isEnrichedMode) {
|
|
219
|
+
doc = await enrichDoc(result, contentTypeUid);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
await resolvePopulated(doc, contentTypeUid);
|
|
223
|
+
return doc;
|
|
224
|
+
}
|
|
225
|
+
if (action === "findMany") {
|
|
226
|
+
if (Array.isArray(result)) {
|
|
227
|
+
return processDocArray(result);
|
|
228
|
+
}
|
|
229
|
+
const resultObj = result;
|
|
230
|
+
if (resultObj?.results && Array.isArray(resultObj.results)) {
|
|
231
|
+
return {
|
|
232
|
+
...resultObj,
|
|
233
|
+
results: await processDocArray(resultObj.results)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
const destroy = ({ strapi }) => {
|
|
241
|
+
};
|
|
242
|
+
const config = {
|
|
243
|
+
default: {},
|
|
244
|
+
validator(config2) {
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const kind$2 = "collectionType";
|
|
248
|
+
const collectionName$2 = "segments";
|
|
249
|
+
const info$2 = {
|
|
250
|
+
singularName: "segment",
|
|
251
|
+
pluralName: "segments",
|
|
252
|
+
displayName: "Segment"
|
|
253
|
+
};
|
|
254
|
+
const options$2 = {
|
|
255
|
+
draftAndPublish: false
|
|
256
|
+
};
|
|
257
|
+
const pluginOptions$2 = {};
|
|
258
|
+
const attributes$2 = {
|
|
259
|
+
name: {
|
|
260
|
+
type: "string",
|
|
261
|
+
required: true,
|
|
262
|
+
unique: true
|
|
263
|
+
},
|
|
264
|
+
slug: {
|
|
265
|
+
type: "string",
|
|
266
|
+
required: true,
|
|
267
|
+
unique: true
|
|
268
|
+
},
|
|
269
|
+
description: {
|
|
270
|
+
type: "text"
|
|
271
|
+
},
|
|
272
|
+
externalId: {
|
|
273
|
+
type: "string"
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
const segmentSchema = {
|
|
277
|
+
kind: kind$2,
|
|
278
|
+
collectionName: collectionName$2,
|
|
279
|
+
info: info$2,
|
|
280
|
+
options: options$2,
|
|
281
|
+
pluginOptions: pluginOptions$2,
|
|
282
|
+
attributes: attributes$2
|
|
283
|
+
};
|
|
284
|
+
const kind$1 = "collectionType";
|
|
285
|
+
const collectionName$1 = "content_variant_links";
|
|
286
|
+
const info$1 = {
|
|
287
|
+
singularName: "variant-link",
|
|
288
|
+
pluralName: "variant-links",
|
|
289
|
+
displayName: "Variant Link"
|
|
290
|
+
};
|
|
291
|
+
const options$1 = {
|
|
292
|
+
draftAndPublish: false
|
|
293
|
+
};
|
|
294
|
+
const pluginOptions$1 = {
|
|
295
|
+
"content-manager": {
|
|
296
|
+
visible: false
|
|
297
|
+
},
|
|
298
|
+
"content-type-builder": {
|
|
299
|
+
visible: false
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
const attributes$1 = {
|
|
303
|
+
baseContentType: {
|
|
304
|
+
type: "string",
|
|
305
|
+
required: true
|
|
306
|
+
},
|
|
307
|
+
baseDocumentId: {
|
|
308
|
+
type: "string",
|
|
309
|
+
required: true
|
|
310
|
+
},
|
|
311
|
+
variantDocumentId: {
|
|
312
|
+
type: "string",
|
|
313
|
+
required: true
|
|
314
|
+
},
|
|
315
|
+
label: {
|
|
316
|
+
type: "string"
|
|
317
|
+
},
|
|
318
|
+
assignments: {
|
|
319
|
+
type: "relation",
|
|
320
|
+
relation: "oneToMany",
|
|
321
|
+
target: "plugin::content-variants.variant-assignment",
|
|
322
|
+
mappedBy: "variantLink"
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const variantLinkSchema = {
|
|
326
|
+
kind: kind$1,
|
|
327
|
+
collectionName: collectionName$1,
|
|
328
|
+
info: info$1,
|
|
329
|
+
options: options$1,
|
|
330
|
+
pluginOptions: pluginOptions$1,
|
|
331
|
+
attributes: attributes$1
|
|
332
|
+
};
|
|
333
|
+
const kind = "collectionType";
|
|
334
|
+
const collectionName = "content_variant_assignments";
|
|
335
|
+
const info = {
|
|
336
|
+
singularName: "variant-assignment",
|
|
337
|
+
pluralName: "variant-assignments",
|
|
338
|
+
displayName: "Variant Assignment"
|
|
339
|
+
};
|
|
340
|
+
const options = {
|
|
341
|
+
draftAndPublish: false
|
|
342
|
+
};
|
|
343
|
+
const pluginOptions = {
|
|
344
|
+
"content-manager": {
|
|
345
|
+
visible: false
|
|
346
|
+
},
|
|
347
|
+
"content-type-builder": {
|
|
348
|
+
visible: false
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const attributes = {
|
|
352
|
+
variantLink: {
|
|
353
|
+
type: "relation",
|
|
354
|
+
relation: "manyToOne",
|
|
355
|
+
target: "plugin::content-variants.variant-link",
|
|
356
|
+
inversedBy: "assignments"
|
|
357
|
+
},
|
|
358
|
+
segment: {
|
|
359
|
+
type: "relation",
|
|
360
|
+
relation: "oneToOne",
|
|
361
|
+
target: "plugin::content-variants.segment"
|
|
362
|
+
},
|
|
363
|
+
priority: {
|
|
364
|
+
type: "integer",
|
|
365
|
+
"default": 0,
|
|
366
|
+
min: 0
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const variantAssignmentSchema = {
|
|
370
|
+
kind,
|
|
371
|
+
collectionName,
|
|
372
|
+
info,
|
|
373
|
+
options,
|
|
374
|
+
pluginOptions,
|
|
375
|
+
attributes
|
|
376
|
+
};
|
|
377
|
+
const contentTypes = {
|
|
378
|
+
segment: { schema: segmentSchema },
|
|
379
|
+
"variant-link": { schema: variantLinkSchema },
|
|
380
|
+
"variant-assignment": { schema: variantAssignmentSchema }
|
|
381
|
+
};
|
|
382
|
+
const segmentController = ({ strapi }) => ({
|
|
383
|
+
async find(ctx) {
|
|
384
|
+
const entities = await strapi.plugin("content-variants").service("segment").find(ctx.query);
|
|
385
|
+
ctx.body = entities;
|
|
386
|
+
},
|
|
387
|
+
async findOne(ctx) {
|
|
388
|
+
const { id } = ctx.params;
|
|
389
|
+
const entity = await strapi.plugin("content-variants").service("segment").findOne(id);
|
|
390
|
+
ctx.body = entity;
|
|
391
|
+
},
|
|
392
|
+
async create(ctx) {
|
|
393
|
+
const entity = await strapi.plugin("content-variants").service("segment").create(ctx.request.body);
|
|
394
|
+
ctx.body = entity;
|
|
395
|
+
},
|
|
396
|
+
async update(ctx) {
|
|
397
|
+
const { id } = ctx.params;
|
|
398
|
+
const entity = await strapi.plugin("content-variants").service("segment").update(id, ctx.request.body);
|
|
399
|
+
ctx.body = entity;
|
|
400
|
+
},
|
|
401
|
+
async delete(ctx) {
|
|
402
|
+
const { id } = ctx.params;
|
|
403
|
+
const entity = await strapi.plugin("content-variants").service("segment").delete(id);
|
|
404
|
+
ctx.body = entity;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
const variantLinkController = ({ strapi }) => ({
|
|
408
|
+
async find(ctx) {
|
|
409
|
+
const entities = await strapi.plugin("content-variants").service("variant-link").find(ctx.query);
|
|
410
|
+
ctx.body = entities;
|
|
411
|
+
},
|
|
412
|
+
async findOne(ctx) {
|
|
413
|
+
const { id } = ctx.params;
|
|
414
|
+
const entity = await strapi.plugin("content-variants").service("variant-link").findOne(id);
|
|
415
|
+
ctx.body = entity;
|
|
416
|
+
},
|
|
417
|
+
async create(ctx) {
|
|
418
|
+
const entity = await strapi.plugin("content-variants").service("variant-link").create(ctx.request.body);
|
|
419
|
+
ctx.body = entity;
|
|
420
|
+
},
|
|
421
|
+
async update(ctx) {
|
|
422
|
+
const { id } = ctx.params;
|
|
423
|
+
const entity = await strapi.plugin("content-variants").service("variant-link").update(id, ctx.request.body);
|
|
424
|
+
ctx.body = entity;
|
|
425
|
+
},
|
|
426
|
+
async delete(ctx) {
|
|
427
|
+
const { id } = ctx.params;
|
|
428
|
+
const entity = await strapi.plugin("content-variants").service("variant-link").delete(id);
|
|
429
|
+
ctx.body = entity;
|
|
430
|
+
},
|
|
431
|
+
async findFamily(ctx) {
|
|
432
|
+
const entity = await strapi.plugin("content-variants").service("variant-link").findFamily(ctx.query);
|
|
433
|
+
ctx.body = entity;
|
|
434
|
+
},
|
|
435
|
+
async findBatch(ctx) {
|
|
436
|
+
const entity = await strapi.plugin("content-variants").service("variant-link").findBatch(ctx.request.body);
|
|
437
|
+
ctx.body = entity;
|
|
438
|
+
},
|
|
439
|
+
/**
|
|
440
|
+
* Clones base content document (current locale only), creates the variant document,
|
|
441
|
+
* then creates variant-link + variant-assignments for selected segments.
|
|
442
|
+
*/
|
|
443
|
+
async createWithVariant(ctx) {
|
|
444
|
+
const entity = await strapi.plugin("content-variants").service("variant-link").createWithVariant(ctx.request.body);
|
|
445
|
+
ctx.body = entity;
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
const controllers = {
|
|
449
|
+
segment: segmentController,
|
|
450
|
+
"variant-link": variantLinkController
|
|
451
|
+
};
|
|
452
|
+
const admin = {
|
|
453
|
+
type: "admin",
|
|
454
|
+
routes: [
|
|
455
|
+
{
|
|
456
|
+
method: "GET",
|
|
457
|
+
path: "/segments",
|
|
458
|
+
handler: "segment.find",
|
|
459
|
+
config: { policies: [] }
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
method: "GET",
|
|
463
|
+
path: "/segments/:id",
|
|
464
|
+
handler: "segment.findOne",
|
|
465
|
+
config: { policies: [] }
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
method: "POST",
|
|
469
|
+
path: "/segments",
|
|
470
|
+
handler: "segment.create",
|
|
471
|
+
config: { policies: [] }
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
method: "PUT",
|
|
475
|
+
path: "/segments/:id",
|
|
476
|
+
handler: "segment.update",
|
|
477
|
+
config: { policies: [] }
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
method: "DELETE",
|
|
481
|
+
path: "/segments/:id",
|
|
482
|
+
handler: "segment.delete",
|
|
483
|
+
config: { policies: [] }
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
method: "GET",
|
|
487
|
+
path: "/links/family",
|
|
488
|
+
handler: "variant-link.findFamily",
|
|
489
|
+
config: { policies: [] }
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
method: "POST",
|
|
493
|
+
path: "/links/batch",
|
|
494
|
+
handler: "variant-link.findBatch",
|
|
495
|
+
config: { policies: [] }
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
method: "POST",
|
|
499
|
+
path: "/links/with-variant",
|
|
500
|
+
handler: "variant-link.createWithVariant",
|
|
501
|
+
config: { policies: [] }
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
method: "GET",
|
|
505
|
+
path: "/links",
|
|
506
|
+
handler: "variant-link.find",
|
|
507
|
+
config: { policies: [] }
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
method: "GET",
|
|
511
|
+
path: "/links/:id",
|
|
512
|
+
handler: "variant-link.findOne",
|
|
513
|
+
config: { policies: [] }
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
method: "POST",
|
|
517
|
+
path: "/links",
|
|
518
|
+
handler: "variant-link.create",
|
|
519
|
+
config: { policies: [] }
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
method: "PUT",
|
|
523
|
+
path: "/links/:id",
|
|
524
|
+
handler: "variant-link.update",
|
|
525
|
+
config: { policies: [] }
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
method: "DELETE",
|
|
529
|
+
path: "/links/:id",
|
|
530
|
+
handler: "variant-link.delete",
|
|
531
|
+
config: { policies: [] }
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
};
|
|
535
|
+
const contentApi = {
|
|
536
|
+
type: "content-api",
|
|
537
|
+
routes: [
|
|
538
|
+
{
|
|
539
|
+
method: "GET",
|
|
540
|
+
path: "/segments",
|
|
541
|
+
handler: "segment.find",
|
|
542
|
+
config: {
|
|
543
|
+
policies: []
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
]
|
|
547
|
+
};
|
|
548
|
+
const routes = {
|
|
549
|
+
admin,
|
|
550
|
+
"content-api": contentApi
|
|
551
|
+
};
|
|
552
|
+
const UID = "plugin::content-variants.segment";
|
|
553
|
+
function slugify(text) {
|
|
554
|
+
return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-");
|
|
555
|
+
}
|
|
556
|
+
const segmentService = ({ strapi }) => ({
|
|
557
|
+
async find(query = {}) {
|
|
558
|
+
return strapi.documents(UID).findMany(query);
|
|
559
|
+
},
|
|
560
|
+
async findOne(documentId) {
|
|
561
|
+
return strapi.documents(UID).findOne({ documentId });
|
|
562
|
+
},
|
|
563
|
+
async create(data) {
|
|
564
|
+
if (!data.slug && data.name) {
|
|
565
|
+
data.slug = slugify(data.name);
|
|
566
|
+
}
|
|
567
|
+
return strapi.documents(UID).create({ data });
|
|
568
|
+
},
|
|
569
|
+
async update(documentId, data) {
|
|
570
|
+
return strapi.documents(UID).update({ documentId, data });
|
|
571
|
+
},
|
|
572
|
+
async delete(documentId) {
|
|
573
|
+
return strapi.documents(UID).delete({ documentId });
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
const PLUGIN_ID = "content-variants";
|
|
577
|
+
const variantResolverService = ({ strapi }) => ({
|
|
578
|
+
/**
|
|
579
|
+
* Returns the list of field names marked as variant on a content type.
|
|
580
|
+
*/
|
|
581
|
+
getVariantFieldNames(contentTypeUid) {
|
|
582
|
+
const contentType = strapi.contentType(contentTypeUid);
|
|
583
|
+
if (!contentType) return [];
|
|
584
|
+
const attributes2 = contentType.attributes || {};
|
|
585
|
+
const names = [];
|
|
586
|
+
for (const [fieldName, attr] of Object.entries(attributes2)) {
|
|
587
|
+
const attribute = attr;
|
|
588
|
+
if (attribute?.pluginOptions?.[PLUGIN_ID]?.variant) {
|
|
589
|
+
names.push(fieldName);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return names;
|
|
593
|
+
},
|
|
594
|
+
/**
|
|
595
|
+
* Extracts only the variant-marked field values from a document.
|
|
596
|
+
*/
|
|
597
|
+
extractVariantFields(document, contentTypeUid) {
|
|
598
|
+
const fieldNames = this.getVariantFieldNames(contentTypeUid);
|
|
599
|
+
const fields = {};
|
|
600
|
+
for (const name of fieldNames) {
|
|
601
|
+
if (name in document) {
|
|
602
|
+
fields[name] = document[name];
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return fields;
|
|
606
|
+
},
|
|
607
|
+
/**
|
|
608
|
+
* Resolve variant fields: start with base document, overlay variant-marked
|
|
609
|
+
* fields from the variant document.
|
|
610
|
+
*/
|
|
611
|
+
resolveDocument(base, variant, options2) {
|
|
612
|
+
if (!base) return base;
|
|
613
|
+
if (!variant) return base;
|
|
614
|
+
const contentType = strapi.contentType(options2.contentTypeUid);
|
|
615
|
+
if (!contentType) return base;
|
|
616
|
+
const ctOptions = contentType.pluginOptions;
|
|
617
|
+
if (!ctOptions?.[PLUGIN_ID]?.enabled) return base;
|
|
618
|
+
const resolved = { ...base };
|
|
619
|
+
const fieldNames = this.getVariantFieldNames(options2.contentTypeUid);
|
|
620
|
+
for (const fieldName of fieldNames) {
|
|
621
|
+
if (fieldName in variant) {
|
|
622
|
+
resolved[fieldName] = variant[fieldName];
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return resolved;
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
const VARIANT_LINK_UID = "plugin::content-variants.variant-link";
|
|
629
|
+
const VARIANT_ASSIGNMENT_UID = "plugin::content-variants.variant-assignment";
|
|
630
|
+
const variantLinkService = ({ strapi }) => ({
|
|
631
|
+
async find(query = {}) {
|
|
632
|
+
const { baseContentType, baseDocumentId, variantDocumentId, populate, ...rest } = query;
|
|
633
|
+
const conditions = [];
|
|
634
|
+
if (baseContentType) {
|
|
635
|
+
conditions.push({ baseContentType: { $eq: baseContentType } });
|
|
636
|
+
}
|
|
637
|
+
if (baseDocumentId) {
|
|
638
|
+
conditions.push({ baseDocumentId: { $eq: baseDocumentId } });
|
|
639
|
+
}
|
|
640
|
+
if (variantDocumentId) {
|
|
641
|
+
conditions.push({ variantDocumentId: { $eq: variantDocumentId } });
|
|
642
|
+
}
|
|
643
|
+
return strapi.documents(VARIANT_LINK_UID).findMany({
|
|
644
|
+
...rest,
|
|
645
|
+
filters: conditions.length > 0 ? { $and: conditions } : void 0,
|
|
646
|
+
populate: populate || ["assignments.segment"]
|
|
647
|
+
});
|
|
648
|
+
},
|
|
649
|
+
async findOne(documentId) {
|
|
650
|
+
return strapi.documents(VARIANT_LINK_UID).findOne({
|
|
651
|
+
documentId,
|
|
652
|
+
populate: ["assignments.segment"]
|
|
653
|
+
});
|
|
654
|
+
},
|
|
655
|
+
async create(data) {
|
|
656
|
+
return strapi.documents(VARIANT_LINK_UID).create({ data });
|
|
657
|
+
},
|
|
658
|
+
async update(documentId, data) {
|
|
659
|
+
return strapi.documents(VARIANT_LINK_UID).update({ documentId, data });
|
|
660
|
+
},
|
|
661
|
+
async delete(documentId) {
|
|
662
|
+
const link = await strapi.documents(VARIANT_LINK_UID).findOne({
|
|
663
|
+
documentId
|
|
664
|
+
});
|
|
665
|
+
const result = await strapi.documents(VARIANT_LINK_UID).delete({ documentId });
|
|
666
|
+
if (link?.variantDocumentId && link?.baseContentType) {
|
|
667
|
+
try {
|
|
668
|
+
await strapi.documents(link.baseContentType).delete({
|
|
669
|
+
documentId: link.variantDocumentId
|
|
670
|
+
});
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return result;
|
|
675
|
+
},
|
|
676
|
+
/**
|
|
677
|
+
* Resolves the full "variant family" for any document (base or variant).
|
|
678
|
+
* Returns { baseDocumentId, isVariant, currentDocumentId, baseStatus, links[] }.
|
|
679
|
+
* Each link is enriched with `variantStatus` ('draft' | 'published' | 'modified').
|
|
680
|
+
*/
|
|
681
|
+
async findFamily(query) {
|
|
682
|
+
const { contentType, documentId } = query;
|
|
683
|
+
if (!contentType || !documentId) {
|
|
684
|
+
throw new Error("contentType and documentId are required");
|
|
685
|
+
}
|
|
686
|
+
const asVariant = await strapi.documents(VARIANT_LINK_UID).findMany({
|
|
687
|
+
filters: {
|
|
688
|
+
$and: [
|
|
689
|
+
{ baseContentType: { $eq: contentType } },
|
|
690
|
+
{ variantDocumentId: { $eq: documentId } }
|
|
691
|
+
]
|
|
692
|
+
},
|
|
693
|
+
populate: ["assignments.segment"]
|
|
694
|
+
});
|
|
695
|
+
let baseDocumentId;
|
|
696
|
+
let isVariant;
|
|
697
|
+
if (asVariant.length > 0) {
|
|
698
|
+
baseDocumentId = asVariant[0].baseDocumentId;
|
|
699
|
+
isVariant = true;
|
|
700
|
+
} else {
|
|
701
|
+
baseDocumentId = documentId;
|
|
702
|
+
isVariant = false;
|
|
703
|
+
}
|
|
704
|
+
const links = await strapi.documents(VARIANT_LINK_UID).findMany({
|
|
705
|
+
filters: {
|
|
706
|
+
$and: [
|
|
707
|
+
{ baseContentType: { $eq: contentType } },
|
|
708
|
+
{ baseDocumentId: { $eq: baseDocumentId } }
|
|
709
|
+
]
|
|
710
|
+
},
|
|
711
|
+
populate: ["assignments.segment"]
|
|
712
|
+
});
|
|
713
|
+
const resolveStatus = async (docId) => {
|
|
714
|
+
try {
|
|
715
|
+
const published = await strapi.documents(contentType).findOne({
|
|
716
|
+
documentId: docId,
|
|
717
|
+
status: "published"
|
|
718
|
+
});
|
|
719
|
+
if (!published) return "draft";
|
|
720
|
+
const draft = await strapi.documents(contentType).findOne({
|
|
721
|
+
documentId: docId
|
|
722
|
+
});
|
|
723
|
+
if (draft && new Date(draft.updatedAt) > new Date(published.updatedAt)) {
|
|
724
|
+
return "modified";
|
|
725
|
+
}
|
|
726
|
+
return "published";
|
|
727
|
+
} catch {
|
|
728
|
+
return "draft";
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
const allDocIds = [baseDocumentId, ...links.map((l) => l.variantDocumentId)].filter(Boolean);
|
|
732
|
+
const statuses = await Promise.all(allDocIds.map(resolveStatus));
|
|
733
|
+
const statusMap = {};
|
|
734
|
+
allDocIds.forEach((id, i) => {
|
|
735
|
+
statusMap[id] = statuses[i];
|
|
736
|
+
});
|
|
737
|
+
const enrichedLinks = links.map((link) => ({
|
|
738
|
+
...link,
|
|
739
|
+
variantStatus: statusMap[link.variantDocumentId] || "draft"
|
|
740
|
+
}));
|
|
741
|
+
return {
|
|
742
|
+
baseDocumentId,
|
|
743
|
+
isVariant,
|
|
744
|
+
currentDocumentId: documentId,
|
|
745
|
+
baseStatus: statusMap[baseDocumentId] || "draft",
|
|
746
|
+
links: enrichedLinks
|
|
747
|
+
};
|
|
748
|
+
},
|
|
749
|
+
/**
|
|
750
|
+
* Returns a Set of all variantDocumentIds for a given content type.
|
|
751
|
+
* Used to filter variant clones out of public API list results.
|
|
752
|
+
*/
|
|
753
|
+
async findVariantDocumentIds(contentType) {
|
|
754
|
+
const links = await strapi.documents(VARIANT_LINK_UID).findMany({
|
|
755
|
+
filters: { baseContentType: { $eq: contentType } },
|
|
756
|
+
fields: ["variantDocumentId"]
|
|
757
|
+
});
|
|
758
|
+
return new Set(links.map((l) => l.variantDocumentId));
|
|
759
|
+
},
|
|
760
|
+
/**
|
|
761
|
+
* Batch fetch variant links for multiple document IDs (base or variant).
|
|
762
|
+
* Searches both baseDocumentId and variantDocumentId fields.
|
|
763
|
+
* Returns a map of documentId → links[].
|
|
764
|
+
*/
|
|
765
|
+
async findBatch(query) {
|
|
766
|
+
const { baseContentType, documentIds } = query;
|
|
767
|
+
if (!baseContentType || !Array.isArray(documentIds) || documentIds.length === 0) {
|
|
768
|
+
return {};
|
|
769
|
+
}
|
|
770
|
+
const links = await strapi.documents(VARIANT_LINK_UID).findMany({
|
|
771
|
+
filters: {
|
|
772
|
+
$and: [
|
|
773
|
+
{ baseContentType: { $eq: baseContentType } },
|
|
774
|
+
{
|
|
775
|
+
$or: [
|
|
776
|
+
{ baseDocumentId: { $in: documentIds } },
|
|
777
|
+
{ variantDocumentId: { $in: documentIds } }
|
|
778
|
+
]
|
|
779
|
+
}
|
|
780
|
+
]
|
|
781
|
+
},
|
|
782
|
+
populate: ["assignments.segment"]
|
|
783
|
+
});
|
|
784
|
+
const grouped = {};
|
|
785
|
+
for (const id of documentIds) {
|
|
786
|
+
grouped[id] = [];
|
|
787
|
+
}
|
|
788
|
+
const idSet = new Set(documentIds);
|
|
789
|
+
for (const link of links) {
|
|
790
|
+
if (idSet.has(link.baseDocumentId)) {
|
|
791
|
+
grouped[link.baseDocumentId].push(link);
|
|
792
|
+
}
|
|
793
|
+
if (idSet.has(link.variantDocumentId)) {
|
|
794
|
+
grouped[link.variantDocumentId].push(link);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return grouped;
|
|
798
|
+
},
|
|
799
|
+
/**
|
|
800
|
+
* Creates a new variant document by cloning the base document (current locale),
|
|
801
|
+
* then creates the variant-link and its variant-assignments (segments + priority).
|
|
802
|
+
*/
|
|
803
|
+
async createWithVariant(payload) {
|
|
804
|
+
const { baseContentType, baseDocumentId, locale, label, segments } = payload || {};
|
|
805
|
+
if (!baseContentType || !baseDocumentId) {
|
|
806
|
+
throw new Error("baseContentType and baseDocumentId are required");
|
|
807
|
+
}
|
|
808
|
+
if (!Array.isArray(segments) || segments.length === 0) {
|
|
809
|
+
throw new Error("segments[] is required");
|
|
810
|
+
}
|
|
811
|
+
const existingLinks = await strapi.documents(VARIANT_LINK_UID).findMany({
|
|
812
|
+
filters: {
|
|
813
|
+
$and: [
|
|
814
|
+
{ baseContentType: { $eq: baseContentType } },
|
|
815
|
+
{ baseDocumentId: { $eq: baseDocumentId } }
|
|
816
|
+
]
|
|
817
|
+
},
|
|
818
|
+
populate: ["assignments.segment"]
|
|
819
|
+
});
|
|
820
|
+
const assignedSegmentIds = /* @__PURE__ */ new Set();
|
|
821
|
+
for (const link of existingLinks) {
|
|
822
|
+
for (const assignment of link.assignments || []) {
|
|
823
|
+
if (assignment.segment?.documentId) {
|
|
824
|
+
assignedSegmentIds.add(assignment.segment.documentId);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const duplicateSegments = segments.filter((s) => assignedSegmentIds.has(s.documentId));
|
|
829
|
+
if (duplicateSegments.length > 0) {
|
|
830
|
+
const names = duplicateSegments.map((s) => s.name || s.documentId).join(", ");
|
|
831
|
+
throw new Error(`Segments already assigned to existing variants: ${names}`);
|
|
832
|
+
}
|
|
833
|
+
const base = await strapi.documents(baseContentType).findOne({
|
|
834
|
+
documentId: baseDocumentId,
|
|
835
|
+
...locale ? { locale } : {}
|
|
836
|
+
});
|
|
837
|
+
const contentType = strapi.contentType(baseContentType);
|
|
838
|
+
const attributes2 = contentType?.attributes || {};
|
|
839
|
+
const clonedData = {};
|
|
840
|
+
for (const attrKey of Object.keys(attributes2)) {
|
|
841
|
+
if (Object.prototype.hasOwnProperty.call(base, attrKey) && typeof base[attrKey] !== "undefined") {
|
|
842
|
+
clonedData[attrKey] = base[attrKey];
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const variant = await strapi.documents(baseContentType).create({
|
|
846
|
+
data: clonedData
|
|
847
|
+
});
|
|
848
|
+
const variantDocumentId = variant?.documentId;
|
|
849
|
+
if (!variantDocumentId) {
|
|
850
|
+
throw new Error("Variant documentId was not returned");
|
|
851
|
+
}
|
|
852
|
+
const segmentNames = segments.map((s) => s.name).filter(Boolean);
|
|
853
|
+
const computedLabel = label?.trim() || (segmentNames.length > 0 ? `Variant for ${segmentNames.join(", ")}` : "Variant");
|
|
854
|
+
const variantLink = await strapi.documents(VARIANT_LINK_UID).create({
|
|
855
|
+
data: {
|
|
856
|
+
baseContentType,
|
|
857
|
+
baseDocumentId,
|
|
858
|
+
variantDocumentId,
|
|
859
|
+
label: computedLabel
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
const variantLinkDocumentId = variantLink?.documentId;
|
|
863
|
+
if (!variantLinkDocumentId) {
|
|
864
|
+
throw new Error("variant-link documentId was not returned");
|
|
865
|
+
}
|
|
866
|
+
await Promise.all(
|
|
867
|
+
segments.map(
|
|
868
|
+
(seg) => strapi.documents(VARIANT_ASSIGNMENT_UID).create({
|
|
869
|
+
data: {
|
|
870
|
+
priority: 0,
|
|
871
|
+
variantLink: { documentId: variantLinkDocumentId },
|
|
872
|
+
segment: { documentId: seg.documentId }
|
|
873
|
+
}
|
|
874
|
+
})
|
|
875
|
+
)
|
|
876
|
+
);
|
|
877
|
+
return { variantDocumentId, variantLinkDocumentId };
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
const services = {
|
|
881
|
+
segment: segmentService,
|
|
882
|
+
"variant-resolver": variantResolverService,
|
|
883
|
+
"variant-link": variantLinkService
|
|
884
|
+
};
|
|
885
|
+
const index = {
|
|
886
|
+
register,
|
|
887
|
+
bootstrap,
|
|
888
|
+
destroy,
|
|
889
|
+
config,
|
|
890
|
+
contentTypes,
|
|
891
|
+
controllers,
|
|
892
|
+
routes,
|
|
893
|
+
services
|
|
894
|
+
};
|
|
895
|
+
module.exports = index;
|