@swell/cli 2.2.1 → 2.3.0
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/app-command.js +1 -1
- package/dist/commands/api/delete.js +4 -1
- package/dist/commands/api/get.js +1 -0
- package/dist/commands/api/index.js +4 -1
- package/dist/commands/api/post.js +1 -0
- package/dist/commands/api/put.js +1 -0
- package/dist/commands/app/init.d.ts +8 -7
- package/dist/commands/app/init.js +36 -18
- package/dist/commands/app/pull.js +2 -2
- package/dist/commands/app/push.js +2 -2
- package/dist/commands/app/version.d.ts +1 -0
- package/dist/commands/app/version.js +14 -4
- package/dist/commands/create/app.d.ts +8 -4
- package/dist/commands/create/app.js +68 -51
- package/dist/commands/create/content.d.ts +5 -6
- package/dist/commands/create/content.js +21 -37
- package/dist/commands/create/function.d.ts +6 -7
- package/dist/commands/create/function.js +94 -31
- package/dist/commands/create/index.js +8 -0
- package/dist/commands/create/model.d.ts +5 -6
- package/dist/commands/create/model.js +21 -33
- package/dist/commands/create/notification.d.ts +9 -9
- package/dist/commands/create/notification.js +117 -95
- package/dist/commands/create/setting.d.ts +21 -0
- package/dist/commands/create/setting.js +120 -0
- package/dist/commands/create/tests.d.ts +4 -5
- package/dist/commands/create/tests.js +8 -11
- package/dist/commands/create/webhook.d.ts +22 -0
- package/dist/commands/create/webhook.js +176 -0
- package/dist/commands/schema.d.ts +1 -0
- package/dist/commands/schema.js +51 -5
- package/dist/commands/theme/init.d.ts +8 -4
- package/dist/commands/theme/init.js +21 -8
- package/dist/create-app-command.d.ts +1 -0
- package/dist/create-app-command.js +15 -10
- package/dist/create-config-command.js +2 -2
- package/dist/help/custom-help.d.ts +89 -0
- package/dist/help/custom-help.js +337 -0
- package/dist/help/types.d.ts +75 -0
- package/dist/help/types.js +1 -0
- package/dist/lib/apps/app-config.js +2 -2
- package/dist/lib/apps/index.d.ts +2 -1
- package/dist/lib/apps/index.js +21 -5
- package/dist/lib/apps/paths.js +7 -6
- package/dist/lib/create/notification.d.ts +1 -0
- package/dist/lib/create/schemas.d.ts +1 -0
- package/dist/lib/create/schemas.js +1 -0
- package/dist/lib/create/setting.d.ts +15 -0
- package/dist/lib/create/setting.js +27 -0
- package/dist/lib/create/tests/templates/env-dts.js +1 -0
- package/dist/lib/create/tests/templates/swell-client.js +2 -0
- package/dist/lib/create/tests/templates/tsconfig.js +2 -0
- package/dist/lib/create/tests/templates/vitest-config.js +1 -0
- package/dist/lib/create/webhook.d.ts +32 -0
- package/dist/lib/create/webhook.js +52 -0
- package/dist/swell-api-command.d.ts +14 -0
- package/dist/swell-api-command.js +113 -7
- package/oclif.manifest.json +609 -259
- package/package.json +2 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface CreateSettingFileBody {
|
|
2
|
+
$schema: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
fields: SettingFieldProps[];
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
interface SettingFieldProps {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
type?: SettingFieldType;
|
|
11
|
+
}
|
|
12
|
+
type SettingFieldType = 'asset' | 'basic_html' | 'boolean' | 'category_lookup' | 'checkbox' | 'checkboxes' | 'collection' | 'color' | 'color_scheme' | 'color_scheme_group' | 'currency' | 'customer_lookup' | 'date' | 'datetime' | 'document' | 'dropdown' | 'email' | 'field_group' | 'font_family' | 'generic_lookup' | 'html' | 'icon' | 'image' | 'long_text' | 'lookup' | 'markdown' | 'number' | 'percent' | 'phone' | 'product_lookup' | 'radio' | 'rich_html' | 'rich_text' | 'select' | 'short_text' | 'slider' | 'slug' | 'tags' | 'text' | 'textarea' | 'time' | 'toggle' | 'url' | 'variant_lookup' | 'video';
|
|
13
|
+
declare const parseFields: (fields: string[]) => SettingFieldProps[];
|
|
14
|
+
declare const toSettingLabel: (value: string) => string;
|
|
15
|
+
export { CreateSettingFileBody, parseFields, toSettingLabel };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { titleize } from 'inflection';
|
|
2
|
+
const parseFields = (fields) => {
|
|
3
|
+
if (fields.length === 0) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
const fieldResponse = [];
|
|
7
|
+
for (const fieldPair of fields) {
|
|
8
|
+
if (fieldPair.includes(':')) {
|
|
9
|
+
const [id, type] = fieldPair.split(':');
|
|
10
|
+
fieldResponse.push({
|
|
11
|
+
id,
|
|
12
|
+
label: titleize(id),
|
|
13
|
+
type,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
fieldResponse.push({
|
|
18
|
+
id: fieldPair,
|
|
19
|
+
label: titleize(fieldPair),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return fieldResponse;
|
|
24
|
+
};
|
|
25
|
+
// Example: 'api-config' => 'Api Config'
|
|
26
|
+
const toSettingLabel = (value) => titleize(value).replaceAll('-', ' ');
|
|
27
|
+
export { parseFields, toSettingLabel };
|
|
@@ -16,6 +16,7 @@ async function makeRequest(
|
|
|
16
16
|
): Promise<any> {
|
|
17
17
|
const baseUrl = env.SWELL_API_BASE_URL || "https://api.swell.store";
|
|
18
18
|
const sessionId = env.SWELL_SESSION_ID;
|
|
19
|
+
const environment = env.SWELL_ENVIRONMENT || "test";
|
|
19
20
|
|
|
20
21
|
if (!sessionId) {
|
|
21
22
|
throw new Error(
|
|
@@ -33,6 +34,7 @@ async function makeRequest(
|
|
|
33
34
|
"Content-Type": "application/json;charset=UTF-8",
|
|
34
35
|
"User-Agent": "swell-app-tests/1.0",
|
|
35
36
|
"X-Session": sessionId,
|
|
37
|
+
"Swell-Env": environment,
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
const options: RequestInit = {
|
|
@@ -3,6 +3,7 @@ export function tsconfigTemplate() {
|
|
|
3
3
|
extends: '../tsconfig.json',
|
|
4
4
|
compilerOptions: {
|
|
5
5
|
moduleResolution: 'bundler',
|
|
6
|
+
skipLibCheck: true,
|
|
6
7
|
types: [
|
|
7
8
|
'@cloudflare/vitest-pool-workers',
|
|
8
9
|
'@swell/app-types',
|
|
@@ -10,6 +11,7 @@ export function tsconfigTemplate() {
|
|
|
10
11
|
],
|
|
11
12
|
},
|
|
12
13
|
include: ['./**/*.ts', '../functions/**/*.ts'],
|
|
14
|
+
exclude: ['../node_modules'],
|
|
13
15
|
};
|
|
14
16
|
return `${JSON.stringify(config, null, 2)}\n`;
|
|
15
17
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface CreateWebhookFileBody {
|
|
2
|
+
$schema: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
events: string[];
|
|
6
|
+
url: string;
|
|
7
|
+
}
|
|
8
|
+
type ErrorHandler = (msg: string, options: {
|
|
9
|
+
exit: number;
|
|
10
|
+
}) => never;
|
|
11
|
+
/**
|
|
12
|
+
* Parse comma-separated events string to array
|
|
13
|
+
* Input: "payment.succeeded,payment.failed"
|
|
14
|
+
* Output: ["payment.succeeded", "payment.failed"]
|
|
15
|
+
*/
|
|
16
|
+
declare const parseEvents: (eventsInput: string) => string[];
|
|
17
|
+
/**
|
|
18
|
+
* Validate event format (model.action pattern)
|
|
19
|
+
* Valid: "product.created", "payment.succeeded"
|
|
20
|
+
* Invalid: "product", "created", "", ".created", "product."
|
|
21
|
+
*/
|
|
22
|
+
declare const validateEventFormat: (events: string[], onError: ErrorHandler) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Validate URL format (basic check for protocol)
|
|
25
|
+
*/
|
|
26
|
+
declare const validateUrl: (url: string, onError: ErrorHandler) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Convert name to webhook label
|
|
29
|
+
* "payment-handler" => "Payment Handler"
|
|
30
|
+
*/
|
|
31
|
+
declare const toWebhookLabel: (value: string) => string;
|
|
32
|
+
export { CreateWebhookFileBody, parseEvents, toWebhookLabel, validateEventFormat, validateUrl, };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { titleize } from 'inflection';
|
|
2
|
+
/**
|
|
3
|
+
* Parse comma-separated events string to array
|
|
4
|
+
* Input: "payment.succeeded,payment.failed"
|
|
5
|
+
* Output: ["payment.succeeded", "payment.failed"]
|
|
6
|
+
*/
|
|
7
|
+
const parseEvents = (eventsInput) => {
|
|
8
|
+
if (!eventsInput) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
return eventsInput
|
|
12
|
+
.split(',')
|
|
13
|
+
.map((e) => e.trim())
|
|
14
|
+
.filter(Boolean);
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Validate event format (model.action pattern)
|
|
18
|
+
* Valid: "product.created", "payment.succeeded"
|
|
19
|
+
* Invalid: "product", "created", "", ".created", "product."
|
|
20
|
+
*/
|
|
21
|
+
const validateEventFormat = (events, onError) => {
|
|
22
|
+
const invalid = [];
|
|
23
|
+
for (const event of events) {
|
|
24
|
+
const parts = event.split('.');
|
|
25
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
26
|
+
invalid.push(event);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (invalid.length > 0) {
|
|
30
|
+
onError(`Invalid event format: ${invalid.join(', ')}\n\nEvents must follow the 'model.action' pattern (e.g., product.created, payment.succeeded).\n\nExample: swell create webhook my-hook -u https://example.com -e order.created,order.updated -y`, { exit: 1 });
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Validate URL format (basic check for protocol)
|
|
35
|
+
*/
|
|
36
|
+
const validateUrl = (url, onError) => {
|
|
37
|
+
try {
|
|
38
|
+
const parsed = new URL(url);
|
|
39
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
40
|
+
throw new Error('Invalid protocol');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
onError(`Invalid URL format: ${url}\n\nURL must be a valid HTTP or HTTPS endpoint.\n\nExample: swell create webhook my-hook -u https://example.com/webhook -e order.created -y`, { exit: 1 });
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Convert name to webhook label
|
|
49
|
+
* "payment-handler" => "Payment Handler"
|
|
50
|
+
*/
|
|
51
|
+
const toWebhookLabel = (value) => titleize(value).replaceAll('-', ' ');
|
|
52
|
+
export { parseEvents, toWebhookLabel, validateEventFormat, validateUrl, };
|
|
@@ -5,6 +5,20 @@ export declare abstract class SwellApiCommand extends SwellCommand {
|
|
|
5
5
|
protected request(command: typeof SwellApiCommand, requestOptions?: Api.RequestOptions): Promise<void>;
|
|
6
6
|
protected catch(error: Error): Promise<any>;
|
|
7
7
|
private parseCommand;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve app ObjectId from a friendly slug or return as-is if already an ObjectId.
|
|
10
|
+
*/
|
|
11
|
+
private resolveAppId;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve function ID from app ID and function name.
|
|
14
|
+
*/
|
|
15
|
+
private resolveFunctionId;
|
|
16
|
+
private isPlainObject;
|
|
17
|
+
/**
|
|
18
|
+
* Build a function invocation request via the admin /:functions endpoint.
|
|
19
|
+
*/
|
|
20
|
+
private buildFunctionCallRequest;
|
|
21
|
+
private parseQueryString;
|
|
8
22
|
private processBody;
|
|
9
23
|
private isFilePath;
|
|
10
24
|
private handleResponse;
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { FetchError } from 'node-fetch';
|
|
4
|
+
import { HttpMethod } from './lib/api.js';
|
|
4
5
|
import { SwellCommand } from './swell-command.js';
|
|
6
|
+
// Pattern to match /functions/{appId}/{functionName} with optional query string
|
|
7
|
+
const FUNCTION_PATH_REGEX = /^\/functions\/([^/]+)\/([^/?]+)(\?.*)?$/;
|
|
8
|
+
// Pattern to match /functions/{functionId} with optional query string
|
|
9
|
+
const FUNCTION_DIRECT_REGEX = /^\/functions\/([^/?]+)(\?.*)?$/;
|
|
5
10
|
export class SwellApiCommand extends SwellCommand {
|
|
6
11
|
async request(command, requestOptions = {}) {
|
|
7
|
-
const { paths, options } = await this.parseCommand(command);
|
|
8
|
-
const
|
|
12
|
+
const { paths, options, methodOverride } = await this.parseCommand(command);
|
|
13
|
+
const method = methodOverride || this.method;
|
|
14
|
+
const response = await this.api[method](paths, {
|
|
9
15
|
rawResponse: true,
|
|
10
16
|
...options,
|
|
11
17
|
...requestOptions,
|
|
@@ -22,22 +28,112 @@ export class SwellApiCommand extends SwellCommand {
|
|
|
22
28
|
async parseCommand(options, argv) {
|
|
23
29
|
const parsedInput = await super.parse(options, argv);
|
|
24
30
|
const { args, flags } = parsedInput;
|
|
25
|
-
const { path } = args;
|
|
31
|
+
const { path: requestPath } = args;
|
|
26
32
|
const { live, body } = flags;
|
|
27
33
|
if (!live) {
|
|
28
34
|
await this.api.setEnv('test');
|
|
29
35
|
}
|
|
30
|
-
if (!
|
|
36
|
+
if (!requestPath.startsWith('/')) {
|
|
31
37
|
throw new Error('Path must start with a forward slash (/)');
|
|
32
38
|
}
|
|
39
|
+
const processedBody = await this.processBody(body);
|
|
40
|
+
// Check if this is a function call by name: /functions/{appId}/{functionName}
|
|
41
|
+
// Must check this first (more specific pattern)
|
|
42
|
+
const functionMatch = requestPath.match(FUNCTION_PATH_REGEX);
|
|
43
|
+
if (functionMatch) {
|
|
44
|
+
const [, appId, functionName, queryString] = functionMatch;
|
|
45
|
+
const functionId = await this.resolveFunctionId(appId, functionName);
|
|
46
|
+
const queryParams = this.parseQueryString(queryString);
|
|
47
|
+
return this.buildFunctionCallRequest(parsedInput, functionId, processedBody, queryParams);
|
|
48
|
+
}
|
|
49
|
+
// Check if this is a direct function call: /functions/{functionId}
|
|
50
|
+
const directMatch = requestPath.match(FUNCTION_DIRECT_REGEX);
|
|
51
|
+
if (directMatch) {
|
|
52
|
+
const [, functionId, queryString] = directMatch;
|
|
53
|
+
const queryParams = this.parseQueryString(queryString);
|
|
54
|
+
return this.buildFunctionCallRequest(parsedInput, functionId, processedBody, queryParams);
|
|
55
|
+
}
|
|
56
|
+
const paths = { adminPath: `/data${requestPath}` };
|
|
33
57
|
return {
|
|
34
58
|
...parsedInput,
|
|
35
|
-
paths
|
|
59
|
+
paths,
|
|
36
60
|
options: {
|
|
37
|
-
body:
|
|
61
|
+
body: processedBody,
|
|
38
62
|
},
|
|
39
63
|
};
|
|
40
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve app ObjectId from a friendly slug or return as-is if already an ObjectId.
|
|
67
|
+
*/
|
|
68
|
+
async resolveAppId(appIdOrSlug) {
|
|
69
|
+
// If it looks like an ObjectId (24 hex chars), return as-is
|
|
70
|
+
if (/^[\da-f]{24}$/i.test(appIdOrSlug)) {
|
|
71
|
+
return appIdOrSlug;
|
|
72
|
+
}
|
|
73
|
+
// Fetch all installed apps and filter by public_id or private_id client-side
|
|
74
|
+
const installedApps = await this.api.get({ adminPath: `/client/apps` });
|
|
75
|
+
const app = installedApps?.results?.find((a) => a.app_public_id === appIdOrSlug ||
|
|
76
|
+
a.app_private_id === `_${appIdOrSlug}`);
|
|
77
|
+
if (!app) {
|
|
78
|
+
throw new Error(`App '${appIdOrSlug}' not found`);
|
|
79
|
+
}
|
|
80
|
+
return app.app_id;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve function ID from app ID and function name.
|
|
84
|
+
*/
|
|
85
|
+
async resolveFunctionId(appIdOrSlug, functionName) {
|
|
86
|
+
const appId = await this.resolveAppId(appIdOrSlug);
|
|
87
|
+
const functionRecord = await this.api.get({ adminPath: `/data/:functions` }, {
|
|
88
|
+
query: {
|
|
89
|
+
app_id: appId,
|
|
90
|
+
name: functionName,
|
|
91
|
+
limit: 1,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
if (!functionRecord?.results?.length) {
|
|
95
|
+
throw new Error(`Function '${functionName}' not found for app '${appIdOrSlug}'`);
|
|
96
|
+
}
|
|
97
|
+
return functionRecord.results[0].id;
|
|
98
|
+
}
|
|
99
|
+
isPlainObject(value) {
|
|
100
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Build a function invocation request via the admin /:functions endpoint.
|
|
104
|
+
*/
|
|
105
|
+
buildFunctionCallRequest(parsedInput, functionId, bodyData, queryParams) {
|
|
106
|
+
// Merge query params with body data (body takes precedence)
|
|
107
|
+
// Only merge if bodyData is a plain object; otherwise use bodyData or query params alone
|
|
108
|
+
const mergedData = this.isPlainObject(bodyData)
|
|
109
|
+
? { ...queryParams, ...bodyData }
|
|
110
|
+
: bodyData ?? queryParams;
|
|
111
|
+
const callBody = {
|
|
112
|
+
$call: {
|
|
113
|
+
data: mergedData,
|
|
114
|
+
method: this.method,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
...parsedInput,
|
|
119
|
+
paths: { adminPath: `/data/:functions/${functionId}` },
|
|
120
|
+
options: {
|
|
121
|
+
body: callBody,
|
|
122
|
+
},
|
|
123
|
+
methodOverride: HttpMethod.PUT,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
parseQueryString(queryString) {
|
|
127
|
+
if (!queryString) {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
const params = new URLSearchParams(queryString.slice(1)); // Remove leading '?'
|
|
131
|
+
const result = {};
|
|
132
|
+
for (const [key, value] of params.entries()) {
|
|
133
|
+
result[key] = value;
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
41
137
|
async processBody(body) {
|
|
42
138
|
if (!body) {
|
|
43
139
|
return;
|
|
@@ -54,10 +150,20 @@ export class SwellApiCommand extends SwellCommand {
|
|
|
54
150
|
}
|
|
55
151
|
async handleResponse(response) {
|
|
56
152
|
const responseText = await response.text();
|
|
57
|
-
|
|
153
|
+
// Handle truly empty responses (no body) - success for 2xx status codes
|
|
154
|
+
// This handles DELETE 204 No Content and similar cases
|
|
155
|
+
if (!responseText) {
|
|
156
|
+
if (response.ok) {
|
|
157
|
+
this.onSuccess({ success: true });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
58
160
|
throw new Error('Not found');
|
|
59
161
|
}
|
|
60
162
|
const result = JSON.parse(responseText);
|
|
163
|
+
// API returned empty/null content in body - this is "not found" semantics
|
|
164
|
+
if (result === null || result === undefined || result === '') {
|
|
165
|
+
throw new Error('Not found');
|
|
166
|
+
}
|
|
61
167
|
if (result.error || result.errors || !response.ok) {
|
|
62
168
|
throw new Error(responseText);
|
|
63
169
|
}
|