payload-smart-deletion 0.0.0

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 ADDED
@@ -0,0 +1,113 @@
1
+ # payload-smart-deletion
2
+
3
+ Cascading deletes for Payload CMS relationship fields, inspired by PostgreSQL referential actions.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/payload-smart-deletion)](https://www.npmjs.com/package/payload-smart-deletion)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
7
+
8
+ ## Overview
9
+
10
+ Mark relationship fields with a cascade action and the plugin handles the rest. When a document is deleted, all referenced documents are deleted too. When trash is enabled, cascades follow soft-delete and restore transitions through the entire document tree.
11
+
12
+ **Features**
13
+
14
+ - **Hard-delete cascade** -- permanently deletes referenced documents when the parent is deleted.
15
+ - **Soft-delete cascade** -- trashes referenced documents when the parent is trashed.
16
+ - **Restore cascade** -- restores referenced documents when the parent is restored.
17
+ - **Polymorphic support** -- works with single and polymorphic (`relationTo: [...]`) relationships.
18
+ - **Nested field support** -- finds cascade fields inside groups, tabs, and blocks.
19
+ - **Automatic trash propagation** -- optionally enables `trash: true` on target collections that need it.
20
+
21
+ ## Installation
22
+
23
+ ```sh
24
+ pnpm add payload-smart-deletion
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### 1. Add the plugin
30
+
31
+ **Important:** `smartDeletionPlugin` scans all collections at config time. It must be listed **after** any plugin that injects cascade relationship fields, so those fields are visible during the scan.
32
+
33
+ ```ts
34
+ // payload.config.ts
35
+ import { buildConfig } from "payload";
36
+ import { discussionsPlugin } from "payload-discussions";
37
+ import { smartDeletionPlugin } from "payload-smart-deletion";
38
+
39
+ export default buildConfig({
40
+ // ...
41
+ plugins: [
42
+ discussionsPlugin({ collections: ["posts"] }), // injects cascade fields
43
+ smartDeletionPlugin(), // must come after
44
+ ],
45
+ });
46
+ ```
47
+
48
+ ### 2. Mark relationship fields
49
+
50
+ Add `custom.smartDeletion: 'cascade'` to any relationship field that should cascade deletes:
51
+
52
+ ```ts
53
+ const Posts: CollectionConfig = {
54
+ slug: "posts",
55
+ trash: true,
56
+ fields: [
57
+ {
58
+ name: "comments",
59
+ type: "relationship",
60
+ relationTo: "comments",
61
+ hasMany: true,
62
+ custom: { smartDeletion: "cascade" },
63
+ },
64
+ ],
65
+ };
66
+ ```
67
+
68
+ Deleting a post now deletes all its comments. If the post is trashed, comments are trashed too. Restoring the post restores the comments. Cascades are recursive -- if the target collection also has cascade fields, they are followed automatically.
69
+
70
+ ### Plugin options
71
+
72
+ | Option | Type | Default | Description |
73
+ | ----------------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
74
+ | `autoEnableTrash` | `boolean` | `true` | When the source collection has `trash: true`, automatically enable `trash: true` on target collections. When `false`, logs a warning instead. |
75
+
76
+ ### Trash behavior
77
+
78
+ Soft-delete and restore hooks are only registered when the source collection has `trash: true`. If a target collection does not have trash enabled:
79
+
80
+ - With `autoEnableTrash: true` (default): the plugin enables it automatically.
81
+ - With `autoEnableTrash: false`: a warning is logged and cascaded deletes will **hard-delete** target documents instead of trashing them.
82
+
83
+ ## Contributing
84
+
85
+ This plugin lives in the [payload-plugins](https://github.com/davincicoding-org/payload-plugins) monorepo.
86
+
87
+ ### Development
88
+
89
+ ```sh
90
+ pnpm install
91
+
92
+ # watch this plugin for changes
93
+ pnpm --filter payload-smart-deletion dev
94
+
95
+ # run the Payload dev app (in a second terminal)
96
+ pnpm --filter sandbox dev
97
+ ```
98
+
99
+ The `sandbox/` directory is a Next.js + Payload app that imports plugins via `workspace:*` -- use it to test changes locally.
100
+
101
+ ### Code quality
102
+
103
+ - **Formatting & linting** -- handled by [Biome](https://biomejs.dev/), enforced on commit via husky + lint-staged.
104
+ - **Commits** -- must follow [Conventional Commits](https://www.conventionalcommits.org/) with a valid scope (e.g. `fix(payload-smart-deletion): ...`).
105
+ - **Changesets** -- please include a [changeset](https://github.com/changesets/changesets) in your PR by running `pnpm release`.
106
+
107
+ ### Issues & PRs
108
+
109
+ Bug reports and feature requests are welcome -- [open an issue](https://github.com/davincicoding-org/payload-plugins/issues).
110
+
111
+ ## License
112
+
113
+ MIT
@@ -0,0 +1,2 @@
1
+ export declare const CUSTOM_KEY = "smartDeletion";
2
+ //# sourceMappingURL=const.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"const.d.ts","sourceRoot":"","sources":["../src/const.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,kBAAkB,CAAC"}
package/dist/const.js ADDED
@@ -0,0 +1,3 @@
1
+ export const CUSTOM_KEY = 'smartDeletion';
2
+
3
+ //# sourceMappingURL=const.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/const.ts"],"sourcesContent":["export const CUSTOM_KEY = 'smartDeletion';\n"],"names":["CUSTOM_KEY"],"mappings":"AAAA,OAAO,MAAMA,aAAa,gBAAgB"}
@@ -0,0 +1,17 @@
1
+ import type { FieldWithPath } from '@repo/common/utils';
2
+ import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, RelationshipField } from 'payload';
3
+ /**
4
+ * Cascades hard-deletes to all documents referenced by a relationship field.
5
+ */
6
+ export declare const createHardDeleteHook: ({ path, relationTo, }: FieldWithPath<RelationshipField>) => CollectionAfterDeleteHook;
7
+ /**
8
+ * Cascades soft-deletes (trash) to all documents referenced by a relationship field
9
+ * when the parent transitions from live to trashed.
10
+ */
11
+ export declare const createSoftDeleteHook: ({ path, relationTo, }: FieldWithPath<RelationshipField>) => CollectionAfterChangeHook;
12
+ /**
13
+ * Cascades restores to all documents referenced by a relationship field
14
+ * when the parent transitions from trashed to live.
15
+ */
16
+ export declare const createRestoreHook: ({ path, relationTo, }: FieldWithPath<RelationshipField>) => CollectionAfterChangeHook;
17
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EACV,yBAAyB,EACzB,yBAAyB,EAEzB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,eAAO,MAAM,oBAAoB,0BAI5B,aAAa,CAAC,iBAAiB,CAAC,KAAG,yBAWrC,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,oBAAoB,0BAI5B,aAAa,CAAC,iBAAiB,CAAC,KAAG,yBAiBrC,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,iBAAiB,0BAIzB,aAAa,CAAC,iBAAiB,CAAC,KAAG,yBAkBrC,CAAC"}
package/dist/hooks.js ADDED
@@ -0,0 +1,98 @@
1
+ import { resolveForeignKey } from '@repo/common';
2
+ /**
3
+ * Cascades hard-deletes to all documents referenced by a relationship field.
4
+ */ export const createHardDeleteHook = ({ path, relationTo })=>async ({ doc, req })=>{
5
+ const grouped = groupByCollection(getByPath(doc, path), relationTo);
6
+ for (const [collection, ids] of grouped){
7
+ await req.payload.delete({
8
+ collection: collection,
9
+ where: {
10
+ id: {
11
+ in: ids
12
+ }
13
+ },
14
+ req
15
+ });
16
+ }
17
+ };
18
+ /**
19
+ * Cascades soft-deletes (trash) to all documents referenced by a relationship field
20
+ * when the parent transitions from live to trashed.
21
+ */ export const createSoftDeleteHook = ({ path, relationTo })=>async ({ doc, previousDoc, req, operation })=>{
22
+ if (operation !== 'update') return doc;
23
+ if (previousDoc.deletedAt || !doc.deletedAt) return doc;
24
+ const grouped = groupByCollection(getByPath(doc, path), relationTo);
25
+ for (const [collection, ids] of grouped){
26
+ await req.payload.update({
27
+ collection: collection,
28
+ where: {
29
+ id: {
30
+ in: ids
31
+ }
32
+ },
33
+ data: {
34
+ deletedAt: new Date().toISOString()
35
+ },
36
+ req
37
+ });
38
+ }
39
+ return doc;
40
+ };
41
+ /**
42
+ * Cascades restores to all documents referenced by a relationship field
43
+ * when the parent transitions from trashed to live.
44
+ */ export const createRestoreHook = ({ path, relationTo })=>async ({ doc, previousDoc, req, operation })=>{
45
+ if (operation !== 'update') return doc;
46
+ if (!previousDoc.deletedAt || doc.deletedAt) return doc;
47
+ const grouped = groupByCollection(getByPath(doc, path), relationTo);
48
+ for (const [collection, ids] of grouped){
49
+ await req.payload.update({
50
+ collection: collection,
51
+ where: {
52
+ id: {
53
+ in: ids
54
+ }
55
+ },
56
+ data: {
57
+ deletedAt: null
58
+ },
59
+ req,
60
+ trash: true
61
+ });
62
+ }
63
+ return doc;
64
+ };
65
+ // MARK: Utilities
66
+ function getByPath(obj, path) {
67
+ return path.reduce((acc, key)=>acc?.[key], obj);
68
+ }
69
+ /**
70
+ * Groups relationship values by target collection slug.
71
+ * Handles both non-polymorphic (plain IDs / populated objects)
72
+ * and polymorphic (`{ relationTo, value }`) shapes.
73
+ */ function groupByCollection(value, relationTo) {
74
+ const entries = Array.isArray(value) ? value : [
75
+ value
76
+ ];
77
+ const isPolymorphic = Array.isArray(relationTo);
78
+ const grouped = new Map();
79
+ for (const entry of entries){
80
+ if (entry == null) continue;
81
+ const ref = entry;
82
+ const col = isPolymorphic ? ref.relationTo : relationTo;
83
+ const raw = isPolymorphic ? ref.value : entry;
84
+ if (!col || raw == null) continue;
85
+ const id = resolveForeignKey(raw);
86
+ const ids = grouped.get(col);
87
+ if (ids) {
88
+ ids.push(id);
89
+ } else {
90
+ grouped.set(col, [
91
+ id
92
+ ]);
93
+ }
94
+ }
95
+ return grouped;
96
+ }
97
+
98
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks.ts"],"sourcesContent":["import { resolveForeignKey } from '@repo/common';\nimport type { FieldWithPath } from '@repo/common/utils';\nimport type {\n CollectionAfterChangeHook,\n CollectionAfterDeleteHook,\n CollectionSlug,\n RelationshipField,\n} from 'payload';\n\n/**\n * Cascades hard-deletes to all documents referenced by a relationship field.\n */\nexport const createHardDeleteHook =\n ({\n path,\n relationTo,\n }: FieldWithPath<RelationshipField>): CollectionAfterDeleteHook =>\n async ({ doc, req }) => {\n const grouped = groupByCollection(getByPath(doc, path), relationTo);\n\n for (const [collection, ids] of grouped) {\n await req.payload.delete({\n collection: collection as CollectionSlug,\n where: { id: { in: ids } },\n req,\n });\n }\n };\n\n/**\n * Cascades soft-deletes (trash) to all documents referenced by a relationship field\n * when the parent transitions from live to trashed.\n */\nexport const createSoftDeleteHook =\n ({\n path,\n relationTo,\n }: FieldWithPath<RelationshipField>): CollectionAfterChangeHook =>\n async ({ doc, previousDoc, req, operation }) => {\n if (operation !== 'update') return doc;\n if (previousDoc.deletedAt || !doc.deletedAt) return doc;\n\n const grouped = groupByCollection(getByPath(doc, path), relationTo);\n\n for (const [collection, ids] of grouped) {\n await req.payload.update({\n collection: collection as CollectionSlug,\n where: { id: { in: ids } },\n data: { deletedAt: new Date().toISOString() },\n req,\n });\n }\n\n return doc;\n };\n\n/**\n * Cascades restores to all documents referenced by a relationship field\n * when the parent transitions from trashed to live.\n */\nexport const createRestoreHook =\n ({\n path,\n relationTo,\n }: FieldWithPath<RelationshipField>): CollectionAfterChangeHook =>\n async ({ doc, previousDoc, req, operation }) => {\n if (operation !== 'update') return doc;\n if (!previousDoc.deletedAt || doc.deletedAt) return doc;\n\n const grouped = groupByCollection(getByPath(doc, path), relationTo);\n\n for (const [collection, ids] of grouped) {\n await req.payload.update({\n collection: collection as CollectionSlug,\n where: { id: { in: ids } },\n data: { deletedAt: null },\n req,\n trash: true,\n });\n }\n\n return doc;\n };\n\n// MARK: Utilities\n\nfunction getByPath(obj: Record<string, unknown>, path: string[]) {\n return path.reduce<unknown>(\n (acc, key) => (acc as Record<string, unknown> | undefined)?.[key],\n obj,\n );\n}\n\n/**\n * Groups relationship values by target collection slug.\n * Handles both non-polymorphic (plain IDs / populated objects)\n * and polymorphic (`{ relationTo, value }`) shapes.\n */\nfunction groupByCollection(\n value: unknown,\n relationTo: CollectionSlug | CollectionSlug[],\n): Map<string, (string | number)[]> {\n const entries = Array.isArray(value) ? value : [value];\n const isPolymorphic = Array.isArray(relationTo);\n const grouped = new Map<string, (string | number)[]>();\n\n for (const entry of entries) {\n if (entry == null) continue;\n const ref = entry as Record<string, unknown>;\n const col = isPolymorphic ? (ref.relationTo as string) : relationTo;\n const raw = isPolymorphic ? ref.value : entry;\n if (!col || raw == null) continue;\n const id = resolveForeignKey(raw);\n const ids = grouped.get(col);\n if (ids) {\n ids.push(id);\n } else {\n grouped.set(col, [id]);\n }\n }\n\n return grouped;\n}\n"],"names":["resolveForeignKey","createHardDeleteHook","path","relationTo","doc","req","grouped","groupByCollection","getByPath","collection","ids","payload","delete","where","id","in","createSoftDeleteHook","previousDoc","operation","deletedAt","update","data","Date","toISOString","createRestoreHook","trash","obj","reduce","acc","key","value","entries","Array","isArray","isPolymorphic","Map","entry","ref","col","raw","get","push","set"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,eAAe;AASjD;;CAEC,GACD,OAAO,MAAMC,uBACX,CAAC,EACCC,IAAI,EACJC,UAAU,EACuB,GACnC,OAAO,EAAEC,GAAG,EAAEC,GAAG,EAAE;QACjB,MAAMC,UAAUC,kBAAkBC,UAAUJ,KAAKF,OAAOC;QAExD,KAAK,MAAM,CAACM,YAAYC,IAAI,IAAIJ,QAAS;YACvC,MAAMD,IAAIM,OAAO,CAACC,MAAM,CAAC;gBACvBH,YAAYA;gBACZI,OAAO;oBAAEC,IAAI;wBAAEC,IAAIL;oBAAI;gBAAE;gBACzBL;YACF;QACF;IACF,EAAE;AAEJ;;;CAGC,GACD,OAAO,MAAMW,uBACX,CAAC,EACCd,IAAI,EACJC,UAAU,EACuB,GACnC,OAAO,EAAEC,GAAG,EAAEa,WAAW,EAAEZ,GAAG,EAAEa,SAAS,EAAE;QACzC,IAAIA,cAAc,UAAU,OAAOd;QACnC,IAAIa,YAAYE,SAAS,IAAI,CAACf,IAAIe,SAAS,EAAE,OAAOf;QAEpD,MAAME,UAAUC,kBAAkBC,UAAUJ,KAAKF,OAAOC;QAExD,KAAK,MAAM,CAACM,YAAYC,IAAI,IAAIJ,QAAS;YACvC,MAAMD,IAAIM,OAAO,CAACS,MAAM,CAAC;gBACvBX,YAAYA;gBACZI,OAAO;oBAAEC,IAAI;wBAAEC,IAAIL;oBAAI;gBAAE;gBACzBW,MAAM;oBAAEF,WAAW,IAAIG,OAAOC,WAAW;gBAAG;gBAC5ClB;YACF;QACF;QAEA,OAAOD;IACT,EAAE;AAEJ;;;CAGC,GACD,OAAO,MAAMoB,oBACX,CAAC,EACCtB,IAAI,EACJC,UAAU,EACuB,GACnC,OAAO,EAAEC,GAAG,EAAEa,WAAW,EAAEZ,GAAG,EAAEa,SAAS,EAAE;QACzC,IAAIA,cAAc,UAAU,OAAOd;QACnC,IAAI,CAACa,YAAYE,SAAS,IAAIf,IAAIe,SAAS,EAAE,OAAOf;QAEpD,MAAME,UAAUC,kBAAkBC,UAAUJ,KAAKF,OAAOC;QAExD,KAAK,MAAM,CAACM,YAAYC,IAAI,IAAIJ,QAAS;YACvC,MAAMD,IAAIM,OAAO,CAACS,MAAM,CAAC;gBACvBX,YAAYA;gBACZI,OAAO;oBAAEC,IAAI;wBAAEC,IAAIL;oBAAI;gBAAE;gBACzBW,MAAM;oBAAEF,WAAW;gBAAK;gBACxBd;gBACAoB,OAAO;YACT;QACF;QAEA,OAAOrB;IACT,EAAE;AAEJ,kBAAkB;AAElB,SAASI,UAAUkB,GAA4B,EAAExB,IAAc;IAC7D,OAAOA,KAAKyB,MAAM,CAChB,CAACC,KAAKC,MAASD,KAA6C,CAACC,IAAI,EACjEH;AAEJ;AAEA;;;;CAIC,GACD,SAASnB,kBACPuB,KAAc,EACd3B,UAA6C;IAE7C,MAAM4B,UAAUC,MAAMC,OAAO,CAACH,SAASA,QAAQ;QAACA;KAAM;IACtD,MAAMI,gBAAgBF,MAAMC,OAAO,CAAC9B;IACpC,MAAMG,UAAU,IAAI6B;IAEpB,KAAK,MAAMC,SAASL,QAAS;QAC3B,IAAIK,SAAS,MAAM;QACnB,MAAMC,MAAMD;QACZ,MAAME,MAAMJ,gBAAiBG,IAAIlC,UAAU,GAAcA;QACzD,MAAMoC,MAAML,gBAAgBG,IAAIP,KAAK,GAAGM;QACxC,IAAI,CAACE,OAAOC,OAAO,MAAM;QACzB,MAAMzB,KAAKd,kBAAkBuC;QAC7B,MAAM7B,MAAMJ,QAAQkC,GAAG,CAACF;QACxB,IAAI5B,KAAK;YACPA,IAAI+B,IAAI,CAAC3B;QACX,OAAO;YACLR,QAAQoC,GAAG,CAACJ,KAAK;gBAACxB;aAAG;QACvB;IACF;IAEA,OAAOR;AACT"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hooks.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.test.d.ts","sourceRoot":"","sources":["../src/hooks.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,17 @@
1
+ import type { Plugin } from 'payload';
2
+ export interface SmartDeletionPluginOptions {
3
+ /**
4
+ * When true, auto-enables `trash: true` on target collections
5
+ * if the source collection uses trash and has a cascade field pointing to them.
6
+ * When false, throws a config error instead.
7
+ * @default true
8
+ */
9
+ autoEnableTrash?: boolean;
10
+ }
11
+ /**
12
+ * Scans all collections for relationship fields marked with
13
+ * `custom.smartDeletion: 'cascade'` and attaches afterDelete / afterChange
14
+ * hooks that cascade hard-deletes, soft-deletes, and restores.
15
+ */
16
+ export declare const smartDeletionPlugin: ({ autoEnableTrash }?: SmartDeletionPluginOptions) => Plugin;
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAS,MAAM,EAAqB,MAAM,SAAS,CAAC;AAQhE,MAAM,WAAW,0BAA0B;IACzC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,yBACD,0BAA0B,KAAQ,MAkD9D,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ import { findFields } from '@repo/common';
2
+ import { CUSTOM_KEY } from './const';
3
+ import { createHardDeleteHook, createRestoreHook, createSoftDeleteHook } from './hooks';
4
+ /**
5
+ * Scans all collections for relationship fields marked with
6
+ * `custom.smartDeletion: 'cascade'` and attaches afterDelete / afterChange
7
+ * hooks that cascade hard-deletes, soft-deletes, and restores.
8
+ */ export const smartDeletionPlugin = ({ autoEnableTrash = true } = {})=>(config)=>{
9
+ config.collections ??= [];
10
+ for (const collection of config.collections){
11
+ const cascadeFields = findFields(collection.fields, isCascadeRelationship);
12
+ if (cascadeFields.length === 0) continue;
13
+ collection.hooks ??= {};
14
+ collection.hooks.afterDelete ??= [];
15
+ collection.hooks.afterChange ??= [];
16
+ for (const field of cascadeFields){
17
+ const relationTo = field.relationTo;
18
+ // Ensure soft-delete cascade support
19
+ if (collection.trash) {
20
+ const targets = Array.isArray(relationTo) ? relationTo : [
21
+ relationTo
22
+ ];
23
+ for (const target of targets){
24
+ const targetCollection = config.collections.find(({ slug })=>slug === target);
25
+ if (!targetCollection) continue;
26
+ if (targetCollection.trash) continue;
27
+ if (autoEnableTrash) {
28
+ targetCollection.trash = true;
29
+ continue;
30
+ }
31
+ console.warn(`[payload-smart-deletion] Collection "${target}" does not have \`trash: true\`, ` + `but "${collection.slug}" does. Cascaded deletes from "${collection.slug}" ` + `will hard-delete "${target}" documents instead of trashing them. ` + `Enable \`trash: true\` on "${target}" or set \`autoEnableTrash: true\`.`);
32
+ }
33
+ }
34
+ collection.hooks.afterDelete.push(createHardDeleteHook(field));
35
+ if (collection.trash) {
36
+ collection.hooks.afterChange.push(createSoftDeleteHook(field));
37
+ collection.hooks.afterChange.push(createRestoreHook(field));
38
+ }
39
+ }
40
+ }
41
+ return config;
42
+ };
43
+ function isCascadeRelationship(field) {
44
+ if (field.type !== 'relationship') return false;
45
+ return field.custom?.[CUSTOM_KEY] === 'cascade';
46
+ }
47
+
48
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { findFields } from '@repo/common';\nimport type { Field, Plugin, RelationshipField } from 'payload';\nimport { CUSTOM_KEY } from './const';\nimport {\n createHardDeleteHook,\n createRestoreHook,\n createSoftDeleteHook,\n} from './hooks';\n\nexport interface SmartDeletionPluginOptions {\n /**\n * When true, auto-enables `trash: true` on target collections\n * if the source collection uses trash and has a cascade field pointing to them.\n * When false, throws a config error instead.\n * @default true\n */\n autoEnableTrash?: boolean;\n}\n\n/**\n * Scans all collections for relationship fields marked with\n * `custom.smartDeletion: 'cascade'` and attaches afterDelete / afterChange\n * hooks that cascade hard-deletes, soft-deletes, and restores.\n */\nexport const smartDeletionPlugin =\n ({ autoEnableTrash = true }: SmartDeletionPluginOptions = {}): Plugin =>\n (config) => {\n config.collections ??= [];\n\n for (const collection of config.collections) {\n const cascadeFields = findFields(\n collection.fields,\n isCascadeRelationship,\n );\n if (cascadeFields.length === 0) continue;\n\n collection.hooks ??= {};\n collection.hooks.afterDelete ??= [];\n collection.hooks.afterChange ??= [];\n\n for (const field of cascadeFields) {\n const relationTo = field.relationTo;\n\n // Ensure soft-delete cascade support\n if (collection.trash) {\n const targets = Array.isArray(relationTo) ? relationTo : [relationTo];\n for (const target of targets) {\n const targetCollection = config.collections.find(\n ({ slug }) => slug === target,\n );\n if (!targetCollection) continue;\n if (targetCollection.trash) continue;\n if (autoEnableTrash) {\n targetCollection.trash = true;\n continue;\n }\n console.warn(\n `[payload-smart-deletion] Collection \"${target}\" does not have \\`trash: true\\`, ` +\n `but \"${collection.slug}\" does. Cascaded deletes from \"${collection.slug}\" ` +\n `will hard-delete \"${target}\" documents instead of trashing them. ` +\n `Enable \\`trash: true\\` on \"${target}\" or set \\`autoEnableTrash: true\\`.`,\n );\n }\n }\n\n collection.hooks.afterDelete.push(createHardDeleteHook(field));\n\n if (collection.trash) {\n collection.hooks.afterChange.push(createSoftDeleteHook(field));\n collection.hooks.afterChange.push(createRestoreHook(field));\n }\n }\n }\n\n return config;\n };\n\nfunction isCascadeRelationship(field: Field): field is RelationshipField {\n if (field.type !== 'relationship') return false;\n return field.custom?.[CUSTOM_KEY] === 'cascade';\n}\n"],"names":["findFields","CUSTOM_KEY","createHardDeleteHook","createRestoreHook","createSoftDeleteHook","smartDeletionPlugin","autoEnableTrash","config","collections","collection","cascadeFields","fields","isCascadeRelationship","length","hooks","afterDelete","afterChange","field","relationTo","trash","targets","Array","isArray","target","targetCollection","find","slug","console","warn","push","type","custom"],"mappings":"AAAA,SAASA,UAAU,QAAQ,eAAe;AAE1C,SAASC,UAAU,QAAQ,UAAU;AACrC,SACEC,oBAAoB,EACpBC,iBAAiB,EACjBC,oBAAoB,QACf,UAAU;AAYjB;;;;CAIC,GACD,OAAO,MAAMC,sBACX,CAAC,EAAEC,kBAAkB,IAAI,EAA8B,GAAG,CAAC,CAAC,GAC5D,CAACC;QACCA,OAAOC,WAAW,KAAK,EAAE;QAEzB,KAAK,MAAMC,cAAcF,OAAOC,WAAW,CAAE;YAC3C,MAAME,gBAAgBV,WACpBS,WAAWE,MAAM,EACjBC;YAEF,IAAIF,cAAcG,MAAM,KAAK,GAAG;YAEhCJ,WAAWK,KAAK,KAAK,CAAC;YACtBL,WAAWK,KAAK,CAACC,WAAW,KAAK,EAAE;YACnCN,WAAWK,KAAK,CAACE,WAAW,KAAK,EAAE;YAEnC,KAAK,MAAMC,SAASP,cAAe;gBACjC,MAAMQ,aAAaD,MAAMC,UAAU;gBAEnC,qCAAqC;gBACrC,IAAIT,WAAWU,KAAK,EAAE;oBACpB,MAAMC,UAAUC,MAAMC,OAAO,CAACJ,cAAcA,aAAa;wBAACA;qBAAW;oBACrE,KAAK,MAAMK,UAAUH,QAAS;wBAC5B,MAAMI,mBAAmBjB,OAAOC,WAAW,CAACiB,IAAI,CAC9C,CAAC,EAAEC,IAAI,EAAE,GAAKA,SAASH;wBAEzB,IAAI,CAACC,kBAAkB;wBACvB,IAAIA,iBAAiBL,KAAK,EAAE;wBAC5B,IAAIb,iBAAiB;4BACnBkB,iBAAiBL,KAAK,GAAG;4BACzB;wBACF;wBACAQ,QAAQC,IAAI,CACV,CAAC,qCAAqC,EAAEL,OAAO,iCAAiC,CAAC,GAC/E,CAAC,KAAK,EAAEd,WAAWiB,IAAI,CAAC,+BAA+B,EAAEjB,WAAWiB,IAAI,CAAC,EAAE,CAAC,GAC5E,CAAC,kBAAkB,EAAEH,OAAO,sCAAsC,CAAC,GACnE,CAAC,2BAA2B,EAAEA,OAAO,mCAAmC,CAAC;oBAE/E;gBACF;gBAEAd,WAAWK,KAAK,CAACC,WAAW,CAACc,IAAI,CAAC3B,qBAAqBe;gBAEvD,IAAIR,WAAWU,KAAK,EAAE;oBACpBV,WAAWK,KAAK,CAACE,WAAW,CAACa,IAAI,CAACzB,qBAAqBa;oBACvDR,WAAWK,KAAK,CAACE,WAAW,CAACa,IAAI,CAAC1B,kBAAkBc;gBACtD;YACF;QACF;QAEA,OAAOV;IACT,EAAE;AAEJ,SAASK,sBAAsBK,KAAY;IACzC,IAAIA,MAAMa,IAAI,KAAK,gBAAgB,OAAO;IAC1C,OAAOb,MAAMc,MAAM,EAAE,CAAC9B,WAAW,KAAK;AACxC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,19 @@
1
+ import type { CollectionConfig, CollectionSlug, TypeWithID } from 'payload';
2
+ import z from 'zod';
3
+ export declare const entityIdSchema: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
4
+ export type EntityID = TypeWithID['id'] & z.infer<typeof entityIdSchema>;
5
+ export declare const isPopulated: <T extends TypeWithID>(relationship: T | EntityID) => relationship is T;
6
+ export declare function assertPopulated<T extends TypeWithID | null>(docsOrIds: (T | EntityID)[], errorMessage?: (id: EntityID) => string): T[];
7
+ export declare function assertPopulated<T extends TypeWithID | null>(docOrId: T | EntityID, errorMessage?: (id: EntityID) => string): T;
8
+ export declare const createCollectionConfigFactory: <T extends Record<string, unknown>>(factory: Omit<CollectionConfig, "slug"> | ((options: T & {
9
+ slug: CollectionSlug;
10
+ }) => Omit<CollectionConfig, "slug">)) => (options: T & {
11
+ slug: CollectionSlug;
12
+ }) => CollectionConfig;
13
+ export type EntityReference = TypeWithID['id'] | TypeWithID;
14
+ export declare function isEntityReference(value: unknown): value is EntityReference;
15
+ export declare const resolveForeignKey: (entity: EntityReference) => string | number;
16
+ export { defineProcedure, type Procedure, type ProcedureBuilder, } from './procedure';
17
+ export { getAdminURL, getApiURL, getServerURL } from './urls';
18
+ export { findFields, uncaughtSwitchCase } from './utils';
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/internals/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,CAAC,MAAM,KAAK,CAAC;AAIpB,eAAO,MAAM,cAAc,iDAAoC,CAAC;AAChE,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAIzE,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,UAAU,gBAChC,CAAC,GAAG,QAAQ,KACzB,YAAY,IAAI,CAAqC,CAAC;AAEzD,wBAAgB,eAAe,CAAC,CAAC,SAAS,UAAU,GAAG,IAAI,EACzD,SAAS,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAC,EAAE,EAC3B,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,QAAQ,KAAK,MAAM,GACtC,CAAC,EAAE,CAAC;AACP,wBAAgB,eAAe,CAAC,CAAC,SAAS,UAAU,GAAG,IAAI,EACzD,OAAO,EAAE,CAAC,GAAG,QAAQ,EACrB,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,QAAQ,KAAK,MAAM,GACtC,CAAC,CAAC;AAeL,eAAO,MAAM,6BAA6B,GACvC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WAE5B,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,GAC9B,CAAC,CACC,OAAO,EAAE,CAAC,GAAG;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,KAClC,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,eAEhC,CAAC,GAAG;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,KAAG,gBAGvC,CAAC;AAEL,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC;AAE5D,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAO1E;AAED,eAAO,MAAM,iBAAiB,WAAY,eAAe,oBACR,CAAC;AAElD,OAAO,EACL,eAAe,EACf,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,35 @@
1
+ import z from 'zod';
2
+ // MARK: Types
3
+ export const entityIdSchema = z.union([
4
+ z.number(),
5
+ z.string()
6
+ ]);
7
+ // MARK: Type Guards
8
+ export const isPopulated = (relationship)=>typeof relationship === 'object';
9
+ export function assertPopulated(value, errorMessage = (id)=>`Doc is not populated: [${id}]`) {
10
+ if (value === null) return value;
11
+ if (Array.isArray(value)) {
12
+ return value.map((item)=>assertPopulated(item, errorMessage));
13
+ }
14
+ if (isPopulated(value)) return value;
15
+ throw new Error(errorMessage(value));
16
+ }
17
+ // MARK: Utilities
18
+ export const createCollectionConfigFactory = (factory)=>(options)=>({
19
+ slug: options.slug,
20
+ ...typeof factory === 'function' ? factory(options) : factory
21
+ });
22
+ export function isEntityReference(value) {
23
+ if (typeof value === 'string') return true;
24
+ if (typeof value === 'number') return true;
25
+ if (value === null) return false;
26
+ if (typeof value !== 'object') return true;
27
+ if (!('id' in value)) return false;
28
+ return isEntityReference(value.id);
29
+ }
30
+ export const resolveForeignKey = (entity)=>typeof entity === 'object' ? entity.id : entity;
31
+ export { defineProcedure } from './procedure';
32
+ export { getAdminURL, getApiURL, getServerURL } from './urls';
33
+ export { findFields, uncaughtSwitchCase } from './utils';
34
+
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/internals/index.ts"],"sourcesContent":["import type { CollectionConfig, CollectionSlug, TypeWithID } from 'payload';\nimport z from 'zod';\n\n// MARK: Types\n\nexport const entityIdSchema = z.union([z.number(), z.string()]);\nexport type EntityID = TypeWithID['id'] & z.infer<typeof entityIdSchema>;\n\n// MARK: Type Guards\n\nexport const isPopulated = <T extends TypeWithID>(\n relationship: T | EntityID,\n): relationship is T => typeof relationship === 'object';\n\nexport function assertPopulated<T extends TypeWithID | null>(\n docsOrIds: (T | EntityID)[],\n errorMessage?: (id: EntityID) => string,\n): T[];\nexport function assertPopulated<T extends TypeWithID | null>(\n docOrId: T | EntityID,\n errorMessage?: (id: EntityID) => string,\n): T;\nexport function assertPopulated<T extends TypeWithID | null>(\n value: T | EntityID | (T | EntityID)[],\n errorMessage = (id: EntityID) => `Doc is not populated: [${id}]`,\n): T | T[] {\n if (value === null) return value;\n if (Array.isArray(value)) {\n return value.map((item) => assertPopulated(item, errorMessage));\n }\n if (isPopulated(value)) return value;\n throw new Error(errorMessage(value as EntityID));\n}\n\n// MARK: Utilities\n\nexport const createCollectionConfigFactory =\n <T extends Record<string, unknown>>(\n factory:\n | Omit<CollectionConfig, 'slug'>\n | ((\n options: T & { slug: CollectionSlug },\n ) => Omit<CollectionConfig, 'slug'>),\n ) =>\n (options: T & { slug: CollectionSlug }): CollectionConfig => ({\n slug: options.slug,\n ...(typeof factory === 'function' ? factory(options) : factory),\n });\n\nexport type EntityReference = TypeWithID['id'] | TypeWithID;\n\nexport function isEntityReference(value: unknown): value is EntityReference {\n if (typeof value === 'string') return true;\n if (typeof value === 'number') return true;\n if (value === null) return false;\n if (typeof value !== 'object') return true;\n if (!('id' in value)) return false;\n return isEntityReference(value.id);\n}\n\nexport const resolveForeignKey = (entity: EntityReference) =>\n typeof entity === 'object' ? entity.id : entity;\n\nexport {\n defineProcedure,\n type Procedure,\n type ProcedureBuilder,\n} from './procedure';\nexport { getAdminURL, getApiURL, getServerURL } from './urls';\nexport { findFields, uncaughtSwitchCase } from './utils';\n"],"names":["z","entityIdSchema","union","number","string","isPopulated","relationship","assertPopulated","value","errorMessage","id","Array","isArray","map","item","Error","createCollectionConfigFactory","factory","options","slug","isEntityReference","resolveForeignKey","entity","defineProcedure","getAdminURL","getApiURL","getServerURL","findFields","uncaughtSwitchCase"],"mappings":"AACA,OAAOA,OAAO,MAAM;AAEpB,cAAc;AAEd,OAAO,MAAMC,iBAAiBD,EAAEE,KAAK,CAAC;IAACF,EAAEG,MAAM;IAAIH,EAAEI,MAAM;CAAG,EAAE;AAGhE,oBAAoB;AAEpB,OAAO,MAAMC,cAAc,CACzBC,eACsB,OAAOA,iBAAiB,SAAS;AAUzD,OAAO,SAASC,gBACdC,KAAsC,EACtCC,eAAe,CAACC,KAAiB,CAAC,uBAAuB,EAAEA,GAAG,CAAC,CAAC;IAEhE,IAAIF,UAAU,MAAM,OAAOA;IAC3B,IAAIG,MAAMC,OAAO,CAACJ,QAAQ;QACxB,OAAOA,MAAMK,GAAG,CAAC,CAACC,OAASP,gBAAgBO,MAAML;IACnD;IACA,IAAIJ,YAAYG,QAAQ,OAAOA;IAC/B,MAAM,IAAIO,MAAMN,aAAaD;AAC/B;AAEA,kBAAkB;AAElB,OAAO,MAAMQ,gCACX,CACEC,UAMF,CAACC,UAA6D,CAAA;YAC5DC,MAAMD,QAAQC,IAAI;YAClB,GAAI,OAAOF,YAAY,aAAaA,QAAQC,WAAWD,OAAO;QAChE,CAAA,EAAG;AAIL,OAAO,SAASG,kBAAkBZ,KAAc;IAC9C,IAAI,OAAOA,UAAU,UAAU,OAAO;IACtC,IAAI,OAAOA,UAAU,UAAU,OAAO;IACtC,IAAIA,UAAU,MAAM,OAAO;IAC3B,IAAI,OAAOA,UAAU,UAAU,OAAO;IACtC,IAAI,CAAE,CAAA,QAAQA,KAAI,GAAI,OAAO;IAC7B,OAAOY,kBAAkBZ,MAAME,EAAE;AACnC;AAEA,OAAO,MAAMW,oBAAoB,CAACC,SAChC,OAAOA,WAAW,WAAWA,OAAOZ,EAAE,GAAGY,OAAO;AAElD,SACEC,eAAe,QAGV,cAAc;AACrB,SAASC,WAAW,EAAEC,SAAS,EAAEC,YAAY,QAAQ,SAAS;AAC9D,SAASC,UAAU,EAAEC,kBAAkB,QAAQ,UAAU"}
@@ -0,0 +1,34 @@
1
+ import type { Endpoint, PayloadRequest } from 'payload';
2
+ type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
3
+ /** Any Zod-like schema with safeParse and inferred output */
4
+ interface ZodLike<TOutput = unknown> {
5
+ safeParse(data: unknown): {
6
+ success: true;
7
+ data: TOutput;
8
+ } | {
9
+ success: false;
10
+ error: unknown;
11
+ };
12
+ }
13
+ type InferOutput<T> = T extends ZodLike<infer O> ? O : never;
14
+ interface ProcedureConfig<TSchema extends ZodLike | undefined = undefined> {
15
+ path: `/${string}`;
16
+ method: Method;
17
+ input?: TSchema;
18
+ }
19
+ export interface Procedure<TInput, TOutput> {
20
+ path: `/${string}`;
21
+ method: Method;
22
+ endpoint(handler: (req: PayloadRequest, ...args: TInput extends void ? [] : [input: TInput]) => Promise<unknown | Response>): Endpoint;
23
+ call(apiUrl: string, ...args: TInput extends void ? [] : [input: TInput]): Promise<TOutput>;
24
+ }
25
+ export interface ProcedureBuilder<TInput> {
26
+ path: string;
27
+ method: Method;
28
+ returns<TOutput>(): Procedure<TInput, TOutput>;
29
+ endpoint(handler: (req: PayloadRequest, ...args: TInput extends void ? [] : [input: TInput]) => Promise<unknown | Response>): Endpoint;
30
+ call(apiUrl: string, ...args: TInput extends void ? [] : [input: TInput]): Promise<unknown>;
31
+ }
32
+ export declare function defineProcedure<TSchema extends ZodLike | undefined = undefined>(config: ProcedureConfig<TSchema>): ProcedureBuilder<TSchema extends ZodLike ? InferOutput<TSchema> : void>;
33
+ export {};
34
+ //# sourceMappingURL=procedure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"procedure.d.ts","sourceRoot":"","sources":["../../src/internals/procedure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAExD,KAAK,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE1D,6DAA6D;AAC7D,UAAU,OAAO,CAAC,OAAO,GAAG,OAAO;IACjC,SAAS,CACP,IAAI,EAAE,OAAO,GACZ;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1E;AAED,KAAK,WAAW,CAAC,CAAC,IAAI,CAAC,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAE7D,UAAU,eAAe,CAAC,OAAO,SAAS,OAAO,GAAG,SAAS,GAAG,SAAS;IACvE,IAAI,EAAE,IAAI,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,SAAS,CAAC,MAAM,EAAE,OAAO;IACxC,IAAI,EAAE,IAAI,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CACN,OAAO,EAAE,CACP,GAAG,EAAE,cAAc,EACnB,GAAG,IAAI,EAAE,MAAM,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,KAChD,OAAO,CAAC,OAAO,GAAG,QAAQ,CAAC,GAC/B,QAAQ,CAAC;IACZ,IAAI,CACF,MAAM,EAAE,MAAM,EACd,GAAG,IAAI,EAAE,MAAM,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,GAClD,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB,CAAC,MAAM;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,OAAO,KAAK,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/C,QAAQ,CACN,OAAO,EAAE,CACP,GAAG,EAAE,cAAc,EACnB,GAAG,IAAI,EAAE,MAAM,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,KAChD,OAAO,CAAC,OAAO,GAAG,QAAQ,CAAC,GAC/B,QAAQ,CAAC;IACZ,IAAI,CACF,MAAM,EAAE,MAAM,EACd,GAAG,IAAI,EAAE,MAAM,SAAS,IAAI,GAAG,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,GAClD,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AA2GD,wBAAgB,eAAe,CAC7B,OAAO,SAAS,OAAO,GAAG,SAAS,GAAG,SAAS,EAE/C,MAAM,EAAE,eAAe,CAAC,OAAO,CAAC,GAC/B,gBAAgB,CAAC,OAAO,SAAS,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAazE"}
@@ -0,0 +1,110 @@
1
+ function wrapOutput(output) {
2
+ if (output instanceof Response) return output;
3
+ return Response.json(output);
4
+ }
5
+ function createProcedure(config, inputSchema) {
6
+ return {
7
+ path: config.path,
8
+ method: config.method,
9
+ endpoint (handler) {
10
+ return {
11
+ path: config.path,
12
+ method: config.method,
13
+ handler: async (req)=>{
14
+ if (inputSchema) {
15
+ if (config.method === 'get') {
16
+ const routeParams = req.routeParams ?? {};
17
+ const searchParams = req.searchParams ? Object.fromEntries(req.searchParams.entries()) : {};
18
+ const merged = {
19
+ ...searchParams,
20
+ ...routeParams
21
+ };
22
+ const result = inputSchema.safeParse(merged);
23
+ if (!result.success) {
24
+ return Response.json({
25
+ error: result.error
26
+ }, {
27
+ status: 400
28
+ });
29
+ }
30
+ // biome-ignore lint/complexity/noBannedTypes: ugly type cast
31
+ const output = await handler(req, result.data);
32
+ return wrapOutput(output);
33
+ }
34
+ const { addDataAndFileToRequest } = await import(/* webpackIgnore: true */ 'payload');
35
+ await addDataAndFileToRequest(req);
36
+ const result = inputSchema.safeParse(req.data);
37
+ if (!result.success) {
38
+ return Response.json({
39
+ error: result.error
40
+ }, {
41
+ status: 400
42
+ });
43
+ }
44
+ // biome-ignore lint/complexity/noBannedTypes: ugly type cast
45
+ const output = await handler(req, result.data);
46
+ return wrapOutput(output);
47
+ }
48
+ // biome-ignore lint/complexity/noBannedTypes: ugly type cast
49
+ const output = await handler(req);
50
+ return wrapOutput(output);
51
+ }
52
+ };
53
+ },
54
+ call (apiUrl, ...args) {
55
+ const input = args[0];
56
+ if (config.method === 'get') {
57
+ let resolvedPath = config.path;
58
+ const queryParams = {};
59
+ if (input) {
60
+ for (const [key, value] of Object.entries(input)){
61
+ if (resolvedPath.includes(`:${key}`)) {
62
+ resolvedPath = resolvedPath.replace(`:${key}`, encodeURIComponent(String(value)));
63
+ } else {
64
+ queryParams[key] = String(value);
65
+ }
66
+ }
67
+ }
68
+ const queryString = new URLSearchParams(queryParams).toString();
69
+ const url = `${apiUrl}${resolvedPath}${queryString ? `?${queryString}` : ''}`;
70
+ return fetch(url, {
71
+ method: 'GET',
72
+ credentials: 'include'
73
+ }).then(async (response)=>{
74
+ if (!response.ok) {
75
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
76
+ }
77
+ return response.json();
78
+ });
79
+ }
80
+ const url = `${apiUrl}${config.path}`;
81
+ return fetch(url, {
82
+ method: config.method.toUpperCase(),
83
+ credentials: 'include',
84
+ headers: {
85
+ 'Content-Type': 'application/json'
86
+ },
87
+ body: input ? JSON.stringify(input) : undefined
88
+ }).then(async (response)=>{
89
+ if (!response.ok) {
90
+ throw new Error(`Request failed: ${response.status} ${response.statusText}`);
91
+ }
92
+ return response.json();
93
+ });
94
+ }
95
+ };
96
+ }
97
+ export function defineProcedure(config) {
98
+ const proc = createProcedure(config, config.input);
99
+ return {
100
+ path: config.path,
101
+ method: config.method,
102
+ returns () {
103
+ return createProcedure(config, config.input);
104
+ },
105
+ endpoint: proc.endpoint,
106
+ call: proc.call
107
+ };
108
+ }
109
+
110
+ //# sourceMappingURL=procedure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/internals/procedure.ts"],"sourcesContent":["import type { Endpoint, PayloadRequest } from 'payload';\n\ntype Method = 'get' | 'post' | 'put' | 'patch' | 'delete';\n\n/** Any Zod-like schema with safeParse and inferred output */\ninterface ZodLike<TOutput = unknown> {\n safeParse(\n data: unknown,\n ): { success: true; data: TOutput } | { success: false; error: unknown };\n}\n\ntype InferOutput<T> = T extends ZodLike<infer O> ? O : never;\n\ninterface ProcedureConfig<TSchema extends ZodLike | undefined = undefined> {\n path: `/${string}`;\n method: Method;\n input?: TSchema;\n}\n\nexport interface Procedure<TInput, TOutput> {\n path: `/${string}`;\n method: Method;\n endpoint(\n handler: (\n req: PayloadRequest,\n ...args: TInput extends void ? [] : [input: TInput]\n ) => Promise<unknown | Response>,\n ): Endpoint;\n call(\n apiUrl: string,\n ...args: TInput extends void ? [] : [input: TInput]\n ): Promise<TOutput>;\n}\n\nexport interface ProcedureBuilder<TInput> {\n path: string;\n method: Method;\n returns<TOutput>(): Procedure<TInput, TOutput>;\n endpoint(\n handler: (\n req: PayloadRequest,\n ...args: TInput extends void ? [] : [input: TInput]\n ) => Promise<unknown | Response>,\n ): Endpoint;\n call(\n apiUrl: string,\n ...args: TInput extends void ? [] : [input: TInput]\n ): Promise<unknown>;\n}\n\nfunction wrapOutput(output: unknown): Response {\n if (output instanceof Response) return output;\n return Response.json(output);\n}\n\nfunction createProcedure<TInput, TOutput>(\n config: ProcedureConfig<ZodLike | undefined>,\n inputSchema: ZodLike | undefined,\n): Procedure<TInput, TOutput> {\n return {\n path: config.path,\n method: config.method,\n endpoint(handler) {\n return {\n path: config.path,\n method: config.method,\n handler: async (req) => {\n if (inputSchema) {\n if (config.method === 'get') {\n const routeParams = req.routeParams ?? {};\n const searchParams = req.searchParams\n ? Object.fromEntries(req.searchParams.entries())\n : {};\n const merged = { ...searchParams, ...routeParams };\n const result = inputSchema.safeParse(merged);\n if (!result.success) {\n return Response.json({ error: result.error }, { status: 400 });\n }\n // biome-ignore lint/complexity/noBannedTypes: ugly type cast\n const output = await (handler as Function)(req, result.data);\n return wrapOutput(output);\n }\n\n const { addDataAndFileToRequest } = await import(\n /* webpackIgnore: true */ 'payload'\n );\n await addDataAndFileToRequest(req);\n const result = inputSchema.safeParse(req.data);\n if (!result.success) {\n return Response.json({ error: result.error }, { status: 400 });\n }\n // biome-ignore lint/complexity/noBannedTypes: ugly type cast\n const output = await (handler as Function)(req, result.data);\n return wrapOutput(output);\n }\n // biome-ignore lint/complexity/noBannedTypes: ugly type cast\n const output = await (handler as Function)(req);\n return wrapOutput(output);\n },\n };\n },\n call(apiUrl, ...args) {\n const input = args[0] as Record<string, unknown> | undefined;\n\n if (config.method === 'get') {\n let resolvedPath = config.path;\n const queryParams: Record<string, string> = {};\n\n if (input) {\n for (const [key, value] of Object.entries(input)) {\n if (resolvedPath.includes(`:${key}`)) {\n resolvedPath = resolvedPath.replace(\n `:${key}`,\n encodeURIComponent(String(value)),\n ) as `/${string}`;\n } else {\n queryParams[key] = String(value);\n }\n }\n }\n\n const queryString = new URLSearchParams(queryParams).toString();\n const url = `${apiUrl}${resolvedPath}${queryString ? `?${queryString}` : ''}`;\n\n return fetch(url, {\n method: 'GET',\n credentials: 'include',\n }).then(async (response) => {\n if (!response.ok) {\n throw new Error(\n `Request failed: ${response.status} ${response.statusText}`,\n );\n }\n return response.json();\n }) as Promise<TOutput>;\n }\n\n const url = `${apiUrl}${config.path}`;\n return fetch(url, {\n method: config.method.toUpperCase(),\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: input ? JSON.stringify(input) : undefined,\n }).then(async (response) => {\n if (!response.ok) {\n throw new Error(\n `Request failed: ${response.status} ${response.statusText}`,\n );\n }\n return response.json();\n }) as Promise<TOutput>;\n },\n };\n}\n\nexport function defineProcedure<\n TSchema extends ZodLike | undefined = undefined,\n>(\n config: ProcedureConfig<TSchema>,\n): ProcedureBuilder<TSchema extends ZodLike ? InferOutput<TSchema> : void> {\n type TInput = TSchema extends ZodLike ? InferOutput<TSchema> : undefined;\n const proc = createProcedure<TInput, unknown>(config, config.input);\n\n return {\n path: config.path,\n method: config.method,\n returns<TOutput>(): Procedure<TInput, TOutput> {\n return createProcedure<TInput, TOutput>(config, config.input);\n },\n endpoint: proc.endpoint as unknown as ProcedureBuilder<TInput>['endpoint'],\n call: proc.call as ProcedureBuilder<TInput>['call'],\n };\n}\n"],"names":["wrapOutput","output","Response","json","createProcedure","config","inputSchema","path","method","endpoint","handler","req","routeParams","searchParams","Object","fromEntries","entries","merged","result","safeParse","success","error","status","data","addDataAndFileToRequest","call","apiUrl","args","input","resolvedPath","queryParams","key","value","includes","replace","encodeURIComponent","String","queryString","URLSearchParams","toString","url","fetch","credentials","then","response","ok","Error","statusText","toUpperCase","headers","body","JSON","stringify","undefined","defineProcedure","proc","returns"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ export declare function getServerURL(req: PayloadRequest): string;
3
+ export declare function getAdminURL({ req, path, }: {
4
+ req: PayloadRequest;
5
+ path?: '' | `/${string}` | null;
6
+ }): string;
7
+ export declare function getApiURL({ req, path, }: {
8
+ req: PayloadRequest;
9
+ path?: '' | `/${string}` | null;
10
+ }): string;
11
+ //# sourceMappingURL=urls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"urls.d.ts","sourceRoot":"","sources":["../../src/internals/urls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM,CAWxD;AAED,wBAAgB,WAAW,CAAC,EAC1B,GAAG,EACH,IAAI,GACL,EAAE;IACD,GAAG,EAAE,cAAc,CAAC;IACpB,IAAI,CAAC,EAAE,EAAE,GAAG,IAAI,MAAM,EAAE,GAAG,IAAI,CAAC;CACjC,GAAG,MAAM,CAMT;AAED,wBAAgB,SAAS,CAAC,EACxB,GAAG,EACH,IAAI,GACL,EAAE;IACD,GAAG,EAAE,cAAc,CAAC;IACpB,IAAI,CAAC,EAAE,EAAE,GAAG,IAAI,MAAM,EAAE,GAAG,IAAI,CAAC;CACjC,GAAG,MAAM,CAMT"}
@@ -0,0 +1,23 @@
1
+ import { formatAdminURL } from 'payload/shared';
2
+ export function getServerURL(req) {
3
+ if (!req.url) throw new Error('Could not get serverURL, since request URL is not available');
4
+ const { config } = req.payload;
5
+ if (config.serverURL) return config.serverURL;
6
+ return `${new URL(req.url).protocol}//${req.headers.get('host')}`;
7
+ }
8
+ export function getAdminURL({ req, path }) {
9
+ return formatAdminURL({
10
+ adminRoute: req.payload.config.routes.admin,
11
+ serverURL: getServerURL(req),
12
+ path
13
+ });
14
+ }
15
+ export function getApiURL({ req, path }) {
16
+ return formatAdminURL({
17
+ apiRoute: req.payload.config.routes.api,
18
+ serverURL: getServerURL(req),
19
+ path
20
+ });
21
+ }
22
+
23
+ //# sourceMappingURL=urls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/internals/urls.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload';\nimport { formatAdminURL } from 'payload/shared';\n\nexport function getServerURL(req: PayloadRequest): string {\n if (!req.url)\n throw new Error(\n 'Could not get serverURL, since request URL is not available',\n );\n\n const { config } = req.payload;\n\n if (config.serverURL) return config.serverURL;\n\n return `${new URL(req.url).protocol}//${req.headers.get('host')}`;\n}\n\nexport function getAdminURL({\n req,\n path,\n}: {\n req: PayloadRequest;\n path?: '' | `/${string}` | null;\n}): string {\n return formatAdminURL({\n adminRoute: req.payload.config.routes.admin,\n serverURL: getServerURL(req),\n path,\n });\n}\n\nexport function getApiURL({\n req,\n path,\n}: {\n req: PayloadRequest;\n path?: '' | `/${string}` | null;\n}): string {\n return formatAdminURL({\n apiRoute: req.payload.config.routes.api,\n serverURL: getServerURL(req),\n path,\n });\n}\n"],"names":["formatAdminURL","getServerURL","req","url","Error","config","payload","serverURL","URL","protocol","headers","get","getAdminURL","path","adminRoute","routes","admin","getApiURL","apiRoute","api"],"mappings":"AACA,SAASA,cAAc,QAAQ,iBAAiB;AAEhD,OAAO,SAASC,aAAaC,GAAmB;IAC9C,IAAI,CAACA,IAAIC,GAAG,EACV,MAAM,IAAIC,MACR;IAGJ,MAAM,EAAEC,MAAM,EAAE,GAAGH,IAAII,OAAO;IAE9B,IAAID,OAAOE,SAAS,EAAE,OAAOF,OAAOE,SAAS;IAE7C,OAAO,GAAG,IAAIC,IAAIN,IAAIC,GAAG,EAAEM,QAAQ,CAAC,EAAE,EAAEP,IAAIQ,OAAO,CAACC,GAAG,CAAC,SAAS;AACnE;AAEA,OAAO,SAASC,YAAY,EAC1BV,GAAG,EACHW,IAAI,EAIL;IACC,OAAOb,eAAe;QACpBc,YAAYZ,IAAII,OAAO,CAACD,MAAM,CAACU,MAAM,CAACC,KAAK;QAC3CT,WAAWN,aAAaC;QACxBW;IACF;AACF;AAEA,OAAO,SAASI,UAAU,EACxBf,GAAG,EACHW,IAAI,EAIL;IACC,OAAOb,eAAe;QACpBkB,UAAUhB,IAAII,OAAO,CAACD,MAAM,CAACU,MAAM,CAACI,GAAG;QACvCZ,WAAWN,aAAaC;QACxBW;IACF;AACF"}
@@ -0,0 +1,8 @@
1
+ import type { Field } from 'payload';
2
+ export declare const uncaughtSwitchCase: (value: never) => never;
3
+ export type FieldWithPath<T extends Field> = T & {
4
+ path: string[];
5
+ };
6
+ export declare function findFields<T extends Field>(fields: Field[], condition: (field: Field) => field is T, path?: string[]): FieldWithPath<T>[];
7
+ export declare function findFields(fields: Field[], condition: (field: Field) => boolean, path?: string[]): FieldWithPath<Field>[];
8
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/internals/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,eAAO,MAAM,kBAAkB,UAAW,KAAK,UAE9C,CAAC;AAEF,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,GAAG;IAC/C,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAAC;AAEF,wBAAgB,UAAU,CAAC,CAAC,SAAS,KAAK,EACxC,MAAM,EAAE,KAAK,EAAE,EACf,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,KAAK,IAAI,CAAC,EACvC,IAAI,CAAC,EAAE,MAAM,EAAE,GACd,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;AACtB,wBAAgB,UAAU,CACxB,MAAM,EAAE,KAAK,EAAE,EACf,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,EACpC,IAAI,CAAC,EAAE,MAAM,EAAE,GACd,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC"}
@@ -0,0 +1,57 @@
1
+ export const uncaughtSwitchCase = (value)=>{
2
+ throw new Error(`Unhandled switch case: ${value}`);
3
+ };
4
+ export function findFields(fields, condition, path = []) {
5
+ return fields.flatMap((field)=>{
6
+ if (condition(field)) {
7
+ return [
8
+ {
9
+ ...field,
10
+ path: 'name' in field ? [
11
+ ...path,
12
+ field.name
13
+ ] : path
14
+ }
15
+ ];
16
+ }
17
+ if ('fields' in field) {
18
+ return findFields(field.fields, condition, 'name' in field ? [
19
+ ...path,
20
+ field.name
21
+ ] : path);
22
+ }
23
+ switch(field.type){
24
+ case 'blocks':
25
+ return field.blocks.flatMap((block)=>findFields(block.fields, condition, [
26
+ ...path,
27
+ field.name
28
+ ]));
29
+ case 'tabs':
30
+ return field.tabs.flatMap((tab)=>findFields(tab.fields, condition, 'name' in tab ? [
31
+ ...path,
32
+ tab.name
33
+ ] : path));
34
+ case 'text':
35
+ case 'richText':
36
+ case 'number':
37
+ case 'checkbox':
38
+ case 'date':
39
+ case 'email':
40
+ case 'select':
41
+ case 'json':
42
+ case 'code':
43
+ case 'join':
44
+ case 'point':
45
+ case 'radio':
46
+ case 'textarea':
47
+ case 'ui':
48
+ case 'relationship':
49
+ case 'upload':
50
+ return [];
51
+ default:
52
+ return uncaughtSwitchCase(field);
53
+ }
54
+ });
55
+ }
56
+
57
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/internals/utils.ts"],"sourcesContent":["import type { Field } from 'payload';\n\nexport const uncaughtSwitchCase = (value: never) => {\n throw new Error(`Unhandled switch case: ${value}`);\n};\n\nexport type FieldWithPath<T extends Field> = T & {\n path: string[];\n};\n\nexport function findFields<T extends Field>(\n fields: Field[],\n condition: (field: Field) => field is T,\n path?: string[],\n): FieldWithPath<T>[];\nexport function findFields(\n fields: Field[],\n condition: (field: Field) => boolean,\n path?: string[],\n): FieldWithPath<Field>[];\nexport function findFields(\n fields: Field[],\n condition: (field: Field) => boolean,\n path: string[] = [],\n): FieldWithPath<Field>[] {\n return fields.flatMap((field) => {\n if (condition(field)) {\n return [\n { ...field, path: 'name' in field ? [...path, field.name] : path },\n ];\n }\n\n if ('fields' in field) {\n return findFields(\n field.fields,\n condition,\n 'name' in field ? [...path, field.name] : path,\n );\n }\n\n switch (field.type) {\n case 'blocks':\n return field.blocks.flatMap((block) =>\n findFields(block.fields, condition, [...path, field.name]),\n );\n case 'tabs':\n return field.tabs.flatMap((tab) =>\n findFields(\n tab.fields,\n condition,\n 'name' in tab ? [...path, tab.name] : path,\n ),\n );\n case 'text':\n case 'richText':\n case 'number':\n case 'checkbox':\n case 'date':\n case 'email':\n case 'select':\n case 'json':\n case 'code':\n case 'join':\n case 'point':\n case 'radio':\n case 'textarea':\n case 'ui':\n case 'relationship':\n case 'upload':\n return [];\n default:\n return uncaughtSwitchCase(field);\n }\n });\n}\n"],"names":["uncaughtSwitchCase","value","Error","findFields","fields","condition","path","flatMap","field","name","type","blocks","block","tabs","tab"],"mappings":"AAEA,OAAO,MAAMA,qBAAqB,CAACC;IACjC,MAAM,IAAIC,MAAM,CAAC,uBAAuB,EAAED,OAAO;AACnD,EAAE;AAgBF,OAAO,SAASE,WACdC,MAAe,EACfC,SAAoC,EACpCC,OAAiB,EAAE;IAEnB,OAAOF,OAAOG,OAAO,CAAC,CAACC;QACrB,IAAIH,UAAUG,QAAQ;YACpB,OAAO;gBACL;oBAAE,GAAGA,KAAK;oBAAEF,MAAM,UAAUE,QAAQ;2BAAIF;wBAAME,MAAMC,IAAI;qBAAC,GAAGH;gBAAK;aAClE;QACH;QAEA,IAAI,YAAYE,OAAO;YACrB,OAAOL,WACLK,MAAMJ,MAAM,EACZC,WACA,UAAUG,QAAQ;mBAAIF;gBAAME,MAAMC,IAAI;aAAC,GAAGH;QAE9C;QAEA,OAAQE,MAAME,IAAI;YAChB,KAAK;gBACH,OAAOF,MAAMG,MAAM,CAACJ,OAAO,CAAC,CAACK,QAC3BT,WAAWS,MAAMR,MAAM,EAAEC,WAAW;2BAAIC;wBAAME,MAAMC,IAAI;qBAAC;YAE7D,KAAK;gBACH,OAAOD,MAAMK,IAAI,CAACN,OAAO,CAAC,CAACO,MACzBX,WACEW,IAAIV,MAAM,EACVC,WACA,UAAUS,MAAM;2BAAIR;wBAAMQ,IAAIL,IAAI;qBAAC,GAAGH;YAG5C,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;YACL,KAAK;gBACH,OAAO,EAAE;YACX;gBACE,OAAON,mBAAmBQ;QAC9B;IACF;AACF"}
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "payload-smart-deletion",
3
+ "version": "0.0.0",
4
+ "description": "Payload CMS plugin that adds PostgreSQL-style referential actions (cascade, set_null, restrict) to relationship fields.",
5
+ "keywords": [
6
+ "payload"
7
+ ],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/davincicoding-org/payload-plugins.git",
11
+ "directory": "packages/smart-deletion"
12
+ },
13
+ "license": "MIT",
14
+ "author": "DAVINCI CODING GmbH",
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "default": "./dist/index.js"
21
+ }
22
+ },
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "scripts": {
29
+ "prebuild": "rm -f src/internals 2>/dev/null; pnpm typecheck",
30
+ "build": "plugin-build",
31
+ "clean": "rm -rf dist && rm -rf node_modules",
32
+ "dev": "plugin-build --watch",
33
+ "lint": "biome check .",
34
+ "lint:fix": "biome check --write .",
35
+ "test": "vitest run --passWithNoTests",
36
+ "test:watch": "vitest",
37
+ "typecheck": "tsc --noEmit"
38
+ },
39
+ "devDependencies": {
40
+ "@repo/common": "workspace:*",
41
+ "@types/node": "^22.5.4",
42
+ "payload": "catalog:payload-stack",
43
+ "typescript": "catalog:payload-stack",
44
+ "vite": "catalog:vite",
45
+ "vitest": "^3.1.2",
46
+ "zod": "catalog:"
47
+ },
48
+ "peerDependencies": {
49
+ "payload": ">=3.72.0"
50
+ },
51
+ "engines": {
52
+ "node": "^18.20.2 || >=20.9.0",
53
+ "pnpm": "^9 || ^10"
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ }
58
+ }