UNPKG

@swell/cli

Version:

Swell's command line interface/utility

198 lines (196 loc) 7.42 kB
import { Args, Flags } from '@oclif/core'; import * as fs from 'node:fs'; import { FetchError } from 'node-fetch'; import { default as localConfig } from '../../lib/config.js'; import { SwellCommand } from '../../swell-command.js'; export default class Models extends SwellCommand { static summary = 'Inspect models for collections deployed in your store.'; static description = ` View models for collections available in your Swell store. Without a collection path, this command lists all available collections grouped by type. With a collection path, it retrieves the model for that specific collection in JSON format. `; static args = { 'collection-path': Args.string({ description: "Optional collection path (must start with '/'). Example: /products, /apps/myapp/orders", required: false, }), }; static flags = { live: Flags.boolean({ description: 'Use the live environment instead of test (default: test).', default: false, }), yes: Flags.boolean({ char: 'y', description: 'Accept all prompts automatically (for agent compatibility; no functional effect).', default: false, }), }; static examples = [ 'swell inspect models', 'swell inspect models /products', 'swell inspect models /apps/myapp/orders', 'swell inspect models /orders --live', ]; async run() { const { args, flags } = await this.parse(Models); const { 'collection-path': path } = args; const { live } = flags; if (!live) { await this.api.setEnv('test'); } // Detail mode: show schema for a specific model if (path) { if (!path.startsWith('/')) { throw new Error(`Collection path must start with '/'. Did you mean '/${path}'?`); } await this.showModelDetail(path); return; } const store = localConfig.getDefaultStore(); // List mode: show all models await this.listModels(store, live); } async catch(error) { if (error instanceof FetchError) { const message = `Could not connect to Swell API. Please try again later: ${error.message}`; return this.error(message, { exit: 2, code: error.code }); } return this.error(error.message, { exit: 1 }); } async listModels(store, live) { const { results: apps } = await this.api.get({ adminPath: '/apps', }); const appsById = Object.fromEntries(apps.map((app) => [app.id, app])); const { results: models } = await this.api.getAll({ adminPath: '/data/:models', }, { query: { content_id: { $exists: false }, deprecated: { $ne: true }, development: { $ne: true }, abstract: { $ne: true }, reserved: { $ne: true }, $or: [ { app_id: { $exists: false } }, { app_id: { $in: Object.keys(appsById) } }, ], }, }); const standardModels = []; const modelsByAppId = {}; for (const model of models) { if (model.app_id) { modelsByAppId[model.app_id] ||= []; modelsByAppId[model.app_id].push(model); } else { standardModels.push(model); } } this.log(`Collections available in '${store}' ${live ? '[live]' : '[test]'}:`); this.log(); this.showStandardModels(standardModels); await this.showAppModels(appsById, modelsByAppId); // Show next step hint this.log(); this.log('View collection model'); this.log(' swell inspect models <collection-path>'); this.log(); this.log('Examples:'); this.log(' swell inspect models /products'); this.log(' swell inspect models /apps/myapp/orders'); } showStandardModels(models) { this.showModels(models, 'Standard'); } async showAppModels(appsById, modelsByAppId) { const currentAppSlugId = await this.getCurrentAppSlugId(); for (const [appId, app] of Object.entries(appsById)) { const models = modelsByAppId[appId]; if (models) { const appSlugId = this.getAppSlugId(app); const pathPrefix = `/apps/${appSlugId}`; const label = `${app.name} App ${currentAppSlugId === appSlugId ? '(current app)' : ''}`; this.log(); this.showModels(models, label, { pathPrefix }); } } } showModels(models, label, options) { this.log(label); this.log(); const sortedModels = [...models].sort((a, b) => a.name.localeCompare(b.name)); for (const model of sortedModels) { this.showCollectionPath(model.name, 2, options); const sortedSubModels = Object.values(model.fields) .filter((field) => field.type === 'collection') .map((field) => field.name) .sort((a, b) => a.localeCompare(b)); for (const sub of sortedSubModels) { this.showCollectionPath(sub, 4, options); } } } showCollectionPath(collection, indent = 0, options) { this.log(`${' '.repeat(indent)}${options?.pathPrefix || ''}/${collection}`); } async getCurrentAppSlugId() { const hasAppContext = fs.existsSync('swell.json'); if (!hasAppContext) { return; } try { const appConfig = JSON.parse(await fs.promises.readFile('swell.json', 'utf8')); return appConfig.id; } catch { // noop } } getAppSlugId(app) { return app.public_id || app.private_id.replace(/^_/, ''); } async showModelDetail(path) { const [modelPath, subModelKey] = await this.resolveModelPath(path); const model = await this.api.get({ adminPath: `/data/:models${modelPath}`, }, { query: { $app: true } }); if (!model) { this.throwModelNotFound(path); } if (!subModelKey) { return this.showModel(model); } const subModel = model.fields[subModelKey]; if (!subModel) { this.throwModelNotFound(path); } this.showModel(subModel); } async resolveModelPath(path) { if (path.startsWith('/apps/')) { return this.resolveAppModelPath(path); } const [modelPath, subModelKey] = path.split(':'); return [modelPath, subModelKey]; } async resolveAppModelPath(path) { const parts = path.split('/'); const appSlugId = parts[2]; const model = parts[3]; const app = await this.api.get({ adminPath: `/apps/${appSlugId}` }); if (!app) { this.throwModelNotFound(path); } return this.resolveModelPath(`/app_${app.id}.${model}`); } throwModelNotFound(path) { throw new Error(`No model found for collection '${path}'.\nList available collections: swell inspect models`); } showModel(model) { this.log(JSON.stringify(model, null, 2)); } }