import Compression from 'compression';
import { getRelease, title } from 'cpb-common';
import express from 'express';
import logger from 'morgan';
import path from 'path';
import responseTime from 'response-time';
import rid from 'rid';
import { fileURLToPath } from 'url';
import CACHE from './cache.js';
import Middleware from './middleware/index.js';
import ProductIdService from './product/idService.js';
import ShopIdService from './shop/shopId.js';
import Subscriptions from './subscription/index.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* @module cpb-api
* @description
* ### REST API for the CPB Backend Storage Operations
*/
export { CACHE, Subscriptions, ShopIdService };
/**
* ### REST API for the CPB Backend Storage Operations
* @memberOf module:cpb-api
* @type {Express}
*/
export const app = express(),
/**
*
* @type {Router}
*/
router = express.Router();
/**
* @methodOf module:cpb-api
* @param {object} data
* @param props
* @return {object}
*/
export function cleanupData(data, props) {
if (!data) throw new TypeError('!data');
if (props.files) props.deletedFiles = props.files;
for (const prop of Object.keys(props)) if (!props[prop]) delete data[prop];
return data;
}
/**
* ### startup scripts
* @returns {Promise<void>}
*/
async function startup() {
await ShopIdService.load().catch(console.error);
}
/**
* ### Application Starter Method
* @method module:cpb-api.app.start
* @param {?number} [port=8080] - Server Port
* @param {?string} [bucket='custom-product-builder'] - GCS Storage Bucket
* @param {?boolean} [compression=true] - use gzip compression via the `compression` package
* @returns {Express} app - Application Instance
*/
app.start = async function start({
port = process.env.PORT || 8080,
bucket = process.env.BUCKET || 'custom-product-builder',
compression = true,
listen = process.env.START_LISTENER,
} = {}) {
const release = getRelease(),
nodeEnv = process.env.NODE_ENV || 'development';
app.set('bucket', bucket);
app.set('x-powered-by', false);
app.set('x-release', release);
app.set('nodeEnv', nodeEnv);
app.set('json spaces', nodeEnv === 'production' ? 2 : 2);
Middleware.session(app);
// static docs
const docsPath = `./doc/${release.replace(':', '/')}`;
app.use('/', express.static(docsPath));
// lzma comp
if (compression) app.use(Compression({ level: 9 }));
// add header with the request handling time
app.use(responseTime());
// creates the request log entry in the format ::1 - - [10/Dec/2021:11:15:03 +0000] "GET /filestore HTTP/1.1" 200 83211
app.use(logger('common', { immediate: false }));
router.param('shopIdOrName', async function (req, res, next, val) {
res.locals.shopIdOrName = val;
res.locals.shop = await ShopIdService.getIdName(val);
console.log('[api/index][router.param.shopIdOrName][res.locals]', res.locals);
next();
});
router.param('chargeStatus', async function (req, res, next, val) {
res.locals.chargeStatus = val.toLowerCase();
console.log('[api/index][router.param.chargeStatus][res.locals]', res.locals);
next();
});
router.param('productIdOrHandle', async function (req, res, next, val) {
res.locals.productIdOrHandle = val;
res.locals.product = await ProductIdService.getIdHandle(val, { shopID: res.locals.shop.id, shopName: res.locals.shop.name });
console.log('[router.param.productIdOrHandle]', res.locals);
next();
});
// adding request_id(rid) and release version headers
router.get('*', function addReleaseHeader(req, res, next) {
res.locals.rid = rid();
res.append('X-Powered-By', release);
res.append('X-RID', res.locals.rid);
next();
});
/**
* @apiDefine ShopifyStore
* @apiDescription Store Management facility for the Product Customizations
*/
/**
* @apiDefine ShopifyStoreId
* @apiParam {Number} shop_id Shopify Store Id. That corresponds to the top-level dir name in the storage bucket
*/
/**
* @api {GET} /filestore [GET] /filestore list all shopify stores and top-level dirs for the bucket
* @apiDescription Contains Shopify Store IDs, dirs for the legacy app installs (*.myshopify.com), and miscellaneous
* dirs for the bucket defined at %ENV.BUCKET% at the server runtime or via `app.set('bucket')` in the
* `src/api/index.js`
* @apiName GetAllStores
* @apiGroup ShopifyStore
* @apiPermission admin
* @apiQuery {String=true,false} fetch=false Rescan data if true or retrieve the existing index (default)
* @apiQuery {String=true,false} files=false Include loose files into the output
* @apiQuery {String=true,false} misc=false Include misc dirs into the output
* @apiQuery {String=true,false} legacy=false Include legacy dirs into the output (ones with the name of the
* shopify domain name)
* @apiSuccess {String} bucket GCS Storage Bucket
* @apiSuccess {Number[]} ids Shopify Store ids
* @apiSuccess {String[]} legacy Storage dirs that are named by the filestore domain names
* @apiSuccess {String[]} misc Storage dirs that could not be identified as shopify data holders
* @apiSuccess {Object[]} files Loose files in the bucket root
* @apiSuccess {String} files.name Loose file name
* @apiSuccess {Object[]} files.versions File versions
* @apiSuccess {String} files.versions.id
* @apiSuccess {Integer} files.versions.generation
* @apiSuccess {Boolean} files.versions.isDeleted
* @apiSuccess {Boolean} files.versions.isCurrent
* @apiSuccess {Integer} files.versions.size File Version size
* @apiSuccess {String} files.versions.url File Version Download Link
* @apiSuccess {Object} files.versions.metadata File versions metadata
* @apiSuccess {String} files.versions.metadata.md5Hash
* @apiSuccess {String} files.versions.metadata.crc32c
* @apiSuccess {Timestamp} files.versions.metadata.timeCreated
* @apiSuccess {Timestamp} files.versions.metadata.updated
* @apiSuccess {Object} stats Counters for the data
* @apiSuccess {Number} stats.ids Number of Store ids
* @apiSuccess {Number} stats.legacy Number of the legacy dirs ( named by the filestore domain names)
* @apiSuccess {Number} stats.misc Number of unidentified dirs
* @apiSuccess {Number} stats.files Number of loose files
* @apiSuccess {Number} stats.deletedFiles Number of loose deleted files
* @apiExample {shell} Get all stores with files and misc and legacy directories within the bucket::
curl localhost:8080/filestore | jq .
* @apiSuccessExample all stores with files and misc and legacy directories within the bucket: Success-Response:
* {
* "bucket": "custom-product-builder",
* "ids": [
* 10003054628,
* 10012622910,
* 10025467982
* ],
* "misc": [
* "cpb-assets/",
* "custom-product-builder-stage/",
* "customproductbuilder/",
* "dev-test-shop/"
* ],
* "legacy": [
* "alpha-crystal-jewellery.myshopify.com/",
* "alpha-wraps.myshopify.com/",
* "alumepixalayof.myshopify.com/",
* "amasal.myshopify.com/"
* ],
* "files": [
* {
* "name": "64audio-5540496998550-J9gcVRJDZzGfeiSZc1ZZslq5.jpg",
* "versions": [
* {
* "id": "custom-product-builder/64audio-5540496998550-J9gcVRJDZzGfeiSZc1ZZslq5.jpg/1627414331517243",
* "generation": 1627414331517243,
* "metadata": {
* "md5Hash": "rBkJMvwCwAYY0QtuO4pYRA==",
* "crc32c": "CsVSMQ==",
* "timeCreated": "2021-07-27T19:32:11.547Z",
* "updated": "2021-07-27T19:32:11.547Z"
* },
* "isDeleted": false,
* "isCurrent": true,
* "size": 52167,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/64audio-5540496998550-J9gcVRJDZzGfeiSZc1ZZslq5.jpg?generation=1627414331517243&alt=media"
* }
* ],
* "isDeleted": false,
* "currentFileSize": 52167,
* "generation": 1627414331517243,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/64audio-5540496998550-J9gcVRJDZzGfeiSZc1ZZslq5.jpg?generation=1627414331517243&alt=media",
* "totalSize": 52167,
* "previousFileVersionsCount": 0,
* "previousFileVersionsSize": 0
* }
* ],
* "deletedFiles": [],
* "stats": {
* "ids": 7177,
* "legacy": 66,
* "misc": 42,
* "files": 1005,
* "deletedFiles": 0
* }
* "debug":{
* "params": {},
* "query": {}
* }
* }
* @apiExample {shell} Get only filestore ids for the bucket
* curl http://localhost:8080/store?files=1&fetch=0&legacy=0&misc=0
*
* @apiSuccessExample Get only filestore ids for the bucket: Success-Response:
* {
* "bucket": "custom-product-builder",
* "ids": [
* 10003054628,
* 10012622910,
* 10025467982,
* 10039296064
* ],
* "stats": {
* "ids": 7183,
* "legacy": 66,
* "misc": 42,
* "files": 1033,
* "deletedFiles": 0
* },
* "debug": {
* "bucket": "custom-product-builder",
* "fetch": false,
* "versions": false,
* "showFiles": true,
* "misc": false,
* "legacy": false,
* "request": {
* "query": {
* "files": "1",
* "fetch": "0",
* "legacy": "0",
* "misc": "0"
* },
* "params": { }
* },
* "timestamp": "2021-12-19T13:17:46.382Z"
* }
* }
*/
router.get('/filestore', Middleware.store);
/**
* @api {GET} /filestore/:shopIdOrName [GET] /filestore/:shopIdOrName SHOPIFY STORE PRODUCTS AND FILES
* @apiQuery {String=true,false} versions=true Include previous file versions
* @apiQuery {String=true,false} fetch=false Rescan data or retrieve the existing index (default)
* @apiDescription Get the filestore data for the given `:shop_id` with the summarized sizes and ShopifyProductId
* @apiName GetStore
* @apiGroup ShopifyStore
* @apiPermission shopifyApp
* @apiUse ShopifyStoreId
* @apiQuery {String=true,false} fetch=false Rescan data or retrieve the existing index (default)
* @apiExample {shell} Example usage:
* STORE_ID=10003054628 curl localhost:8080/filestore/${STORE_ID} | jq .
* @apiSuccess {String} id Shopify Store ID
* @apiSuccess {String} bucket GCS Storage Bucket
* @apiSuccess {Number[]} products Shopify Product IDs
* @apiSuccess {Number[]} deletedProducts Deleted Shopify Product IDs
* @apiSuccess {Object[]} files Bucket Files
* @apiSuccess {String} files.name The name of the stored file
* @apiSuccess {Number} files.currentFileSize The size of the current file version in bytes
* @apiSuccess {Number} files.size The Total size of all the file versions in bytes
* @apiSuccess {Number} files.previousFileVersionsSize The size of the previous file versions in bytes
* @apiSuccess {Number} files.previousFileVersionsCount The number of the previous file versions
* @apiSuccess {Number} files.generation The Generation Number (unique for the file version)
* @apiSuccess {String} files.url Download URL for the current file version
* @apiSuccess {[Object[]]} files.versions The file versions
* @apiSuccess {String} files.versions.id The ID of the file version
* @apiSuccess {Number} files.versions.generation The Generation Number of the file version
* @apiSuccess {Boolean} files.versions.isCurrent Indicates whether the given file version is current
* @apiSuccess {String} files.versions.url The Download URL for the file version
* @apiSuccess {Number} files.versions.size The Size of the file version in bytes
* @apiSuccess {Object} files.versions.metadata The partial gce metadata
* @apiSuccess {Timestamp} files.versions.metadata.timeCreated
* @apiSuccess {Timestamp} files.versions.metadata.updated
* @apiSuccess {Timestamp} files.versions.metadata.timeDeleted
* @apiSuccess {Object[]} deletedFiles Deleted Files in bucket
* @apiSuccess {Object} stats Contains the summarized quantitative metrics for the stored data
* @apiSuccess {Number} stats.files Number of files stored
* @apiSuccess {Number} stats.deletedFiles Number of deleted files in bucket
// * @apiSuccess {[Number]} stats.previousFileVersions Number of all file versions stored
* @apiSuccess {Number} stats.products Number of products
* @apiSuccess {Number} stats.deletedProducts Number of *deleted* products
* @apiSuccess {Number} stats.size Total Size of Data Stored
* @apiSuccess {[Number]} stats.previousFileVersionsSize Size of the previous versions
* @apiSuccess {[Number]} stats.previousFileVersionsCount The number of the previous file versions
* @apiSuccess {Number} stats.currentFileSize Size of the current versions
* @apiSuccessExample Success-Response:
* {
* "id": 10003054628,
* "bucket": "custom-product-builder",
* "products": [
* 7000615387329,
* 7000615420097,
* 7000615452865,
* 7000615813313
* ],
* "deletedProducts": [],
* "deletedFiles": [],
* "files": [
* {
* "name": "7000615387329.json",
* "versions": [
* {
* "id": "custom-product-builder/10003054628/7000615387329.json/1635936486341218",
* "generation": "1635936486341218",
* "metadata": {
* "timeCreated": "2021-11-03T10:48:06.376Z",
* "updated": "2021-11-03T10:48:06.376Z",
* "timeDeleted": "2021-11-03T10:48:06.641Z"
* },
* "isDeleted": true,
* "size": 100904,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/10003054628%2F7000615387329.json?generation=1635936486341218&alt=media"
* },
* {
* "id": "custom-product-builder/10003054628/7000615387329.json/1635936486517468",
* "generation": "1635936486517468",
* "metadata": {
* "timeCreated": "2021-11-03T10:48:06.641Z",
* "updated": "2021-11-03T10:48:06.641Z"
* },
* "isCurrent": true,
* "size": 100904,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/10003054628%2F7000615387329.json?generation=1635936486517468&alt=media"
* }
* ],
* "currentFileSize": 100904,
* "generation": 1635936486517468,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/10003054628%2F7000615387329.json?generation=1635936486517468&alt=media",
* "totalSize": 201808,
* "previousFileVersionsSize": 100904
* },
* {
* "name": "7000615420097.json",
* "versions": [
* {
* "id": "custom-product-builder/10003054628/7000615420097.json/1635936486855324",
* "generation": "1635936486855324",
* "metadata": {
* "timeCreated": "2021-11-03T10:48:06.890Z",
* "updated": "2021-11-03T10:48:06.890Z",
* "timeDeleted": "2021-11-03T10:48:07.148Z"
* },
* "isDeleted": true,
* "size": 39404,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/10003054628%2F7000615420097.json?generation=1635936486855324&alt=media"
* },
* {
* "id": "custom-product-builder/10003054628/7000615420097.json/1635936487029707",
* "generation": "1635936487029707",
* "metadata": {
* "timeCreated": "2021-11-03T10:48:07.148Z",
* "updated": "2021-11-03T10:48:07.148Z"
* },
* "isCurrent": true,
* "size": 39404,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/10003054628%2F7000615420097.json?generation=1635936487029707&alt=media"
* }
* ],
* "currentFileSize": 39404,
* "generation": 1635936487029707,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/10003054628%2F7000615420097.json?generation=1635936487029707&alt=media",
* "totalSize": 78808,
* "previousFileVersionsSize": 39404
* },
* {
* "name": "7000615452865.json",
* "versions": [
* {
* "id": "custom-product-builder/10003054628/7000615452865.json/1635936486472693",
* "generation": "1635936486472693",
* "metadata": {
* "timeCreated": "2021-11-03T10:48:06.508Z",
* "updated": "2021-11-03T10:48:06.508Z",
* "timeDeleted": "2021-11-03T10:48:06.777Z"
* },
* "isDeleted": true,
* "size": 90487,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/10003054628%2F7000615452865.json?generation=1635936486472693&alt=media"
* },
* {
* "id": "custom-product-builder/10003054628/7000615452865.json/1635936486654280",
* "generation": "1635936486654280",
* "metadata": {
* "timeCreated": "2021-11-03T10:48:06.777Z",
* "updated": "2021-11-03T10:48:06.777Z"
* },
* "isCurrent": true,
* "size": 90487,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/10003054628%2F7000615452865.json?generation=1635936486654280&alt=media"
* }
* ],
* "currentFileSize": 90487,
* "generation": 1635936486654280,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/10003054628%2F7000615452865.json?generation=1635936486654280&alt=media",
* "totalSize": 180974,
* "previousFileVersionsSize": 90487
* },
* {
* "name": "7000615813313.json",
* "versions": [
* {
* "id": "custom-product-builder/10003054628/7000615813313.json/1635936606084578",
* "generation": "1635936606084578",
* "metadata": {
* "timeCreated": "2021-11-03T10:50:06.120Z",
* "updated": "2021-11-03T10:50:06.120Z"
* },
* "isCurrent": true,
* "size": 652,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/10003054628%2F7000615813313.json?generation=1635936606084578&alt=media"
* }
* ],
* "currentFileSize": 652,
* "generation": 1635936606084578,
* "url":
* "https://storage.googleapis.com/download/storage/v1/b/custom-product-builder/o/10003054628%2F7000615813313.json?generation=1635936606084578&alt=media",
* "totalSize": 652,
* "previousFileVersionsSize": 0
* }
* ],
* "stats": {
* "files": 4,
* "previousFileVersions": 7,
* "products": 4,
* "size": 462242,
* "previousFileVersionsSize": 230795,
* "currentFileSize": 231447
* },
* "debug": {
* "params": {
* "id": "10003054628"
* },
* "query": {}
* }
* } */
router.get('/filestore/:shopIdOrName', Middleware.store);
//router.get('/shop', Middleware.shop);
router.get('/shop/:shopIdOrName', Middleware.shop);
//router.get('/shop/:shopIdOrName/charges', Middleware.charge);
//router.get('/cpb/:shopIdOrName/charges/:chargeStatus', Middleware.charge);
// router.get('/shop/:shopIdOrName/webhooks', Middleware.webhook);
/**
* @api {GET} /cpb/:shopIdOrShopName GET ALL STORE INFORMATION
* @apiName CustomProductBuilderShop
* @apiPermission public
* @apiParam {String} shopIdOrShopName - Shopify *shopID* or CPB **shopName**
* @apiQuery {String=true,false} fetch=false Rescan data if true or retrieve the existing cache from **${BUCKET}/${shopID}/cpb.json file (default)
* @apiQuery {String=true,false} files=false Include storage files (**storage**) into the output
* @apiQuery {String=true,false} version=false Include storage file versions (**products.config.versions** & **storage.files.versions**) into the output
* @apiQuery {String=true,false} charges=true Include Shopify Recurring Charges (**charges**) into the output. Currently only active charge is displayed
* @apiQuery {String=true,false} shop=true Include Shopify Shop data (**shop**) into the output.Included by default. If **fetch** flag is set it requests
* new **shopData** from Shopify
*/
router.get('/cpb/:shopIdOrName', Middleware.cpb);
router.get('/cpb/:shopIdOrName/charges', Middleware.charge);
router.get('/cpb/:shopIdOrName/charges/:chargeStatus', Middleware.charge);
router.get('/cpb/:shopIdOrName/fullInfo/products', Middleware.cpb);
router.get('/cpb/:shopIdOrName/fullInfo/product/:productIdOrHandle', Middleware.cpb);
router.get('/cpb/:shopIdOrName/fullInfo/product/:productIdOrHandle/generation/:generation', Middleware.cpb);
router.get('/cpb/:shopIdOrName/products', Middleware.product);
router.get('/cpb/:shopIdOrName/product/:productIdOrHandle', Middleware.product);
router.get('/cpb/:shopIdOrName/webhooks', Middleware.webhook);
app.use(router);
app.use(function appErrorTrap(error, req, res, next) {
console.error({ error });
error.rid = res.locals.rid;
/**
* @type {Express.Response}
*/
res.type('json').status(500).json(error).end();
});
// await
startup().catch(console.error);
if (listen)
app.listen(port, function () {
title(`[${release}][${nodeEnv}][${bucket}]listening at http://localhost:${port}`);
return app;
});
};
/**
* @exports module:cpb-api.app
* @type {Express}
*/
export default app;