fastmode-mcp 1.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 +561 -0
- package/bin/run.js +50 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +802 -0
- package/dist/lib/api-client.d.ts +81 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +237 -0
- package/dist/lib/auth-state.d.ts +13 -0
- package/dist/lib/auth-state.d.ts.map +1 -0
- package/dist/lib/auth-state.js +24 -0
- package/dist/lib/context-fetcher.d.ts +67 -0
- package/dist/lib/context-fetcher.d.ts.map +1 -0
- package/dist/lib/context-fetcher.js +190 -0
- package/dist/lib/credentials.d.ts +52 -0
- package/dist/lib/credentials.d.ts.map +1 -0
- package/dist/lib/credentials.js +196 -0
- package/dist/lib/device-flow.d.ts +14 -0
- package/dist/lib/device-flow.d.ts.map +1 -0
- package/dist/lib/device-flow.js +244 -0
- package/dist/tools/cms-items.d.ts +56 -0
- package/dist/tools/cms-items.d.ts.map +1 -0
- package/dist/tools/cms-items.js +376 -0
- package/dist/tools/create-site.d.ts +9 -0
- package/dist/tools/create-site.d.ts.map +1 -0
- package/dist/tools/create-site.js +202 -0
- package/dist/tools/deploy-package.d.ts +9 -0
- package/dist/tools/deploy-package.d.ts.map +1 -0
- package/dist/tools/deploy-package.js +434 -0
- package/dist/tools/generate-samples.d.ts +19 -0
- package/dist/tools/generate-samples.d.ts.map +1 -0
- package/dist/tools/generate-samples.js +272 -0
- package/dist/tools/get-conversion-guide.d.ts +7 -0
- package/dist/tools/get-conversion-guide.d.ts.map +1 -0
- package/dist/tools/get-conversion-guide.js +1323 -0
- package/dist/tools/get-example.d.ts +7 -0
- package/dist/tools/get-example.d.ts.map +1 -0
- package/dist/tools/get-example.js +1568 -0
- package/dist/tools/get-field-types.d.ts +30 -0
- package/dist/tools/get-field-types.d.ts.map +1 -0
- package/dist/tools/get-field-types.js +154 -0
- package/dist/tools/get-schema.d.ts +5 -0
- package/dist/tools/get-schema.d.ts.map +1 -0
- package/dist/tools/get-schema.js +320 -0
- package/dist/tools/get-started.d.ts +21 -0
- package/dist/tools/get-started.d.ts.map +1 -0
- package/dist/tools/get-started.js +624 -0
- package/dist/tools/get-tenant-schema.d.ts +18 -0
- package/dist/tools/get-tenant-schema.d.ts.map +1 -0
- package/dist/tools/get-tenant-schema.js +158 -0
- package/dist/tools/list-projects.d.ts +5 -0
- package/dist/tools/list-projects.d.ts.map +1 -0
- package/dist/tools/list-projects.js +101 -0
- package/dist/tools/sync-schema.d.ts +41 -0
- package/dist/tools/sync-schema.d.ts.map +1 -0
- package/dist/tools/sync-schema.js +483 -0
- package/dist/tools/validate-manifest.d.ts +5 -0
- package/dist/tools/validate-manifest.d.ts.map +1 -0
- package/dist/tools/validate-manifest.js +311 -0
- package/dist/tools/validate-package.d.ts +5 -0
- package/dist/tools/validate-package.d.ts.map +1 -0
- package/dist/tools/validate-package.js +337 -0
- package/dist/tools/validate-template.d.ts +12 -0
- package/dist/tools/validate-template.d.ts.map +1 -0
- package/dist/tools/validate-template.js +790 -0
- package/package.json +54 -0
- package/scripts/postinstall.js +129 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sync Schema Tool
|
|
4
|
+
*
|
|
5
|
+
* Creates collections and/or fields in Fast Mode.
|
|
6
|
+
* Requires authentication. Will skip duplicates.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.syncSchema = syncSchema;
|
|
10
|
+
const api_client_1 = require("../lib/api-client");
|
|
11
|
+
const device_flow_1 = require("../lib/device-flow");
|
|
12
|
+
const get_field_types_1 = require("./get-field-types");
|
|
13
|
+
// ============ Constants ============
|
|
14
|
+
const VALID_FIELD_TYPES = get_field_types_1.AVAILABLE_FIELD_TYPES.map(ft => ft.value);
|
|
15
|
+
const AUTH_REQUIRED_MESSAGE = `# Authentication Required
|
|
16
|
+
|
|
17
|
+
This tool requires authentication to create collections and fields.
|
|
18
|
+
|
|
19
|
+
**To authenticate:**
|
|
20
|
+
1. Set the FASTMODE_AUTH_TOKEN environment variable, OR
|
|
21
|
+
2. Run this tool again and follow the browser-based login flow
|
|
22
|
+
|
|
23
|
+
Use \`list_projects\` to verify your authentication status.
|
|
24
|
+
`;
|
|
25
|
+
// ============ Helper Functions ============
|
|
26
|
+
/**
|
|
27
|
+
* Normalize select/multiSelect options to array format before sending to API.
|
|
28
|
+
* Handles comma-separated strings and converts to JSON array.
|
|
29
|
+
*/
|
|
30
|
+
function normalizeOptionsForApi(options, fieldType) {
|
|
31
|
+
if (!options)
|
|
32
|
+
return undefined;
|
|
33
|
+
// Only process select/multiSelect fields
|
|
34
|
+
if (fieldType !== 'select' && fieldType !== 'multiSelect') {
|
|
35
|
+
return options;
|
|
36
|
+
}
|
|
37
|
+
// If it already looks like a JSON array, validate and return
|
|
38
|
+
if (options.startsWith('[')) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(options);
|
|
41
|
+
if (Array.isArray(parsed))
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Invalid JSON, fall through to comma-separated handling
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Convert comma-separated string to JSON array
|
|
49
|
+
const arr = options.split(',').map(s => s.trim()).filter(Boolean);
|
|
50
|
+
return JSON.stringify(arr);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Validate a field type against available types
|
|
54
|
+
*/
|
|
55
|
+
function validateFieldType(type) {
|
|
56
|
+
if (!type) {
|
|
57
|
+
return { valid: false, error: 'Field type is required' };
|
|
58
|
+
}
|
|
59
|
+
if (!VALID_FIELD_TYPES.includes(type)) {
|
|
60
|
+
return {
|
|
61
|
+
valid: false,
|
|
62
|
+
error: `Invalid field type "${type}". Valid types: ${VALID_FIELD_TYPES.join(', ')}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return { valid: true };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Validate all fields in input
|
|
69
|
+
*/
|
|
70
|
+
function validateFields(fields) {
|
|
71
|
+
const errors = [];
|
|
72
|
+
for (const field of fields) {
|
|
73
|
+
if (!field.slug) {
|
|
74
|
+
errors.push(`Field missing slug`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!field.name) {
|
|
78
|
+
errors.push(`Field "${field.slug}" missing name`);
|
|
79
|
+
}
|
|
80
|
+
if (!field.type) {
|
|
81
|
+
errors.push(`Field "${field.slug}" missing type. Use get_field_types to see available types.`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const typeValidation = validateFieldType(field.type);
|
|
85
|
+
if (!typeValidation.valid) {
|
|
86
|
+
errors.push(`Field "${field.slug}": ${typeValidation.error}`);
|
|
87
|
+
}
|
|
88
|
+
// Check for required options/referenceCollection
|
|
89
|
+
if ((field.type === 'select' || field.type === 'multiSelect') && !field.options) {
|
|
90
|
+
errors.push(`Field "${field.slug}" (${field.type}) requires "options" parameter with comma-separated values`);
|
|
91
|
+
}
|
|
92
|
+
if (field.type === 'relation' && !field.referenceCollection) {
|
|
93
|
+
errors.push(`Field "${field.slug}" (relation) requires "referenceCollection" parameter`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { valid: errors.length === 0, errors };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve project identifier to tenant ID
|
|
100
|
+
*/
|
|
101
|
+
async function resolveProjectId(projectIdentifier) {
|
|
102
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
103
|
+
if (uuidPattern.test(projectIdentifier)) {
|
|
104
|
+
return { tenantId: projectIdentifier };
|
|
105
|
+
}
|
|
106
|
+
const response = await (0, api_client_1.apiRequest)('/api/tenants');
|
|
107
|
+
if ((0, api_client_1.isApiError)(response)) {
|
|
108
|
+
return { error: `Failed to look up project: ${response.error}` };
|
|
109
|
+
}
|
|
110
|
+
const match = response.data.find(p => p.name.toLowerCase() === projectIdentifier.toLowerCase());
|
|
111
|
+
if (match) {
|
|
112
|
+
return { tenantId: match.id };
|
|
113
|
+
}
|
|
114
|
+
const partialMatch = response.data.find(p => p.name.toLowerCase().includes(projectIdentifier.toLowerCase()));
|
|
115
|
+
if (partialMatch) {
|
|
116
|
+
return { tenantId: partialMatch.id };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
error: `Project "${projectIdentifier}" not found. Use list_projects to see available projects.`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Fetch existing collections for a project
|
|
124
|
+
*/
|
|
125
|
+
async function fetchExistingCollections(tenantId) {
|
|
126
|
+
const collectionsRes = await (0, api_client_1.apiRequest)('/api/collections', { tenantId });
|
|
127
|
+
if ((0, api_client_1.isApiError)(collectionsRes)) {
|
|
128
|
+
return { error: `Failed to fetch collections: ${collectionsRes.error}` };
|
|
129
|
+
}
|
|
130
|
+
return collectionsRes.data;
|
|
131
|
+
}
|
|
132
|
+
// ============ Main Function ============
|
|
133
|
+
/**
|
|
134
|
+
* Sync schema - create collections and/or fields
|
|
135
|
+
*
|
|
136
|
+
* @param input - The sync schema input with projectId, collections, and/or fieldsToAdd
|
|
137
|
+
*/
|
|
138
|
+
async function syncSchema(input) {
|
|
139
|
+
// Check authentication
|
|
140
|
+
if (await (0, api_client_1.needsAuthentication)()) {
|
|
141
|
+
const authResult = await (0, device_flow_1.ensureAuthenticated)();
|
|
142
|
+
if (!authResult.authenticated) {
|
|
143
|
+
return AUTH_REQUIRED_MESSAGE;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const { projectId, collections, fieldsToAdd } = input;
|
|
147
|
+
// Validate input
|
|
148
|
+
if (!projectId) {
|
|
149
|
+
return `# Error: Missing projectId
|
|
150
|
+
|
|
151
|
+
Please provide a projectId. Use \`list_projects\` to see your available projects.
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
if ((!collections || collections.length === 0) && (!fieldsToAdd || fieldsToAdd.length === 0)) {
|
|
155
|
+
return `# Error: Nothing to sync
|
|
156
|
+
|
|
157
|
+
Please provide either:
|
|
158
|
+
- \`collections\`: Array of new collections to create
|
|
159
|
+
- \`fieldsToAdd\`: Array of fields to add to existing collections
|
|
160
|
+
|
|
161
|
+
Use \`get_field_types\` to see available field types.
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
// Validate all field types before making any API calls
|
|
165
|
+
const allValidationErrors = [];
|
|
166
|
+
const fieldTypeTips = [];
|
|
167
|
+
// Field type hints based on common naming patterns
|
|
168
|
+
const fieldTypeHints = {
|
|
169
|
+
'video': { suggestedType: 'videoEmbed', tip: 'videoEmbed provides responsive iframe helpers for YouTube/Vimeo' },
|
|
170
|
+
'youtube': { suggestedType: 'videoEmbed', tip: 'videoEmbed handles YouTube embeds with correct settings' },
|
|
171
|
+
'vimeo': { suggestedType: 'videoEmbed', tip: 'videoEmbed handles Vimeo embeds properly' },
|
|
172
|
+
'loom': { suggestedType: 'videoEmbed', tip: 'videoEmbed supports Loom video URLs' },
|
|
173
|
+
'wistia': { suggestedType: 'videoEmbed', tip: 'videoEmbed supports Wistia video URLs' },
|
|
174
|
+
'author': { suggestedType: 'relation', tip: 'relation links to an authors collection' },
|
|
175
|
+
'category': { suggestedType: 'relation', tip: 'relation links to a categories collection' },
|
|
176
|
+
'categories': { suggestedType: 'relation', tip: 'relation links to a categories collection' },
|
|
177
|
+
'tag': { suggestedType: 'relation', tip: 'relation links to a tags collection' },
|
|
178
|
+
'tags': { suggestedType: 'relation', tip: 'relation links to a tags collection' },
|
|
179
|
+
'parent': { suggestedType: 'relation', tip: 'relation links to a parent collection' },
|
|
180
|
+
'related': { suggestedType: 'relation', tip: 'relation links to related items' },
|
|
181
|
+
};
|
|
182
|
+
// Helper to check field type hints
|
|
183
|
+
const checkFieldTypeHint = (field) => {
|
|
184
|
+
const hint = fieldTypeHints[field.slug.toLowerCase()];
|
|
185
|
+
if (hint && field.type !== hint.suggestedType) {
|
|
186
|
+
// Only add tip if type is a "close but not optimal" choice
|
|
187
|
+
if ((field.type === 'url' || field.type === 'text') && hint.suggestedType === 'videoEmbed') {
|
|
188
|
+
fieldTypeTips.push(`💡 "${field.slug}": Consider using "${hint.suggestedType}" type - ${hint.tip}`);
|
|
189
|
+
}
|
|
190
|
+
else if (field.type === 'text' && hint.suggestedType === 'relation') {
|
|
191
|
+
fieldTypeTips.push(`💡 "${field.slug}": Consider using "${hint.suggestedType}" type - ${hint.tip}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
if (collections) {
|
|
196
|
+
for (const col of collections) {
|
|
197
|
+
if (!col.slug)
|
|
198
|
+
allValidationErrors.push(`Collection missing slug`);
|
|
199
|
+
if (!col.name)
|
|
200
|
+
allValidationErrors.push(`Collection "${col.slug || 'unknown'}" missing name`);
|
|
201
|
+
if (!col.nameSingular)
|
|
202
|
+
allValidationErrors.push(`Collection "${col.slug || 'unknown'}" missing nameSingular`);
|
|
203
|
+
if (col.fields && col.fields.length > 0) {
|
|
204
|
+
const fieldValidation = validateFields(col.fields);
|
|
205
|
+
if (!fieldValidation.valid) {
|
|
206
|
+
allValidationErrors.push(...fieldValidation.errors.map(e => `Collection "${col.slug}": ${e}`));
|
|
207
|
+
}
|
|
208
|
+
// Check for field type hints
|
|
209
|
+
for (const field of col.fields) {
|
|
210
|
+
checkFieldTypeHint(field);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (fieldsToAdd) {
|
|
216
|
+
for (const group of fieldsToAdd) {
|
|
217
|
+
if (!group.collectionSlug) {
|
|
218
|
+
allValidationErrors.push(`fieldsToAdd entry missing collectionSlug`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
// Note: isBuiltin is deprecated and ignored - all collections are now custom
|
|
222
|
+
if (group.isBuiltin) {
|
|
223
|
+
allValidationErrors.push(`isBuiltin is no longer supported. All collections are custom collections. Use the collection slug directly.`);
|
|
224
|
+
}
|
|
225
|
+
if (!group.fields || group.fields.length === 0) {
|
|
226
|
+
allValidationErrors.push(`fieldsToAdd for "${group.collectionSlug}" has no fields`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const fieldValidation = validateFields(group.fields);
|
|
230
|
+
if (!fieldValidation.valid) {
|
|
231
|
+
allValidationErrors.push(...fieldValidation.errors.map(e => `${group.collectionSlug}: ${e}`));
|
|
232
|
+
}
|
|
233
|
+
// Check for field type hints
|
|
234
|
+
for (const field of group.fields) {
|
|
235
|
+
checkFieldTypeHint(field);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (allValidationErrors.length > 0) {
|
|
240
|
+
return `# Validation Errors
|
|
241
|
+
|
|
242
|
+
Please fix the following errors before syncing:
|
|
243
|
+
|
|
244
|
+
${allValidationErrors.map(e => `- ${e}`).join('\n')}
|
|
245
|
+
|
|
246
|
+
**Tip:** Use \`get_field_types\` to see available field types and their requirements.
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
// Resolve project ID
|
|
250
|
+
const resolved = await resolveProjectId(projectId);
|
|
251
|
+
if ('error' in resolved) {
|
|
252
|
+
return `# Project Not Found
|
|
253
|
+
|
|
254
|
+
${resolved.error}
|
|
255
|
+
`;
|
|
256
|
+
}
|
|
257
|
+
const { tenantId } = resolved;
|
|
258
|
+
// Fetch existing collections
|
|
259
|
+
const existingResult = await fetchExistingCollections(tenantId);
|
|
260
|
+
if ('error' in existingResult) {
|
|
261
|
+
// Check if auth error
|
|
262
|
+
if (existingResult.error.includes('401') || existingResult.error.includes('auth')) {
|
|
263
|
+
const authResult = await (0, device_flow_1.ensureAuthenticated)();
|
|
264
|
+
if (!authResult.authenticated) {
|
|
265
|
+
return AUTH_REQUIRED_MESSAGE;
|
|
266
|
+
}
|
|
267
|
+
// Retry
|
|
268
|
+
const retry = await fetchExistingCollections(tenantId);
|
|
269
|
+
if ('error' in retry) {
|
|
270
|
+
return `# Error\n\n${retry.error}`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
return `# Error\n\n${existingResult.error}`;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
let existingCollections = Array.isArray(existingResult) ? existingResult : [];
|
|
278
|
+
// Track results
|
|
279
|
+
const collectionResults = [];
|
|
280
|
+
const fieldResults = [];
|
|
281
|
+
const created = { collections: 0, fields: 0 };
|
|
282
|
+
const skipped = { collections: 0, fields: 0 };
|
|
283
|
+
const failed = { collections: 0, fields: 0 };
|
|
284
|
+
// Build a map of collection slug -> ID (for both existing and newly created)
|
|
285
|
+
const collectionIdMap = new Map();
|
|
286
|
+
const collectionFieldsMap = new Map();
|
|
287
|
+
// Initialize with existing collections
|
|
288
|
+
for (const col of existingCollections) {
|
|
289
|
+
collectionIdMap.set(col.slug.toLowerCase(), col.id);
|
|
290
|
+
collectionFieldsMap.set(col.slug.toLowerCase(), col.fields);
|
|
291
|
+
}
|
|
292
|
+
// ============ PHASE 1: Create/Resolve ALL Collections ============
|
|
293
|
+
if (collections && collections.length > 0) {
|
|
294
|
+
collectionResults.push('### Phase 1: Collections\n');
|
|
295
|
+
for (const col of collections) {
|
|
296
|
+
const slugLower = col.slug.toLowerCase();
|
|
297
|
+
// Check if collection already exists
|
|
298
|
+
if (collectionIdMap.has(slugLower)) {
|
|
299
|
+
collectionResults.push(`| ${col.slug} | Skipped | Already exists |`);
|
|
300
|
+
skipped.collections++;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// Create new collection (WITHOUT fields - those come in Phase 2)
|
|
304
|
+
let createRes = await (0, api_client_1.apiRequest)('/api/collections', {
|
|
305
|
+
tenantId,
|
|
306
|
+
method: 'POST',
|
|
307
|
+
body: {
|
|
308
|
+
slug: col.slug,
|
|
309
|
+
name: col.name,
|
|
310
|
+
nameSingular: col.nameSingular,
|
|
311
|
+
description: col.description,
|
|
312
|
+
hasSlug: col.hasSlug ?? true,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
// Retry once if failed
|
|
316
|
+
if ((0, api_client_1.isApiError)(createRes)) {
|
|
317
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
318
|
+
createRes = await (0, api_client_1.apiRequest)('/api/collections', {
|
|
319
|
+
tenantId,
|
|
320
|
+
method: 'POST',
|
|
321
|
+
body: {
|
|
322
|
+
slug: col.slug,
|
|
323
|
+
name: col.name,
|
|
324
|
+
nameSingular: col.nameSingular,
|
|
325
|
+
description: col.description,
|
|
326
|
+
hasSlug: col.hasSlug ?? true,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if ((0, api_client_1.isApiError)(createRes)) {
|
|
331
|
+
collectionResults.push(`| ${col.slug} | FAILED | ${createRes.error} |`);
|
|
332
|
+
failed.collections++;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// Add to our maps
|
|
336
|
+
collectionIdMap.set(slugLower, createRes.data.id);
|
|
337
|
+
collectionFieldsMap.set(slugLower, []); // New collection has no fields yet
|
|
338
|
+
collectionResults.push(`| ${col.slug} | Created | ${col.name} |`);
|
|
339
|
+
created.collections++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const fieldJobs = [];
|
|
343
|
+
// Fields from new collections
|
|
344
|
+
if (collections) {
|
|
345
|
+
for (const col of collections) {
|
|
346
|
+
if (col.fields && col.fields.length > 0) {
|
|
347
|
+
for (const field of col.fields) {
|
|
348
|
+
fieldJobs.push({ collectionSlug: col.slug, field });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Fields from fieldsToAdd
|
|
354
|
+
if (fieldsToAdd) {
|
|
355
|
+
for (const group of fieldsToAdd) {
|
|
356
|
+
for (const field of group.fields) {
|
|
357
|
+
fieldJobs.push({ collectionSlug: group.collectionSlug, field });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (fieldJobs.length > 0) {
|
|
362
|
+
fieldResults.push('### Phase 2: Fields\n');
|
|
363
|
+
fieldResults.push('| Collection | Field | Type | Status |');
|
|
364
|
+
fieldResults.push('|------------|-------|------|--------|');
|
|
365
|
+
for (const job of fieldJobs) {
|
|
366
|
+
const slugLower = job.collectionSlug.toLowerCase();
|
|
367
|
+
const collectionId = collectionIdMap.get(slugLower);
|
|
368
|
+
if (!collectionId) {
|
|
369
|
+
fieldResults.push(`| ${job.collectionSlug} | ${job.field.slug} | ${job.field.type} | FAILED: Collection not found |`);
|
|
370
|
+
failed.fields++;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
// Check if field already exists
|
|
374
|
+
const existingFields = collectionFieldsMap.get(slugLower) || [];
|
|
375
|
+
const fieldExists = existingFields.some(f => f.slug.toLowerCase() === job.field.slug.toLowerCase());
|
|
376
|
+
if (fieldExists) {
|
|
377
|
+
fieldResults.push(`| ${job.collectionSlug} | ${job.field.slug} | ${job.field.type} | Skipped (exists) |`);
|
|
378
|
+
skipped.fields++;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
// Normalize options for select/multiSelect fields before sending
|
|
382
|
+
const normalizedOptions = normalizeOptionsForApi(job.field.options, job.field.type);
|
|
383
|
+
// Create the field with retry logic
|
|
384
|
+
let fieldRes = await (0, api_client_1.apiRequest)(`/api/collections/${collectionId}/fields`, {
|
|
385
|
+
tenantId,
|
|
386
|
+
method: 'POST',
|
|
387
|
+
body: {
|
|
388
|
+
slug: job.field.slug,
|
|
389
|
+
name: job.field.name,
|
|
390
|
+
type: job.field.type,
|
|
391
|
+
description: job.field.description,
|
|
392
|
+
isRequired: job.field.isRequired,
|
|
393
|
+
options: normalizedOptions,
|
|
394
|
+
referenceCollection: job.field.referenceCollection,
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
// Retry once if failed (network issues, temporary errors)
|
|
398
|
+
if ((0, api_client_1.isApiError)(fieldRes)) {
|
|
399
|
+
// Wait a moment and retry
|
|
400
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
401
|
+
fieldRes = await (0, api_client_1.apiRequest)(`/api/collections/${collectionId}/fields`, {
|
|
402
|
+
tenantId,
|
|
403
|
+
method: 'POST',
|
|
404
|
+
body: {
|
|
405
|
+
slug: job.field.slug,
|
|
406
|
+
name: job.field.name,
|
|
407
|
+
type: job.field.type,
|
|
408
|
+
description: job.field.description,
|
|
409
|
+
isRequired: job.field.isRequired,
|
|
410
|
+
options: normalizedOptions,
|
|
411
|
+
referenceCollection: job.field.referenceCollection,
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
if ((0, api_client_1.isApiError)(fieldRes)) {
|
|
416
|
+
fieldResults.push(`| ${job.collectionSlug} | ${job.field.slug} | ${job.field.type} | FAILED: ${fieldRes.error} |`);
|
|
417
|
+
failed.fields++;
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
fieldResults.push(`| ${job.collectionSlug} | ${job.field.slug} | ${job.field.type} | Created |`);
|
|
421
|
+
created.fields++;
|
|
422
|
+
// Update the fields map so subsequent checks know this field exists
|
|
423
|
+
const fields = collectionFieldsMap.get(slugLower) || [];
|
|
424
|
+
fields.push({ slug: job.field.slug });
|
|
425
|
+
collectionFieldsMap.set(slugLower, fields);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// ============ Build Summary ============
|
|
430
|
+
const hasFailures = failed.collections > 0 || failed.fields > 0;
|
|
431
|
+
let output = `# Schema Sync ${hasFailures ? 'Completed with Errors' : 'Complete'}
|
|
432
|
+
|
|
433
|
+
**Project ID:** \`${tenantId}\`
|
|
434
|
+
|
|
435
|
+
## Summary
|
|
436
|
+
|
|
437
|
+
| Metric | Created | Skipped | Failed |
|
|
438
|
+
|--------|---------|---------|--------|
|
|
439
|
+
| Collections | ${created.collections} | ${skipped.collections} | ${failed.collections} |
|
|
440
|
+
| Fields | ${created.fields} | ${skipped.fields} | ${failed.fields} |
|
|
441
|
+
|
|
442
|
+
`;
|
|
443
|
+
if (collectionResults.length > 1) {
|
|
444
|
+
output += `## Collections
|
|
445
|
+
|
|
446
|
+
| Slug | Status | Details |
|
|
447
|
+
|------|--------|---------|
|
|
448
|
+
${collectionResults.slice(1).join('\n')}
|
|
449
|
+
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
452
|
+
if (fieldResults.length > 0) {
|
|
453
|
+
output += `## Fields
|
|
454
|
+
|
|
455
|
+
${fieldResults.join('\n')}
|
|
456
|
+
|
|
457
|
+
`;
|
|
458
|
+
}
|
|
459
|
+
if (hasFailures) {
|
|
460
|
+
output += `---
|
|
461
|
+
|
|
462
|
+
## ACTION REQUIRED
|
|
463
|
+
|
|
464
|
+
Some items failed to create. Please review the errors above and:
|
|
465
|
+
1. Fix any issues with field types or parameters
|
|
466
|
+
2. Run sync_schema again - it will skip already-created items and retry failed ones
|
|
467
|
+
`;
|
|
468
|
+
}
|
|
469
|
+
// Add field type tips if any were collected
|
|
470
|
+
if (fieldTypeTips.length > 0) {
|
|
471
|
+
output += `---
|
|
472
|
+
|
|
473
|
+
## Tips
|
|
474
|
+
|
|
475
|
+
The following suggestions may help improve your schema:
|
|
476
|
+
|
|
477
|
+
${fieldTypeTips.join('\n')}
|
|
478
|
+
|
|
479
|
+
These are suggestions only - your current field types will still work.
|
|
480
|
+
`;
|
|
481
|
+
}
|
|
482
|
+
return output;
|
|
483
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-manifest.d.ts","sourceRoot":"","sources":["../../src/tools/validate-manifest.ts"],"names":[],"mappings":"AAmBA;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoT5E"}
|