@swell/cli 2.5.8 → 2.7.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/commands/app/frontend/dev.js +18 -0
- package/dist/commands/inspect/content.d.ts +25 -14
- package/dist/commands/inspect/content.js +34 -144
- package/dist/commands/inspect/functions.d.ts +48 -0
- package/dist/commands/inspect/functions.js +83 -0
- package/dist/commands/inspect/index.js +8 -7
- package/dist/commands/inspect/models.d.ts +17 -2
- package/dist/commands/inspect/models.js +114 -47
- package/dist/commands/inspect/notifications.d.ts +31 -0
- package/dist/commands/inspect/notifications.js +125 -0
- package/dist/commands/inspect/settings.d.ts +28 -0
- package/dist/commands/inspect/settings.js +82 -0
- package/dist/commands/inspect/webhooks.d.ts +34 -0
- package/dist/commands/inspect/webhooks.js +61 -0
- package/dist/create-app-command.d.ts +7 -0
- package/dist/create-app-command.js +66 -3
- package/dist/inspect-resource-command.d.ts +91 -0
- package/dist/inspect-resource-command.js +232 -0
- package/dist/lib/apps/index.d.ts +2 -1
- package/dist/lib/apps/index.js +43 -6
- package/dist/lib/apps/inspect-scope.d.ts +58 -0
- package/dist/lib/apps/inspect-scope.js +60 -0
- package/dist/lib/apps/object-id.d.ts +7 -0
- package/dist/lib/apps/object-id.js +9 -0
- package/dist/lib/apps/paths.js +8 -1
- package/dist/lib/apps/resolve.d.ts +16 -0
- package/dist/lib/apps/resolve.js +39 -0
- package/dist/lib/apps/slug.d.ts +29 -0
- package/dist/lib/apps/slug.js +12 -0
- 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/inspect/content.d.ts +39 -0
- package/dist/lib/inspect/content.js +76 -0
- package/dist/lib/inspect/notifications.d.ts +115 -0
- package/dist/lib/inspect/notifications.js +173 -0
- package/dist/lib/inspect/settings.d.ts +61 -0
- package/dist/lib/inspect/settings.js +56 -0
- package/dist/lib/inspect/table.d.ts +29 -0
- package/dist/lib/inspect/table.js +61 -0
- package/dist/lib/swell-function-wrapper.d.ts +7 -3
- package/dist/lib/swell-function-wrapper.js +53 -15
- package/dist/push-app-command.js +3 -2
- package/dist/swell-api-command.d.ts +2 -4
- package/dist/swell-api-command.js +58 -35
- package/oclif.manifest.json +364 -35
- package/package.json +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { GroupInfo } from './table.js';
|
|
2
|
+
export interface SettingRecord {
|
|
3
|
+
api?: string;
|
|
4
|
+
app_id?: string | null;
|
|
5
|
+
deprecated?: boolean | null;
|
|
6
|
+
fields?: Record<string, unknown>;
|
|
7
|
+
id?: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Settings identifier grammar (calibrated against `/data/:settings`).
|
|
13
|
+
*
|
|
14
|
+
* `app.<slug-or-hex>` → kind 'app' — slug must be translated to hex before
|
|
15
|
+
* a path GET; hex passes through.
|
|
16
|
+
* anything else valid → kind 'path' — direct path GET. The endpoint resolves
|
|
17
|
+
* bare system names (`taxes`), bare app
|
|
18
|
+
* hexes, and full dotted ids
|
|
19
|
+
* (`com.taxes`) on the path. No `name`
|
|
20
|
+
* query needed.
|
|
21
|
+
* anything else → kind 'invalid'
|
|
22
|
+
*
|
|
23
|
+
* The 3-segment `app.<slug>.<name>` form is rejected: settings collapse to one
|
|
24
|
+
* record per app at push time, so there is no sub-section identity the API can
|
|
25
|
+
* resolve back. Users wanting a single section run `--json | jq`.
|
|
26
|
+
*/
|
|
27
|
+
export type ParsedSettingsKey = {
|
|
28
|
+
kind: 'app';
|
|
29
|
+
ref: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'path';
|
|
32
|
+
segment: string;
|
|
33
|
+
} | {
|
|
34
|
+
input: string;
|
|
35
|
+
kind: 'invalid';
|
|
36
|
+
};
|
|
37
|
+
export declare function parseSettingsKey(input: string): ParsedSettingsKey;
|
|
38
|
+
/**
|
|
39
|
+
* Produce the column-1 paste-back key for a settings record.
|
|
40
|
+
*
|
|
41
|
+
* System (no app_id): `record.name` (e.g. `taxes`).
|
|
42
|
+
* App with slug: `app.<slug>`.
|
|
43
|
+
* App without slug: `app.<hex>` (still pasteable — `app.<hex>` resolves).
|
|
44
|
+
*
|
|
45
|
+
* App records carry `name === app_id` (the hex), which is meaningless to a
|
|
46
|
+
* human; we never surface it. Ultimate fallback is `record.id` for degenerate
|
|
47
|
+
* records that have neither `app_id` nor `name`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatSettingsKey(record: SettingRecord, appSlugById: Record<string, string>): string;
|
|
50
|
+
/**
|
|
51
|
+
* Group a settings record. System rows (`app_id` null/undefined) belong to the
|
|
52
|
+
* platform and render under a `system` divider at the top, mirroring the
|
|
53
|
+
* notifications shape.
|
|
54
|
+
*/
|
|
55
|
+
export declare function groupSettingsRecord(record: SettingRecord, appSlugById: Record<string, string>): GroupInfo;
|
|
56
|
+
/**
|
|
57
|
+
* `general` and `admin` are flagged `deprecated: true` in the base schema and
|
|
58
|
+
* still come back from the default list (no server-side filter equivalent —
|
|
59
|
+
* `deprecated[$ne]=true` doesn't take). Filter them out client-side.
|
|
60
|
+
*/
|
|
61
|
+
export declare function isDeprecatedRecord(record: SettingRecord): boolean;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const PATH_SEGMENT = /^[\w.-]+$/;
|
|
2
|
+
const APP_PREFIX = /^app\.([^.]+)$/;
|
|
3
|
+
export function parseSettingsKey(input) {
|
|
4
|
+
const appMatch = input.match(APP_PREFIX);
|
|
5
|
+
if (appMatch) {
|
|
6
|
+
return { kind: 'app', ref: appMatch[1] };
|
|
7
|
+
}
|
|
8
|
+
if (input.startsWith('app.')) {
|
|
9
|
+
return { kind: 'invalid', input };
|
|
10
|
+
}
|
|
11
|
+
if (PATH_SEGMENT.test(input)) {
|
|
12
|
+
return { kind: 'path', segment: input };
|
|
13
|
+
}
|
|
14
|
+
return { kind: 'invalid', input };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Produce the column-1 paste-back key for a settings record.
|
|
18
|
+
*
|
|
19
|
+
* System (no app_id): `record.name` (e.g. `taxes`).
|
|
20
|
+
* App with slug: `app.<slug>`.
|
|
21
|
+
* App without slug: `app.<hex>` (still pasteable — `app.<hex>` resolves).
|
|
22
|
+
*
|
|
23
|
+
* App records carry `name === app_id` (the hex), which is meaningless to a
|
|
24
|
+
* human; we never surface it. Ultimate fallback is `record.id` for degenerate
|
|
25
|
+
* records that have neither `app_id` nor `name`.
|
|
26
|
+
*/
|
|
27
|
+
export function formatSettingsKey(record, appSlugById) {
|
|
28
|
+
if (!record.app_id) {
|
|
29
|
+
return record.name ?? record.id ?? '-';
|
|
30
|
+
}
|
|
31
|
+
const slug = appSlugById[record.app_id] ?? record.app_id;
|
|
32
|
+
return `app.${slug}`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Group a settings record. System rows (`app_id` null/undefined) belong to the
|
|
36
|
+
* platform and render under a `system` divider at the top, mirroring the
|
|
37
|
+
* notifications shape.
|
|
38
|
+
*/
|
|
39
|
+
export function groupSettingsRecord(record, appSlugById) {
|
|
40
|
+
if (!record.app_id) {
|
|
41
|
+
return { slug: '<system>', label: 'system', order: 0 };
|
|
42
|
+
}
|
|
43
|
+
const resolved = appSlugById[record.app_id];
|
|
44
|
+
if (!resolved) {
|
|
45
|
+
return { slug: '<not resolved>', order: 2 };
|
|
46
|
+
}
|
|
47
|
+
return { slug: resolved };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `general` and `admin` are flagged `deprecated: true` in the base schema and
|
|
51
|
+
* still come back from the default list (no server-side filter equivalent —
|
|
52
|
+
* `deprecated[$ne]=true` doesn't take). Filter them out client-side.
|
|
53
|
+
*/
|
|
54
|
+
export function isDeprecatedRecord(record) {
|
|
55
|
+
return record.deprecated === true;
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface GroupInfo {
|
|
2
|
+
/** Grouping identity. Rows with the same slug land in the same section. */
|
|
3
|
+
slug: string;
|
|
4
|
+
/** Divider text. Defaults to `slug` when omitted. */
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Sort priority; lower renders first. Ties preserve insertion order. Default 1. */
|
|
7
|
+
order?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface KeyMetaRow {
|
|
10
|
+
/** Paste-back identifier rendered as column 1. */
|
|
11
|
+
key: string;
|
|
12
|
+
/** Optional compact status string. Empty or undefined renders the key alone. */
|
|
13
|
+
meta?: string;
|
|
14
|
+
/** Optional group; when some rows carry one, multiple groups produce dividers. */
|
|
15
|
+
group?: GroupInfo;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Render a two-column list: paste-back key + optional compact meta string.
|
|
19
|
+
*
|
|
20
|
+
* - Rows with meta render as ` <key-padded> <meta>`.
|
|
21
|
+
* - Rows without meta render as ` <key>` with no trailing whitespace.
|
|
22
|
+
* - Key column width is computed globally across all rows so meta columns
|
|
23
|
+
* align vertically.
|
|
24
|
+
* - Rows with a `group` are collected into sections; sections are separated
|
|
25
|
+
* by a blank line and a `── <label>` divider. Sections sort by `order`
|
|
26
|
+
* (lower first), ties preserve insertion order. A single group collapses
|
|
27
|
+
* to a flat listing.
|
|
28
|
+
*/
|
|
29
|
+
export declare function renderKeyMetaTable(rows: KeyMetaRow[]): string[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a two-column list: paste-back key + optional compact meta string.
|
|
3
|
+
*
|
|
4
|
+
* - Rows with meta render as ` <key-padded> <meta>`.
|
|
5
|
+
* - Rows without meta render as ` <key>` with no trailing whitespace.
|
|
6
|
+
* - Key column width is computed globally across all rows so meta columns
|
|
7
|
+
* align vertically.
|
|
8
|
+
* - Rows with a `group` are collected into sections; sections are separated
|
|
9
|
+
* by a blank line and a `── <label>` divider. Sections sort by `order`
|
|
10
|
+
* (lower first), ties preserve insertion order. A single group collapses
|
|
11
|
+
* to a flat listing.
|
|
12
|
+
*/
|
|
13
|
+
export function renderKeyMetaTable(rows) {
|
|
14
|
+
if (rows.length === 0) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
const keyWidth = Math.max(...rows.map((r) => r.key.length));
|
|
18
|
+
const formatRow = (row) => {
|
|
19
|
+
if (row.meta && row.meta.length > 0) {
|
|
20
|
+
return ` ${row.key.padEnd(keyWidth)} ${row.meta}`;
|
|
21
|
+
}
|
|
22
|
+
return ` ${row.key}`;
|
|
23
|
+
};
|
|
24
|
+
const hasGroups = rows.some((r) => r.group !== undefined);
|
|
25
|
+
if (!hasGroups) {
|
|
26
|
+
return rows.map((row) => formatRow(row));
|
|
27
|
+
}
|
|
28
|
+
const groupMap = new Map();
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
const info = row.group;
|
|
31
|
+
if (!info)
|
|
32
|
+
continue;
|
|
33
|
+
const existing = groupMap.get(info.slug);
|
|
34
|
+
if (existing) {
|
|
35
|
+
existing.rows.push(row);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
groupMap.set(info.slug, {
|
|
39
|
+
slug: info.slug,
|
|
40
|
+
label: info.label ?? info.slug,
|
|
41
|
+
order: info.order ?? 1,
|
|
42
|
+
rows: [row],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (groupMap.size === 1) {
|
|
47
|
+
return rows.map((row) => formatRow(row));
|
|
48
|
+
}
|
|
49
|
+
const ordered = [...groupMap.values()].sort((a, b) => a.order - b.order);
|
|
50
|
+
const lines = [];
|
|
51
|
+
for (const group of ordered) {
|
|
52
|
+
if (lines.length > 0) {
|
|
53
|
+
lines.push('');
|
|
54
|
+
}
|
|
55
|
+
lines.push(`── ${group.label}`);
|
|
56
|
+
for (const row of group.rows) {
|
|
57
|
+
lines.push(formatRow(row));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return lines;
|
|
61
|
+
}
|
|
@@ -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
|
package/dist/push-app-command.js
CHANGED
|
@@ -6,8 +6,9 @@ import * as path from 'node:path';
|
|
|
6
6
|
import Stream from 'node:stream';
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import { default as swellConfig } from './lib/app-config.js';
|
|
9
|
-
import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, CUSTOM_FRAMEWORK_SLUG, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig,
|
|
9
|
+
import { ConfigType, getFrontendProjectValidValues, allConfigFilesInDir, CUSTOM_FRAMEWORK_SLUG, appConfigFromFile, filePathExists, filePathExistsAsync, findAppConfig, getConfigTypeFromPath, getConfigTypeKeyFromValue, getFrontendProjectType, getProjectCommands, globAllFilesByPath, hashString, isPathDirectory, } from './lib/apps/index.js';
|
|
10
10
|
import { getGlobIgnorePathsChecker } from './lib/apps/paths.js';
|
|
11
|
+
import { slugFromApp } from './lib/apps/slug.js';
|
|
11
12
|
import { default as localConfig } from './lib/config.js';
|
|
12
13
|
import { toAppId } from './lib/create/index.js';
|
|
13
14
|
import { detectPackageManager, transformCommand, } from './lib/package-manager.js';
|
|
@@ -139,7 +140,7 @@ export class PushAppCommand extends RemoteAppCommand {
|
|
|
139
140
|
}
|
|
140
141
|
const appSlugId = (await select({
|
|
141
142
|
choices: apps.results.map((app) => ({
|
|
142
|
-
name: `${style.appConfigValue(app.name)} (${
|
|
143
|
+
name: `${style.appConfigValue(app.name)} (${slugFromApp(app) ?? app.id})`,
|
|
143
144
|
value: app.private_id,
|
|
144
145
|
})),
|
|
145
146
|
message: `Choose ${typeLabelPrefixed} to pull`,
|
|
@@ -1,14 +1,11 @@
|
|
|
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>;
|
|
6
7
|
protected catch(error: Error): Promise<any>;
|
|
7
8
|
private parseCommand;
|
|
8
|
-
/**
|
|
9
|
-
* Resolve app ObjectId from a friendly slug or return as-is if already an ObjectId.
|
|
10
|
-
*/
|
|
11
|
-
private resolveAppId;
|
|
12
9
|
/**
|
|
13
10
|
* Resolve function ID from app ID and function name.
|
|
14
11
|
*/
|
|
@@ -18,6 +15,7 @@ export declare abstract class SwellApiCommand extends SwellCommand {
|
|
|
18
15
|
* Build a function invocation request via the admin /:functions endpoint.
|
|
19
16
|
*/
|
|
20
17
|
private buildFunctionCallRequest;
|
|
18
|
+
private parseHeaders;
|
|
21
19
|
private parseQueryString;
|
|
22
20
|
private processBody;
|
|
23
21
|
private isFilePath;
|
|
@@ -1,12 +1,21 @@
|
|
|
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';
|
|
4
5
|
import { HttpMethod } from './lib/api.js';
|
|
6
|
+
import { resolveAppId } from './lib/apps/resolve.js';
|
|
5
7
|
import { SwellCommand } from './swell-command.js';
|
|
6
8
|
// Pattern to match /functions/{appId}/{functionName} with optional query string
|
|
7
9
|
const FUNCTION_PATH_REGEX = /^\/functions\/([^/]+)\/([^/?]+)(\?.*)?$/;
|
|
8
10
|
// Pattern to match /functions/{functionId} with optional query string
|
|
9
11
|
const FUNCTION_DIRECT_REGEX = /^\/functions\/([^/?]+)(\?.*)?$/;
|
|
12
|
+
// Shared --header / -H flag for function-call paths. Pass once per header.
|
|
13
|
+
// Only forwarded as $call.headers on /functions/* paths; ignored elsewhere.
|
|
14
|
+
export const headerFlag = Flags.string({
|
|
15
|
+
char: 'H',
|
|
16
|
+
description: "HTTP header to forward to a function (format: 'Name: value'). Repeat for multiple. Only applies to /functions/* paths.",
|
|
17
|
+
multiple: true,
|
|
18
|
+
});
|
|
10
19
|
export class SwellApiCommand extends SwellCommand {
|
|
11
20
|
async request(command, requestOptions = {}) {
|
|
12
21
|
const { paths, options, methodOverride } = await this.parseCommand(command);
|
|
@@ -29,7 +38,7 @@ export class SwellApiCommand extends SwellCommand {
|
|
|
29
38
|
const parsedInput = await super.parse(options, argv);
|
|
30
39
|
const { args, flags } = parsedInput;
|
|
31
40
|
const { path: requestPath } = args;
|
|
32
|
-
const { live, api, body } = flags;
|
|
41
|
+
const { live, api, body, header } = flags;
|
|
33
42
|
const isFrontendAPI = api === 'frontend';
|
|
34
43
|
if (!live) {
|
|
35
44
|
await this.api.setEnv('test');
|
|
@@ -41,21 +50,30 @@ export class SwellApiCommand extends SwellCommand {
|
|
|
41
50
|
throw new Error('Path must start with a forward slash (/)');
|
|
42
51
|
}
|
|
43
52
|
const processedBody = await this.processBody(body);
|
|
53
|
+
const callHeaders = this.parseHeaders(header);
|
|
44
54
|
// Check if this is a function call by name: /functions/{appId}/{functionName}
|
|
45
55
|
// Must check this first (more specific pattern)
|
|
46
56
|
const functionMatch = requestPath.match(FUNCTION_PATH_REGEX);
|
|
47
57
|
if (functionMatch) {
|
|
48
58
|
const [, appId, functionName, queryString] = functionMatch;
|
|
49
59
|
const functionId = await this.resolveFunctionId(appId, functionName);
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
return this.buildFunctionCallRequest(parsedInput, {
|
|
61
|
+
functionId,
|
|
62
|
+
body: processedBody,
|
|
63
|
+
query: this.parseQueryString(queryString),
|
|
64
|
+
headers: callHeaders,
|
|
65
|
+
});
|
|
52
66
|
}
|
|
53
67
|
// Check if this is a direct function call: /functions/{functionId}
|
|
54
68
|
const directMatch = requestPath.match(FUNCTION_DIRECT_REGEX);
|
|
55
69
|
if (directMatch) {
|
|
56
70
|
const [, functionId, queryString] = directMatch;
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
return this.buildFunctionCallRequest(parsedInput, {
|
|
72
|
+
functionId,
|
|
73
|
+
body: processedBody,
|
|
74
|
+
query: this.parseQueryString(queryString),
|
|
75
|
+
headers: callHeaders,
|
|
76
|
+
});
|
|
59
77
|
}
|
|
60
78
|
const paths = isFrontendAPI
|
|
61
79
|
? { frontendPath: requestPath }
|
|
@@ -68,28 +86,11 @@ export class SwellApiCommand extends SwellCommand {
|
|
|
68
86
|
},
|
|
69
87
|
};
|
|
70
88
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Resolve app ObjectId from a friendly slug or return as-is if already an ObjectId.
|
|
73
|
-
*/
|
|
74
|
-
async resolveAppId(appIdOrSlug) {
|
|
75
|
-
// If it looks like an ObjectId (24 hex chars), return as-is
|
|
76
|
-
if (/^[\da-f]{24}$/i.test(appIdOrSlug)) {
|
|
77
|
-
return appIdOrSlug;
|
|
78
|
-
}
|
|
79
|
-
// Fetch all installed apps and filter by public_id or private_id client-side
|
|
80
|
-
const installedApps = await this.api.get({ adminPath: `/client/apps` });
|
|
81
|
-
const app = installedApps?.results?.find((a) => a.app_public_id === appIdOrSlug ||
|
|
82
|
-
a.app_private_id === `_${appIdOrSlug}`);
|
|
83
|
-
if (!app) {
|
|
84
|
-
throw new Error(`App '${appIdOrSlug}' not found`);
|
|
85
|
-
}
|
|
86
|
-
return app.app_id;
|
|
87
|
-
}
|
|
88
89
|
/**
|
|
89
90
|
* Resolve function ID from app ID and function name.
|
|
90
91
|
*/
|
|
91
92
|
async resolveFunctionId(appIdOrSlug, functionName) {
|
|
92
|
-
const appId = await this.
|
|
93
|
+
const appId = await resolveAppId(this.api, appIdOrSlug);
|
|
93
94
|
const functionRecord = await this.api.get({ adminPath: `/data/:functions` }, {
|
|
94
95
|
query: {
|
|
95
96
|
app_id: appId,
|
|
@@ -108,27 +109,49 @@ export class SwellApiCommand extends SwellCommand {
|
|
|
108
109
|
/**
|
|
109
110
|
* Build a function invocation request via the admin /:functions endpoint.
|
|
110
111
|
*/
|
|
111
|
-
buildFunctionCallRequest(parsedInput,
|
|
112
|
+
buildFunctionCallRequest(parsedInput, call) {
|
|
112
113
|
// 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
|
-
},
|
|
114
|
+
// Only merge if body is a plain object; otherwise use body or query alone
|
|
115
|
+
const mergedData = this.isPlainObject(call.body)
|
|
116
|
+
? { ...call.query, ...call.body }
|
|
117
|
+
: call.body ?? call.query;
|
|
118
|
+
const $call = {
|
|
119
|
+
data: mergedData,
|
|
120
|
+
method: this.method,
|
|
122
121
|
};
|
|
122
|
+
if (Object.keys(call.headers).length > 0) {
|
|
123
|
+
$call.headers = call.headers;
|
|
124
|
+
}
|
|
123
125
|
return {
|
|
124
126
|
...parsedInput,
|
|
125
|
-
paths: { adminPath: `/data/:functions/${functionId}` },
|
|
127
|
+
paths: { adminPath: `/data/:functions/${call.functionId}` },
|
|
126
128
|
options: {
|
|
127
|
-
body:
|
|
129
|
+
body: { $call },
|
|
128
130
|
},
|
|
129
131
|
methodOverride: HttpMethod.PUT,
|
|
130
132
|
};
|
|
131
133
|
}
|
|
134
|
+
// Parse repeated `-H "Name: value"` flag entries into a map. Splits on first colon
|
|
135
|
+
// so values containing colons (e.g. `Authorization: Bearer x:y`) survive intact.
|
|
136
|
+
parseHeaders(headerArgs) {
|
|
137
|
+
if (!headerArgs?.length) {
|
|
138
|
+
return {};
|
|
139
|
+
}
|
|
140
|
+
const result = {};
|
|
141
|
+
for (const entry of headerArgs) {
|
|
142
|
+
const colonIndex = entry.indexOf(':');
|
|
143
|
+
if (colonIndex < 1) {
|
|
144
|
+
throw new Error(`Invalid header '${entry}'. Expected format: 'Name: value'.`);
|
|
145
|
+
}
|
|
146
|
+
const name = entry.slice(0, colonIndex).trim();
|
|
147
|
+
const value = entry.slice(colonIndex + 1).trim();
|
|
148
|
+
if (!name) {
|
|
149
|
+
throw new Error(`Invalid header '${entry}'. Header name cannot be empty.`);
|
|
150
|
+
}
|
|
151
|
+
result[name] = value;
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
132
155
|
parseQueryString(queryString) {
|
|
133
156
|
if (!queryString) {
|
|
134
157
|
return {};
|