UNPKG

@swell/cli

Version:

Swell's command line interface/utility

388 lines (387 loc) 12.8 kB
"use strict"; const origialConsoleLog = console.log; addEventListener('fetch', (event) => { event.respondWith(request(event.request, event.env, event)); }); /** * Handle a function request. * @param {Request} originalRequest from cloudflare * @param {*} _env cloudflare environment vars * @param {*} context cloudflare conttext * @returns {Promise<SwellResponse>} */ async function request(originalRequest, _env, context) { const req = new SwellRequest(originalRequest, context); await req.initialize(); let response; try { response = await executeModuleHandler(req, context); } catch (err) { // Log the error for Swell console.error(err); response = new SwellResponse({ error: err.message }, { status: err.status || 500 }); } return SwellResponse._respond(req, response, context); } /** * Invokes one of the function types that are available. * * Type-1: Named export function handlers * ``` * export function post (req) { * * } * ``` * * Important note: * - `delete` func cannot be implemented in Type-1 since it's a reserved name. So, consider using Type-3. * * Type-2: Default export function handlers * ``` * export default function (req) { * * } * ``` * * Type-3: Default object with named function handlers * ``` * export default { * delete(req) { * * }, * post(req) { * * }, * } * ``` * * @param {SwellRequest} req * @param {Event} context */ async function executeModuleHandler(req, context) { const method = req.method.toLowerCase(); const defaults = moduleExports.default; if (moduleExports[method]) { return moduleExports[method](req, context); } else if (defaults) { if (typeof defaults === 'function') { return defaults(req, context); } else if (defaults[method]) { return defaults[method](req, context); } } throw new Error(`Function does not export a method to handle ${method.toUpperCase()} requests`); } /** * Class representing a Swell request. */ class SwellRequest { constructor(req, context) { this.originalRequest = req; this.assignRequestProps(req); // Set environment specific variables this.context = context; this.appId = req.headers.get('Swell-App-Id'); this.storeId = req.headers.get('Swell-Store-Id'); this.accessToken = req.headers.get('Swell-Access-Token'); this.publicKey = req.headers.get('Swell-Public-Key'); this.store = this.parseJson(req.headers.get('Swell-Store-Details')); this.session = this.parseJson(req.headers.get('Swell-Session')); this.logParams = this.parseJson(req.headers.get('Swell-Request-Log')); this.apiHost = req.headers.get('Swell-API-Host') || 'https://api.schema.io'; this.id = req.headers.get('Swell-Request-ID') || this.logParams?.req_id; // Swell client this.swell = new SwellAPI(this, context); // URL of the original request this.url; // Original body of the request, JSON if applicable this.body = {}; // URL query parameters as an object this.query = {}; // Combined object of body and query parameters this.data = {}; // Internal logs this._logs = []; } assignRequestProps(req) { ['ur', 'method', 'headers', 'referrer', 'credentials'].forEach((prop) => { this[prop] = req[prop]; }); } async initialize() { this.body = await this.originalRequest.text(); try { this.data = JSON.parse(this.body); this.body = { ...this.data }; } catch (err) { this.data = {}; } this.url = new URL(this.originalRequest.url); // Convert the query parameters to an object this.url.searchParams.forEach((value, key) => { this.query[key] = value; this.data[key] = value; }); // Bind the console methods to the request console.log = this.log.bind(this, 'info'); console.info = this.log.bind(this, 'info'); console.debug = this.log.bind(this, 'debug'); console.warn = this.log.bind(this, 'warn'); console.error = this.log.bind(this, 'error'); } parseJson(input) { try { return JSON.parse(input); } catch (err) { return {}; } } log(level, ...line) { origialConsoleLog(...line); this._logs.push({ date: Date.now(), line: line.map((l) => (l instanceof Error ? l.stack : JSON.stringify(l))), ...(level !== 'info' ? { level } : {}), }); } getIngestableLogs(response) { if (!this.logParams) { return; } if (this.logParams.$start) { this.logParams.time = Date.now() - this.logParams.$start; delete this.logParams.$start; } return { params: { ...this.logParams, message: { ...this.logParams?.message, logs: this._logs, status: response.status, }, }, }; } async ingestLogs(response) { const ingestableLogs = this.getIngestableLogs(response); if (!ingestableLogs) { return; } const result = await this.swell.post('/:logs', { $ingest_function_logs: ingestableLogs, }); if (!result?.success) { console.error('Error ingesting logs', result); } } /** * Merge values into app data for the current request. * @param {object|string} idOrValues string to indicate app ID, or values to merge * @param {object|undefined} values values to merge into app data * @returns {object|undefined} existing app data merged with values if passed */ appValues(idOrValues, values = undefined) { const appId = typeof idOrValues === 'string' ? appIdOrValues : this.appId; const appValues = typeof idOrValues === 'string' ? values : idOrValues; if (!appId || !isOrdinaryObject(appValues)) { return undefined; } return { $app: { [appId]: appValues, }, }; } } /** * Class representing the Swell backend API. */ class SwellAPI { constructor(req, context) { this.request = req; this.baseUrl = req.apiHost; this.basicAuth = `${req.storeId}:${req.accessToken}`; this.context = context; } toBase64(inputString) { const utf8Bytes = new TextEncoder().encode(inputString); let base64String = ''; for (let i = 0; i < utf8Bytes.length; i += 3) { const chunk = utf8Bytes.slice(i, i + 3); base64String += btoa(String.fromCharCode(...chunk)); } return base64String; } stringifyQuery(queryObject, prefix) { const result = []; for (const [key, value] of Object.entries(queryObject)) { const prefixKey = prefix ? `${prefix}[${key}]` : key; const isObject = value !== null && typeof value === 'object'; const encodedResult = isObject ? this.stringifyQuery(value, prefixKey) : `${encodeURIComponent(prefixKey)}=${encodeURIComponent(value)}`; result.push(encodedResult); } return result.join('&'); } async makeRequest(method, url, data) { const requestOptions = { method, headers: { Authorization: `Basic ${this.toBase64(this.basicAuth)}`, 'User-Agent': 'swell-functions/1.0', 'Content-Type': 'application/json', ...(this.request.id ? { 'Swell-Request-ID': this.request.id } : {}), }, }; let query = ''; if (data) { try { if (method === 'GET') { query = `?${this.stringifyQuery(data)}`; } else { requestOptions.body = JSON.stringify(data); requestOptions.headers['Content-Length'] = requestOptions.body.length; } } catch { throw new Error(`Error serializing data: ${data}`); } } const endpointUrl = String(url).startsWith('/') ? url.substring(1) : url; const response = await fetch(`${this.baseUrl}/${endpointUrl}${query}`, requestOptions); const responseText = await response.text(); let result; try { result = JSON.parse(responseText); } catch { result = String(responseText || '').trim(); } if (response.status > 299) { throw new SwellError(result, { status: response.status, method, endpointUrl, }); } else if (method !== 'GET' && result?.errors) { throw new SwellError(result.errors, { status: 400, method, endpointUrl }); } return result; } async get(url, query) { return this.makeRequest('GET', url, query); } async put(url, data) { return this.makeRequest('PUT', url, data); } async post(url, data) { return this.makeRequest('POST', url, data); } async delete(url, data) { return this.makeRequest('DELETE', url, data); } async settings(id = this.request.appId) { return this.makeRequest('GET', `/settings/${id}`); } } /** * Class representing a Swell error. */ class SwellError extends Error { constructor(message, options = {}) { let formattedMessage; if (typeof message === 'string') { formattedMessage = message; } else { formattedMessage = JSON.stringify(message, null, 2); } if (options.method && options.endpointUrl) { formattedMessage = `${options.method} /${options.endpointUrl}\n${formattedMessage}`; } super(formattedMessage); this.name = 'SwellError'; this.status = options.status || 500; } } /** * Class representing a Swell response. */ class SwellResponse extends Response { constructor(data, options = {}) { const resultHeaders = {}; let result = ''; if (typeof data === 'string') { result = data; resultHeaders['Content-Type'] = 'text/plain;charset=UTF-8'; } else if (data !== undefined) { result = JSON.stringify(data, null, 2); resultHeaders['Content-Type'] = 'application/json;charset=UTF-8'; } super(result?.toString?.('utf-8'), { status: 200, ...options, headers: { ...resultHeaders, ...(options.headers || {}), }, }); // Saved for future access this._swellData = data; this._swellOptions = options || {}; } static _respond(req, response, context) { let finalResponse = response; // Convert a plain Response instance to SwellResponse if (finalResponse instanceof Response && !(finalResponse instanceof SwellResponse)) { finalResponse = new SwellResponse(response.body, { status: response.status, headers: response.headers, }); } else if (!(finalResponse instanceof SwellResponse)) { finalResponse = new SwellResponse(response); } // Send logs back with the response for event hooks if (req.data?.$event?.hook) { return SwellResponse._respondWithLogs(finalResponse, req); } // Ingest logs in the background context.waitUntil(req.ingestLogs(finalResponse)); return finalResponse; } static _respondWithLogs(response, req) { const ingestableLogs = req.getIngestableLogs(response); // Rebuild response with logs const responseData = response instanceof SwellResponse ? response?._swellData : response; const resultData = ingestableLogs ? { $logs: ingestableLogs, $data: responseData, } : responseData; return new SwellResponse(resultData, response?._swellOptions); } } /** * Helper to determine if a value is an ordinary object. * @param {any} obj * @returns {boolean} */ function isOrdinaryObject(val) { return (typeof val === 'object' && val !== null && Object.getPrototypeOf(val) === Object.prototype); } ;