@@ -10,9 +10,10 @@ export interface ImplementationContext { | |||||
endpoint: Endpoint; | endpoint: Endpoint; | ||||
params: Record<string, unknown>; | params: Record<string, unknown>; | ||||
query?: URLSearchParams; | 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> { | export interface Backend<App extends BaseApp = BaseApp> { | ||||
app: App; | app: App; | ||||
@@ -88,6 +88,7 @@ class ServerInstance<Backend extends BaseBackend> implements Server<Backend> { | |||||
endpoint, | endpoint, | ||||
params: endpointParams ?? {}, | params: endpointParams ?? {}, | ||||
query: typeof search !== 'undefined' ? new URLSearchParams(search) : undefined, | query: typeof search !== 'undefined' ? new URLSearchParams(search) : undefined, | ||||
dataSource: this.backend.dataSource, | |||||
}); | }); | ||||
if (typeof responseSpec === 'undefined') { | 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> { | class ClientInstance<App extends BaseApp> implements Client<App> { | ||||
readonly app: App; | readonly app: App; | ||||
private readonly fetchFn: typeof fetch; | private readonly fetchFn: typeof fetch; | ||||
@@ -29,8 +37,8 @@ class ClientInstance<App extends BaseApp> implements Client<App> { | |||||
async connect(params: ServiceParams): Promise<ClientConnection> { | async connect(params: ServiceParams): Promise<ClientConnection> { | ||||
const connection = { | 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 ?? '', | basePath: params.basePath ?? '', | ||||
}; | }; | ||||
@@ -52,14 +60,15 @@ class ClientInstance<App extends BaseApp> implements Client<App> { | |||||
makeRequest(operation: Operation) { | makeRequest(operation: Operation) { | ||||
const baseUrlFragments = [ | 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()); | baseUrlFragments.push(thePort.toString()); | ||||
} | } | ||||
// TODO how to set to https? | |||||
const scheme = 'http'; | const scheme = 'http'; | ||||
// todo need a way to decode url back to endpoint queue | // todo need a way to decode url back to endpoint queue | ||||
const urlString = serializeEndpointQueue(this.endpointQueue); | const urlString = serializeEndpointQueue(this.endpointQueue); | ||||
@@ -72,9 +81,9 @@ class ClientInstance<App extends BaseApp> implements Client<App> { | |||||
if (typeof operation.searchParams !== 'undefined') { | if (typeof operation.searchParams !== 'undefined') { | ||||
url.search = operation.searchParams.toString(); | 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) | const finalEffectiveMethod = (AVAILABLE_EXTENSION_METHODS as unknown as string[]).includes(rawEffectiveMethod) | ||||
? 'POST' as const | |||||
? EXTENSION_METHOD_EFFECTIVE_METHOD | |||||
: rawEffectiveMethod; | : rawEffectiveMethod; | ||||
if (typeof operation.body !== 'undefined') { | if (typeof operation.body !== 'undefined') { | ||||
@@ -1,12 +1,13 @@ | |||||
import {Recipe} from '../../common/recipe'; | 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 {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 { | interface AddResourceRecipeParams { | ||||
endpointName: string; | endpointName: string; | ||||
@@ -15,9 +16,13 @@ interface AddResourceRecipeParams { | |||||
export const addResourceRecipe = (params: AddResourceRecipeParams): Recipe => (a) => { | export const addResourceRecipe = (params: AddResourceRecipeParams): Recipe => (a) => { | ||||
const operations = { | 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({ | const theEndpoint = endpoint({ | ||||
@@ -27,10 +32,22 @@ export const addResourceRecipe = (params: AddResourceRecipeParams): Recipe => (a | |||||
}), | }), | ||||
}) | }) | ||||
.param('resourceId') | .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 | const enhancedApp = a.app | ||||
.operation(operations.fetch) | .operation(operations.fetch) | ||||
.operation(operations.create) | |||||
.operation(operations.emplace) | |||||
.operation(operations.patchMerge) | |||||
.operation(operations.patchDelta) | |||||
.operation(operations.query) | |||||
.operation(operations.delete) | |||||
.endpoint(theEndpoint); | .endpoint(theEndpoint); | ||||
const theBackend = a.backend ?? backend({ | const theBackend = a.backend ?? backend({ | ||||
@@ -39,39 +56,13 @@ export const addResourceRecipe = (params: AddResourceRecipeParams): Recipe => (a | |||||
}); | }); | ||||
theBackend | 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 { | return { | ||||
operations, | 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) => { | |||||
}; |