Making progress on api operations

This commit is contained in:
Luis Alvarez 2021-05-03 12:44:28 -05:00
parent 2281490342
commit beccca8eaf
6 changed files with 167 additions and 95 deletions

View File

@ -1,13 +1,19 @@
import type { NextApiHandler } from 'next' import type { NextApiHandler } from 'next'
import type { RequestInit } from '@vercel/fetch' import type { RequestInit } from '@vercel/fetch'
import { import { CommerceAPI, CommerceAPIConfig } from '@commerce/api'
CommerceAPI as CoreCommerceAPI,
CommerceAPIConfig,
} from '@commerce/api'
import fetchGraphqlApi from './utils/fetch-graphql-api' import fetchGraphqlApi from './utils/fetch-graphql-api'
import fetchStoreApi from './utils/fetch-store-api' import fetchStoreApi from './utils/fetch-store-api'
import type { CartAPI } from './cart' 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 { export interface BigcommerceConfig extends CommerceAPIConfig {
// Indicates if the returned metadata with translations should be applied to the // Indicates if the returned metadata with translations should be applied to the
@ -103,25 +109,29 @@ const config2: BigcommerceConfig = {
export const provider = { export const provider = {
config: config2, config: config2,
operations: { login },
} }
export type Provider = typeof provider export type Provider = typeof provider
export type APIs = CartAPI export type APIs = CartAPI
export class CommerceAPI extends CoreCommerceAPI<Provider> { export function getCommerceApi<P extends Provider>(
constructor(customProvider: Provider = provider) { customProvider: P = provider as any
super(customProvider) ) {
} const commerce = new CommerceAPI(customProvider)
const operations = getOperations(customProvider.operations, { commerce })
endpoint<E extends APIs>( return Object.assign(commerce, operations, {
context: E['endpoint'] & { endpoint<E extends APIs>(
config?: Provider['config'] context: E['endpoint'] & {
options?: E['schema']['endpoint']['options'] config?: P['config']
} options?: E['schema']['endpoint']['options']
): NextApiHandler { }
return super.endpoint(context) ): NextApiHandler {
} return super.endpoint(context)
},
})
} }
export function getConfig(userConfig?: Partial<BigcommerceConfig>) { export function getConfig(userConfig?: Partial<BigcommerceConfig>) {

View File

@ -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 extends { result?: any } = { result?: string }> = T
export type LoginVariables = LoginMutationVariables
function loginOperation({ commerce }: OperationContext<Provider>) {
async function login(opts: {
variables: LoginVariables
config?: BigcommerceConfig
res: ServerResponse
}): Promise<LoginResult>
async function login<T extends { result?: any }, V = any>(opts: {
query: string
variables: V
res: ServerResponse
config?: BigcommerceConfig
}): Promise<LoginResult<T>>
async function login({
query = loginMutation,
variables,
res: response,
config,
}: {
query?: string
variables: LoginVariables
res: ServerResponse
config?: BigcommerceConfig
}): Promise<LoginResult> {
config = commerce.getConfig(config)
const { data, res } = await config.fetch<RecursivePartial<LoginMutation>>(
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

View File

@ -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 extends { result?: any } = { result?: string }> = T
export type LoginVariables = LoginMutationVariables
async function login(opts: {
variables: LoginVariables
config?: BigcommerceConfig
res: ServerResponse
}): Promise<LoginResult>
async function login<T extends { result?: any }, V = any>(opts: {
query: string
variables: V
res: ServerResponse
config?: BigcommerceConfig
}): Promise<LoginResult<T>>
async function login({
query = loginMutation,
variables,
res: response,
config,
}: {
query?: string
variables: LoginVariables
res: ServerResponse
config?: BigcommerceConfig
}): Promise<LoginResult> {
config = getConfig(config)
const { data, res } = await config.fetch<RecursivePartial<LoginMutation>>(
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

View File

@ -2,6 +2,7 @@ import type { NextApiHandler } from 'next'
import type { RequestInit, Response } from '@vercel/fetch' import type { RequestInit, Response } from '@vercel/fetch'
import type { APIEndpoint, APIHandler } from './utils/types' import type { APIEndpoint, APIHandler } from './utils/types'
import type { CartSchema } from '../types/cart' import type { CartSchema } from '../types/cart'
import { APIOperations } from './operations'
export type APISchemas = CartSchema export type APISchemas = CartSchema
@ -48,14 +49,13 @@ export type EndpointHandlers<
export type APIProvider = { export type APIProvider = {
config: CommerceAPIConfig config: CommerceAPIConfig
operations: APIOperations<any>
} }
export class CommerceAPI<P extends APIProvider = APIProvider> { export class CommerceAPI<P extends APIProvider = APIProvider> {
constructor(readonly provider: P) { constructor(readonly provider: P) {}
this.provider = provider
}
getConfig(userConfig: Partial<P['config']> = {}) { getConfig(userConfig: Partial<P['config']> = {}): P['config'] {
return Object.entries(userConfig).reduce( return Object.entries(userConfig).reduce(
(cfg, [key, value]) => Object.assign(cfg, { [key]: value }), (cfg, [key, value]) => Object.assign(cfg, { [key]: value }),
{ ...this.provider.config } { ...this.provider.config }

View File

@ -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 extends { result?: any } = { result?: string }> = 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<P extends APIProvider>(
ops: P['operations'],
ctx: { commerce: CommerceAPI<P> }
) {
return OPERATIONS.reduce<Operations<P>>((carry, k) => {
carry[k] = ops[k]({ ...ctx, operations: carry })
return carry
}, defaultOperations) as APIOperations2<P>
}
export type AllowedOperations = typeof OPERATIONS[number]
export type Operations<P extends APIProvider> = {
login: {
(opts: {
variables: any
config?: P['config']
res: ServerResponse
}): Promise<LoginResult>
<T extends { result?: any }, V = any>(opts: {
query: string
variables: V
res: ServerResponse
config?: P['config'] | undefined
}): Promise<LoginResult<T>>
}
}
export type APIOperations<P extends APIProvider> = {
[K in keyof Operations<P>]: (ctx: OperationContext<P>) => Operations<P>[K]
}
export type APIOperations2<P extends APIProvider> = {
[K in keyof APIOperations<P>]: ReturnType<P['operations'][K]>
}
export type OperationContext<P extends APIProvider> = {
commerce: CommerceAPI<P>
operations: Operations<P>
}

View File

@ -1,3 +1,3 @@
import { CommerceAPI } from '@framework/api' import { getCommerceApi } from '@framework/api'
export default new CommerceAPI() export default getCommerceApi()