@captiveorigins/payload-plugin-onephylogeny 0.1.0-alpha.0 → 0.1.0-alpha.1
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/index.d.ts +6 -1
- package/dist/index.js +36 -8
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ type OnePhylogenyPluginOptions = {
|
|
|
14
14
|
files?: string;
|
|
15
15
|
importRuns?: string;
|
|
16
16
|
};
|
|
17
|
-
/** Who may create panels and results, and activate panels. Default: nobody but trusted local scripts. */
|
|
17
|
+
/** Who may create panels and results, and activate panels. Default: nobody but trusted local scripts. Activating also needs a signed-in user, who is recorded. */
|
|
18
18
|
manageAccess?: Access;
|
|
19
19
|
/** Who may approve or reject results. Default: same as manageAccess. */
|
|
20
20
|
reviewAccess?: Access;
|
|
@@ -22,6 +22,11 @@ type OnePhylogenyPluginOptions = {
|
|
|
22
22
|
readReleasedAccess?: Access;
|
|
23
23
|
/** Field added to assignments for multi-tenant hosts' isolation plugins to find. */
|
|
24
24
|
group?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Which panels results may be made against. `activeOrShown` also allows a `candidate` a maintainer ticked "shown as
|
|
27
|
+
* exploratory", and lets readers see such candidates. Default: `active`.
|
|
28
|
+
*/
|
|
29
|
+
resultsAgainst?: 'active' | 'activeOrShown';
|
|
25
30
|
};
|
|
26
31
|
/**
|
|
27
32
|
* OnePhylogeny for Payload. Adds immutable panels and reviewed results, relating to whatever collections a site already
|
package/dist/index.js
CHANGED
|
@@ -19,16 +19,31 @@ var stripIds = (rows) => Array.isArray(rows) ? rows.map((r) => {
|
|
|
19
19
|
const { id: _id, ...rest } = r;
|
|
20
20
|
return rest;
|
|
21
21
|
}) : rows;
|
|
22
|
-
var
|
|
22
|
+
var stamp = (v) => v == null || v === "" ? "" : v instanceof Date ? v.toISOString() : String(v);
|
|
23
|
+
var ACTIVATION_FIELDS = ["activatedBy", "activatedAt"];
|
|
24
|
+
var panelBeforeChange = ({ data, originalDoc, operation, req }) => {
|
|
25
|
+
const leaving = operation === "update" && String(data.status ?? originalDoc.status) !== "candidate";
|
|
26
|
+
if (leaving) {
|
|
27
|
+
if (data.shownAsExploratory === true && String(originalDoc.status) !== "candidate") fail("Only a candidate panel can be shown as exploratory.");
|
|
28
|
+
data.shownAsExploratory = false;
|
|
29
|
+
}
|
|
23
30
|
if (hasCoordinates(data.groups)) fail("Panels use generalized regions and never hold coordinates.");
|
|
24
31
|
if (operation === "create") {
|
|
25
32
|
if ((data.status ?? "candidate") !== "candidate") fail("A new panel starts as a candidate.");
|
|
33
|
+
if (ACTIVATION_FIELDS.some((f) => stamp(data[f]))) fail("Who activated a panel, and when, are set by activating.");
|
|
26
34
|
return data;
|
|
27
35
|
}
|
|
28
36
|
const from = String(originalDoc.status);
|
|
29
37
|
const to = String(data.status ?? from);
|
|
30
38
|
if (to !== from && !PANEL_TRANSITIONS[from]?.includes(to)) fail(`A panel can't go from ${from} to ${to}.`);
|
|
31
|
-
if (from === "candidate" && to === "active"
|
|
39
|
+
if (from === "candidate" && to === "active") {
|
|
40
|
+
if (!(data.validatedScope ?? originalDoc.validatedScope)?.length) fail("A panel with no validated claims can't become active.");
|
|
41
|
+
if (!req.user) fail("Activating a panel needs a signed-in person, who is recorded.");
|
|
42
|
+
data.activatedBy = String(req.user.id);
|
|
43
|
+
data.activatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
44
|
+
} else if (ACTIVATION_FIELDS.some((f) => f in data && stamp(data[f]) !== stamp(originalDoc[f]))) {
|
|
45
|
+
fail("Who activated a panel, and when, are set by activating.");
|
|
46
|
+
}
|
|
32
47
|
if (from !== "candidate") {
|
|
33
48
|
for (const f of LOCKED_PANEL_FIELDS) {
|
|
34
49
|
if (!(f in data)) continue;
|
|
@@ -44,11 +59,14 @@ var supersedeOthers = (panels) => async ({ doc, previousDoc, req }) => {
|
|
|
44
59
|
for (const other of others.docs) await req.payload.update({ collection: panels, id: other.id, data: { status: "superseded" }, req, overrideAccess: true });
|
|
45
60
|
return doc;
|
|
46
61
|
};
|
|
47
|
-
var assignmentBeforeChange = (panels) => async ({ data, originalDoc, operation, req }) => {
|
|
62
|
+
var assignmentBeforeChange = (panels, againstShown) => async ({ data, originalDoc, operation, req }) => {
|
|
48
63
|
if (operation === "create") {
|
|
49
64
|
if ((data.reviewStatus ?? "pending") !== "pending") fail("A new result starts as pending review.");
|
|
50
65
|
const panel = await req.payload.findByID({ collection: panels, id: idOf(data.panel), depth: 0, req, overrideAccess: true });
|
|
51
|
-
|
|
66
|
+
const shown = panel.status === "candidate" && panel.shownAsExploratory === true;
|
|
67
|
+
if (panel.status !== "active" && !(againstShown && shown)) fail(againstShown ? "Results can only be made against an active panel, or a candidate shown as exploratory." : "Results can only be made against an active panel.");
|
|
68
|
+
data.panelStatus = panel.status;
|
|
69
|
+
data.panelShownAsExploratory = shown;
|
|
52
70
|
const result = data.result;
|
|
53
71
|
const problems = validateResult(result).filter((d) => d.severity === "error");
|
|
54
72
|
if (problems.length) fail(problems.map((p) => p.message).join("; "));
|
|
@@ -64,6 +82,9 @@ var assignmentBeforeChange = (panels) => async ({ data, originalDoc, operation,
|
|
|
64
82
|
for (const f of ["result", "panel", "sequence", "subject", "summary", "importRun"]) {
|
|
65
83
|
if (f in data && (f === "result" || f === "summary" ? !same(data[f], originalDoc[f]) : idOf(data[f]) !== idOf(originalDoc[f]))) fail("A result can't be edited. Make a new one instead.");
|
|
66
84
|
}
|
|
85
|
+
for (const f of ["panelStatus", "panelShownAsExploratory"]) {
|
|
86
|
+
if (f in data && !same(data[f], originalDoc[f])) fail("The panel status a result was made against is set when it is made.");
|
|
87
|
+
}
|
|
67
88
|
if (to !== from) {
|
|
68
89
|
if (!REVIEW_TRANSITIONS[from]?.includes(to)) fail(`A result can't go from ${from} to ${to}.`);
|
|
69
90
|
if (to === "approved") {
|
|
@@ -86,6 +107,7 @@ var onePhylogeny = (options = {}) => (config) => {
|
|
|
86
107
|
const manage = options.manageAccess ?? (() => false);
|
|
87
108
|
const review = options.reviewAccess ?? manage;
|
|
88
109
|
const readReleased = options.readReleasedAccess ?? (({ req }) => Boolean(req.user));
|
|
110
|
+
const againstShown = options.resultsAgainst === "activeOrShown";
|
|
89
111
|
const panelCollection = {
|
|
90
112
|
slug: panels,
|
|
91
113
|
labels: { singular: "Reference panel", plural: "Reference panels" },
|
|
@@ -93,7 +115,8 @@ var onePhylogeny = (options = {}) => (config) => {
|
|
|
93
115
|
access: {
|
|
94
116
|
read: async (args) => {
|
|
95
117
|
if (await manage(args)) return true;
|
|
96
|
-
|
|
118
|
+
const published = { status: { in: ["active", "superseded"] } };
|
|
119
|
+
return againstShown ? { or: [published, { and: [{ status: { equals: "candidate" } }, { shownAsExploratory: { equals: true } }] }] } : published;
|
|
97
120
|
},
|
|
98
121
|
create: manage,
|
|
99
122
|
update: manage,
|
|
@@ -103,6 +126,7 @@ var onePhylogeny = (options = {}) => (config) => {
|
|
|
103
126
|
{ name: "key", type: "text", required: true, index: true, admin: { description: "Stays the same across versions of one panel." } },
|
|
104
127
|
{ name: "version", type: "text", required: true },
|
|
105
128
|
{ name: "status", type: "select", required: true, defaultValue: "candidate", index: true, options: ["candidate", "active", "superseded", "withdrawn"] },
|
|
129
|
+
{ name: "shownAsExploratory", type: "checkbox", defaultValue: false, index: true, admin: { description: "Show this candidate as exploratory, before review. Only candidates; cleared when it moves on." } },
|
|
106
130
|
{ name: "marker", type: "text", required: true },
|
|
107
131
|
{ name: "checksum", type: "text", required: true, index: true },
|
|
108
132
|
...relation("taxa", r.taxa, { hasMany: true }),
|
|
@@ -110,7 +134,9 @@ var onePhylogeny = (options = {}) => (config) => {
|
|
|
110
134
|
...relation("importRun", r.importRuns),
|
|
111
135
|
{ name: "validatedScope", type: "text", hasMany: true, admin: { description: "The claims this panel is validated for. Pages show only these." } },
|
|
112
136
|
{ name: "notCovered", type: "text", hasMany: true },
|
|
113
|
-
{ name: "groups", type: "array", fields: [{ name: "groupId", type: "text", required: true }, { name: "label", type: "text", required: true }, { name: "regionName", type: "text", admin: { description: "A general region, never coordinates." } }] }
|
|
137
|
+
{ name: "groups", type: "array", fields: [{ name: "groupId", type: "text", required: true }, { name: "label", type: "text", required: true }, { name: "regionName", type: "text", admin: { description: "A general region, never coordinates." } }] },
|
|
138
|
+
{ name: "activatedBy", type: "text", admin: { readOnly: true, description: "The signed-in user who activated this panel. Set by activating." } },
|
|
139
|
+
{ name: "activatedAt", type: "date", admin: { readOnly: true } }
|
|
114
140
|
],
|
|
115
141
|
hooks: { beforeChange: [panelBeforeChange], afterChange: [supersedeOthers(panels)] }
|
|
116
142
|
};
|
|
@@ -139,9 +165,11 @@ var onePhylogeny = (options = {}) => (config) => {
|
|
|
139
165
|
...relation("importRun", r.importRuns),
|
|
140
166
|
{ name: "reviewStatus", type: "select", required: true, defaultValue: "pending", index: true, options: ["pending", "approved", "rejected", "withdrawn"] },
|
|
141
167
|
{ name: "reviewer", type: "text", admin: { readOnly: true } },
|
|
142
|
-
{ name: "releasedAt", type: "date", admin: { readOnly: true } }
|
|
168
|
+
{ name: "releasedAt", type: "date", admin: { readOnly: true } },
|
|
169
|
+
{ name: "panelStatus", type: "text", admin: { readOnly: true, description: "The panel's status when this result was made." } },
|
|
170
|
+
{ name: "panelShownAsExploratory", type: "checkbox", admin: { readOnly: true } }
|
|
143
171
|
],
|
|
144
|
-
hooks: { beforeChange: [assignmentBeforeChange(panels)] }
|
|
172
|
+
hooks: { beforeChange: [assignmentBeforeChange(panels, againstShown)] }
|
|
145
173
|
};
|
|
146
174
|
const released = {
|
|
147
175
|
path: "/onephylogeny/released",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@captiveorigins/payload-plugin-onephylogeny",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
4
|
"description": "OnePhylogeny for Payload: immutable reference panels and released results on your own collections, with server-enforced review, safe endpoints and no coordinates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"phylogenetics",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"node": ">=22"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@captiveorigins/onephylogeny-core": "^0.1.0-alpha.
|
|
38
|
+
"@captiveorigins/onephylogeny-core": "^0.1.0-alpha.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"payload": "^3.80.0"
|