Browse Source

Implement new API

New API is much more flexible in implementation.
refactor/new-arch
TheoryOfNekomata 7 months ago
parent
commit
f5b6700301
17 changed files with 666 additions and 298 deletions
  1. +52
    -0
      packages/core/src/backend/common.ts
  2. +2
    -21
      packages/core/src/backend/index.ts
  3. +9
    -27
      packages/core/src/backend/server.ts
  4. +6
    -21
      packages/core/src/client/index.ts
  5. +24
    -157
      packages/core/src/common/app.ts
  6. +2
    -0
      packages/core/src/common/data-source.ts
  7. +170
    -0
      packages/core/src/common/endpoint.ts
  8. +6
    -5
      packages/core/src/common/index.ts
  9. +51
    -0
      packages/core/src/common/operation.ts
  10. +5
    -0
      packages/core/src/common/service.ts
  11. +114
    -0
      packages/core/src/extenders/http/backend.ts
  12. +67
    -0
      packages/core/src/extenders/http/client.ts
  13. +0
    -62
      packages/core/src/index.ts
  14. +155
    -5
      packages/core/test/index.test.ts
  15. +3
    -0
      packages/examples/cms-web-api/posts.jsonl
  16. BIN
      packages/examples/duckdb/test.db
  17. BIN
      packages/examples/duckdb/test.db.wal

+ 52
- 0
packages/core/src/backend/common.ts View File

@@ -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);
};

+ 2
- 21
packages/core/src/backend/index.ts View File

@@ -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';

+ 9
- 27
packages/core/src/backend/server.ts View File

@@ -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);
};

+ 6
- 21
packages/core/src/client/index.ts View File

@@ -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);
};

+ 24
- 157
packages/core/src/common/app.ts View File

@@ -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';

+ 2
- 0
packages/core/src/common/data-source.ts View File

@@ -0,0 +1,2 @@

export interface DataSource {}

+ 170
- 0
packages/core/src/common/endpoint.ts View File

@@ -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;

+ 6
- 5
packages/core/src/common/index.ts View File

@@ -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';

+ 51
- 0
packages/core/src/common/operation.ts View File

@@ -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);
};

+ 5
- 0
packages/core/src/common/service.ts View File

@@ -0,0 +1,5 @@
export interface ServiceParams {
host?: string;
port?: number;
basePath?: string;
}

+ 114
- 0
packages/core/src/extenders/http/backend.ts View File

@@ -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);
};

+ 67
- 0
packages/core/src/extenders/http/client.ts View File

@@ -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);
};

+ 0
- 62
packages/core/src/index.ts View File

@@ -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
// });
// }
// };

+ 155
- 5
packages/core/test/index.test.ts View File

@@ -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'
// });

+ 3
- 0
packages/examples/cms-web-api/posts.jsonl View File

@@ -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}

BIN
packages/examples/duckdb/test.db View File


BIN
packages/examples/duckdb/test.db.wal View File


Loading…
Cancel
Save