@@ -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; | backend: Backend; | ||||
} | } | ||||
export interface Server<Backend extends BaseBackend = BaseBackend> { | export interface Server<Backend extends BaseBackend = BaseBackend> { | ||||
backend: Backend; | 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; | app: App; | ||||
fetch?: typeof fetch; | |||||
} | } | ||||
interface Client<App extends BaseApp> { | |||||
export interface Client<App extends BaseApp> { | |||||
app: App; | app: App; | ||||
connect(params: ServiceParams): this; | 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 { | export interface BaseAppState { | ||||
endpoints: unknown; | endpoints: unknown; | ||||
operations: 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> | T extends App<any, infer R> | ||||
? R extends BaseAppState | ? R extends BaseAppState | ||||
? keyof R['operations'] | ? 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: []; | endpoints: []; | ||||
operations: Record<never, []>; | |||||
operations: Record<string, []>; | |||||
}> { | }> { | ||||
name: Name; | |||||
name: AppName; | |||||
operations: Set<Operation>; | |||||
endpoints: Set<Endpoint>; | |||||
operation< | operation< | ||||
OperationName extends string, | OperationName extends string, | ||||
OperationParams extends BaseOperationParams<OperationName>, | OperationParams extends BaseOperationParams<OperationName>, | ||||
NewOperation extends Operation<OperationParams> | NewOperation extends Operation<OperationParams> | ||||
>(newOperation: NewOperation): App< | >(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']]: ( | [Key in NewOperation['name']]: ( | ||||
Exclude<NewOperation['args'], undefined> extends readonly string[] ? Exclude<NewOperation['args'], undefined> : never[] | 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[] | 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< | 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> { | interface AppParams<Name extends string = string> { | ||||
name: Name; | name: Name; | ||||
} | } | ||||
@@ -159,59 +72,15 @@ class AppInstance<Params extends AppParams, State extends BaseAppState> implemen | |||||
this.operations = new Set<Operation>(); | 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); | 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); | this.endpoints.add(newEndpoint); | ||||
return this; | 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']> => { | export const app = <Params extends AppParams>(params: Params): App<Params['name']> => { | ||||
return new AppInstance(params); | 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} |