@swell/cli 2.9.31 → 2.9.32
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/dist/commands/app/action.d.ts +25 -0
- package/dist/commands/app/action.js +233 -0
- package/dist/commands/app/frontend/deploy.d.ts +1 -0
- package/dist/commands/app/frontend/deploy.js +4 -0
- package/dist/commands/app/push.d.ts +2 -0
- package/dist/commands/app/push.js +63 -2
- package/dist/commands/create/webhook.js +7 -13
- package/dist/commands/schema.js +5 -0
- package/dist/help/custom-help.js +5 -1
- package/dist/lib/apps/actions.d.ts +45 -0
- package/dist/lib/apps/actions.js +114 -0
- package/dist/lib/apps/listing.d.ts +1 -0
- package/dist/lib/apps/listing.js +41 -0
- package/dist/lib/bundle.js +11 -39
- package/dist/lib/function-declarations.d.ts +8 -0
- package/dist/lib/function-declarations.js +27 -0
- package/dist/lib/swell-function-wrapper.js +8 -1
- package/dist/lib/swell-workflow-request.d.ts +34 -0
- package/dist/lib/swell-workflow-request.js +58 -0
- package/dist/push-app-command.d.ts +2 -1
- package/dist/push-app-command.js +11 -3
- package/dist/remote-app-command.d.ts +1 -1
- package/dist/remote-app-command.js +1 -4
- package/oclif.manifest.json +125 -5
- package/package.json +2 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { RemoteAppCommand } from '../../remote-app-command.js';
|
|
2
|
+
export default class AppAction extends RemoteAppCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
id: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
all: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
|
+
collection: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
|
+
ids: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
|
+
live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
13
|
+
record: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
14
|
+
settings: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
15
|
+
source: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
16
|
+
values: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
17
|
+
view: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
18
|
+
};
|
|
19
|
+
static summary: string;
|
|
20
|
+
run(): Promise<void>;
|
|
21
|
+
private findAction;
|
|
22
|
+
private findFunctionId;
|
|
23
|
+
private getActionParams;
|
|
24
|
+
private parseValues;
|
|
25
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { functionSlug, getContentActions, getModalFieldIds, getSettingsActions, } from '../../lib/apps/actions.js';
|
|
4
|
+
import style from '../../lib/style.js';
|
|
5
|
+
import { RemoteAppCommand } from '../../remote-app-command.js';
|
|
6
|
+
const UNAVAILABLE_CODES = new Set([
|
|
7
|
+
'notfound',
|
|
8
|
+
'connrefused',
|
|
9
|
+
'proxy_notfound',
|
|
10
|
+
]);
|
|
11
|
+
export default class AppAction extends RemoteAppCommand {
|
|
12
|
+
static args = {
|
|
13
|
+
id: Args.string({
|
|
14
|
+
description: 'id of the action, as declared in content or settings',
|
|
15
|
+
required: true,
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
static description = `Runs a function action of the app as the logged-in dashboard user, the way a click in the dashboard does. The action is looked up in the app's content and settings as pushed.
|
|
19
|
+
|
|
20
|
+
A record action or action field needs ${style.command('--record')}, and a bulk action ${style.command('--ids')} or ${style.command('--all')}. Pass the values of the action's modal with ${style.command('--values')}. When the same id is declared in more than one place, pick one with ${style.command('--collection')}, ${style.command('--view')}, ${style.command('--settings')} or ${style.command('--source')}.`;
|
|
21
|
+
static examples = [
|
|
22
|
+
'swell app action sync_now',
|
|
23
|
+
'swell app action ship --record 64e8dfb633c44a0039477251',
|
|
24
|
+
`swell app action ship --record 64e8dfb633c44a0039477251 --values '{"carrier":"ups"}'`,
|
|
25
|
+
'swell app action ship-selected --ids 64e8dfb633c44a0039477251,64e8dfb633c44a0039477252',
|
|
26
|
+
'swell app action ship --collection shipments --view edit --record 64e8dfb633c44a0039477251',
|
|
27
|
+
'swell app action export --source bulk --all',
|
|
28
|
+
];
|
|
29
|
+
static flags = {
|
|
30
|
+
all: Flags.boolean({
|
|
31
|
+
description: 'run a bulk action on all records its view lists with no filters set, as selecting all in the dashboard does',
|
|
32
|
+
exclusive: ['ids'],
|
|
33
|
+
}),
|
|
34
|
+
collection: Flags.string({
|
|
35
|
+
description: "collection of the content that declares the action, or the model's name for a collection of the app",
|
|
36
|
+
}),
|
|
37
|
+
ids: Flags.string({
|
|
38
|
+
description: 'comma-separated ids of the records a bulk action runs on',
|
|
39
|
+
exclusive: ['all'],
|
|
40
|
+
}),
|
|
41
|
+
live: Flags.boolean({
|
|
42
|
+
default: false,
|
|
43
|
+
description: 'run in the live environment instead of test',
|
|
44
|
+
}),
|
|
45
|
+
record: Flags.string({
|
|
46
|
+
description: 'id of the record a record action or action field runs on',
|
|
47
|
+
}),
|
|
48
|
+
settings: Flags.string({
|
|
49
|
+
description: 'name of the settings that declare the action',
|
|
50
|
+
}),
|
|
51
|
+
source: Flags.string({
|
|
52
|
+
description: "where the action is declared: list or record (a view's actions), bulk (a view's bulk actions), field (an action field) or settings (a settings action)",
|
|
53
|
+
options: ['list', 'record', 'bulk', 'field', 'settings'],
|
|
54
|
+
}),
|
|
55
|
+
values: Flags.string({
|
|
56
|
+
description: "values of the action's modal fields, as JSON",
|
|
57
|
+
}),
|
|
58
|
+
view: Flags.string({
|
|
59
|
+
description: 'id of the content view that declares the action',
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
static summary = 'Run an app action from the command line.';
|
|
63
|
+
async run() {
|
|
64
|
+
const { args, flags } = await this.parse(AppAction);
|
|
65
|
+
const values = flags.values === undefined ? undefined : this.parseValues(flags.values);
|
|
66
|
+
if (!flags.live) {
|
|
67
|
+
await this.api.setEnv('test');
|
|
68
|
+
}
|
|
69
|
+
this.app = await this.getAppWithConfig();
|
|
70
|
+
const action = await this.findAction(args.id, flags);
|
|
71
|
+
const $action = this.getActionParams(action, { ...flags, values });
|
|
72
|
+
const functionId = await this.findFunctionId(action.function);
|
|
73
|
+
const name = style.appConfigValue(action.id);
|
|
74
|
+
const spinner = ora();
|
|
75
|
+
spinner.start(`Running action ${name}...`);
|
|
76
|
+
const result = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/data/:functions/${functionId}` }, { body: { $action } }), () => spinner.fail(`Action ${action.id} failed`));
|
|
77
|
+
if (!(result?.status >= 200 && result.status < 300)) {
|
|
78
|
+
spinner.fail(`Action ${name} failed`);
|
|
79
|
+
this.error(getErrorMessage(result, name));
|
|
80
|
+
}
|
|
81
|
+
// A workflow runs in the background, so the platform answers with the run
|
|
82
|
+
// it started
|
|
83
|
+
const run = result.response?.run;
|
|
84
|
+
if (result.status === 202 && run?.id && run.workflow_name) {
|
|
85
|
+
spinner.succeed(`Started run ${style.appConfigValue(run.id)} of workflow ${style.appConfigValue(run.workflow_name)}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
spinner.succeed(`Ran action ${name}`);
|
|
89
|
+
if (result.response !== undefined) {
|
|
90
|
+
this.log(JSON.stringify(result.response, null, 2));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async findAction(id, flags) {
|
|
94
|
+
const [contents, settings] = await Promise.all([
|
|
95
|
+
this.api.get({ adminPath: '/data/:content' }, { query: { app_id: this.app.id, limit: 1000 } }),
|
|
96
|
+
this.api.get({ adminPath: `/data/:settings/${this.app.id}` }),
|
|
97
|
+
]);
|
|
98
|
+
const actions = [
|
|
99
|
+
...(contents?.results || []).flatMap((content) => getContentActions(content).map((action) => ({
|
|
100
|
+
...action,
|
|
101
|
+
collection: content.collection,
|
|
102
|
+
content_id: content.id,
|
|
103
|
+
}))),
|
|
104
|
+
...Object.entries(settings?.content || {}).flatMap(([name, values]) => getSettingsActions(values, name)),
|
|
105
|
+
];
|
|
106
|
+
// Content of the app's own model has the collection apps/<app id>/<model>
|
|
107
|
+
const appCollection = `apps/${this.app.id}/`;
|
|
108
|
+
const matches = actions.filter((action) => action.id === id &&
|
|
109
|
+
(!flags.collection ||
|
|
110
|
+
action.collection === flags.collection ||
|
|
111
|
+
action.collection === `${appCollection}${flags.collection}`) &&
|
|
112
|
+
(!flags.view || action.view === flags.view) &&
|
|
113
|
+
(!flags.settings || action.settings === flags.settings) &&
|
|
114
|
+
(!flags.source || action.source === flags.source));
|
|
115
|
+
if (matches.length === 0) {
|
|
116
|
+
this.error(`Action ${style.appConfigValue(id)} not found in the app's content or settings. Push the app first if it is new.`);
|
|
117
|
+
}
|
|
118
|
+
if (matches.length > 1) {
|
|
119
|
+
this.error(`Action ${style.appConfigValue(id)} is declared in more than one place:\n${matches
|
|
120
|
+
.map((action) => ` ${describePlace(action, appCollection)}`)
|
|
121
|
+
.join('\n')}\nPick one with --collection, --view, --settings or --source.`);
|
|
122
|
+
}
|
|
123
|
+
return matches[0];
|
|
124
|
+
}
|
|
125
|
+
async findFunctionId(name) {
|
|
126
|
+
const functions = await this.api.get({ adminPath: '/data/:functions' }, { query: { app_id: this.app.id, fields: 'id, name', limit: 1000 } });
|
|
127
|
+
const record = (functions?.results || []).find((record) => functionSlug(record.name) === functionSlug(name));
|
|
128
|
+
if (!record) {
|
|
129
|
+
this.error(`Function ${style.appConfigValue(name)} of the action is not installed. Push the app first.`);
|
|
130
|
+
}
|
|
131
|
+
return record.id;
|
|
132
|
+
}
|
|
133
|
+
getActionParams(action, flags) {
|
|
134
|
+
const name = style.appConfigValue(action.id);
|
|
135
|
+
const isBulk = action.source === 'bulk';
|
|
136
|
+
const needsRecord = action.source === 'record' ||
|
|
137
|
+
(action.source === 'field' && !action.settings);
|
|
138
|
+
if (needsRecord && !flags.record) {
|
|
139
|
+
this.error(`Action ${name} runs on a record: pass --record.`);
|
|
140
|
+
}
|
|
141
|
+
if (!needsRecord && flags.record) {
|
|
142
|
+
this.error(isBulk
|
|
143
|
+
? `Action ${name} is a bulk action, which runs on --ids or --all: leave out --record.`
|
|
144
|
+
: `Action ${name} does not run on a record: leave out --record.`);
|
|
145
|
+
}
|
|
146
|
+
if (isBulk && !flags.ids && !flags.all) {
|
|
147
|
+
this.error(`Action ${name} is a bulk action: pass --ids or --all.`);
|
|
148
|
+
}
|
|
149
|
+
if (!isBulk && (flags.ids || flags.all)) {
|
|
150
|
+
this.error(`Action ${name} is not a bulk action: leave out ${flags.ids ? '--ids' : '--all'}.`);
|
|
151
|
+
}
|
|
152
|
+
// The platform passes on only the values of the modal's fields
|
|
153
|
+
if (flags.values) {
|
|
154
|
+
if (!action.modal) {
|
|
155
|
+
this.error(`Action ${name} has no modal: leave out --values.`);
|
|
156
|
+
}
|
|
157
|
+
const fieldIds = getModalFieldIds(action.modal);
|
|
158
|
+
const unknownKeys = Object.keys(flags.values).filter((key) => !fieldIds.includes(key));
|
|
159
|
+
if (unknownKeys.length > 0) {
|
|
160
|
+
this.error(`--values names fields that the modal of action ${name} does not have: ${unknownKeys.join(', ')}.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
id: action.id,
|
|
165
|
+
source: action.source,
|
|
166
|
+
...(action.settings
|
|
167
|
+
? { settings: action.settings }
|
|
168
|
+
: { content_id: action.content_id }),
|
|
169
|
+
...(action.view ? { view: action.view } : undefined),
|
|
170
|
+
...(needsRecord ? { record_id: flags.record } : undefined),
|
|
171
|
+
...(isBulk
|
|
172
|
+
? { selection: getSelection(flags.ids, action.query) }
|
|
173
|
+
: undefined),
|
|
174
|
+
...(flags.values ? { values: flags.values } : undefined),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
parseValues(values) {
|
|
178
|
+
let parsed;
|
|
179
|
+
try {
|
|
180
|
+
parsed = JSON.parse(values);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// ignore
|
|
184
|
+
}
|
|
185
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
186
|
+
this.error('--values must be a JSON object of the modal field values, like \'{"carrier":"ups"}\'');
|
|
187
|
+
}
|
|
188
|
+
return parsed;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// The function answers as it likes, so only a string is a message
|
|
192
|
+
function resultMessage(message) {
|
|
193
|
+
return typeof message === 'string' ? message : '';
|
|
194
|
+
}
|
|
195
|
+
// As the dashboard reads a result that did not succeed
|
|
196
|
+
function getErrorMessage(result, name) {
|
|
197
|
+
const { code, response } = result || {};
|
|
198
|
+
if (code === 'timeout') {
|
|
199
|
+
return `Action ${name} did not finish within the time limit. It may still be running; check before running it again.`;
|
|
200
|
+
}
|
|
201
|
+
if (UNAVAILABLE_CODES.has(code)) {
|
|
202
|
+
return 'The app function is unavailable.';
|
|
203
|
+
}
|
|
204
|
+
return (resultMessage(response?.$reject?.message) ||
|
|
205
|
+
resultMessage(response?.error?.message) ||
|
|
206
|
+
resultMessage(response?.error) ||
|
|
207
|
+
`Action ${name} failed`);
|
|
208
|
+
}
|
|
209
|
+
function describePlace(action, appCollection) {
|
|
210
|
+
const collection = action.collection?.startsWith(appCollection)
|
|
211
|
+
? action.collection.slice(appCollection.length)
|
|
212
|
+
: action.collection;
|
|
213
|
+
return action.settings
|
|
214
|
+
? `settings ${action.settings} (${action.source})`
|
|
215
|
+
: `content ${collection}${action.view ? `, view ${action.view}` : ''} (${action.source})`;
|
|
216
|
+
}
|
|
217
|
+
// As the dashboard selects records: by their ids, or all of them that the
|
|
218
|
+
// view lists with no filters set
|
|
219
|
+
function getSelection(ids, viewQuery = {}) {
|
|
220
|
+
if (!ids) {
|
|
221
|
+
return { all: true, count: null, query: viewQuery };
|
|
222
|
+
}
|
|
223
|
+
const list = ids
|
|
224
|
+
.split(',')
|
|
225
|
+
.map((id) => id.trim())
|
|
226
|
+
.filter(Boolean);
|
|
227
|
+
return {
|
|
228
|
+
all: false,
|
|
229
|
+
count: list.length,
|
|
230
|
+
ids: list,
|
|
231
|
+
query: { $and: [{ id: { $in: list } }] },
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -31,6 +31,10 @@ If deployment fails, correct any reported errors and run this command again.`;
|
|
|
31
31
|
env: 'test',
|
|
32
32
|
};
|
|
33
33
|
static summary = `Push frontend files to your Swell app.`;
|
|
34
|
+
// A deploy pushes no app files, so it applies nothing of the listing
|
|
35
|
+
pushesSwellJson() {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
34
38
|
async run() {
|
|
35
39
|
const { flags } = await this.parse(AppFrontendDeploy);
|
|
36
40
|
const { force } = flags;
|
|
@@ -28,5 +28,7 @@ export default class AppPush extends PushAppCommand {
|
|
|
28
28
|
*/
|
|
29
29
|
confirmPushConflicts(conflictsFlag?: string, file?: string): Promise<boolean>;
|
|
30
30
|
run(): Promise<void>;
|
|
31
|
+
protected pushesSwellJson(file?: string): boolean;
|
|
32
|
+
private checkActionFunctions;
|
|
31
33
|
private exitIfFilesFailed;
|
|
32
34
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { functionSlug, getContentActions, getSettingsActions, } from '../../lib/apps/actions.js';
|
|
5
|
+
import { affectsFrontendDeployment, AllConfigPaths, filePathExistsAsync, isPathDirectory, sameIntegrations, toPosixPath, } from '../../lib/apps/index.js';
|
|
4
6
|
import { default as localConfig } from '../../lib/config.js';
|
|
5
7
|
import style from '../../lib/style.js';
|
|
6
8
|
import { PushAppCommand } from '../../push-app-command.js';
|
|
@@ -19,7 +21,7 @@ If the app does not exist, it will be created and its global ID saved to a .swel
|
|
|
19
21
|
|
|
20
22
|
- If no file is specified, all configuration files will be pushed to the store.
|
|
21
23
|
This includes the app icon (assets/icon.png), screenshots (assets/screenshots/),
|
|
22
|
-
|
|
24
|
+
DESCRIPTION.md, and swell.json.
|
|
23
25
|
- If a file is specified, only that file will be pushed to the store.
|
|
24
26
|
- If a directory is specified, only files in that directory will be pushed.
|
|
25
27
|
|
|
@@ -151,6 +153,7 @@ ${Object.values(AllConfigPaths)
|
|
|
151
153
|
if (!noDeploy && affectsFrontendDeployment(relativePath)) {
|
|
152
154
|
await this.deployAppFrontend(force);
|
|
153
155
|
}
|
|
156
|
+
await this.checkActionFunctions(toPosixPath(relativePath));
|
|
154
157
|
this.log();
|
|
155
158
|
this.logDashboardUrl(flags);
|
|
156
159
|
this.exitIfFilesFailed();
|
|
@@ -158,6 +161,7 @@ ${Object.values(AllConfigPaths)
|
|
|
158
161
|
}
|
|
159
162
|
}
|
|
160
163
|
await this.pushAppConfigs(force);
|
|
164
|
+
await this.checkActionFunctions();
|
|
161
165
|
if (!noDeploy) {
|
|
162
166
|
await this.deployAppFrontend(force);
|
|
163
167
|
this.log();
|
|
@@ -165,6 +169,63 @@ ${Object.values(AllConfigPaths)
|
|
|
165
169
|
this.logDashboardUrl(flags);
|
|
166
170
|
this.exitIfFilesFailed();
|
|
167
171
|
}
|
|
172
|
+
// The app path is the working directory unless --app-path names it, so a
|
|
173
|
+
// scoped push's file resolves here as in resolvePushPaths
|
|
174
|
+
pushesSwellJson(file) {
|
|
175
|
+
if (!file) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
const relativePath = path.relative(this.appPath, path.resolve(this.appPath, file));
|
|
179
|
+
return relativePath === '' || relativePath === 'swell.json';
|
|
180
|
+
}
|
|
181
|
+
// Content and settings are installed before functions, so their actions
|
|
182
|
+
// are checked against the app's functions once the push is done. A scoped
|
|
183
|
+
// push fails only for the files it pushed, and for the files whose action
|
|
184
|
+
// function it pushed or removed.
|
|
185
|
+
async checkActionFunctions(scope) {
|
|
186
|
+
const configs = await this.api
|
|
187
|
+
.getAll({ adminPath: `/apps/${this.app.id}/configs` }, { query: { type: { $in: ['function', 'content', 'setting'] } } })
|
|
188
|
+
.catch((error) => {
|
|
189
|
+
ora().warn(`Skipped checking the actions against the app functions: ${error.message}`);
|
|
190
|
+
return null;
|
|
191
|
+
});
|
|
192
|
+
if (!configs) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const { results } = configs;
|
|
196
|
+
const actionFunctions = new Set(results
|
|
197
|
+
.filter((config) => config.type === 'function' && config.values?.action)
|
|
198
|
+
.map((config) => functionSlug(config.name)));
|
|
199
|
+
const inScope = (filePath) => !scope || filePath === scope || filePath?.startsWith(`${scope}/`);
|
|
200
|
+
// The app's configs from before the push still name the functions it
|
|
201
|
+
// removed
|
|
202
|
+
const pushedFunctions = new Set([...results, ...(this.app.configs || [])]
|
|
203
|
+
.filter((config) => config.type === 'function' && inScope(config.file_path))
|
|
204
|
+
.map((config) => functionSlug(config.name)));
|
|
205
|
+
for (const config of results) {
|
|
206
|
+
const actions = config.type === 'content'
|
|
207
|
+
? getContentActions(config.values)
|
|
208
|
+
: config.type === 'setting'
|
|
209
|
+
? getSettingsActions(config.values, config.name)
|
|
210
|
+
: [];
|
|
211
|
+
const missing = actions.filter((action) => !actionFunctions.has(functionSlug(action.function)));
|
|
212
|
+
let failed = false;
|
|
213
|
+
for (const action of missing) {
|
|
214
|
+
const message = `${config.file_path}: action "${action.id}" runs function "${action.function}", which the app does not have as an action function`;
|
|
215
|
+
if (inScope(config.file_path) ||
|
|
216
|
+
pushedFunctions.has(functionSlug(action.function))) {
|
|
217
|
+
ora().fail(message);
|
|
218
|
+
failed = true;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
ora().warn(message);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (failed && !this.failedFiles.includes(config.file_path)) {
|
|
225
|
+
this.failedFiles.push(config.file_path);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
168
229
|
// A skipped file is a failed push, for scripts and CI too
|
|
169
230
|
exitIfFilesFailed() {
|
|
170
231
|
if (this.failedFiles.length > 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { input } from '@inquirer/prompts';
|
|
2
2
|
import { Args, Flags } from '@oclif/core';
|
|
3
3
|
import { CreateConfigCommand } from '../../create-config-command.js';
|
|
4
4
|
import { ConfigType } from '../../lib/apps/index.js';
|
|
@@ -16,7 +16,7 @@ export default class CreateWebhook extends CreateConfigCommand {
|
|
|
16
16
|
static examples = [
|
|
17
17
|
'$ swell create webhook',
|
|
18
18
|
'$ swell create webhook order-sync -u https://example.com/hook -e order.created -y',
|
|
19
|
-
'$ swell create webhook payment-handler -u https://api.example.com/payments -e payment.succeeded,payment.failed --enabled -y',
|
|
19
|
+
'$ swell create webhook payment-handler -u https://api.example.com/payments -e payment.succeeded,payment.failed --no-enabled -y',
|
|
20
20
|
];
|
|
21
21
|
static helpMeta = {
|
|
22
22
|
usageDirect: '<name> -u <url> -e <events> [...] -y',
|
|
@@ -38,8 +38,9 @@ export default class CreateWebhook extends CreateConfigCommand {
|
|
|
38
38
|
description: 'Description',
|
|
39
39
|
}),
|
|
40
40
|
enabled: Flags.boolean({
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
allowNo: true,
|
|
42
|
+
default: true,
|
|
43
|
+
description: 'Enable webhook (default: true)',
|
|
43
44
|
}),
|
|
44
45
|
overwrite: Flags.boolean({
|
|
45
46
|
default: false,
|
|
@@ -148,21 +149,14 @@ export default class CreateWebhook extends CreateConfigCommand {
|
|
|
148
149
|
events = parseEvents(eventsInput);
|
|
149
150
|
}
|
|
150
151
|
// Description prompt (optional)
|
|
151
|
-
let { description
|
|
152
|
-
const { overwrite } = flags;
|
|
152
|
+
let { description } = flags;
|
|
153
|
+
const { enabled, overwrite } = flags;
|
|
153
154
|
if (!description) {
|
|
154
155
|
description = await input({
|
|
155
156
|
default: '',
|
|
156
157
|
message: 'Describe what this webhook does',
|
|
157
158
|
});
|
|
158
159
|
}
|
|
159
|
-
// Enabled prompt (optional)
|
|
160
|
-
if (!enabled) {
|
|
161
|
-
enabled = await confirm({
|
|
162
|
-
default: false,
|
|
163
|
-
message: 'Enable webhook now?',
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
160
|
const fileName = toFileName(name);
|
|
167
161
|
const fileBody = {
|
|
168
162
|
$schema: SCHEMAS.WEBHOOK,
|
package/dist/commands/schema.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as fs from 'node:fs/promises';
|
|
|
6
6
|
import parseJson from 'parse-json';
|
|
7
7
|
import { bundleFunction } from '../lib/bundle.js';
|
|
8
8
|
import { SCHEMAS_BASE_URL } from '../lib/constants.js';
|
|
9
|
+
import { getFunctionDeclarations } from '../lib/function-declarations.js';
|
|
9
10
|
import { analyzeFunctionSource, getFunctionTriggers, hasKindDiagnostic, workflowStaticConfigError, } from '../lib/function-source-analysis.js';
|
|
10
11
|
import { SwellCommand } from '../swell-command.js';
|
|
11
12
|
const SCHEMA_DEFINITIONS = Object.freeze({
|
|
@@ -164,6 +165,10 @@ export default class Schema extends SwellCommand {
|
|
|
164
165
|
break;
|
|
165
166
|
}
|
|
166
167
|
case 'dts': {
|
|
168
|
+
if (type === 'function') {
|
|
169
|
+
output = await getFunctionDeclarations(process.cwd());
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
167
172
|
const dtsUrl = this.getTypeScriptDeclarationsUrl(type);
|
|
168
173
|
const response = await fetch(dtsUrl);
|
|
169
174
|
if (!response.ok) {
|
package/dist/help/custom-help.js
CHANGED
|
@@ -280,7 +280,11 @@ export default class CustomHelp extends Help {
|
|
|
280
280
|
const value = ('helpValue' in flag && flag.helpValue) ||
|
|
281
281
|
('options' in flag && flag.options ? 'option' : 'value');
|
|
282
282
|
const suffix = flag.type === 'boolean' ? '' : `=<${value}>`;
|
|
283
|
-
|
|
283
|
+
const negatable = flag.type === 'boolean' && flag.allowNo;
|
|
284
|
+
return [
|
|
285
|
+
name,
|
|
286
|
+
`${shortChar}--${negatable ? '[no-]' : ''}${name}${suffix}`,
|
|
287
|
+
];
|
|
284
288
|
}));
|
|
285
289
|
const flagWidth = Math.max(30, ...[...flagLabels.values()].map((label) => label.length));
|
|
286
290
|
for (const [name, flag] of flagEntries) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type Values = Record<string, any>;
|
|
2
|
+
export type ActionSource = 'bulk' | 'field' | 'list' | 'record' | 'settings';
|
|
3
|
+
/** An app function action, with what the dashboard sends to run it */
|
|
4
|
+
export interface DeclaredAction {
|
|
5
|
+
function: string;
|
|
6
|
+
id: string;
|
|
7
|
+
modal?: Values;
|
|
8
|
+
/** The query of the view whose records a bulk action runs on */
|
|
9
|
+
query?: Values;
|
|
10
|
+
/** The name of the settings config, for a settings action or action field */
|
|
11
|
+
settings?: string;
|
|
12
|
+
source: ActionSource;
|
|
13
|
+
view?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The function actions a content config declares.
|
|
17
|
+
*
|
|
18
|
+
* @param values the values of the content config
|
|
19
|
+
* @returns the actions
|
|
20
|
+
*/
|
|
21
|
+
export declare function getContentActions(values: Values): DeclaredAction[];
|
|
22
|
+
/**
|
|
23
|
+
* The function actions a settings config declares.
|
|
24
|
+
*
|
|
25
|
+
* @param values the values of the settings config
|
|
26
|
+
* @param settings the name of the settings config
|
|
27
|
+
* @returns the actions
|
|
28
|
+
*/
|
|
29
|
+
export declare function getSettingsActions(values: Values, settings: string): DeclaredAction[];
|
|
30
|
+
/**
|
|
31
|
+
* The ids of the fields of an action's modal, as the platform finds them to
|
|
32
|
+
* pass on only their values.
|
|
33
|
+
*
|
|
34
|
+
* @param modal the modal of the action
|
|
35
|
+
* @returns the field ids
|
|
36
|
+
*/
|
|
37
|
+
export declare function getModalFieldIds(modal: Values): string[];
|
|
38
|
+
/**
|
|
39
|
+
* A function name as the platform compares an action's `function` with it.
|
|
40
|
+
*
|
|
41
|
+
* @param name a function name
|
|
42
|
+
* @returns the name in the form the platform compares
|
|
43
|
+
*/
|
|
44
|
+
export declare function functionSlug(name: string): string;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
function isFunctionAction(item) {
|
|
2
|
+
return (typeof item?.id === 'string' &&
|
|
3
|
+
typeof item?.function === 'string');
|
|
4
|
+
}
|
|
5
|
+
// The dashboard renders the fields of rows and groups in place
|
|
6
|
+
function flattenFields(fields) {
|
|
7
|
+
if (!Array.isArray(fields)) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
return fields.flatMap((field) => field?.type === 'field_row' || field?.type === 'field_group'
|
|
11
|
+
? flattenFields(field.fields)
|
|
12
|
+
: [field]);
|
|
13
|
+
}
|
|
14
|
+
function getActionFields(fields) {
|
|
15
|
+
return flattenFields(fields).filter((field) => field?.type === 'action');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The function actions a content config declares.
|
|
19
|
+
*
|
|
20
|
+
* @param values the values of the content config
|
|
21
|
+
* @returns the actions
|
|
22
|
+
*/
|
|
23
|
+
export function getContentActions(values) {
|
|
24
|
+
const views = Array.isArray(values?.views) ? values.views : [];
|
|
25
|
+
const actions = [];
|
|
26
|
+
for (const view of views) {
|
|
27
|
+
const source = view.type || (view.id === 'list' ? 'list' : 'record');
|
|
28
|
+
for (const action of [
|
|
29
|
+
...(view.actions || []),
|
|
30
|
+
...(view.extra_actions || []),
|
|
31
|
+
]) {
|
|
32
|
+
if (isFunctionAction(action)) {
|
|
33
|
+
actions.push({ ...pickAction(action), source, view: view.id });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const action of view.bulk_actions || []) {
|
|
37
|
+
if (isFunctionAction(action)) {
|
|
38
|
+
actions.push({
|
|
39
|
+
...pickAction(action),
|
|
40
|
+
...(view.query ? { query: view.query } : undefined),
|
|
41
|
+
source: 'bulk',
|
|
42
|
+
view: view.id,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const fields = [
|
|
48
|
+
...(values?.fields || []),
|
|
49
|
+
...views.flatMap((view) => [
|
|
50
|
+
...(view.fields || []),
|
|
51
|
+
...(view.tabs || []).flatMap((tab) => tab?.fields || []),
|
|
52
|
+
]),
|
|
53
|
+
];
|
|
54
|
+
for (const field of getActionFields(fields)) {
|
|
55
|
+
if (isFunctionAction(field)) {
|
|
56
|
+
actions.push({ ...pickAction(field), source: 'field' });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return actions;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The function actions a settings config declares.
|
|
63
|
+
*
|
|
64
|
+
* @param values the values of the settings config
|
|
65
|
+
* @param settings the name of the settings config
|
|
66
|
+
* @returns the actions
|
|
67
|
+
*/
|
|
68
|
+
export function getSettingsActions(values, settings) {
|
|
69
|
+
const actions = [];
|
|
70
|
+
for (const action of values?.actions || []) {
|
|
71
|
+
if (isFunctionAction(action)) {
|
|
72
|
+
actions.push({ ...pickAction(action), settings, source: 'settings' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const field of getActionFields(values?.fields)) {
|
|
76
|
+
if (isFunctionAction(field)) {
|
|
77
|
+
actions.push({ ...pickAction(field), settings, source: 'field' });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return actions;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The ids of the fields of an action's modal, as the platform finds them to
|
|
84
|
+
* pass on only their values.
|
|
85
|
+
*
|
|
86
|
+
* @param modal the modal of the action
|
|
87
|
+
* @returns the field ids
|
|
88
|
+
*/
|
|
89
|
+
export function getModalFieldIds(modal) {
|
|
90
|
+
return flattenFields(modal.fields)
|
|
91
|
+
.map((field) => field?.id)
|
|
92
|
+
.filter((id) => typeof id === 'string');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A function name as the platform compares an action's `function` with it.
|
|
96
|
+
*
|
|
97
|
+
* @param name a function name
|
|
98
|
+
* @returns the name in the form the platform compares
|
|
99
|
+
*/
|
|
100
|
+
export function functionSlug(name) {
|
|
101
|
+
return name
|
|
102
|
+
.replaceAll(/[^\s\w./:-]/g, '')
|
|
103
|
+
.replaceAll(/[\s./:_-]+/g, '-')
|
|
104
|
+
.replaceAll(/^-+|-+$/g, '')
|
|
105
|
+
.replaceAll(/([a-z])([A-Z])/g, '$1-$2')
|
|
106
|
+
.toLowerCase();
|
|
107
|
+
}
|
|
108
|
+
function pickAction(action) {
|
|
109
|
+
return {
|
|
110
|
+
function: action.function,
|
|
111
|
+
id: action.id,
|
|
112
|
+
...(action.modal ? { modal: action.modal } : undefined),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function warnListingSources(appPath: string, config: Record<string, any>, warn: (message: string) => void): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
const LISTING_SOURCE_FIELDS = ['logo_src', 'preview_src', 'preview_mobile_src'];
|
|
4
|
+
const LISTING_SOURCE_LISTS = ['images', 'highlights'];
|
|
5
|
+
// The listing takes its full description from DESCRIPTION.md and each image
|
|
6
|
+
// from the asset whose path from the app root is the swell.json value as
|
|
7
|
+
// written, so values that point elsewhere do nothing. The platform takes an
|
|
8
|
+
// empty value as unset.
|
|
9
|
+
export function warnListingSources(appPath, config, warn) {
|
|
10
|
+
if (config.full_description !== undefined) {
|
|
11
|
+
warn('swell.json sets full_description, which push does not apply: the full description is the content of DESCRIPTION.md at the app root.');
|
|
12
|
+
}
|
|
13
|
+
const sources = [
|
|
14
|
+
...LISTING_SOURCE_FIELDS.map((field) => [
|
|
15
|
+
field,
|
|
16
|
+
config[field],
|
|
17
|
+
]),
|
|
18
|
+
...LISTING_SOURCE_LISTS.flatMap((list) => (Array.isArray(config[list]) ? config[list] : []).map((item, index) => [
|
|
19
|
+
`${list}[${index}].image_src`,
|
|
20
|
+
item?.image_src,
|
|
21
|
+
])),
|
|
22
|
+
];
|
|
23
|
+
for (const [field, source] of sources) {
|
|
24
|
+
if (typeof source !== 'string' || source === '') {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const filePath = path.posix.normalize(source.replaceAll('\\', '/'));
|
|
28
|
+
const stats = statSync(path.join(appPath, filePath), {
|
|
29
|
+
throwIfNoEntry: false,
|
|
30
|
+
});
|
|
31
|
+
if (!stats?.isFile()) {
|
|
32
|
+
warn(`swell.json ${field} names ${source}, which the app does not have: the listing gets no image from it.`);
|
|
33
|
+
}
|
|
34
|
+
else if (!filePath.startsWith('assets/')) {
|
|
35
|
+
warn(`swell.json ${field} names ${source}, which is not in assets/: the listing gets no image from it. Move the file to assets/ and write assets/${path.posix.basename(filePath)}.`);
|
|
36
|
+
}
|
|
37
|
+
else if (filePath !== source) {
|
|
38
|
+
warn(`swell.json ${field} names ${source}, which is not the file's path from the app root: the listing gets no image from it. Write ${filePath}.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
package/dist/lib/bundle.js
CHANGED
|
@@ -60,7 +60,7 @@ export async function bundleFunction(filePath) {
|
|
|
60
60
|
}
|
|
61
61
|
export async function bundleWorkflow(filePath, analysis) {
|
|
62
62
|
try {
|
|
63
|
-
const entrySource = getSwellWorkflowEntrypoint(filePath);
|
|
63
|
+
const entrySource = await getSwellWorkflowEntrypoint(filePath);
|
|
64
64
|
const buildResult = await esbuild.build({
|
|
65
65
|
bundle: true,
|
|
66
66
|
stdin: {
|
|
@@ -130,13 +130,22 @@ async function getSwellFunctionWrapper() {
|
|
|
130
130
|
const __dirname = path.dirname(__filename);
|
|
131
131
|
return fs.readFile(path.join(__dirname, './swell-function-wrapper.js'), 'utf8');
|
|
132
132
|
}
|
|
133
|
-
|
|
133
|
+
// The entry defines these functions itself, so the bundle names no file of
|
|
134
|
+
// the CLI and the workflow module exports only its entrypoint
|
|
135
|
+
async function getSwellWorkflowRequest() {
|
|
136
|
+
const source = await fs.readFile(path.join(path.dirname(fileURLToPath(import.meta.url)), './swell-workflow-request.js'), 'utf8');
|
|
137
|
+
return source.replaceAll(/^export /gm, '');
|
|
138
|
+
}
|
|
139
|
+
async function getSwellWorkflowEntrypoint(filePath) {
|
|
134
140
|
const workflowImport = `./${path.basename(filePath)}`;
|
|
141
|
+
const workflowRequest = await getSwellWorkflowRequest();
|
|
135
142
|
return `
|
|
136
143
|
import { WorkflowEntrypoint } from 'cloudflare:workers';
|
|
137
144
|
import { NonRetryableError } from 'cloudflare:workflows';
|
|
138
145
|
import UserWorkflow from ${JSON.stringify(workflowImport)};
|
|
139
146
|
|
|
147
|
+
${workflowRequest}
|
|
148
|
+
|
|
140
149
|
const RUNTIME_PROXY_PATH = '/:workflows/runtime/request';
|
|
141
150
|
const RUNTIME_TOKEN_PATTERN = /\\bwf_rt_[0-9A-Za-z]+_[0-9A-Za-z]{12,}\\b/g;
|
|
142
151
|
|
|
@@ -210,38 +219,6 @@ function getInvocation(event) {
|
|
|
210
219
|
return payload;
|
|
211
220
|
}
|
|
212
221
|
|
|
213
|
-
function buildWorkflowRequest(invocation, proxy) {
|
|
214
|
-
const runtime = invocation.runtime;
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
id: runtime.request_id,
|
|
218
|
-
appId: runtime.app_slug || runtime.app_id,
|
|
219
|
-
environmentId: runtimeEnvironmentId(runtime),
|
|
220
|
-
store: {
|
|
221
|
-
id: runtime.store_id,
|
|
222
|
-
},
|
|
223
|
-
app: {
|
|
224
|
-
admin_url: runtime.app_admin_url,
|
|
225
|
-
},
|
|
226
|
-
data: invocation.data,
|
|
227
|
-
workflow: {
|
|
228
|
-
workflow_id: runtime.workflow_id,
|
|
229
|
-
workflow_name: runtime.workflow_name,
|
|
230
|
-
workflow_instance_id: runtime.workflow_instance_id,
|
|
231
|
-
trigger: runtime.trigger,
|
|
232
|
-
request_id: runtime.request_id,
|
|
233
|
-
},
|
|
234
|
-
isLocalDev: false,
|
|
235
|
-
swell: {
|
|
236
|
-
get: (path, data) => proxy.api('get', path, data),
|
|
237
|
-
post: (path, data) => proxy.api('post', path, data),
|
|
238
|
-
put: (path, data) => proxy.api('put', path, data),
|
|
239
|
-
delete: (path, data) => proxy.api('delete', path, data),
|
|
240
|
-
settings: () => proxy.settings(),
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
222
|
function buildWorkflowStep(cloudflareStep, proxy) {
|
|
246
223
|
return Object.freeze({
|
|
247
224
|
do(name, optionsOrCallback, maybeCallback) {
|
|
@@ -361,11 +338,6 @@ function getRuntimeApiHost(runtime) {
|
|
|
361
338
|
return apiHost.replace(/\\/$/, '');
|
|
362
339
|
}
|
|
363
340
|
|
|
364
|
-
// 'test' in the test environment, null in live
|
|
365
|
-
function runtimeEnvironmentId(runtime) {
|
|
366
|
-
return runtime.environment_id || null;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
341
|
function runtimeIdentity(runtime) {
|
|
370
342
|
return {
|
|
371
343
|
app_id: runtime.app_id,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The types an app's functions type-check against: the app's own
|
|
3
|
+
* @swell/app-types when it has the package, otherwise the CLI's.
|
|
4
|
+
*
|
|
5
|
+
* @param directory a directory of the app
|
|
6
|
+
* @returns the declarations, after a line naming the package version
|
|
7
|
+
*/
|
|
8
|
+
export declare function getFunctionDeclarations(directory: string): Promise<string>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* The types an app's functions type-check against: the app's own
|
|
6
|
+
* @swell/app-types when it has the package, otherwise the CLI's.
|
|
7
|
+
*
|
|
8
|
+
* @param directory a directory of the app
|
|
9
|
+
* @returns the declarations, after a line naming the package version
|
|
10
|
+
*/
|
|
11
|
+
export async function getFunctionDeclarations(directory) {
|
|
12
|
+
const file = resolveAppTypes(path.join(directory, 'package.json')) ??
|
|
13
|
+
resolveAppTypes(import.meta.url);
|
|
14
|
+
if (!file) {
|
|
15
|
+
throw new Error('@swell/app-types is not installed');
|
|
16
|
+
}
|
|
17
|
+
const { version } = JSON.parse(await fs.readFile(path.join(path.dirname(file), 'package.json'), 'utf8'));
|
|
18
|
+
return `// @swell/app-types ${version}\n${await fs.readFile(file, 'utf8')}`;
|
|
19
|
+
}
|
|
20
|
+
function resolveAppTypes(from) {
|
|
21
|
+
try {
|
|
22
|
+
return createRequire(from).resolve('@swell/app-types/index.d.ts');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -30,7 +30,13 @@ async function request(originalRequest, _env, context) {
|
|
|
30
30
|
...(error.body ?? { error: error.message }),
|
|
31
31
|
retry: error.retry === false ? false : undefined,
|
|
32
32
|
};
|
|
33
|
-
|
|
33
|
+
const retryAfter = Number(error.retryAfter);
|
|
34
|
+
response = new SwellResponse(errorBody, {
|
|
35
|
+
status: error.status || 500,
|
|
36
|
+
...(retryAfter > 0
|
|
37
|
+
? { headers: { 'Retry-After': String(Math.ceil(retryAfter)) } }
|
|
38
|
+
: {}),
|
|
39
|
+
});
|
|
34
40
|
}
|
|
35
41
|
return SwellResponse._respond(req, response, context);
|
|
36
42
|
}
|
|
@@ -447,6 +453,7 @@ class SwellError extends Error {
|
|
|
447
453
|
this.body = body;
|
|
448
454
|
this.code = options.code || body?.error?.code;
|
|
449
455
|
this.retry = options.retry;
|
|
456
|
+
this.retryAfter = options.retryAfter;
|
|
450
457
|
}
|
|
451
458
|
get isRetryable() {
|
|
452
459
|
return RETRYABLE_CODES.has(this.code);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function runtimeEnvironmentId(runtime: any): any;
|
|
2
|
+
export function buildWorkflowRequest(invocation: any, proxy: any): {
|
|
3
|
+
id: any;
|
|
4
|
+
appId: any;
|
|
5
|
+
environmentId: any;
|
|
6
|
+
store: {
|
|
7
|
+
id: any;
|
|
8
|
+
};
|
|
9
|
+
app: {
|
|
10
|
+
admin_url: any;
|
|
11
|
+
frontend_url: any;
|
|
12
|
+
};
|
|
13
|
+
data: any;
|
|
14
|
+
workflow: {
|
|
15
|
+
workflow_id: any;
|
|
16
|
+
workflow_name: any;
|
|
17
|
+
workflow_instance_id: any;
|
|
18
|
+
trigger: any;
|
|
19
|
+
request_id: any;
|
|
20
|
+
};
|
|
21
|
+
isLocalDev: boolean;
|
|
22
|
+
swell: {
|
|
23
|
+
get: (path: any, data: any) => any;
|
|
24
|
+
post: (path: any, data: any) => any;
|
|
25
|
+
put: (path: any, data: any) => any;
|
|
26
|
+
delete: (path: any, data: any) => any;
|
|
27
|
+
settings: () => any;
|
|
28
|
+
};
|
|
29
|
+
appValues(idOrValues: any, values: any): {
|
|
30
|
+
$app: {
|
|
31
|
+
[x: number]: any;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The request a workflow's run(req, step) gets, built from the invocation the
|
|
2
|
+
// platform starts the run with. Every workflow bundle includes this module.
|
|
3
|
+
// 'test' in the test environment, null in live
|
|
4
|
+
export function runtimeEnvironmentId(runtime) {
|
|
5
|
+
return runtime.environment_id || null;
|
|
6
|
+
}
|
|
7
|
+
export function buildWorkflowRequest(invocation, proxy) {
|
|
8
|
+
const { data, runtime } = invocation;
|
|
9
|
+
const appId = runtime.app_slug || runtime.app_id;
|
|
10
|
+
return {
|
|
11
|
+
id: runtime.request_id,
|
|
12
|
+
appId,
|
|
13
|
+
environmentId: runtimeEnvironmentId(runtime),
|
|
14
|
+
store: {
|
|
15
|
+
id: runtime.store_id,
|
|
16
|
+
},
|
|
17
|
+
app: {
|
|
18
|
+
admin_url: runtime.app_admin_url,
|
|
19
|
+
frontend_url: runtime.app_frontend_url,
|
|
20
|
+
},
|
|
21
|
+
data,
|
|
22
|
+
workflow: {
|
|
23
|
+
workflow_id: runtime.workflow_id,
|
|
24
|
+
workflow_name: runtime.workflow_name,
|
|
25
|
+
workflow_instance_id: runtime.workflow_instance_id,
|
|
26
|
+
trigger: runtime.trigger,
|
|
27
|
+
request_id: runtime.request_id,
|
|
28
|
+
},
|
|
29
|
+
isLocalDev: false,
|
|
30
|
+
swell: {
|
|
31
|
+
get: (path, data) => proxy.api('get', path, data),
|
|
32
|
+
post: (path, data) => proxy.api('post', path, data),
|
|
33
|
+
put: (path, data) => proxy.api('put', path, data),
|
|
34
|
+
delete: (path, data) => proxy.api('delete', path, data),
|
|
35
|
+
settings: () => proxy.settings(),
|
|
36
|
+
},
|
|
37
|
+
appValues(idOrValues, values) {
|
|
38
|
+
const targetAppId = typeof idOrValues === 'string' ? idOrValues : appId;
|
|
39
|
+
const appValues = typeof idOrValues === 'string' ? values : idOrValues;
|
|
40
|
+
if (!targetAppId) {
|
|
41
|
+
throw new Error('appValues: missing app id (req.appId is empty)');
|
|
42
|
+
}
|
|
43
|
+
if (!isOrdinaryObject(appValues)) {
|
|
44
|
+
throw new Error('appValues: values must be a plain object (arrays, class instances, null, and primitives are not allowed)');
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
$app: {
|
|
48
|
+
[targetAppId]: appValues,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function isOrdinaryObject(val) {
|
|
55
|
+
return (typeof val === 'object' &&
|
|
56
|
+
val !== null &&
|
|
57
|
+
Object.getPrototypeOf(val) === Object.prototype);
|
|
58
|
+
}
|
|
@@ -21,13 +21,14 @@ export declare abstract class PushAppCommand extends RemoteAppCommand {
|
|
|
21
21
|
watchingIgnoreFilter: GlobbyFilterFunction | null;
|
|
22
22
|
watchingTimer: NodeJS.Timeout | null;
|
|
23
23
|
watchListener: (eventType: fs.WatchEventType, filename: null | string) => Promise<void>;
|
|
24
|
+
protected pushesSwellJson(file?: string): boolean;
|
|
24
25
|
protected showFrontendMigrationError(): never;
|
|
25
26
|
buildFrontend(projectType?: FrontendProjectType): Promise<void>;
|
|
26
27
|
chooseAppToPull(query?: any): Promise<App>;
|
|
27
28
|
createAppStorefront(hasOtherStorefronts?: boolean, nonInteractive?: boolean): Promise<any>;
|
|
28
29
|
deployAppFrontend(force?: boolean, log?: boolean): Promise<void>;
|
|
29
30
|
deployFrontend(projectType: FrontendProjectType): Promise<string>;
|
|
30
|
-
ensureAppExists(file?: string, shouldCreate?: boolean
|
|
31
|
+
ensureAppExists(file?: string, shouldCreate?: boolean): Promise<boolean>;
|
|
31
32
|
ensureLoggedIn(): Promise<void>;
|
|
32
33
|
exec(command: string, cwd?: string, onOutput?: (string: string) => any | false): Promise<void>;
|
|
33
34
|
execFrontend(command: string, onOutput?: (string: string) => any | false): Promise<void>;
|
package/dist/push-app-command.js
CHANGED
|
@@ -10,6 +10,7 @@ import { componentsToRebuild } from './lib/apps/components.js';
|
|
|
10
10
|
import { buildFrontendPackage, getManagedFrontendFramework, } from './lib/apps/frontend-package.js';
|
|
11
11
|
import { warnFrontendRouteCollisions } from './lib/apps/frontend-routes.js';
|
|
12
12
|
import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, CUSTOM_FRAMEWORK_SLUG, appConfigFromFile, filePathExistsAsync, findAppConfig, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashFile, hashString, isPathDirectory, toPosixPath, } from './lib/apps/index.js';
|
|
13
|
+
import { warnListingSources } from './lib/apps/listing.js';
|
|
13
14
|
import { getGlobIgnorePathsChecker } from './lib/apps/paths.js';
|
|
14
15
|
import { slugFromApp } from './lib/apps/slug.js';
|
|
15
16
|
import { getLocalInputs } from './lib/bundle.js';
|
|
@@ -119,6 +120,10 @@ export class PushAppCommand extends RemoteAppCommand {
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
};
|
|
123
|
+
// The platform applies the listing in swell.json when the file is pushed
|
|
124
|
+
pushesSwellJson(file) {
|
|
125
|
+
return !file;
|
|
126
|
+
}
|
|
122
127
|
showFrontendMigrationError() {
|
|
123
128
|
return this.error(style.funcWarn(`⚠️ Your frontend directory exists but appears to use an older structure.\n` +
|
|
124
129
|
`Swell CLI v2.1.0+ requires a Workers-based frontend with package.json.\n\n` +
|
|
@@ -330,7 +335,7 @@ export class PushAppCommand extends RemoteAppCommand {
|
|
|
330
335
|
this.error(`Error deploying frontend: ${error.message}`);
|
|
331
336
|
}
|
|
332
337
|
}
|
|
333
|
-
async ensureAppExists(file, shouldCreate = true
|
|
338
|
+
async ensureAppExists(file, shouldCreate = true) {
|
|
334
339
|
const klass = this.ctor;
|
|
335
340
|
const currentStore = localConfig.getDefaultStore();
|
|
336
341
|
await this.ensureLoggedIn();
|
|
@@ -356,10 +361,13 @@ export class PushAppCommand extends RemoteAppCommand {
|
|
|
356
361
|
this.app = app;
|
|
357
362
|
return true;
|
|
358
363
|
}
|
|
364
|
+
if (this.pushesSwellJson(file)) {
|
|
365
|
+
warnListingSources(this.appPath, this.swellConfig.store, (message) => this.warn(message));
|
|
366
|
+
}
|
|
359
367
|
// If app exists but owned by another client, create a new instance
|
|
360
368
|
if (app?.client_id && app.client_id !== currentStore) {
|
|
361
369
|
// Create a new app
|
|
362
|
-
this.app = await this.getCreateUpdateApp(
|
|
370
|
+
this.app = await this.getCreateUpdateApp();
|
|
363
371
|
if (!this.app) {
|
|
364
372
|
return false;
|
|
365
373
|
}
|
|
@@ -373,7 +381,7 @@ export class PushAppCommand extends RemoteAppCommand {
|
|
|
373
381
|
}
|
|
374
382
|
else {
|
|
375
383
|
// Update existing app if not targeting a file
|
|
376
|
-
this.app = await this.getCreateUpdateApp(app
|
|
384
|
+
this.app = await this.getCreateUpdateApp(app);
|
|
377
385
|
}
|
|
378
386
|
return true;
|
|
379
387
|
}
|
|
@@ -26,7 +26,7 @@ export declare abstract class RemoteAppCommand extends AppCommand {
|
|
|
26
26
|
protected getApp(id?: string): Promise<App>;
|
|
27
27
|
protected getAppWithConfig(id?: string): Promise<App>;
|
|
28
28
|
getConfigData(configId: string): Promise<any>;
|
|
29
|
-
protected getCreateUpdateApp(updateApp?: App | null
|
|
29
|
+
protected getCreateUpdateApp(updateApp?: App | null): Promise<App>;
|
|
30
30
|
protected getInstalledApp(): Promise<any>;
|
|
31
31
|
/**
|
|
32
32
|
* Anything on the target store already using the services named by
|
|
@@ -176,7 +176,7 @@ export class RemoteAppCommand extends AppCommand {
|
|
|
176
176
|
adminPath: `/apps/${this.app.id}/configs/${configId}/data`,
|
|
177
177
|
}, { query: { ...(themeId ? { theme_id: themeId } : undefined) } });
|
|
178
178
|
}
|
|
179
|
-
async getCreateUpdateApp(updateApp
|
|
179
|
+
async getCreateUpdateApp(updateApp) {
|
|
180
180
|
const spinner = ora();
|
|
181
181
|
let sourceApp;
|
|
182
182
|
try {
|
|
@@ -205,9 +205,6 @@ export class RemoteAppCommand extends AppCommand {
|
|
|
205
205
|
? { private_id: this.swellConfig.store.id }
|
|
206
206
|
: undefined),
|
|
207
207
|
...(sourceApp?.id ? { source_id: sourceApp.id } : undefined),
|
|
208
|
-
// Overwrite name/description/logo_icon on the server
|
|
209
|
-
// instead of only filling them in when empty
|
|
210
|
-
...(force ? { $force_meta: true } : undefined),
|
|
211
208
|
};
|
|
212
209
|
const appLabel = this.app.type === 'theme' ? 'theme' : 'app';
|
|
213
210
|
let app = updateApp || {};
|
package/oclif.manifest.json
CHANGED
|
@@ -721,6 +721,126 @@
|
|
|
721
721
|
"_uninstall.js"
|
|
722
722
|
]
|
|
723
723
|
},
|
|
724
|
+
"app:action": {
|
|
725
|
+
"aliases": [],
|
|
726
|
+
"args": {
|
|
727
|
+
"id": {
|
|
728
|
+
"description": "id of the action, as declared in content or settings",
|
|
729
|
+
"name": "id",
|
|
730
|
+
"required": true
|
|
731
|
+
}
|
|
732
|
+
},
|
|
733
|
+
"description": "Runs a function action of the app as the logged-in dashboard user, the way a click in the dashboard does. The action is looked up in the app's content and settings as pushed.\n\nA record action or action field needs --record, and a bulk action --ids or --all. Pass the values of the action's modal with --values. When the same id is declared in more than one place, pick one with --collection, --view, --settings or --source.",
|
|
734
|
+
"examples": [
|
|
735
|
+
"swell app action sync_now",
|
|
736
|
+
"swell app action ship --record 64e8dfb633c44a0039477251",
|
|
737
|
+
"swell app action ship --record 64e8dfb633c44a0039477251 --values '{\"carrier\":\"ups\"}'",
|
|
738
|
+
"swell app action ship-selected --ids 64e8dfb633c44a0039477251,64e8dfb633c44a0039477252",
|
|
739
|
+
"swell app action ship --collection shipments --view edit --record 64e8dfb633c44a0039477251",
|
|
740
|
+
"swell app action export --source bulk --all"
|
|
741
|
+
],
|
|
742
|
+
"flags": {
|
|
743
|
+
"app-path": {
|
|
744
|
+
"description": "Path to your app directory",
|
|
745
|
+
"name": "app-path",
|
|
746
|
+
"hasDynamicHelp": false,
|
|
747
|
+
"multiple": false,
|
|
748
|
+
"type": "option"
|
|
749
|
+
},
|
|
750
|
+
"all": {
|
|
751
|
+
"description": "run a bulk action on all records its view lists with no filters set, as selecting all in the dashboard does",
|
|
752
|
+
"exclusive": [
|
|
753
|
+
"ids"
|
|
754
|
+
],
|
|
755
|
+
"name": "all",
|
|
756
|
+
"allowNo": false,
|
|
757
|
+
"type": "boolean"
|
|
758
|
+
},
|
|
759
|
+
"collection": {
|
|
760
|
+
"description": "collection of the content that declares the action, or the model's name for a collection of the app",
|
|
761
|
+
"name": "collection",
|
|
762
|
+
"hasDynamicHelp": false,
|
|
763
|
+
"multiple": false,
|
|
764
|
+
"type": "option"
|
|
765
|
+
},
|
|
766
|
+
"ids": {
|
|
767
|
+
"description": "comma-separated ids of the records a bulk action runs on",
|
|
768
|
+
"exclusive": [
|
|
769
|
+
"all"
|
|
770
|
+
],
|
|
771
|
+
"name": "ids",
|
|
772
|
+
"hasDynamicHelp": false,
|
|
773
|
+
"multiple": false,
|
|
774
|
+
"type": "option"
|
|
775
|
+
},
|
|
776
|
+
"live": {
|
|
777
|
+
"description": "run in the live environment instead of test",
|
|
778
|
+
"name": "live",
|
|
779
|
+
"allowNo": false,
|
|
780
|
+
"type": "boolean"
|
|
781
|
+
},
|
|
782
|
+
"record": {
|
|
783
|
+
"description": "id of the record a record action or action field runs on",
|
|
784
|
+
"name": "record",
|
|
785
|
+
"hasDynamicHelp": false,
|
|
786
|
+
"multiple": false,
|
|
787
|
+
"type": "option"
|
|
788
|
+
},
|
|
789
|
+
"settings": {
|
|
790
|
+
"description": "name of the settings that declare the action",
|
|
791
|
+
"name": "settings",
|
|
792
|
+
"hasDynamicHelp": false,
|
|
793
|
+
"multiple": false,
|
|
794
|
+
"type": "option"
|
|
795
|
+
},
|
|
796
|
+
"source": {
|
|
797
|
+
"description": "where the action is declared: list or record (a view's actions), bulk (a view's bulk actions), field (an action field) or settings (a settings action)",
|
|
798
|
+
"name": "source",
|
|
799
|
+
"hasDynamicHelp": false,
|
|
800
|
+
"multiple": false,
|
|
801
|
+
"options": [
|
|
802
|
+
"list",
|
|
803
|
+
"record",
|
|
804
|
+
"bulk",
|
|
805
|
+
"field",
|
|
806
|
+
"settings"
|
|
807
|
+
],
|
|
808
|
+
"type": "option"
|
|
809
|
+
},
|
|
810
|
+
"values": {
|
|
811
|
+
"description": "values of the action's modal fields, as JSON",
|
|
812
|
+
"name": "values",
|
|
813
|
+
"hasDynamicHelp": false,
|
|
814
|
+
"multiple": false,
|
|
815
|
+
"type": "option"
|
|
816
|
+
},
|
|
817
|
+
"view": {
|
|
818
|
+
"description": "id of the content view that declares the action",
|
|
819
|
+
"name": "view",
|
|
820
|
+
"hasDynamicHelp": false,
|
|
821
|
+
"multiple": false,
|
|
822
|
+
"type": "option"
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
"hasDynamicHelp": false,
|
|
826
|
+
"hiddenAliases": [],
|
|
827
|
+
"id": "app:action",
|
|
828
|
+
"pluginAlias": "@swell/cli",
|
|
829
|
+
"pluginName": "@swell/cli",
|
|
830
|
+
"pluginType": "core",
|
|
831
|
+
"strict": true,
|
|
832
|
+
"summary": "Run an app action from the command line.",
|
|
833
|
+
"enableJsonFlag": false,
|
|
834
|
+
"requiresSwellConfig": true,
|
|
835
|
+
"delayOrientation": false,
|
|
836
|
+
"isESM": true,
|
|
837
|
+
"relativePath": [
|
|
838
|
+
"dist",
|
|
839
|
+
"commands",
|
|
840
|
+
"app",
|
|
841
|
+
"action.js"
|
|
842
|
+
]
|
|
843
|
+
},
|
|
724
844
|
"app:delete": {
|
|
725
845
|
"aliases": [],
|
|
726
846
|
"args": {},
|
|
@@ -1237,7 +1357,7 @@
|
|
|
1237
1357
|
"name": "file"
|
|
1238
1358
|
}
|
|
1239
1359
|
},
|
|
1240
|
-
"description": "Push all app files, a specific file, or a specific configuration\ntype to an app in your store's test environment.\n\nIf the app does not exist, it will be created and its global ID saved to a .swellrc file.\n\n- If no file is specified, all configuration files will be pushed to the store.\n This includes the app icon (assets/icon.png), screenshots (assets/screenshots/),\n
|
|
1360
|
+
"description": "Push all app files, a specific file, or a specific configuration\ntype to an app in your store's test environment.\n\nIf the app does not exist, it will be created and its global ID saved to a .swellrc file.\n\n- If no file is specified, all configuration files will be pushed to the store.\n This includes the app icon (assets/icon.png), screenshots (assets/screenshots/),\n DESCRIPTION.md, and swell.json.\n- If a file is specified, only that file will be pushed to the store.\n- If a directory is specified, only files in that directory will be pushed.\n\nPushing anything under frontend/ also builds and deploys the app frontend, so\nthe deployed frontend matches what was pushed. Use --no-deploy to skip it.\n\nApp file directories:\nassets/\ncontent/\nfrontend/\ncomponents/\nfunctions/\nmodels/\nnotifications/\nsettings/\ntheme/\nwebhooks/",
|
|
1241
1361
|
"examples": [
|
|
1242
1362
|
"swell app push",
|
|
1243
1363
|
"swell app push content",
|
|
@@ -2475,7 +2595,7 @@
|
|
|
2475
2595
|
"examples": [
|
|
2476
2596
|
"$ swell create webhook",
|
|
2477
2597
|
"$ swell create webhook order-sync -u https://example.com/hook -e order.created -y",
|
|
2478
|
-
"$ swell create webhook payment-handler -u https://api.example.com/payments -e payment.succeeded,payment.failed --enabled -y"
|
|
2598
|
+
"$ swell create webhook payment-handler -u https://api.example.com/payments -e payment.succeeded,payment.failed --no-enabled -y"
|
|
2479
2599
|
],
|
|
2480
2600
|
"flags": {
|
|
2481
2601
|
"app-path": {
|
|
@@ -2513,9 +2633,9 @@
|
|
|
2513
2633
|
"type": "option"
|
|
2514
2634
|
},
|
|
2515
2635
|
"enabled": {
|
|
2516
|
-
"description": "Enable webhook (default:
|
|
2636
|
+
"description": "Enable webhook (default: true)",
|
|
2517
2637
|
"name": "enabled",
|
|
2518
|
-
"allowNo":
|
|
2638
|
+
"allowNo": true,
|
|
2519
2639
|
"type": "boolean"
|
|
2520
2640
|
},
|
|
2521
2641
|
"overwrite": {
|
|
@@ -3738,5 +3858,5 @@
|
|
|
3738
3858
|
]
|
|
3739
3859
|
}
|
|
3740
3860
|
},
|
|
3741
|
-
"version": "2.9.
|
|
3861
|
+
"version": "2.9.32"
|
|
3742
3862
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swell/cli",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Swell's command line interface/utility",
|
|
6
6
|
"keywords": [
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@npmcli/git": "5.0.3",
|
|
31
31
|
"@oclif/core": "3.7.1",
|
|
32
32
|
"@oclif/plugin-help": "6.0.4",
|
|
33
|
+
"@swell/app-types": "1.3.0",
|
|
33
34
|
"ajv": "8.20.0",
|
|
34
35
|
"ajv-errors": "3.0.0",
|
|
35
36
|
"ajv-formats": "3.0.1",
|