diff --git a/framework/bigcommerce/api/index.ts b/framework/bigcommerce/api/index.ts index 199868abc..9d30b1f2c 100644 --- a/framework/bigcommerce/api/index.ts +++ b/framework/bigcommerce/api/index.ts @@ -1,13 +1,19 @@ import type { NextApiHandler } from 'next' import type { RequestInit } from '@vercel/fetch' -import { - CommerceAPI as CoreCommerceAPI, - CommerceAPIConfig, -} from '@commerce/api' +import { CommerceAPI, CommerceAPIConfig } from '@commerce/api' import fetchGraphqlApi from './utils/fetch-graphql-api' import fetchStoreApi from './utils/fetch-store-api' import type { CartAPI } from './cart' +import login from './operations/login' +import { + Operations, + defaultOperations, + AllowedOperations, + OPERATIONS, + getOperations, + APIOperations, +} from '@commerce/api/operations' export interface BigcommerceConfig extends CommerceAPIConfig { // Indicates if the returned metadata with translations should be applied to the @@ -103,25 +109,29 @@ const config2: BigcommerceConfig = { export const provider = { config: config2, + operations: { login }, } export type Provider = typeof provider export type APIs = CartAPI -export class CommerceAPI extends CoreCommerceAPI { - constructor(customProvider: Provider = provider) { - super(customProvider) - } +export function getCommerceApi

( + customProvider: P = provider as any +) { + const commerce = new CommerceAPI(customProvider) + const operations = getOperations(customProvider.operations, { commerce }) - endpoint( - context: E['endpoint'] & { - config?: Provider['config'] - options?: E['schema']['endpoint']['options'] - } - ): NextApiHandler { - return super.endpoint(context) - } + return Object.assign(commerce, operations, { + endpoint( + context: E['endpoint'] & { + config?: P['config'] + options?: E['schema']['endpoint']['options'] + } + ): NextApiHandler { + return super.endpoint(context) + }, + }) } export function getConfig(userConfig?: Partial) { diff --git a/framework/bigcommerce/api/operations/login.ts b/framework/bigcommerce/api/operations/login.ts new file mode 100644 index 000000000..0b4c46d23 --- /dev/null +++ b/framework/bigcommerce/api/operations/login.ts @@ -0,0 +1,78 @@ +import type { ServerResponse } from 'http' +import type { OperationContext } from '@commerce/api/operations' +import type { LoginMutation, LoginMutationVariables } from '../../schema' +import type { RecursivePartial } from '../utils/types' +import concatHeader from '../utils/concat-cookie' +import type { BigcommerceConfig, Provider } from '..' + +export const loginMutation = /* GraphQL */ ` + mutation login($email: String!, $password: String!) { + login(email: $email, password: $password) { + result + } + } +` + +export type LoginResult = T + +export type LoginVariables = LoginMutationVariables + +function loginOperation({ commerce }: OperationContext) { + async function login(opts: { + variables: LoginVariables + config?: BigcommerceConfig + res: ServerResponse + }): Promise + + async function login(opts: { + query: string + variables: V + res: ServerResponse + config?: BigcommerceConfig + }): Promise> + + async function login({ + query = loginMutation, + variables, + res: response, + config, + }: { + query?: string + variables: LoginVariables + res: ServerResponse + config?: BigcommerceConfig + }): Promise { + config = commerce.getConfig(config) + + const { data, res } = await config.fetch>( + query, + { variables } + ) + // Bigcommerce returns a Set-Cookie header with the auth cookie + let cookie = res.headers.get('Set-Cookie') + + if (cookie && typeof cookie === 'string') { + // In development, don't set a secure cookie or the browser will ignore it + if (process.env.NODE_ENV !== 'production') { + cookie = cookie.replace('; Secure', '') + // SameSite=none can't be set unless the cookie is Secure + // bc seems to sometimes send back SameSite=None rather than none so make + // this case insensitive + cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax') + } + + response.setHeader( + 'Set-Cookie', + concatHeader(response.getHeader('Set-Cookie'), cookie)! + ) + } + + return { + result: data.login?.result, + } + } + + return login +} + +export default loginOperation diff --git a/framework/bigcommerce/auth/login.ts b/framework/bigcommerce/auth/login.ts deleted file mode 100644 index 3fef29879..000000000 --- a/framework/bigcommerce/auth/login.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { ServerResponse } from 'http' -import type { LoginMutation, LoginMutationVariables } from '../schema' -import type { RecursivePartial } from '../api/utils/types' -import concatHeader from '../api/utils/concat-cookie' -import { BigcommerceConfig, getConfig } from '../api' - -export const loginMutation = /* GraphQL */ ` - mutation login($email: String!, $password: String!) { - login(email: $email, password: $password) { - result - } - } -` - -export type LoginResult = T - -export type LoginVariables = LoginMutationVariables - -async function login(opts: { - variables: LoginVariables - config?: BigcommerceConfig - res: ServerResponse -}): Promise - -async function login(opts: { - query: string - variables: V - res: ServerResponse - config?: BigcommerceConfig -}): Promise> - -async function login({ - query = loginMutation, - variables, - res: response, - config, -}: { - query?: string - variables: LoginVariables - res: ServerResponse - config?: BigcommerceConfig -}): Promise { - config = getConfig(config) - - const { data, res } = await config.fetch>( - query, - { variables } - ) - // Bigcommerce returns a Set-Cookie header with the auth cookie - let cookie = res.headers.get('Set-Cookie') - - if (cookie && typeof cookie === 'string') { - // In development, don't set a secure cookie or the browser will ignore it - if (process.env.NODE_ENV !== 'production') { - cookie = cookie.replace('; Secure', '') - // SameSite=none can't be set unless the cookie is Secure - // bc seems to sometimes send back SameSite=None rather than none so make - // this case insensitive - cookie = cookie.replace(/; SameSite=none/gi, '; SameSite=lax') - } - - response.setHeader( - 'Set-Cookie', - concatHeader(response.getHeader('Set-Cookie'), cookie)! - ) - } - - return { - result: data.login?.result, - } -} - -export default login diff --git a/framework/commerce/api/index.ts b/framework/commerce/api/index.ts index ed3e80378..6d45bfa0c 100644 --- a/framework/commerce/api/index.ts +++ b/framework/commerce/api/index.ts @@ -2,6 +2,7 @@ import type { NextApiHandler } from 'next' import type { RequestInit, Response } from '@vercel/fetch' import type { APIEndpoint, APIHandler } from './utils/types' import type { CartSchema } from '../types/cart' +import { APIOperations } from './operations' export type APISchemas = CartSchema @@ -48,14 +49,13 @@ export type EndpointHandlers< export type APIProvider = { config: CommerceAPIConfig + operations: APIOperations } export class CommerceAPI

{ - constructor(readonly provider: P) { - this.provider = provider - } + constructor(readonly provider: P) {} - getConfig(userConfig: Partial = {}) { + getConfig(userConfig: Partial = {}): P['config'] { return Object.entries(userConfig).reduce( (cfg, [key, value]) => Object.assign(cfg, { [key]: value }), { ...this.provider.config } diff --git a/framework/commerce/api/operations.ts b/framework/commerce/api/operations.ts new file mode 100644 index 000000000..353c1e332 --- /dev/null +++ b/framework/commerce/api/operations.ts @@ -0,0 +1,57 @@ +import type { ServerResponse } from 'http' +import type { APIProvider, CommerceAPI, CommerceAPIConfig } from '.' + +const noop = () => { + throw new Error('Not implemented') +} + +export type LoginResult = T + +export const OPERATIONS = ['login'] as const + +export const defaultOperations = OPERATIONS.reduce((ops, k) => { + ops[k] = noop + return ops +}, {} as { [K in AllowedOperations]: typeof noop }) + +export function getOperations

( + ops: P['operations'], + ctx: { commerce: CommerceAPI

} +) { + return OPERATIONS.reduce>((carry, k) => { + carry[k] = ops[k]({ ...ctx, operations: carry }) + return carry + }, defaultOperations) as APIOperations2

+} + +export type AllowedOperations = typeof OPERATIONS[number] + +export type Operations

= { + login: { + (opts: { + variables: any + config?: P['config'] + res: ServerResponse + }): Promise + + (opts: { + query: string + variables: V + res: ServerResponse + config?: P['config'] | undefined + }): Promise> + } +} + +export type APIOperations

= { + [K in keyof Operations

]: (ctx: OperationContext

) => Operations

[K] +} + +export type APIOperations2

= { + [K in keyof APIOperations

]: ReturnType +} + +export type OperationContext

= { + commerce: CommerceAPI

+ operations: Operations

+} diff --git a/lib/api/commerce.ts b/lib/api/commerce.ts index 35b659a79..499137004 100644 --- a/lib/api/commerce.ts +++ b/lib/api/commerce.ts @@ -1,3 +1,3 @@ -import { CommerceAPI } from '@framework/api' +import { getCommerceApi } from '@framework/api' -export default new CommerceAPI() +export default getCommerceApi()