@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 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 panelBeforeChange = ({ data, originalDoc, operation }) => {
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" && !(data.validatedScope ?? originalDoc.validatedScope)?.length) fail("A panel with no validated claims can't become 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
- if (panel.status !== "active") fail("Results can only be made against an active panel.");
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
- return { status: { in: ["active", "superseded"] } };
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.0",
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.0"
38
+ "@captiveorigins/onephylogeny-core": "^0.1.0-alpha.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "payload": "^3.80.0"