@swell/cli
Version:
Swell's command line interface/utility
198 lines (196 loc) • 7.42 kB
JavaScript
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));
}
}