@captiveorigins/payload-plugin-onepedigree 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +88 -2
- package/dist/client/ParentRoleLabel.d.ts +8 -0
- package/dist/client/ParentRoleLabel.js +11 -0
- package/dist/client/PedigreePanel.d.ts +13 -0
- package/dist/client/PedigreePanel.js +43 -0
- package/dist/client/ThemeField.d.ts +5 -0
- package/dist/client/ThemeField.js +13 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.js +4 -0
- package/dist/collections.d.ts +16 -0
- package/dist/collections.js +25 -0
- package/dist/endpoints.d.ts +75 -0
- package/dist/endpoints.js +245 -0
- package/dist/fields.d.ts +41 -0
- package/dist/fields.js +135 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/integrity.d.ts +41 -0
- package/dist/integrity.js +211 -0
- package/dist/mapping.d.ts +22 -0
- package/dist/mapping.js +105 -0
- package/dist/plugin.d.ts +10 -0
- package/dist/plugin.js +135 -0
- package/dist/types.d.ts +130 -0
- package/dist/types.js +1 -0
- package/package.json +66 -3
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { PEDIGREE_VIEWS } from '@captiveorigins/onepedigree-core';
|
|
2
|
+
import { createCohortsCollection, createIndividualsCollection } from './collections.js';
|
|
3
|
+
import { cohortMembersEndpoint, pedigreeEndpoint, relativesEndpoint, validateEndpoint } from './endpoints.js';
|
|
4
|
+
import { createParentageField } from './fields.js';
|
|
5
|
+
import { cohortBirthParentBeforeChange, parentageBeforeChange, parentBeforeDelete, rememberAccessMode } from './integrity.js';
|
|
6
|
+
import { resolveOptions } from './mapping.js';
|
|
7
|
+
const CLIENT = '@captiveorigins/payload-plugin-onepedigree/client';
|
|
8
|
+
const panelField = (opts) => ({
|
|
9
|
+
name: 'pedigreePanel',
|
|
10
|
+
type: 'ui',
|
|
11
|
+
admin: {
|
|
12
|
+
components: {
|
|
13
|
+
Field: {
|
|
14
|
+
path: `${CLIENT}#PedigreePanel`,
|
|
15
|
+
clientProps: {
|
|
16
|
+
views: opts.views?.enabled ?? PEDIGREE_VIEWS,
|
|
17
|
+
defaultView: opts.views?.default ?? 'pedigree',
|
|
18
|
+
settingsGlobal: opts.admin?.settings === false ? null : opts.settingsGlobal,
|
|
19
|
+
terminology: opts.terminology ?? {},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const graphGlobal = (opts) => ({
|
|
26
|
+
slug: opts.graphGlobal,
|
|
27
|
+
admin: { hidden: true },
|
|
28
|
+
// Internal: written in the same transaction as each parentage change (serialization and cache revision).
|
|
29
|
+
access: { read: () => false, update: () => false },
|
|
30
|
+
fields: [{ name: 'revision', type: 'number', defaultValue: 0 }],
|
|
31
|
+
});
|
|
32
|
+
const settingsGlobal = (opts) => ({
|
|
33
|
+
slug: opts.settingsGlobal,
|
|
34
|
+
label: 'Pedigree settings',
|
|
35
|
+
access: { read: () => true, update: opts.admin?.settingsAccess ?? (({ req }) => Boolean(req.user)) },
|
|
36
|
+
fields: [
|
|
37
|
+
{
|
|
38
|
+
name: 'theme',
|
|
39
|
+
type: 'json',
|
|
40
|
+
admin: {
|
|
41
|
+
description: 'Colors of pedigree views: choose a preset and adjust any color with a hex code.',
|
|
42
|
+
components: { Field: `${CLIENT}#ThemeField` },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'defaultView',
|
|
47
|
+
type: 'select',
|
|
48
|
+
defaultValue: opts.views?.default ?? 'pedigree',
|
|
49
|
+
options: (opts.views?.enabled ?? PEDIGREE_VIEWS).map((v) => ({ label: v[0].toUpperCase() + v.slice(1), value: v })),
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
/**
|
|
54
|
+
* OnePedigree for Payload. Maps (or creates) an individuals collection,
|
|
55
|
+
* validates parentage on every write, serializes parentage changes, applies
|
|
56
|
+
* the deletion policy, and adds permission-aware pedigree endpoints and Admin
|
|
57
|
+
* components. Never creates collections for parents, roles, modes, edges or
|
|
58
|
+
* occurrences.
|
|
59
|
+
*/
|
|
60
|
+
export const onePedigree = (options) => (config) => {
|
|
61
|
+
const opts = resolveOptions(options);
|
|
62
|
+
let collections = [...(config.collections ?? [])];
|
|
63
|
+
if (opts.mode === 'managed') {
|
|
64
|
+
if (opts.cohorts?.managed && !collections.some((c) => c.slug === opts.cohorts.collection))
|
|
65
|
+
collections.push((options.overrides?.cohorts ?? ((c) => c))(createCohortsCollection({ slug: opts.cohorts.collection })));
|
|
66
|
+
if (!collections.some((c) => c.slug === opts.collection))
|
|
67
|
+
collections.push((options.overrides?.individuals ?? ((c) => c))(createIndividualsCollection({ slug: opts.collection, cohorts: opts.cohorts?.collection ?? null, roles: opts.roles, selectors: opts.selectors, terms: opts.terminology?.parents, userPreference: opts.terminology?.userPreference })));
|
|
68
|
+
}
|
|
69
|
+
if (!collections.some((c) => c.slug === opts.collection))
|
|
70
|
+
throw new Error(`OnePedigree: collection "${opts.collection}" not found`);
|
|
71
|
+
collections = collections.map((c) => {
|
|
72
|
+
if (c.slug === opts.collection) {
|
|
73
|
+
const inject = opts.mode === 'standard-fields' && !c.fields.some((f) => 'name' in f && f.name === 'parentage')
|
|
74
|
+
? [(options.overrides?.parentageField ?? ((f) => f))(createParentageField({
|
|
75
|
+
individuals: opts.collection,
|
|
76
|
+
cohorts: opts.cohorts?.collection,
|
|
77
|
+
roles: opts.roles,
|
|
78
|
+
defaultMode: opts.reproductiveModes?.default,
|
|
79
|
+
selectors: opts.selectors,
|
|
80
|
+
terms: opts.terminology?.parents,
|
|
81
|
+
userPreference: opts.terminology?.userPreference,
|
|
82
|
+
}))]
|
|
83
|
+
: [];
|
|
84
|
+
if (opts.disabled)
|
|
85
|
+
return { ...c, fields: [...c.fields, ...inject] };
|
|
86
|
+
return {
|
|
87
|
+
...c,
|
|
88
|
+
fields: [...c.fields, ...inject, ...(opts.admin?.panel === false ? [] : [panelField(opts)])],
|
|
89
|
+
hooks: {
|
|
90
|
+
...c.hooks,
|
|
91
|
+
beforeOperation: [...(c.hooks?.beforeOperation ?? []), rememberAccessMode],
|
|
92
|
+
beforeChange: [...(c.hooks?.beforeChange ?? []), parentageBeforeChange(opts)],
|
|
93
|
+
beforeDelete: [...(c.hooks?.beforeDelete ?? []), parentBeforeDelete(opts)],
|
|
94
|
+
},
|
|
95
|
+
endpoints: [...(Array.isArray(c.endpoints) ? c.endpoints : []), pedigreeEndpoint(opts), relativesEndpoint(opts), validateEndpoint(opts)],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (opts.cohorts && c.slug === opts.cohorts.collection && !opts.disabled)
|
|
99
|
+
return {
|
|
100
|
+
...c,
|
|
101
|
+
endpoints: [...(Array.isArray(c.endpoints) ? c.endpoints : []), cohortMembersEndpoint(opts)],
|
|
102
|
+
hooks: { ...c.hooks, beforeChange: [...(c.hooks?.beforeChange ?? []), cohortBirthParentBeforeChange(opts)] },
|
|
103
|
+
};
|
|
104
|
+
return c;
|
|
105
|
+
});
|
|
106
|
+
const globals = [...(config.globals ?? [])];
|
|
107
|
+
if (!opts.disabled) {
|
|
108
|
+
globals.push(graphGlobal(opts));
|
|
109
|
+
if (opts.admin?.settings !== false && !globals.some((g) => g.slug === opts.settingsGlobal))
|
|
110
|
+
globals.push(settingsGlobal(opts));
|
|
111
|
+
}
|
|
112
|
+
// The graph revision document must exist before the first parentage write: two transactions
|
|
113
|
+
// that each create it would not conflict, so concurrent writes could both commit.
|
|
114
|
+
const onInit = async (payload) => {
|
|
115
|
+
await config.onInit?.(payload);
|
|
116
|
+
if (opts.disabled)
|
|
117
|
+
return;
|
|
118
|
+
// Retried: on a fresh database this can race other startup writes creating the same
|
|
119
|
+
// MongoDB collection (a transient transaction error). Startup is never blocked by it.
|
|
120
|
+
for (let attempt = 1; attempt <= 5; attempt++)
|
|
121
|
+
try {
|
|
122
|
+
const current = (await payload.findGlobal({ slug: opts.graphGlobal, depth: 0, overrideAccess: true }));
|
|
123
|
+
if (!current?.createdAt)
|
|
124
|
+
await payload.updateGlobal({ slug: opts.graphGlobal, data: { revision: current?.revision ?? 0 }, overrideAccess: true, context: { onepedigreeInternal: true } });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (attempt === 5)
|
|
129
|
+
payload.logger.error({ err: error }, 'OnePedigree: could not create the pedigree graph revision document; it is created on the next start');
|
|
130
|
+
else
|
|
131
|
+
await new Promise((r) => setTimeout(r, 200 * attempt));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
return { ...config, collections, globals, onInit, custom: { ...config.custom, onepedigree: opts } };
|
|
135
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { LineageRoleConfig, ParentTerms, ParentTermsPreset, PedigreeViewId, ReproductiveModeId, ValidationRule } from '@captiveorigins/onepedigree-core';
|
|
2
|
+
import type { Access, CollectionConfig, Field } from 'payload';
|
|
3
|
+
/** How parentage is stored on the host collection. */
|
|
4
|
+
export type ParentageMapping = {
|
|
5
|
+
/** `parents[{ individual, role }]`, `reproductiveMode`, `cohort` in a group (default path `parentage`). */
|
|
6
|
+
kind: 'parent-array';
|
|
7
|
+
path?: string;
|
|
8
|
+
/** Names inside each parent row (defaults: `individual`, `role`). */
|
|
9
|
+
individualField?: string;
|
|
10
|
+
roleField?: string;
|
|
11
|
+
reproductiveMode?: string;
|
|
12
|
+
cohort?: string;
|
|
13
|
+
} | {
|
|
14
|
+
/** One relationship field per role, e.g. `{ mother: 'dam', father: 'sire' }`. */
|
|
15
|
+
kind: 'role-fields';
|
|
16
|
+
roles: Record<string, string>;
|
|
17
|
+
reproductiveMode?: string;
|
|
18
|
+
cohort?: string;
|
|
19
|
+
};
|
|
20
|
+
export type OnePedigreePluginOptions = {
|
|
21
|
+
/** Collection holding individuals (e.g. `animals`). */
|
|
22
|
+
collection: string;
|
|
23
|
+
/**
|
|
24
|
+
* `managed`: the plugin creates the collection (and cohorts) with standard fields.
|
|
25
|
+
* `standard-fields`: inject the standard parentage group into your existing collection.
|
|
26
|
+
* `mapped`: use your existing fields as described by `fields.parentage`.
|
|
27
|
+
* Default `standard-fields`.
|
|
28
|
+
*/
|
|
29
|
+
mode?: 'managed' | 'standard-fields' | 'mapped';
|
|
30
|
+
fields?: {
|
|
31
|
+
/** Display label field (default `name`, falling back to the ID). */
|
|
32
|
+
label?: string;
|
|
33
|
+
sex?: string;
|
|
34
|
+
parentage?: ParentageMapping;
|
|
35
|
+
/** Extra fields copied into projections as display data (explicitly approved; never everything). */
|
|
36
|
+
display?: readonly string[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Cohort collection: birthing events (a clutch, a litter, twins). `managed: true` creates it.
|
|
40
|
+
* `birthParentField`: the cohort's field naming the birthing parent; members whose recorded
|
|
41
|
+
* mother (maternal role) differs are refused. Fathers may differ per member. False: no cohorts.
|
|
42
|
+
*/
|
|
43
|
+
cohorts?: {
|
|
44
|
+
collection: string;
|
|
45
|
+
label?: string;
|
|
46
|
+
managed?: boolean;
|
|
47
|
+
birthParentField?: string;
|
|
48
|
+
} | false;
|
|
49
|
+
reproductiveModes?: {
|
|
50
|
+
default?: ReproductiveModeId | null;
|
|
51
|
+
};
|
|
52
|
+
/** Mother/Father pickers above the parents list (standard fields; labels from `terminology.parents`). False hides them. */
|
|
53
|
+
selectors?: boolean;
|
|
54
|
+
/** Role options in the parent editor (extend the built-ins). */
|
|
55
|
+
roles?: readonly string[];
|
|
56
|
+
lineageRoles?: LineageRoleConfig;
|
|
57
|
+
navigation?: {
|
|
58
|
+
individualUrl?: string;
|
|
59
|
+
cohortUrl?: string;
|
|
60
|
+
};
|
|
61
|
+
views?: {
|
|
62
|
+
enabled?: readonly PedigreeViewId[];
|
|
63
|
+
default?: PedigreeViewId;
|
|
64
|
+
};
|
|
65
|
+
/** Bounds for public traversal (requests are clamped to them). */
|
|
66
|
+
limits?: {
|
|
67
|
+
maxDepth?: number;
|
|
68
|
+
maxNodes?: number;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* A related record the requester cannot read: `omit` (default) leaves it out;
|
|
72
|
+
* `placeholder` shows an anonymous "private record" without its ID or name.
|
|
73
|
+
*/
|
|
74
|
+
hiddenRecords?: 'omit' | 'placeholder';
|
|
75
|
+
/** Deleting an individual that is someone's parent: refuse (default) or remove the references. */
|
|
76
|
+
onReferencedIndividualDelete?: 'restrict' | 'detach';
|
|
77
|
+
/** Host biological rules (e.g. taxonomy). Errors block the write. */
|
|
78
|
+
rules?: readonly ValidationRule[];
|
|
79
|
+
/** Labels only; stored keys never change. */
|
|
80
|
+
terminology?: {
|
|
81
|
+
individual?: string;
|
|
82
|
+
individuals?: string;
|
|
83
|
+
cohort?: string;
|
|
84
|
+
cohorts?: string;
|
|
85
|
+
/** Default words for the parent roles: a preset ('mother-father', 'dam-sire', 'female-male-parent') or custom. Default Mother/Father. */
|
|
86
|
+
parents?: ParentTermsPreset | ParentTerms;
|
|
87
|
+
/** Path on the signed-in user's document holding their own choice (see `createParentTermsPreferenceField`). */
|
|
88
|
+
userPreference?: string;
|
|
89
|
+
};
|
|
90
|
+
admin?: {
|
|
91
|
+
/** Pedigree preview, relatives and notices on the individual's edit view (default true). */
|
|
92
|
+
panel?: boolean;
|
|
93
|
+
/** A "Pedigree settings" global with the color theme editor (default true). */
|
|
94
|
+
settings?: boolean;
|
|
95
|
+
/** Who may change the settings (default: any signed-in user; restrict this in apps with member accounts). */
|
|
96
|
+
settingsAccess?: Access;
|
|
97
|
+
};
|
|
98
|
+
/** Access, fields or hooks to add to collections the plugin creates (managed mode). */
|
|
99
|
+
overrides?: {
|
|
100
|
+
individuals?: (c: CollectionConfig) => CollectionConfig;
|
|
101
|
+
cohorts?: (c: CollectionConfig) => CollectionConfig;
|
|
102
|
+
parentageField?: (f: Field) => Field;
|
|
103
|
+
};
|
|
104
|
+
/** Turn off everything but the fields (e.g. for migrations). */
|
|
105
|
+
disabled?: boolean;
|
|
106
|
+
};
|
|
107
|
+
export type ResolvedOptions = Required<Pick<OnePedigreePluginOptions, 'collection' | 'mode' | 'hiddenRecords' | 'onReferencedIndividualDelete'>> & Omit<OnePedigreePluginOptions, 'fields' | 'cohorts' | 'limits'> & {
|
|
108
|
+
fields: {
|
|
109
|
+
label: string;
|
|
110
|
+
sex: string | null;
|
|
111
|
+
parentage: Required<Extract<ParentageMapping, {
|
|
112
|
+
kind: 'parent-array';
|
|
113
|
+
}>> | Extract<ParentageMapping, {
|
|
114
|
+
kind: 'role-fields';
|
|
115
|
+
}>;
|
|
116
|
+
display: readonly string[];
|
|
117
|
+
};
|
|
118
|
+
cohorts: {
|
|
119
|
+
collection: string;
|
|
120
|
+
label: string;
|
|
121
|
+
managed: boolean;
|
|
122
|
+
birthParentField: string | null;
|
|
123
|
+
} | null;
|
|
124
|
+
limits: {
|
|
125
|
+
maxDepth: number;
|
|
126
|
+
maxNodes: number;
|
|
127
|
+
};
|
|
128
|
+
graphGlobal: string;
|
|
129
|
+
settingsGlobal: string;
|
|
130
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@captiveorigins/payload-plugin-onepedigree",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
|
+
"description": "OnePedigree for Payload CMS: parentage fields for existing or managed collections, cycle-safe serialized parentage writes, deletion policies, permission-aware pedigree endpoints for all 13 views, and Admin preview, relatives and color settings.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"payload",
|
|
7
|
+
"payload-plugin",
|
|
8
|
+
"payloadcms",
|
|
9
|
+
"pedigree",
|
|
10
|
+
"genealogy",
|
|
11
|
+
"breeding"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/mgilardi/onepedigree.git",
|
|
17
|
+
"directory": "packages/payload"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./client": {
|
|
26
|
+
"types": "./dist/client/index.d.ts",
|
|
27
|
+
"import": "./dist/client/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@captiveorigins/onepedigree-core": "^0.1.0-alpha.4",
|
|
42
|
+
"@captiveorigins/onepedigree-react": "^0.1.0-alpha.4"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@payloadcms/ui": "^3.80.0",
|
|
46
|
+
"payload": "^3.80.0",
|
|
47
|
+
"react": ">=19"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@payloadcms/ui": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@payloadcms/db-mongodb": "3.92.1",
|
|
56
|
+
"@payloadcms/ui": "^3.92.1",
|
|
57
|
+
"@types/react": "^19.3.0",
|
|
58
|
+
"graphql": "^16.14.2",
|
|
59
|
+
"mongodb-memory-server": "^11.3.0",
|
|
60
|
+
"payload": "^3.92.1",
|
|
61
|
+
"react": "^19.3.1",
|
|
62
|
+
"react-dom": "^19.3.1"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"test": "vitest run"
|
|
68
|
+
}
|
|
6
69
|
}
|