@delmaredigital/payload-experiments 0.2.0-rc.7 → 0.3.0-rc.4
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 +202 -26
- package/dist/admin/PreviewLinks.d.ts +2 -1
- package/dist/admin/PreviewLinks.js +50 -21
- package/dist/admin/TestsNavLink.d.ts +3 -1
- package/dist/admin/TestsNavLink.js +2 -2
- package/dist/admin/actions/ConfirmDialog.d.ts +7 -4
- package/dist/admin/actions/ConfirmDialog.js +37 -82
- package/dist/admin/actions/DecideDialog.d.ts +3 -1
- package/dist/admin/actions/DecideDialog.js +30 -13
- package/dist/admin/actions/MakeContentResult.d.ts +13 -2
- package/dist/admin/actions/MakeContentResult.js +115 -16
- package/dist/admin/actions/actions.model.d.ts +2 -4
- package/dist/admin/actions/actions.model.js +2 -3
- package/dist/admin/allocation.model.js +3 -5
- package/dist/admin/api.d.ts +10 -4
- package/dist/admin/api.js +11 -7
- package/dist/admin/armSelect/ArmSelectField.js +14 -22
- package/dist/admin/arms/arms.model.d.ts +1 -21
- package/dist/admin/arms/arms.model.js +1 -153
- package/dist/admin/copy/actions.d.ts +39 -3
- package/dist/admin/copy/actions.js +40 -4
- package/dist/admin/copy/armSelect.js +5 -5
- package/dist/admin/copy/arms.d.ts +1 -6
- package/dist/admin/copy/arms.js +1 -16
- package/dist/admin/copy/audience.d.ts +2 -1
- package/dist/admin/copy/audience.js +3 -2
- package/dist/admin/copy/common.d.ts +13 -6
- package/dist/admin/copy/common.js +14 -8
- package/dist/admin/copy/goals.d.ts +21 -6
- package/dist/admin/copy/goals.js +27 -6
- package/dist/admin/copy/readiness.d.ts +22 -0
- package/dist/admin/copy/readiness.js +21 -0
- package/dist/admin/copy/recommendation.d.ts +0 -3
- package/dist/admin/copy/recommendation.js +0 -3
- package/dist/admin/copy/results.d.ts +43 -2
- package/dist/admin/copy/results.js +44 -3
- package/dist/admin/copy/review.d.ts +2 -2
- package/dist/admin/copy/review.js +2 -2
- package/dist/admin/copy/setting.d.ts +2 -0
- package/dist/admin/copy/setting.js +2 -0
- package/dist/admin/copy/surface.d.ts +7 -5
- package/dist/admin/copy/surface.js +9 -5
- package/dist/admin/copy/testPage.d.ts +10 -0
- package/dist/admin/copy/testPage.js +10 -0
- package/dist/admin/format/date.d.ts +2 -0
- package/dist/admin/format/date.js +10 -0
- package/dist/admin/paths.d.ts +2 -1
- package/dist/admin/paths.js +2 -1
- package/dist/admin/previewLinks.model.d.ts +4 -2
- package/dist/admin/previewLinks.model.js +8 -7
- package/dist/admin/results/CiChart.js +11 -21
- package/dist/admin/results/DataQualityPanel.d.ts +4 -2
- package/dist/admin/results/DataQualityPanel.js +30 -17
- package/dist/admin/results/DataTable.d.ts +16 -0
- package/dist/admin/results/DataTable.js +152 -0
- package/dist/admin/results/StatTile.d.ts +7 -0
- package/dist/admin/results/StatTile.js +22 -0
- package/dist/admin/results/VerdictHero.d.ts +13 -0
- package/dist/admin/results/{VerdictCard.js → VerdictHero.js} +15 -12
- package/dist/admin/results/legacy.d.ts +16 -0
- package/dist/admin/results/legacy.js +27 -0
- package/dist/admin/results/releasePages.model.d.ts +17 -0
- package/dist/admin/results/releasePages.model.js +10 -0
- package/dist/admin/results/results.css +64 -0
- package/dist/admin/results/results.model.d.ts +11 -7
- package/dist/admin/results/results.model.js +24 -7
- package/dist/admin/results/snapshot.d.ts +14 -0
- package/dist/admin/results/snapshot.js +3 -0
- package/dist/admin/surface/ResultsRoute.d.ts +1 -1
- package/dist/admin/surface/ResultsRoute.js +59 -34
- package/dist/admin/surface/SurfaceShell.d.ts +11 -2
- package/dist/admin/surface/SurfaceShell.js +49 -14
- package/dist/admin/surface/TestsHome.d.ts +6 -1
- package/dist/admin/surface/TestsHome.js +182 -114
- package/dist/admin/surface/TestsSurface.js +56 -16
- package/dist/admin/surface/goals/GoalForm.js +341 -308
- package/dist/admin/surface/goals/GoalsList.js +123 -89
- package/dist/admin/surface/goals/data.d.ts +8 -2
- package/dist/admin/surface/goals/data.js +24 -16
- package/dist/admin/surface/goals/goals.css +10 -13
- package/dist/admin/surface/home.model.d.ts +8 -8
- package/dist/admin/surface/home.model.js +23 -12
- package/dist/admin/surface/newTest/NewTest.js +9 -9
- package/dist/admin/surface/pageBlock/parts.js +31 -26
- package/dist/admin/surface/readiness/ActionDialogs.js +47 -19
- package/dist/admin/surface/readiness/Banner.d.ts +3 -0
- package/dist/admin/surface/readiness/Banner.js +58 -33
- package/dist/admin/surface/readiness/SaveStatus.d.ts +1 -0
- package/dist/admin/surface/readiness/SaveStatus.js +6 -3
- package/dist/admin/surface/readiness/readiness.model.d.ts +2 -0
- package/dist/admin/surface/readiness/readiness.model.js +25 -9
- package/dist/admin/surface/readiness/useTransition.d.ts +1 -0
- package/dist/admin/surface/readiness/useTransition.js +2 -1
- package/dist/admin/surface/results/ReleasePages.d.ts +14 -0
- package/dist/admin/surface/results/ReleasePages.js +116 -0
- package/dist/admin/surface/results/ResultsBody.d.ts +11 -1
- package/dist/admin/surface/results/ResultsBody.js +277 -105
- package/dist/admin/surface/results/ResultsHeader.d.ts +10 -0
- package/dist/admin/surface/results/ResultsHeader.js +177 -0
- package/dist/admin/surface/results/recommendation.model.d.ts +3 -2
- package/dist/admin/surface/results/recommendation.model.js +12 -9
- package/dist/admin/surface/results/useResults.d.ts +21 -3
- package/dist/admin/surface/results/useResults.js +2 -1
- package/dist/admin/surface/setting/parts.js +75 -88
- package/dist/admin/surface/setting/setting.css +3 -15
- package/dist/admin/surface/surface.css +61 -47
- package/dist/admin/surface/test/AdvancedSection.js +55 -48
- package/dist/admin/surface/test/GoalSection.js +46 -47
- package/dist/admin/surface/test/LengthSection.js +46 -34
- package/dist/admin/surface/test/Rail.d.ts +4 -0
- package/dist/admin/surface/test/Rail.js +83 -0
- package/dist/admin/surface/test/ReadinessBar.d.ts +5 -0
- package/dist/admin/surface/test/ReadinessBar.js +33 -0
- package/dist/admin/surface/test/Section.d.ts +1 -1
- package/dist/admin/surface/test/Section.js +36 -16
- package/dist/admin/surface/test/TargetingSection.js +168 -142
- package/dist/admin/surface/test/TestPage.d.ts +2 -1
- package/dist/admin/surface/test/TestPage.js +146 -68
- package/dist/admin/surface/test/VersionsSection.js +151 -120
- package/dist/admin/surface/test/fields.d.ts +11 -4
- package/dist/admin/surface/test/fields.js +84 -85
- package/dist/admin/surface/test/jumpToSection.d.ts +3 -0
- package/dist/admin/surface/test/jumpToSection.js +10 -0
- package/dist/admin/surface/test/pageParts.js +9 -3
- package/dist/admin/surface/test/testPage.css +81 -14
- package/dist/admin/surface/test/testPage.model.d.ts +2 -3
- package/dist/admin/surface/test/testPage.model.js +1 -21
- package/dist/admin/surface/useTestDocument.js +12 -5
- package/dist/admin/targeting/targeting.model.d.ts +1 -3
- package/dist/admin/targeting/targeting.model.js +3 -8
- package/dist/admin/theme.css +115 -36
- package/dist/admin/theme.d.ts +38 -0
- package/dist/admin/theme.js +38 -1
- package/dist/admin/useApi.d.ts +17 -2
- package/dist/admin/useLayerUsage.js +14 -23
- package/dist/admin/views/ExperimentsRootView.js +12 -2
- package/dist/admin/views/TestsNavLinkServer.d.ts +2 -0
- package/dist/admin/views/TestsNavLinkServer.js +43 -0
- package/dist/arms/copy.d.ts +6 -0
- package/dist/arms/copy.js +16 -0
- package/dist/arms/model.d.ts +23 -0
- package/dist/arms/model.js +173 -0
- package/dist/collections/experiments/config.js +25 -0
- package/dist/collections/experiments/lockHook.js +3 -1
- package/dist/collections/identities.js +6 -1
- package/dist/collections/metrics.js +7 -0
- package/dist/collections/metricsGuards.d.ts +9 -0
- package/dist/collections/metricsGuards.js +33 -0
- package/dist/contentDone.d.ts +4 -0
- package/dist/contentDone.js +5 -0
- package/dist/db/appends.d.ts +6 -2
- package/dist/db/appends.js +10 -4
- package/dist/db/identities.d.ts +8 -7
- package/dist/db/identities.js +9 -6
- package/dist/db/locks.d.ts +6 -0
- package/dist/db/locks.js +13 -1
- package/dist/db/metricUsage.d.ts +19 -0
- package/dist/db/metricUsage.js +54 -0
- package/dist/db/names.d.ts +5 -0
- package/dist/db/names.js +5 -0
- package/dist/db/pageOccupancy.d.ts +12 -0
- package/dist/db/pageOccupancy.js +45 -0
- package/dist/db/publishedContent.d.ts +10 -0
- package/dist/db/publishedContent.js +43 -0
- package/dist/db/results.d.ts +2 -0
- package/dist/db/results.js +6 -2
- package/dist/db/snapshots.d.ts +18 -6
- package/dist/db/snapshots.js +25 -8
- package/dist/db/sweep.js +11 -6
- package/dist/endpoints/index.js +19 -1
- package/dist/endpoints/results.js +5 -0
- package/dist/endpoints/sanitizeReadinessDetail.d.ts +1 -0
- package/dist/endpoints/sanitizeReadinessDetail.js +2 -0
- package/dist/endpoints/summary.js +20 -2
- package/dist/endpoints/transitions.d.ts +15 -0
- package/dist/endpoints/transitions.js +36 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +28 -0
- package/dist/exports/adminRsc.d.ts +1 -0
- package/dist/exports/adminRsc.js +1 -0
- package/dist/exports/index.d.ts +2 -1
- package/dist/exports/index.js +1 -0
- package/dist/exports/puck.d.ts +2 -1
- package/dist/exports/puck.js +1 -1
- package/dist/exports/puckEditor.d.ts +4 -1
- package/dist/exports/puckEditor.js +3 -1
- package/dist/exports/rsc.d.ts +6 -1
- package/dist/exports/rsc.js +2 -0
- package/dist/hooks/pageGuard.d.ts +5 -0
- package/dist/hooks/pageGuard.js +139 -0
- package/dist/lib/fetchAllDocs.d.ts +16 -0
- package/dist/lib/fetchAllDocs.js +56 -0
- package/dist/log.js +6 -3
- package/dist/manifest/compile.d.ts +5 -0
- package/dist/manifest/compile.js +5 -1
- package/dist/options.js +16 -0
- package/dist/plugin.js +30 -2
- package/dist/puck/ExperimentPicker.js +47 -16
- package/dist/puck/ExperimentSlot.d.ts +51 -1
- package/dist/puck/ExperimentSlot.js +23 -12
- package/dist/puck/copy.d.ts +6 -5
- package/dist/puck/copy.js +6 -5
- package/dist/puck/editor/CreateExperimentDrawer.js +46 -56
- package/dist/puck/editor/ExperimentSlotEditor.d.ts +52 -1
- package/dist/puck/editor/ExperimentSlotEditor.js +292 -137
- package/dist/puck/editor/ResultsReadout.d.ts +6 -0
- package/dist/puck/editor/ResultsReadout.js +99 -0
- package/dist/puck/editor/VersionBuilder.d.ts +34 -0
- package/dist/puck/editor/VersionBuilder.js +317 -0
- package/dist/puck/editor/armWrites.d.ts +7 -0
- package/dist/puck/editor/armWrites.js +71 -0
- package/dist/puck/editor/armsCache.d.ts +14 -0
- package/dist/puck/editor/armsCache.js +78 -0
- package/dist/puck/editor/copy.d.ts +33 -1
- package/dist/puck/editor/copy.js +45 -8
- package/dist/puck/editor/createTest.d.ts +1 -1
- package/dist/puck/editor/createTest.js +25 -14
- package/dist/puck/editor/editor.chrome.css +121 -0
- package/dist/puck/editor/editor.css +288 -204
- package/dist/puck/editor/editor.model.d.ts +4 -1
- package/dist/puck/editor/editor.model.js +10 -7
- package/dist/puck/editor/experimentOutlinePlugin.d.ts +3 -0
- package/dist/puck/editor/experimentOutlinePlugin.js +89 -0
- package/dist/puck/experimentSlotField.d.ts +2 -0
- package/dist/puck/experimentSlotField.js +11 -3
- package/dist/puck/hostContext.d.ts +10 -0
- package/dist/puck/hostContext.js +12 -0
- package/dist/puck/routes.d.ts +12 -0
- package/dist/puck/routes.js +11 -0
- package/dist/puck/slotConfig.d.ts +1 -1
- package/dist/puck/slotConfig.js +2 -2
- package/dist/results/aggregate.js +4 -3
- package/dist/results/analyse.d.ts +4 -1
- package/dist/results/analyse.js +27 -11
- package/dist/results/cuped.js +3 -2
- package/dist/results/identityCredit.d.ts +15 -7
- package/dist/results/identityCredit.js +32 -33
- package/dist/results/rolloutUnits.js +2 -1
- package/dist/results/snapshot.js +8 -2
- package/dist/results/srm.js +2 -1
- package/dist/sanitizeReadinessDetail.d.ts +7 -0
- package/dist/sanitizeReadinessDetail.js +63 -0
- package/dist/server/linkIdentity.d.ts +10 -1
- package/dist/server/linkIdentity.js +17 -15
- package/dist/server/metadata.d.ts +5 -0
- package/dist/server/metadata.js +18 -3
- package/dist/server/normaliseContentRelationships.d.ts +7 -0
- package/dist/server/normaliseContentRelationships.js +62 -0
- package/dist/server/recordContentChange.d.ts +1 -1
- package/dist/server/recordContentChange.js +17 -8
- package/dist/server/runResults.d.ts +3 -2
- package/dist/server/runResults.js +157 -35
- package/dist/targeting/values.d.ts +4 -0
- package/dist/targeting/values.js +10 -0
- package/dist/transitions/addMetric.js +8 -0
- package/dist/transitions/archive.js +2 -3
- package/dist/transitions/checkStart.d.ts +10 -3
- package/dist/transitions/checkStart.js +25 -0
- package/dist/transitions/contentFields.d.ts +7 -0
- package/dist/transitions/contentFields.js +20 -0
- package/dist/transitions/decide.js +22 -2
- package/dist/transitions/dismissContentEntry.d.ts +8 -0
- package/dist/transitions/dismissContentEntry.js +45 -0
- package/dist/transitions/makeContent.d.ts +5 -2
- package/dist/transitions/makeContent.js +217 -118
- package/dist/transitions/phaseRecord.d.ts +8 -2
- package/dist/transitions/placements.d.ts +3 -6
- package/dist/transitions/placements.js +12 -1
- package/dist/transitions/preflightData.d.ts +9 -1
- package/dist/transitions/preflightData.js +138 -20
- package/dist/transitions/releasePages.d.ts +8 -0
- package/dist/transitions/releasePages.js +26 -0
- package/dist/transitions/restoreContentEntry.d.ts +8 -0
- package/dist/transitions/restoreContentEntry.js +47 -0
- package/dist/transitions/start.js +62 -7
- package/dist/types/contracts.d.ts +24 -4
- package/dist/types/endpoints.d.ts +34 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/reasons.d.ts +3 -0
- package/dist/types/reasons.js +1 -0
- package/dist/types/snapshot.d.ts +6 -1
- package/dist/ui/Banner.d.ts +2 -0
- package/dist/ui/Banner.js +2 -0
- package/dist/ui/Breadcrumb.d.ts +7 -0
- package/dist/ui/Breadcrumb.js +25 -0
- package/dist/ui/Button.d.ts +15 -0
- package/dist/ui/Button.js +32 -0
- package/dist/ui/Card.d.ts +12 -0
- package/dist/ui/Card.js +40 -0
- package/dist/ui/Checkbox.d.ts +2 -0
- package/dist/ui/Checkbox.js +2 -0
- package/dist/{admin/components → ui}/ChipsInput.d.ts +4 -3
- package/dist/{admin/components → ui}/ChipsInput.js +20 -12
- package/dist/ui/Dialog.d.ts +13 -0
- package/dist/ui/Dialog.js +85 -0
- package/dist/ui/EmptyState.d.ts +2 -0
- package/dist/ui/EmptyState.js +2 -0
- package/dist/ui/Feedback.d.ts +24 -0
- package/dist/ui/Feedback.js +40 -0
- package/dist/ui/Inputs.d.ts +34 -0
- package/dist/ui/Inputs.js +225 -0
- package/dist/ui/NumberInput.d.ts +2 -0
- package/dist/ui/NumberInput.js +2 -0
- package/dist/ui/Range.d.ts +2 -0
- package/dist/ui/Range.js +2 -0
- package/dist/ui/SearchInput.d.ts +2 -0
- package/dist/ui/SearchInput.js +2 -0
- package/dist/ui/SectionCard.d.ts +2 -0
- package/dist/ui/SectionCard.js +2 -0
- package/dist/ui/Select.d.ts +2 -0
- package/dist/ui/Select.js +2 -0
- package/dist/ui/StatusChip.d.ts +2 -0
- package/dist/ui/StatusChip.js +2 -0
- package/dist/ui/Tabs.d.ts +16 -0
- package/dist/ui/Tabs.js +61 -0
- package/dist/ui/TextInput.d.ts +2 -0
- package/dist/ui/TextInput.js +2 -0
- package/dist/ui/Textarea.d.ts +2 -0
- package/dist/ui/Textarea.js +2 -0
- package/dist/ui/Toggle.d.ts +2 -0
- package/dist/ui/Toggle.js +2 -0
- package/dist/ui/accent.d.ts +40 -0
- package/dist/ui/accent.js +139 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.js +9 -0
- package/dist/ui/ui.css +92 -0
- package/package.json +5 -2
- package/dist/admin/components/chipsInput.css +0 -11
- package/dist/admin/results/ArmTable.d.ts +0 -8
- package/dist/admin/results/ArmTable.js +0 -113
- package/dist/admin/results/VerdictCard.d.ts +0 -9
- package/dist/admin/surface/goals/GoalsBreadcrumb.d.ts +0 -5
- package/dist/admin/surface/goals/GoalsBreadcrumb.js +0 -47
- package/dist/admin/surface/test/TestHeader.d.ts +0 -4
- package/dist/admin/surface/test/TestHeader.js +0 -70
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
import { commitTransaction, initTransaction, killTransaction } from 'payload';
|
|
1
|
+
import { Forbidden, NotFound, ValidationError, Locked, commitTransaction, initTransaction, killTransaction } from 'payload';
|
|
2
|
+
import { latestDraftContent, publishedContent as readPublishedContent } from '../db/publishedContent.js';
|
|
3
|
+
import { contentBlocks, hasLocalizedFields } from './contentFields.js';
|
|
4
|
+
import { safeError } from '../log.js';
|
|
5
|
+
import { XpRefusal } from '../errors.js';
|
|
2
6
|
import { lockContentDefaults } from '../db/appends.js';
|
|
3
7
|
import { pooledExecutor, txExecutor } from '../db/executor.js';
|
|
4
8
|
import { currentTxid, txidStatus } from '../db/txid.js';
|
|
5
9
|
import { buildAndStoreManifest } from '../manifest/compile.js';
|
|
6
10
|
import { puckCollections } from '../options.js';
|
|
11
|
+
import { contentDone } from '../contentDone.js';
|
|
7
12
|
import { archive } from './archive.js';
|
|
8
13
|
import { relationshipId } from './checkStart.js';
|
|
9
14
|
import { withTransition } from './kernel.js';
|
|
10
15
|
import { scanBlockPlacements } from './placements.js';
|
|
11
16
|
import { readExperiment, refuse, updateExperiment } from './start.js';
|
|
17
|
+
/** Classify by type/name only; host error messages can contain row data. */ function contentFailureReason(error) {
|
|
18
|
+
// Payload classes may use APIError as their name, so check instances too.
|
|
19
|
+
const name = error instanceof Error ? error.name : undefined;
|
|
20
|
+
if (error instanceof LocalizedContent) return 'localized';
|
|
21
|
+
if (error instanceof Forbidden || name === 'Forbidden') return 'forbidden';
|
|
22
|
+
if (error instanceof NotFound || name === 'NotFound') return 'not-found';
|
|
23
|
+
if (error instanceof ValidationError || name === 'ValidationError') return 'validation';
|
|
24
|
+
if (error instanceof Locked || name === 'Locked' || error instanceof XpRefusal && error.code === 'locked-placement') return 'locked';
|
|
25
|
+
return 'update-failed';
|
|
26
|
+
}
|
|
27
|
+
class LocalizedContent extends Error {
|
|
28
|
+
}
|
|
29
|
+
const isNotFound = (error)=>contentFailureReason(error) === 'not-found' || Boolean(error && typeof error === 'object' && 'status' in error && error.status === 404);
|
|
12
30
|
const object = (value)=>value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
13
31
|
/** Flatten selected slots in every content array, including nested selected slots. */ function unwrap(value, slug, slot) {
|
|
14
32
|
if (Array.isArray(value)) return value.flatMap((node)=>{
|
|
@@ -32,26 +50,69 @@ const object = (value)=>value !== null && typeof value === 'object' && !Array.is
|
|
|
32
50
|
}
|
|
33
51
|
/** Payload update merges omissions from the latest version even on publish.
|
|
34
52
|
* Restore each captured document's fields, omitting API/adapter metadata.
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
...Object.fromEntries(Object.entries(doc).filter(([key])=>!
|
|
53
|
+
*/ const contentMetadata = new Set([
|
|
54
|
+
'id',
|
|
55
|
+
'createdAt',
|
|
56
|
+
'updatedAt',
|
|
57
|
+
'_isLocked',
|
|
58
|
+
'_userEditing'
|
|
59
|
+
]);
|
|
60
|
+
function contentData(doc, field, content, fields, blocks = []) {
|
|
61
|
+
const data = {
|
|
62
|
+
...Object.fromEntries(Object.entries(doc).filter(([key])=>!contentMetadata.has(key))),
|
|
45
63
|
[field]: content
|
|
46
64
|
};
|
|
65
|
+
return fillContentFields(fields, data, blocks);
|
|
66
|
+
}
|
|
67
|
+
/** Adapter reads can omit empty fields (notably single polymorphic links).
|
|
68
|
+
* Make every stored value explicit so Payload cannot fill it from the other version.
|
|
69
|
+
*/ function fillContentFields(fields, source, blocks = []) {
|
|
70
|
+
const result = {
|
|
71
|
+
...source
|
|
72
|
+
};
|
|
73
|
+
for (const field of fields){
|
|
74
|
+
if (field.type === 'ui' || field.type === 'join' || 'virtual' in field && field.virtual) continue;
|
|
75
|
+
const name = 'name' in field ? field.name : undefined;
|
|
76
|
+
if (name && contentMetadata.has(name)) continue;
|
|
77
|
+
if (name && result[name] === undefined) result[name] = field.type === 'array' || field.type === 'blocks' || 'hasMany' in field && field.hasMany ? [] : null;
|
|
78
|
+
if ('tabs' in field) for (const tab of field.tabs){
|
|
79
|
+
if ('name' in tab && tab.name) {
|
|
80
|
+
const value = result[tab.name];
|
|
81
|
+
// Payload traverses groups/tabs as objects; null would be dereferenced.
|
|
82
|
+
result[tab.name] = fillContentFields(tab.fields, object(value) ? value : {}, blocks);
|
|
83
|
+
} else Object.assign(result, fillContentFields(tab.fields, result, blocks));
|
|
84
|
+
}
|
|
85
|
+
const value = name ? result[name] : result;
|
|
86
|
+
if ('fields' in field) {
|
|
87
|
+
if (Array.isArray(value)) result[name] = value.map((row)=>object(row) ? fillContentFields(field.fields, row, blocks) : row);
|
|
88
|
+
else if (object(value) || field.type === 'group') {
|
|
89
|
+
const filled = fillContentFields(field.fields, object(value) ? value : {}, blocks);
|
|
90
|
+
if (name) result[name] = filled;
|
|
91
|
+
else Object.assign(result, filled);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (field.type === 'blocks' && Array.isArray(value)) {
|
|
95
|
+
const definitions = contentBlocks(field, blocks);
|
|
96
|
+
result[name] = value.map((row)=>{
|
|
97
|
+
const block = object(row) ? definitions.find((block)=>block.slug === row.blockType) : undefined;
|
|
98
|
+
return block && object(row) ? fillContentFields(block.fields, row, blocks) : row;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
47
103
|
}
|
|
48
|
-
/**
|
|
104
|
+
/** Payload strips denied updates and fills them from the latest version. Only
|
|
105
|
+
* refuse when that substitution would change the proposed value (KI-1).
|
|
106
|
+
*/ // Payload beforeChange/getExistingRowDoc.js matches array/block rows by ID.
|
|
107
|
+
const existingRow = (row, original)=>object(row) && row.id && Array.isArray(original) ? original.find((value)=>object(value) && value.id === row.id) ?? {} : {};
|
|
108
|
+
async function fieldRefusal(fields, req, doc, operation = 'update', data = doc, siblingData = data, blockData, originalSibling = fillContentFields(fields, doc, req.payload.config.blocks)) {
|
|
49
109
|
for (const field of fields){
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
110
|
+
if (field.type === 'ui' || field.type === 'join' || 'virtual' in field && field.virtual) continue;
|
|
111
|
+
if ('name' in field && ![
|
|
112
|
+
'id',
|
|
113
|
+
'createdAt',
|
|
114
|
+
'updatedAt'
|
|
115
|
+
].includes(field.name) && JSON.stringify(siblingData[field.name] ?? null) !== JSON.stringify(originalSibling[field.name] ?? null) && 'access' in field && field.access?.[operation] && !await field.access[operation]({
|
|
55
116
|
id: doc.id,
|
|
56
117
|
req,
|
|
57
118
|
doc,
|
|
@@ -59,13 +120,11 @@ const object = (value)=>value !== null && typeof value === 'object' && !Array.is
|
|
|
59
120
|
siblingData,
|
|
60
121
|
blockData
|
|
61
122
|
})) {
|
|
62
|
-
return
|
|
123
|
+
return 'This page has fields you cannot update; update it by hand.';
|
|
63
124
|
}
|
|
64
125
|
const value = 'name' in field ? siblingData[field.name] : siblingData;
|
|
65
126
|
if ('tabs' in field) for (const tab of field.tabs){
|
|
66
|
-
if ('
|
|
67
|
-
if ('hidden' in tab && tab.hidden === true) return 'This page has hidden fields; update it by hand.';
|
|
68
|
-
if ('access' in tab && tab.access?.[operation] && !await tab.access[operation]({
|
|
127
|
+
if ('name' in tab && tab.name && JSON.stringify(siblingData[tab.name] ?? null) !== JSON.stringify(originalSibling[tab.name] ?? null) && 'access' in tab && tab.access?.[operation] && !await tab.access[operation]({
|
|
69
128
|
id: doc.id,
|
|
70
129
|
req,
|
|
71
130
|
doc,
|
|
@@ -73,31 +132,29 @@ const object = (value)=>value !== null && typeof value === 'object' && !Array.is
|
|
|
73
132
|
siblingData,
|
|
74
133
|
blockData
|
|
75
134
|
})) {
|
|
76
|
-
return
|
|
135
|
+
return 'This page has fields you cannot update; update it by hand.';
|
|
77
136
|
}
|
|
78
137
|
const tabData = 'name' in tab && tab.name ? siblingData[tab.name] : siblingData;
|
|
79
|
-
const
|
|
138
|
+
const originalTab = 'name' in tab && tab.name ? originalSibling[tab.name] : originalSibling;
|
|
139
|
+
const refusal = await fieldRefusal(tab.fields, req, doc, operation, data, object(tabData) ? tabData : {}, blockData, object(originalTab) ? originalTab : {});
|
|
80
140
|
if (refusal) return refusal;
|
|
81
141
|
}
|
|
142
|
+
const originalValue = 'name' in field ? originalSibling[field.name] : originalSibling;
|
|
82
143
|
if ('fields' in field) for (const row of Array.isArray(value) && value.length ? value : [
|
|
83
144
|
value
|
|
84
145
|
]){
|
|
85
|
-
const
|
|
146
|
+
const original = Array.isArray(value) ? existingRow(row, originalValue) : originalValue;
|
|
147
|
+
const refusal = await fieldRefusal(field.fields, req, doc, operation, data, object(row) ? row : {}, blockData, object(original) ? original : {});
|
|
86
148
|
if (refusal) return refusal;
|
|
87
149
|
}
|
|
88
150
|
if (field.type === 'blocks') {
|
|
89
|
-
const blocks =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
{}
|
|
97
|
-
]){
|
|
98
|
-
const refusal = await fieldRefusal(block.fields, req, doc, operation, data, row, row, branch);
|
|
99
|
-
if (refusal) return refusal;
|
|
100
|
-
}
|
|
151
|
+
const blocks = contentBlocks(field, req.payload.config.blocks);
|
|
152
|
+
for (const row of Array.isArray(value) ? value : []){
|
|
153
|
+
const block = object(row) ? blocks.find((block)=>block.slug === row.blockType) : undefined;
|
|
154
|
+
if (!block || !object(row)) continue;
|
|
155
|
+
const original = existingRow(row, originalValue);
|
|
156
|
+
const refusal = await fieldRefusal(block.fields, req, doc, operation, data, row, row, object(original) ? original : {});
|
|
157
|
+
if (refusal) return refusal;
|
|
101
158
|
}
|
|
102
159
|
}
|
|
103
160
|
}
|
|
@@ -142,29 +199,71 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
142
199
|
const decision = current.decision;
|
|
143
200
|
const winner = current.arms.find((arm)=>arm.key === decision?.winnerArm && arm.kind !== 'holdout');
|
|
144
201
|
if (!winner) refuse('invalid-winner');
|
|
202
|
+
const report = decision?.contentReport;
|
|
203
|
+
const previous = new Map((report?.docs ?? []).map((doc)=>[
|
|
204
|
+
doc.collection + ':' + doc.id,
|
|
205
|
+
doc
|
|
206
|
+
]));
|
|
145
207
|
const docs = [];
|
|
146
|
-
|
|
208
|
+
// Dismissed failures are handled by the manager: retain them unchanged and
|
|
209
|
+
// never retry them until restored. Each retried entry gets a fresh outcome.
|
|
210
|
+
const retainDismissed = (collection, id)=>{
|
|
211
|
+
const entry = previous.get(collection + ':' + id);
|
|
212
|
+
if (!entry?.dismissedAt) return false;
|
|
213
|
+
docs.push(entry);
|
|
214
|
+
return true;
|
|
215
|
+
};
|
|
216
|
+
const attempt = async (collection, id, fn, missingOk = false)=>{
|
|
217
|
+
if (retainDismissed(collection, id)) return;
|
|
218
|
+
let missing = false;
|
|
147
219
|
try {
|
|
148
|
-
await hostTransaction(req,
|
|
220
|
+
await hostTransaction(req, async ()=>{
|
|
221
|
+
try {
|
|
222
|
+
await fn();
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (missingOk && isNotFound(error)) {
|
|
225
|
+
// Payload may already have killed the failed write's transaction.
|
|
226
|
+
// Roll back any remaining writes, then check the row while still
|
|
227
|
+
// detached from the outer transition and its privileged context.
|
|
228
|
+
await killTransaction(req);
|
|
229
|
+
if (!await initTransaction(req)) throw new Error('A content transaction could not be started.');
|
|
230
|
+
missing = !await readPublishedContent(req, collection, id);
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
149
235
|
docs.push({
|
|
150
236
|
collection,
|
|
151
237
|
id,
|
|
152
238
|
ok: true
|
|
153
239
|
});
|
|
154
240
|
} catch (error) {
|
|
155
|
-
|
|
241
|
+
let detail;
|
|
242
|
+
if (isNotFound(error)) {
|
|
243
|
+
if (missingOk) detail = 'forbidden';
|
|
244
|
+
else detail = 'not-found';
|
|
245
|
+
} else {
|
|
246
|
+
detail = contentFailureReason(error);
|
|
247
|
+
}
|
|
248
|
+
req.payload.logger.error({
|
|
249
|
+
err: safeError(error),
|
|
250
|
+
experimentId,
|
|
251
|
+
collection,
|
|
252
|
+
id
|
|
253
|
+
}, 'Experiment content update failed');
|
|
156
254
|
docs.push({
|
|
157
255
|
collection,
|
|
158
256
|
id,
|
|
159
|
-
ok:
|
|
160
|
-
detail,
|
|
161
|
-
|
|
257
|
+
ok: Boolean(missing),
|
|
258
|
+
detail: missing ? 'not-found' : detail,
|
|
259
|
+
...!missing ? {
|
|
260
|
+
error: detail
|
|
261
|
+
} : {}
|
|
162
262
|
});
|
|
163
263
|
}
|
|
164
264
|
};
|
|
165
265
|
if (current.kind === 'block') {
|
|
166
266
|
if (!winner.slot) refuse('invalid-winner');
|
|
167
|
-
const report = decision?.contentReport;
|
|
168
267
|
const registrations = puckCollections(options);
|
|
169
268
|
const scanned = [];
|
|
170
269
|
for (const registration of registrations){
|
|
@@ -180,13 +279,18 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
180
279
|
} catch (error) {
|
|
181
280
|
// A collection made unreadable since the last attempt still has
|
|
182
281
|
// retained placements to report below. Never sacrifice the outer tx.
|
|
282
|
+
req.payload.logger.error({
|
|
283
|
+
err: safeError(error),
|
|
284
|
+
experimentId,
|
|
285
|
+
collection: registration.slug
|
|
286
|
+
}, 'Experiment content scan failed');
|
|
183
287
|
const denied = error && typeof error === 'object' && 'status' in error && error.status === 403;
|
|
184
288
|
if (!denied || !report?.docs?.some((doc)=>doc.collection === registration.slug)) throw error;
|
|
185
289
|
} finally{
|
|
186
290
|
req.transactionID = transactionID;
|
|
187
291
|
}
|
|
188
292
|
}
|
|
189
|
-
if (scanned.some((doc)=>doc.locked)) refuse('locked-placement');
|
|
293
|
+
if (scanned.some((doc)=>doc.locked && !previous.get(doc.collection + ':' + doc.id)?.dismissedAt)) refuse('locked-placement');
|
|
190
294
|
const placements = new Map();
|
|
191
295
|
for (const doc of [
|
|
192
296
|
...report?.docs ?? [],
|
|
@@ -196,14 +300,13 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
196
300
|
// Probe with no borrowed transaction: disableErrors handles expected
|
|
197
301
|
// NotFound/Forbidden, and even a host read hook cannot kill the outer tx.
|
|
198
302
|
for (const [key, { collection, id }] of placements){
|
|
303
|
+
if (retainDismissed(collection, id)) {
|
|
304
|
+
skipped.add(key);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
199
307
|
const transactionID = req.transactionID;
|
|
200
308
|
delete req.transactionID;
|
|
201
309
|
try {
|
|
202
|
-
const config = req.payload.config.collections.find((config)=>config.slug === collection);
|
|
203
|
-
const access = config?.access?.read ? await config.access.read({
|
|
204
|
-
id,
|
|
205
|
-
req
|
|
206
|
-
}) : true;
|
|
207
310
|
const doc = await req.payload.findByID({
|
|
208
311
|
collection,
|
|
209
312
|
id,
|
|
@@ -214,34 +317,44 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
214
317
|
includeLockStatus: true,
|
|
215
318
|
disableErrors: true
|
|
216
319
|
});
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const detail = ok ? 'This page no longer exists; nothing to change.' : access === false ? 'You can no longer read this page.' : 'You can no longer read this page. If it was deleted, archive this experiment with discard or hard-coded instead.';
|
|
222
|
-
docs.push({
|
|
223
|
-
collection,
|
|
224
|
-
id,
|
|
225
|
-
ok,
|
|
226
|
-
detail,
|
|
227
|
-
...!ok ? {
|
|
228
|
-
error: detail
|
|
229
|
-
} : {}
|
|
230
|
-
});
|
|
231
|
-
skipped.add(key);
|
|
232
|
-
} else if (doc._isLocked === true || doc.locked === true) refuse('locked-placement', {
|
|
320
|
+
// Null can also mean trashed or hidden by a host hook. Use the same
|
|
321
|
+
// unfiltered existence check as a thrown NotFound before reporting done.
|
|
322
|
+
if (!doc) throw new NotFound();
|
|
323
|
+
if (doc._isLocked === true || doc.locked === true) refuse('locked-placement', {
|
|
233
324
|
collection,
|
|
234
325
|
id
|
|
235
326
|
});
|
|
236
327
|
} catch (error) {
|
|
237
328
|
if (error && typeof error === 'object' && 'code' in error && error.code === 'locked-placement') throw error;
|
|
238
|
-
|
|
239
|
-
|
|
329
|
+
let missing = false;
|
|
330
|
+
let detail = isNotFound(error) ? 'forbidden' : contentFailureReason(error);
|
|
331
|
+
if (isNotFound(error)) {
|
|
332
|
+
try {
|
|
333
|
+
await hostTransaction(req, async ()=>{
|
|
334
|
+
missing = !await readPublishedContent(req, collection, id);
|
|
335
|
+
});
|
|
336
|
+
} catch (checkError) {
|
|
337
|
+
missing = false;
|
|
338
|
+
detail = 'update-failed';
|
|
339
|
+
req.payload.logger.error({
|
|
340
|
+
err: safeError(checkError),
|
|
341
|
+
experimentId,
|
|
342
|
+
collection,
|
|
343
|
+
id
|
|
344
|
+
}, 'Experiment content existence check failed');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
req.payload.logger.error({
|
|
348
|
+
err: safeError(error),
|
|
349
|
+
experimentId,
|
|
350
|
+
collection,
|
|
351
|
+
id
|
|
352
|
+
}, 'Experiment content read failed');
|
|
240
353
|
docs.push({
|
|
241
354
|
collection,
|
|
242
355
|
id,
|
|
243
356
|
ok: Boolean(missing),
|
|
244
|
-
detail,
|
|
357
|
+
detail: missing ? 'not-found' : detail,
|
|
245
358
|
...!missing ? {
|
|
246
359
|
error: detail
|
|
247
360
|
} : {}
|
|
@@ -256,7 +369,9 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
256
369
|
await attempt(collection, id, async ()=>{
|
|
257
370
|
const field = registrations.find((entry)=>entry.slug === collection)?.field;
|
|
258
371
|
if (!field) throw new Error('This content collection is no longer configured.');
|
|
259
|
-
const
|
|
372
|
+
const config = req.payload.config.collections.find((config)=>config.slug === collection);
|
|
373
|
+
if (hasLocalizedFields(config?.fields ?? [], req.payload.config)) throw new LocalizedContent();
|
|
374
|
+
await req.payload.findByID({
|
|
260
375
|
collection,
|
|
261
376
|
id,
|
|
262
377
|
req,
|
|
@@ -264,50 +379,48 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
264
379
|
depth: 0,
|
|
265
380
|
draft: true
|
|
266
381
|
});
|
|
267
|
-
const config = req.payload.config.collections.find((config)=>config.slug === collection);
|
|
268
382
|
const drafts = Boolean(config?.versions && config.versions.drafts);
|
|
269
|
-
const published =
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
depth: 0,
|
|
275
|
-
draft: false
|
|
276
|
-
}) : draft;
|
|
277
|
-
const draftContent = unwrap(draft[field], current.slug, winner.slot);
|
|
383
|
+
const published = await readPublishedContent(req, collection, id);
|
|
384
|
+
if (!published) throw new NotFound();
|
|
385
|
+
const draft = drafts ? await latestDraftContent(req, collection, id) ?? published : published;
|
|
386
|
+
const draftValue = draft[field] ?? null;
|
|
387
|
+
const draftContent = unwrap(draftValue, current.slug, winner.slot);
|
|
278
388
|
const publishedContent = unwrap(published[field], current.slug, winner.slot);
|
|
279
|
-
const changedDraft = JSON.stringify(draftContent) !== JSON.stringify(
|
|
389
|
+
const changedDraft = JSON.stringify(draftContent) !== JSON.stringify(draftValue);
|
|
280
390
|
const changedPublished = JSON.stringify(publishedContent) !== JSON.stringify(published[field]);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const contentField = topLevelField(config?.fields ?? [], field);
|
|
284
|
-
// Payload strips hidden and read-denied fields from a read, so presence in both proves readability;
|
|
285
|
-
// a localized content field compares only one locale, so it never takes the shortcut.
|
|
286
|
-
const contentReadable = Object.hasOwn(draft, field) && Object.hasOwn(published, field) && !contentField?.localized;
|
|
287
|
-
if (contentReadable && !changedDraft && !changedPublished) return;
|
|
288
|
-
for (const doc of [
|
|
289
|
-
draft,
|
|
290
|
-
published
|
|
291
|
-
]){
|
|
292
|
-
const refusal = await fieldRefusal(config?.fields ?? [], req, doc);
|
|
293
|
-
if (refusal) throw new Error(refusal);
|
|
294
|
-
}
|
|
295
|
-
const publishedData = contentData(published, field, publishedContent);
|
|
391
|
+
if (!changedDraft && !changedPublished) return;
|
|
392
|
+
const publishedData = contentData(published, field, publishedContent, config?.fields ?? [], req.payload.config.blocks);
|
|
296
393
|
const draftData = {
|
|
297
|
-
...contentData(draft, field, draftContent),
|
|
394
|
+
...contentData(draft, field, draftContent, config?.fields ?? [], req.payload.config.blocks),
|
|
298
395
|
_status: 'draft'
|
|
299
396
|
};
|
|
300
397
|
// Payload beforeValidate receives the latest version as doc, and the
|
|
301
398
|
// proposed write as data/siblingData (blockData is the proposed row).
|
|
302
|
-
|
|
303
|
-
|
|
399
|
+
if (changedPublished) {
|
|
400
|
+
const publicationRefusal = await fieldRefusal(config?.fields ?? [], req, draft, 'update', publishedData);
|
|
401
|
+
if (publicationRefusal) throw new ValidationError({
|
|
402
|
+
errors: [
|
|
403
|
+
{
|
|
404
|
+
message: publicationRefusal,
|
|
405
|
+
path: field
|
|
406
|
+
}
|
|
407
|
+
]
|
|
408
|
+
});
|
|
409
|
+
}
|
|
304
410
|
if (drafts) {
|
|
305
411
|
const draftOriginal = changedPublished ? {
|
|
306
412
|
...published,
|
|
307
413
|
...publishedData
|
|
308
414
|
} : draft;
|
|
309
415
|
const draftRefusal = await fieldRefusal(config?.fields ?? [], req, draftOriginal, 'update', draftData);
|
|
310
|
-
if (draftRefusal) throw new
|
|
416
|
+
if (draftRefusal) throw new ValidationError({
|
|
417
|
+
errors: [
|
|
418
|
+
{
|
|
419
|
+
message: draftRefusal,
|
|
420
|
+
path: field
|
|
421
|
+
}
|
|
422
|
+
]
|
|
423
|
+
});
|
|
311
424
|
}
|
|
312
425
|
// Host writes are access checked with the manager's req (§7.8).
|
|
313
426
|
if (changedPublished) await req.payload.update({
|
|
@@ -328,7 +441,7 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
328
441
|
draft: true,
|
|
329
442
|
data: draftData
|
|
330
443
|
});
|
|
331
|
-
});
|
|
444
|
+
}, true);
|
|
332
445
|
}
|
|
333
446
|
} else if (current.kind === 'flag') {
|
|
334
447
|
const point = options.flagPoints?.find((point)=>point.id === current.flagPoint);
|
|
@@ -341,6 +454,7 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
341
454
|
},
|
|
342
455
|
req
|
|
343
456
|
}));
|
|
457
|
+
else retainDismissed('xp-experiments', experimentId);
|
|
344
458
|
// Lock the shared state before its read/modify/write, after L/F/E.
|
|
345
459
|
await lockContentDefaults(exec);
|
|
346
460
|
// §7.8 transition exception: durable made-content defaults in plugin state.
|
|
@@ -376,7 +490,9 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
376
490
|
}
|
|
377
491
|
}
|
|
378
492
|
});
|
|
379
|
-
if (
|
|
493
|
+
if (contentDone({
|
|
494
|
+
docs
|
|
495
|
+
})) return {
|
|
380
496
|
...await archive(req, {
|
|
381
497
|
experimentId,
|
|
382
498
|
mode: 'after-content'
|
|
@@ -392,20 +508,3 @@ export async function makeContent(req, { experimentId, options }) {
|
|
|
392
508
|
});
|
|
393
509
|
return result;
|
|
394
510
|
}
|
|
395
|
-
/** The top-level data field with this name, looking through unnamed rows, collapsibles and tabs. */ function topLevelField(fields, name) {
|
|
396
|
-
for (const raw of fields){
|
|
397
|
-
const field = raw;
|
|
398
|
-
if (field.name === name) return field;
|
|
399
|
-
if (!field.name && (field.type === 'row' || field.type === 'collapsible') && field.fields) {
|
|
400
|
-
const found = topLevelField(field.fields, name);
|
|
401
|
-
if (found) return found;
|
|
402
|
-
}
|
|
403
|
-
if (field.type === 'tabs') {
|
|
404
|
-
for (const tab of field.tabs ?? [])if (!tab.name && tab.fields) {
|
|
405
|
-
const found = topLevelField(tab.fields, name);
|
|
406
|
-
if (found) return found;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return undefined;
|
|
411
|
-
}
|
|
@@ -13,14 +13,20 @@ export interface PhaseRecord {
|
|
|
13
13
|
allocationChanges?: AllocationChange[];
|
|
14
14
|
targeting: TargetingRule[];
|
|
15
15
|
metricDefinitions?: Array<Record<string, unknown>>;
|
|
16
|
-
analysis?: Record<string, unknown
|
|
16
|
+
analysis?: Record<string, unknown> & {
|
|
17
|
+
sequentialTuningParameter?: number | null;
|
|
18
|
+
};
|
|
17
19
|
contentChanges?: ContentChange[];
|
|
18
20
|
}
|
|
19
21
|
export declare function readPhaseRecord(req: PayloadRequest, experimentId: DocId, phase: number): Promise<PhaseRecord & {
|
|
20
22
|
id: DocId;
|
|
21
23
|
}>;
|
|
22
24
|
/** Caller holds the experiment lock and passes the kernel's transaction req. */
|
|
23
|
-
export declare function writePhaseRecord(req: PayloadRequest, record: PhaseRecord
|
|
25
|
+
export declare function writePhaseRecord(req: PayloadRequest, record: PhaseRecord & {
|
|
26
|
+
analysis: Record<string, unknown> & {
|
|
27
|
+
sequentialTuningParameter: number;
|
|
28
|
+
};
|
|
29
|
+
}): Promise<import("payload").JsonObject & import("payload").TypeWithID>;
|
|
24
30
|
/** End only an open phase, preserving the original stop time on a later decide.
|
|
25
31
|
* Caller holds the experiment lock and passes the kernel's transaction req.
|
|
26
32
|
*/
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import type { PayloadRequest } from 'payload';
|
|
2
|
-
import type { Arm, DocId, FlagPoint, ResolvedOptions } from '../types/contracts.js';
|
|
2
|
+
import type { Arm, DocId, FlagPoint, PreviewPlacement, ResolvedOptions } from '../types/contracts.js';
|
|
3
3
|
import type { ArmSlot } from '../types/puck.js';
|
|
4
|
-
export interface BlockPlacement {
|
|
5
|
-
collection: string;
|
|
6
|
-
id: DocId;
|
|
7
|
-
title: string;
|
|
8
|
-
published: boolean;
|
|
4
|
+
export interface BlockPlacement extends PreviewPlacement {
|
|
9
5
|
slots: Record<ArmSlot, number>;
|
|
10
6
|
locked?: boolean;
|
|
7
|
+
localized?: boolean;
|
|
11
8
|
}
|
|
12
9
|
export type FlagPlacement = Awaited<ReturnType<NonNullable<FlagPoint['placements']>>>[number];
|
|
13
10
|
/** Each matching ExperimentSlot is checked separately, including multiple slots
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasLocalizedFields } from './contentFields.js';
|
|
1
2
|
import { puckCollections as normalisePuckCollections } from '../options.js';
|
|
2
3
|
const slots = [
|
|
3
4
|
'control',
|
|
@@ -23,6 +24,7 @@ export async function scanBlockPlacements(req, args) {
|
|
|
23
24
|
}
|
|
24
25
|
})){
|
|
25
26
|
const publishedIds = new Set();
|
|
27
|
+
const publishedSlugs = new Map();
|
|
26
28
|
const draftsEnabled = req.payload.config.collections?.some((config)=>config.slug === collection && config.versions && config.versions.drafts);
|
|
27
29
|
for (const draft of [
|
|
28
30
|
false,
|
|
@@ -50,7 +52,10 @@ export async function scanBlockPlacements(req, args) {
|
|
|
50
52
|
for (const doc of result.docs){
|
|
51
53
|
if (!draft && doc._status === 'draft') continue;
|
|
52
54
|
if (draft && doc._status !== 'draft' && publishedIds.has(String(doc.id))) continue;
|
|
53
|
-
if (!draft)
|
|
55
|
+
if (!draft) {
|
|
56
|
+
publishedIds.add(String(doc.id));
|
|
57
|
+
if (typeof doc.slug === 'string') publishedSlugs.set(String(doc.id), doc.slug);
|
|
58
|
+
}
|
|
54
59
|
function walk(value) {
|
|
55
60
|
if (!value || typeof value !== 'object') return;
|
|
56
61
|
if (Array.isArray(value)) {
|
|
@@ -67,10 +72,16 @@ export async function scanBlockPlacements(req, args) {
|
|
|
67
72
|
id: doc.id,
|
|
68
73
|
title: String(doc.title ?? doc.name ?? doc.id),
|
|
69
74
|
published: !draft || doc._status === 'published',
|
|
75
|
+
...publishedSlugs.has(String(doc.id)) ? {
|
|
76
|
+
slug: publishedSlugs.get(String(doc.id))
|
|
77
|
+
} : {},
|
|
70
78
|
slots: Object.fromEntries(slots.map((slot)=>[
|
|
71
79
|
slot,
|
|
72
80
|
Array.isArray(props[slot]) ? props[slot].length : 0
|
|
73
81
|
])),
|
|
82
|
+
...hasLocalizedFields(req.payload.config.collections?.find((config)=>config.slug === collection)?.fields ?? [], req.payload.config) ? {
|
|
83
|
+
localized: true
|
|
84
|
+
} : {},
|
|
74
85
|
locked: doc._isLocked === true || doc.locked === true
|
|
75
86
|
});
|
|
76
87
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type PayloadRequest } from 'payload';
|
|
2
2
|
import type { PowerWeeksInput, PowerWeeksResult } from '@delmaredigital/experiments-stats';
|
|
3
3
|
import type { DocId, ResolvedOptions } from '../types/contracts.js';
|
|
4
4
|
import type { PreflightResult } from '../types/endpoints.js';
|
|
@@ -13,6 +13,14 @@ export interface PreflightDependencies {
|
|
|
13
13
|
export declare function loadStartInputs<TUser extends {
|
|
14
14
|
id: DocId;
|
|
15
15
|
}>(req: PayloadRequest, options: ResolvedOptions<TUser>, experimentId: DocId): Promise<CheckStartInput | null>;
|
|
16
|
+
/** Occupancy from the arms the runtime routes on (frozen rounds), shared by preflight and
|
|
17
|
+
* Start's locked recheck. Raw SQL on the request's transaction when there is one. */
|
|
18
|
+
export declare function loadPageTestsInUse(req: PayloadRequest): Promise<NonNullable<CheckStartInput['pageTestsInUse']>>;
|
|
19
|
+
/** Page targets no registered collection reports as published. A lookup that throws counts as
|
|
20
|
+
* unpublished (logged). Start calls it again under its page-path locks on a detached request. */
|
|
21
|
+
export declare function unpublishedPageTargets<TUser extends {
|
|
22
|
+
id: DocId;
|
|
23
|
+
}>(req: PayloadRequest, options: ResolvedOptions<TUser>, experiment: Pick<StartExperiment, 'id' | 'arms'>): Promise<string[]>;
|
|
16
24
|
/** Shared, request-scoped reads for summary; a failed row is null in tolerant mode.
|
|
17
25
|
* Preflight/Start use the same builder and retain their throwing read behaviour.
|
|
18
26
|
*/
|