@@ -1,5 +1,5 @@ | |||||
{ | { | ||||
"name": "yasumi", | |||||
"name": "@modal-sh/yasumi", | |||||
"version": "0.0.0", | "version": "0.0.0", | ||||
"files": [ | "files": [ | ||||
"dist", | "dist", | ||||
@@ -1,3 +1,8 @@ | |||||
{ | { | ||||
"target": "es2018" | |||||
"target": "es2018", | |||||
"entrypoints": { | |||||
".": "src/index.ts", | |||||
"./backend": "src/backend/index.ts", | |||||
"./client": "src/client/index.ts" | |||||
} | |||||
} | } |
@@ -1,7 +1,6 @@ | |||||
import {ApplicationState, ContentNegotiation, Resource} from '../common'; | |||||
import {DataSource} from './data-source'; | |||||
import {BaseSchema} from 'valibot'; | import {BaseSchema} from 'valibot'; | ||||
import {Middleware} from './http/server'; | |||||
import {ApplicationState, ContentNegotiation, Language, LanguageStatusMessageMap, Resource} from '../common'; | |||||
import {DataSource} from './data-source'; | |||||
export interface BackendState { | export interface BackendState { | ||||
app: ApplicationState; | app: ApplicationState; | ||||
@@ -15,6 +14,28 @@ export interface BackendState { | |||||
export interface RequestContext {} | export interface RequestContext {} | ||||
export interface Middleware {} | |||||
export class MiddlewareError extends Error {} | |||||
export interface MiddlewareResponseErrorParams extends Omit<Response, 'statusMessage'> { | |||||
cause?: unknown; | |||||
} | |||||
export abstract class MiddlewareResponseError extends MiddlewareError implements Response { | |||||
readonly statusMessage: Response['statusMessage']; | |||||
readonly statusCode: Response['statusCode']; | |||||
readonly headers: Response['headers']; | |||||
constructor(statusMessage: keyof Language['statusMessages'], params: MiddlewareResponseErrorParams) { | |||||
super(statusMessage, { cause: params.cause }); | |||||
this.statusCode = params.statusCode; | |||||
this.headers = params.headers; | |||||
this.statusMessage = statusMessage; | |||||
} | |||||
} | |||||
export type RequestDecorator = (req: RequestContext) => RequestContext | Promise<RequestContext>; | export type RequestDecorator = (req: RequestContext) => RequestContext | Promise<RequestContext>; | ||||
export type ParamRequestDecorator<Params extends Array<unknown> = []> = (...args: Params) => RequestDecorator; | export type ParamRequestDecorator<Params extends Array<unknown> = []> = (...args: Params) => RequestDecorator; | ||||
@@ -27,3 +48,14 @@ export interface AllowedMiddlewareSpecification<Schema extends BaseSchema = Base | |||||
constructBodySchema?: (resource: Resource<Schema>, resourceId?: string) => BaseSchema; | constructBodySchema?: (resource: Resource<Schema>, resourceId?: string) => BaseSchema; | ||||
allowed: (resource: Resource<Schema>) => boolean; | allowed: (resource: Resource<Schema>) => boolean; | ||||
} | } | ||||
export interface Response { | |||||
// type of response | |||||
statusCode: number; | |||||
// description of response | |||||
statusMessage?: keyof LanguageStatusMessageMap; | |||||
// metadata of the response | |||||
headers?: Record<string, string>; | |||||
} |
@@ -1,6 +1,6 @@ | |||||
import {ApplicationState, FALLBACK_CHARSET, FALLBACK_LANGUAGE, FALLBACK_MEDIA_TYPE, Resource} from '../common'; | import {ApplicationState, FALLBACK_CHARSET, FALLBACK_LANGUAGE, FALLBACK_MEDIA_TYPE, Resource} from '../common'; | ||||
import http from 'http'; | import http from 'http'; | ||||
import {createServer, CreateServerParams} from './http/server'; | |||||
import {createServer, CreateServerParams} from './servers/http'; | |||||
import https from 'https'; | import https from 'https'; | ||||
import {BackendState} from './common'; | import {BackendState} from './common'; | ||||
import {DataSource} from './data-source'; | import {DataSource} from './data-source'; | ||||
@@ -1,9 +1,15 @@ | |||||
import http from 'http'; | import http from 'http'; | ||||
import {AllowedMiddlewareSpecification, BackendState, RequestContext} from '../common'; | |||||
import {Language, Resource, LanguageStatusMessageMap} from '../../common'; | |||||
import https from 'https'; | import https from 'https'; | ||||
import {constants} from 'http2'; | import {constants} from 'http2'; | ||||
import * as v from 'valibot'; | import * as v from 'valibot'; | ||||
import { | |||||
AllowedMiddlewareSpecification, | |||||
BackendState, | |||||
Middleware, | |||||
RequestContext, | |||||
Response | |||||
} from '../../common'; | |||||
import {Resource} from '../../../common'; | |||||
import { | import { | ||||
handleGetRoot, handleOptions, | handleGetRoot, handleOptions, | ||||
} from './handlers/default'; | } from './handlers/default'; | ||||
@@ -15,79 +21,11 @@ import { | |||||
handleGetItem, | handleGetItem, | ||||
handlePatchItem, | handlePatchItem, | ||||
} from './handlers/resource'; | } from './handlers/resource'; | ||||
import {getBody} from './utils'; | |||||
import {getBody, isTextMediaType} from './utils'; | |||||
import {decorateRequestWithBackend} from './decorators/backend'; | import {decorateRequestWithBackend} from './decorators/backend'; | ||||
import {decorateRequestWithMethod} from './decorators/method'; | import {decorateRequestWithMethod} from './decorators/method'; | ||||
import {decorateRequestWithUrl} from './decorators/url'; | import {decorateRequestWithUrl} from './decorators/url'; | ||||
declare module '../common' { | |||||
interface RequestContext extends http.IncomingMessage { | |||||
body?: unknown; | |||||
} | |||||
} | |||||
export interface Response { | |||||
statusCode: number; | |||||
statusMessage?: keyof LanguageStatusMessageMap; | |||||
headers?: Record<string, string>; | |||||
} | |||||
interface ResponseParams { | |||||
statusCode: Response['statusCode']; | |||||
statusMessage?: Response['statusMessage']; | |||||
headers?: Response['headers']; | |||||
} | |||||
export class MiddlewareError extends Error {} | |||||
interface PlainResponseParams<T = unknown> extends ResponseParams { | |||||
body?: T; | |||||
} | |||||
interface HttpMiddlewareErrorParams<T = unknown> extends Omit<PlainResponseParams<T>, 'statusMessage'> { | |||||
cause?: unknown | |||||
} | |||||
export class PlainResponse<T = unknown> implements Response { | |||||
readonly statusCode: Response['statusCode']; | |||||
readonly statusMessage?: keyof LanguageStatusMessageMap; | |||||
readonly headers: Response['headers']; | |||||
readonly body?: T; | |||||
constructor(args: PlainResponseParams<T>) { | |||||
this.statusCode = args.statusCode; | |||||
this.statusMessage = args.statusMessage; | |||||
this.headers = args.headers; | |||||
this.body = args.body; | |||||
} | |||||
} | |||||
export class HttpMiddlewareError extends MiddlewareError { | |||||
readonly response: PlainResponse; | |||||
constructor(statusMessage: keyof Language['statusMessages'], params: HttpMiddlewareErrorParams) { | |||||
super(statusMessage, { cause: params.cause }); | |||||
this.response = new PlainResponse({ | |||||
...params, | |||||
statusMessage, | |||||
}); | |||||
} | |||||
} | |||||
export interface CreateServerParams { | |||||
basePath?: string; | |||||
host?: string; | |||||
cert?: string; | |||||
key?: string; | |||||
requestTimeout?: number; | |||||
// CQRS | |||||
streamResponses?: boolean; | |||||
} | |||||
import {HttpMiddlewareError, PlainResponse} from './response'; | |||||
type RequiredResource = Required<Pick<RequestContext, 'resource'>>['resource']; | type RequiredResource = Required<Pick<RequestContext, 'resource'>>['resource']; | ||||
@@ -99,8 +37,14 @@ interface ResourceRequestContext extends Omit<RequestContext, 'resource'> { | |||||
resource: ResourceWithDataSource; | resource: ResourceWithDataSource; | ||||
} | } | ||||
export interface Middleware<Req extends ResourceRequestContext = ResourceRequestContext> { | |||||
(req: Req): undefined | Response | Promise<undefined | Response>; | |||||
declare module '../../common' { | |||||
interface RequestContext extends http.IncomingMessage { | |||||
body?: unknown; | |||||
} | |||||
interface Middleware<Req extends ResourceRequestContext = ResourceRequestContext> { | |||||
(req: Req): undefined | Response | Promise<undefined | Response>; | |||||
} | |||||
} | } | ||||
const constructPostSchema = <T extends v.BaseSchema>(resource: Resource<T>) => { | const constructPostSchema = <T extends v.BaseSchema>(resource: Resource<T>) => { | ||||
@@ -143,7 +87,6 @@ const constructPatchSchema = <T extends v.BaseSchema>(resource: Resource<T>) => | |||||
: schema | : schema | ||||
); | ); | ||||
}; | }; | ||||
// TODO add a way to define custom middlewares | // TODO add a way to define custom middlewares | ||||
const defaultCollectionMiddlewares: AllowedMiddlewareSpecification[] = [ | const defaultCollectionMiddlewares: AllowedMiddlewareSpecification[] = [ | ||||
{ | { | ||||
@@ -184,6 +127,16 @@ const defaultItemMiddlewares: AllowedMiddlewareSpecification[] = [ | |||||
}, | }, | ||||
]; | ]; | ||||
export interface CreateServerParams { | |||||
basePath?: string; | |||||
host?: string; | |||||
cert?: string; | |||||
key?: string; | |||||
requestTimeout?: number; | |||||
// CQRS | |||||
streamResponses?: boolean; | |||||
} | |||||
export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => { | export const createServer = (backendState: BackendState, serverParams = {} as CreateServerParams) => { | ||||
const isHttps = 'key' in serverParams && 'cert' in serverParams; | const isHttps = 'key' in serverParams && 'cert' in serverParams; | ||||
@@ -203,14 +156,6 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||||
decorateRequestWithBackend(backendState), | decorateRequestWithBackend(backendState), | ||||
]; | ]; | ||||
const isTextMediaType = (mediaType: string) => ( | |||||
mediaType.startsWith('text/') | |||||
|| [ | |||||
'application/json', | |||||
'application/xml' | |||||
].includes(mediaType) | |||||
); | |||||
const handleMiddlewares = async (currentHandlerState: Awaited<ReturnType<Middleware>>, currentMiddleware: AllowedMiddlewareSpecification, req: ResourceRequestContext) => { | const handleMiddlewares = async (currentHandlerState: Awaited<ReturnType<Middleware>>, currentMiddleware: AllowedMiddlewareSpecification, req: ResourceRequestContext) => { | ||||
const { method: middlewareMethod, middleware, constructBodySchema} = currentMiddleware; | const { method: middlewareMethod, middleware, constructBodySchema} = currentMiddleware; | ||||
const effectiveMethod = req.method === 'HEAD' ? 'GET' : req.method; | const effectiveMethod = req.method === 'HEAD' ? 'GET' : req.method; | ||||
@@ -350,8 +295,9 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||||
return await effectiveRequestDecorators.reduce( | return await effectiveRequestDecorators.reduce( | ||||
async (resultRequestPromise, decorator) => { | async (resultRequestPromise, decorator) => { | ||||
const resultRequest = await resultRequestPromise; | const resultRequest = await resultRequestPromise; | ||||
return await decorator(resultRequest); | |||||
const decoratedRequest = await decorator(resultRequest); | |||||
// TODO log decorators | |||||
return decoratedRequest; | |||||
}, | }, | ||||
Promise.resolve(reqRaw as RequestContext) | Promise.resolve(reqRaw as RequestContext) | ||||
); | ); | ||||
@@ -373,11 +319,11 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||||
middlewareState = await processRequestFn(resourceReq) as any; // TODO fix this | middlewareState = await processRequestFn(resourceReq) as any; // TODO fix this | ||||
} catch (processRequestErrRaw) { | } catch (processRequestErrRaw) { | ||||
const finalErr = processRequestErrRaw as HttpMiddlewareError; | const finalErr = processRequestErrRaw as HttpMiddlewareError; | ||||
const headers = finalErr.response.headers ?? {}; | |||||
const headers = finalErr.headers ?? {}; | |||||
let encoded: Buffer | undefined; | let encoded: Buffer | undefined; | ||||
let serialized; | let serialized; | ||||
try { | try { | ||||
serialized = typeof finalErr.response.body !== 'undefined' ? resourceReq.backend.cn.mediaType.serialize(finalErr.response.body) : undefined; | |||||
serialized = typeof finalErr.body !== 'undefined' ? resourceReq.backend.cn.mediaType.serialize(finalErr.body) : undefined; | |||||
} catch (cause) { | } catch (cause) { | ||||
res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToSerializeResponse']?.replace( | res.statusMessage = resourceReq.backend.cn.language.statusMessages['unableToSerializeResponse']?.replace( | ||||
/\$RESOURCE/g, | /\$RESOURCE/g, | ||||
@@ -401,9 +347,9 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||||
`charset=${resourceReq.backend.cn.charset.name}`, | `charset=${resourceReq.backend.cn.charset.name}`, | ||||
].join('; '); | ].join('; '); | ||||
const statusMessageKey = finalErr.response.statusMessage ? resourceReq.backend.cn.language.statusMessages[finalErr.response.statusMessage] : undefined; | |||||
const statusMessageKey = finalErr.statusMessage ? resourceReq.backend.cn.language.statusMessages[finalErr.statusMessage] : undefined; | |||||
res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, resourceReq.resource!.state.itemName) ?? ''; | res.statusMessage = statusMessageKey?.replace(/\$RESOURCE/g, resourceReq.resource!.state.itemName) ?? ''; | ||||
res.writeHead(finalErr.response.statusCode, headers); | |||||
res.writeHead(finalErr.statusCode, headers); | |||||
if (typeof encoded !== 'undefined') { | if (typeof encoded !== 'undefined') { | ||||
res.end(encoded); | res.end(encoded); | ||||
return; | return; | ||||
@@ -503,7 +449,12 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||||
return; | return; | ||||
} | } | ||||
throw new Error('Not implemented'); | |||||
res.statusMessage = reqRaw.backend.cn.language.statusMessages.notImplemented.replace(/\$RESOURCE/g, | |||||
reqRaw.resource!.state.itemName) ?? ''; | |||||
res.writeHead(constants.HTTP_STATUS_NOT_IMPLEMENTED, { | |||||
'Content-Language': reqRaw.backend.cn.language.name, | |||||
}); | |||||
res.end(); | |||||
}; | }; | ||||
server.on('request', handleRequest); | server.on('request', handleRequest); |
@@ -1,8 +1,8 @@ | |||||
import {ContentNegotiation} from '../../../../common'; | |||||
import {RequestDecorator} from '../../../common'; | |||||
import {ContentNegotiation} from '../../../../../common'; | |||||
import {RequestDecorator} from '../../../../common'; | |||||
import Negotiator from 'negotiator'; | import Negotiator from 'negotiator'; | ||||
declare module '../../../common' { | |||||
declare module '../../../../common' { | |||||
interface RequestContext { | interface RequestContext { | ||||
cn: ContentNegotiation; | cn: ContentNegotiation; | ||||
} | } |
@@ -1,8 +1,8 @@ | |||||
import {BackendState, ParamRequestDecorator} from '../../../common'; | |||||
import {BackendState, ParamRequestDecorator} from '../../../../common'; | |||||
import {decorateRequestWithContentNegotiation} from './content-negotiation'; | import {decorateRequestWithContentNegotiation} from './content-negotiation'; | ||||
import {decorateRequestWithResource} from './resource'; | import {decorateRequestWithResource} from './resource'; | ||||
declare module '../../../common' { | |||||
declare module '../../../../common' { | |||||
interface RequestContext { | interface RequestContext { | ||||
backend: BackendState; | backend: BackendState; | ||||
} | } |
@@ -1,7 +1,7 @@ | |||||
import {RequestDecorator} from '../../../common'; | |||||
import {Resource} from '../../../../common'; | |||||
import {Resource} from '../../../../../common'; | |||||
import {RequestDecorator} from '../../../../common'; | |||||
declare module '../../../common' { | |||||
declare module '../../../../common' { | |||||
interface RequestContext { | interface RequestContext { | ||||
resource?: Resource; | resource?: Resource; | ||||
resourceId?: string; | resourceId?: string; |
@@ -1,4 +1,4 @@ | |||||
import {RequestDecorator} from '../../../common'; | |||||
import {RequestDecorator} from '../../../../common'; | |||||
export const decorateRequestWithMethod: RequestDecorator = (req) => { | export const decorateRequestWithMethod: RequestDecorator = (req) => { | ||||
req.method = req.method?.trim().toUpperCase() ?? ''; | req.method = req.method?.trim().toUpperCase() ?? ''; |
@@ -1,6 +1,6 @@ | |||||
import {ParamRequestDecorator} from '../../../common'; | |||||
import {ParamRequestDecorator} from '../../../../common'; | |||||
declare module '../../../common' { | |||||
declare module '../../../../common' { | |||||
interface RequestContext { | interface RequestContext { | ||||
basePath: string; | basePath: string; | ||||
} | } |
@@ -1,6 +1,6 @@ | |||||
import {ParamRequestDecorator} from '../../../common'; | |||||
import {ParamRequestDecorator} from '../../../../common'; | |||||
declare module '../../../common' { | |||||
declare module '../../../../common' { | |||||
interface RequestContext { | interface RequestContext { | ||||
host: string; | host: string; | ||||
} | } |
@@ -1,10 +1,10 @@ | |||||
import {ParamRequestDecorator} from '../../../common'; | |||||
import {CreateServerParams} from '../../server'; | |||||
import {ParamRequestDecorator} from '../../../../common'; | |||||
import {CreateServerParams} from '../../index'; | |||||
import {decorateRequestWithScheme} from './scheme'; | import {decorateRequestWithScheme} from './scheme'; | ||||
import {decorateRequestWithHost} from './host'; | import {decorateRequestWithHost} from './host'; | ||||
import {decorateRequestWithBasePath} from './base-path'; | import {decorateRequestWithBasePath} from './base-path'; | ||||
declare module '../../../common' { | |||||
declare module '../../../../common' { | |||||
interface RequestContext { | interface RequestContext { | ||||
rawUrl?: string; | rawUrl?: string; | ||||
query: URLSearchParams; | query: URLSearchParams; |
@@ -1,6 +1,6 @@ | |||||
import {ParamRequestDecorator} from '../../../common'; | |||||
import {ParamRequestDecorator} from '../../../../common'; | |||||
declare module '../../../common' { | |||||
declare module '../../../../common' { | |||||
interface RequestContext { | interface RequestContext { | ||||
scheme: string; | scheme: string; | ||||
} | } |
@@ -1,7 +1,7 @@ | |||||
import {HttpMiddlewareError, Middleware, PlainResponse} from '../server'; | |||||
import {LinkMap} from '../utils'; | |||||
import {constants} from 'http2'; | import {constants} from 'http2'; | ||||
import {AllowedMiddlewareSpecification} from '../../common'; | |||||
import {AllowedMiddlewareSpecification, Middleware} from '../../../common'; | |||||
import {LinkMap} from '../utils'; | |||||
import {PlainResponse, HttpMiddlewareError} from '../response'; | |||||
export const handleGetRoot: Middleware = (req) => { | export const handleGetRoot: Middleware = (req) => { | ||||
const { backend, basePath } = req; | const { backend, basePath } = req; |
@@ -1,6 +1,7 @@ | |||||
import { constants } from 'http2'; | import { constants } from 'http2'; | ||||
import * as v from 'valibot'; | import * as v from 'valibot'; | ||||
import {HttpMiddlewareError, PlainResponse, Middleware} from '../server'; | |||||
import {Middleware} from '../../../common'; | |||||
import {HttpMiddlewareError, PlainResponse} from '../response'; | |||||
export const handleGetCollection: Middleware = async (req) => { | export const handleGetCollection: Middleware = async (req) => { | ||||
const { query, resource, backend } = req; | const { query, resource, backend } = req; |
@@ -0,0 +1 @@ | |||||
export * from './core'; |
@@ -0,0 +1,36 @@ | |||||
import {Language, LanguageStatusMessageMap} from '../../../common'; | |||||
import {MiddlewareResponseError, Response} from '../../common'; | |||||
interface PlainResponseParams<T = unknown> extends Response { | |||||
body?: T; | |||||
} | |||||
interface HttpMiddlewareErrorParams<T = unknown> extends Omit<PlainResponseParams<T>, 'statusMessage'> { | |||||
cause?: unknown | |||||
} | |||||
export class HttpMiddlewareError<T = unknown> extends MiddlewareResponseError implements PlainResponseParams<T> { | |||||
body?: T; | |||||
constructor(statusMessage: keyof Language['statusMessages'], params: HttpMiddlewareErrorParams<T>) { | |||||
super(statusMessage, params); | |||||
this.body = params.body; | |||||
} | |||||
} | |||||
export class PlainResponse<T = unknown> implements Response { | |||||
readonly statusCode: Response['statusCode']; | |||||
readonly statusMessage?: keyof LanguageStatusMessageMap; | |||||
readonly headers: Response['headers']; | |||||
readonly body?: T; | |||||
constructor(args: PlainResponseParams<T>) { | |||||
this.statusCode = args.statusCode; | |||||
this.statusMessage = args.statusMessage; | |||||
this.headers = args.headers; | |||||
this.body = args.body; | |||||
} | |||||
} |
@@ -1,5 +1,13 @@ | |||||
import {IncomingMessage} from 'http'; | import {IncomingMessage} from 'http'; | ||||
export const isTextMediaType = (mediaType: string) => ( | |||||
mediaType.startsWith('text/') | |||||
|| [ | |||||
'application/json', | |||||
'application/xml' | |||||
].includes(mediaType) | |||||
); | |||||
export const getBody = ( | export const getBody = ( | ||||
req: IncomingMessage, | req: IncomingMessage, | ||||
) => new Promise<Buffer>((resolve, reject) => { | ) => new Promise<Buffer>((resolve, reject) => { |
@@ -33,6 +33,7 @@ export const LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS = [ | |||||
'resourcePatched', | 'resourcePatched', | ||||
'resourceCreated', | 'resourceCreated', | ||||
'resourceReplaced', | 'resourceReplaced', | ||||
'notImplemented', | |||||
] as const; | ] as const; | ||||
export type LanguageDefaultStatusMessageKey = typeof LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS[number]; | export type LanguageDefaultStatusMessageKey = typeof LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS[number]; | ||||
@@ -90,6 +91,7 @@ export const FALLBACK_LANGUAGE = { | |||||
unableToEmplaceResource: 'Unable To Emplace $RESOURCE', | unableToEmplaceResource: 'Unable To Emplace $RESOURCE', | ||||
resourceIdNotGiven: '$RESOURCE ID Not Given', | resourceIdNotGiven: '$RESOURCE ID Not Given', | ||||
unableToCreateResource: 'Unable To Create $RESOURCE', | unableToCreateResource: 'Unable To Create $RESOURCE', | ||||
notImplemented: 'Not Implemented' | |||||
}, | }, | ||||
bodies: { | bodies: { | ||||
languageNotAcceptable: [], | languageNotAcceptable: [], | ||||
@@ -1,3 +1 @@ | |||||
export * from './common'; | export * from './common'; | ||||
export * as dataSources from './backend/data-sources'; |
@@ -0,0 +1,100 @@ | |||||
import {describe, afterAll, afterEach, beforeAll, beforeEach, it} from 'vitest'; | |||||
import {mkdtemp, rm} from 'fs/promises'; | |||||
import {join} from 'path'; | |||||
import {tmpdir} from 'os'; | |||||
import {application, resource, Resource, validation as v} from '../../src'; | |||||
import {dataSources} from '../../src/backend'; | |||||
import {Server} from 'http'; | |||||
import {autoIncrement} from '../fixtures'; | |||||
const PORT = 3001; | |||||
const HOST = '127.0.0.1'; | |||||
const BASE_PATH = '/api'; | |||||
const ACCEPT = 'application/json'; | |||||
const ACCEPT_LANGUAGE = 'en'; | |||||
const CONTENT_TYPE_CHARSET = 'utf-8'; | |||||
const CONTENT_TYPE = ACCEPT; | |||||
describe('decorators', () => { | |||||
let baseDir: string; | |||||
beforeAll(async () => { | |||||
try { | |||||
baseDir = await mkdtemp(join(tmpdir(), 'yasumi-')); | |||||
} catch { | |||||
// noop | |||||
} | |||||
}); | |||||
afterAll(async () => { | |||||
try { | |||||
await rm(baseDir, { | |||||
recursive: true, | |||||
}); | |||||
} catch { | |||||
// noop | |||||
} | |||||
}); | |||||
let Piano: Resource; | |||||
beforeEach(() => { | |||||
Piano = resource(v.object( | |||||
{ | |||||
brand: v.string() | |||||
}, | |||||
v.never() | |||||
)) | |||||
.name('Piano' as const) | |||||
.route('pianos' as const) | |||||
.id('id' as const, { | |||||
generationStrategy: autoIncrement, | |||||
serialize: (id) => id?.toString() ?? '0', | |||||
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, | |||||
schema: v.number(), | |||||
}); | |||||
}); | |||||
let server: Server; | |||||
beforeEach(() => { | |||||
const app = application({ | |||||
name: 'piano-service', | |||||
}) | |||||
.resource(Piano); | |||||
const backend = app | |||||
.createBackend({ | |||||
dataSource: new dataSources.jsonlFile.DataSource(baseDir), | |||||
}) | |||||
.throwsErrorOnDeletingNotFound(); | |||||
server = backend.createHttpServer({ | |||||
basePath: BASE_PATH | |||||
}); | |||||
return new Promise((resolve, reject) => { | |||||
server.on('error', (err) => { | |||||
reject(err); | |||||
}); | |||||
server.on('listening', () => { | |||||
resolve(); | |||||
}); | |||||
server.listen({ | |||||
port: PORT | |||||
}); | |||||
}); | |||||
}); | |||||
afterEach(() => new Promise((resolve, reject) => { | |||||
server.close((err) => { | |||||
if (err) { | |||||
reject(err); | |||||
} | |||||
resolve(); | |||||
}); | |||||
})); | |||||
it('decorates requests', () => { | |||||
}); | |||||
}); |
@@ -20,32 +20,17 @@ import { | |||||
} from 'path'; | } from 'path'; | ||||
import {request, Server} from 'http'; | import {request, Server} from 'http'; | ||||
import {constants} from 'http2'; | import {constants} from 'http2'; | ||||
import {DataSource} from '../../src/backend/data-source'; | |||||
import { dataSources } from '../../src/backend'; | |||||
import {BackendBuilder, dataSources} from '../../src/backend'; | |||||
import { application, resource, validation as v, Resource } from '../../src'; | import { application, resource, validation as v, Resource } from '../../src'; | ||||
import { autoIncrement } from '../fixtures'; | |||||
const PORT = 3000; | const PORT = 3000; | ||||
const HOST = '127.0.0.1'; | const HOST = '127.0.0.1'; | ||||
const BASE_PATH = '/api'; | |||||
const ACCEPT = 'application/json'; | const ACCEPT = 'application/json'; | ||||
const ACCEPT_LANGUAGE = 'en'; | const ACCEPT_LANGUAGE = 'en'; | ||||
const CONTENT_TYPE_CHARSET = 'utf-8'; | const CONTENT_TYPE_CHARSET = 'utf-8'; | ||||
const CONTENT_TYPE = ACCEPT; | const CONTENT_TYPE = ACCEPT; | ||||
const BASE_PATH = '/api'; | |||||
const autoIncrement = async (dataSource: DataSource) => { | |||||
const data = await dataSource.getMultiple() as Record<string, string>[]; | |||||
const highestId = data.reduce<number>( | |||||
(highestId, d) => (Number(d.id) > highestId ? Number(d.id) : highestId), | |||||
-Infinity | |||||
); | |||||
if (Number.isFinite(highestId)) { | |||||
return (highestId + 1); | |||||
} | |||||
return 1; | |||||
}; | |||||
describe('yasumi HTTP', () => { | describe('yasumi HTTP', () => { | ||||
let baseDir: string; | let baseDir: string; | ||||
@@ -84,6 +69,7 @@ describe('yasumi HTTP', () => { | |||||
}); | }); | ||||
}); | }); | ||||
let backend: BackendBuilder; | |||||
let server: Server; | let server: Server; | ||||
beforeEach(() => { | beforeEach(() => { | ||||
const app = application({ | const app = application({ | ||||
@@ -91,11 +77,9 @@ describe('yasumi HTTP', () => { | |||||
}) | }) | ||||
.resource(Piano); | .resource(Piano); | ||||
const backend = app | |||||
.createBackend({ | |||||
dataSource: new dataSources.jsonlFile.DataSource(baseDir), | |||||
}) | |||||
.throwsErrorOnDeletingNotFound(); | |||||
backend = app.createBackend({ | |||||
dataSource: new dataSources.jsonlFile.DataSource(baseDir), | |||||
}); | |||||
server = backend.createHttpServer({ | server = backend.createHttpServer({ | ||||
basePath: BASE_PATH | basePath: BASE_PATH | ||||
@@ -1062,10 +1046,12 @@ describe('yasumi HTTP', () => { | |||||
beforeEach(() => { | beforeEach(() => { | ||||
Piano.canDelete(); | Piano.canDelete(); | ||||
backend.throwsErrorOnDeletingNotFound(); | |||||
}); | }); | ||||
afterEach(() => { | afterEach(() => { | ||||
Piano.canDelete(false); | Piano.canDelete(false); | ||||
backend.throwsErrorOnDeletingNotFound(false); | |||||
}); | }); | ||||
it('throws on item not found', () => { | it('throws on item not found', () => { | ||||
@@ -0,0 +1,16 @@ | |||||
import {DataSource} from '../src/backend/data-source'; | |||||
export const autoIncrement = async (dataSource: DataSource) => { | |||||
const data = await dataSource.getMultiple() as Record<string, string>[]; | |||||
const highestId = data.reduce<number>( | |||||
(highestId, d) => (Number(d.id) > highestId ? Number(d.id) : highestId), | |||||
-Infinity | |||||
); | |||||
if (Number.isFinite(highestId)) { | |||||
return (highestId + 1); | |||||
} | |||||
return 1; | |||||
}; |