@swell/cli 1.0.2 → 2.0.1-alpha.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/LICENSE.md +21 -0
- package/README.md +1 -1
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +7 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +7 -0
- package/dist/app-command.d.ts +22 -0
- package/dist/app-command.js +45 -0
- package/dist/commands/app/_pull.d.ts +8 -0
- package/dist/commands/app/_pull.js +45 -0
- package/dist/commands/app/_uninstall.d.ts +12 -0
- package/dist/commands/app/_uninstall.js +45 -0
- package/dist/commands/app/info.d.ts +15 -0
- package/dist/commands/app/info.js +92 -0
- package/dist/commands/app/install.d.ts +17 -0
- package/dist/commands/app/install.js +177 -0
- package/dist/commands/app/push.d.ts +17 -0
- package/dist/commands/app/push.js +158 -0
- package/dist/commands/app/version.d.ts +18 -0
- package/dist/commands/app/version.js +156 -0
- package/dist/commands/app/watch.d.ts +15 -0
- package/dist/commands/app/watch.js +110 -0
- package/dist/commands/create/app.d.ts +21 -0
- package/dist/commands/create/app.js +210 -0
- package/dist/commands/create/content.d.ts +19 -0
- package/dist/commands/create/content.js +87 -0
- package/dist/commands/create/function.d.ts +25 -0
- package/dist/commands/create/function.js +191 -0
- package/dist/commands/create/index.d.ts +6 -0
- package/dist/commands/create/index.js +34 -0
- package/dist/commands/create/model.d.ts +19 -0
- package/dist/commands/create/model.js +87 -0
- package/dist/commands/create/notification.d.ts +25 -0
- package/dist/commands/create/notification.js +170 -0
- package/dist/commands/login.d.ts +22 -0
- package/dist/commands/login.js +77 -0
- package/dist/commands/logout.d.ts +8 -0
- package/dist/commands/logout.js +45 -0
- package/dist/commands/logs.d.ts +23 -0
- package/dist/commands/logs.js +207 -0
- package/dist/commands/switch.d.ts +10 -0
- package/dist/commands/switch.js +41 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +24 -0
- package/dist/create-collection-command.d.ts +26 -0
- package/dist/create-collection-command.js +97 -0
- package/dist/create-command.d.ts +16 -0
- package/dist/create-command.js +59 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/api.d.ts +13 -0
- package/dist/lib/api.js +117 -0
- package/dist/lib/app-config.d.ts +29 -0
- package/dist/lib/app-config.js +127 -0
- package/dist/lib/apps/app-config.d.ts +42 -0
- package/dist/lib/apps/app-config.js +167 -0
- package/dist/lib/apps/index.d.ts +67 -0
- package/dist/lib/apps/index.js +167 -0
- package/dist/lib/apps/paths.d.ts +18 -0
- package/dist/lib/apps/paths.js +54 -0
- package/dist/lib/bundle.d.ts +1 -0
- package/dist/lib/bundle.js +40 -0
- package/dist/lib/config.d.ts +37 -0
- package/dist/lib/config.js +131 -0
- package/dist/lib/constants.d.ts +5 -0
- package/dist/lib/constants.js +17 -0
- package/dist/lib/create/content.d.ts +23 -0
- package/dist/lib/create/content.js +40 -0
- package/dist/lib/create/function.d.ts +17 -0
- package/dist/lib/create/function.js +36 -0
- package/dist/lib/create/index.d.ts +13 -0
- package/dist/lib/create/index.js +20 -0
- package/dist/lib/create/model.d.ts +22 -0
- package/dist/lib/create/model.js +30 -0
- package/dist/lib/create/notification.d.ts +25 -0
- package/dist/lib/create/notification.js +1 -0
- package/dist/lib/info.d.ts +38 -0
- package/dist/lib/info.js +91 -0
- package/dist/lib/logs/index.d.ts +23 -0
- package/dist/lib/logs/index.js +88 -0
- package/dist/lib/logs/line-output.d.ts +5 -0
- package/dist/lib/logs/line-output.js +13 -0
- package/dist/lib/logs/output.d.ts +7 -0
- package/dist/lib/logs/output.js +8 -0
- package/dist/lib/logs/table-output.d.ts +11 -0
- package/dist/lib/logs/table-output.js +128 -0
- package/dist/lib/sessions.d.ts +9 -0
- package/dist/lib/sessions.js +110 -0
- package/dist/lib/stores.d.ts +63 -0
- package/dist/lib/stores.js +173 -0
- package/dist/lib/style.d.ts +24 -0
- package/dist/lib/style.js +29 -0
- package/dist/lib/swell-function-wrapper.d.ts +88 -0
- package/dist/lib/swell-function-wrapper.js +302 -0
- package/dist/remote-app-command.d.ts +36 -0
- package/dist/remote-app-command.js +367 -0
- package/dist/swell-command.d.ts +21 -0
- package/dist/swell-command.js +43 -0
- package/oclif.manifest.json +1085 -0
- package/package.json +89 -42
- package/LICENSE +0 -674
- package/dist/bin/multipart-parser-85ef9626.js +0 -456
- package/dist/bin/swell-0f71f07d.js +0 -30670
- package/dist/bin/swell.js +0 -24
- package/dist/man/swell.1 +0 -85
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const origialConsoleLog = console.log;
|
|
3
|
+
addEventListener('fetch', (event) => {
|
|
4
|
+
event.respondWith(handleRequest(event.request, event.env, event));
|
|
5
|
+
});
|
|
6
|
+
async function handleRequest(originalRequest, _env, context) {
|
|
7
|
+
const req = new SwellRequest(originalRequest, context);
|
|
8
|
+
await req.initialize();
|
|
9
|
+
let response;
|
|
10
|
+
try {
|
|
11
|
+
response = await executeModuleHandler(req, context);
|
|
12
|
+
}
|
|
13
|
+
catch (err) {
|
|
14
|
+
// Log the error for Swell
|
|
15
|
+
console.error(err.stack);
|
|
16
|
+
response = new SwellResponse({ error: err.message }, { status: err.status || 500 });
|
|
17
|
+
}
|
|
18
|
+
// Create a response by default if one is not returned
|
|
19
|
+
if (!(response instanceof Response)) {
|
|
20
|
+
response = new SwellResponse(response);
|
|
21
|
+
}
|
|
22
|
+
// Send logs back to Swell
|
|
23
|
+
context.waitUntil(req.ingestLogs(response));
|
|
24
|
+
return response;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Invokes one of the function types that are available.
|
|
28
|
+
*
|
|
29
|
+
* Type-1: Named export function handlers
|
|
30
|
+
* ```
|
|
31
|
+
* export function post (req) {
|
|
32
|
+
*
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* Important note:
|
|
37
|
+
* - `delete` func cannot be implemented in Type-1 since it's a reserved name. So, consider using Type-3.
|
|
38
|
+
*
|
|
39
|
+
* Type-2: Default export function handlers
|
|
40
|
+
* ```
|
|
41
|
+
* export default function (req) {
|
|
42
|
+
*
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* Type-3: Default object with named function handlers
|
|
47
|
+
* ```
|
|
48
|
+
* export default {
|
|
49
|
+
* delete(req) {
|
|
50
|
+
*
|
|
51
|
+
* },
|
|
52
|
+
* post(req) {
|
|
53
|
+
*
|
|
54
|
+
* },
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @param {SwellRequest} req
|
|
59
|
+
* @param {Event} context
|
|
60
|
+
*/
|
|
61
|
+
async function executeModuleHandler(req, context) {
|
|
62
|
+
const method = req.method.toLowerCase();
|
|
63
|
+
const defaults = moduleExports.default;
|
|
64
|
+
if (moduleExports[method]) {
|
|
65
|
+
return moduleExports[method](req, context);
|
|
66
|
+
}
|
|
67
|
+
else if (defaults) {
|
|
68
|
+
if (typeof defaults === 'function') {
|
|
69
|
+
return defaults(req, context);
|
|
70
|
+
}
|
|
71
|
+
else if (defaults[method]) {
|
|
72
|
+
return defaults[method](req, context);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`Function does not export a method to handle ${method.toUpperCase()} requests`);
|
|
76
|
+
}
|
|
77
|
+
class SwellRequest {
|
|
78
|
+
constructor(req, context) {
|
|
79
|
+
this.originalRequest = req;
|
|
80
|
+
this.assignRequestProps(req);
|
|
81
|
+
// Set environment specific variables
|
|
82
|
+
this.context = context;
|
|
83
|
+
this.appId = req.headers.get('Swell-App-Id');
|
|
84
|
+
this.storeId = req.headers.get('Swell-Store-Id');
|
|
85
|
+
this.accessToken = req.headers.get('Swell-Access-Token');
|
|
86
|
+
this.publicKey = req.headers.get('Swell-Public-Key');
|
|
87
|
+
this.store = this.parseJson(req.headers.get('Swell-Store-Details'));
|
|
88
|
+
this.session = this.parseJson(req.headers.get('Swell-Session'));
|
|
89
|
+
this.logParams = this.parseJson(req.headers.get('Swell-Request-Log'));
|
|
90
|
+
this.apiHost = req.headers.get('Swell-API-Host') || 'https://api.schema.io';
|
|
91
|
+
this.id = req.headers.get('Swell-Request-ID') || this.logParams?.req_id;
|
|
92
|
+
// Swell client
|
|
93
|
+
this.swell = new SwellAPI(this, context);
|
|
94
|
+
// URL of the original request
|
|
95
|
+
this.url;
|
|
96
|
+
// Original body of the request, JSON if applicable
|
|
97
|
+
this.body = {};
|
|
98
|
+
// URL query parameters as an object
|
|
99
|
+
this.query = {};
|
|
100
|
+
// Combined object of body and query parameters
|
|
101
|
+
this.data = {};
|
|
102
|
+
// Internal logs
|
|
103
|
+
this._logs = [];
|
|
104
|
+
}
|
|
105
|
+
assignRequestProps(req) {
|
|
106
|
+
['ur', 'method', 'headers', 'referrer', 'credentials'].forEach((prop) => {
|
|
107
|
+
this[prop] = req[prop];
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async initialize() {
|
|
111
|
+
this.body = await this.originalRequest.text();
|
|
112
|
+
try {
|
|
113
|
+
this.data = JSON.parse(this.body);
|
|
114
|
+
this.body = { ...this.data };
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
this.data = {};
|
|
118
|
+
}
|
|
119
|
+
this.url = new URL(this.originalRequest.url);
|
|
120
|
+
// Convert the query parameters to an object
|
|
121
|
+
this.url.searchParams.forEach((value, key) => {
|
|
122
|
+
this.query[key] = value;
|
|
123
|
+
this.data[key] = value;
|
|
124
|
+
});
|
|
125
|
+
// Bind the console methods to the request
|
|
126
|
+
console.log = this.log.bind(this, 'info');
|
|
127
|
+
console.info = this.log.bind(this, 'info');
|
|
128
|
+
console.debug = this.log.bind(this, 'debug');
|
|
129
|
+
console.warn = this.log.bind(this, 'warn');
|
|
130
|
+
console.error = this.log.bind(this, 'error');
|
|
131
|
+
}
|
|
132
|
+
parseJson(input) {
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(input);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
log(level, ...line) {
|
|
141
|
+
origialConsoleLog(...line);
|
|
142
|
+
this._logs.push({
|
|
143
|
+
date: Date.now(),
|
|
144
|
+
line: line.map((l) => JSON.stringify(l)),
|
|
145
|
+
...(level !== 'info' ? { level } : {}),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async ingestLogs(response) {
|
|
149
|
+
if (!this.logParams) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (this.logParams.$start) {
|
|
153
|
+
this.logParams.time = Date.now() - this.logParams.$start;
|
|
154
|
+
delete this.logParams.$start;
|
|
155
|
+
}
|
|
156
|
+
const result = await this.swell.post('/:logs', {
|
|
157
|
+
$ingest_function_logs: {
|
|
158
|
+
params: {
|
|
159
|
+
...this.logParams,
|
|
160
|
+
message: {
|
|
161
|
+
...this.logParams?.message,
|
|
162
|
+
logs: this._logs,
|
|
163
|
+
status: response.status,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
if (!result?.success) {
|
|
169
|
+
console.error('Error ingesting logs', result);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
class SwellAPI {
|
|
174
|
+
constructor(req, context) {
|
|
175
|
+
this.request = req;
|
|
176
|
+
this.baseUrl = req.apiHost;
|
|
177
|
+
this.basicAuth = `${req.storeId}:${req.accessToken}`;
|
|
178
|
+
this.context = context;
|
|
179
|
+
}
|
|
180
|
+
toBase64(inputString) {
|
|
181
|
+
const utf8Bytes = new TextEncoder().encode(inputString);
|
|
182
|
+
let base64String = '';
|
|
183
|
+
for (let i = 0; i < utf8Bytes.length; i += 3) {
|
|
184
|
+
const chunk = utf8Bytes.slice(i, i + 3);
|
|
185
|
+
base64String += btoa(String.fromCharCode(...chunk));
|
|
186
|
+
}
|
|
187
|
+
return base64String;
|
|
188
|
+
}
|
|
189
|
+
stringifyQuery(queryObject, prefix) {
|
|
190
|
+
const result = [];
|
|
191
|
+
for (const [key, value] of Object.entries(queryObject)) {
|
|
192
|
+
const prefixKey = prefix ? `${prefix}[${key}]` : key;
|
|
193
|
+
const isObject = value !== null && typeof value === 'object';
|
|
194
|
+
const encodedResult = isObject
|
|
195
|
+
? this.stringifyQuery(value, prefixKey)
|
|
196
|
+
: `${encodeURIComponent(prefixKey)}=${encodeURIComponent(value)}`;
|
|
197
|
+
result.push(encodedResult);
|
|
198
|
+
}
|
|
199
|
+
return result.join('&');
|
|
200
|
+
}
|
|
201
|
+
async makeRequest(method, url, data) {
|
|
202
|
+
const requestOptions = {
|
|
203
|
+
method,
|
|
204
|
+
headers: {
|
|
205
|
+
Authorization: `Basic ${this.toBase64(this.basicAuth)}`,
|
|
206
|
+
'User-Agent': 'swell-functions/1.0',
|
|
207
|
+
'Content-Type': 'application/json',
|
|
208
|
+
...(this.request.id ? { 'Swell-Request-ID': this.request.id } : {}),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
let query = '';
|
|
212
|
+
if (data) {
|
|
213
|
+
try {
|
|
214
|
+
if (method === 'GET') {
|
|
215
|
+
query = `?${this.stringifyQuery(data)}`;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
requestOptions.body = JSON.stringify(data);
|
|
219
|
+
requestOptions.headers['Content-Length'] = requestOptions.body.length;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
throw new Error(`Error serializing data: ${data}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const endpointUrl = String(url).startsWith('/') ? url.substring(1) : url;
|
|
227
|
+
const response = await fetch(`${this.baseUrl}/${endpointUrl}${query}`, requestOptions);
|
|
228
|
+
const responseText = await response.text();
|
|
229
|
+
let result;
|
|
230
|
+
try {
|
|
231
|
+
result = JSON.parse(responseText);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
result = String(responseText || '').trim();
|
|
235
|
+
}
|
|
236
|
+
if (response.status > 299) {
|
|
237
|
+
throw new SwellError(result, {
|
|
238
|
+
status: response.status,
|
|
239
|
+
method,
|
|
240
|
+
endpointUrl,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
else if (method !== 'GET' && result?.errors) {
|
|
244
|
+
throw new SwellError(result.errors, { status: 400, method, endpointUrl });
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
async get(url, query) {
|
|
249
|
+
return this.makeRequest('GET', url, query);
|
|
250
|
+
}
|
|
251
|
+
async put(url, data) {
|
|
252
|
+
return this.makeRequest('PUT', url, data);
|
|
253
|
+
}
|
|
254
|
+
async post(url, data) {
|
|
255
|
+
return this.makeRequest('POST', url, data);
|
|
256
|
+
}
|
|
257
|
+
async delete(url, data) {
|
|
258
|
+
return this.makeRequest('DELETE', url, data);
|
|
259
|
+
}
|
|
260
|
+
async settings(id = this.request.appId) {
|
|
261
|
+
return this.makeRequest('GET', `/settings/${id}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
class SwellError extends Error {
|
|
265
|
+
constructor(message, options = {}) {
|
|
266
|
+
let formattedMessage;
|
|
267
|
+
if (typeof message === 'string') {
|
|
268
|
+
formattedMessage = message;
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
formattedMessage = JSON.stringify(message, null, 2);
|
|
272
|
+
}
|
|
273
|
+
if (options.method && options.endpointUrl) {
|
|
274
|
+
formattedMessage = `${options.method} /${options.endpointUrl}\n${formattedMessage}`;
|
|
275
|
+
}
|
|
276
|
+
super(formattedMessage);
|
|
277
|
+
this.name = 'SwellError';
|
|
278
|
+
this.status = options.status || 500;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
class SwellResponse extends Response {
|
|
282
|
+
constructor(data, options = {}) {
|
|
283
|
+
const resultHeaders = {};
|
|
284
|
+
let result;
|
|
285
|
+
if (typeof data === 'string') {
|
|
286
|
+
result = data;
|
|
287
|
+
resultHeaders['Content-Type'] = 'text/plain;charset=UTF-8';
|
|
288
|
+
}
|
|
289
|
+
else if (data !== undefined) {
|
|
290
|
+
result = JSON.stringify(data, null, 2);
|
|
291
|
+
resultHeaders['Content-Type'] = 'application/json;charset=UTF-8';
|
|
292
|
+
}
|
|
293
|
+
super(result?.toString?.('utf-8'), {
|
|
294
|
+
status: 200,
|
|
295
|
+
...options,
|
|
296
|
+
headers: {
|
|
297
|
+
...resultHeaders,
|
|
298
|
+
...(options.headers || {}),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AppCommand } from './app-command.js';
|
|
2
|
+
import { App, AppConfig, AppVersion } from './lib/apps/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Extends the AppCommand with methods that ease managing remote apps and
|
|
5
|
+
* configurations.
|
|
6
|
+
*/
|
|
7
|
+
export declare abstract class RemoteAppCommand extends AppCommand {
|
|
8
|
+
static orientation: AppCommandOrientation;
|
|
9
|
+
app: App;
|
|
10
|
+
dashboardUrl(flags?: {
|
|
11
|
+
[flag: string]: any;
|
|
12
|
+
}, storeId?: string): string;
|
|
13
|
+
protected deleteConfig(configId: string): Promise<any>;
|
|
14
|
+
protected getApp(id?: string): Promise<App>;
|
|
15
|
+
protected getAppWithConfig(id?: string, errorNotFound?: boolean): Promise<App>;
|
|
16
|
+
protected getCreateUpdateApp(updateApp?: App): Promise<App>;
|
|
17
|
+
protected handleRequestErrors(request: () => any, spinnerError?: () => void): Promise<any>;
|
|
18
|
+
init(): Promise<void>;
|
|
19
|
+
protected postConfig(configJson: any): Promise<any>;
|
|
20
|
+
protected pushConfigFile(appConfig: AppConfig): Promise<any>;
|
|
21
|
+
protected removeConfig(config: AppConfig): Promise<any>;
|
|
22
|
+
protected showErrors(errors: any, spinnerError?: () => void): void;
|
|
23
|
+
protected syncAppConfigs(): Promise<{
|
|
24
|
+
installed: string[];
|
|
25
|
+
removed: string[];
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Reads the .swellrc file and sets up the remote app configuration.
|
|
29
|
+
*
|
|
30
|
+
* @returns App
|
|
31
|
+
*/
|
|
32
|
+
private initApp;
|
|
33
|
+
private showOrientation;
|
|
34
|
+
protected getVersions(version?: string | undefined | null): Promise<AppVersion[]>;
|
|
35
|
+
protected swellRcPath(): string;
|
|
36
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { inflect } from 'inflection';
|
|
2
|
+
import snakeCase from 'lodash/snakeCase.js';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { AppCommand } from './app-command.js';
|
|
7
|
+
import { appConfigFileExists, readRcFile, writeRcFile, } from './lib/app-config.js';
|
|
8
|
+
import { FunctionProcessingError, IgnoringFileError, allConfigFilesPaths, appConfigFromFile, appLogoIcon, batchAppConfigsByType, configFilePath, isAppLogoIcon, } from './lib/apps/index.js';
|
|
9
|
+
import { default as swellConfig } from './lib/config.js';
|
|
10
|
+
import { getLoginHost } from './lib/constants.js';
|
|
11
|
+
import style from './lib/style.js';
|
|
12
|
+
/**
|
|
13
|
+
* Extends the AppCommand with methods that ease managing remote apps and
|
|
14
|
+
* configurations.
|
|
15
|
+
*/
|
|
16
|
+
export class RemoteAppCommand extends AppCommand {
|
|
17
|
+
// allows child commands to set orientation
|
|
18
|
+
// most of the times this will be the target environment
|
|
19
|
+
static orientation;
|
|
20
|
+
// an API instance of the current app definition
|
|
21
|
+
app = {};
|
|
22
|
+
dashboardUrl(flags = {}, storeId) {
|
|
23
|
+
const { env } = flags;
|
|
24
|
+
const envFlag = env || this.ctor.orientation?.env;
|
|
25
|
+
const outputEnv = envFlag === 'live' ? '' : !envFlag ? '' : 'test';
|
|
26
|
+
return `${getLoginHost(storeId)}/admin/${outputEnv ? `${outputEnv}/` : ''}apps/${this.app.id}`;
|
|
27
|
+
}
|
|
28
|
+
async deleteConfig(configId) {
|
|
29
|
+
if (!this.app) {
|
|
30
|
+
this.error('App is not defined');
|
|
31
|
+
}
|
|
32
|
+
return this.api.delete({
|
|
33
|
+
adminPath: `/apps/${this.app.id}/configs/${configId}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async getApp(id = '') {
|
|
37
|
+
const app = await this.api.get({ adminPath: `/apps/${id}` });
|
|
38
|
+
// Get app configs for processing
|
|
39
|
+
app.configs = [];
|
|
40
|
+
const configsResponse = await this.api.get({ adminPath: `/apps/${id}/configs` }, { query: { limit: 1000 } });
|
|
41
|
+
this.debugJson('getApp: configsResponse', configsResponse);
|
|
42
|
+
// for each config, we check if there's a local file and we add it to the
|
|
43
|
+
// AppConfig object
|
|
44
|
+
for (const config of configsResponse.results || []) {
|
|
45
|
+
const localPath = configFilePath(this.appPath, config);
|
|
46
|
+
if (appConfigFileExists(localPath)) {
|
|
47
|
+
config.localPath = localPath;
|
|
48
|
+
}
|
|
49
|
+
app.configs.push(config);
|
|
50
|
+
}
|
|
51
|
+
this.debugJson('getApp: remote app', app);
|
|
52
|
+
return app;
|
|
53
|
+
}
|
|
54
|
+
async getAppWithConfig(id = '', errorNotFound = true) {
|
|
55
|
+
const swellId = this.app?.id;
|
|
56
|
+
const appConfigId = this.appConfig.get('id');
|
|
57
|
+
if (!appConfigId) {
|
|
58
|
+
this.error(`App ${style.appConfigName('id')} is not defined in swell.json.`);
|
|
59
|
+
}
|
|
60
|
+
let appId = id || swellId;
|
|
61
|
+
if (!appId) {
|
|
62
|
+
const privateId = this.app?.private_id;
|
|
63
|
+
if (!privateId) {
|
|
64
|
+
this.error(`App ${style.appConfigName('id')} is not defined in swell.json.`);
|
|
65
|
+
}
|
|
66
|
+
if (privateId !== appConfigId) {
|
|
67
|
+
this.error(`App ${style.appConfigName('id')} must be defined in snake_case: ${style.appConfigValue(appConfigId)}.`);
|
|
68
|
+
}
|
|
69
|
+
appId = privateId;
|
|
70
|
+
}
|
|
71
|
+
// we initialize the app with the id so we can use it in the API calls
|
|
72
|
+
let app = {};
|
|
73
|
+
try {
|
|
74
|
+
app = await this.getApp(appId);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
this.debug('getApp: app not found', error);
|
|
78
|
+
let errorJson;
|
|
79
|
+
try {
|
|
80
|
+
errorJson = JSON.parse(error.message);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// if there's a JSON parse error, throw the original error
|
|
84
|
+
this.error(error);
|
|
85
|
+
}
|
|
86
|
+
// if the app doesn't exist, we don't want to throw an error as we might
|
|
87
|
+
// want to create it.
|
|
88
|
+
//
|
|
89
|
+
// if the `id` was passed as argument though, we want to throw an error so
|
|
90
|
+
// we don't create a loop.
|
|
91
|
+
//
|
|
92
|
+
// we don't throw an error ever sometimes, like when pushing an app
|
|
93
|
+
const shouldThrow = !errorNotFound && (errorJson.error.message !== 'App not found' || id);
|
|
94
|
+
if (shouldThrow) {
|
|
95
|
+
const errorMessage = typeof errorJson.error === 'string'
|
|
96
|
+
? errorJson.error
|
|
97
|
+
: errorJson.error.message;
|
|
98
|
+
this.error(errorMessage);
|
|
99
|
+
}
|
|
100
|
+
if (swellId) {
|
|
101
|
+
app = await this.getAppWithConfig(swellId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return app;
|
|
105
|
+
}
|
|
106
|
+
async getCreateUpdateApp(updateApp) {
|
|
107
|
+
const spinner = ora();
|
|
108
|
+
const remoteApp = await readRcFile(this.swellRcPath());
|
|
109
|
+
/* eslint-disable camelcase */
|
|
110
|
+
const body = {
|
|
111
|
+
...this.appConfig.store,
|
|
112
|
+
// we store locally the private id as the id, but we need to switch it
|
|
113
|
+
// when posting to the API
|
|
114
|
+
id: undefined,
|
|
115
|
+
logo_icon: null,
|
|
116
|
+
private_id: this.app?.private_id,
|
|
117
|
+
...(remoteApp?.id ? { source_id: remoteApp.id } : undefined),
|
|
118
|
+
};
|
|
119
|
+
const logoIcon = appLogoIcon(this.appPath);
|
|
120
|
+
if (logoIcon) {
|
|
121
|
+
const icon = fs.readFileSync(logoIcon, { encoding: 'base64' });
|
|
122
|
+
body.logo_icon = icon;
|
|
123
|
+
}
|
|
124
|
+
/* eslint-enable camelcase */
|
|
125
|
+
let app = updateApp || {};
|
|
126
|
+
let appCreated = false;
|
|
127
|
+
if (!app.id) {
|
|
128
|
+
// May already have a dev instance
|
|
129
|
+
const existingDevApp = await this.api.get({
|
|
130
|
+
adminPath: `/apps/${this.app?.private_id}`,
|
|
131
|
+
});
|
|
132
|
+
spinner.start(`Creating app...`);
|
|
133
|
+
if (existingDevApp) {
|
|
134
|
+
app = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${existingDevApp.id}` }, { body }), () => spinner.fail('Error creating app'));
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
app = await this.handleRequestErrors(async () => this.api.post({ adminPath: '/apps' }, { body }), () => spinner.fail('Error creating app'));
|
|
138
|
+
}
|
|
139
|
+
appCreated = true;
|
|
140
|
+
}
|
|
141
|
+
else if (app.id) {
|
|
142
|
+
spinner.start(`Updating app...`);
|
|
143
|
+
app = await this.handleRequestErrors(async () => this.api.put({ adminPath: `/apps/${app.id}` }, { body }), () => spinner.fail('Error updating app'));
|
|
144
|
+
}
|
|
145
|
+
if (app?.id) {
|
|
146
|
+
await writeRcFile(this.swellRcPath(), { id: app.id });
|
|
147
|
+
// if not failed, we want to display correct messaging
|
|
148
|
+
if (spinner.isSpinning) {
|
|
149
|
+
if (appCreated) {
|
|
150
|
+
spinner.succeed('Development app created.');
|
|
151
|
+
this.log();
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
spinner.stop();
|
|
155
|
+
}
|
|
156
|
+
this.log('App details');
|
|
157
|
+
spinner.succeed('app details');
|
|
158
|
+
if (logoIcon) {
|
|
159
|
+
spinner.succeed('app logo');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.log();
|
|
163
|
+
return app;
|
|
164
|
+
}
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
async handleRequestErrors(request, spinnerError) {
|
|
168
|
+
try {
|
|
169
|
+
const response = await request();
|
|
170
|
+
if (response?.status !== 200 && response?.errors) {
|
|
171
|
+
this.showErrors(response.errors, spinnerError);
|
|
172
|
+
this.error('API request failed');
|
|
173
|
+
}
|
|
174
|
+
return response;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
let jsonError;
|
|
178
|
+
try {
|
|
179
|
+
jsonError = JSON.parse(error.message);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// noop, likely JSON parse error
|
|
183
|
+
}
|
|
184
|
+
if (jsonError && jsonError.error) {
|
|
185
|
+
this.showErrors(jsonError, spinnerError);
|
|
186
|
+
this.error('API request failed');
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async init() {
|
|
192
|
+
await super.init();
|
|
193
|
+
this.app = this.initApp();
|
|
194
|
+
const klass = this.ctor;
|
|
195
|
+
const { flags } = await this.parse(klass);
|
|
196
|
+
if (klass.orientation) {
|
|
197
|
+
this.showOrientation(klass.orientation, flags);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async postConfig(configJson) {
|
|
201
|
+
if (!this.app) {
|
|
202
|
+
this.error('App is not defined');
|
|
203
|
+
}
|
|
204
|
+
this.debug('postConfig: configJson', configJson);
|
|
205
|
+
return this.api.post({ adminPath: `/apps/${this.app.id}/configs` }, { body: configJson });
|
|
206
|
+
}
|
|
207
|
+
async pushConfigFile(appConfig) {
|
|
208
|
+
const { localPath } = appConfig;
|
|
209
|
+
const spinner = ora();
|
|
210
|
+
let postData;
|
|
211
|
+
if (!localPath) {
|
|
212
|
+
throw new Error('Missing local config path');
|
|
213
|
+
}
|
|
214
|
+
// we don't want to treat the app logo asset as config file
|
|
215
|
+
// it is dealt with at the app level
|
|
216
|
+
if (isAppLogoIcon(localPath)) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const pathDesc = localPath.split('/').slice(-2).join('/');
|
|
220
|
+
spinner.start(`Pushing ${pathDesc}`);
|
|
221
|
+
try {
|
|
222
|
+
postData = await appConfig.postData();
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (error instanceof IgnoringFileError) {
|
|
226
|
+
spinner.fail(`Ignoring file: ${error.message} in ${localPath}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (error instanceof FunctionProcessingError) {
|
|
230
|
+
this.debug(error);
|
|
231
|
+
this.debug(error.originalError);
|
|
232
|
+
spinner.fail(`${error.message} ${error.originalError?.message?.toLowerCase()}`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
spinner.fail(error.message);
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
const result = await this.handleRequestErrors(async () => this.postConfig(postData), () => spinner.fail(`Error pushing ${pathDesc}`));
|
|
239
|
+
spinner.succeed(`Pushed ${pathDesc}`);
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
async removeConfig(config) {
|
|
243
|
+
const spinner = ora();
|
|
244
|
+
spinner.start(`Removing ${config.type} ${config.name}`);
|
|
245
|
+
const result = await this.handleRequestErrors(async () => this.deleteConfig(config.id), () => spinner.fail(`Error removing ${config.type} ${config.name}`));
|
|
246
|
+
spinner.warn(`Removed ${config.type} ${config.name}`);
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
showErrors(errors, spinnerError) {
|
|
250
|
+
const actualErrors = Object.keys(errors).filter((err) => Object.prototype.hasOwnProperty.call(errors, err));
|
|
251
|
+
if (spinnerError) {
|
|
252
|
+
spinnerError();
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
this.logError(inflect('Error', actualErrors.length));
|
|
256
|
+
}
|
|
257
|
+
for (const err of actualErrors) {
|
|
258
|
+
// we need to display `id` instead of `private_id`
|
|
259
|
+
// developers don't have any knowledge of private ids
|
|
260
|
+
const displayErr = err === 'private_id' ? 'id' : err;
|
|
261
|
+
this.logError([
|
|
262
|
+
' -',
|
|
263
|
+
`${displayErr}:`.toLowerCase().replace(/^error:/, ''),
|
|
264
|
+
errors[err].message,
|
|
265
|
+
]
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.join(' '));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async syncAppConfigs() {
|
|
271
|
+
const configsToPush = [];
|
|
272
|
+
const installed = [];
|
|
273
|
+
const removed = [];
|
|
274
|
+
for (const { configFile, configType } of allConfigFilesPaths(this.appPath)) {
|
|
275
|
+
// skip logo icon as it is dealt with at the app level
|
|
276
|
+
if (isAppLogoIcon(configFile)) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const appConfig = appConfigFromFile(configFile, configType);
|
|
280
|
+
configsToPush.push(appConfig);
|
|
281
|
+
}
|
|
282
|
+
const batches = batchAppConfigsByType(configsToPush);
|
|
283
|
+
for (const batch of batches) {
|
|
284
|
+
this.log(style.appConfigName(batch.label));
|
|
285
|
+
// eslint-disable-next-line no-await-in-loop
|
|
286
|
+
for (const appConfig of batch.configs) {
|
|
287
|
+
const installedConfig = await this.pushConfigFile(appConfig);
|
|
288
|
+
if (installedConfig)
|
|
289
|
+
installed.push(installedConfig.id);
|
|
290
|
+
}
|
|
291
|
+
this.log();
|
|
292
|
+
}
|
|
293
|
+
// pull the app again after the latest push
|
|
294
|
+
// this allows us to get the latest configs updates that the remote app
|
|
295
|
+
// might have done
|
|
296
|
+
this.app = await this.getAppWithConfig(this.app.id);
|
|
297
|
+
this.debugJson('syncAppConfigs: app', this.app);
|
|
298
|
+
// if the remote app has configs that are not in the local app, we want to
|
|
299
|
+
// remove them
|
|
300
|
+
for (const config of this.app?.configs || []) {
|
|
301
|
+
const appConfig = config;
|
|
302
|
+
if (!appConfig.localPath) {
|
|
303
|
+
// eslint-disable-next-line no-await-in-loop
|
|
304
|
+
await this.removeConfig(appConfig);
|
|
305
|
+
removed.push(appConfig.id);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { installed, removed };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Reads the .swellrc file and sets up the remote app configuration.
|
|
312
|
+
*
|
|
313
|
+
* @returns App
|
|
314
|
+
*/
|
|
315
|
+
initApp() {
|
|
316
|
+
let remoteApp = {};
|
|
317
|
+
try {
|
|
318
|
+
this.debug('initApp: swellRcPath', this.swellRcPath());
|
|
319
|
+
remoteApp = readRcFile(this.swellRcPath());
|
|
320
|
+
this.debug('initApp: remoteApp', remoteApp);
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
// if the file doesn't exist, we don't want to throw an error as we might
|
|
324
|
+
// want to create it later.
|
|
325
|
+
if (error.code !== 'ENOENT') {
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (this.appConfig.get('id')) {
|
|
330
|
+
// eslint-disable-next-line camelcase
|
|
331
|
+
remoteApp.private_id = snakeCase(this.appConfig.get('id'));
|
|
332
|
+
}
|
|
333
|
+
return remoteApp;
|
|
334
|
+
}
|
|
335
|
+
showOrientation(orientation, flags) {
|
|
336
|
+
const orientationOutput = [];
|
|
337
|
+
const currentStore = swellConfig.getDefaultStore();
|
|
338
|
+
const { env, store } = flags;
|
|
339
|
+
if (store) {
|
|
340
|
+
orientationOutput.push(`[store] ${style.storeId(store)}`);
|
|
341
|
+
}
|
|
342
|
+
else if (currentStore) {
|
|
343
|
+
orientationOutput.push(`[store] ${style.storeId(currentStore)}`);
|
|
344
|
+
}
|
|
345
|
+
const outputEnv = env || orientation.env;
|
|
346
|
+
if (outputEnv) {
|
|
347
|
+
const outputStyle = outputEnv === 'live' ? style.appEnvLive : style.appEnv;
|
|
348
|
+
orientationOutput.push(`[env] ${outputStyle(outputEnv)}`);
|
|
349
|
+
}
|
|
350
|
+
this.log(orientationOutput.join(' '));
|
|
351
|
+
this.log();
|
|
352
|
+
}
|
|
353
|
+
async getVersions(version) {
|
|
354
|
+
const versionsResponse = await this.api.get({ adminPath: `/apps/${this.app.id}/versions` }, {
|
|
355
|
+
query: {
|
|
356
|
+
limit: 1000,
|
|
357
|
+
sort: 'version desc',
|
|
358
|
+
...(version ? { version } : undefined),
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
this.debugJson('getApp: versionsResponse', versionsResponse);
|
|
362
|
+
return versionsResponse.results || [];
|
|
363
|
+
}
|
|
364
|
+
swellRcPath() {
|
|
365
|
+
return path.join(this.appPath, '.swellrc');
|
|
366
|
+
}
|
|
367
|
+
}
|