diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 275bff5db33..800ff9c2e93 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -125,6 +125,9 @@ jobs: - name: Build core run: yarn build:core + - name: Build middleware + run: yarn build:middleware + - name: Build api-client run: cd packages/${{ matrix.integration }}/api-client && yarn build diff --git a/package.json b/package.json index a9668e26aee..809b734c591 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build:docs": "cd packages/core/docs && yarn build", "dev:docs": "cd packages/core/docs && yarn dev", "build:core": "cd packages/core/core && yarn build", + "build:middleware": "cd packages/core/middleware && yarn build", "build:ayc:api-client": "cd packages/about-you/api-client && yarn build", "build:ayc:composables": "cd packages/about-you/composables && yarn build", "build:ayc:theme": "cd packages/about-you/theme && yarn build", @@ -28,7 +29,7 @@ "dev:bp": "cd packages/boilerplate/theme && yarn dev", "build:ct:api-client": "cd packages/commercetools/api-client && yarn build", "build:ct:composables": "cd packages/commercetools/composables && yarn build", - "build:ct:tools": "yarn build:core && yarn build:ct:api-client && yarn build:ct:composables", + "build:ct:tools": "yarn build:core && yarn build:middleware && yarn build:ct:api-client && yarn build:ct:composables", "build:ct:theme": "cd packages/commercetools/theme && yarn build", "build:ct": "yarn build:core && yarn build:ct:tools && yarn build:ct:theme", "build:sp:tools": "yarn build:core && yarn build:sp:api-client && yarn build:sp:composables", diff --git a/packages/commercetools/api-client/src/index.server.ts b/packages/commercetools/api-client/src/index.server.ts index a7a9bea18aa..8c40bcd9b49 100644 --- a/packages/commercetools/api-client/src/index.server.ts +++ b/packages/commercetools/api-client/src/index.server.ts @@ -58,29 +58,32 @@ const parseToken = (rawToken) => { } }; -const tokenExtension: ApiClientExtension = (req, res) => { - const rawCurrentToken = req.cookies['vsf-commercetools-token']; - const currentToken = parseToken(rawCurrentToken); +const tokenExtension: ApiClientExtension = { + name: 'tokenExtension', + hooks: (req, res) => { + const rawCurrentToken = req.cookies['vsf-commercetools-token']; + const currentToken = parseToken(rawCurrentToken); - return { - beforeCreate: (config) => ({ - ...config, - auth: { - onTokenChange: (newToken) => { - if (!currentToken || currentToken.access_token !== newToken.access_token) { - res.cookie('vsf-commercetools-token', JSON.stringify(newToken)); + return { + beforeCreate: ({ configuration }) => ({ + ...configuration, + auth: { + onTokenChange: (newToken) => { + if (!currentToken || currentToken.access_token !== newToken.access_token) { + res.cookie('vsf-commercetools-token', JSON.stringify(newToken)); + } + }, + onTokenRead: () => { + res.cookie('vsf-commercetools-token', rawCurrentToken); + return currentToken; + }, + onTokenRemove: () => { + delete req.cookies['vsf-commercetools-token']; } - }, - onTokenRead: () => { - res.cookie('vsf-commercetools-token', rawCurrentToken); - return currentToken; - }, - onTokenRemove: () => { - delete req.cookies['vsf-commercetools-token']; } - } - }) - }; + }) + }; + } }; const { createApiClient } = apiClientFactory({ diff --git a/packages/commercetools/composables/nuxt/index.js b/packages/commercetools/composables/nuxt/index.js index 627e1387d23..c60622c40a0 100644 --- a/packages/commercetools/composables/nuxt/index.js +++ b/packages/commercetools/composables/nuxt/index.js @@ -1,5 +1,4 @@ import path from 'path'; -import { createMiddleware } from '@vue-storefront/core/server'; const mapI18nSettings = (i18n) => ({ locale: i18n.defaultLocale, @@ -18,8 +17,6 @@ const getMissingFields = (options) => .filter(o => options[o] === undefined); export default function (moduleOptions) { - const { middleware } = createMiddleware(moduleOptions); - const options = isNuxtI18nUsed(moduleOptions) ? { ...moduleOptions, ...mapI18nSettings(this.options.i18n) } : moduleOptions; @@ -39,7 +36,4 @@ export default function (moduleOptions) { options }); - if (moduleOptions.apiMiddleware !== false) { - this.addServerMiddleware(middleware); - } } diff --git a/packages/commercetools/theme/middleware.config.js b/packages/commercetools/theme/middleware.config.js new file mode 100644 index 00000000000..834ffc13788 --- /dev/null +++ b/packages/commercetools/theme/middleware.config.js @@ -0,0 +1,30 @@ +module.exports = { + integrations: { + ct: { + location: '@vue-storefront/commercetools-api/server', + configuration: { + api: { + uri: 'https://api.commercetools.com/vsf-ct-dev/graphql', + authHost: 'https://auth.sphere.io', + projectKey: 'vsf-ct-dev', + clientId: 'RT4iJGDbDzZe4b2E6RyeNe9s', + clientSecret: '5eBt3yfZJWw1j7V6kXjfKXpuFP-YQXpg', + scopes: [ + 'manage_products:vsf-ct-dev', + 'create_anonymous_token:vsf-ct-dev', + 'manage_my_profile:vsf-ct-dev', + 'manage_customer_groups:vsf-ct-dev', + 'view_categories:vsf-ct-dev', + 'introspect_oauth_tokens:vsf-ct-dev', + 'manage_my_payments:vsf-ct-dev', + 'manage_my_orders:vsf-ct-dev', + 'manage_my_shopping_lists:vsf-ct-dev', + 'view_published_products:vsf-ct-dev' + ] + }, + currency: 'USD', + country: 'US' + } + } + } +}; diff --git a/packages/commercetools/theme/middleware.js b/packages/commercetools/theme/middleware.js new file mode 100644 index 00000000000..694622d079b --- /dev/null +++ b/packages/commercetools/theme/middleware.js @@ -0,0 +1,8 @@ +const { createServer } = require('@vue-storefront/middleware'); +const { integrations } = require('./middleware.config'); + +const app = createServer({ integrations }); + +app.listen(8181, () => { + console.log('Middleware started'); +}); diff --git a/packages/commercetools/theme/nuxt.config.js b/packages/commercetools/theme/nuxt.config.js index c54d6602bd8..24dfcc321fc 100644 --- a/packages/commercetools/theme/nuxt.config.js +++ b/packages/commercetools/theme/nuxt.config.js @@ -81,25 +81,6 @@ export default { ['@vue-storefront/nuxt-theme'], project-only-end */ ['@vue-storefront/commercetools/nuxt', { - api: { - uri: 'https://api.commercetools.com/vsf-ct-dev/graphql', - authHost: 'https://auth.sphere.io', - projectKey: 'vsf-ct-dev', - clientId: 'RT4iJGDbDzZe4b2E6RyeNe9s', - clientSecret: '5eBt3yfZJWw1j7V6kXjfKXpuFP-YQXpg', - scopes: [ - 'manage_products:vsf-ct-dev', - 'create_anonymous_token:vsf-ct-dev', - 'manage_my_profile:vsf-ct-dev', - 'manage_customer_groups:vsf-ct-dev', - 'view_categories:vsf-ct-dev', - 'introspect_oauth_tokens:vsf-ct-dev', - 'manage_my_payments:vsf-ct-dev', - 'manage_my_orders:vsf-ct-dev', - 'manage_my_shopping_lists:vsf-ct-dev', - 'view_published_products:vsf-ct-dev' - ] - }, i18n: { useNuxtI18nConfig: true } @@ -109,14 +90,7 @@ export default { 'nuxt-i18n', 'cookie-universal-nuxt', 'vue-scrollto/nuxt', - ['@vue-storefront/middleware/nuxt', { - integrations: { - ct: { - api: '@vue-storefront/commercetools-api/server', - module: '@vue-storefront/commercetools/nuxt' - } - } - }] + '@vue-storefront/middleware/nuxt' ], i18n: { currency: 'USD', diff --git a/packages/commercetools/theme/package.json b/packages/commercetools/theme/package.json index 7d12ef8b7c1..db27f26f044 100644 --- a/packages/commercetools/theme/package.json +++ b/packages/commercetools/theme/package.json @@ -20,6 +20,7 @@ "@vue-storefront/commercetools": "^1.1.2", "@vue-storefront/nuxt": "^2.2.1", "@vue-storefront/nuxt-theme": "^2.2.1", + "@vue-storefront/middleware": "^2.2.1", "cookie-universal-nuxt": "^2.1.3", "core-js": "^2.6.5", "nuxt": "^2.13.3", diff --git a/packages/core/core/__tests__/factories/apiClientFactory.spec.ts b/packages/core/core/__tests__/factories/apiClientFactory.spec.ts index aed9841ed2a..d63269a83ef 100644 --- a/packages/core/core/__tests__/factories/apiClientFactory.spec.ts +++ b/packages/core/core/__tests__/factories/apiClientFactory.spec.ts @@ -48,11 +48,12 @@ describe('[CORE - factories] apiClientFactory', () => { }); it('Should run given extensions', () => { - const extensionFns = { - beforeCreate: jest.fn(a => a), - afterCreate: jest.fn(a => a) + const beforeCreate = jest.fn(a => a); + const afterCreate = jest.fn(a => a); + const extension = { + name: 'extTest', + hooks: () => ({ beforeCreate, afterCreate }) }; - const extension = () => extensionFns; const params = { onCreate: jest.fn((config) => ({ config })), @@ -61,10 +62,11 @@ describe('[CORE - factories] apiClientFactory', () => { }; const { createApiClient } = apiClientFactory(params as any); + const extensions = (createApiClient as any)._predefinedExtensions; - createApiClient.bind({ middleware: { req: null, res: null } })({}); + createApiClient.bind({ middleware: { req: null, res: null, extensions } })({}); - expect(extensionFns.beforeCreate).toHaveBeenCalled(); - expect(extensionFns.afterCreate).toHaveBeenCalled(); + expect(beforeCreate).toHaveBeenCalled(); + expect(afterCreate).toHaveBeenCalled(); }); }); diff --git a/packages/core/core/__tests__/middleware/createMiddleware.spec.ts b/packages/core/core/__tests__/middleware/createMiddleware.spec.ts deleted file mode 100644 index 8a364c87615..00000000000 --- a/packages/core/core/__tests__/middleware/createMiddleware.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createMiddleware } from '../../src/server'; - -jest.mock('express', () => ({ - __esModule: true, - json: jest.fn(), - default: () => ({ - use: () => {} - }) -})); - -describe('[CORE - server] createMiddleware', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('creates middleware', () => { - const { middleware } = createMiddleware({} as any); - - expect(middleware.handler).toBeInstanceOf(Object); - expect(middleware.path).toEqual('/api'); - }); - - it('extends middleware', () => { - const extendApi = jest.fn(); - const { extend } = createMiddleware({ apiMiddleware: { extend: extendApi } }); - - extend(extendApi); - - expect(extendApi).toBeCalledTimes(2); - }); -}); diff --git a/packages/core/core/package.json b/packages/core/core/package.json index 0880a686a1f..8b8b44513da 100644 --- a/packages/core/core/package.json +++ b/packages/core/core/package.json @@ -7,10 +7,8 @@ "mainServer": "server/index.js", "types": "lib/src/index.d.ts", "scripts": { - "build:client": "rollup -c rollup-client.config.js", - "build:server": "rollup -c rollup-server.config.js", - "build": "yarn build:client && yarn build:server", - "dev": "rollup -c rollup-client.config.js -w", + "build": "rimraf lib && rollup -c rollup.config.js", + "dev": "rimraf lib && rollup -c rollup.config.js -w", "prepublish": "yarn build" }, "dependencies": { diff --git a/packages/core/core/rollup-server.config.js b/packages/core/core/rollup-server.config.js deleted file mode 100644 index 4258b445601..00000000000 --- a/packages/core/core/rollup-server.config.js +++ /dev/null @@ -1,33 +0,0 @@ -import pkg from './package.json'; -import typescript from 'rollup-plugin-typescript2'; -import replace from '@rollup/plugin-replace'; - -export function generateBaseConfig(pkg) { - return { - input: 'src/server/index.ts', - output: [ - { - file: pkg.mainServer, - format: 'cjs', - sourcemap: true - } - ], - external: [ - ...Object.keys(pkg.dependencies || {}) - ], - plugins: [ - typescript({ - // eslint-disable-next-line global-require - typescript: require('typescript') - }), - replace({ - __DEV__: process.env.NODE_ENV === 'development', - delimiters: ['', ''] - }) - ] - }; -} - -const baseConfig = generateBaseConfig(pkg); - -export default baseConfig; diff --git a/packages/core/core/rollup-client.config.js b/packages/core/core/rollup.config.js similarity index 100% rename from packages/core/core/rollup-client.config.js rename to packages/core/core/rollup.config.js diff --git a/packages/core/core/src/factories/apiClientFactory.ts b/packages/core/core/src/factories/apiClientFactory.ts index eacaa2a5b71..15f5ec5584e 100644 --- a/packages/core/core/src/factories/apiClientFactory.ts +++ b/packages/core/core/src/factories/apiClientFactory.ts @@ -1,37 +1,41 @@ -import { ApiClientFactoryParams, ApiClientConfig, ApiInstance, ApiClientFactory } from './../types'; +import { ApiClientFactoryParams, ApiClientConfig, ApiInstance, ApiClientFactory, ApiClientExtension } from './../types'; import { applyContextToApi } from './../utils/context'; import { Logger } from './../utils'; +const isFn = (x) => typeof x === 'function'; + const apiClientFactory = (factoryParams: ApiClientFactoryParams): ApiClientFactory => { function createApiClient (config: any, customApi: any = {}): ApiInstance { - const extensions = factoryParams.extensions && this && this.middleware - // eslint-disable-next-line - ? Object.values(factoryParams.extensions).map((extensionFn) => extensionFn(this.middleware.req, this.middleware.res)) - : []; + const rawExtensions: ApiClientExtension[] = this?.middleware?.extensions || []; + const lifecycles = Object.values(rawExtensions) + .filter(ext => isFn(ext.hooks)) + .map(({ hooks }) => hooks(this?.middleware?.req, this?.middleware?.res)); + const extendedApis = Object.keys(rawExtensions) + .reduce((prev, curr) => ({ ...prev, ...rawExtensions[curr].extendApiMethods }), customApi); - const _config = extensions - .filter(ext => ext.beforeCreate) - .reduce((prev, curr) => curr.beforeCreate(prev), config); + const _config = lifecycles + .filter(ext => isFn(ext.beforeCreate)) + .reduce((prev, curr) => curr.beforeCreate({ configuration: prev }), config); const settings = factoryParams.onCreate ? factoryParams.onCreate(_config) : { config, client: config.client }; Logger.debug('apiClientFactory.create', settings); - settings.config = extensions - .filter(ext => ext.afterCreate) - .reduce((prev, curr) => curr.afterCreate(prev), settings.config); + settings.config = lifecycles + .filter(ext => isFn(ext.afterCreate)) + .reduce((prev, curr) => curr.afterCreate({ configuration: prev }), settings.config); const extensionHooks = { - before: (args) => extensions - .filter(e => e.beforeCall) - .reduce((prev, e) => e.beforeCall(prev), args), - after: (resp) => extensions - .filter(e => e.afterCall) - .reduce((prev, e) => e.afterCall(prev), resp) + before: (params) => lifecycles + .filter(e => isFn(e.beforeCall)) + .reduce((args, e) => e.beforeCall({ ...params, configuration: settings.config, args}), params.args), + after: (params) => lifecycles + .filter(e => isFn(e.afterCall)) + .reduce((response, e) => e.afterCall({ ...params, configuration: settings.config, response }), params.response) }; const api = applyContextToApi( - { ...factoryParams.api, ...customApi }, + { ...factoryParams.api, ...extendedApis }, settings, extensionHooks ); @@ -43,6 +47,8 @@ const apiClientFactory = (f }; } + (createApiClient as any)._predefinedExtensions = factoryParams.extensions || []; + return { createApiClient }; }; diff --git a/packages/core/core/src/server/index.ts b/packages/core/core/src/server/index.ts deleted file mode 100644 index d8833d56e73..00000000000 --- a/packages/core/core/src/server/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import express, { json } from 'express'; - -const app = express(); -app.use(json()); - -const extend = (fn) => fn(app); - -const createMiddleware = ({ apiMiddleware }) => { - if (apiMiddleware && apiMiddleware.extend) { - extend(apiMiddleware.extend); - } - - return { - middleware: { - path: '/api', - handler: app - }, - extend - }; -}; - -export { createMiddleware }; diff --git a/packages/core/core/src/types.ts b/packages/core/core/src/types.ts index 39cd74cd00d..911b92cf2ca 100644 --- a/packages/core/core/src/types.ts +++ b/packages/core/core/src/types.ts @@ -583,17 +583,18 @@ export interface FactoryParams { provide?: (context: Context) => any; } -// TODO: Implement proper typing -// https://github.com/vuestorefront/vue-storefront/issues/5431 export interface ApiClientExtensionLifecycle { - beforeCreate?: (config, headers?: Record) => any; - afterCreate?: ({ config, client }, headers?: Record) => { config; client }; - beforeCall?: ({ config, functionName, params }) => any; - afterCall?: ({ config, functionName, params }) => any; + beforeCreate?: ({ configuration }) => any; + afterCreate?: ({ configuration }) => any; + beforeCall?: ({ configuration, callName, args }) => any; + afterCall?: ({ configuration, callName, args }) => any; } -export type ApiClientExtension = (req: any, res: any) => ApiClientExtensionLifecycle; - +export interface ApiClientExtension { + name: string; + extendApiMethods?: Record; + hooks?: (req: any, res: any) => ApiClientExtensionLifecycle; +} export interface ApiClientFactoryParams { api: F; isProxy?: boolean; diff --git a/packages/core/core/src/utils/context/index.ts b/packages/core/core/src/utils/context/index.ts index aaf49e7b504..4f936f6f495 100644 --- a/packages/core/core/src/utils/context/index.ts +++ b/packages/core/core/src/utils/context/index.ts @@ -5,8 +5,8 @@ interface ContextConfiguration { } interface ApplyingContextHooks { - before: (args: any[]) => any[]; - after: (response: any) => any; + before: ({ callName, args }) => any[]; + after: ({ callName, args, response }) => any; } let useVSFContext = () => ({}) as Context; @@ -15,7 +15,9 @@ const configureContext = (config: ContextConfiguration) => { useVSFContext = config.useVSFContext || useVSFContext; }; -const NOP = (x) => x; +const nopBefore = ({ args }) => args; +const nopAfter = ({ response }) => response; + const applyContextToApi = ( api: Record, context: any, @@ -25,12 +27,18 @@ const applyContextToApi = ( * It's useful in extensions, when someone don't want to inject into changing arguments or the response, * in that case, we use default function, to handle that scenario - NOP */ - hooks: ApplyingContextHooks = { before: NOP, after: NOP } + hooks: ApplyingContextHooks = { before: nopBefore, after: nopAfter } ) => Object.entries(api) - .reduce((prev, [key, fn]: any) => ({ + .reduce((prev, [callName, fn]: any) => ({ ...prev, - [key]: async (...args) => hooks.after(await fn(context, ...hooks.before(args))) + [callName]: async (...args) => { + const transformedArgs = hooks.before({ callName, args }); + const response = await fn(context, ...transformedArgs); + const transformedResponse = hooks.after({ callName, args, response }); + + return transformedResponse; + } }), {}); const generateContext = (factoryParams) => { diff --git a/packages/core/core/src/utils/nuxt/index.ts b/packages/core/core/src/utils/nuxt/index.ts index 38cb75cdca4..d3351080378 100644 --- a/packages/core/core/src/utils/nuxt/index.ts +++ b/packages/core/core/src/utils/nuxt/index.ts @@ -10,8 +10,14 @@ export const integrationPlugin = (pluginFn: NuxtPlugin) => (nuxtCtx: NuxtContext const configure = (tag, configuration) => { const injectInContext = createAddIntegrationToCtx({ tag, nuxtCtx, inject }); const config = getIntegrationConfig(nuxtCtx, configuration); + const { middlewareUrl } = (nuxtCtx as any).$config; + + if (middlewareUrl) { + config.axios.baseURL = middlewareUrl; + } + const client = axios.create(config.axios); - const api = createProxiedApi({ givenApi: configuration.api, client, tag }); + const api = createProxiedApi({ givenApi: configuration.api || {}, client, tag }); injectInContext({ api, client, config }); }; diff --git a/packages/core/middleware/middleware.js b/packages/core/middleware/middleware.js deleted file mode 100644 index 69f1571512f..00000000000 --- a/packages/core/middleware/middleware.js +++ /dev/null @@ -1,54 +0,0 @@ -const express = require('express'); -const cookieParser = require('cookie-parser'); - -const app = express(); -app.use(express.json()); -app.use(cookieParser()); - -function getIntegrationConfigFromModule (moduleName, nuxtOptions) { - const integrationModule = nuxtOptions.buildModules.find(module => - Array.isArray(module) && module[0] === moduleName - ); - - return integrationModule ? integrationModule[1] : {}; -} - -function loadApiClient (apiClientPackageName, { req, res }) { - const apiClientPackage = require(apiClientPackageName); - const context = { middleware: { req, res } }; - const createApiClient = apiClientPackage.createApiClient.bind(context); - - return { ...apiClientPackage, createApiClient }; -} - -function createProxyEndpoint (moduleOptions, nuxtOptions) { - app.post('/:integrationName/:functionName', async (req, res) => { - const { integrationName, functionName } = req.params; - const integration = moduleOptions.integrations[integrationName]; - - const initialConifguration = getIntegrationConfigFromModule(integration.module, nuxtOptions); - - const { createApiClient } = loadApiClient(integration.api, { req, res }); - - const apiClient = createApiClient({ - ...initialConifguration, - locale: 'en', - country: 'US', - currency: 'USD' - }); - - const apiFunction = apiClient.api[functionName]; - - const platformResponse = await apiFunction(...req.body); - - res.send(platformResponse); - }); - - return { - path: '/api', - handler: app - }; -} - -module.exports = createProxyEndpoint; - diff --git a/packages/core/middleware/nuxt/index.js b/packages/core/middleware/nuxt/index.js index c40dba256e0..9a96d0c7045 100644 --- a/packages/core/middleware/nuxt/index.js +++ b/packages/core/middleware/nuxt/index.js @@ -1,5 +1,9 @@ -const createProxyEndpoint = require('./../middleware'); +const { createServer } = require('@vue-storefront/middleware'); -module.exports = function VueStorefrontMiddleware (moduleOptions) { - this.addServerMiddleware(createProxyEndpoint(moduleOptions, this.options)); +module.exports = function VueStorefrontMiddleware () { + const { integrations } = require(this.nuxt.options.rootDir + '/middleware.config.js'); + const handler = createServer({ integrations }); + const serverMiddleware = { path: '/api', handler }; + + this.addServerMiddleware(serverMiddleware); }; diff --git a/packages/core/middleware/package.json b/packages/core/middleware/package.json index 354942d6f76..94fe5eb0107 100644 --- a/packages/core/middleware/package.json +++ b/packages/core/middleware/package.json @@ -1,17 +1,21 @@ { "name": "@vue-storefront/middleware", - "version": "2.1.1-rc.1", + "version": "2.2.1", "description": "", - "main": "module.js", + "main": "lib/index.cjs.js", + "module": "lib/index.es.js", + "types": "lib/src/index.d.ts", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "build": "rimraf lib && rollup -c" }, "author": "Vue Storefront", "license": "MIT", "dependencies": { + "consola": "2.15.3", "express": "^4.17.1", "cookie-parser": "^1.4.5", - "is-https": "^3.0.2" + "is-https": "^3.0.2", + "cors": "^2.8.5" }, "publishConfig": { "access": "public" diff --git a/packages/core/middleware/rollup.config.js b/packages/core/middleware/rollup.config.js new file mode 100644 index 00000000000..10dc02f5e26 --- /dev/null +++ b/packages/core/middleware/rollup.config.js @@ -0,0 +1,4 @@ +import pkg from './package.json'; +import { generateBaseConfig } from '../../rollup.base.config'; + +export default generateBaseConfig(pkg); diff --git a/packages/core/middleware/src/createServer.ts b/packages/core/middleware/src/createServer.ts new file mode 100644 index 00000000000..95787245baa --- /dev/null +++ b/packages/core/middleware/src/createServer.ts @@ -0,0 +1,44 @@ +import express from 'express'; +import cookieParser from 'cookie-parser'; +import cors from 'cors'; +import consola from 'consola'; +import { registerIntegrations } from './integrations'; + +const app = express(); +app.use(express.json()); +app.use(cookieParser()); +app.use(cors()); + +const applyMiddlewreContext = (createApiClient, { req, res, extensions }) => { + const context = { + middleware: { req, res, extensions } + }; + + return createApiClient.bind(context); +}; + +function createServer (config) { + consola.info('Middleware starting....'); + consola.info('Loading integartions...'); + + const integrations = registerIntegrations(config.integrations); + consola.success('Integrations loaded!'); + + app.post('/:integrationName/:functionName', async (req, res) => { + const { integrationName, functionName } = req.params; + const { apiClient, configuration, extensions } = integrations[integrationName]; + const middlewareContext = { req, res, extensions }; + const createApiClient = applyMiddlewreContext(apiClient.createApiClient, middlewareContext); + const apiClientInstance = createApiClient(configuration); + const apiFunction = apiClientInstance.api[functionName]; + const platformResponse = await apiFunction(...req.body); + + res.send(platformResponse); + }); + + consola.success('Middleware created!'); + + return app; +} + +export { createServer }; diff --git a/packages/core/middleware/src/index.ts b/packages/core/middleware/src/index.ts new file mode 100644 index 00000000000..f94bdb51cba --- /dev/null +++ b/packages/core/middleware/src/index.ts @@ -0,0 +1 @@ +export * from './createServer'; diff --git a/packages/core/middleware/src/integrations.ts b/packages/core/middleware/src/integrations.ts new file mode 100644 index 00000000000..2d71baf3590 --- /dev/null +++ b/packages/core/middleware/src/integrations.ts @@ -0,0 +1,38 @@ +import consola from 'consola'; + +const createRawExtensions = (apiClient, integrationConfig) => { + const extensionsCreateFn = integrationConfig.extensions; + const predefinedExtensions = apiClient.createApiClient._predefinedExtensions; + return extensionsCreateFn ? extensionsCreateFn(predefinedExtensions) : predefinedExtensions; +}; + +const lookUpExternal = (curr) => typeof curr === 'string' ? require(curr) : [curr]; + +const createExtensions = (rawExtensions) => rawExtensions + .reduce((prev, curr) => [...prev, ...lookUpExternal(curr)], []); + +const registerIntegrations = (integrations) => + Object.entries(integrations).reduce((prev, [tag, integrationConfig]: any) => { + consola.info(`- Loading: ${tag} ${integrationConfig.location}`); + + const apiClient = require(integrationConfig.location); + const rawExtensions = createRawExtensions(apiClient, integrationConfig); + const extensions = createExtensions(rawExtensions); + + extensions.forEach(({ name }) => { + consola.info(`- Loading: ${tag} extension: ${name}`); + }); + + consola.success(`- Integration: ${tag} loaded!`); + + return { + ...prev, + [tag]: { + apiClient, + configuration: integrationConfig.configuration, + extensions + } + }; + }, {}); + +export { registerIntegrations }; diff --git a/packages/core/middleware/tsconfig.json b/packages/core/middleware/tsconfig.json new file mode 100644 index 00000000000..5f31c268ff0 --- /dev/null +++ b/packages/core/middleware/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "outDir": "./lib", + "esModuleInterop": true, + "target": "es5", + "module": "ES2015", + "moduleResolution": "node", + "importHelpers": true, + "noEmitHelpers": true, + "sourceMap": true, + "declaration": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": "./", + "lib": ["es6", "es7", "dom"], + "strict": false, + }, + "exclude": ["node_modules", "**/*.spec.ts"] +} diff --git a/yarn.lock b/yarn.lock index 18d1229bfdc..867cd46e1a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5217,6 +5217,11 @@ connect@^3.7.0: parseurl "~1.3.3" utils-merge "1.0.1" +consola@2.15.3: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + consola@^1.4.5: version "1.4.5" resolved "https://registry.yarnpkg.com/consola/-/consola-1.4.5.tgz#09732d07cb50af07332e54e0f42fafb92b962c4a" @@ -5463,6 +5468,14 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + cosmiconfig@^5.0.0, cosmiconfig@^5.1.0: version "5.2.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" @@ -11135,7 +11148,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -15437,7 +15450,7 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" -vary@^1.1.2, vary@~1.1.2: +vary@^1, vary@^1.1.2, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=