New API is much more flexible in implementation.refactor/new-arch
@@ -0,0 +1,52 @@ | |||
import {App as BaseApp, AppOperations, Endpoint} from '../common'; | |||
interface BackendParams<App extends BaseApp> { | |||
app: App; | |||
} | |||
type AppOperationArgs<App extends BaseApp, Operation extends string> = ( | |||
App extends BaseApp<string, { endpoints: any; operations: infer S }> | |||
? ( | |||
S extends Record<string, readonly string[]> | |||
? S[Operation][number] | |||
: never | |||
) | |||
: never | |||
); | |||
interface ImplementationFunctionParams<App extends BaseApp, Operation extends AppOperations<App> = AppOperations<App>> { | |||
endpoint: Endpoint; | |||
params: unknown; | |||
arg?: AppOperationArgs<App, Operation>; | |||
query?: URLSearchParams; | |||
} | |||
type ImplementationFunction<App extends BaseApp, Operation extends AppOperations<App> = AppOperations<App>> = (params: ImplementationFunctionParams<App, Operation>) => void; | |||
export interface Backend<App extends BaseApp = BaseApp> { | |||
app: App; | |||
implementations: Map<string, ImplementationFunction<App>>; | |||
implementOperation<Operation extends AppOperations<App>>(operation: Operation, implementation: ImplementationFunction<App, Operation>): this; | |||
} | |||
class BackendInstance<App extends BaseApp> implements Backend<App> { | |||
readonly app: App; | |||
readonly implementations: Map<string, ImplementationFunction<App>>; | |||
constructor(params: BackendParams<App>) { | |||
this.app = params.app; | |||
this.implementations = new Map<string, ImplementationFunction<App>>(); | |||
} | |||
implementOperation<Operation extends AppOperations<App>>( | |||
operation: Operation, | |||
implementation: ImplementationFunction<App, Operation> | |||
) { | |||
this.implementations.set(operation, implementation); | |||
return this; | |||
} | |||
} | |||
export const backend = <App extends BaseApp>(params: BackendParams<App>): Backend<App> => { | |||
return new BackendInstance(params); | |||
}; |
@@ -1,21 +1,2 @@ | |||
import { App as BaseApp } from '../common/app'; | |||
interface BackendParams<App extends BaseApp> { | |||
app: App; | |||
} | |||
export interface Backend<App extends BaseApp = BaseApp> { | |||
app: App; | |||
} | |||
class BackendInstance<App extends BaseApp> implements Backend<App> { | |||
readonly app: App; | |||
constructor(params: BackendParams<App>) { | |||
this.app = params.app; | |||
} | |||
} | |||
export const backend = <App extends BaseApp>(params: BackendParams<App>): Backend<App> => { | |||
return new BackendInstance(params); | |||
}; | |||
export * from './common'; | |||
export * from './server'; |
@@ -1,34 +1,16 @@ | |||
import { Backend as BaseBackend } from './index'; | |||
import http from 'http'; | |||
import {Backend as BaseBackend} from './common'; | |||
import {ServiceParams} from '../common'; | |||
interface ServerParams<Backend extends BaseBackend = BaseBackend> { | |||
export interface ServerRequest {} | |||
export interface ServerResponse {} | |||
export interface ServerParams<Backend extends BaseBackend = BaseBackend> { | |||
backend: Backend; | |||
} | |||
export interface Server<Backend extends BaseBackend = BaseBackend> { | |||
backend: Backend; | |||
host(params: ServiceParams): this; | |||
serve(params: ServiceParams): Promise<void>; | |||
close(): Promise<void>; | |||
} | |||
class ServerInstance<Backend extends BaseBackend> implements Server<Backend> { | |||
readonly backend: Backend; | |||
private readonly serverInternal; | |||
constructor(params: ServerParams<Backend>) { | |||
this.backend = params.backend; | |||
this.serverInternal = new http.Server(this.requestListener); | |||
} | |||
private readonly requestListener = (req, res) => { | |||
}; | |||
host(params: ServiceParams) { | |||
this.serverInternal.listen(params.port, params.host); | |||
return this; | |||
} | |||
} | |||
export const server = <Backend extends BaseBackend>(params: ServerParams<Backend>): Server<Backend> => { | |||
return new ServerInstance(params); | |||
}; |
@@ -1,28 +1,13 @@ | |||
import { App as BaseApp } from '../common/app'; | |||
import {ServiceParams, App as BaseApp, Endpoint, GetEndpointParams, Operation} from '../common'; | |||
interface ClientParams<App extends BaseApp> { | |||
export interface ClientParams<App extends BaseApp> { | |||
app: App; | |||
fetch?: typeof fetch; | |||
} | |||
interface Client<App extends BaseApp> { | |||
export interface Client<App extends BaseApp> { | |||
app: App; | |||
connect(params: ServiceParams): this; | |||
at<TheEndpoint extends Endpoint = Endpoint>(endpoint: TheEndpoint, params?: Record<GetEndpointParams<TheEndpoint>, unknown>): this; | |||
makeRequest(operation: Operation): ReturnType<typeof fetch>; | |||
} | |||
class ClientInstance<App extends BaseApp> implements Client<App> { | |||
readonly app: App; | |||
private connection: ServiceParams; | |||
constructor(params: ClientParams<App>) { | |||
this.app = params.app; | |||
} | |||
connect(params: ServiceParams) { | |||
this.connection = params; | |||
return this; | |||
} | |||
} | |||
export const client = <App extends BaseApp>(params: ClientParams<App>): Client<App> => { | |||
return new ClientInstance(params); | |||
}; |
@@ -1,104 +1,43 @@ | |||
import * as v from 'valibot'; | |||
interface BaseEndpointState { | |||
operations: unknown; | |||
} | |||
type OpValueType = undefined | boolean; | |||
export interface Endpoint<Schema extends {} = {}, State extends BaseEndpointState = BaseEndpointState> { | |||
schema: Schema; | |||
can<OpName extends string = string, OpValue extends OpValueType = OpValueType>( | |||
op: OpName, | |||
value?: OpValue | |||
): Endpoint< | |||
Schema, | |||
{ | |||
operations: State['operations'] extends string[] ? readonly [...State['operations'], OpName] : [OpName], | |||
} | |||
>; | |||
} | |||
interface EndpointParams<Schema extends v.BaseSchema = v.BaseSchema> { | |||
schema: Schema; | |||
} | |||
class EndpointInstance< | |||
Params extends EndpointParams, | |||
State extends BaseEndpointState | |||
> implements Endpoint<Params['schema'], State> { | |||
readonly operations: Set<string>; | |||
readonly schema: Params['schema']; | |||
constructor(params: Params) { | |||
this.schema = params.schema; | |||
this.operations = new Set<string>(); | |||
} | |||
can<OpName extends string = string, OpValue extends OpValueType = OpValueType>( | |||
op: OpName, | |||
value?: OpValue | |||
): Endpoint< | |||
Params['schema'], | |||
{ | |||
operations: State['operations'] extends string[] ? readonly [...State['operations'], OpName] : [OpName], | |||
} | |||
> { | |||
if (value) { | |||
this.operations.add(op); | |||
} else { | |||
this.operations.delete(op); | |||
} | |||
return this; | |||
} | |||
} | |||
export const endpoint = <Params extends EndpointParams>(params: Params): Endpoint<v.Output<Params['schema']>> => { | |||
return new EndpointInstance(params); | |||
}; | |||
import {Endpoint, EndpointOperations} from './endpoint'; | |||
import {BaseOperationParams, Operation} from './operation'; | |||
export interface BaseAppState { | |||
endpoints: unknown; | |||
operations: unknown; | |||
} | |||
type EndpointOperations<T extends Endpoint> = T extends Endpoint<any, infer R> ? ( | |||
R extends { operations: Record<number, any> } ? R['operations'][number] : [] | |||
) : []; | |||
type AppOperations<T extends App> = ( | |||
export type AppOperations<T extends App> = ( | |||
T extends App<any, infer R> | |||
? R extends BaseAppState | |||
? keyof R['operations'] | |||
: never | |||
: never | |||
: string | |||
: string | |||
); | |||
export interface App<Name extends string = string, State extends BaseAppState = { | |||
export interface App<AppName extends string = string, AppState extends BaseAppState = { | |||
endpoints: []; | |||
operations: Record<never, []>; | |||
operations: Record<string, []>; | |||
}> { | |||
name: Name; | |||
name: AppName; | |||
operations: Set<Operation>; | |||
endpoints: Set<Endpoint>; | |||
operation< | |||
OperationName extends string, | |||
OperationParams extends BaseOperationParams<OperationName>, | |||
NewOperation extends Operation<OperationParams> | |||
>(newOperation: NewOperation): App< | |||
Name, | |||
AppName, | |||
{ | |||
endpoints: State['endpoints'], | |||
operations: keyof State['operations'] extends never ? { | |||
endpoints: AppState['endpoints'], | |||
operations: keyof AppState['operations'] extends never ? { | |||
[Key in NewOperation['name']]: ( | |||
Exclude<NewOperation['args'], undefined> extends readonly string[] ? Exclude<NewOperation['args'], undefined> : never[] | |||
) | |||
} : { | |||
[Key in NewOperation['name'] | keyof State['operations']]: ( | |||
State['operations'] extends Record<Key, any> | |||
[Key in NewOperation['name'] | keyof AppState['operations']]: ( | |||
AppState['operations'] extends Record<Key, any> | |||
? ( | |||
State['operations'][Key] extends readonly string[] ? State['operations'][Key] : ( | |||
AppState['operations'][Key] extends readonly string[] ? AppState['operations'][Key] : ( | |||
Exclude<NewOperation['args'], undefined> extends readonly string[] ? Exclude<NewOperation['args'], undefined> : never[] | |||
) | |||
) | |||
@@ -110,40 +49,14 @@ export interface App<Name extends string = string, State extends BaseAppState = | |||
} | |||
>; | |||
endpoint<NewEndpoint extends Endpoint = Endpoint>(newEndpoint: EndpointOperations<NewEndpoint> extends AppOperations<this> ? NewEndpoint : never): App< | |||
Name, | |||
AppName, | |||
{ | |||
endpoints: State['endpoints'] extends Array<unknown> ? [...State['endpoints'], NewEndpoint] : [NewEndpoint], | |||
operations: State['operations'] | |||
endpoints: AppState['endpoints'] extends Array<unknown> ? [...AppState['endpoints'], NewEndpoint] : [NewEndpoint], | |||
operations: AppState['operations'] | |||
} | |||
>; | |||
} | |||
interface BaseOperationParams<Name extends string = string, Args extends readonly string[] = readonly string[]> { | |||
name: Name; | |||
args?: Args; | |||
} | |||
interface Operation<Params extends BaseOperationParams = BaseOperationParams> { | |||
name: Params['name']; | |||
args: Params['args']; | |||
} | |||
class OperationInstance<Params extends BaseOperationParams = BaseOperationParams> implements Operation<Params> { | |||
readonly name: Params['name']; | |||
readonly args: Params['args']; | |||
constructor(params: Params) { | |||
this.name = params.name; | |||
this.args = params.args; | |||
} | |||
} | |||
export const operation = <Params extends BaseOperationParams = BaseOperationParams>( | |||
params: Params | |||
): Operation<Params> => { | |||
return new OperationInstance(params); | |||
}; | |||
interface AppParams<Name extends string = string> { | |||
name: Name; | |||
} | |||
@@ -159,59 +72,15 @@ class AppInstance<Params extends AppParams, State extends BaseAppState> implemen | |||
this.operations = new Set<Operation>(); | |||
} | |||
operation<NewOperation extends Operation>(newOperation: NewOperation): App< | |||
Params['name'], | |||
{ | |||
endpoints: State['endpoints'], | |||
operations: keyof State['operations'] extends never ? { | |||
[Key in NewOperation['name']]: ( | |||
Exclude<NewOperation['args'], undefined> extends readonly string[] ? Exclude<NewOperation['args'], undefined> : never[] | |||
) | |||
} : { | |||
[Key in NewOperation['name'] | keyof State['operations']]: ( | |||
State['operations'] extends Record<Key, any> | |||
? ( | |||
State['operations'][Key] extends readonly string[] ? State['operations'][Key] : ( | |||
Exclude<NewOperation['args'], undefined> extends readonly string[] ? Exclude<NewOperation['args'], undefined> : never[] | |||
) | |||
) | |||
: ( | |||
Exclude<NewOperation['args'], undefined> extends readonly string[] ? Exclude<NewOperation['args'], undefined> : never[] | |||
) | |||
); | |||
} | |||
} | |||
> { | |||
operation<NewOperation extends Operation>(newOperation: NewOperation) { | |||
this.operations.add(newOperation); | |||
return this as App< | |||
Params['name'], | |||
{ | |||
endpoints: State['endpoints'], | |||
operations: { | |||
[Key in NewOperation['name'] | keyof State['operations']]: ( | |||
State['operations'] extends Record<Key, any> | |||
? ( | |||
State['operations'][Key] extends readonly string[] ? State['operations'][Key] : ( | |||
Exclude<NewOperation['args'], undefined> extends readonly string[] ? Exclude<NewOperation['args'], undefined> : never[] | |||
) | |||
) | |||
: ( | |||
Exclude<NewOperation['args'], undefined> extends readonly string[] ? Exclude<NewOperation['args'], undefined> : never[] | |||
) | |||
); | |||
} | |||
} | |||
>; | |||
return this; | |||
} | |||
endpoint<NewEndpoint extends Endpoint = Endpoint>(newEndpoint: NewEndpoint): App< | |||
Params['name'], | |||
{ | |||
endpoints: State['endpoints'] extends Array<unknown> ? [...State['endpoints'], NewEndpoint] : [NewEndpoint], | |||
operations: State['operations'] | |||
} | |||
> { | |||
endpoint<NewEndpoint extends Endpoint = Endpoint>(newEndpoint: NewEndpoint) { | |||
this.endpoints.add(newEndpoint); | |||
return this; | |||
} | |||
} | |||
@@ -219,5 +88,3 @@ class AppInstance<Params extends AppParams, State extends BaseAppState> implemen | |||
export const app = <Params extends AppParams>(params: Params): App<Params['name']> => { | |||
return new AppInstance(params); | |||
}; | |||
export * as validation from 'valibot'; |
@@ -0,0 +1,2 @@ | |||
export interface DataSource {} |
@@ -0,0 +1,170 @@ | |||
import * as v from 'valibot'; | |||
import {DataSource} from './data-source'; | |||
export type EndpointQueue = [Endpoint, Record<string, unknown> | undefined][]; | |||
export const serializeEndpointQueue = (endpointQueue: EndpointQueue) => { | |||
return endpointQueue | |||
.map(([endpoint, param]) => { | |||
if (typeof param === 'undefined') { | |||
return `/${endpoint.name}`; | |||
} | |||
return [ | |||
endpoint.name, | |||
...Array.from(endpoint.params).map((s) => param[s] ?? '_') | |||
] | |||
.map((s) => `/${s}`) | |||
.join(''); | |||
}) | |||
}; | |||
export const parseToEndpointQueue = (urlWithoutBase: string, endpoints: Set<Endpoint>) => { | |||
const fragments = urlWithoutBase.split('/').filter((s) => s.trim().length > 0); | |||
const endpointsArray = Array.from(endpoints); | |||
return fragments.reduce( | |||
(theEndpointQueueRaw, s) => { | |||
const theEndpointQueue = theEndpointQueueRaw as EndpointQueue; | |||
const [lastEndpoint, lastEndpointParams] = theEndpointQueue.at(-1) ?? []; | |||
const endpoint = endpointsArray.find((e) => e.name === s); | |||
if (typeof endpoint !== 'undefined') { | |||
if (typeof lastEndpoint === 'undefined') { | |||
return [ | |||
...theEndpointQueue, | |||
[endpoint, {}] | |||
]; | |||
} | |||
} | |||
if (typeof lastEndpoint === 'undefined') { | |||
throw new Error(`Invalid URL: ${urlWithoutBase}`); | |||
} | |||
const lastEndpointParamsOrdering = Array.from(lastEndpoint.params); | |||
const lastEndpointParamsOrderingLength = lastEndpointParamsOrdering.length; | |||
if (lastEndpointParamsOrderingLength > 0) { | |||
if (typeof lastEndpointParams === 'undefined') { | |||
return [ | |||
...theEndpointQueue.slice(0, -1), | |||
[lastEndpoint, { | |||
[lastEndpointParamsOrdering[0]]: s | |||
}] | |||
]; | |||
} | |||
const nextIndex = Object.keys(lastEndpointParams).length; | |||
if (nextIndex === lastEndpointParamsOrderingLength) { | |||
throw new Error(`Invalid URL: ${urlWithoutBase}`); | |||
} | |||
return [ | |||
...theEndpointQueue.slice(0, -1), | |||
[lastEndpoint, { | |||
...lastEndpointParams, | |||
[lastEndpointParamsOrdering[nextIndex]]: s | |||
}] | |||
]; | |||
} | |||
throw new Error(`Invalid URL: ${urlWithoutBase}`); | |||
}, | |||
[] as unknown | |||
) as EndpointQueue; | |||
}; | |||
interface BaseEndpointState { | |||
operations: unknown; | |||
params: unknown; | |||
} | |||
type OpValueType = undefined | boolean | Record<string, boolean> | readonly string[]; | |||
export interface Endpoint<Name extends string = string, Schema extends v.BaseSchema = v.BaseSchema, State extends BaseEndpointState = BaseEndpointState> { | |||
name: Name; | |||
schema: Schema; | |||
params: Set<string>; | |||
operations: Set<string>; | |||
can<OpName extends string = string, OpValue extends OpValueType = OpValueType>( | |||
op: OpName, | |||
value?: OpValue | |||
): Endpoint< | |||
Name, | |||
Schema, | |||
{ | |||
operations: State['operations'] extends string[] ? readonly [...State['operations'], OpName] : [OpName]; | |||
params: State['params'] | |||
} | |||
>; | |||
param<ParamName extends string = string>(name: ParamName): Endpoint< | |||
Name, | |||
Schema, | |||
{ | |||
operations: State['operations']; | |||
params: State['params'] extends string[] ? readonly [...State['params'], ParamName]: [ParamName]; | |||
} | |||
> | |||
} | |||
export type GetEndpointParams<T extends Endpoint> = T extends Endpoint<any, infer R> ? ( | |||
R extends { params: Record<number, any> } ? R['params'][number] : never | |||
) : never; | |||
interface EndpointParams<Name extends string = string, Schema extends v.BaseSchema = v.BaseSchema> { | |||
name: Name; | |||
schema: Schema; | |||
dataSource?: DataSource; | |||
} | |||
class EndpointInstance< | |||
Params extends EndpointParams, | |||
State extends BaseEndpointState | |||
> implements Endpoint<Params['name'], Params['schema'], State> { | |||
readonly name: Params['name']; | |||
readonly operations: Set<string>; | |||
readonly params: Set<string>; | |||
readonly schema: Params['schema']; | |||
constructor(params: Params) { | |||
this.name = params.name; | |||
this.schema = params.schema; | |||
this.operations = new Set<string>(); | |||
this.params = new Set<string>(); | |||
} | |||
can<OpName extends string = string, OpValue extends OpValueType = OpValueType>( | |||
op: OpName, | |||
value = true as OpValue | |||
) { | |||
if (value) { | |||
this.operations.add(op); | |||
} else { | |||
// todo remove operation at type level | |||
this.operations.delete(op); | |||
} | |||
return this; | |||
} | |||
param<ParamName extends string = string>( | |||
name: ParamName | |||
) { | |||
this.params.add(name); | |||
return this; | |||
} | |||
} | |||
export const endpoint = <Params extends EndpointParams>(params: Params): Endpoint< | |||
Params['name'], | |||
Params['schema'], | |||
{ | |||
operations: []; | |||
params: []; | |||
} | |||
> => { | |||
return new EndpointInstance(params); | |||
}; | |||
export type EndpointOperations<T extends Endpoint> = T extends Endpoint<any, infer R> ? ( | |||
R extends { operations: Record<number, any> } ? R['operations'][number] : never | |||
) : never; |
@@ -1,5 +1,6 @@ | |||
interface ServiceParams { | |||
host?: string; | |||
port?: number; | |||
basePath?: string; | |||
} | |||
export * from './app'; | |||
export * from './data-source'; | |||
export * from './endpoint'; | |||
export * from './operation'; | |||
export * from './service'; | |||
export * as validation from 'valibot'; |
@@ -0,0 +1,51 @@ | |||
export const AVAILABLE_METHODS = [ | |||
'HEAD', | |||
'GET', | |||
'POST', | |||
'PUT', | |||
'PATCH', | |||
'DELETE', | |||
'OPTIONS' | |||
] as const; | |||
export const AVAILABLE_EXTENSION_METHODS = [ | |||
'QUERY' | |||
] as const; | |||
export type Method = typeof AVAILABLE_METHODS[number]; | |||
export type MethodWithExtensions = Method | typeof AVAILABLE_EXTENSION_METHODS[number]; | |||
export interface BaseOperationParams< | |||
Name extends string = string, | |||
Method extends MethodWithExtensions = MethodWithExtensions, | |||
Args extends readonly string[] = readonly string[] | |||
> { | |||
name: Name; | |||
method?: Method; | |||
args?: Args; | |||
} | |||
export interface Operation<Params extends BaseOperationParams = BaseOperationParams> { | |||
name: Params['name']; | |||
method: Params['method']; | |||
args: Params['args']; | |||
} | |||
class OperationInstance<Params extends BaseOperationParams = BaseOperationParams> implements Operation<Params> { | |||
readonly name: Params['name']; | |||
readonly method: Params['method']; | |||
readonly args: Params['args']; | |||
constructor(params: Params) { | |||
this.name = params.name; | |||
this.method = params.method ?? 'GET'; | |||
this.args = params.args; | |||
} | |||
} | |||
export const operation = <Params extends BaseOperationParams = BaseOperationParams>( | |||
params: Params | |||
): Operation<Params> => { | |||
return new OperationInstance(params); | |||
}; |
@@ -0,0 +1,5 @@ | |||
export interface ServiceParams { | |||
host?: string; | |||
port?: number; | |||
basePath?: string; | |||
} |
@@ -0,0 +1,114 @@ | |||
import {parseToEndpointQueue, ServiceParams} from '../../common'; | |||
import { | |||
Backend as BaseBackend, | |||
Server, | |||
ServerRequest, | |||
ServerResponse, | |||
ServerParams, | |||
} from '../../backend'; | |||
import http from 'http'; | |||
import { constants } from 'http2'; | |||
declare module '../../backend/server' { | |||
interface ServerRequest extends http.IncomingMessage {} | |||
interface ServerResponse extends http.ServerResponse {} | |||
} | |||
class ServerInstance<Backend extends BaseBackend> implements Server<Backend> { | |||
readonly backend: Backend; | |||
private serverInternal?: http.Server; | |||
constructor(params: ServerParams<Backend>) { | |||
this.backend = params.backend; | |||
} | |||
private readonly requestListener = (req: ServerRequest, res: ServerResponse) => { | |||
// const endpoints = this.backend.app.endpoints; | |||
if (typeof req.method === 'undefined') { | |||
res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, {}); | |||
res.end(); | |||
return; | |||
} | |||
if (typeof req.url === 'undefined') { | |||
res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, {}); | |||
res.end(); | |||
return; | |||
} | |||
const endpoints = parseToEndpointQueue(req.url, this.backend.app.endpoints); | |||
const [endpoint, endpointParams] = endpoints.at(-1) ?? []; | |||
const appOperations = Array.from(this.backend.app.operations) | |||
const foundAppOperation = appOperations | |||
.find((op) => op.method === req.method?.toUpperCase()); | |||
if (typeof foundAppOperation === 'undefined') { | |||
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, { | |||
'Allow': appOperations.map((op) => op.method).join(',') | |||
}); | |||
res.end(); | |||
return; | |||
} | |||
const endpointOperations = Array.from(endpoint?.operations ?? []); | |||
if (!endpointOperations.includes(foundAppOperation.name)) { | |||
res.writeHead(constants.HTTP_STATUS_METHOD_NOT_ALLOWED, { | |||
'Allow': endpointOperations | |||
.map((a) => appOperations.find((aa) => aa.name === a)?.method) | |||
.join(',') | |||
}); | |||
res.end(); | |||
return; | |||
} | |||
const implementation = this.backend.implementations.get(foundAppOperation.name); | |||
if (typeof implementation === 'undefined') { | |||
res.writeHead(constants.HTTP_STATUS_NOT_IMPLEMENTED); | |||
res.end(); | |||
return; | |||
} | |||
if (typeof endpoint === 'undefined') { | |||
res.writeHead(constants.HTTP_STATUS_NOT_IMPLEMENTED); | |||
res.end(); | |||
return; | |||
} | |||
implementation({ | |||
endpoint, | |||
params: endpointParams | |||
}); | |||
res.writeHead(constants.HTTP_STATUS_OK, {}); | |||
res.statusMessage = 'Yes'; | |||
res.end(); | |||
}; | |||
serve(params: ServiceParams) { | |||
return new Promise<void>((resolve) => { | |||
this.serverInternal = new http.Server(this.requestListener); | |||
this.serverInternal.listen(params.port ?? 80, params.host ?? '0.0.0.0', undefined, resolve); | |||
}); | |||
} | |||
close() { | |||
return new Promise<void>((resolve, reject) => { | |||
if (typeof this.serverInternal === 'undefined') { | |||
resolve(); | |||
return; | |||
} | |||
this.serverInternal.close((err) => { | |||
if (err) { | |||
reject(err); | |||
return; | |||
} | |||
resolve(); | |||
}); | |||
}); | |||
} | |||
} | |||
export const server = <Backend extends BaseBackend>(params: ServerParams<Backend>): Server<Backend> => { | |||
return new ServerInstance(params); | |||
}; |
@@ -0,0 +1,67 @@ | |||
import { | |||
App as BaseApp, | |||
AVAILABLE_EXTENSION_METHODS, | |||
Endpoint, EndpointQueue, | |||
GetEndpointParams, | |||
Operation, serializeEndpointQueue, | |||
ServiceParams, | |||
} from '../../common'; | |||
import {Client, ClientParams} from '../../client'; | |||
class ClientInstance<App extends BaseApp> implements Client<App> { | |||
readonly app: App; | |||
private readonly fetchFn: typeof fetch; | |||
private connection?: ServiceParams; | |||
private endpointQueue = [] as EndpointQueue; | |||
constructor(params: ClientParams<App>) { | |||
this.app = params.app; | |||
this.fetchFn = params.fetch ?? fetch; | |||
} | |||
connect(params: ServiceParams) { | |||
this.connection = params; | |||
return this; | |||
} | |||
at<TheEndpoint extends Endpoint = Endpoint>(endpoint: TheEndpoint, params?: Record<GetEndpointParams<TheEndpoint>, unknown>) { | |||
if (Array.isArray(this.endpointQueue)) { | |||
this.endpointQueue?.push([endpoint, params]); | |||
} | |||
return this; | |||
} | |||
makeRequest(operation: Operation) { | |||
const baseUrlFragments = [ | |||
this.connection?.host ?? '0.0.0.0' | |||
]; | |||
const thePort = (this.connection?.port ?? 80); | |||
if (thePort !== 80) { | |||
baseUrlFragments.push(thePort.toString()); | |||
} | |||
const scheme = 'http'; | |||
// todo need a way to decode url back to endpoint queue | |||
const url = serializeEndpointQueue(this.endpointQueue); | |||
this.endpointQueue = []; | |||
const rawEffectiveMethod = (operation.method ?? 'GET').toUpperCase(); | |||
const finalEffectiveMethod = (AVAILABLE_EXTENSION_METHODS as unknown as string[]).includes(rawEffectiveMethod) | |||
? 'POST' as const | |||
: rawEffectiveMethod; | |||
return this.fetchFn( | |||
new URL( | |||
this.connection?.basePath ? `${this.connection.basePath}${url}` : url, | |||
`${scheme}://${baseUrlFragments.join(':')}` | |||
), | |||
{ | |||
method: finalEffectiveMethod, | |||
}, | |||
); | |||
} | |||
} | |||
export const client = <App extends BaseApp>(params: ClientParams<App>): Client<App> => { | |||
return new ClientInstance(params); | |||
}; |
@@ -1,62 +0,0 @@ | |||
import {app, endpoint, operation, validation as v} from './common/app'; | |||
const theEndpoint = endpoint({ | |||
schema: v.object({ | |||
username: v.string(), | |||
}), | |||
}) | |||
.can('patch') | |||
.can('query'); | |||
const canPatch = operation({ | |||
name: 'patch' as const, | |||
args: [ | |||
'merge', | |||
'delta', | |||
] as const, | |||
// TODO define resource-specific stuff, like defining URL params, etc. | |||
}); | |||
const canFetch = operation({ | |||
name: 'fetch' as const, | |||
args: [ | |||
'item', | |||
'collection', | |||
] as const, | |||
}); | |||
const canQuery = operation({ | |||
name: 'query' as const, | |||
}); | |||
const canCreate = operation({ | |||
name: 'create' as const, | |||
}); | |||
const canEmplace = operation({ | |||
name: 'emplace' as const, | |||
}); | |||
const canDelete = operation({ | |||
name: 'delete' as const, | |||
}); | |||
export const theApp = app({ | |||
name: 'foo' as const, | |||
}) | |||
.operation(canQuery) | |||
.operation(canPatch) | |||
.operation(canFetch) | |||
.operation(canCreate) | |||
.operation(canEmplace) | |||
.operation(canDelete) | |||
.endpoint(theEndpoint); | |||
// | |||
// const bootstrap = async (theApp: App) => { | |||
// if (typeof window === 'undefined') { | |||
// const { backend } = await import('./backend'); | |||
// const theBackend = backend({ | |||
// app: theApp | |||
// }); | |||
// } | |||
// }; |
@@ -1,8 +1,158 @@ | |||
import { describe, it, expect } from 'vitest'; | |||
import add from '../src'; | |||
import {describe, it, expect, beforeAll} from 'vitest'; | |||
import {App, app, Endpoint, endpoint, Operation, operation, validation as v} from '../src/common'; | |||
import {server} from '../src/extenders/http/backend'; | |||
import {Backend, backend, Server} from '../src/backend'; | |||
import {Client} from '../src/client'; | |||
import {client} from '../src/extenders/http/client'; | |||
describe('blah', () => { | |||
it('works', () => { | |||
expect(add(1, 1)).toEqual(2); | |||
describe('app', () => { | |||
let theApp: App; | |||
let theBackend: Backend; | |||
let theEndpoint: Endpoint; | |||
let theServer: Server; | |||
let theClient: Client<App>; | |||
let theOperation: Operation; | |||
beforeAll(async () => { | |||
theOperation = operation({ | |||
name: 'fetch' as const, | |||
}); | |||
theEndpoint = endpoint({ | |||
name: 'users' as const, | |||
schema: v.object({ | |||
username: v.string() | |||
}), | |||
}) | |||
.param('resourceId') | |||
.can('fetch'); | |||
theApp = app({ | |||
name: 'foo' as const | |||
}) | |||
.operation(theOperation) | |||
.endpoint(theEndpoint); | |||
theBackend = backend({ | |||
app: theApp | |||
}); | |||
// add recipes function that will wrap app and backend to add operations and implement them, and will return a set | |||
// of operations. | |||
// | |||
// recipes should have a backend and client counterpart. | |||
theBackend.implementOperation('fetch', (params) => { | |||
// noop | |||
}); | |||
theServer = server({ | |||
backend: theBackend | |||
}); | |||
const connectionParams = { | |||
port: 3000, | |||
}; | |||
await theServer.serve(connectionParams); | |||
theClient = client({ | |||
app: theApp | |||
}) | |||
.connect(connectionParams); | |||
}); | |||
it('works', async () => { | |||
const response = await theClient | |||
.at(theEndpoint, { resourceId: 3 }) | |||
.makeRequest(theOperation); | |||
expect(response).toHaveProperty('status', 200); | |||
}); | |||
}); | |||
// const theEndpoint = endpoint({ | |||
// schema: v.object({ | |||
// username: v.string(), | |||
// }), | |||
// }) | |||
// .can('patch') | |||
// .can('query'); | |||
// | |||
// const canPatch = operation({ | |||
// name: 'patch' as const, | |||
// args: [ | |||
// 'merge', | |||
// 'delta', | |||
// ] as const, | |||
// // TODO define resource-specific stuff, like defining URL params, etc. | |||
// }); | |||
// | |||
// const canFetch = operation({ | |||
// name: 'fetch' as const, | |||
// args: [ | |||
// 'item', | |||
// 'default', | |||
// ] as const, | |||
// }); | |||
// | |||
// const canQuery = operation({ | |||
// name: 'query' as const, | |||
// }); | |||
// | |||
// const canCreate = operation({ | |||
// name: 'create' as const, | |||
// }); | |||
// | |||
// const canEmplace = operation({ | |||
// name: 'emplace' as const, | |||
// }); | |||
// | |||
// const canDelete = operation({ | |||
// name: 'delete' as const, | |||
// }); | |||
// | |||
// export const theApp = app({ | |||
// name: 'foo' as const, | |||
// }) | |||
// .operation(canQuery) | |||
// .operation(canPatch) | |||
// .operation(canFetch) | |||
// .operation(canCreate) | |||
// .operation(canEmplace) | |||
// .operation(canDelete) | |||
// .endpoint(theEndpoint); | |||
// // | |||
// // const bootstrap = async (theApp: App) => { | |||
// // if (typeof window === 'undefined') { | |||
// // const { backend } = await import('./backend'); | |||
// // const theBackend = backend({ | |||
// // app: theApp | |||
// // }); | |||
// // } | |||
// // }; | |||
// | |||
// const b = backend({ | |||
// app: theApp, | |||
// }) | |||
// .implementOperation({ | |||
// operation: 'fetch' as const, | |||
// implementation: ({ | |||
// endpoint, | |||
// arg | |||
// }) => { | |||
// switch (arg) { | |||
// case 'default': { | |||
// | |||
// } | |||
// } | |||
// }, | |||
// }); | |||
// | |||
// const s = server({ | |||
// backend: b, | |||
// }) | |||
// .serve({ | |||
// host: '0.0.0.0', | |||
// port: 3000, | |||
// basePath: '/api' | |||
// }); |
@@ -0,0 +1,3 @@ | |||
{"id":"9ba60691-0cd3-4e8a-9f44-e92b19fcacbc","title":"Modified Post","content":"I changed the content via merge.","createdAt":1713320757029,"updatedAt":1713352134951,"user":"I changed the content via merge."} | |||
{"id":"1c23a980-eaab-4993-a0d3-58c06226062d","title":"New Post","content":"Hello there","createdAt":1713333787728,"updatedAt":1713333787728} | |||
{"id":"f6ed1dc3-f99f-4f8d-8a14-dbaa2949f795","title":"New Post","content":"Hello there","createdAt":1713352248696,"updatedAt":1713352248696} |