@swell/cli
Version:
Swell's command line interface/utility
126 lines (122 loc) • 5.68 kB
JavaScript
import { InspectResourceCommand, inspectResourceBaseArgs, inspectResourceBaseFlags, } from '../../inspect-resource-command.js';
import { isObjectId } from '../../lib/apps/object-id.js';
import { buildAppSlugMap, resolveAppId } from '../../lib/apps/resolve.js';
import { buildNotificationTemplate, expandModelCandidates, formatDisambiguationError, formatNotificationKey, groupNotificationRecord, parseNotificationKey, } from '../../lib/inspect/notifications.js';
export default class Notifications extends InspectResourceCommand {
static summary = 'Notifications across apps and platform.';
static description = `Lists notifications across all apps plus platform 'com.*' system records, grouped by owner. Pass --app=<slug> or --app=. (current swell.json) to scope.
App keys include the model (app.<slug>.<model>.<name>) since identity is (api, model, name, app_id?) — two notifications in one app can share a name on different models. Own-app prefix apps/<own>/... is stripped in display.
Pass an identifier to view a single record as JSON.
Identifier forms:
com.<model>.<name> — platform record
app.<app>.<model>.<name> — app record (full key)
app.<app>.<name> — legacy; ambiguous → errors with candidates
<name> — requires --app= scope
<24-char id> — any scope
`;
static args = { ...inspectResourceBaseArgs };
static flags = { ...inspectResourceBaseFlags };
static examples = [
'swell inspect notifications',
'swell inspect notifications --app=my-app',
'swell inspect notifications com.orders.receipt.v2',
'swell inspect notifications app.notify_me.subscriptions.back-in-stock',
'swell inspect notifications back-in-stock --app=notify_me',
'swell inspect notifications --live',
];
resourceLabel = 'Notifications';
resourceLabelSingular = 'notification';
adminPath = '/data/:notifications';
commandName = 'notifications';
async run() {
const { args, flags } = await this.parse(Notifications);
await this.runInspect({ args, flags });
}
keyFor(record, appSlugById) {
return formatNotificationKey(record, appSlugById);
}
groupForRecord(record, appSlugById) {
return groupNotificationRecord(record, appSlugById);
}
metaFor(r) {
return r.enabled === false ? 'disabled' : undefined;
}
hints(record) {
const template = buildNotificationTemplate(record);
if (!template) {
return [];
}
return [
`swell api get '/notifications?where[template]=${template}&limit=10'`,
];
}
async showDetail(identifier, scope, flags) {
const parsed = parseNotificationKey(identifier);
let record = null;
switch (parsed.kind) {
case 'hex':
case 'system_id': {
record = await this.api.get({
adminPath: `${this.adminPath}/${parsed.id}`,
});
break;
}
case 'app_full': {
const appId = await this.resolveSlugOrHex(parsed.slug);
const candidates = await expandModelCandidates(parsed.model, appId, (s) => resolveAppId(this.api, s));
record = await this.lookupTriplet(appId, candidates, parsed.name);
break;
}
case 'app_short': {
const appId = await this.resolveSlugOrHex(parsed.slug);
record = await this.lookupAndDisambiguate(appId, parsed.name, identifier);
break;
}
case 'bare': {
if (!scope.appId) {
this.error(`Bare ${this.resourceLabelSingular} name '${identifier}' requires --app=<slug> or --app=. to scope. ` +
`Alternatively, pass a full key (app.<app>.<model>.<name> or com.<model>.<name>).`, { exit: 1 });
}
record = await this.lookupAndDisambiguate(scope.appId, parsed.name, identifier);
break;
}
case 'invalid': {
this.error(`Invalid ${this.resourceLabelSingular} identifier '${identifier}'. ` +
`Expected bare name, app.<app>.<model>.<name>, com.<model>.<name>, or 24-char id.`, { exit: 1 });
}
}
if (!record) {
this.throwNotFound(identifier);
}
this.emitDetail(record, flags);
}
async resolveSlugOrHex(slugOrHex) {
return isObjectId(slugOrHex)
? slugOrHex
: resolveAppId(this.api, slugOrHex);
}
async lookupTriplet(appId, modelCandidates, name) {
const response = await this.api.get({ adminPath: this.adminPath }, {
query: {
app_id: appId,
model: { $in: modelCandidates },
name,
limit: 1,
},
});
return response?.results?.[0] ?? null;
}
async lookupAndDisambiguate(appId, name, identifier) {
// Fetch one past the display cap so the formatter can distinguish
// "exactly 10 candidates" from "10+ candidates and we're truncating".
const response = await this.api.get({ adminPath: this.adminPath }, { query: { app_id: appId, name, limit: 11 } });
const results = response?.results ?? [];
if (results.length <= 1) {
return results[0] ?? null;
}
const appSlugById = await buildAppSlugMap(this.api);
this.error(formatDisambiguationError(results, appSlugById, identifier), {
exit: 1,
});
}
}