Source: api/index.js

import Compression from 'compression';
import { getRelease, title } from 'cpb-common';
import * as dotenv from 'dotenv';
dotenv.config({path: process.env.DOTENV || '.env'});
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';
import cors from 'cors';

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(req, res) {
  await ShopIdService.load().catch(console.error);
  if(res?.status) res.status(200).send();
}

/**
 * ### 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);
  const corsOptions = {
    origin: '*',
    methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
    allowedHeaders: "Content-Type, Authorization, Access-Control-Allow-Origin, *",
    exposedHeaders: '*',
    maxAge: "60000",
    optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204
  }
  app.use(cors(corsOptions));
  app.options('*', cors(corsOptions));
  app.disable('etag');

  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);
  router.get('/startup', startup)
  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;