@govcraft/payload-cms-mcp 1.0.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/README.md +164 -0
- package/dist/config/index.d.ts +27 -0
- package/dist/config/index.js +31 -0
- package/dist/config/index.js.map +1 -0
- package/dist/controllers/mcp.controller.d.ts +7 -0
- package/dist/controllers/mcp.controller.js +328 -0
- package/dist/controllers/mcp.controller.js.map +1 -0
- package/dist/controllers/payload-mcp.controller.d.ts +4 -0
- package/dist/controllers/payload-mcp.controller.js +185 -0
- package/dist/controllers/payload-mcp.controller.js.map +1 -0
- package/dist/generate-tools.d.ts +1 -0
- package/dist/generate-tools.js +1417 -0
- package/dist/generate-tools.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/anthropic-mcp.d.ts +3 -0
- package/dist/mcp/anthropic-mcp.js +332 -0
- package/dist/mcp/anthropic-mcp.js.map +1 -0
- package/dist/mcp/generated/index.d.ts +44 -0
- package/dist/mcp/generated/index.js +45 -0
- package/dist/mcp/generated/index.js.map +1 -0
- package/dist/mcp/generated/payload-tools.d.ts +7 -0
- package/dist/mcp/generated/payload-tools.js +69 -0
- package/dist/mcp/generated/payload-tools.js.map +1 -0
- package/dist/mcp/generated/payload-tools.json +13024 -0
- package/dist/mcp/generated/tools/create.json +9138 -0
- package/dist/mcp/generated/tools/createAccess.json +10 -0
- package/dist/mcp/generated/tools/createAfterChangeHook.json +9 -0
- package/dist/mcp/generated/tools/createAfterDeleteHook.json +9 -0
- package/dist/mcp/generated/tools/createAfterErrorHook.json +9 -0
- package/dist/mcp/generated/tools/createAfterForgotPasswordHook.json +9 -0
- package/dist/mcp/generated/tools/createAfterLoginHook.json +9 -0
- package/dist/mcp/generated/tools/createAfterLogoutHook.json +9 -0
- package/dist/mcp/generated/tools/createAfterMeHook.json +9 -0
- package/dist/mcp/generated/tools/createAfterOperationHook.json +9 -0
- package/dist/mcp/generated/tools/createAfterReadHook.json +9 -0
- package/dist/mcp/generated/tools/createAfterRefreshHook.json +9 -0
- package/dist/mcp/generated/tools/createArrayField.json +90 -0
- package/dist/mcp/generated/tools/createBeforeChangeHook.json +9 -0
- package/dist/mcp/generated/tools/createBeforeDeleteHook.json +9 -0
- package/dist/mcp/generated/tools/createBeforeLoginHook.json +9 -0
- package/dist/mcp/generated/tools/createBeforeOperationHook.json +9 -0
- package/dist/mcp/generated/tools/createBeforeReadHook.json +9 -0
- package/dist/mcp/generated/tools/createBeforeValidateHook.json +9 -0
- package/dist/mcp/generated/tools/createBlocksField.json +79 -0
- package/dist/mcp/generated/tools/createCodeField.json +79 -0
- package/dist/mcp/generated/tools/createCollection.json +422 -0
- package/dist/mcp/generated/tools/createCollectionAdminOptions.json +2789 -0
- package/dist/mcp/generated/tools/createDateField.json +84 -0
- package/dist/mcp/generated/tools/createEmailField.json +87 -0
- package/dist/mcp/generated/tools/createField.json +31 -0
- package/dist/mcp/generated/tools/createGlobal.json +220 -0
- package/dist/mcp/generated/tools/createGroupField.json +79 -0
- package/dist/mcp/generated/tools/createJSONField.json +79 -0
- package/dist/mcp/generated/tools/createJoinField.json +79 -0
- package/dist/mcp/generated/tools/createMeHook.json +9 -0
- package/dist/mcp/generated/tools/createNumberField.json +31 -0
- package/dist/mcp/generated/tools/createPointField.json +79 -0
- package/dist/mcp/generated/tools/createPolymorphicRelationshipField.json +31 -0
- package/dist/mcp/generated/tools/createRadioField.json +94 -0
- package/dist/mcp/generated/tools/createRefreshHook.json +9 -0
- package/dist/mcp/generated/tools/createRelationshipField.json +31 -0
- package/dist/mcp/generated/tools/createRichTextField.json +79 -0
- package/dist/mcp/generated/tools/createSelectField.json +31 -0
- package/dist/mcp/generated/tools/createSingleRelationshipField.json +31 -0
- package/dist/mcp/generated/tools/createTextField.json +31 -0
- package/dist/mcp/generated/tools/createTextareaField.json +87 -0
- package/dist/mcp/generated/tools/createUploadField.json +31 -0
- package/dist/mcp/handler.d.ts +6 -0
- package/dist/mcp/handler.js +147 -0
- package/dist/mcp/handler.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +28 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/io/index.d.ts +2 -0
- package/dist/mcp/io/index.js +31 -0
- package/dist/mcp/io/index.js.map +1 -0
- package/dist/mcp/io/stdin.d.ts +2 -0
- package/dist/mcp/io/stdin.js +118 -0
- package/dist/mcp/io/stdin.js.map +1 -0
- package/dist/mcp/session.d.ts +16 -0
- package/dist/mcp/session.js +85 -0
- package/dist/mcp/session.js.map +1 -0
- package/dist/mcp/sse.d.ts +10 -0
- package/dist/mcp/sse.js +86 -0
- package/dist/mcp/sse.js.map +1 -0
- package/dist/mcp/tools/calculator.d.ts +2 -0
- package/dist/mcp/tools/calculator.js +68 -0
- package/dist/mcp/tools/calculator.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +3 -0
- package/dist/mcp/tools/index.js +6 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/list-tools.d.ts +2 -0
- package/dist/mcp/tools/list-tools.js +47 -0
- package/dist/mcp/tools/list-tools.js.map +1 -0
- package/dist/mcp/tools/payload-tools-loader.d.ts +2 -0
- package/dist/mcp/tools/payload-tools-loader.js +21 -0
- package/dist/mcp/tools/payload-tools-loader.js.map +1 -0
- package/dist/mcp/types.d.ts +69 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/middleware/errorHandler.d.ts +14 -0
- package/dist/middleware/errorHandler.js +25 -0
- package/dist/middleware/errorHandler.js.map +1 -0
- package/dist/middleware/notFoundHandler.d.ts +2 -0
- package/dist/middleware/notFoundHandler.js +8 -0
- package/dist/middleware/notFoundHandler.js.map +1 -0
- package/dist/routes/health.d.ts +2 -0
- package/dist/routes/health.js +18 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/mcp.d.ts +2 -0
- package/dist/routes/mcp.js +11 -0
- package/dist/routes/mcp.js.map +1 -0
- package/dist/routes/payload-mcp.routes.d.ts +3 -0
- package/dist/routes/payload-mcp.routes.js +8 -0
- package/dist/routes/payload-mcp.routes.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +35 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,1417 @@
|
|
|
1
|
+
import { Project } from 'ts-morph';
|
|
2
|
+
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { logger } from './utils/logger.js';
|
|
5
|
+
const OUTPUT_DIR = path.join(process.cwd(), 'src', 'mcp', 'generated');
|
|
6
|
+
if (!existsSync(OUTPUT_DIR))
|
|
7
|
+
mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
8
|
+
function extractJSDocDescription(node) {
|
|
9
|
+
const jsDocs = node.getJsDocs();
|
|
10
|
+
if (jsDocs.length === 0)
|
|
11
|
+
return undefined;
|
|
12
|
+
const combinedDescription = jsDocs.map(doc => {
|
|
13
|
+
const description = doc.getDescription().trim();
|
|
14
|
+
const tags = doc.getTags().map(tag => {
|
|
15
|
+
const tagName = tag.getTagName();
|
|
16
|
+
const comment = tag.getComment() || '';
|
|
17
|
+
return `@${tagName} ${comment}`.trim();
|
|
18
|
+
}).join('\n');
|
|
19
|
+
return [description, tags].filter(Boolean).join('\n');
|
|
20
|
+
}).join('\n\n');
|
|
21
|
+
return combinedDescription || undefined;
|
|
22
|
+
}
|
|
23
|
+
function extractPropertyJSDoc(prop) {
|
|
24
|
+
return extractJSDocDescription(prop);
|
|
25
|
+
}
|
|
26
|
+
function enhanceSchemaWithJSDocs(schema, _type, decl) {
|
|
27
|
+
if (!schema || schema.type !== 'object' || !schema.properties)
|
|
28
|
+
return schema;
|
|
29
|
+
if (decl) {
|
|
30
|
+
const typeDescription = extractJSDocDescription(decl);
|
|
31
|
+
if (typeDescription) {
|
|
32
|
+
schema.description = typeDescription;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (decl && 'getProperties' in decl) {
|
|
36
|
+
const properties = decl.getProperties();
|
|
37
|
+
for (const prop of properties) {
|
|
38
|
+
const propName = prop.getName();
|
|
39
|
+
if (schema.properties[propName]) {
|
|
40
|
+
const propJSDoc = extractPropertyJSDoc(prop);
|
|
41
|
+
if (propJSDoc) {
|
|
42
|
+
schema.properties[propName].description = propJSDoc;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return schema;
|
|
48
|
+
}
|
|
49
|
+
function cleanTypeName(typeName) {
|
|
50
|
+
let cleaned = typeName.replace(/import\(".*?\/node_modules\/payload\/dist\/(.*?)"\)\./, 'Payload.');
|
|
51
|
+
cleaned = cleaned.replace(/<.*?>/, '');
|
|
52
|
+
cleaned = cleaned.replace(/import\(".*?"\)\./g, '');
|
|
53
|
+
return cleaned;
|
|
54
|
+
}
|
|
55
|
+
function typeToJsonSchema(type, _decl) {
|
|
56
|
+
if (type.isString())
|
|
57
|
+
return { type: 'string' };
|
|
58
|
+
if (type.isNumber())
|
|
59
|
+
return { type: 'number' };
|
|
60
|
+
if (type.isBoolean())
|
|
61
|
+
return { type: 'boolean' };
|
|
62
|
+
if (type.isArray()) {
|
|
63
|
+
const elementType = type.getArrayElementType();
|
|
64
|
+
if (elementType && elementType.isUnion() && elementType.getUnionTypes().some(t => t.getProperties().some(p => p.getName() === 'type'))) {
|
|
65
|
+
return {
|
|
66
|
+
type: 'array',
|
|
67
|
+
items: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
name: { type: 'string', description: 'The name of the field' },
|
|
71
|
+
type: { type: 'string', description: 'The type of field' },
|
|
72
|
+
label: { type: 'string', description: 'The label shown in the admin UI' },
|
|
73
|
+
required: { type: 'boolean', description: 'Whether this field is required' },
|
|
74
|
+
admin: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
description: 'Admin panel configuration for this field',
|
|
77
|
+
properties: {
|
|
78
|
+
description: { type: 'string', description: 'Additional description shown in the admin UI' },
|
|
79
|
+
position: { type: 'string', enum: ['sidebar'], description: 'Position of the field in the admin UI' },
|
|
80
|
+
width: { type: 'string', description: 'Width of the field in the admin UI' },
|
|
81
|
+
style: { type: 'object', description: 'Custom CSS styles for the field' },
|
|
82
|
+
className: { type: 'string', description: 'CSS class name for the field' },
|
|
83
|
+
readOnly: { type: 'boolean', description: 'Whether the field is read-only in the admin UI' },
|
|
84
|
+
hidden: { type: 'boolean', description: 'Whether the field is hidden in the admin UI' },
|
|
85
|
+
condition: { type: 'string', description: 'Condition for showing/hiding the field' },
|
|
86
|
+
components: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
description: 'Custom components for the field',
|
|
89
|
+
properties: {
|
|
90
|
+
Field: { type: 'string', description: 'Custom field component' },
|
|
91
|
+
Cell: { type: 'string', description: 'Custom cell component for list views' },
|
|
92
|
+
Filter: { type: 'string', description: 'Custom filter component' }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
required: ['name', 'type']
|
|
99
|
+
},
|
|
100
|
+
description: 'Array of field configurations'
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (elementType && elementType.isObject() &&
|
|
104
|
+
elementType.getProperties().some(p => p.getName() === 'label') &&
|
|
105
|
+
elementType.getProperties().some(p => p.getName() === 'value')) {
|
|
106
|
+
return {
|
|
107
|
+
type: 'array',
|
|
108
|
+
items: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
label: { type: 'string', description: 'Display label for the option' },
|
|
112
|
+
value: {
|
|
113
|
+
oneOf: [
|
|
114
|
+
{ type: 'string', description: 'Value stored in the database' },
|
|
115
|
+
{ type: 'number', description: 'Numeric value stored in the database' }
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
required: ['label', 'value']
|
|
120
|
+
},
|
|
121
|
+
description: 'Array of options for select/radio fields'
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
type: 'array',
|
|
126
|
+
items: elementType ? typeToJsonSchema(elementType) : { type: 'object', description: 'Generic object' }
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (type.isObject()) {
|
|
130
|
+
const properties = {};
|
|
131
|
+
const required = [];
|
|
132
|
+
type.getProperties().forEach((prop) => {
|
|
133
|
+
const propName = prop.getName();
|
|
134
|
+
try {
|
|
135
|
+
const declarations = prop.getDeclarations();
|
|
136
|
+
const propType = declarations.length > 0 ? prop.getTypeAtLocation(declarations[0]) : prop.getType();
|
|
137
|
+
if (propType.getCallSignatures().length > 0) {
|
|
138
|
+
properties[propName] = {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: 'Function expression (e.g., "({ req }) => req.user.role === \'admin\'")'
|
|
141
|
+
};
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
let propDescription;
|
|
145
|
+
if (declarations.length > 0 && declarations[0].getKind() === 166) {
|
|
146
|
+
propDescription = extractPropertyJSDoc(declarations[0]);
|
|
147
|
+
}
|
|
148
|
+
properties[propName] = typeToJsonSchema(propType);
|
|
149
|
+
if (propDescription) {
|
|
150
|
+
properties[propName].description = propDescription;
|
|
151
|
+
}
|
|
152
|
+
if (!prop.isOptional())
|
|
153
|
+
required.push(propName);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
logger.warn(`Failed to process property ${propName} in type ${type.getText()}: ${error instanceof Error ? error.message : String(error)}`);
|
|
157
|
+
if (propName.includes('path') || propName.includes('url') || propName.includes('name') ||
|
|
158
|
+
propName.includes('label') || propName.includes('title') || propName.includes('description')) {
|
|
159
|
+
properties[propName] = { type: 'string', description: `${propName} string value` };
|
|
160
|
+
}
|
|
161
|
+
else if (propName.includes('count') || propName.includes('limit') || propName.includes('max') ||
|
|
162
|
+
propName.includes('min') || propName.includes('size') || propName.includes('length')) {
|
|
163
|
+
properties[propName] = { type: 'number', description: `${propName} numeric value` };
|
|
164
|
+
}
|
|
165
|
+
else if (propName.includes('enabled') || propName.includes('disabled') || propName.includes('required') ||
|
|
166
|
+
propName.includes('visible') || propName.includes('hidden') || propName.startsWith('is') ||
|
|
167
|
+
propName.startsWith('has') || propName.startsWith('can')) {
|
|
168
|
+
properties[propName] = { type: 'boolean', description: `${propName} boolean flag` };
|
|
169
|
+
}
|
|
170
|
+
else if (propName.includes('options') || propName.includes('items') || propName.includes('list')) {
|
|
171
|
+
properties[propName] = {
|
|
172
|
+
type: 'array',
|
|
173
|
+
items: { type: 'object' },
|
|
174
|
+
description: `${propName} array of items`
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
properties[propName] = {
|
|
179
|
+
type: 'object',
|
|
180
|
+
description: `${propName} configuration object`
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
type: 'object',
|
|
187
|
+
properties,
|
|
188
|
+
required: required.length ? required : undefined
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (type.isUnion()) {
|
|
192
|
+
const unionTypes = type.getUnionTypes();
|
|
193
|
+
const isFieldUnion = unionTypes.some(t => {
|
|
194
|
+
const props = t.getProperties();
|
|
195
|
+
return props.some(p => p.getName() === 'type') &&
|
|
196
|
+
props.some(p => p.getName() === 'name');
|
|
197
|
+
});
|
|
198
|
+
if (isFieldUnion) {
|
|
199
|
+
return {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
name: { type: 'string', description: 'The name of the field' },
|
|
203
|
+
type: { type: 'string', description: 'The type of field' },
|
|
204
|
+
label: { type: 'string', description: 'The label shown in the admin UI' },
|
|
205
|
+
required: { type: 'boolean', description: 'Whether this field is required' }
|
|
206
|
+
},
|
|
207
|
+
required: ['name', 'type'],
|
|
208
|
+
description: 'Field configuration'
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const stringLiterals = unionTypes.filter(t => t.isStringLiteral());
|
|
212
|
+
if (stringLiterals.length === unionTypes.length) {
|
|
213
|
+
return {
|
|
214
|
+
type: 'string',
|
|
215
|
+
enum: stringLiterals.map(t => t.getLiteralValueOrThrow()),
|
|
216
|
+
description: 'One of the allowed string values'
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const numberLiterals = unionTypes.filter(t => t.isNumberLiteral());
|
|
220
|
+
if (numberLiterals.length === unionTypes.length) {
|
|
221
|
+
return {
|
|
222
|
+
type: 'number',
|
|
223
|
+
enum: numberLiterals.map(t => t.getLiteralValueOrThrow()),
|
|
224
|
+
description: 'One of the allowed numeric values'
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const booleanTypes = unionTypes.filter(t => t.isBoolean());
|
|
228
|
+
if (booleanTypes.length > 0) {
|
|
229
|
+
return { type: 'boolean', description: 'Boolean value' };
|
|
230
|
+
}
|
|
231
|
+
const stringTypes = unionTypes.filter(t => t.isString());
|
|
232
|
+
const numberTypes = unionTypes.filter(t => t.isNumber());
|
|
233
|
+
if (stringTypes.length > 0 && numberTypes.length > 0 &&
|
|
234
|
+
stringTypes.length + numberTypes.length === unionTypes.length) {
|
|
235
|
+
return {
|
|
236
|
+
oneOf: [
|
|
237
|
+
{ type: 'string', description: 'String value' },
|
|
238
|
+
{ type: 'number', description: 'Numeric value' }
|
|
239
|
+
],
|
|
240
|
+
description: 'String or number value'
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const nonNullTypes = unionTypes.filter(t => !t.isNull() && !t.isUndefined());
|
|
244
|
+
if (nonNullTypes.length === 1) {
|
|
245
|
+
return typeToJsonSchema(nonNullTypes[0]);
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
oneOf: unionTypes
|
|
249
|
+
.filter(t => !t.isNull() && !t.isUndefined())
|
|
250
|
+
.map((t) => typeToJsonSchema(t)),
|
|
251
|
+
description: 'One of the allowed types'
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (type.isEnum()) {
|
|
255
|
+
const enumMembers = type.getSymbol()?.getDeclarations()?.[0]?.getType().getUnionTypes() || [];
|
|
256
|
+
const enumValues = enumMembers.map((t) => t.getLiteralValueOrThrow());
|
|
257
|
+
return {
|
|
258
|
+
type: 'string',
|
|
259
|
+
enum: enumValues,
|
|
260
|
+
description: 'One of the enum values'
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const typeName = type.getText();
|
|
264
|
+
const cleanedTypeName = cleanTypeName(typeName);
|
|
265
|
+
if (typeName.includes('string') || typeName.includes('String')) {
|
|
266
|
+
return { type: 'string', description: 'String value' };
|
|
267
|
+
}
|
|
268
|
+
else if (typeName.includes('number') || typeName.includes('Number') ||
|
|
269
|
+
typeName.includes('int') || typeName.includes('float')) {
|
|
270
|
+
return { type: 'number', description: 'Numeric value' };
|
|
271
|
+
}
|
|
272
|
+
else if (typeName.includes('boolean') || typeName.includes('Boolean')) {
|
|
273
|
+
return { type: 'boolean', description: 'Boolean value' };
|
|
274
|
+
}
|
|
275
|
+
else if (typeName.includes('[]') || typeName.includes('Array')) {
|
|
276
|
+
return { type: 'array', items: { type: 'object' }, description: 'Array of items' };
|
|
277
|
+
}
|
|
278
|
+
else if (typeName.includes('Record') || typeName.includes('Map') || typeName.includes('Object')) {
|
|
279
|
+
return { type: 'object', properties: {}, description: 'Object with properties' };
|
|
280
|
+
}
|
|
281
|
+
else if (typeName.includes('Function') || typeName.includes('Callback')) {
|
|
282
|
+
return { type: 'string', description: 'Function expression' };
|
|
283
|
+
}
|
|
284
|
+
else if (typeName.includes('Date')) {
|
|
285
|
+
return { type: 'string', format: 'date-time', description: 'Date string (ISO format)' };
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
type: 'object',
|
|
289
|
+
description: `${cleanedTypeName} configuration object`
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function generateToolName(name) {
|
|
293
|
+
return `create${name.endsWith('Field') || name.endsWith('Hook') ? name : name.replace(/Config$/, '')}`;
|
|
294
|
+
}
|
|
295
|
+
async function generatePayloadTools() {
|
|
296
|
+
logger.info('Generating Payload CMS tools...');
|
|
297
|
+
const project = new Project();
|
|
298
|
+
const sourceFiles = project.addSourceFilesAtPaths([
|
|
299
|
+
'node_modules/payload/dist/**/*.d.ts',
|
|
300
|
+
'node_modules/payload/types/**/*.d.ts',
|
|
301
|
+
'node_modules/payload/dist/types.d.ts',
|
|
302
|
+
'node_modules/payload/dist/index.d.ts',
|
|
303
|
+
]);
|
|
304
|
+
project.resolveSourceFileDependencies();
|
|
305
|
+
if (sourceFiles.length === 0) {
|
|
306
|
+
throw new Error('No Payload type definitions found. Run `pnpm install payload` first.');
|
|
307
|
+
}
|
|
308
|
+
logger.info(`Loaded ${sourceFiles.length} source files`);
|
|
309
|
+
const allInterfaces = sourceFiles.flatMap(file => file.getInterfaces());
|
|
310
|
+
const allTypeAliases = sourceFiles.flatMap(file => file.getTypeAliases());
|
|
311
|
+
const tools = [];
|
|
312
|
+
const targetNames = [
|
|
313
|
+
'CollectionConfig', 'GlobalConfig', 'Config', 'SanitizedConfig', 'PayloadConfig',
|
|
314
|
+
'Access', 'CollectionAdminOptions',
|
|
315
|
+
'BeforeChangeHook', 'CollectionBeforeChangeHook', 'BeforeDeleteHook', 'CollectionBeforeDeleteHook',
|
|
316
|
+
'BeforeLoginHook', 'CollectionBeforeLoginHook', 'BeforeOperationHook', 'CollectionBeforeOperationHook',
|
|
317
|
+
'BeforeReadHook', 'CollectionBeforeReadHook', 'BeforeValidateHook', 'CollectionBeforeValidateHook',
|
|
318
|
+
'AfterChangeHook', 'CollectionAfterChangeHook', 'AfterDeleteHook', 'CollectionAfterDeleteHook',
|
|
319
|
+
'AfterErrorHook', 'CollectionAfterErrorHook', 'AfterForgotPasswordHook', 'CollectionAfterForgotPasswordHook',
|
|
320
|
+
'AfterLoginHook', 'CollectionAfterLoginHook', 'AfterLogoutHook', 'CollectionAfterLogoutHook',
|
|
321
|
+
'AfterMeHook', 'CollectionAfterMeHook', 'AfterOperationHook', 'CollectionAfterOperationHook',
|
|
322
|
+
'AfterReadHook', 'CollectionAfterReadHook', 'AfterRefreshHook', 'CollectionAfterRefreshHook',
|
|
323
|
+
'MeHook', 'CollectionMeHook', 'RefreshHook', 'CollectionRefreshHook',
|
|
324
|
+
'Field', 'TextField', 'NumberField', 'DateField', 'EmailField', 'TextareaField',
|
|
325
|
+
'RelationshipField', 'PolymorphicRelationshipField', 'SingleRelationshipField',
|
|
326
|
+
'ArrayField', 'RichTextField', 'CodeField', 'JSONField', 'SelectField', 'RadioField',
|
|
327
|
+
'PointField', 'BlocksField', 'JoinField', 'UploadField', 'GroupField'
|
|
328
|
+
];
|
|
329
|
+
const processedNames = new Set();
|
|
330
|
+
const TOOLS_DIR = path.join(OUTPUT_DIR, 'tools');
|
|
331
|
+
if (!existsSync(TOOLS_DIR))
|
|
332
|
+
mkdirSync(TOOLS_DIR, { recursive: true });
|
|
333
|
+
for (const iface of allInterfaces) {
|
|
334
|
+
const name = iface.getName();
|
|
335
|
+
const baseName = name.replace('Sanitized', '');
|
|
336
|
+
if (targetNames.includes(name) || (name.startsWith('Sanitized') && targetNames.includes(baseName))) {
|
|
337
|
+
if (!processedNames.has(baseName)) {
|
|
338
|
+
logger.info(`Processing interface: ${name} from ${iface.getSourceFile().getFilePath()}`);
|
|
339
|
+
addTool(iface, name, tools);
|
|
340
|
+
processedNames.add(baseName);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
logger.debug(`Skipping duplicate interface: ${name} (base: ${baseName})`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
logger.debug(`Skipping interface: ${name}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const typeAlias of allTypeAliases) {
|
|
351
|
+
const name = typeAlias.getName();
|
|
352
|
+
const baseName = name.replace('Sanitized', '');
|
|
353
|
+
if (targetNames.includes(name) || (name.startsWith('Sanitized') && targetNames.includes(baseName))) {
|
|
354
|
+
if (!processedNames.has(baseName)) {
|
|
355
|
+
logger.info(`Processing type alias: ${name} from ${typeAlias.getSourceFile().getFilePath()}`);
|
|
356
|
+
addTool(typeAlias, name, tools);
|
|
357
|
+
processedNames.add(baseName);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
logger.debug(`Skipping duplicate type alias: ${name} (base: ${baseName})`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
logger.debug(`Skipping type alias: ${name}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const missingTargets = targetNames.filter(name => !processedNames.has(name) && !processedNames.has(name.replace('Sanitized', '')));
|
|
368
|
+
if (missingTargets.length > 0) {
|
|
369
|
+
logger.warn('Target names not found:', missingTargets);
|
|
370
|
+
}
|
|
371
|
+
logger.info('Generated tools:', tools.map(t => t.name));
|
|
372
|
+
for (const tool of tools) {
|
|
373
|
+
const toolFilePath = path.join(TOOLS_DIR, `${tool.name}.json`);
|
|
374
|
+
writeFileSync(toolFilePath, JSON.stringify(tool, null, 2));
|
|
375
|
+
logger.info(`Generated ${tool.name}.json`);
|
|
376
|
+
}
|
|
377
|
+
const jsContent = `
|
|
378
|
+
/**
|
|
379
|
+
* Auto-generated Payload CMS tools for MCP
|
|
380
|
+
* DO NOT EDIT DIRECTLY - Generated by generate-tools.ts
|
|
381
|
+
*/
|
|
382
|
+
import { logger } from '../../utils/logger.js';
|
|
383
|
+
import fs from 'fs';
|
|
384
|
+
import path from 'path';
|
|
385
|
+
import { fileURLToPath } from 'url';
|
|
386
|
+
|
|
387
|
+
// Get the directory name of the current module
|
|
388
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
389
|
+
const __dirname = path.dirname(__filename);
|
|
390
|
+
|
|
391
|
+
// Read all tool files from the tools directory
|
|
392
|
+
const toolsDir = path.join(__dirname, 'tools');
|
|
393
|
+
logger.info(\`Loading tools from directory: \${toolsDir}\`);
|
|
394
|
+
|
|
395
|
+
let payloadTools = [];
|
|
396
|
+
let toolsMap = {};
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
// Check if the directory exists
|
|
400
|
+
if (!fs.existsSync(toolsDir)) {
|
|
401
|
+
logger.error(\`Tools directory does not exist: \${toolsDir}\`);
|
|
402
|
+
throw new Error(\`Tools directory does not exist: \${toolsDir}\`);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// List all files in the directory
|
|
406
|
+
const allFiles = fs.readdirSync(toolsDir);
|
|
407
|
+
logger.info(\`Found \${allFiles.length} files in tools directory\`);
|
|
408
|
+
|
|
409
|
+
// Filter for JSON files
|
|
410
|
+
const toolFiles = allFiles.filter(file => file.endsWith('.json'));
|
|
411
|
+
logger.info(\`Found \${toolFiles.length} JSON files in tools directory\`);
|
|
412
|
+
|
|
413
|
+
// Load each tool file
|
|
414
|
+
payloadTools = toolFiles.map(file => {
|
|
415
|
+
try {
|
|
416
|
+
const filePath = path.join(toolsDir, file);
|
|
417
|
+
logger.info(\`Loading tool file: \${filePath}\`);
|
|
418
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
419
|
+
const tool = JSON.parse(fileContent);
|
|
420
|
+
return {
|
|
421
|
+
name: tool.name,
|
|
422
|
+
description: tool.description,
|
|
423
|
+
parameters: tool.inputSchema,
|
|
424
|
+
template: tool.template
|
|
425
|
+
};
|
|
426
|
+
} catch (error) {
|
|
427
|
+
logger.error(\`Error loading tool file \${file}:\`, error);
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
}).filter(Boolean); // Remove any null entries from failed loads
|
|
431
|
+
|
|
432
|
+
logger.info(\`Successfully loaded \${payloadTools.length} tools\`);
|
|
433
|
+
|
|
434
|
+
// For backward compatibility
|
|
435
|
+
toolsMap = Object.fromEntries(
|
|
436
|
+
payloadTools.map(tool => [tool.name, tool])
|
|
437
|
+
);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
logger.error(\`Error loading tools:\`, error);
|
|
440
|
+
// Provide empty arrays as fallbacks
|
|
441
|
+
payloadTools = [];
|
|
442
|
+
toolsMap = {};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export { payloadTools, toolsMap };
|
|
446
|
+
`;
|
|
447
|
+
writeFileSync(path.join(OUTPUT_DIR, 'payload-tools.js'), jsContent);
|
|
448
|
+
logger.info('Generated payload-tools.js');
|
|
449
|
+
const dtsContent = `
|
|
450
|
+
/**
|
|
451
|
+
* Auto-generated Payload CMS tools for MCP
|
|
452
|
+
*/
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Tool interface matching the SDK requirements
|
|
456
|
+
*/
|
|
457
|
+
export interface PayloadTool {
|
|
458
|
+
name: string;
|
|
459
|
+
description: string;
|
|
460
|
+
parameters: {
|
|
461
|
+
type: string;
|
|
462
|
+
properties: Record<string, unknown>;
|
|
463
|
+
required?: string[];
|
|
464
|
+
};
|
|
465
|
+
template: string;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export const payloadTools: PayloadTool[];
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Map of tool names to tool definitions
|
|
472
|
+
*/
|
|
473
|
+
export interface PayloadToolsMap {
|
|
474
|
+
tools: Record<string, PayloadTool>;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* For backward compatibility
|
|
479
|
+
*/
|
|
480
|
+
export const toolsMap: Record<string, PayloadTool>;
|
|
481
|
+
`;
|
|
482
|
+
writeFileSync(path.join(OUTPUT_DIR, 'payload-tools.d.ts'), dtsContent);
|
|
483
|
+
logger.info('Generated payload-tools.d.ts');
|
|
484
|
+
const indexTsContent = `
|
|
485
|
+
/**
|
|
486
|
+
* Auto-generated Payload CMS tools for MCP
|
|
487
|
+
* DO NOT EDIT DIRECTLY - Generated by generate-tools.ts
|
|
488
|
+
*/
|
|
489
|
+
|
|
490
|
+
// Export all tools
|
|
491
|
+
export * from './payload-tools.js';
|
|
492
|
+
|
|
493
|
+
// Export individual tools for direct imports
|
|
494
|
+
${tools.map(tool => `export { default as ${tool.name} } from './tools/${tool.name}.json' with { type: 'json' };`).join('\n')}
|
|
495
|
+
`;
|
|
496
|
+
writeFileSync(path.join(OUTPUT_DIR, 'index.ts'), indexTsContent);
|
|
497
|
+
logger.info('Generated index.ts');
|
|
498
|
+
}
|
|
499
|
+
function addTool(decl, name, toolsArray) {
|
|
500
|
+
const jsDocDescription = extractJSDocDescription(decl);
|
|
501
|
+
const schema = enhanceSchemaWithJSDocs(typeToJsonSchema(decl.getType(), decl), decl.getType(), decl);
|
|
502
|
+
let template = '';
|
|
503
|
+
const baseName = name.replace('Sanitized', '');
|
|
504
|
+
const toolName = generateToolName(baseName);
|
|
505
|
+
if (baseName === 'CollectionConfig') {
|
|
506
|
+
template = `// Collection configuration for Payload CMS
|
|
507
|
+
export const {slug} = {
|
|
508
|
+
slug: '{slug}', // URL-friendly identifier for this collection
|
|
509
|
+
admin: {
|
|
510
|
+
useAsTitle: 'title', // Field to use as the title in the admin UI
|
|
511
|
+
defaultColumns: ['title', 'createdAt'], // Default columns in the admin UI list view
|
|
512
|
+
},
|
|
513
|
+
// Define the fields for this collection
|
|
514
|
+
fields: [
|
|
515
|
+
{
|
|
516
|
+
name: 'title',
|
|
517
|
+
type: 'text',
|
|
518
|
+
required: true,
|
|
519
|
+
},
|
|
520
|
+
// Add more fields as needed
|
|
521
|
+
{fields}
|
|
522
|
+
],
|
|
523
|
+
// Optional: Add hooks, access control, etc.
|
|
524
|
+
...{rest}
|
|
525
|
+
};`;
|
|
526
|
+
}
|
|
527
|
+
else if (baseName === 'GlobalConfig') {
|
|
528
|
+
template = `// Global configuration for Payload CMS
|
|
529
|
+
export const {slug} = {
|
|
530
|
+
slug: '{slug}', // URL-friendly identifier for this global
|
|
531
|
+
admin: {
|
|
532
|
+
group: 'Settings', // Group in the admin UI
|
|
533
|
+
},
|
|
534
|
+
// Define the fields for this global
|
|
535
|
+
fields: [
|
|
536
|
+
// Add your fields here
|
|
537
|
+
{fields}
|
|
538
|
+
],
|
|
539
|
+
// Optional: Add hooks, access control, etc.
|
|
540
|
+
...{rest}
|
|
541
|
+
};`;
|
|
542
|
+
}
|
|
543
|
+
else if (baseName === 'PayloadConfig' || baseName === 'Config' || baseName === 'SanitizedConfig') {
|
|
544
|
+
template = `// Main Payload CMS configuration
|
|
545
|
+
export default {
|
|
546
|
+
admin: {
|
|
547
|
+
user: 'users', // Collection used for authentication
|
|
548
|
+
meta: {
|
|
549
|
+
titleSuffix: '- My Payload App', // Suffix for browser tab titles
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
collections: [
|
|
553
|
+
// Reference your collections here
|
|
554
|
+
],
|
|
555
|
+
globals: [
|
|
556
|
+
// Reference your globals here
|
|
557
|
+
],
|
|
558
|
+
// Optional: Add plugins, localization, etc.
|
|
559
|
+
...{rest}
|
|
560
|
+
};`;
|
|
561
|
+
}
|
|
562
|
+
else if (baseName === 'Field' || baseName.endsWith('Field')) {
|
|
563
|
+
const typeProp = 'getProperty' in decl ? decl.getProperty('type') : undefined;
|
|
564
|
+
const typeEnum = typeProp && typeProp.getType().isUnion()
|
|
565
|
+
? typeProp.getType().getUnionTypes().map((t) => t.isLiteral() ? t.getLiteralValueOrThrow() : null)
|
|
566
|
+
: baseName === 'Field' ? [
|
|
567
|
+
'text', 'number', 'date', 'email', 'textarea', 'relationship', 'array', 'richText',
|
|
568
|
+
'code', 'json', 'select', 'radio', 'point', 'blocks', 'join', 'upload', 'group'
|
|
569
|
+
] : [baseName.toLowerCase()];
|
|
570
|
+
const fieldType = baseName.replace('Field', '').toLowerCase();
|
|
571
|
+
if (fieldType === 'text' || fieldType === '') {
|
|
572
|
+
template = `{
|
|
573
|
+
name: '{name}', // Database field name
|
|
574
|
+
type: 'text',
|
|
575
|
+
label: 'Text Field', // Label shown in the admin UI
|
|
576
|
+
required: true,
|
|
577
|
+
minLength: 2,
|
|
578
|
+
maxLength: 100,
|
|
579
|
+
// Optional: Add custom validation
|
|
580
|
+
validate: (value, { siblingData }) => {
|
|
581
|
+
if (value && value.toLowerCase().includes('forbidden')) {
|
|
582
|
+
return 'This field cannot contain the word "forbidden"';
|
|
583
|
+
}
|
|
584
|
+
return true; // Return true if valid
|
|
585
|
+
},
|
|
586
|
+
// Optional: Admin UI configuration
|
|
587
|
+
admin: {
|
|
588
|
+
description: 'Enter text content here',
|
|
589
|
+
position: 'sidebar',
|
|
590
|
+
width: '50%',
|
|
591
|
+
readOnly: false,
|
|
592
|
+
hidden: false
|
|
593
|
+
}
|
|
594
|
+
}`;
|
|
595
|
+
}
|
|
596
|
+
else if (fieldType === 'email') {
|
|
597
|
+
template = `{
|
|
598
|
+
name: '{name}', // Database field name
|
|
599
|
+
type: 'email',
|
|
600
|
+
label: 'Email Field', // Label shown in the admin UI
|
|
601
|
+
required: true,
|
|
602
|
+
// Optional: Add custom validation beyond the built-in email validation
|
|
603
|
+
validate: (value) => {
|
|
604
|
+
if (value && !value.includes('@example.com')) {
|
|
605
|
+
return 'Only example.com email addresses are allowed';
|
|
606
|
+
}
|
|
607
|
+
return true; // Return true if valid
|
|
608
|
+
},
|
|
609
|
+
// Optional: Admin UI configuration
|
|
610
|
+
admin: {
|
|
611
|
+
description: 'Enter a valid email address',
|
|
612
|
+
position: 'sidebar'
|
|
613
|
+
}
|
|
614
|
+
}`;
|
|
615
|
+
}
|
|
616
|
+
else if (fieldType === 'number') {
|
|
617
|
+
template = `{
|
|
618
|
+
name: '{name}', // Database field name
|
|
619
|
+
type: 'number',
|
|
620
|
+
label: 'Number Field', // Label shown in the admin UI
|
|
621
|
+
required: true,
|
|
622
|
+
min: 0,
|
|
623
|
+
max: 100,
|
|
624
|
+
// Optional: Add custom validation
|
|
625
|
+
validate: (value) => {
|
|
626
|
+
if (value && value % 1 !== 0) {
|
|
627
|
+
return 'Please enter a whole number';
|
|
628
|
+
}
|
|
629
|
+
return true; // Return true if valid
|
|
630
|
+
},
|
|
631
|
+
// Optional: Admin UI configuration
|
|
632
|
+
admin: {
|
|
633
|
+
description: 'Enter a numeric value',
|
|
634
|
+
width: '25%'
|
|
635
|
+
}
|
|
636
|
+
}`;
|
|
637
|
+
}
|
|
638
|
+
else if (fieldType === 'date') {
|
|
639
|
+
template = `{
|
|
640
|
+
name: '{name}', // Database field name
|
|
641
|
+
type: 'date',
|
|
642
|
+
label: 'Date Field', // Label shown in the admin UI
|
|
643
|
+
required: true,
|
|
644
|
+
admin: {
|
|
645
|
+
date: {
|
|
646
|
+
pickerAppearance: 'dayAndTime', // 'dayOnly', 'timeOnly', or 'dayAndTime'
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
// Optional: Add custom validation
|
|
650
|
+
validate: (value) => {
|
|
651
|
+
const date = new Date(value);
|
|
652
|
+
if (date < new Date()) {
|
|
653
|
+
return 'Date must be in the future';
|
|
654
|
+
}
|
|
655
|
+
return true; // Return true if valid
|
|
656
|
+
},
|
|
657
|
+
}`;
|
|
658
|
+
}
|
|
659
|
+
else if (fieldType === 'textarea') {
|
|
660
|
+
template = `{
|
|
661
|
+
name: '{name}', // Database field name
|
|
662
|
+
type: 'textarea',
|
|
663
|
+
label: 'Textarea Field', // Label shown in the admin UI
|
|
664
|
+
required: true,
|
|
665
|
+
minLength: 2,
|
|
666
|
+
maxLength: 500,
|
|
667
|
+
// Optional: Admin UI configuration
|
|
668
|
+
admin: {
|
|
669
|
+
description: 'Enter longer text content here'
|
|
670
|
+
}
|
|
671
|
+
}`;
|
|
672
|
+
}
|
|
673
|
+
else if (fieldType === 'relationship' || fieldType === 'polymorphicrelationship' || fieldType === 'singlerelationship') {
|
|
674
|
+
template = `{
|
|
675
|
+
name: '{name}', // Database field name
|
|
676
|
+
type: 'relationship',
|
|
677
|
+
label: 'Relationship Field', // Label shown in the admin UI
|
|
678
|
+
required: true,
|
|
679
|
+
relationTo: 'collection-name', // Replace with your collection slug
|
|
680
|
+
hasMany: false, // Set to true for multiple relationships
|
|
681
|
+
// Optional: Admin UI configuration
|
|
682
|
+
admin: {
|
|
683
|
+
description: 'Select a related document'
|
|
684
|
+
}
|
|
685
|
+
}`;
|
|
686
|
+
}
|
|
687
|
+
else if (fieldType === 'richtext') {
|
|
688
|
+
template = `{
|
|
689
|
+
name: '{name}', // Database field name
|
|
690
|
+
type: 'richText',
|
|
691
|
+
label: 'Rich Text Field', // Label shown in the admin UI
|
|
692
|
+
required: true,
|
|
693
|
+
admin: {
|
|
694
|
+
elements: ['h2', 'h3', 'link', 'ol', 'ul', 'indent'],
|
|
695
|
+
leaves: ['bold', 'italic', 'underline'],
|
|
696
|
+
},
|
|
697
|
+
// Optional: Add custom validation
|
|
698
|
+
validate: (value) => {
|
|
699
|
+
if (value && JSON.stringify(value).length < 20) {
|
|
700
|
+
return 'Please enter more content';
|
|
701
|
+
}
|
|
702
|
+
return true; // Return true if valid
|
|
703
|
+
},
|
|
704
|
+
}`;
|
|
705
|
+
}
|
|
706
|
+
else if (fieldType === 'select') {
|
|
707
|
+
template = `{
|
|
708
|
+
name: '{name}', // Database field name
|
|
709
|
+
type: 'select',
|
|
710
|
+
label: 'Select Field', // Label shown in the admin UI
|
|
711
|
+
required: true,
|
|
712
|
+
options: [
|
|
713
|
+
{ label: 'Option 1', value: 'option1' },
|
|
714
|
+
{ label: 'Option 2', value: 'option2' },
|
|
715
|
+
{ label: 'Option 3', value: 'option3' }
|
|
716
|
+
],
|
|
717
|
+
// Optional: Admin UI configuration
|
|
718
|
+
admin: {
|
|
719
|
+
description: 'Select one option from the list'
|
|
720
|
+
}
|
|
721
|
+
}`;
|
|
722
|
+
}
|
|
723
|
+
else if (fieldType === 'radio') {
|
|
724
|
+
template = `{
|
|
725
|
+
name: '{name}', // Database field name
|
|
726
|
+
type: 'radio',
|
|
727
|
+
label: 'Radio Field', // Label shown in the admin UI
|
|
728
|
+
required: true,
|
|
729
|
+
options: [
|
|
730
|
+
{
|
|
731
|
+
label: 'Option One',
|
|
732
|
+
value: 'option-one',
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
label: 'Option Two',
|
|
736
|
+
value: 'option-two',
|
|
737
|
+
},
|
|
738
|
+
],
|
|
739
|
+
defaultValue: 'option-one',
|
|
740
|
+
// Optional: Add custom validation
|
|
741
|
+
validate: (value) => {
|
|
742
|
+
// Custom validation logic
|
|
743
|
+
return true; // Return true if valid
|
|
744
|
+
},
|
|
745
|
+
}`;
|
|
746
|
+
}
|
|
747
|
+
else if (fieldType === 'checkbox') {
|
|
748
|
+
template = `{
|
|
749
|
+
name: '{name}', // Database field name
|
|
750
|
+
type: 'checkbox',
|
|
751
|
+
label: 'Checkbox Field', // Label shown in the admin UI
|
|
752
|
+
defaultValue: false,
|
|
753
|
+
// Optional: Add custom validation
|
|
754
|
+
validate: (value, { siblingData }) => {
|
|
755
|
+
if (siblingData.requiresAgreement && value !== true) {
|
|
756
|
+
return 'You must agree to the terms';
|
|
757
|
+
}
|
|
758
|
+
return true; // Return true if valid
|
|
759
|
+
},
|
|
760
|
+
}`;
|
|
761
|
+
}
|
|
762
|
+
else if (fieldType === 'group') {
|
|
763
|
+
template = `{
|
|
764
|
+
name: '{name}', // Database field name
|
|
765
|
+
type: 'group',
|
|
766
|
+
label: 'Group Field', // Label shown in the admin UI
|
|
767
|
+
fields: [
|
|
768
|
+
{
|
|
769
|
+
name: 'groupedField1',
|
|
770
|
+
type: 'text',
|
|
771
|
+
required: true,
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: 'groupedField2',
|
|
775
|
+
type: 'number',
|
|
776
|
+
},
|
|
777
|
+
// Add more fields as needed
|
|
778
|
+
],
|
|
779
|
+
// Optional: Add custom validation for the entire group
|
|
780
|
+
validate: (value) => {
|
|
781
|
+
if (value && (!value.groupedField1 || !value.groupedField2)) {
|
|
782
|
+
return 'Both fields in the group are required';
|
|
783
|
+
}
|
|
784
|
+
return true; // Return true if valid
|
|
785
|
+
},
|
|
786
|
+
}`;
|
|
787
|
+
}
|
|
788
|
+
else if (fieldType === 'blocks') {
|
|
789
|
+
template = `{
|
|
790
|
+
name: '{name}', // Database field name
|
|
791
|
+
type: 'blocks',
|
|
792
|
+
label: 'Blocks Field', // Label shown in the admin UI
|
|
793
|
+
minRows: 1,
|
|
794
|
+
maxRows: 10,
|
|
795
|
+
blocks: [
|
|
796
|
+
{
|
|
797
|
+
slug: 'textBlock',
|
|
798
|
+
fields: [
|
|
799
|
+
{
|
|
800
|
+
name: 'text',
|
|
801
|
+
type: 'richText',
|
|
802
|
+
required: true,
|
|
803
|
+
},
|
|
804
|
+
],
|
|
805
|
+
},
|
|
806
|
+
{
|
|
807
|
+
slug: 'imageBlock',
|
|
808
|
+
fields: [
|
|
809
|
+
{
|
|
810
|
+
name: 'image',
|
|
811
|
+
type: 'upload',
|
|
812
|
+
relationTo: 'media',
|
|
813
|
+
required: true,
|
|
814
|
+
},
|
|
815
|
+
],
|
|
816
|
+
},
|
|
817
|
+
// Add more block types as needed
|
|
818
|
+
],
|
|
819
|
+
// Optional: Add custom validation
|
|
820
|
+
validate: (value) => {
|
|
821
|
+
if (value && value.length > 0) {
|
|
822
|
+
// Validate the blocks as a whole
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
return 'Please add at least one block';
|
|
826
|
+
},
|
|
827
|
+
}`;
|
|
828
|
+
}
|
|
829
|
+
else if (fieldType === 'upload') {
|
|
830
|
+
template = `{
|
|
831
|
+
name: '{name}', // Database field name
|
|
832
|
+
type: 'upload',
|
|
833
|
+
label: 'Upload Field', // Label shown in the admin UI
|
|
834
|
+
relationTo: 'media', // The collection to upload to
|
|
835
|
+
required: true,
|
|
836
|
+
// Optional: Add custom validation
|
|
837
|
+
validate: (value) => {
|
|
838
|
+
if (!value) {
|
|
839
|
+
return 'Please upload a file';
|
|
840
|
+
}
|
|
841
|
+
return true; // Return true if valid
|
|
842
|
+
},
|
|
843
|
+
}`;
|
|
844
|
+
}
|
|
845
|
+
else if (fieldType === 'code') {
|
|
846
|
+
template = `{
|
|
847
|
+
name: '{name}', // Database field name
|
|
848
|
+
type: 'code',
|
|
849
|
+
label: 'Code Field', // Label shown in the admin UI
|
|
850
|
+
admin: {
|
|
851
|
+
language: 'javascript', // The language for syntax highlighting
|
|
852
|
+
},
|
|
853
|
+
// Optional: Add custom validation
|
|
854
|
+
validate: (value) => {
|
|
855
|
+
if (value && value.length < 10) {
|
|
856
|
+
return 'Please enter more code';
|
|
857
|
+
}
|
|
858
|
+
return true; // Return true if valid
|
|
859
|
+
},
|
|
860
|
+
}`;
|
|
861
|
+
}
|
|
862
|
+
else if (fieldType === 'json') {
|
|
863
|
+
template = `{
|
|
864
|
+
name: '{name}', // Database field name
|
|
865
|
+
type: 'json',
|
|
866
|
+
label: 'JSON Field', // Label shown in the admin UI
|
|
867
|
+
// Optional: Add custom validation
|
|
868
|
+
validate: (value) => {
|
|
869
|
+
try {
|
|
870
|
+
if (typeof value === 'string') {
|
|
871
|
+
JSON.parse(value);
|
|
872
|
+
}
|
|
873
|
+
return true; // Return true if valid
|
|
874
|
+
} catch (err) {
|
|
875
|
+
return 'Invalid JSON format';
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
}`;
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
template = `{
|
|
882
|
+
name: '{name}', // Database field name
|
|
883
|
+
type: '${fieldType || '{type}'}',
|
|
884
|
+
label: '${fieldType ? fieldType.charAt(0).toUpperCase() + fieldType.slice(1) : 'Custom'} Field', // Label shown in the admin UI
|
|
885
|
+
required: true,
|
|
886
|
+
// Add field-specific properties here
|
|
887
|
+
|
|
888
|
+
// Optional: Add custom validation
|
|
889
|
+
validate: (value, { siblingData, operation }) => {
|
|
890
|
+
// Custom validation logic based on the value, sibling data, or operation
|
|
891
|
+
if (!value && operation === 'create') {
|
|
892
|
+
return 'This field is required for new records';
|
|
893
|
+
}
|
|
894
|
+
return true; // Return true if valid
|
|
895
|
+
},
|
|
896
|
+
...{rest}
|
|
897
|
+
}`;
|
|
898
|
+
}
|
|
899
|
+
if (schema.type === 'any' || !schema.properties) {
|
|
900
|
+
schema.type = 'object';
|
|
901
|
+
schema.properties = {
|
|
902
|
+
name: { type: 'string', description: 'The name of the field, used as the property name in the database' },
|
|
903
|
+
type: {
|
|
904
|
+
type: 'string',
|
|
905
|
+
enum: baseName === 'Field' ?
|
|
906
|
+
['text', 'number', 'email', 'textarea', 'date', 'checkbox', 'select', 'radio', 'relationship', 'array', 'richText', 'code', 'json', 'point', 'blocks', 'group', 'upload'] :
|
|
907
|
+
[baseName.replace('Field', '').toLowerCase()],
|
|
908
|
+
description: 'The type of field'
|
|
909
|
+
},
|
|
910
|
+
label: { type: 'string', description: 'The label shown in the admin UI' },
|
|
911
|
+
required: { type: 'boolean', description: 'Whether this field is required' },
|
|
912
|
+
admin: {
|
|
913
|
+
type: 'object',
|
|
914
|
+
description: 'Admin panel configuration for this field',
|
|
915
|
+
properties: {
|
|
916
|
+
description: { type: 'string', description: 'Additional description shown in the admin UI' },
|
|
917
|
+
position: { type: 'string', enum: ['sidebar'], description: 'Position of the field in the admin UI' },
|
|
918
|
+
width: { type: 'string', description: 'Width of the field in the admin UI' },
|
|
919
|
+
style: { type: 'object', description: 'Custom CSS styles for the field' },
|
|
920
|
+
className: { type: 'string', description: 'CSS class name for the field' },
|
|
921
|
+
readOnly: { type: 'boolean', description: 'Whether the field is read-only in the admin UI' },
|
|
922
|
+
hidden: { type: 'boolean', description: 'Whether the field is hidden in the admin UI' },
|
|
923
|
+
condition: { type: 'string', description: 'Condition for showing/hiding the field' }
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
if (baseName === 'TextField' || baseName === 'EmailField' || baseName === 'TextareaField') {
|
|
928
|
+
schema.properties.minLength = { type: 'number', description: 'Minimum length of the text' };
|
|
929
|
+
schema.properties.maxLength = { type: 'number', description: 'Maximum length of the text' };
|
|
930
|
+
schema.properties.validate = {
|
|
931
|
+
type: 'string',
|
|
932
|
+
description: 'Custom validation function that returns true if valid or an error message string if invalid'
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
else if (baseName === 'NumberField') {
|
|
936
|
+
schema.properties.min = { type: 'number', description: 'Minimum value' };
|
|
937
|
+
schema.properties.max = { type: 'number', description: 'Maximum value' };
|
|
938
|
+
schema.properties.validate = {
|
|
939
|
+
type: 'string',
|
|
940
|
+
description: 'Custom validation function that returns true if valid or an error message string if invalid'
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
else if (baseName === 'DateField') {
|
|
944
|
+
schema.properties.defaultValue = { type: 'string', description: 'Default date value' };
|
|
945
|
+
schema.properties.validate = {
|
|
946
|
+
type: 'string',
|
|
947
|
+
description: 'Custom validation function that returns true if valid or an error message string if invalid'
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
else if (baseName === 'RelationshipField' || baseName === 'PolymorphicRelationshipField') {
|
|
951
|
+
schema.properties.relationTo = {
|
|
952
|
+
oneOf: [
|
|
953
|
+
{ type: 'string', description: 'Collection to relate to' },
|
|
954
|
+
{ type: 'array', items: { type: 'string' }, description: 'Collections to relate to' }
|
|
955
|
+
]
|
|
956
|
+
};
|
|
957
|
+
schema.properties.hasMany = { type: 'boolean', description: 'Whether this field can relate to multiple documents' };
|
|
958
|
+
schema.properties.validate = {
|
|
959
|
+
type: 'string',
|
|
960
|
+
description: 'Custom validation function that returns true if valid or an error message string if invalid'
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
else if (baseName === 'ArrayField') {
|
|
964
|
+
schema.properties.minRows = { type: 'number', description: 'Minimum number of rows' };
|
|
965
|
+
schema.properties.maxRows = { type: 'number', description: 'Maximum number of rows' };
|
|
966
|
+
schema.properties.validate = {
|
|
967
|
+
type: 'string',
|
|
968
|
+
description: 'Custom validation function that returns true if valid or an error message string if invalid'
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
else if (baseName === 'SelectField' || baseName === 'RadioField') {
|
|
972
|
+
schema.properties.options = {
|
|
973
|
+
type: 'array',
|
|
974
|
+
items: {
|
|
975
|
+
type: 'object',
|
|
976
|
+
properties: {
|
|
977
|
+
label: { type: 'string' },
|
|
978
|
+
value: { type: 'string' }
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
description: 'Options for the select/radio field'
|
|
982
|
+
};
|
|
983
|
+
schema.properties.validate = {
|
|
984
|
+
type: 'string',
|
|
985
|
+
description: 'Custom validation function that returns true if valid or an error message string if invalid'
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
schema.properties.validate = {
|
|
990
|
+
type: 'string',
|
|
991
|
+
description: 'Custom validation function that returns true if valid or an error message string if invalid'
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
schema.required = ['name', 'type'];
|
|
995
|
+
}
|
|
996
|
+
else if (typeProp) {
|
|
997
|
+
schema.properties.type = {
|
|
998
|
+
type: 'string',
|
|
999
|
+
enum: typeEnum.filter(Boolean),
|
|
1000
|
+
description: 'The type of field'
|
|
1001
|
+
};
|
|
1002
|
+
if (!schema.properties.admin) {
|
|
1003
|
+
schema.properties.admin = {
|
|
1004
|
+
type: 'object',
|
|
1005
|
+
description: 'Admin panel configuration for this field',
|
|
1006
|
+
properties: {
|
|
1007
|
+
description: { type: 'string', description: 'Additional description shown in the admin UI' },
|
|
1008
|
+
position: { type: 'string', enum: ['sidebar'], description: 'Position of the field in the admin UI' },
|
|
1009
|
+
width: { type: 'string', description: 'Width of the field in the admin UI' },
|
|
1010
|
+
style: { type: 'object', description: 'Custom CSS styles for the field' },
|
|
1011
|
+
className: { type: 'string', description: 'CSS class name for the field' },
|
|
1012
|
+
readOnly: { type: 'boolean', description: 'Whether the field is read-only in the admin UI' },
|
|
1013
|
+
hidden: { type: 'boolean', description: 'Whether the field is hidden in the admin UI' },
|
|
1014
|
+
condition: { type: 'string', description: 'Condition for showing/hiding the field' }
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
schema.required = schema.required?.includes('type') ? ['name', 'type'] : ['name'];
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
let description = `Creates a Payload CMS 3.0 ${baseName} configuration`;
|
|
1022
|
+
if (jsDocDescription) {
|
|
1023
|
+
description = `${description}\n\n${jsDocDescription}`;
|
|
1024
|
+
}
|
|
1025
|
+
let examples = '';
|
|
1026
|
+
if (baseName === 'CollectionConfig') {
|
|
1027
|
+
examples = `
|
|
1028
|
+
Example:
|
|
1029
|
+
\`\`\`typescript
|
|
1030
|
+
// Collection for a basic blog post
|
|
1031
|
+
export const Posts = {
|
|
1032
|
+
slug: 'posts',
|
|
1033
|
+
admin: {
|
|
1034
|
+
useAsTitle: 'title',
|
|
1035
|
+
},
|
|
1036
|
+
fields: [
|
|
1037
|
+
{
|
|
1038
|
+
name: 'title',
|
|
1039
|
+
type: 'text',
|
|
1040
|
+
required: true,
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
name: 'content',
|
|
1044
|
+
type: 'richText',
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
name: 'author',
|
|
1048
|
+
type: 'relationship',
|
|
1049
|
+
relationTo: 'users',
|
|
1050
|
+
},
|
|
1051
|
+
],
|
|
1052
|
+
}
|
|
1053
|
+
\`\`\``;
|
|
1054
|
+
}
|
|
1055
|
+
else if (baseName.endsWith('Field')) {
|
|
1056
|
+
const fieldType = baseName.replace('Field', '').toLowerCase();
|
|
1057
|
+
if (fieldType === 'text') {
|
|
1058
|
+
examples = `
|
|
1059
|
+
Example:
|
|
1060
|
+
\`\`\`typescript
|
|
1061
|
+
{
|
|
1062
|
+
name: 'title',
|
|
1063
|
+
type: 'text',
|
|
1064
|
+
required: true,
|
|
1065
|
+
label: 'Post Title',
|
|
1066
|
+
minLength: 10,
|
|
1067
|
+
maxLength: 100,
|
|
1068
|
+
// Custom validation example
|
|
1069
|
+
validate: (value, { siblingData }) => {
|
|
1070
|
+
if (value && value.toLowerCase().includes('forbidden')) {
|
|
1071
|
+
return 'Title cannot contain the word "forbidden"';
|
|
1072
|
+
}
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
\`\`\``;
|
|
1077
|
+
}
|
|
1078
|
+
else if (fieldType === 'relationship') {
|
|
1079
|
+
examples = `
|
|
1080
|
+
Example:
|
|
1081
|
+
\`\`\`typescript
|
|
1082
|
+
{
|
|
1083
|
+
name: 'author',
|
|
1084
|
+
type: 'relationship',
|
|
1085
|
+
relationTo: 'users',
|
|
1086
|
+
hasMany: false,
|
|
1087
|
+
required: true,
|
|
1088
|
+
// Custom validation example
|
|
1089
|
+
validate: async (value, { req }) => {
|
|
1090
|
+
// Check if the related document exists and is published
|
|
1091
|
+
if (value) {
|
|
1092
|
+
const relatedDoc = await req.payload.findByID({
|
|
1093
|
+
collection: 'users',
|
|
1094
|
+
id: value,
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
if (!relatedDoc) {
|
|
1098
|
+
return 'Selected user does not exist';
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return true;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
\`\`\``;
|
|
1105
|
+
}
|
|
1106
|
+
else if (fieldType === 'number') {
|
|
1107
|
+
examples = `
|
|
1108
|
+
Example:
|
|
1109
|
+
\`\`\`typescript
|
|
1110
|
+
{
|
|
1111
|
+
name: 'price',
|
|
1112
|
+
type: 'number',
|
|
1113
|
+
required: true,
|
|
1114
|
+
label: 'Product Price',
|
|
1115
|
+
min: 0,
|
|
1116
|
+
max: 1000,
|
|
1117
|
+
// Custom validation example
|
|
1118
|
+
validate: (value) => {
|
|
1119
|
+
if (value && value % 1 !== 0) {
|
|
1120
|
+
return 'Price must be a whole number';
|
|
1121
|
+
}
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
\`\`\``;
|
|
1126
|
+
}
|
|
1127
|
+
else if (fieldType === 'select') {
|
|
1128
|
+
examples = `
|
|
1129
|
+
Example:
|
|
1130
|
+
\`\`\`typescript
|
|
1131
|
+
{
|
|
1132
|
+
name: 'status',
|
|
1133
|
+
type: 'select',
|
|
1134
|
+
required: true,
|
|
1135
|
+
label: 'Status',
|
|
1136
|
+
options: [
|
|
1137
|
+
{ label: 'Draft', value: 'draft' },
|
|
1138
|
+
{ label: 'Published', value: 'published' },
|
|
1139
|
+
{ label: 'Archived', value: 'archived' }
|
|
1140
|
+
],
|
|
1141
|
+
defaultValue: 'draft',
|
|
1142
|
+
// Custom validation example
|
|
1143
|
+
validate: (value, { siblingData }) => {
|
|
1144
|
+
if (value === 'published' && !siblingData.publishedAt) {
|
|
1145
|
+
return 'Cannot set status to published without a publish date';
|
|
1146
|
+
}
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
\`\`\``;
|
|
1151
|
+
}
|
|
1152
|
+
else if (fieldType === 'array') {
|
|
1153
|
+
examples = `
|
|
1154
|
+
Example:
|
|
1155
|
+
\`\`\`typescript
|
|
1156
|
+
{
|
|
1157
|
+
name: 'items',
|
|
1158
|
+
type: 'array',
|
|
1159
|
+
label: 'Items',
|
|
1160
|
+
minRows: 1,
|
|
1161
|
+
maxRows: 10,
|
|
1162
|
+
fields: [
|
|
1163
|
+
{
|
|
1164
|
+
name: 'name',
|
|
1165
|
+
type: 'text',
|
|
1166
|
+
required: true
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
name: 'quantity',
|
|
1170
|
+
type: 'number',
|
|
1171
|
+
required: true,
|
|
1172
|
+
min: 1
|
|
1173
|
+
}
|
|
1174
|
+
],
|
|
1175
|
+
// Custom validation example
|
|
1176
|
+
validate: (value) => {
|
|
1177
|
+
if (value && value.length > 0) {
|
|
1178
|
+
const totalQuantity = value.reduce((sum, item) => sum + (item.quantity || 0), 0);
|
|
1179
|
+
if (totalQuantity > 100) {
|
|
1180
|
+
return 'Total quantity cannot exceed 100';
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return true;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
\`\`\``;
|
|
1187
|
+
}
|
|
1188
|
+
else {
|
|
1189
|
+
examples = `
|
|
1190
|
+
Example:
|
|
1191
|
+
\`\`\`typescript
|
|
1192
|
+
{
|
|
1193
|
+
name: '${fieldType}Field',
|
|
1194
|
+
type: '${fieldType}',
|
|
1195
|
+
required: true,
|
|
1196
|
+
label: '${fieldType.charAt(0).toUpperCase() + fieldType.slice(1)} Field',
|
|
1197
|
+
// Custom validation example
|
|
1198
|
+
validate: (value, { siblingData, operation }) => {
|
|
1199
|
+
// Validation logic based on the value, sibling data, or operation
|
|
1200
|
+
if (operation === 'create' && !value) {
|
|
1201
|
+
return 'This field is required for new records';
|
|
1202
|
+
}
|
|
1203
|
+
return true;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
\`\`\``;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
else if (baseName.includes('Hook')) {
|
|
1210
|
+
if (baseName.includes('BeforeChange')) {
|
|
1211
|
+
template = `/**
|
|
1212
|
+
* This hook runs before a document is saved to the database
|
|
1213
|
+
* It allows you to modify the data or perform validation
|
|
1214
|
+
*/
|
|
1215
|
+
const beforeChangeHook = ({ data, req, operation, originalDoc }) => {
|
|
1216
|
+
// 'data' contains the data being saved
|
|
1217
|
+
// 'req' is the Express request object with the Payload instance
|
|
1218
|
+
// 'operation' is either 'create' or 'update'
|
|
1219
|
+
// 'originalDoc' is the document before changes (for updates)
|
|
1220
|
+
|
|
1221
|
+
// Example: Add a timestamp
|
|
1222
|
+
return {
|
|
1223
|
+
...data,
|
|
1224
|
+
modifiedAt: new Date().toISOString(),
|
|
1225
|
+
modifiedBy: req.user?.id,
|
|
1226
|
+
};
|
|
1227
|
+
};`;
|
|
1228
|
+
}
|
|
1229
|
+
else if (baseName.includes('AfterChange')) {
|
|
1230
|
+
template = `/**
|
|
1231
|
+
* This hook runs after a document has been saved to the database
|
|
1232
|
+
* It allows you to perform side effects but cannot modify the saved data
|
|
1233
|
+
*/
|
|
1234
|
+
const afterChangeHook = ({ doc, req, operation, previousDoc }) => {
|
|
1235
|
+
// 'doc' contains the saved document
|
|
1236
|
+
// 'req' is the Express request object with the Payload instance
|
|
1237
|
+
// 'operation' is either 'create' or 'update'
|
|
1238
|
+
// 'previousDoc' is the document before changes (for updates)
|
|
1239
|
+
|
|
1240
|
+
// Example: Send a notification
|
|
1241
|
+
if (operation === 'create') {
|
|
1242
|
+
// Send notification about new document
|
|
1243
|
+
logger.info(\`New document created: \${doc.id}\`);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Return the document (cannot be modified)
|
|
1247
|
+
return doc;
|
|
1248
|
+
};`;
|
|
1249
|
+
}
|
|
1250
|
+
else if (baseName.includes('BeforeDelete')) {
|
|
1251
|
+
template = `/**
|
|
1252
|
+
* This hook runs before a document is deleted
|
|
1253
|
+
* It allows you to perform validation or prevent deletion
|
|
1254
|
+
*/
|
|
1255
|
+
const beforeDeleteHook = ({ req, id }) => {
|
|
1256
|
+
// 'req' is the Express request object with the Payload instance
|
|
1257
|
+
// 'id' is the ID of the document being deleted
|
|
1258
|
+
|
|
1259
|
+
// Example: Check if deletion is allowed
|
|
1260
|
+
// If you return a string, it will prevent deletion with that error message
|
|
1261
|
+
// If you throw an error, it will prevent deletion with that error
|
|
1262
|
+
|
|
1263
|
+
// To allow deletion, return undefined or void
|
|
1264
|
+
return;
|
|
1265
|
+
};`;
|
|
1266
|
+
}
|
|
1267
|
+
else if (baseName.includes('AfterDelete')) {
|
|
1268
|
+
template = `/**
|
|
1269
|
+
* This hook runs after a document has been deleted
|
|
1270
|
+
* It allows you to perform side effects
|
|
1271
|
+
*/
|
|
1272
|
+
const afterDeleteHook = ({ req, id, doc }) => {
|
|
1273
|
+
// 'req' is the Express request object with the Payload instance
|
|
1274
|
+
// 'id' is the ID of the document that was deleted
|
|
1275
|
+
// 'doc' is the document that was deleted
|
|
1276
|
+
|
|
1277
|
+
// Example: Clean up related data
|
|
1278
|
+
logger.info(\`Document deleted: \${id}\`);
|
|
1279
|
+
|
|
1280
|
+
// No return value is expected
|
|
1281
|
+
};`;
|
|
1282
|
+
}
|
|
1283
|
+
else if (baseName.includes('BeforeValidate')) {
|
|
1284
|
+
template = `/**
|
|
1285
|
+
* This hook runs before validation occurs
|
|
1286
|
+
* It allows you to modify the data before validation
|
|
1287
|
+
*/
|
|
1288
|
+
const beforeValidateHook = ({ data, req, operation, originalDoc }) => {
|
|
1289
|
+
// 'data' contains the data to be validated
|
|
1290
|
+
// 'req' is the Express request object with the Payload instance
|
|
1291
|
+
// 'operation' is either 'create' or 'update'
|
|
1292
|
+
// 'originalDoc' is the document before changes (for updates)
|
|
1293
|
+
|
|
1294
|
+
// Example: Set default values
|
|
1295
|
+
return {
|
|
1296
|
+
...data,
|
|
1297
|
+
status: data.status || 'draft',
|
|
1298
|
+
};
|
|
1299
|
+
};`;
|
|
1300
|
+
}
|
|
1301
|
+
else if (baseName.includes('BeforeRead') || baseName.includes('AfterRead')) {
|
|
1302
|
+
template = `/**
|
|
1303
|
+
* This hook runs before/after documents are returned from the database
|
|
1304
|
+
* It allows you to modify the data before it's sent to the client
|
|
1305
|
+
*/
|
|
1306
|
+
const readHook = ({ doc, req }) => {
|
|
1307
|
+
// 'doc' contains the document(s) being read
|
|
1308
|
+
// 'req' is the Express request object with the Payload instance
|
|
1309
|
+
|
|
1310
|
+
// Example: Add computed properties
|
|
1311
|
+
return {
|
|
1312
|
+
...doc,
|
|
1313
|
+
computedProperty: \`\${doc.firstName} \${doc.lastName}\`,
|
|
1314
|
+
};
|
|
1315
|
+
};`;
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
template = `/**
|
|
1319
|
+
* Generic Payload CMS hook
|
|
1320
|
+
* See documentation for specific hook parameters
|
|
1321
|
+
*/
|
|
1322
|
+
const hook = (args) => {
|
|
1323
|
+
// Extract relevant properties from args based on hook type
|
|
1324
|
+
const { req } = args;
|
|
1325
|
+
|
|
1326
|
+
// Example: Log hook execution
|
|
1327
|
+
logger.info('Hook executed');
|
|
1328
|
+
|
|
1329
|
+
// For hooks that modify data, return the modified data
|
|
1330
|
+
// For other hooks, return as appropriate for the hook type
|
|
1331
|
+
return args.data ? { ...args.data, modified: true } : undefined;
|
|
1332
|
+
};`;
|
|
1333
|
+
}
|
|
1334
|
+
if (schema.type === 'any' || !schema.properties) {
|
|
1335
|
+
schema.type = 'object';
|
|
1336
|
+
schema.properties = {
|
|
1337
|
+
description: { type: 'string', description: 'Description of what this hook does' }
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
else if (baseName === 'Access') {
|
|
1342
|
+
template = `/**
|
|
1343
|
+
* Access control function to determine if the operation is allowed
|
|
1344
|
+
* Returns true if access is granted, false if denied
|
|
1345
|
+
* Can also return a string with an error message if denied
|
|
1346
|
+
*/
|
|
1347
|
+
const accessControl = ({ req, id, data, doc }) => {
|
|
1348
|
+
// 'req' is the Express request object with the Payload instance and user
|
|
1349
|
+
// 'id' is the ID of the document being accessed (for operations on existing documents)
|
|
1350
|
+
// 'data' contains the data for create/update operations
|
|
1351
|
+
// 'doc' is the existing document for update operations
|
|
1352
|
+
|
|
1353
|
+
// Example: Only allow access if user is logged in
|
|
1354
|
+
if (!req.user) {
|
|
1355
|
+
return false; // Or return 'You must be logged in'
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// Example: Check user roles
|
|
1359
|
+
if (req.user.role === 'admin') {
|
|
1360
|
+
return true; // Admins have full access
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Example: Users can only access their own documents
|
|
1364
|
+
if (id && doc && doc.createdBy === req.user.id) {
|
|
1365
|
+
return true;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Deny access with a custom message
|
|
1369
|
+
return 'You do not have permission to access this resource';
|
|
1370
|
+
};`;
|
|
1371
|
+
if (schema.type === 'any' || !schema.properties) {
|
|
1372
|
+
schema.type = 'object';
|
|
1373
|
+
schema.properties = {
|
|
1374
|
+
description: { type: 'string', description: 'Description of this access control function' }
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
else if (baseName === 'CollectionAdminOptions') {
|
|
1379
|
+
template = `/**
|
|
1380
|
+
* Admin UI options for a collection
|
|
1381
|
+
*/
|
|
1382
|
+
{
|
|
1383
|
+
// Field to use as the title in the admin UI
|
|
1384
|
+
useAsTitle: 'title',
|
|
1385
|
+
|
|
1386
|
+
// Default columns to show in the admin UI list view
|
|
1387
|
+
defaultColumns: ['title', 'status', 'createdAt'],
|
|
1388
|
+
|
|
1389
|
+
// Group collections in the admin UI sidebar
|
|
1390
|
+
group: 'Content',
|
|
1391
|
+
|
|
1392
|
+
// Custom admin components (requires importing from your components)
|
|
1393
|
+
// components: {
|
|
1394
|
+
// views: {
|
|
1395
|
+
// List: MyCustomListView,
|
|
1396
|
+
// },
|
|
1397
|
+
// },
|
|
1398
|
+
|
|
1399
|
+
// Additional options
|
|
1400
|
+
...{rest}
|
|
1401
|
+
}`;
|
|
1402
|
+
}
|
|
1403
|
+
if (examples) {
|
|
1404
|
+
description = `${description}\n${examples}`;
|
|
1405
|
+
}
|
|
1406
|
+
toolsArray.push({
|
|
1407
|
+
name: toolName,
|
|
1408
|
+
description: description,
|
|
1409
|
+
inputSchema: schema,
|
|
1410
|
+
template
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
generatePayloadTools().catch(error => {
|
|
1414
|
+
console.error('Failed to generate tools:', error.message);
|
|
1415
|
+
process.exit(1);
|
|
1416
|
+
});
|
|
1417
|
+
//# sourceMappingURL=generate-tools.js.map
|