@swell/cli 2.5.8 → 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.js +2 -5
- 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 +7 -3
- package/dist/lib/swell-function-wrapper.js +53 -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() {
|
package/dist/commands/app/dev.js
CHANGED
|
@@ -257,11 +257,8 @@ ENVIRONMENT = "development"
|
|
|
257
257
|
.map((m) => String(m).toUpperCase())
|
|
258
258
|
.join(', '))}`);
|
|
259
259
|
}
|
|
260
|
-
if (config.route.headers) {
|
|
261
|
-
|
|
262
|
-
this.log(` Headers:`);
|
|
263
|
-
for (const header of headers)
|
|
264
|
-
this.log(` ${style.dim(header)}`);
|
|
260
|
+
if (config.route.headers?.length) {
|
|
261
|
+
this.log(` Headers: ${style.dim(config.route.headers.join(', '))}`);
|
|
265
262
|
}
|
|
266
263
|
if (config.route.cache?.timeout) {
|
|
267
264
|
this.log(` Cache timeout: ${style.dim(config.route.cache.timeout)}`);
|
|
@@ -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: {};
|
|
@@ -106,9 +107,10 @@ declare class SwellRequest {
|
|
|
106
107
|
* Merge values into app data for the current request.
|
|
107
108
|
* @param {object|string} idOrValues string to indicate app ID, or values to merge
|
|
108
109
|
* @param {object|undefined} values values to merge into app data
|
|
109
|
-
* @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
|
|
110
112
|
*/
|
|
111
|
-
appValues(idOrValues: object | string, values?: object | undefined): object
|
|
113
|
+
appValues(idOrValues: object | string, values?: object | undefined): object;
|
|
112
114
|
}
|
|
113
115
|
/**
|
|
114
116
|
* Class representing the Swell backend API.
|
|
@@ -134,12 +136,14 @@ declare class SwellAPI {
|
|
|
134
136
|
declare class SwellError extends Error {
|
|
135
137
|
constructor(message: any, options?: {});
|
|
136
138
|
status: any;
|
|
139
|
+
body: any;
|
|
137
140
|
}
|
|
138
141
|
/**
|
|
139
142
|
* Class representing a Swell response.
|
|
140
143
|
*/
|
|
141
144
|
declare class SwellResponse extends Response {
|
|
142
|
-
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>;
|
|
143
147
|
static _respondWithLogs(response: any, req: any): SwellResponse;
|
|
144
148
|
constructor(data: any, options?: {});
|
|
145
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) {
|
|
@@ -202,13 +208,17 @@ class SwellRequest {
|
|
|
202
208
|
* Merge values into app data for the current request.
|
|
203
209
|
* @param {object|string} idOrValues string to indicate app ID, or values to merge
|
|
204
210
|
* @param {object|undefined} values values to merge into app data
|
|
205
|
-
* @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
|
|
206
213
|
*/
|
|
207
214
|
appValues(idOrValues, values = undefined) {
|
|
208
|
-
const appId = typeof idOrValues === 'string' ?
|
|
215
|
+
const appId = typeof idOrValues === 'string' ? idOrValues : this.appId;
|
|
209
216
|
const appValues = typeof idOrValues === 'string' ? values : idOrValues;
|
|
210
|
-
if (!appId
|
|
211
|
-
|
|
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)');
|
|
212
222
|
}
|
|
213
223
|
return {
|
|
214
224
|
$app: {
|
|
@@ -316,6 +326,7 @@ class SwellAPI {
|
|
|
316
326
|
*/
|
|
317
327
|
class SwellError extends Error {
|
|
318
328
|
constructor(message, options = {}) {
|
|
329
|
+
const body = typeof message === 'string' ? undefined : message;
|
|
319
330
|
let formattedMessage;
|
|
320
331
|
if (typeof message === 'string') {
|
|
321
332
|
formattedMessage = message;
|
|
@@ -329,6 +340,7 @@ class SwellError extends Error {
|
|
|
329
340
|
super(formattedMessage);
|
|
330
341
|
this.name = 'SwellError';
|
|
331
342
|
this.status = options.status || 500;
|
|
343
|
+
this.body = body;
|
|
332
344
|
}
|
|
333
345
|
}
|
|
334
346
|
/**
|
|
@@ -358,27 +370,53 @@ class SwellResponse extends Response {
|
|
|
358
370
|
this._swellData = data;
|
|
359
371
|
this._swellOptions = options || {};
|
|
360
372
|
}
|
|
361
|
-
static _respond(req, response, context) {
|
|
373
|
+
static async _respond(req, response, context) {
|
|
362
374
|
let finalResponse = response;
|
|
363
|
-
|
|
375
|
+
const isHook = Boolean(req.data?.$event?.hook);
|
|
364
376
|
if (finalResponse instanceof Response &&
|
|
365
377
|
!(finalResponse instanceof SwellResponse)) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
+
}
|
|
370
385
|
}
|
|
371
386
|
else if (!(finalResponse instanceof SwellResponse)) {
|
|
372
387
|
finalResponse = new SwellResponse(response);
|
|
373
388
|
}
|
|
374
389
|
// Send logs back with the response for event hooks
|
|
375
|
-
if (
|
|
390
|
+
if (isHook) {
|
|
376
391
|
return SwellResponse._respondWithLogs(finalResponse, req);
|
|
377
392
|
}
|
|
378
393
|
// Ingest logs in the background
|
|
379
394
|
context.waitUntil(req.ingestLogs(finalResponse));
|
|
380
395
|
return finalResponse;
|
|
381
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
|
+
}
|
|
382
420
|
static _respondWithLogs(response, req) {
|
|
383
421
|
const ingestableLogs = req.getIngestableLogs(response);
|
|
384
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
|
}
|