|
1 | 1 | import type { CollectionSlug, PayloadHandler } from "payload"; |
2 | 2 | import { AB_PASS_PERCENTAGE_FIELD, AB_VARIANT_OF_FIELD, DEFAULT_SLUG_FIELD } from "../constants"; |
| 3 | +import { recomputeManifestForParent } from "../utils/recomputeManifest"; |
| 4 | +import type { AbTestingPluginConfig } from "../types/config"; |
3 | 5 |
|
4 | 6 | // 6-char alphanumeric hash — no external dep needed |
5 | 7 | function nanoid(): string { |
6 | 8 | return Math.random().toString(36).slice(2, 8); |
7 | 9 | } |
8 | 10 |
|
9 | | -export const duplicateVariantHandler: PayloadHandler = async (req) => { |
10 | | - if (!req.payload) { |
11 | | - return Response.json({ error: "Payload not available" }, { status: 500 }); |
12 | | - } |
| 11 | +export const buildDuplicateVariantHandler = |
| 12 | + <TVariantData extends object>(pluginConfig: AbTestingPluginConfig<TVariantData>): PayloadHandler => |
| 13 | + async (req) => { |
| 14 | + if (!req.payload) { |
| 15 | + return Response.json({ error: "Payload not available" }, { status: 500 }); |
| 16 | + } |
13 | 17 |
|
14 | | - let body: { collectionSlug?: string; docId?: string; slugField?: string }; |
15 | | - try { |
16 | | - if (!req.json) { |
| 18 | + let body: { collectionSlug?: string; docId?: string; slugField?: string }; |
| 19 | + try { |
| 20 | + if (!req.json) { |
| 21 | + return Response.json({ error: "Invalid JSON body" }, { status: 400 }); |
| 22 | + } |
| 23 | + body = await req.json(); |
| 24 | + } catch { |
17 | 25 | return Response.json({ error: "Invalid JSON body" }, { status: 400 }); |
18 | 26 | } |
19 | | - body = await req.json(); |
20 | | - } catch { |
21 | | - return Response.json({ error: "Invalid JSON body" }, { status: 400 }); |
22 | | - } |
23 | | - |
24 | | - const { collectionSlug, docId, slugField = DEFAULT_SLUG_FIELD } = body; |
25 | | - |
26 | | - if (!collectionSlug || !docId) { |
27 | | - return Response.json({ error: "collectionSlug and docId are required" }, { status: 400 }); |
28 | | - } |
29 | | - |
30 | | - let parentDoc: Record<string, unknown>; |
31 | | - try { |
32 | | - parentDoc = (await req.payload.findByID({ |
33 | | - collection: collectionSlug as CollectionSlug, |
34 | | - id: docId, |
35 | | - depth: 0, |
36 | | - overrideAccess: false, |
37 | | - req, |
38 | | - })) as Record<string, unknown>; |
39 | | - |
40 | | - if (!parentDoc) { |
| 27 | + |
| 28 | + const { collectionSlug, docId, slugField = DEFAULT_SLUG_FIELD } = body; |
| 29 | + |
| 30 | + if (!collectionSlug || !docId) { |
| 31 | + return Response.json({ error: "collectionSlug and docId are required" }, { status: 400 }); |
| 32 | + } |
| 33 | + |
| 34 | + const abConfig = pluginConfig.collections[collectionSlug]; |
| 35 | + if (!abConfig) { |
| 36 | + return Response.json( |
| 37 | + { error: `Collection "${collectionSlug}" is not configured for A/B testing` }, |
| 38 | + { status: 400 }, |
| 39 | + ); |
| 40 | + } |
| 41 | + |
| 42 | + let parentDoc: Record<string, unknown>; |
| 43 | + try { |
| 44 | + parentDoc = (await req.payload.findByID({ |
| 45 | + collection: collectionSlug as CollectionSlug, |
| 46 | + id: docId, |
| 47 | + depth: 0, |
| 48 | + overrideAccess: false, |
| 49 | + req, |
| 50 | + })) as Record<string, unknown>; |
| 51 | + |
| 52 | + if (!parentDoc) { |
| 53 | + return Response.json({ error: "Parent document not found" }, { status: 404 }); |
| 54 | + } |
| 55 | + } catch { |
41 | 56 | return Response.json({ error: "Parent document not found" }, { status: 404 }); |
42 | 57 | } |
43 | | - } catch { |
44 | | - return Response.json({ error: "Parent document not found" }, { status: 404 }); |
45 | | - } |
46 | | - |
47 | | - const originalSlug = (parentDoc[slugField] as string) ?? docId; |
48 | | - |
49 | | - let existingVariants: Array<Record<string, unknown>>; |
50 | | - try { |
51 | | - const result = await req.payload.find({ |
52 | | - collection: collectionSlug as CollectionSlug, |
53 | | - where: { [AB_VARIANT_OF_FIELD]: { equals: docId } }, |
54 | | - depth: 0, |
55 | | - limit: 100, |
56 | | - overrideAccess: true, |
57 | | - req, |
58 | | - }); |
59 | | - existingVariants = result.docs as Array<Record<string, unknown>>; |
60 | | - } catch (err) { |
61 | | - const message = err instanceof Error ? err.message : "Failed to read existing variants"; |
62 | | - return Response.json({ error: message }, { status: 500 }); |
63 | | - } |
64 | | - |
65 | | - const totalSlots = existingVariants.length + 2; |
66 | | - const perSlot = Math.max(1, Math.floor(100 / totalSlots)); |
67 | | - |
68 | | - const transactionID = (await req.payload.db.beginTransaction?.()) ?? undefined; |
69 | | - if (transactionID) { |
70 | | - req.transactionID = transactionID; |
71 | | - } |
72 | | - |
73 | | - try { |
74 | | - for (const variant of existingVariants) { |
75 | | - await req.payload.update({ |
| 58 | + |
| 59 | + const originalSlug = (parentDoc[slugField] as string) ?? docId; |
| 60 | + |
| 61 | + let existingVariants: Array<Record<string, unknown>>; |
| 62 | + try { |
| 63 | + const result = await req.payload.find({ |
76 | 64 | collection: collectionSlug as CollectionSlug, |
77 | | - id: variant.id as string, |
78 | | - data: { [AB_PASS_PERCENTAGE_FIELD]: perSlot }, |
79 | | - draft: true, |
| 65 | + where: { [AB_VARIANT_OF_FIELD]: { equals: docId } }, |
| 66 | + depth: 0, |
| 67 | + limit: 100, |
80 | 68 | overrideAccess: true, |
81 | 69 | req, |
82 | 70 | }); |
| 71 | + existingVariants = result.docs as Array<Record<string, unknown>>; |
| 72 | + } catch (err) { |
| 73 | + const message = err instanceof Error ? err.message : "Failed to read existing variants"; |
| 74 | + return Response.json({ error: message }, { status: 500 }); |
83 | 75 | } |
84 | 76 |
|
85 | | - const newDoc = (await req.payload.duplicate({ |
86 | | - collection: collectionSlug as CollectionSlug, |
87 | | - id: docId, |
88 | | - data: { |
89 | | - [slugField]: `${originalSlug}--${nanoid()}`, |
90 | | - [AB_VARIANT_OF_FIELD]: docId, |
91 | | - [AB_PASS_PERCENTAGE_FIELD]: perSlot, |
92 | | - }, |
93 | | - draft: true, |
94 | | - overrideAccess: true, |
95 | | - req, |
96 | | - })) as Record<string, unknown>; |
| 77 | + const totalSlots = existingVariants.length + 2; |
| 78 | + const perSlot = Math.max(1, Math.floor(100 / totalSlots)); |
97 | 79 |
|
| 80 | + const transactionID = (await req.payload.db.beginTransaction?.()) ?? undefined; |
98 | 81 | if (transactionID) { |
99 | | - await req.payload.db.commitTransaction?.(transactionID); |
| 82 | + req.transactionID = transactionID; |
100 | 83 | } |
101 | 84 |
|
102 | | - return Response.json({ id: newDoc.id, slug: newDoc[slugField], passPercentage: perSlot }, { status: 201 }); |
103 | | - } catch (err) { |
104 | | - if (transactionID) { |
105 | | - await req.payload.db.rollbackTransaction?.(transactionID); |
| 85 | + try { |
| 86 | + for (const variant of existingVariants) { |
| 87 | + await req.payload.db.updateOne({ |
| 88 | + collection: collectionSlug, |
| 89 | + id: variant.id as string | number, |
| 90 | + data: { [AB_PASS_PERCENTAGE_FIELD]: perSlot }, |
| 91 | + req, |
| 92 | + }); |
| 93 | + } |
| 94 | + |
| 95 | + const newDoc = (await req.payload.duplicate({ |
| 96 | + collection: collectionSlug as CollectionSlug, |
| 97 | + id: docId, |
| 98 | + data: { |
| 99 | + [slugField]: `${originalSlug}--${nanoid()}`, |
| 100 | + [AB_VARIANT_OF_FIELD]: docId, |
| 101 | + [AB_PASS_PERCENTAGE_FIELD]: perSlot, |
| 102 | + }, |
| 103 | + overrideAccess: true, |
| 104 | + req, |
| 105 | + })) as Record<string, unknown>; |
| 106 | + |
| 107 | + await recomputeManifestForParent(docId, collectionSlug, abConfig, pluginConfig, req); |
| 108 | + |
| 109 | + if (transactionID) { |
| 110 | + await req.payload.db.commitTransaction?.(transactionID); |
| 111 | + } |
| 112 | + |
| 113 | + return Response.json({ id: newDoc.id, slug: newDoc[slugField], passPercentage: perSlot }, { status: 201 }); |
| 114 | + } catch (err) { |
| 115 | + if (transactionID) { |
| 116 | + await req.payload.db.rollbackTransaction?.(transactionID); |
| 117 | + } |
| 118 | + const message = err instanceof Error ? err.message : "Failed to create variant"; |
| 119 | + return Response.json({ error: message }, { status: 500 }); |
106 | 120 | } |
107 | | - const message = err instanceof Error ? err.message : "Failed to create variant"; |
108 | | - return Response.json({ error: message }, { status: 500 }); |
109 | | - } |
110 | | -}; |
| 121 | + }; |
0 commit comments