@swell/cli 2.5.7 → 2.6.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/commands/api/delete.d.ts +1 -0
- package/dist/commands/api/delete.js +3 -1
- package/dist/commands/api/get.d.ts +1 -0
- package/dist/commands/api/get.js +3 -1
- package/dist/commands/api/post.d.ts +1 -0
- package/dist/commands/api/post.js +3 -1
- package/dist/commands/api/put.d.ts +1 -0
- package/dist/commands/api/put.js +3 -1
- package/dist/commands/app/dev.d.ts +3 -0
- package/dist/commands/app/dev.js +25 -12
- package/dist/lib/create/tests/templates/mock-request.js +12 -1
- package/dist/lib/create/tests/templates/setup-globals.js +2 -0
- package/dist/lib/swell-function-wrapper.d.ts +8 -3
- package/dist/lib/swell-function-wrapper.js +63 -15
- package/dist/swell-api-command.d.ts +2 -0
- package/dist/swell-api-command.js +56 -17
- package/oclif.manifest.json +37 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ export default class ApiDelete extends SwellApiCommand {
|
|
|
9
9
|
static flags: {
|
|
10
10
|
live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
11
|
api: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
|
+
header: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
13
|
};
|
|
13
14
|
static examples: string[];
|
|
14
15
|
get method(): HttpMethod;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { HttpMethod } from '../../lib/api.js';
|
|
3
|
-
import { SwellApiCommand } from '../../swell-api-command.js';
|
|
3
|
+
import { SwellApiCommand, headerFlag } from '../../swell-api-command.js';
|
|
4
4
|
export default class ApiDelete extends SwellApiCommand {
|
|
5
5
|
static summary = 'Send a DELETE request to the Swell API.';
|
|
6
6
|
static description = `Remove existing resources from the Swell API.
|
|
@@ -21,10 +21,12 @@ export default class ApiDelete extends SwellApiCommand {
|
|
|
21
21
|
options: ['backend', 'frontend'],
|
|
22
22
|
default: 'backend',
|
|
23
23
|
}),
|
|
24
|
+
header: headerFlag,
|
|
24
25
|
};
|
|
25
26
|
static examples = [
|
|
26
27
|
'swell api delete /products/abc123',
|
|
27
28
|
'swell api delete /functions/my-app/clear-cache',
|
|
29
|
+
`swell api delete /functions/my-app/clear-cache -H 'Authorization: Bearer xxx'`,
|
|
28
30
|
'swell api delete /products/abc123 --api frontend',
|
|
29
31
|
];
|
|
30
32
|
get method() {
|
|
@@ -9,6 +9,7 @@ export default class ApiGet extends SwellApiCommand {
|
|
|
9
9
|
static flags: {
|
|
10
10
|
live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
11
|
api: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
|
+
header: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
13
|
};
|
|
13
14
|
static examples: string[];
|
|
14
15
|
get method(): HttpMethod;
|
package/dist/commands/api/get.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { HttpMethod } from '../../lib/api.js';
|
|
3
|
-
import { SwellApiCommand } from '../../swell-api-command.js';
|
|
3
|
+
import { SwellApiCommand, headerFlag } from '../../swell-api-command.js';
|
|
4
4
|
export default class ApiGet extends SwellApiCommand {
|
|
5
5
|
static summary = 'Send a GET request to the Swell API.';
|
|
6
6
|
static description = `Retrieve data from the Swell API.
|
|
@@ -22,12 +22,14 @@ export default class ApiGet extends SwellApiCommand {
|
|
|
22
22
|
options: ['backend', 'frontend'],
|
|
23
23
|
default: 'backend',
|
|
24
24
|
}),
|
|
25
|
+
header: headerFlag,
|
|
25
26
|
};
|
|
26
27
|
static examples = [
|
|
27
28
|
'swell api get /products',
|
|
28
29
|
'swell api get "/products?limit=10&category=shoes"',
|
|
29
30
|
'swell api get /products/abc123',
|
|
30
31
|
'swell api get /functions/my-app/get-inventory',
|
|
32
|
+
`swell api get /functions/my-app/get-inventory -H 'Authorization: Bearer xxx'`,
|
|
31
33
|
'swell api get /products --api frontend',
|
|
32
34
|
];
|
|
33
35
|
get method() {
|
|
@@ -10,6 +10,7 @@ export default class ApiPost extends SwellApiCommand {
|
|
|
10
10
|
body: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
11
|
live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
12
12
|
api: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
header: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
14
|
};
|
|
14
15
|
static examples: string[];
|
|
15
16
|
get method(): HttpMethod;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { HttpMethod } from '../../lib/api.js';
|
|
3
|
-
import { SwellApiCommand } from '../../swell-api-command.js';
|
|
3
|
+
import { SwellApiCommand, headerFlag } from '../../swell-api-command.js';
|
|
4
4
|
export default class ApiPost extends SwellApiCommand {
|
|
5
5
|
static summary = 'Send a POST request to the Swell API.';
|
|
6
6
|
static description = `Create new resources in the Swell API.
|
|
@@ -25,11 +25,13 @@ export default class ApiPost extends SwellApiCommand {
|
|
|
25
25
|
options: ['backend', 'frontend'],
|
|
26
26
|
default: 'backend',
|
|
27
27
|
}),
|
|
28
|
+
header: headerFlag,
|
|
28
29
|
};
|
|
29
30
|
static examples = [
|
|
30
31
|
'swell api post /products --body \'{"name": "Product", "price": 100}\'',
|
|
31
32
|
'swell api post /products --body ./product.json',
|
|
32
33
|
'swell api post /functions/my-app/create-order --body \'{"items":["sku-1"]}\'',
|
|
34
|
+
`swell api post /functions/my-app/webhook --body @./payload.json -H 'Stripe-Signature: t=123,v1=abc'`,
|
|
33
35
|
`swell api post /products --body '{"name": "Product"}' --api frontend`,
|
|
34
36
|
];
|
|
35
37
|
get method() {
|
|
@@ -10,6 +10,7 @@ export default class ApiPut extends SwellApiCommand {
|
|
|
10
10
|
body: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
11
|
live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
12
12
|
api: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
header: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
14
|
};
|
|
14
15
|
static examples: string[];
|
|
15
16
|
get method(): HttpMethod;
|
package/dist/commands/api/put.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import { HttpMethod } from '../../lib/api.js';
|
|
3
|
-
import { SwellApiCommand } from '../../swell-api-command.js';
|
|
3
|
+
import { SwellApiCommand, headerFlag } from '../../swell-api-command.js';
|
|
4
4
|
export default class ApiPut extends SwellApiCommand {
|
|
5
5
|
static summary = 'Send a PUT request to the Swell API.';
|
|
6
6
|
static description = `Update existing resources in the Swell API.
|
|
@@ -25,11 +25,13 @@ export default class ApiPut extends SwellApiCommand {
|
|
|
25
25
|
options: ['backend', 'frontend'],
|
|
26
26
|
default: 'backend',
|
|
27
27
|
}),
|
|
28
|
+
header: headerFlag,
|
|
28
29
|
};
|
|
29
30
|
static examples = [
|
|
30
31
|
'swell api put /products/{id} --body \'{"id": "abc123", "name": "Updated"}\'',
|
|
31
32
|
'swell api put /products/abc123 --body ./product.json',
|
|
32
33
|
'swell api put /functions/my-app/update-stock --body \'{"sku":"abc","qty":10}\'',
|
|
34
|
+
`swell api put /functions/my-app/update-stock --body '{"sku":"abc"}' -H 'X-Idempotency-Key: 42'`,
|
|
33
35
|
`swell api put /products/abc123 --body '{"name": "Updated"}' --api frontend`,
|
|
34
36
|
];
|
|
35
37
|
get method() {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { ChildProcess } from 'node:child_process';
|
|
1
3
|
import { PushAppCommand } from '../../push-app-command.js';
|
|
2
4
|
export default class AppDev extends PushAppCommand {
|
|
3
5
|
static delayOrientation: boolean;
|
|
@@ -16,6 +18,7 @@ export default class AppDev extends PushAppCommand {
|
|
|
16
18
|
static summary: string;
|
|
17
19
|
functionErrors: Map<string, string>;
|
|
18
20
|
functionPorts: Map<string, number>;
|
|
21
|
+
functionProcesses: Map<string, ChildProcess>;
|
|
19
22
|
tmpDir: string;
|
|
20
23
|
frontendPort: number | null;
|
|
21
24
|
isCleaningUp: boolean;
|
package/dist/commands/app/dev.js
CHANGED
|
@@ -50,6 +50,8 @@ export default class AppDev extends PushAppCommand {
|
|
|
50
50
|
functionErrors = new Map();
|
|
51
51
|
// All available functions
|
|
52
52
|
functionPorts = new Map();
|
|
53
|
+
// Wrangler child processes to kill on cleanup
|
|
54
|
+
functionProcesses = new Map();
|
|
53
55
|
// Directory for compiled function files and wrangler context
|
|
54
56
|
tmpDir = '';
|
|
55
57
|
// Port for frontend dev server (used for routing non-function requests)
|
|
@@ -84,6 +86,9 @@ export default class AppDev extends PushAppCommand {
|
|
|
84
86
|
if (this.isCleaningUp)
|
|
85
87
|
return;
|
|
86
88
|
this.isCleaningUp = true;
|
|
89
|
+
for (const [, proc] of this.functionProcesses) {
|
|
90
|
+
proc.kill();
|
|
91
|
+
}
|
|
87
92
|
try {
|
|
88
93
|
await this.updateLocalProxy(null, this.storefront?.id);
|
|
89
94
|
}
|
|
@@ -252,11 +257,8 @@ ENVIRONMENT = "development"
|
|
|
252
257
|
.map((m) => String(m).toUpperCase())
|
|
253
258
|
.join(', '))}`);
|
|
254
259
|
}
|
|
255
|
-
if (config.route.headers) {
|
|
256
|
-
|
|
257
|
-
this.log(` Headers:`);
|
|
258
|
-
for (const header of headers)
|
|
259
|
-
this.log(` ${style.dim(header)}`);
|
|
260
|
+
if (config.route.headers?.length) {
|
|
261
|
+
this.log(` Headers: ${style.dim(config.route.headers.join(', '))}`);
|
|
260
262
|
}
|
|
261
263
|
if (config.route.cache?.timeout) {
|
|
262
264
|
this.log(` Cache timeout: ${style.dim(config.route.cache.timeout)}`);
|
|
@@ -296,7 +298,9 @@ ENVIRONMENT = "development"
|
|
|
296
298
|
// Re-bundle the function
|
|
297
299
|
const { code } = await bundleFunction(fullPath);
|
|
298
300
|
// Update the bundled file (wrangler dev will auto-reload)
|
|
299
|
-
const
|
|
301
|
+
const functionDir = path.join(this.tmpDir, `${appConfig.name}`);
|
|
302
|
+
await fs.promises.mkdir(functionDir, { recursive: true });
|
|
303
|
+
const bundledPath = path.join(functionDir, `${appConfig.name}.js`);
|
|
300
304
|
await fs.promises.writeFile(bundledPath, code);
|
|
301
305
|
if (this.functionPorts.has(appConfig.name)) {
|
|
302
306
|
this.log(`\nUpdating function ${appConfig.name}...`);
|
|
@@ -355,6 +359,7 @@ ENVIRONMENT = "development"
|
|
|
355
359
|
}
|
|
356
360
|
async startFunctionServers(functions) {
|
|
357
361
|
const functionStatus = new Map();
|
|
362
|
+
const allocatedFunctionPorts = new Set();
|
|
358
363
|
const allocatedInspectorPorts = new Set();
|
|
359
364
|
/* eslint-disable no-await-in-loop */
|
|
360
365
|
for (const func of functions) {
|
|
@@ -363,7 +368,11 @@ ENVIRONMENT = "development"
|
|
|
363
368
|
// Function server already running
|
|
364
369
|
continue;
|
|
365
370
|
}
|
|
366
|
-
const functionPort = await getPort({
|
|
371
|
+
const functionPort = await getPort({
|
|
372
|
+
port: portNumbers(9000, 9100),
|
|
373
|
+
exclude: [...allocatedFunctionPorts],
|
|
374
|
+
});
|
|
375
|
+
allocatedFunctionPorts.add(functionPort);
|
|
367
376
|
const inspectorPort = await getPort({
|
|
368
377
|
port: portNumbers(9229, 9329),
|
|
369
378
|
exclude: [...allocatedInspectorPorts],
|
|
@@ -373,12 +382,15 @@ ENVIRONMENT = "development"
|
|
|
373
382
|
// Bundle the function
|
|
374
383
|
const fullPath = path.join(this.appPath, func.filePath);
|
|
375
384
|
const { code } = await bundleFunction(fullPath);
|
|
376
|
-
//
|
|
377
|
-
const
|
|
385
|
+
// Each function gets its own directory to avoid wrangler state conflicts
|
|
386
|
+
const functionDir = path.join(this.tmpDir, functionName);
|
|
387
|
+
await fs.promises.mkdir(functionDir, { recursive: true });
|
|
388
|
+
// Write bundled function to its directory
|
|
389
|
+
const bundledPath = path.join(functionDir, `${functionName}.js`);
|
|
378
390
|
await fs.promises.writeFile(bundledPath, code);
|
|
379
391
|
// Generate wrangler config
|
|
380
392
|
const wranglerConfig = this.generateWranglerConfig(functionName, bundledPath);
|
|
381
|
-
const configPath = path.join(
|
|
393
|
+
const configPath = path.join(functionDir, `${functionName}.toml`);
|
|
382
394
|
await fs.promises.writeFile(configPath, wranglerConfig);
|
|
383
395
|
functionStatus.set(functionName, 'starting');
|
|
384
396
|
// Start wrangler process in background using detected package manager
|
|
@@ -392,7 +404,7 @@ ENVIRONMENT = "development"
|
|
|
392
404
|
`--port=${functionPort}`,
|
|
393
405
|
`--inspector-port=${inspectorPort}`,
|
|
394
406
|
], {
|
|
395
|
-
cwd:
|
|
407
|
+
cwd: functionDir,
|
|
396
408
|
// stdio: 'pipe', // Capture output for debugging
|
|
397
409
|
detached: false,
|
|
398
410
|
});
|
|
@@ -458,7 +470,8 @@ ENVIRONMENT = "development"
|
|
|
458
470
|
this.functionErrors.set(functionName, `Process exited with code ${code}`);
|
|
459
471
|
}
|
|
460
472
|
});
|
|
461
|
-
// Store the port for routing
|
|
473
|
+
// Store the process and port for routing
|
|
474
|
+
this.functionProcesses.set(functionName, wranglerProcess);
|
|
462
475
|
this.functionPorts.set(functionName, functionPort);
|
|
463
476
|
}
|
|
464
477
|
catch (error) {
|
|
@@ -97,7 +97,18 @@ export function createMockRequest(options: MockRequestOptions = {}): SwellReques
|
|
|
97
97
|
const targetAppId =
|
|
98
98
|
typeof idOrValues === "string" ? idOrValues : resolvedAppId;
|
|
99
99
|
const appValues = typeof idOrValues === "string" ? values : idOrValues;
|
|
100
|
-
if (!targetAppId
|
|
100
|
+
if (!targetAppId) {
|
|
101
|
+
throw new Error("appValues: missing app id (req.appId is empty)");
|
|
102
|
+
}
|
|
103
|
+
if (
|
|
104
|
+
typeof appValues !== "object" ||
|
|
105
|
+
appValues === null ||
|
|
106
|
+
Object.getPrototypeOf(appValues) !== Object.prototype
|
|
107
|
+
) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
"appValues: values must be a plain object (arrays, class instances, null, and primitives are not allowed)"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
101
112
|
return {
|
|
102
113
|
$app: {
|
|
103
114
|
[targetAppId]: appValues,
|
|
@@ -2,6 +2,7 @@ export function setupGlobalsTemplate() {
|
|
|
2
2
|
return `\
|
|
3
3
|
class SwellErrorImpl extends Error {
|
|
4
4
|
status: number;
|
|
5
|
+
body?: unknown;
|
|
5
6
|
|
|
6
7
|
constructor(message: string | object, options: { status?: number } = {}) {
|
|
7
8
|
const text =
|
|
@@ -12,6 +13,7 @@ class SwellErrorImpl extends Error {
|
|
|
12
13
|
super(text);
|
|
13
14
|
this.name = "SwellError";
|
|
14
15
|
this.status = options.status ?? 500;
|
|
16
|
+
this.body = typeof message === "string" ? undefined : message;
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -88,6 +88,7 @@ declare class SwellRequest {
|
|
|
88
88
|
id: any;
|
|
89
89
|
isLocalDev: boolean;
|
|
90
90
|
swell: SwellAPI;
|
|
91
|
+
rawBody: string;
|
|
91
92
|
body: {};
|
|
92
93
|
query: {};
|
|
93
94
|
data: {};
|
|
@@ -100,14 +101,16 @@ declare class SwellRequest {
|
|
|
100
101
|
getIngestableLogs(response: any): {
|
|
101
102
|
params: any;
|
|
102
103
|
} | undefined;
|
|
104
|
+
formatRequestData(): string;
|
|
103
105
|
ingestLogs(response: any): Promise<void>;
|
|
104
106
|
/**
|
|
105
107
|
* Merge values into app data for the current request.
|
|
106
108
|
* @param {object|string} idOrValues string to indicate app ID, or values to merge
|
|
107
109
|
* @param {object|undefined} values values to merge into app data
|
|
108
|
-
* @returns {object
|
|
110
|
+
* @returns {object} existing app data merged with values
|
|
111
|
+
* @throws {Error} if app id is missing or values is not a plain object
|
|
109
112
|
*/
|
|
110
|
-
appValues(idOrValues: object | string, values?: object | undefined): object
|
|
113
|
+
appValues(idOrValues: object | string, values?: object | undefined): object;
|
|
111
114
|
}
|
|
112
115
|
/**
|
|
113
116
|
* Class representing the Swell backend API.
|
|
@@ -133,12 +136,14 @@ declare class SwellAPI {
|
|
|
133
136
|
declare class SwellError extends Error {
|
|
134
137
|
constructor(message: any, options?: {});
|
|
135
138
|
status: any;
|
|
139
|
+
body: any;
|
|
136
140
|
}
|
|
137
141
|
/**
|
|
138
142
|
* Class representing a Swell response.
|
|
139
143
|
*/
|
|
140
144
|
declare class SwellResponse extends Response {
|
|
141
|
-
static _respond(req: any, response: any, context: any): any
|
|
145
|
+
static _respond(req: any, response: any, context: any): Promise<any>;
|
|
146
|
+
static _consumeNativeResponse(response: any): Promise<SwellResponse>;
|
|
142
147
|
static _respondWithLogs(response: any, req: any): SwellResponse;
|
|
143
148
|
constructor(data: any, options?: {});
|
|
144
149
|
_swellData: any;
|
|
@@ -90,6 +90,8 @@ class SwellRequest {
|
|
|
90
90
|
this.assignRequestProps(req);
|
|
91
91
|
// Set environment specific variables
|
|
92
92
|
this.context = context;
|
|
93
|
+
// Slug-form app identifier (e.g. 'klaviyo') matching keys in record.$app[...].
|
|
94
|
+
// Derived from the app's private_id with the leading underscore stripped.
|
|
93
95
|
this.appId = req.headers.get('Swell-App-Id');
|
|
94
96
|
this.storeId = req.headers.get('Swell-Store-Id');
|
|
95
97
|
this.accessToken = req.headers.get('Swell-Access-Token');
|
|
@@ -105,11 +107,14 @@ class SwellRequest {
|
|
|
105
107
|
this.swell = new SwellAPI(this, context);
|
|
106
108
|
// URL of the original request
|
|
107
109
|
this.url;
|
|
108
|
-
//
|
|
110
|
+
// Raw request body text, untouched by parsing.
|
|
111
|
+
// Use on route triggers for HMAC/webhook signature verification.
|
|
112
|
+
this.rawBody = '';
|
|
113
|
+
// Parsed JSON body as object, or raw text string when body isn't JSON.
|
|
109
114
|
this.body = {};
|
|
110
115
|
// URL query parameters as an object
|
|
111
116
|
this.query = {};
|
|
112
|
-
// Combined object of body and query parameters
|
|
117
|
+
// Combined object of body and query parameters (query keys overwrite body keys)
|
|
113
118
|
this.data = {};
|
|
114
119
|
// Internal logs
|
|
115
120
|
this._logs = [];
|
|
@@ -120,9 +125,10 @@ class SwellRequest {
|
|
|
120
125
|
});
|
|
121
126
|
}
|
|
122
127
|
async initialize() {
|
|
123
|
-
this.
|
|
128
|
+
this.rawBody = await this.originalRequest.text();
|
|
129
|
+
this.body = this.rawBody;
|
|
124
130
|
try {
|
|
125
|
-
this.data = JSON.parse(this.
|
|
131
|
+
this.data = JSON.parse(this.rawBody);
|
|
126
132
|
this.body = { ...this.data };
|
|
127
133
|
}
|
|
128
134
|
catch (err) {
|
|
@@ -170,12 +176,22 @@ class SwellRequest {
|
|
|
170
176
|
...this.logParams,
|
|
171
177
|
message: {
|
|
172
178
|
...this.logParams?.message,
|
|
179
|
+
data: this.logParams?.message?.data || this.formatRequestData(),
|
|
173
180
|
logs: this._logs,
|
|
174
181
|
status: response.status,
|
|
175
182
|
},
|
|
176
183
|
},
|
|
177
184
|
};
|
|
178
185
|
}
|
|
186
|
+
formatRequestData() {
|
|
187
|
+
try {
|
|
188
|
+
const stringData = JSON.stringify(this.body ?? null);
|
|
189
|
+
return stringData.substring(0, 1024000);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
179
195
|
async ingestLogs(response) {
|
|
180
196
|
const ingestableLogs = this.getIngestableLogs(response);
|
|
181
197
|
if (!ingestableLogs) {
|
|
@@ -192,13 +208,17 @@ class SwellRequest {
|
|
|
192
208
|
* Merge values into app data for the current request.
|
|
193
209
|
* @param {object|string} idOrValues string to indicate app ID, or values to merge
|
|
194
210
|
* @param {object|undefined} values values to merge into app data
|
|
195
|
-
* @returns {object
|
|
211
|
+
* @returns {object} existing app data merged with values
|
|
212
|
+
* @throws {Error} if app id is missing or values is not a plain object
|
|
196
213
|
*/
|
|
197
214
|
appValues(idOrValues, values = undefined) {
|
|
198
|
-
const appId = typeof idOrValues === 'string' ?
|
|
215
|
+
const appId = typeof idOrValues === 'string' ? idOrValues : this.appId;
|
|
199
216
|
const appValues = typeof idOrValues === 'string' ? values : idOrValues;
|
|
200
|
-
if (!appId
|
|
201
|
-
|
|
217
|
+
if (!appId) {
|
|
218
|
+
throw new Error('appValues: missing app id (req.appId is empty)');
|
|
219
|
+
}
|
|
220
|
+
if (!isOrdinaryObject(appValues)) {
|
|
221
|
+
throw new Error('appValues: values must be a plain object (arrays, class instances, null, and primitives are not allowed)');
|
|
202
222
|
}
|
|
203
223
|
return {
|
|
204
224
|
$app: {
|
|
@@ -306,6 +326,7 @@ class SwellAPI {
|
|
|
306
326
|
*/
|
|
307
327
|
class SwellError extends Error {
|
|
308
328
|
constructor(message, options = {}) {
|
|
329
|
+
const body = typeof message === 'string' ? undefined : message;
|
|
309
330
|
let formattedMessage;
|
|
310
331
|
if (typeof message === 'string') {
|
|
311
332
|
formattedMessage = message;
|
|
@@ -319,6 +340,7 @@ class SwellError extends Error {
|
|
|
319
340
|
super(formattedMessage);
|
|
320
341
|
this.name = 'SwellError';
|
|
321
342
|
this.status = options.status || 500;
|
|
343
|
+
this.body = body;
|
|
322
344
|
}
|
|
323
345
|
}
|
|
324
346
|
/**
|
|
@@ -348,27 +370,53 @@ class SwellResponse extends Response {
|
|
|
348
370
|
this._swellData = data;
|
|
349
371
|
this._swellOptions = options || {};
|
|
350
372
|
}
|
|
351
|
-
static _respond(req, response, context) {
|
|
373
|
+
static async _respond(req, response, context) {
|
|
352
374
|
let finalResponse = response;
|
|
353
|
-
|
|
375
|
+
const isHook = Boolean(req.data?.$event?.hook);
|
|
354
376
|
if (finalResponse instanceof Response &&
|
|
355
377
|
!(finalResponse instanceof SwellResponse)) {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
378
|
+
// Non-hook responses pass through unchanged so the handler's body,
|
|
379
|
+
// status, and headers reach the caller intact. Hooks need the parsed
|
|
380
|
+
// body so $logs can be merged into the payload below.
|
|
381
|
+
if (isHook) {
|
|
382
|
+
finalResponse =
|
|
383
|
+
await SwellResponse._consumeNativeResponse(finalResponse);
|
|
384
|
+
}
|
|
360
385
|
}
|
|
361
386
|
else if (!(finalResponse instanceof SwellResponse)) {
|
|
362
387
|
finalResponse = new SwellResponse(response);
|
|
363
388
|
}
|
|
364
389
|
// Send logs back with the response for event hooks
|
|
365
|
-
if (
|
|
390
|
+
if (isHook) {
|
|
366
391
|
return SwellResponse._respondWithLogs(finalResponse, req);
|
|
367
392
|
}
|
|
368
393
|
// Ingest logs in the background
|
|
369
394
|
context.waitUntil(req.ingestLogs(finalResponse));
|
|
370
395
|
return finalResponse;
|
|
371
396
|
}
|
|
397
|
+
static async _consumeNativeResponse(response) {
|
|
398
|
+
const headers = {};
|
|
399
|
+
response.headers.forEach((value, key) => {
|
|
400
|
+
headers[key] = value;
|
|
401
|
+
});
|
|
402
|
+
try {
|
|
403
|
+
const text = await response.text();
|
|
404
|
+
let data;
|
|
405
|
+
try {
|
|
406
|
+
data = JSON.parse(text);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
data = text;
|
|
410
|
+
}
|
|
411
|
+
return new SwellResponse(data, {
|
|
412
|
+
status: response.status,
|
|
413
|
+
headers,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
return new SwellResponse({ error: `Unable to read response body: ${err.message}` }, { status: 500 });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
372
420
|
static _respondWithLogs(response, req) {
|
|
373
421
|
const ingestableLogs = req.getIngestableLogs(response);
|
|
374
422
|
// Rebuild response with logs
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { HttpMethod } from './lib/api.js';
|
|
2
2
|
import { SwellCommand } from './swell-command.js';
|
|
3
|
+
export declare const headerFlag: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
3
4
|
export declare abstract class SwellApiCommand extends SwellCommand {
|
|
4
5
|
protected abstract get method(): HttpMethod;
|
|
5
6
|
protected request(command: typeof SwellApiCommand, requestOptions?: Api.RequestOptions): Promise<void>;
|
|
@@ -18,6 +19,7 @@ export declare abstract class SwellApiCommand extends SwellCommand {
|
|
|
18
19
|
* Build a function invocation request via the admin /:functions endpoint.
|
|
19
20
|
*/
|
|
20
21
|
private buildFunctionCallRequest;
|
|
22
|
+
private parseHeaders;
|
|
21
23
|
private parseQueryString;
|
|
22
24
|
private processBody;
|
|
23
25
|
private isFilePath;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
1
2
|
import * as fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { FetchError } from 'node-fetch';
|
|
@@ -7,6 +8,13 @@ import { SwellCommand } from './swell-command.js';
|
|
|
7
8
|
const FUNCTION_PATH_REGEX = /^\/functions\/([^/]+)\/([^/?]+)(\?.*)?$/;
|
|
8
9
|
// Pattern to match /functions/{functionId} with optional query string
|
|
9
10
|
const FUNCTION_DIRECT_REGEX = /^\/functions\/([^/?]+)(\?.*)?$/;
|
|
11
|
+
// Shared --header / -H flag for function-call paths. Pass once per header.
|
|
12
|
+
// Only forwarded as $call.headers on /functions/* paths; ignored elsewhere.
|
|
13
|
+
export const headerFlag = Flags.string({
|
|
14
|
+
char: 'H',
|
|
15
|
+
description: "HTTP header to forward to a function (format: 'Name: value'). Repeat for multiple. Only applies to /functions/* paths.",
|
|
16
|
+
multiple: true,
|
|
17
|
+
});
|
|
10
18
|
export class SwellApiCommand extends SwellCommand {
|
|
11
19
|
async request(command, requestOptions = {}) {
|
|
12
20
|
const { paths, options, methodOverride } = await this.parseCommand(command);
|
|
@@ -29,7 +37,7 @@ export class SwellApiCommand extends SwellCommand {
|
|
|
29
37
|
const parsedInput = await super.parse(options, argv);
|
|
30
38
|
const { args, flags } = parsedInput;
|
|
31
39
|
const { path: requestPath } = args;
|
|
32
|
-
const { live, api, body } = flags;
|
|
40
|
+
const { live, api, body, header } = flags;
|
|
33
41
|
const isFrontendAPI = api === 'frontend';
|
|
34
42
|
if (!live) {
|
|
35
43
|
await this.api.setEnv('test');
|
|
@@ -41,21 +49,30 @@ export class SwellApiCommand extends SwellCommand {
|
|
|
41
49
|
throw new Error('Path must start with a forward slash (/)');
|
|
42
50
|
}
|
|
43
51
|
const processedBody = await this.processBody(body);
|
|
52
|
+
const callHeaders = this.parseHeaders(header);
|
|
44
53
|
// Check if this is a function call by name: /functions/{appId}/{functionName}
|
|
45
54
|
// Must check this first (more specific pattern)
|
|
46
55
|
const functionMatch = requestPath.match(FUNCTION_PATH_REGEX);
|
|
47
56
|
if (functionMatch) {
|
|
48
57
|
const [, appId, functionName, queryString] = functionMatch;
|
|
49
58
|
const functionId = await this.resolveFunctionId(appId, functionName);
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
return this.buildFunctionCallRequest(parsedInput, {
|
|
60
|
+
functionId,
|
|
61
|
+
body: processedBody,
|
|
62
|
+
query: this.parseQueryString(queryString),
|
|
63
|
+
headers: callHeaders,
|
|
64
|
+
});
|
|
52
65
|
}
|
|
53
66
|
// Check if this is a direct function call: /functions/{functionId}
|
|
54
67
|
const directMatch = requestPath.match(FUNCTION_DIRECT_REGEX);
|
|
55
68
|
if (directMatch) {
|
|
56
69
|
const [, functionId, queryString] = directMatch;
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
return this.buildFunctionCallRequest(parsedInput, {
|
|
71
|
+
functionId,
|
|
72
|
+
body: processedBody,
|
|
73
|
+
query: this.parseQueryString(queryString),
|
|
74
|
+
headers: callHeaders,
|
|
75
|
+
});
|
|
59
76
|
}
|
|
60
77
|
const paths = isFrontendAPI
|
|
61
78
|
? { frontendPath: requestPath }
|
|
@@ -108,27 +125,49 @@ export class SwellApiCommand extends SwellCommand {
|
|
|
108
125
|
/**
|
|
109
126
|
* Build a function invocation request via the admin /:functions endpoint.
|
|
110
127
|
*/
|
|
111
|
-
buildFunctionCallRequest(parsedInput,
|
|
128
|
+
buildFunctionCallRequest(parsedInput, call) {
|
|
112
129
|
// Merge query params with body data (body takes precedence)
|
|
113
|
-
// Only merge if
|
|
114
|
-
const mergedData = this.isPlainObject(
|
|
115
|
-
? { ...
|
|
116
|
-
:
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
method: this.method,
|
|
121
|
-
},
|
|
130
|
+
// Only merge if body is a plain object; otherwise use body or query alone
|
|
131
|
+
const mergedData = this.isPlainObject(call.body)
|
|
132
|
+
? { ...call.query, ...call.body }
|
|
133
|
+
: call.body ?? call.query;
|
|
134
|
+
const $call = {
|
|
135
|
+
data: mergedData,
|
|
136
|
+
method: this.method,
|
|
122
137
|
};
|
|
138
|
+
if (Object.keys(call.headers).length > 0) {
|
|
139
|
+
$call.headers = call.headers;
|
|
140
|
+
}
|
|
123
141
|
return {
|
|
124
142
|
...parsedInput,
|
|
125
|
-
paths: { adminPath: `/data/:functions/${functionId}` },
|
|
143
|
+
paths: { adminPath: `/data/:functions/${call.functionId}` },
|
|
126
144
|
options: {
|
|
127
|
-
body:
|
|
145
|
+
body: { $call },
|
|
128
146
|
},
|
|
129
147
|
methodOverride: HttpMethod.PUT,
|
|
130
148
|
};
|
|
131
149
|
}
|
|
150
|
+
// Parse repeated `-H "Name: value"` flag entries into a map. Splits on first colon
|
|
151
|
+
// so values containing colons (e.g. `Authorization: Bearer x:y`) survive intact.
|
|
152
|
+
parseHeaders(headerArgs) {
|
|
153
|
+
if (!headerArgs?.length) {
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
const result = {};
|
|
157
|
+
for (const entry of headerArgs) {
|
|
158
|
+
const colonIndex = entry.indexOf(':');
|
|
159
|
+
if (colonIndex < 1) {
|
|
160
|
+
throw new Error(`Invalid header '${entry}'. Expected format: 'Name: value'.`);
|
|
161
|
+
}
|
|
162
|
+
const name = entry.slice(0, colonIndex).trim();
|
|
163
|
+
const value = entry.slice(colonIndex + 1).trim();
|
|
164
|
+
if (!name) {
|
|
165
|
+
throw new Error(`Invalid header '${entry}'. Header name cannot be empty.`);
|
|
166
|
+
}
|
|
167
|
+
result[name] = value;
|
|
168
|
+
}
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
132
171
|
parseQueryString(queryString) {
|
|
133
172
|
if (!queryString) {
|
|
134
173
|
return {};
|
package/oclif.manifest.json
CHANGED
|
@@ -385,6 +385,7 @@
|
|
|
385
385
|
"examples": [
|
|
386
386
|
"swell api delete /products/abc123",
|
|
387
387
|
"swell api delete /functions/my-app/clear-cache",
|
|
388
|
+
"swell api delete /functions/my-app/clear-cache -H 'Authorization: Bearer xxx'",
|
|
388
389
|
"swell api delete /products/abc123 --api frontend"
|
|
389
390
|
],
|
|
390
391
|
"flags": {
|
|
@@ -405,6 +406,14 @@
|
|
|
405
406
|
"frontend"
|
|
406
407
|
],
|
|
407
408
|
"type": "option"
|
|
409
|
+
},
|
|
410
|
+
"header": {
|
|
411
|
+
"char": "H",
|
|
412
|
+
"description": "HTTP header to forward to a function (format: 'Name: value'). Repeat for multiple. Only applies to /functions/* paths.",
|
|
413
|
+
"name": "header",
|
|
414
|
+
"hasDynamicHelp": false,
|
|
415
|
+
"multiple": true,
|
|
416
|
+
"type": "option"
|
|
408
417
|
}
|
|
409
418
|
},
|
|
410
419
|
"hasDynamicHelp": false,
|
|
@@ -438,6 +447,7 @@
|
|
|
438
447
|
"swell api get \"/products?limit=10&category=shoes\"",
|
|
439
448
|
"swell api get /products/abc123",
|
|
440
449
|
"swell api get /functions/my-app/get-inventory",
|
|
450
|
+
"swell api get /functions/my-app/get-inventory -H 'Authorization: Bearer xxx'",
|
|
441
451
|
"swell api get /products --api frontend"
|
|
442
452
|
],
|
|
443
453
|
"flags": {
|
|
@@ -458,6 +468,14 @@
|
|
|
458
468
|
"frontend"
|
|
459
469
|
],
|
|
460
470
|
"type": "option"
|
|
471
|
+
},
|
|
472
|
+
"header": {
|
|
473
|
+
"char": "H",
|
|
474
|
+
"description": "HTTP header to forward to a function (format: 'Name: value'). Repeat for multiple. Only applies to /functions/* paths.",
|
|
475
|
+
"name": "header",
|
|
476
|
+
"hasDynamicHelp": false,
|
|
477
|
+
"multiple": true,
|
|
478
|
+
"type": "option"
|
|
461
479
|
}
|
|
462
480
|
},
|
|
463
481
|
"hasDynamicHelp": false,
|
|
@@ -521,6 +539,7 @@
|
|
|
521
539
|
"swell api post /products --body '{\"name\": \"Product\", \"price\": 100}'",
|
|
522
540
|
"swell api post /products --body ./product.json",
|
|
523
541
|
"swell api post /functions/my-app/create-order --body '{\"items\":[\"sku-1\"]}'",
|
|
542
|
+
"swell api post /functions/my-app/webhook --body @./payload.json -H 'Stripe-Signature: t=123,v1=abc'",
|
|
524
543
|
"swell api post /products --body '{\"name\": \"Product\"}' --api frontend"
|
|
525
544
|
],
|
|
526
545
|
"flags": {
|
|
@@ -548,6 +567,14 @@
|
|
|
548
567
|
"frontend"
|
|
549
568
|
],
|
|
550
569
|
"type": "option"
|
|
570
|
+
},
|
|
571
|
+
"header": {
|
|
572
|
+
"char": "H",
|
|
573
|
+
"description": "HTTP header to forward to a function (format: 'Name: value'). Repeat for multiple. Only applies to /functions/* paths.",
|
|
574
|
+
"name": "header",
|
|
575
|
+
"hasDynamicHelp": false,
|
|
576
|
+
"multiple": true,
|
|
577
|
+
"type": "option"
|
|
551
578
|
}
|
|
552
579
|
},
|
|
553
580
|
"hasDynamicHelp": false,
|
|
@@ -580,6 +607,7 @@
|
|
|
580
607
|
"swell api put /products/{id} --body '{\"id\": \"abc123\", \"name\": \"Updated\"}'",
|
|
581
608
|
"swell api put /products/abc123 --body ./product.json",
|
|
582
609
|
"swell api put /functions/my-app/update-stock --body '{\"sku\":\"abc\",\"qty\":10}'",
|
|
610
|
+
"swell api put /functions/my-app/update-stock --body '{\"sku\":\"abc\"}' -H 'X-Idempotency-Key: 42'",
|
|
583
611
|
"swell api put /products/abc123 --body '{\"name\": \"Updated\"}' --api frontend"
|
|
584
612
|
],
|
|
585
613
|
"flags": {
|
|
@@ -607,6 +635,14 @@
|
|
|
607
635
|
"frontend"
|
|
608
636
|
],
|
|
609
637
|
"type": "option"
|
|
638
|
+
},
|
|
639
|
+
"header": {
|
|
640
|
+
"char": "H",
|
|
641
|
+
"description": "HTTP header to forward to a function (format: 'Name: value'). Repeat for multiple. Only applies to /functions/* paths.",
|
|
642
|
+
"name": "header",
|
|
643
|
+
"hasDynamicHelp": false,
|
|
644
|
+
"multiple": true,
|
|
645
|
+
"type": "option"
|
|
610
646
|
}
|
|
611
647
|
},
|
|
612
648
|
"hasDynamicHelp": false,
|
|
@@ -2969,5 +3005,5 @@
|
|
|
2969
3005
|
]
|
|
2970
3006
|
}
|
|
2971
3007
|
},
|
|
2972
|
-
"version": "2.
|
|
3008
|
+
"version": "2.6.0"
|
|
2973
3009
|
}
|