diff --git a/packages/core/src/backend/common.ts b/packages/core/src/backend/common.ts new file mode 100644 index 0000000..0236707 --- /dev/null +++ b/packages/core/src/backend/common.ts @@ -0,0 +1,52 @@ +import {App as BaseApp, AppOperations, Endpoint} from '../common'; + +interface BackendParams { + app: App; +} + +type AppOperationArgs = ( + App extends BaseApp + ? ( + S extends Record + ? S[Operation][number] + : never + ) + : never + ); + +interface ImplementationFunctionParams = AppOperations> { + endpoint: Endpoint; + params: unknown; + arg?: AppOperationArgs; + query?: URLSearchParams; +} + +type ImplementationFunction = AppOperations> = (params: ImplementationFunctionParams) => void; + +export interface Backend { + app: App; + implementations: Map>; + implementOperation>(operation: Operation, implementation: ImplementationFunction): this; +} + +class BackendInstance implements Backend { + readonly app: App; + readonly implementations: Map>; + + constructor(params: BackendParams) { + this.app = params.app; + this.implementations = new Map>(); + } + + implementOperation>( + operation: Operation, + implementation: ImplementationFunction + ) { + this.implementations.set(operation, implementation); + return this; + } +} + +export const backend = (params: BackendParams): Backend => { + return new BackendInstance(params); +}; diff --git a/packages/core/src/backend/index.ts b/packages/core/src/backend/index.ts index 4fc38f5..bbeac2e 100644 --- a/packages/core/src/backend/index.ts +++ b/packages/core/src/backend/index.ts @@ -1,21 +1,2 @@ -import { App as BaseApp } from '../common/app'; - -interface BackendParams { - app: App; -} - -export interface Backend { - app: App; -} - -class BackendInstance implements Backend { - readonly app: App; - - constructor(params: BackendParams) { - this.app = params.app; - } -} - -export const backend = (params: BackendParams): Backend => { - return new BackendInstance(params); -}; +export * from './common'; +export * from './server'; diff --git a/packages/core/src/backend/server.ts b/packages/core/src/backend/server.ts index 4cd8860..7d0fef0 100644 --- a/packages/core/src/backend/server.ts +++ b/packages/core/src/backend/server.ts @@ -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 { +export interface ServerRequest {} + +export interface ServerResponse {} + +export interface ServerParams { backend: Backend; } export interface Server { backend: Backend; - host(params: ServiceParams): this; + serve(params: ServiceParams): Promise; + close(): Promise; } - -class ServerInstance implements Server { - readonly backend: Backend; - private readonly serverInternal; - - constructor(params: ServerParams) { - 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 = (params: ServerParams): Server => { - return new ServerInstance(params); -}; diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts index 54270da..c6e9557 100644 --- a/packages/core/src/client/index.ts +++ b/packages/core/src/client/index.ts @@ -1,28 +1,13 @@ -import { App as BaseApp } from '../common/app'; +import {ServiceParams, App as BaseApp, Endpoint, GetEndpointParams, Operation} from '../common'; -interface ClientParams { +export interface ClientParams { app: App; + fetch?: typeof fetch; } -interface Client { +export interface Client { app: App; connect(params: ServiceParams): this; + at(endpoint: TheEndpoint, params?: Record, unknown>): this; + makeRequest(operation: Operation): ReturnType; } - -class ClientInstance implements Client { - readonly app: App; - private connection: ServiceParams; - - constructor(params: ClientParams) { - this.app = params.app; - } - - connect(params: ServiceParams) { - this.connection = params; - return this; - } -} - -export const client = (params: ClientParams): Client => { - return new ClientInstance(params); -}; diff --git a/packages/core/src/common/app.ts b/packages/core/src/common/app.ts index fe28fab..28b4bdf 100644 --- a/packages/core/src/common/app.ts +++ b/packages/core/src/common/app.ts @@ -1,104 +1,43 @@ -import * as v from 'valibot'; - -interface BaseEndpointState { - operations: unknown; -} - -type OpValueType = undefined | boolean; - -export interface Endpoint { - schema: Schema; - can( - op: OpName, - value?: OpValue - ): Endpoint< - Schema, - { - operations: State['operations'] extends string[] ? readonly [...State['operations'], OpName] : [OpName], - } - >; -} - -interface EndpointParams { - schema: Schema; -} - -class EndpointInstance< - Params extends EndpointParams, - State extends BaseEndpointState -> implements Endpoint { - readonly operations: Set; - readonly schema: Params['schema']; - - constructor(params: Params) { - this.schema = params.schema; - this.operations = new Set(); - } - - can( - 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: Params): Endpoint> => { - 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 ? ( - R extends { operations: Record } ? R['operations'][number] : [] - ) : []; - -type AppOperations = ( +export type AppOperations = ( T extends App ? R extends BaseAppState ? keyof R['operations'] - : never - : never + : string + : string ); - - -export interface App; + operations: Record; }> { - name: Name; + name: AppName; + operations: Set; + endpoints: Set; operation< OperationName extends string, OperationParams extends BaseOperationParams, NewOperation extends Operation >(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 extends readonly string[] ? Exclude : never[] ) } : { - [Key in NewOperation['name'] | keyof State['operations']]: ( - State['operations'] extends Record + [Key in NewOperation['name'] | keyof AppState['operations']]: ( + AppState['operations'] extends Record ? ( - State['operations'][Key] extends readonly string[] ? State['operations'][Key] : ( + AppState['operations'][Key] extends readonly string[] ? AppState['operations'][Key] : ( Exclude extends readonly string[] ? Exclude : never[] ) ) @@ -110,40 +49,14 @@ export interface App; endpoint(newEndpoint: EndpointOperations extends AppOperations ? NewEndpoint : never): App< - Name, + AppName, { - endpoints: State['endpoints'] extends Array ? [...State['endpoints'], NewEndpoint] : [NewEndpoint], - operations: State['operations'] + endpoints: AppState['endpoints'] extends Array ? [...AppState['endpoints'], NewEndpoint] : [NewEndpoint], + operations: AppState['operations'] } >; } -interface BaseOperationParams { - name: Name; - args?: Args; -} - -interface Operation { - name: Params['name']; - args: Params['args']; -} - -class OperationInstance implements Operation { - readonly name: Params['name']; - readonly args: Params['args']; - - constructor(params: Params) { - this.name = params.name; - this.args = params.args; - } -} - -export const operation = ( - params: Params -): Operation => { - return new OperationInstance(params); -}; - interface AppParams { name: Name; } @@ -159,59 +72,15 @@ class AppInstance implemen this.operations = new Set(); } - operation(newOperation: NewOperation): App< - Params['name'], - { - endpoints: State['endpoints'], - operations: keyof State['operations'] extends never ? { - [Key in NewOperation['name']]: ( - Exclude extends readonly string[] ? Exclude : never[] - ) - } : { - [Key in NewOperation['name'] | keyof State['operations']]: ( - State['operations'] extends Record - ? ( - State['operations'][Key] extends readonly string[] ? State['operations'][Key] : ( - Exclude extends readonly string[] ? Exclude : never[] - ) - ) - : ( - Exclude extends readonly string[] ? Exclude : never[] - ) - ); - } - } - > { + 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 - ? ( - State['operations'][Key] extends readonly string[] ? State['operations'][Key] : ( - Exclude extends readonly string[] ? Exclude : never[] - ) - ) - : ( - Exclude extends readonly string[] ? Exclude : never[] - ) - ); - } - } - >; + + return this; } - endpoint(newEndpoint: NewEndpoint): App< - Params['name'], - { - endpoints: State['endpoints'] extends Array ? [...State['endpoints'], NewEndpoint] : [NewEndpoint], - operations: State['operations'] - } - > { + endpoint(newEndpoint: NewEndpoint) { this.endpoints.add(newEndpoint); + return this; } } @@ -219,5 +88,3 @@ class AppInstance implemen export const app = (params: Params): App => { return new AppInstance(params); }; - -export * as validation from 'valibot'; diff --git a/packages/core/src/common/data-source.ts b/packages/core/src/common/data-source.ts new file mode 100644 index 0000000..3387d37 --- /dev/null +++ b/packages/core/src/common/data-source.ts @@ -0,0 +1,2 @@ + +export interface DataSource {} diff --git a/packages/core/src/common/endpoint.ts b/packages/core/src/common/endpoint.ts new file mode 100644 index 0000000..df9e3d2 --- /dev/null +++ b/packages/core/src/common/endpoint.ts @@ -0,0 +1,170 @@ +import * as v from 'valibot'; +import {DataSource} from './data-source'; + +export type EndpointQueue = [Endpoint, Record | 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) => { + 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 | readonly string[]; + +export interface Endpoint { + name: Name; + schema: Schema; + params: Set; + operations: Set; + can( + op: OpName, + value?: OpValue + ): Endpoint< + Name, + Schema, + { + operations: State['operations'] extends string[] ? readonly [...State['operations'], OpName] : [OpName]; + params: State['params'] + } + >; + param(name: ParamName): Endpoint< + Name, + Schema, + { + operations: State['operations']; + params: State['params'] extends string[] ? readonly [...State['params'], ParamName]: [ParamName]; + } + > +} + +export type GetEndpointParams = T extends Endpoint ? ( + R extends { params: Record } ? R['params'][number] : never +) : never; + +interface EndpointParams { + name: Name; + schema: Schema; + dataSource?: DataSource; +} + +class EndpointInstance< + Params extends EndpointParams, + State extends BaseEndpointState +> implements Endpoint { + readonly name: Params['name']; + readonly operations: Set; + readonly params: Set; + readonly schema: Params['schema']; + + constructor(params: Params) { + this.name = params.name; + this.schema = params.schema; + this.operations = new Set(); + this.params = new Set(); + } + + can( + 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( + name: ParamName + ) { + this.params.add(name); + + return this; + } +} + +export const endpoint = (params: Params): Endpoint< + Params['name'], + Params['schema'], + { + operations: []; + params: []; + } +> => { + return new EndpointInstance(params); +}; + + +export type EndpointOperations = T extends Endpoint ? ( + R extends { operations: Record } ? R['operations'][number] : never +) : never; diff --git a/packages/core/src/common/index.ts b/packages/core/src/common/index.ts index 6c4e647..e551ed8 100644 --- a/packages/core/src/common/index.ts +++ b/packages/core/src/common/index.ts @@ -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'; diff --git a/packages/core/src/common/operation.ts b/packages/core/src/common/operation.ts new file mode 100644 index 0000000..c2c4416 --- /dev/null +++ b/packages/core/src/common/operation.ts @@ -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 { + name: Params['name']; + method: Params['method']; + args: Params['args']; +} + +class OperationInstance implements Operation { + 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: Params +): Operation => { + return new OperationInstance(params); +}; diff --git a/packages/core/src/common/service.ts b/packages/core/src/common/service.ts new file mode 100644 index 0000000..4b48ad8 --- /dev/null +++ b/packages/core/src/common/service.ts @@ -0,0 +1,5 @@ +export interface ServiceParams { + host?: string; + port?: number; + basePath?: string; +} diff --git a/packages/core/src/extenders/http/backend.ts b/packages/core/src/extenders/http/backend.ts new file mode 100644 index 0000000..b68b90e --- /dev/null +++ b/packages/core/src/extenders/http/backend.ts @@ -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 implements Server { + readonly backend: Backend; + private serverInternal?: http.Server; + + constructor(params: ServerParams) { + 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((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((resolve, reject) => { + if (typeof this.serverInternal === 'undefined') { + resolve(); + return; + } + this.serverInternal.close((err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); + } +} + +export const server = (params: ServerParams): Server => { + return new ServerInstance(params); +}; diff --git a/packages/core/src/extenders/http/client.ts b/packages/core/src/extenders/http/client.ts new file mode 100644 index 0000000..9887d82 --- /dev/null +++ b/packages/core/src/extenders/http/client.ts @@ -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 implements Client { + readonly app: App; + private readonly fetchFn: typeof fetch; + private connection?: ServiceParams; + private endpointQueue = [] as EndpointQueue; + + constructor(params: ClientParams) { + this.app = params.app; + this.fetchFn = params.fetch ?? fetch; + } + + connect(params: ServiceParams) { + this.connection = params; + return this; + } + + at(endpoint: TheEndpoint, params?: Record, 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 = (params: ClientParams): Client => { + return new ClientInstance(params); +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a9b27a6..e69de29 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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 -// }); -// } -// }; diff --git a/packages/core/test/index.test.ts b/packages/core/test/index.test.ts index 441ca94..9b0cd77 100644 --- a/packages/core/test/index.test.ts +++ b/packages/core/test/index.test.ts @@ -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; + 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' +// }); diff --git a/packages/examples/cms-web-api/posts.jsonl b/packages/examples/cms-web-api/posts.jsonl new file mode 100644 index 0000000..d050cc4 --- /dev/null +++ b/packages/examples/cms-web-api/posts.jsonl @@ -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} \ No newline at end of file diff --git a/packages/examples/duckdb/test.db b/packages/examples/duckdb/test.db new file mode 100644 index 0000000..db4ab21 Binary files /dev/null and b/packages/examples/duckdb/test.db differ diff --git a/packages/examples/duckdb/test.db.wal b/packages/examples/duckdb/test.db.wal new file mode 100644 index 0000000..820f4f6 Binary files /dev/null and b/packages/examples/duckdb/test.db.wal differ