"use strict";
/**
* Generate Sample Items Tool
*
* Generates sample items for collections with automatic dependency ordering.
* Handles relation fields by generating parent collections first.
* Requires authentication.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateSampleItems = generateSampleItems;
const api_client_1 = require("../lib/api-client");
const device_flow_1 = require("../lib/device-flow");
// ============ Constants ============
const AUTH_REQUIRED_MESSAGE = `# Authentication Required
This tool requires authentication to generate sample items.
**To authenticate:**
1. Set the FASTMODE_AUTH_TOKEN environment variable, OR
2. Run this tool again and follow the browser-based login flow
Use \`list_projects\` to verify your authentication status.
`;
// ============ Helper Functions ============
/**
* Resolve project identifier to tenant ID
*/
async function resolveProjectId(projectIdentifier) {
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (uuidPattern.test(projectIdentifier)) {
return { tenantId: projectIdentifier };
}
const response = await (0, api_client_1.apiRequest)('/api/tenants');
if ((0, api_client_1.isApiError)(response)) {
return { error: `Failed to look up project: ${response.error}` };
}
const match = response.data.find(p => p.name.toLowerCase() === projectIdentifier.toLowerCase());
if (match) {
return { tenantId: match.id };
}
const partialMatch = response.data.find(p => p.name.toLowerCase().includes(projectIdentifier.toLowerCase()));
if (partialMatch) {
return { tenantId: partialMatch.id };
}
return {
error: `Project "${projectIdentifier}" not found. Use list_projects to see available projects.`
};
}
/**
* Topological sort of collections based on relation dependencies
* Returns collections ordered so that dependencies come first
*/
function sortByDependencies(collections) {
const slugToCollection = new Map();
const inDegree = new Map();
const adjacencyList = new Map();
// Initialize
for (const col of collections) {
slugToCollection.set(col.slug, col);
inDegree.set(col.slug, 0);
adjacencyList.set(col.slug, []);
}
// Build dependency graph
// If collection A has a relation to collection B, then B must be populated first
// So B -> A (B is a dependency of A)
for (const col of collections) {
for (const field of col.fields) {
if (field.type === 'relation' && field.referenceCollection) {
const refSlug = field.referenceCollection;
// Only count if the referenced collection is in our list
if (slugToCollection.has(refSlug)) {
// refSlug must come before col.slug
const deps = adjacencyList.get(refSlug) || [];
deps.push(col.slug);
adjacencyList.set(refSlug, deps);
inDegree.set(col.slug, (inDegree.get(col.slug) || 0) + 1);
}
}
}
}
// Kahn's algorithm for topological sort
const queue = [];
const result = [];
// Start with nodes that have no dependencies
for (const [slug, degree] of inDegree) {
if (degree === 0) {
queue.push(slug);
}
}
while (queue.length > 0) {
const current = queue.shift();
const col = slugToCollection.get(current);
if (col) {
result.push(col);
}
// Reduce in-degree for dependent collections
const dependents = adjacencyList.get(current) || [];
for (const dep of dependents) {
const newDegree = (inDegree.get(dep) || 0) - 1;
inDegree.set(dep, newDegree);
if (newDegree === 0) {
queue.push(dep);
}
}
}
// If there are still collections not in result (cycle detected), add them anyway
for (const col of collections) {
if (!result.find(c => c.slug === col.slug)) {
result.push(col);
}
}
return result;
}
// ============ Main Function ============
/**
* Generate sample items for collections
*
* @param input - The input with projectId and optional collectionSlugs
*/
async function generateSampleItems(input) {
// Check authentication
if (await (0, api_client_1.needsAuthentication)()) {
const authResult = await (0, device_flow_1.ensureAuthenticated)();
if (!authResult.authenticated) {
return AUTH_REQUIRED_MESSAGE;
}
}
const { projectId, collectionSlugs } = input;
// Validate input
if (!projectId) {
return `# Error: Missing projectId
Please provide a projectId. Use \`list_projects\` to see your available projects.
`;
}
// Resolve project ID
const resolved = await resolveProjectId(projectId);
if ('error' in resolved) {
return `# Project Not Found
${resolved.error}
`;
}
const { tenantId } = resolved;
// Fetch all collections for this project
const collectionsRes = await (0, api_client_1.apiRequest)('/api/collections', { tenantId });
if ((0, api_client_1.isApiError)(collectionsRes)) {
// Check if auth error
if (collectionsRes.error.includes('401') || collectionsRes.error.includes('auth')) {
const authResult = await (0, device_flow_1.ensureAuthenticated)();
if (!authResult.authenticated) {
return AUTH_REQUIRED_MESSAGE;
}
}
return `# Error
Failed to fetch collections: ${collectionsRes.error}
`;
}
const allCollections = collectionsRes.data;
if (allCollections.length === 0) {
return `# No Collections Found
This project has no collections yet. Use \`sync_schema\` to create collections first.
`;
}
// Determine which collections to generate samples for
let targetCollections;
if (collectionSlugs && collectionSlugs.length > 0) {
// Specific collections requested
targetCollections = [];
const notFound = [];
for (const slug of collectionSlugs) {
const col = allCollections.find(c => c.slug.toLowerCase() === slug.toLowerCase());
if (col) {
targetCollections.push(col);
}
else {
notFound.push(slug);
}
}
if (notFound.length > 0) {
return `# Collections Not Found
The following collections were not found: ${notFound.join(', ')}
Available collections: ${allCollections.map(c => c.slug).join(', ')}
`;
}
}
else {
// All collections - check which ones are empty
const emptyCollections = [];
for (const col of allCollections) {
const itemsRes = await (0, api_client_1.apiRequest)(`/api/collections/${col.id}/items`, { tenantId });
if (!(0, api_client_1.isApiError)(itemsRes) && itemsRes.data.length === 0) {
emptyCollections.push(col);
}
}
if (emptyCollections.length === 0) {
return `# No Empty Collections
All collections already have items. To regenerate samples for specific collections, provide their slugs.
Collections with items: ${allCollections.map(c => c.slug).join(', ')}
`;
}
targetCollections = emptyCollections;
}
// Sort collections by dependencies (parents first)
const sortedCollections = sortByDependencies(targetCollections);
// Generate samples for each collection in order
const results = [];
const generated = [];
const failed = [];
results.push('## Generation Order\n');
results.push('Collections are processed in dependency order (parent collections first):\n');
results.push(sortedCollections.map((c, i) => `${i + 1}. ${c.name} (\`${c.slug}\`)`).join('\n'));
results.push('\n');
for (const col of sortedCollections) {
// Check for relation fields and their dependencies
const relationFields = col.fields.filter(f => f.type === 'relation' && f.referenceCollection);
const dependencies = relationFields.map(f => f.referenceCollection).filter(Boolean);
let dependencyNote = '';
if (dependencies.length > 0) {
dependencyNote = ` (depends on: ${dependencies.join(', ')})`;
}
// Call the generate-samples endpoint
const generateRes = await (0, api_client_1.apiRequest)(`/api/collections/${col.id}/generate-samples`, {
tenantId,
method: 'POST',
});
if ((0, api_client_1.isApiError)(generateRes)) {
failed.push({ collection: col.slug, error: generateRes.error });
results.push(`❌ **${col.name}** - Failed: ${generateRes.error}${dependencyNote}`);
}
else {
const count = generateRes.data.data?.length || 5;
generated.push({ collection: col.slug, count });
results.push(`✅ **${col.name}** - Generated ${count} sample items${dependencyNote}`);
}
}
// Build summary
let output = `# Sample Items Generated
**Project ID:** \`${tenantId}\`
## Summary
| Metric | Count |
|--------|-------|
| Collections Processed | ${sortedCollections.length} |
| Successfully Generated | ${generated.length} |
| Failed | ${failed.length} |
${results.join('\n')}
`;
if (failed.length > 0) {
output += `\n## Errors\n\n`;
for (const f of failed) {
output += `- **${f.collection}**: ${f.error}\n`;
}
}
if (generated.length > 0) {
output += `\n---\n\n**Next Steps:**
- View the generated items in the CMS dashboard
- Edit or delete sample items as needed
- These are placeholder items - replace with real content when ready
`;
}
return output;
}