@@ -10,9 +10,10 @@ export interface ImplementationContext { | |||
endpoint: Endpoint; | |||
params: Record<string, unknown>; | |||
query?: URLSearchParams; | |||
dataSource?: DataSource; | |||
} | |||
type ImplementationFunction = (params: ImplementationContext) => Promise<Response | void>; | |||
export type ImplementationFunction = (params: ImplementationContext) => Promise<Response | void>; | |||
export interface Backend<App extends BaseApp = BaseApp> { | |||
app: App; | |||
@@ -88,6 +88,7 @@ class ServerInstance<Backend extends BaseBackend> implements Server<Backend> { | |||
endpoint, | |||
params: endpointParams ?? {}, | |||
query: typeof search !== 'undefined' ? new URLSearchParams(search) : undefined, | |||
dataSource: this.backend.dataSource, | |||
}); | |||
if (typeof responseSpec === 'undefined') { | |||
@@ -16,6 +16,14 @@ declare module '../../client' { | |||
} | |||
} | |||
const DEFAULT_HOST = '0.0.0.0' as const; | |||
const DEFAULT_PORT = 80 as const; | |||
const DEFAULT_METHOD = 'GET' as const; | |||
const EXTENSION_METHOD_EFFECTIVE_METHOD = 'POST' as const; | |||
class ClientInstance<App extends BaseApp> implements Client<App> { | |||
readonly app: App; | |||
private readonly fetchFn: typeof fetch; | |||
@@ -29,8 +37,8 @@ class ClientInstance<App extends BaseApp> implements Client<App> { | |||
async connect(params: ServiceParams): Promise<ClientConnection> { | |||
const connection = { | |||
host: params.host ?? '0.0.0.0', | |||
port: params.port ?? 80, | |||
host: params.host ?? DEFAULT_HOST, | |||
port: params.port ?? DEFAULT_PORT, | |||
basePath: params.basePath ?? '', | |||
}; | |||
@@ -52,14 +60,15 @@ class ClientInstance<App extends BaseApp> implements Client<App> { | |||
makeRequest(operation: Operation) { | |||
const baseUrlFragments = [ | |||
this.connection?.host ?? '0.0.0.0' | |||
this.connection?.host ?? DEFAULT_HOST | |||
]; | |||
const thePort = (this.connection?.port ?? 80); | |||
if (thePort !== 80) { | |||
const thePort = (this.connection?.port ?? DEFAULT_PORT); | |||
if (thePort !== DEFAULT_PORT) { | |||
baseUrlFragments.push(thePort.toString()); | |||
} | |||
// TODO how to set to https? | |||
const scheme = 'http'; | |||
// todo need a way to decode url back to endpoint queue | |||
const urlString = serializeEndpointQueue(this.endpointQueue); | |||
@@ -72,9 +81,9 @@ class ClientInstance<App extends BaseApp> implements Client<App> { | |||
if (typeof operation.searchParams !== 'undefined') { | |||
url.search = operation.searchParams.toString(); | |||
} | |||
const rawEffectiveMethod = (operation.method ?? 'GET').toUpperCase(); | |||
const rawEffectiveMethod = (operation.method ?? DEFAULT_METHOD).toUpperCase(); | |||
const finalEffectiveMethod = (AVAILABLE_EXTENSION_METHODS as unknown as string[]).includes(rawEffectiveMethod) | |||
? 'POST' as const | |||
? EXTENSION_METHOD_EFFECTIVE_METHOD | |||
: rawEffectiveMethod; | |||
if (typeof operation.body !== 'undefined') { | |||
@@ -1,12 +1,13 @@ | |||
import {Recipe} from '../../common/recipe'; | |||
import {endpoint, operation, validation as v} from '../../common'; | |||
import {endpoint, validation as v} from '../../common'; | |||
import {backend, DataSource} from '../../backend'; | |||
import { | |||
DataSourceNotFoundResponseError, | |||
ItemNotFoundReponseError, | |||
ResourceCollectionFetchedResponse, | |||
ResourceItemFetchedResponse, | |||
} from './response'; | |||
import * as fetchOperation from './implementation/fetch'; | |||
import * as createOperation from './implementation/create'; | |||
import * as emplaceOperation from './implementation/emplace'; | |||
import * as patchDeltaOperation from './implementation/patch-delta'; | |||
import * as patchMergeOperation from './implementation/patch-merge'; | |||
import * as queryOperation from './implementation/query'; | |||
import * as deleteOperation from './implementation/delete'; | |||
interface AddResourceRecipeParams { | |||
endpointName: string; | |||
@@ -15,9 +16,13 @@ interface AddResourceRecipeParams { | |||
export const addResourceRecipe = (params: AddResourceRecipeParams): Recipe => (a) => { | |||
const operations = { | |||
fetch: operation({ | |||
name: 'fetch' as const, | |||
}), | |||
fetch: fetchOperation.operation, | |||
create: createOperation.operation, | |||
emplace: emplaceOperation.operation, | |||
patchMerge: patchMergeOperation.operation, | |||
patchDelta: patchDeltaOperation.operation, | |||
query: queryOperation.operation, | |||
delete: deleteOperation.operation, | |||
}; | |||
const theEndpoint = endpoint({ | |||
@@ -27,10 +32,22 @@ export const addResourceRecipe = (params: AddResourceRecipeParams): Recipe => (a | |||
}), | |||
}) | |||
.param('resourceId') | |||
.can('fetch'); | |||
.can(fetchOperation.name) | |||
.can(createOperation.name) | |||
.can(emplaceOperation.name) | |||
.can(patchMergeOperation.name) | |||
.can(patchDeltaOperation.name) | |||
.can(queryOperation.name) | |||
.can(deleteOperation.name); | |||
const enhancedApp = a.app | |||
.operation(operations.fetch) | |||
.operation(operations.create) | |||
.operation(operations.emplace) | |||
.operation(operations.patchMerge) | |||
.operation(operations.patchDelta) | |||
.operation(operations.query) | |||
.operation(operations.delete) | |||
.endpoint(theEndpoint); | |||
const theBackend = a.backend ?? backend({ | |||
@@ -39,39 +56,13 @@ export const addResourceRecipe = (params: AddResourceRecipeParams): Recipe => (a | |||
}); | |||
theBackend | |||
.implementOperation('fetch', async (ctx) => { | |||
const dataSource: DataSource = ctx.endpoint.dataSource ?? theBackend.dataSource ?? {} as DataSource; | |||
// need to genericise the response here so we don't depend on the HTTP responses. | |||
const { resourceId } = ctx.params; | |||
const { getById, getMultiple } = dataSource; | |||
if (typeof resourceId === 'undefined') { | |||
if (typeof getMultiple === 'undefined') { | |||
throw new DataSourceNotFoundResponseError(); | |||
} | |||
// TODO add query here | |||
const items = await getMultiple(); | |||
return new ResourceCollectionFetchedResponse({ | |||
statusMessage: 'Resource Collection Fetched', | |||
body: items, | |||
}); | |||
} | |||
if (typeof getById === 'undefined') { | |||
throw new DataSourceNotFoundResponseError(); | |||
} | |||
const item = await getById(resourceId); | |||
if (!item) { | |||
throw new ItemNotFoundReponseError(); | |||
} | |||
return new ResourceItemFetchedResponse({ | |||
statusMessage: 'Resource Item Fetched', | |||
body: item, | |||
}); | |||
}); | |||
.implementOperation(fetchOperation.name, fetchOperation.implementation) | |||
.implementOperation(createOperation.name, createOperation.implementation) | |||
.implementOperation(deleteOperation.name, deleteOperation.implementation) | |||
.implementOperation(queryOperation.name, queryOperation.implementation) | |||
.implementOperation(patchDeltaOperation.name, patchDeltaOperation.implementation) | |||
.implementOperation(patchMergeOperation.name, patchMergeOperation.implementation) | |||
.implementOperation(emplaceOperation.name, emplaceOperation.implementation); | |||
return { | |||
operations, | |||
@@ -0,0 +1,15 @@ | |||
import {ImplementationFunction} from '../../../backend'; | |||
import {operation as defineOperation} from '../../../common'; | |||
export const name = 'create' as const; | |||
export const method = 'POST' as const; | |||
export const operation = defineOperation({ | |||
name, | |||
method, | |||
}); | |||
export const implementation: ImplementationFunction = async (ctx) => { | |||
}; |
@@ -0,0 +1,15 @@ | |||
import {ImplementationFunction} from '../../../backend'; | |||
import {operation as defineOperation} from '../../../common'; | |||
export const name = 'delete' as const; | |||
export const method = 'DELETE' as const; | |||
export const operation = defineOperation({ | |||
name, | |||
method, | |||
}); | |||
export const implementation: ImplementationFunction = async (ctx) => { | |||
}; |
@@ -0,0 +1,15 @@ | |||
import {ImplementationFunction} from '../../../backend'; | |||
import {operation as defineOperation} from '../../../common'; | |||
export const name = 'emplace' as const; | |||
export const method = 'PUT' as const; | |||
export const operation = defineOperation({ | |||
name, | |||
method, | |||
}); | |||
export const implementation: ImplementationFunction = async (ctx) => { | |||
}; |
@@ -0,0 +1,51 @@ | |||
import {DataSource, ImplementationFunction} from '../../../backend'; | |||
import { | |||
DataSourceNotFoundResponseError, | |||
ItemNotFoundReponseError, | |||
ResourceCollectionFetchedResponse, | |||
ResourceItemFetchedResponse, | |||
} from '../response'; | |||
import {operation as defineOperation} from '../../../common'; | |||
export const name = 'fetch' as const; | |||
export const method = 'GET' as const; | |||
export const operation = defineOperation({ | |||
name, | |||
method, | |||
}); | |||
export const implementation: ImplementationFunction = async (ctx) => { | |||
const dataSource: DataSource = ctx.endpoint.dataSource ?? ctx.dataSource ?? {} as DataSource; | |||
// need to genericise the response here so we don't depend on the HTTP responses. | |||
const { resourceId } = ctx.params; | |||
const { getById, getMultiple } = dataSource; | |||
if (typeof resourceId === 'undefined') { | |||
if (typeof getMultiple === 'undefined') { | |||
throw new DataSourceNotFoundResponseError(); | |||
} | |||
// TODO add query here | |||
const items = await getMultiple(); | |||
return new ResourceCollectionFetchedResponse({ | |||
statusMessage: 'Resource Collection Fetched', | |||
body: items, | |||
}); | |||
} | |||
if (typeof getById === 'undefined') { | |||
throw new DataSourceNotFoundResponseError(); | |||
} | |||
const item = await getById(resourceId); | |||
if (!item) { | |||
throw new ItemNotFoundReponseError(); | |||
} | |||
return new ResourceItemFetchedResponse({ | |||
statusMessage: 'Resource Item Fetched', | |||
body: item, | |||
}); | |||
}; |
@@ -0,0 +1,15 @@ | |||
import {ImplementationFunction} from '../../../backend'; | |||
import {operation as defineOperation} from '../../../common'; | |||
export const name = 'patchDelta' as const; | |||
export const method = 'PATCH' as const; | |||
export const operation = defineOperation({ | |||
name, | |||
method, | |||
}); | |||
export const implementation: ImplementationFunction = async (ctx) => { | |||
}; |
@@ -0,0 +1,15 @@ | |||
import {ImplementationFunction} from '../../../backend'; | |||
import {operation as defineOperation} from '../../../common'; | |||
export const name = 'patchMerge' as const; | |||
export const method = 'PATCH' as const; | |||
export const operation = defineOperation({ | |||
name, | |||
method, | |||
}); | |||
export const implementation: ImplementationFunction = async (ctx) => { | |||
}; |
@@ -0,0 +1,15 @@ | |||
import {ImplementationFunction} from '../../../backend'; | |||
import {operation as defineOperation} from '../../../common'; | |||
export const name = 'query' as const; | |||
export const method = 'QUERY' as const; | |||
export const operation = defineOperation({ | |||
name, | |||
method, | |||
}); | |||
export const implementation: ImplementationFunction = async (ctx) => { | |||
}; |