@@ -1,9 +1,7 @@ | |||
import {dataSources, Resource} from '../../src'; | |||
import {DataSource} from '../../src/backend/data-source'; | |||
import {BaseDataSource} from '../../src/common/data-source'; | |||
export const autoIncrement = async (dataSource: BaseDataSource) => { | |||
const data = await (dataSource as DataSource).getMultiple() as Record<string, string>[]; | |||
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), | |||
@@ -16,5 +14,3 @@ export const autoIncrement = async (dataSource: BaseDataSource) => { | |||
return 1; | |||
}; | |||
export const dataSource = (resource: Resource) => new dataSources.jsonlFile.DataSource(resource, 'examples/basic'); |
@@ -1,10 +1,10 @@ | |||
import { | |||
application, | |||
application, dataSources, | |||
resource, | |||
validation as v, | |||
} from '../../src'; | |||
import {TEXT_SERIALIZER_PAIR} from './serializers'; | |||
import {autoIncrement, dataSource} from './data-source'; | |||
import {autoIncrement} from './data-source'; | |||
const Piano = resource(v.object( | |||
{ | |||
@@ -14,18 +14,18 @@ const Piano = resource(v.object( | |||
)) | |||
.name('Piano') | |||
.route('pianos') | |||
.id('id', { | |||
generationStrategy: autoIncrement, | |||
serialize: (id) => id?.toString() ?? '0', | |||
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, | |||
schema: v.number(), | |||
}) | |||
.canFetchItem() | |||
.canFetchCollection() | |||
.canCreate() | |||
.canEmplace() | |||
.canPatch() | |||
.canDelete(); | |||
.canDelete() | |||
.id('id', { | |||
generationStrategy: autoIncrement, | |||
serialize: (id) => id?.toString() ?? '0', | |||
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, | |||
schema: v.number(), | |||
}); | |||
const User = resource(v.object( | |||
{ | |||
@@ -37,15 +37,15 @@ const User = resource(v.object( | |||
}, | |||
v.never() | |||
)) | |||
.name('User') | |||
.route('users') | |||
.fullText('bio') | |||
.id('id' as const, { | |||
generationStrategy: autoIncrement, | |||
serialize: (id) => id?.toString() ?? '0', | |||
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, | |||
schema: v.number(), | |||
}) | |||
.name('User') | |||
.route('users') | |||
.fullText('bio'); | |||
}); | |||
const app = application({ | |||
name: 'piano-service', | |||
@@ -55,7 +55,7 @@ const app = application({ | |||
.resource(User); | |||
const backend = app.createBackend({ | |||
dataSource, | |||
dataSource: new dataSources.jsonlFile.DataSource('examples/basic'), | |||
}); | |||
const server = backend.createHttpServer({ | |||
@@ -1,12 +1,12 @@ | |||
import {ApplicationState, ContentNegotiation, Resource} from '../common'; | |||
import {BaseDataSource} from '../common/data-source'; | |||
import {ApplicationState, ContentNegotiation} from '../common'; | |||
import {DataSource} from './data-source'; | |||
export interface BackendState { | |||
app: ApplicationState; | |||
dataSource: (resource: Resource) => BaseDataSource; | |||
dataSource: DataSource; | |||
cn: ContentNegotiation; | |||
showTotalItemCountOnGetCollection: boolean; | |||
throws404OnDeletingNotFound: boolean; | |||
throwsErrorOnDeletingNotFound: boolean; | |||
checksSerializersOnDelete: boolean; | |||
showTotalItemCountOnCreateItem: boolean; | |||
} | |||
@@ -1,25 +1,11 @@ | |||
import * as v from 'valibot'; | |||
import {ApplicationState, FALLBACK_CHARSET, FALLBACK_LANGUAGE, FALLBACK_MEDIA_TYPE, Resource} from '../common'; | |||
import http from 'http'; | |||
import {createServer, CreateServerParams} from './http/server'; | |||
import https from 'https'; | |||
import {BackendState} from './common'; | |||
import {BaseDataSource} from '../common/data-source'; | |||
import {DataSource} from './data-source'; | |||
export interface BackendResource< | |||
DataSourceType extends BaseDataSource = DataSource, | |||
ResourceSchema extends v.BaseSchema = v.BaseSchema, | |||
ResourceName extends string = string, | |||
ResourceRouteName extends string = string, | |||
IdAttr extends string = string, | |||
IdSchema extends v.BaseSchema = v.BaseSchema | |||
> extends Resource<ResourceSchema, ResourceName, ResourceRouteName, IdAttr, IdSchema> { | |||
newId(dataSource: DataSourceType): string | number | unknown; | |||
dataSource: DataSourceType; | |||
} | |||
export interface BackendBuilder<T extends BaseDataSource = BaseDataSource> { | |||
export interface BackendBuilder<T extends DataSource = DataSource> { | |||
showTotalItemCountOnGetCollection(b?: boolean): this; | |||
showTotalItemCountOnCreateItem(b?: boolean): this; | |||
checksSerializersOnDelete(b?: boolean): this; | |||
@@ -30,7 +16,7 @@ export interface BackendBuilder<T extends BaseDataSource = BaseDataSource> { | |||
export interface CreateBackendParams { | |||
app: ApplicationState; | |||
dataSource: (resource: Resource) => BaseDataSource; | |||
dataSource: DataSource; | |||
} | |||
export const createBackend = (params: CreateBackendParams) => { | |||
@@ -44,7 +30,7 @@ export const createBackend = (params: CreateBackendParams) => { | |||
}, | |||
showTotalItemCountOnGetCollection: false, | |||
showTotalItemCountOnCreateItem: false, | |||
throws404OnDeletingNotFound: false, | |||
throwsErrorOnDeletingNotFound: false, | |||
checksSerializersOnDelete: false, | |||
}; | |||
@@ -58,7 +44,7 @@ export const createBackend = (params: CreateBackendParams) => { | |||
return this; | |||
}, | |||
throwsErrorOnDeletingNotFound(b = true) { | |||
backendState.throws404OnDeletingNotFound = b; | |||
backendState.throwsErrorOnDeletingNotFound = b; | |||
return this; | |||
}, | |||
checksSerializersOnDelete(b = true) { | |||
@@ -1,13 +1,34 @@ | |||
import {BaseDataSource} from '../common/data-source'; | |||
import * as v from 'valibot'; | |||
import {Resource} from '../common'; | |||
export interface DataSource<T = object, Q = object> extends BaseDataSource { | |||
type IsCreated = boolean; | |||
type TotalCount = number; | |||
type DeleteResult = unknown; | |||
export interface DataSource<Schema = object, Query = object, ID = string> { | |||
initialize(): Promise<unknown>; | |||
getTotalCount?(query?: Q): Promise<number>; | |||
getMultiple(query?: Q): Promise<T[]>; | |||
getById(id: string): Promise<T | null>; | |||
getSingle?(query?: Q): Promise<T | null>; | |||
create(data: T): Promise<T>; | |||
delete(id: string): Promise<unknown>; | |||
emplace(id: string, data: T): Promise<[T, boolean]>; | |||
patch(id: string, data: Partial<T>): Promise<T | null>; | |||
getTotalCount?(query?: Query): Promise<TotalCount>; | |||
getMultiple(query?: Query): Promise<Schema[]>; | |||
getById(id: ID): Promise<Schema | null>; | |||
getSingle?(query?: Query): Promise<Schema | null>; | |||
create(data: Schema): Promise<Schema>; | |||
delete(id: ID): Promise<DeleteResult>; | |||
emplace(id: ID, data: Schema): Promise<[Schema, IsCreated]>; | |||
patch(id: ID, data: Partial<Schema>): Promise<Schema | null>; | |||
prepareResource(resource: Resource): void; | |||
newId(): Promise<ID>; | |||
} | |||
export interface ResourceIdConfig<IdSchema extends v.BaseSchema> { | |||
generationStrategy: GenerationStrategy; | |||
serialize: (id: unknown) => string; | |||
deserialize: (id: string) => v.Output<IdSchema>; | |||
schema: IdSchema; | |||
} | |||
export interface GenerationStrategy { | |||
(dataSource: DataSource, ...args: unknown[]): Promise<string | number | unknown>; | |||
} |
@@ -1,18 +1,64 @@ | |||
import {readFile, writeFile} from 'fs/promises'; | |||
import {join} from 'path'; | |||
import {DataSource as DataSourceInterface} from '../data-source'; | |||
import {DataSource as DataSourceInterface, ResourceIdConfig} from '../data-source'; | |||
import {Resource} from '../..'; | |||
import * as v from 'valibot'; | |||
declare module '../..' { | |||
interface BaseResourceState { | |||
idAttr: string; | |||
idConfig: ResourceIdConfig<v.BaseSchema> | |||
} | |||
interface Resource< | |||
Schema extends v.BaseSchema = v.BaseSchema, | |||
CurrentName extends string = string, | |||
CurrentRouteName extends string = string, | |||
CurrentIdAttr extends string = string, | |||
IdSchema extends v.BaseSchema = v.BaseSchema | |||
> { | |||
dataSource?: DataSourceInterface; | |||
id<NewIdAttr extends CurrentIdAttr, TheIdSchema extends IdSchema>( | |||
newIdAttr: NewIdAttr, | |||
params: ResourceIdConfig<TheIdSchema> | |||
): Resource<Schema, CurrentName, CurrentRouteName, NewIdAttr, TheIdSchema>; | |||
fullText(fullTextAttr: string): this; | |||
} | |||
} | |||
export class DataSource<T extends Record<string, string>> implements DataSourceInterface<T> { | |||
private readonly path: string; | |||
private path?: string; | |||
private resource?: Resource; | |||
data: T[] = []; | |||
constructor(private readonly resource: Resource, baseDir = '') { | |||
this.path = join(baseDir, `${this.resource.state.routeName}.jsonl`); | |||
constructor(private readonly baseDir = '') { | |||
// noop | |||
} | |||
prepareResource< | |||
Schema extends v.BaseSchema = v.BaseSchema, | |||
CurrentName extends string = string, | |||
CurrentRouteName extends string = string | |||
>(resource: Resource<Schema, CurrentName, CurrentRouteName>) { | |||
this.path = join(this.baseDir, `${resource.state.routeName}.jsonl`); | |||
resource.dataSource = resource.dataSource ?? this; | |||
const originalResourceId = resource.id; | |||
resource.id = <NewIdAttr extends string, NewIdSchema extends v.BaseSchema>(newIdAttr: NewIdAttr, params: ResourceIdConfig<NewIdSchema>) => { | |||
originalResourceId(newIdAttr, params); | |||
return resource as Resource<Schema, CurrentName, CurrentRouteName, NewIdAttr, NewIdSchema>; | |||
}; | |||
this.resource = resource; | |||
} | |||
async initialize() { | |||
if (typeof this.path !== 'string') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
try { | |||
const fileContents = await readFile(this.path, 'utf-8'); | |||
const lines = fileContents.split('\n'); | |||
@@ -30,9 +76,38 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI | |||
return [...this.data]; | |||
} | |||
async newId() { | |||
const idConfig = this.resource?.state.shared.get('idConfig') as ResourceIdConfig<any>; | |||
if (typeof idConfig === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const theNewId = await idConfig.generationStrategy(this); | |||
return theNewId as string; | |||
} | |||
async getById(idSerialized: string) { | |||
const id = this.resource.state.idConfig.deserialize(idSerialized); | |||
const foundData = this.data.find((s) => s[this.resource.state.idAttr as string] === id); | |||
if (typeof this.resource === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
if (typeof this.path !== 'string') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const theIdAttr = this.resource.state.shared.get('idAttr'); | |||
if (typeof theIdAttr === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const idAttr = theIdAttr as string; | |||
const theIdConfigRaw = this.resource.state.shared.get('idConfig'); | |||
if (typeof theIdConfigRaw === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const theIdConfig = theIdConfigRaw as ResourceIdConfig<any>; | |||
const id = theIdConfig.deserialize(idSerialized); | |||
const foundData = this.data.find((s) => s[idAttr] === id); | |||
if (foundData) { | |||
return { | |||
@@ -44,12 +119,31 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI | |||
} | |||
async create(data: T) { | |||
if (typeof this.resource === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
if (typeof this.path !== 'string') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const theIdAttr = this.resource.state.shared.get('idAttr'); | |||
if (typeof theIdAttr === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const idAttr = theIdAttr as string; | |||
const theIdConfigRaw = this.resource.state.shared.get('idConfig'); | |||
if (typeof theIdConfigRaw === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const theIdConfig = theIdConfigRaw as ResourceIdConfig<any>; | |||
const newData = { | |||
...data | |||
} as Record<string, unknown>; | |||
if (this.resource.state.idAttr in newData) { | |||
newData[this.resource.state.idAttr] = this.resource.state.idConfig.deserialize(newData[this.resource.state.idAttr] as string); | |||
if (idAttr in newData) { | |||
newData[idAttr] = theIdConfig.deserialize(newData[idAttr] as string); | |||
} | |||
const newCollection = [ | |||
@@ -63,10 +157,29 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI | |||
} | |||
async delete(idSerialized: string) { | |||
if (typeof this.resource === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
if (typeof this.path !== 'string') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const theIdAttr = this.resource.state.shared.get('idAttr'); | |||
if (typeof theIdAttr === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const idAttr = theIdAttr as string; | |||
const theIdConfigRaw = this.resource.state.shared.get('idConfig'); | |||
if (typeof theIdConfigRaw === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const theIdConfig = theIdConfigRaw as ResourceIdConfig<any>; | |||
const oldDataLength = this.data.length; | |||
const id = this.resource.state.idConfig.deserialize(idSerialized); | |||
const newData = this.data.filter((s) => !(s[this.resource.state.idAttr] === id)); | |||
const id = theIdConfig.deserialize(idSerialized); | |||
const newData = this.data.filter((s) => !(s[idAttr] === id)); | |||
await writeFile(this.path, newData.map((d) => JSON.stringify(d)).join('\n')); | |||
@@ -74,17 +187,36 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI | |||
} | |||
async emplace(idSerialized: string, dataWithId: T) { | |||
if (typeof this.resource === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
if (typeof this.path !== 'string') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const theIdAttr = this.resource.state.shared.get('idAttr'); | |||
if (typeof theIdAttr === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const idAttr = theIdAttr as string; | |||
const theIdConfigRaw = this.resource.state.shared.get('idConfig'); | |||
if (typeof theIdConfigRaw === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const theIdConfig = theIdConfigRaw as ResourceIdConfig<any>; | |||
const existing = await this.getById(idSerialized); | |||
const id = this.resource.state.idConfig.deserialize(idSerialized); | |||
const { [this.resource.state.idAttr]: idFromResource, ...data } = dataWithId; | |||
const id = theIdConfig.deserialize(idSerialized); | |||
const { [idAttr]: idFromResource, ...data } = dataWithId; | |||
const dataToEmplace = { | |||
...data, | |||
[this.resource.state.idAttr]: id, | |||
[idAttr]: id, | |||
} as T; | |||
if (existing) { | |||
const newData = this.data.map((d) => { | |||
if (d[this.resource.state.idAttr] === id) { | |||
if (d[idAttr] === id) { | |||
return dataToEmplace; | |||
} | |||
@@ -101,6 +233,25 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI | |||
} | |||
async patch(idSerialized: string, data: Partial<T>) { | |||
if (typeof this.resource === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
if (typeof this.path !== 'string') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const theIdAttr = this.resource.state.shared.get('idAttr'); | |||
if (typeof theIdAttr === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const idAttr = theIdAttr as string; | |||
const theIdConfigRaw = this.resource.state.shared.get('idConfig'); | |||
if (typeof theIdConfigRaw === 'undefined') { | |||
throw new Error('Resource not prepared.'); | |||
} | |||
const theIdConfig = theIdConfigRaw as ResourceIdConfig<any>; | |||
const existing = await this.getById(idSerialized); | |||
if (!existing) { | |||
return null; | |||
@@ -111,9 +262,9 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI | |||
...data, | |||
} | |||
const id = this.resource.state.idConfig.deserialize(idSerialized); | |||
const id = theIdConfig.deserialize(idSerialized); | |||
const newData = this.data.map((d) => { | |||
if (d[this.resource.state.idAttr as string] === id) { | |||
if (d[idAttr] === id) { | |||
return newItem; | |||
} | |||
@@ -1,7 +1,5 @@ | |||
import {RequestDecorator} from '../../../common'; | |||
import {DataSource} from '../../../data-source'; | |||
import {Resource} from '../../../../common'; | |||
import {BackendResource} from '../../../core'; | |||
declare module '../../../common' { | |||
interface RequestContext { | |||
@@ -11,14 +9,16 @@ declare module '../../../common' { | |||
} | |||
export const decorateRequestWithResource: RequestDecorator = (req) => { | |||
const [, resourceRouteName, resourceId = ''] = req.url?.split('/') ?? []; | |||
const [, resourceRouteName, resourceId] = req.url?.split('/') ?? []; | |||
const resource = Array.from(req.backend.app.resources) | |||
.find((r) => r.state.routeName === resourceRouteName) as BackendResource | undefined; | |||
.find((r) => r.state.routeName === resourceRouteName) as Resource | undefined; | |||
if (typeof resource !== 'undefined') { | |||
req.backend.dataSource.prepareResource(resource); | |||
req.resource = resource; | |||
req.resource.dataSource = req.backend.dataSource(req.resource) as DataSource; | |||
req.resourceId = resourceId; | |||
if (resourceId?.trim().length > 0) { | |||
req.resourceId = resourceId; | |||
} | |||
} | |||
return req; | |||
@@ -2,7 +2,6 @@ import { constants } from 'http2'; | |||
import * as v from 'valibot'; | |||
import {HttpMiddlewareError, PlainResponse, Middleware} from './server'; | |||
import {LinkMap} from './utils'; | |||
import {BackendResource} from '../core'; | |||
export const handleGetRoot: Middleware = (req) => { | |||
const { backend, basePath } = req; | |||
@@ -41,12 +40,19 @@ export const handleGetRoot: Middleware = (req) => { | |||
}; | |||
export const handleGetCollection: Middleware = async (req) => { | |||
const { query, resource: resourceRaw, backend } = req; | |||
const { query, resource, backend } = req; | |||
if (typeof resourceRaw === 'undefined') { | |||
throw new Error('No resource'); | |||
if (typeof resource === 'undefined') { | |||
throw new HttpMiddlewareError('resourceNotFound', { | |||
statusCode: constants.HTTP_STATUS_NOT_FOUND, | |||
}); | |||
} | |||
if (typeof resource.dataSource === 'undefined') { | |||
throw new HttpMiddlewareError('unableToInitializeResourceDataSource', { | |||
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, | |||
}); | |||
} | |||
const resource = resourceRaw as BackendResource; | |||
let data: v.Output<typeof resource.schema>[]; | |||
let totalItemCount: number | undefined; | |||
@@ -80,12 +86,19 @@ export const handleGetCollection: Middleware = async (req) => { | |||
}; | |||
export const handleGetItem: Middleware = async (req) => { | |||
const { resource: resourceRaw, resourceId } = req; | |||
const { resource, resourceId } = req; | |||
if (typeof resourceRaw === 'undefined') { | |||
throw new Error('No resource'); | |||
if (typeof resource === 'undefined') { | |||
throw new HttpMiddlewareError('resourceNotFound', { | |||
statusCode: constants.HTTP_STATUS_NOT_FOUND, | |||
}); | |||
} | |||
if (typeof resource.dataSource === 'undefined') { | |||
throw new HttpMiddlewareError('unableToInitializeResourceDataSource', { | |||
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, | |||
}); | |||
} | |||
const resource = resourceRaw as BackendResource; | |||
if (typeof resourceId === 'undefined') { | |||
throw new HttpMiddlewareError( | |||
@@ -135,12 +148,19 @@ export const handleGetItem: Middleware = async (req) => { | |||
}; | |||
export const handleDeleteItem: Middleware = async (req) => { | |||
const { resource: resourceRaw, resourceId, backend } = req; | |||
const { resource, resourceId, backend } = req; | |||
if (typeof resourceRaw === 'undefined') { | |||
throw new Error('No resource'); | |||
if (typeof resource === 'undefined') { | |||
throw new HttpMiddlewareError('resourceNotFound', { | |||
statusCode: constants.HTTP_STATUS_NOT_FOUND, | |||
}); | |||
} | |||
if (typeof resource.dataSource === 'undefined') { | |||
throw new HttpMiddlewareError('unableToInitializeResourceDataSource', { | |||
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, | |||
}); | |||
} | |||
const resource = resourceRaw as BackendResource; | |||
if (typeof resourceId === 'undefined') { | |||
throw new HttpMiddlewareError( | |||
@@ -161,7 +181,7 @@ export const handleDeleteItem: Middleware = async (req) => { | |||
}); | |||
} | |||
if (!existing && backend!.throws404OnDeletingNotFound) { | |||
if (!existing && backend!.throwsErrorOnDeletingNotFound) { | |||
throw new HttpMiddlewareError('deleteNonExistingResource', { | |||
statusCode: constants.HTTP_STATUS_NOT_FOUND | |||
}); | |||
@@ -185,12 +205,19 @@ export const handleDeleteItem: Middleware = async (req) => { | |||
}; | |||
export const handlePatchItem: Middleware = async (req) => { | |||
const { resource: resourceRaw, resourceId, body } = req; | |||
const { resource, resourceId, body } = req; | |||
if (typeof resourceRaw === 'undefined') { | |||
throw new Error('No resource'); | |||
if (typeof resource === 'undefined') { | |||
throw new HttpMiddlewareError('resourceNotFound', { | |||
statusCode: constants.HTTP_STATUS_NOT_FOUND, | |||
}); | |||
} | |||
if (typeof resource.dataSource === 'undefined') { | |||
throw new HttpMiddlewareError('unableToInitializeResourceDataSource', { | |||
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, | |||
}); | |||
} | |||
const resource = resourceRaw as BackendResource; | |||
if (typeof resourceId === 'undefined') { | |||
throw new HttpMiddlewareError( | |||
@@ -232,24 +259,37 @@ export const handlePatchItem: Middleware = async (req) => { | |||
statusMessage: 'resourcePatched', | |||
body: newObject, | |||
}); | |||
// TODO finish the rest of the handlers!!! | |||
}; | |||
export const handleCreateItem: Middleware = async (req) => { | |||
const { resource: resourceRaw, body, backend, basePath } = req; | |||
const { resource, body, backend, basePath } = req; | |||
if (typeof resource === 'undefined') { | |||
throw new HttpMiddlewareError('resourceNotFound', { | |||
statusCode: constants.HTTP_STATUS_NOT_FOUND, | |||
}); | |||
} | |||
if (typeof resourceRaw === 'undefined') { | |||
throw new Error('No resource'); | |||
if (typeof resource.dataSource === 'undefined') { | |||
throw new HttpMiddlewareError('unableToInitializeResourceDataSource', { | |||
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, | |||
}); | |||
} | |||
const idAttrRaw = resource.state.shared.get('idAttr'); | |||
if (typeof idAttrRaw === 'undefined') { | |||
throw new HttpMiddlewareError('unableToInitializeResourceDataSource', { | |||
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, | |||
}); | |||
} | |||
const resource = resourceRaw as BackendResource; | |||
const idAttr = idAttrRaw as string; | |||
let newId; | |||
let params: v.Output<typeof resource.schema>; | |||
try { | |||
newId = await resource.newId(resource.dataSource); | |||
newId = await resource.dataSource.newId(); | |||
params = { ...body as Record<string, unknown> }; | |||
params[resource.state.idAttr] = newId; | |||
params[idAttr] = newId; | |||
} catch (cause) { | |||
throw new HttpMiddlewareError('unableToGenerateIdFromResourceDataSource', { | |||
cause, | |||
@@ -296,18 +336,33 @@ export const handleCreateItem: Middleware = async (req) => { | |||
} | |||
export const handleEmplaceItem: Middleware = async (req) => { | |||
const { resource: resourceRaw, resourceId, basePath, body, backend } = req; | |||
const { resource, resourceId, basePath, body, backend } = req; | |||
if (typeof resource === 'undefined') { | |||
throw new HttpMiddlewareError('resourceNotFound', { | |||
statusCode: constants.HTTP_STATUS_NOT_FOUND, | |||
}); | |||
} | |||
if (typeof resourceRaw === 'undefined') { | |||
throw new Error('No resource'); | |||
if (typeof resource.dataSource === 'undefined') { | |||
throw new HttpMiddlewareError('unableToInitializeResourceDataSource', { | |||
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, | |||
}); | |||
} | |||
const idAttrRaw = resource.state.shared.get('idAttr'); | |||
if (typeof idAttrRaw === 'undefined') { | |||
throw new HttpMiddlewareError('unableToInitializeResourceDataSource', { | |||
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, | |||
}); | |||
} | |||
const resource = resourceRaw as BackendResource; | |||
const idAttr = idAttrRaw as string; | |||
let newObject: v.Output<typeof resource.schema>; | |||
let isCreated: boolean; | |||
try { | |||
const params = { ...body as Record<string, unknown> }; | |||
params[resource.state.idAttr] = resource.state.idConfig.deserialize(params[resource.state.idAttr] as string); | |||
params[idAttr] = resourceId; | |||
[newObject, isCreated] = await resource.dataSource.emplace(resourceId!, params); | |||
} catch (cause) { | |||
throw new HttpMiddlewareError('unableToEmplaceResource', { | |||
@@ -13,9 +13,6 @@ import { | |||
handleGetRoot, | |||
handlePatchItem, | |||
} from './handlers'; | |||
import { | |||
BackendResource, | |||
} from '../core'; | |||
import {getBody} from './utils'; | |||
import {decorateRequestWithBackend} from './decorators/backend'; | |||
import {decorateRequestWithMethod} from './decorators/method'; | |||
@@ -94,14 +91,18 @@ export interface Middleware<Req extends RequestContext = RequestContext> { | |||
(req: Req): undefined | Response | Promise<undefined | Response>; | |||
} | |||
const getAllowedMiddlewares = <T extends v.BaseSchema>(resource?: Resource<T>, mainResourceId = '') => { | |||
const getAllowedMiddlewares = <T extends v.BaseSchema>(resource?: Resource<T>, mainResourceId?: string) => { | |||
const middlewares = [] as [string, Middleware, v.BaseSchema?][]; | |||
if (typeof resource === 'undefined') { | |||
return middlewares; | |||
} | |||
if (mainResourceId === '') { | |||
if (typeof resource.dataSource === 'undefined') { | |||
return middlewares; | |||
} | |||
if (typeof mainResourceId !== 'string') { | |||
if (resource.state.canFetchCollection) { | |||
middlewares.push(['GET', handleGetCollection]); | |||
} | |||
@@ -116,14 +117,16 @@ const getAllowedMiddlewares = <T extends v.BaseSchema>(resource?: Resource<T>, m | |||
} | |||
if (resource.state.canEmplace) { | |||
const schema = resource.schema.type === 'object' ? resource.schema as unknown as v.ObjectSchema<any> : resource.schema; | |||
const idAttr = resource.state.shared.get('idAttr') as string; | |||
const idConfig = resource.state.shared.get('idConfig') as any; | |||
const putSchema = ( | |||
schema.type === 'object' | |||
? v.merge([ | |||
schema as v.ObjectSchema<any>, | |||
v.object({ | |||
[resource.state.idAttr]: v.transform( | |||
[idAttr]: v.transform( | |||
v.any(), | |||
input => resource.state.idConfig.serialize(input), | |||
input => idConfig!.serialize(input), | |||
v.literal(mainResourceId) | |||
) | |||
}) | |||
@@ -178,13 +181,18 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||
if (typeof req.resource === 'undefined') { | |||
throw new HttpMiddlewareError('resourceNotFound', { | |||
statusCode: constants.HTTP_STATUS_NOT_FOUND | |||
statusCode: constants.HTTP_STATUS_NOT_FOUND, | |||
}); | |||
} | |||
if (typeof req.resource.dataSource === 'undefined') { | |||
throw new HttpMiddlewareError('unableToInitializeResourceDataSource', { | |||
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, | |||
}); | |||
} | |||
const resource = req.resource as BackendResource; | |||
try { | |||
await resource.dataSource.initialize(); | |||
await req.resource.dataSource.initialize(); | |||
} catch (cause) { | |||
throw new HttpMiddlewareError( | |||
'unableToInitializeResourceDataSource', | |||
@@ -211,8 +219,6 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||
} | |||
if (schema) { | |||
const availableSerializers = Array.from(req.backend.app.mediaTypes.values()); | |||
const availableCharsets = Array.from(req.backend.app.charsets.values()); | |||
const contentTypeHeader = req.headers['content-type'] ?? 'application/octet-stream'; | |||
const fragments = contentTypeHeader.split(';'); | |||
const mediaType = fragments[0]; | |||
@@ -227,9 +233,22 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||
? charsetRaw.slice(1, -1).trim() | |||
: charsetRaw.trim() | |||
) | |||
const deserializerPair = availableSerializers.find((l) => l.name === mediaType); | |||
const encodingPair = availableCharsets.find((l) => l.name === charset); | |||
(req as unknown as Record<string, unknown>).body = await getBody(req, schema, encodingPair, deserializerPair); | |||
const theBodyBuffer = await getBody(req); | |||
const encodingPair = req.backend.app.charsets.get(charset); | |||
if (typeof encodingPair === 'undefined') { | |||
throw new HttpMiddlewareError('unableToDecodeResource', { | |||
statusCode: constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, | |||
}); | |||
} | |||
const deserializerPair = req.backend.app.mediaTypes.get(mediaType); | |||
if (typeof deserializerPair === 'undefined') { | |||
throw new HttpMiddlewareError('unableToDeserializeResource', { | |||
statusCode: constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE, | |||
}); | |||
} | |||
const theBodyStr = encodingPair.decode(theBodyBuffer); | |||
const theBody = deserializerPair.deserialize(theBodyStr); | |||
req.body = await v.parseAsync(schema, theBody, { abortEarly: false, abortPipeEarly: false }); | |||
} | |||
const result = await middleware(req); | |||
@@ -284,7 +303,7 @@ export const createServer = (backendState: BackendState, serverParams = {} as Cr | |||
const handleRequest = async (reqRaw: RequestContext, res: http.ServerResponse<RequestContext>) => { | |||
const req = await decorateRequest(reqRaw); | |||
const middlewares = getAllowedMiddlewares(req.resource, req.resourceId ?? ''); | |||
const middlewares = getAllowedMiddlewares(req.resource, req.resourceId); | |||
const processRequestFn = processRequest(middlewares); | |||
let middlewareState: Response; | |||
try { | |||
@@ -1,30 +1,19 @@ | |||
import {IncomingMessage} from 'http'; | |||
import {MediaType, Charset} from '../../common'; | |||
import {BaseSchema, parseAsync} from 'valibot'; | |||
export const getBody = ( | |||
req: IncomingMessage, | |||
schema: BaseSchema, | |||
encodingPair?: Charset, | |||
deserializer?: MediaType, | |||
) => new Promise((resolve, reject) => { | |||
) => new Promise<Buffer>((resolve, reject) => { | |||
let body = Buffer.from(''); | |||
req.on('data', (chunk) => { | |||
body = Buffer.concat([body, chunk]); | |||
}); | |||
req.on('end', async () => { | |||
const bodyStr = encodingPair?.decode(body) ?? body.toString(); | |||
try { | |||
const bodyDeserialized = await parseAsync( | |||
schema, | |||
deserializer?.deserialize(bodyStr) ?? body, | |||
{abortEarly: false}, | |||
); | |||
resolve(bodyDeserialized); | |||
} catch (err) { | |||
reject(err); | |||
} | |||
resolve(body); | |||
}); | |||
req.on('error', (err) => { | |||
reject(err); | |||
}) | |||
}); | |||
interface LinkMapEntry { | |||
@@ -24,7 +24,11 @@ export interface ApplicationBuilder { | |||
mediaType(mediaType: MediaType): this; | |||
language(language: Language): this; | |||
charset(charset: Charset): this; | |||
resource<T extends v.BaseSchema>(resRaw: Resource<T>): this; | |||
resource< | |||
Schema extends v.BaseSchema, | |||
CurrentItemName extends string = string, | |||
CurrentRouteName extends string = string | |||
>(resRaw: Resource<Schema, CurrentItemName, CurrentRouteName>): this; | |||
createBackend(params: Omit<CreateBackendParams, 'app'>): BackendBuilder; | |||
createClient(params: Omit<CreateClientParams, 'app'>): ClientBuilder; | |||
} | |||
@@ -22,6 +22,8 @@ export const LANGUAGE_DEFAULT_STATUS_MESSAGE_KEYS = [ | |||
'unableToSerializeResponse', | |||
'unableToEncodeResponse', | |||
'unableToDeleteResource', | |||
'unableToDeserializeResource', | |||
'unableToDecodeResource', | |||
'resourceDeleted', | |||
'unableToDeserializeRequest', | |||
'patchNonExistingResource', | |||
@@ -64,6 +66,8 @@ export const FALLBACK_LANGUAGE = { | |||
unableToDeleteResource: 'Unable To Delete $RESOURCE', | |||
languageNotAcceptable: 'Language Not Acceptable', | |||
encodingNotAcceptable: 'Encoding Not Acceptable', | |||
unableToDeserializeResource: 'Unable To Deserialize $RESOURCE', | |||
unableToDecodeResource: 'Unable To Decode $RESOURCE', | |||
mediaTypeNotAcceptable: 'Media Type Not Acceptable', | |||
methodNotAllowed: 'Method Not Allowed', | |||
urlNotFound: 'URL Not Found', | |||
@@ -1,24 +1,12 @@ | |||
import * as v from 'valibot'; | |||
import {BaseDataSource, GenerationStrategy} from './data-source'; | |||
export interface ResourceIdConfig<IdSchema extends v.BaseSchema, DataSource extends BaseDataSource = BaseDataSource> { | |||
generationStrategy: GenerationStrategy<DataSource>; | |||
serialize: (id: unknown) => string; | |||
deserialize: (id: string) => v.Output<IdSchema>; | |||
schema: IdSchema; | |||
} | |||
export interface ResourceState< | |||
ItemName extends string = string, | |||
RouteName extends string = string, | |||
IdAttr extends string = string, | |||
IdSchema extends v.BaseSchema = v.BaseSchema | |||
RouteName extends string = string | |||
> { | |||
idAttr: IdAttr; | |||
shared: Map<string, unknown>; | |||
itemName: ItemName; | |||
routeName: RouteName; | |||
idConfig: ResourceIdConfig<IdSchema>; | |||
fullTextAttrs: Set<string>; | |||
canCreate: boolean; | |||
canFetchCollection: boolean; | |||
canFetchItem: boolean; | |||
@@ -30,20 +18,12 @@ export interface ResourceState< | |||
export interface Resource< | |||
Schema extends v.BaseSchema = v.BaseSchema, | |||
CurrentName extends string = string, | |||
CurrentRouteName extends string = string, | |||
CurrentIdAttr extends string = string, | |||
IdSchema extends v.BaseSchema = v.BaseSchema | |||
CurrentRouteName extends string = string | |||
> { | |||
dataSource?: unknown; | |||
schema: Schema; | |||
state: ResourceState<CurrentName, CurrentRouteName, CurrentIdAttr, IdSchema>; | |||
id<NewIdAttr extends CurrentIdAttr, TheIdSchema extends v.BaseSchema>( | |||
newIdAttr: NewIdAttr, | |||
params: ResourceIdConfig<TheIdSchema> | |||
): Resource<Schema, CurrentName, CurrentRouteName, NewIdAttr, TheIdSchema>; | |||
fullText(fullTextAttr: string): this; | |||
name<NewName extends CurrentName>(n: NewName): Resource<Schema, NewName, CurrentRouteName, CurrentIdAttr, IdSchema>; | |||
route<NewRouteName extends CurrentRouteName>(n: NewRouteName): Resource<Schema, CurrentName, NewRouteName, CurrentIdAttr, IdSchema>; | |||
state: ResourceState<CurrentName, CurrentRouteName>; | |||
name<NewName extends CurrentName>(n: NewName): Resource<Schema, NewName, CurrentRouteName>; | |||
route<NewRouteName extends CurrentRouteName>(n: NewRouteName): Resource<Schema, CurrentName, NewRouteName>; | |||
canFetchCollection(b?: boolean): this; | |||
canFetchItem(b?: boolean): this; | |||
canCreate(b?: boolean): this; | |||
@@ -55,25 +35,23 @@ export interface Resource< | |||
export const resource = < | |||
Schema extends v.BaseSchema, | |||
CurrentName extends string = string, | |||
CurrentRouteName extends string = string, | |||
CurrentIdAttr extends string = string, | |||
IdSchema extends v.BaseSchema = v.BaseSchema | |||
>(schema: Schema): Resource<Schema, CurrentName, CurrentRouteName, CurrentIdAttr, IdSchema> => { | |||
CurrentRouteName extends string = string | |||
>(schema: Schema): Resource<Schema, CurrentName, CurrentRouteName> => { | |||
const resourceState = { | |||
fullTextAttrs: new Set<string>(), | |||
shared: new Map(), | |||
canCreate: false, | |||
canFetchCollection: false, | |||
canFetchItem: false, | |||
canPatch: false, | |||
canEmplace: false, | |||
canDelete: false, | |||
} as ResourceState<CurrentName, CurrentRouteName, CurrentIdAttr, IdSchema>; | |||
} as ResourceState<CurrentName, CurrentRouteName>; | |||
return { | |||
get state(): ResourceState<CurrentName, CurrentRouteName, CurrentIdAttr, IdSchema> { | |||
get state(): ResourceState<CurrentName, CurrentRouteName> { | |||
return Object.freeze({ | |||
...resourceState | |||
}) as unknown as ResourceState<CurrentName, CurrentRouteName, CurrentIdAttr, IdSchema>; | |||
}) as unknown as ResourceState<CurrentName, CurrentRouteName>; | |||
}, | |||
canFetchCollection(b = true) { | |||
resourceState.canFetchCollection = b; | |||
@@ -99,42 +77,24 @@ export const resource = < | |||
resourceState.canDelete = b; | |||
return this; | |||
}, | |||
id<NewIdAttr extends CurrentIdAttr, NewIdSchema extends IdSchema>(newIdAttr: NewIdAttr, params: ResourceIdConfig<NewIdSchema>) { | |||
resourceState.idAttr = newIdAttr; | |||
resourceState.idConfig = params; | |||
return this as Resource<Schema, CurrentName, CurrentRouteName, NewIdAttr, NewIdSchema>; | |||
}, | |||
newId(dataSource: BaseDataSource) { | |||
return resourceState?.idConfig?.generationStrategy?.(dataSource); | |||
id(idName, config) { | |||
resourceState.shared.set('idAttr', idName); | |||
resourceState.shared.set('idConfig', config); | |||
return this; | |||
}, | |||
fullText(fullTextAttr: string) { | |||
if ( | |||
schema.type === 'object' | |||
&& ( | |||
schema as unknown as v.ObjectSchema< | |||
Record<string, v.BaseSchema>, | |||
undefined, | |||
Record<string, string> | |||
> | |||
) | |||
.entries[fullTextAttr]?.type === 'string' | |||
) { | |||
resourceState.fullTextAttrs?.add(fullTextAttr); | |||
return this; | |||
} | |||
throw new Error(`Could not set attribute ${fullTextAttr} as fulltext.`); | |||
fullText(attrName) { | |||
const fullTextAttrs = (resourceState.shared.get('fullText') ?? new Set()) as Set<string>; | |||
fullTextAttrs.add(attrName); | |||
resourceState.shared.set('fullText', fullTextAttrs); | |||
return this; | |||
}, | |||
name<NewName extends CurrentName>(n: NewName) { | |||
resourceState.itemName = n; | |||
return this as Resource<Schema, NewName, CurrentRouteName, CurrentIdAttr, IdSchema>; | |||
return this as Resource<Schema, NewName, CurrentRouteName>; | |||
}, | |||
route<NewRouteName extends CurrentRouteName>(n: NewRouteName) { | |||
resourceState.routeName = n; | |||
return this as Resource<Schema, CurrentName, NewRouteName, CurrentIdAttr, IdSchema>; | |||
}, | |||
get idAttr() { | |||
return resourceState.idAttr; | |||
return this as Resource<Schema, CurrentName, NewRouteName>; | |||
}, | |||
get itemName() { | |||
return resourceState.itemName; | |||
@@ -145,9 +105,7 @@ export const resource = < | |||
get schema() { | |||
return schema; | |||
}, | |||
} as Resource<Schema, CurrentName, CurrentRouteName, CurrentIdAttr, IdSchema>; | |||
} as Resource<Schema, CurrentName, CurrentRouteName>; | |||
}; | |||
export type ResourceType<R extends Resource> = v.Output<R['schema']>; | |||
export type ResourceTypeWithId<R extends Resource> = ResourceType<R> & Record<R['state']['idAttr'], v.Output<R['state']['idConfig']['schema']>>; |
@@ -25,9 +25,11 @@ import { dataSources } from '../../src/backend'; | |||
import { application, resource, validation as v, Resource } from '../../src'; | |||
const PORT = 3000; | |||
const HOST = 'localhost'; | |||
const ACCEPT_CHARSET = 'utf-8'; | |||
const HOST = '127.0.0.1'; | |||
const ACCEPT = 'application/json'; | |||
const ACCEPT_LANGUAGE = 'en'; | |||
const CONTENT_TYPE_CHARSET = 'utf-8'; | |||
const CONTENT_TYPE = ACCEPT; | |||
const autoIncrement = async (dataSource: DataSource) => { | |||
const data = await dataSource.getMultiple() as Record<string, string>[]; | |||
@@ -74,7 +76,7 @@ describe('yasumi', () => { | |||
.name('Piano' as const) | |||
.route('pianos' as const) | |||
.id('id' as const, { | |||
generationStrategy: autoIncrement as any, | |||
generationStrategy: autoIncrement, | |||
serialize: (id) => id?.toString() ?? '0', | |||
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, | |||
schema: v.number(), | |||
@@ -90,7 +92,7 @@ describe('yasumi', () => { | |||
const backend = app | |||
.createBackend({ | |||
dataSource: (resource) => new dataSources.jsonlFile.DataSource(resource, baseDir), | |||
dataSource: new dataSources.jsonlFile.DataSource(baseDir), | |||
}) | |||
.throwsErrorOnDeletingNotFound(); | |||
@@ -146,8 +148,8 @@ describe('yasumi', () => { | |||
path: '/api/pianos', | |||
method: 'GET', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Accept-Language': 'en', | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -165,7 +167,7 @@ describe('yasumi', () => { | |||
}); | |||
res.on('close', () => { | |||
const resBufferJson = resBuffer.toString(ACCEPT_CHARSET); | |||
const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET); | |||
const resData = JSON.parse(resBufferJson); | |||
expect(resData).toEqual([]); | |||
resolve(); | |||
@@ -190,8 +192,8 @@ describe('yasumi', () => { | |||
path: '/api/pianos', | |||
method: 'HEAD', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Accept-Language': 'en', | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -247,7 +249,8 @@ describe('yasumi', () => { | |||
path: '/api/pianos/1', | |||
method: 'GET', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -264,7 +267,7 @@ describe('yasumi', () => { | |||
}); | |||
res.on('close', () => { | |||
const resBufferJson = resBuffer.toString(ACCEPT_CHARSET); | |||
const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET); | |||
const resData = JSON.parse(resBufferJson); | |||
expect(resData).toEqual(data); | |||
resolve(); | |||
@@ -290,7 +293,8 @@ describe('yasumi', () => { | |||
path: '/api/pianos/1', | |||
method: 'HEAD', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -320,7 +324,8 @@ describe('yasumi', () => { | |||
path: '/api/pianos/2', | |||
method: 'GET', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -351,7 +356,8 @@ describe('yasumi', () => { | |||
path: '/api/pianos/2', | |||
method: 'HEAD', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -406,8 +412,9 @@ describe('yasumi', () => { | |||
path: '/api/pianos', | |||
method: 'POST', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Content-Type': ACCEPT, | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
'Content-Type': `${CONTENT_TYPE}; charset="${CONTENT_TYPE_CHARSET}"`, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -424,7 +431,7 @@ describe('yasumi', () => { | |||
}); | |||
res.on('close', () => { | |||
const resBufferJson = resBuffer.toString(ACCEPT_CHARSET); | |||
const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET); | |||
const resData = JSON.parse(resBufferJson); | |||
expect(resData).toEqual({ | |||
...newData, | |||
@@ -478,8 +485,9 @@ describe('yasumi', () => { | |||
path: `/api/pianos/${data.id}`, | |||
method: 'PATCH', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Content-Type': ACCEPT, | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
'Content-Type': `${CONTENT_TYPE}; charset="${CONTENT_TYPE_CHARSET}"`, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -496,7 +504,7 @@ describe('yasumi', () => { | |||
}); | |||
res.on('close', () => { | |||
const resBufferJson = resBuffer.toString(ACCEPT_CHARSET); | |||
const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET); | |||
const resData = JSON.parse(resBufferJson); | |||
expect(resData).toEqual({ | |||
...data, | |||
@@ -525,8 +533,9 @@ describe('yasumi', () => { | |||
path: '/api/pianos/2', | |||
method: 'PATCH', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Content-Type': ACCEPT, | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
'Content-Type': `${CONTENT_TYPE}; charset="${CONTENT_TYPE_CHARSET}"`, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -582,8 +591,9 @@ describe('yasumi', () => { | |||
path: `/api/pianos/${newData.id}`, | |||
method: 'PUT', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Content-Type': ACCEPT, | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
'Content-Type': `${CONTENT_TYPE}; charset="${CONTENT_TYPE_CHARSET}"`, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -600,7 +610,7 @@ describe('yasumi', () => { | |||
}); | |||
res.on('close', () => { | |||
const resBufferJson = resBuffer.toString(ACCEPT_CHARSET); | |||
const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET); | |||
const resData = JSON.parse(resBufferJson); | |||
expect(resData).toEqual(newData); | |||
resolve(); | |||
@@ -628,8 +638,9 @@ describe('yasumi', () => { | |||
path: `/api/pianos/${id}`, | |||
method: 'PUT', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Content-Type': ACCEPT, | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
'Content-Type': `${CONTENT_TYPE}; charset="${CONTENT_TYPE_CHARSET}"`, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -646,7 +657,7 @@ describe('yasumi', () => { | |||
}); | |||
res.on('close', () => { | |||
const resBufferJson = resBuffer.toString(ACCEPT_CHARSET); | |||
const resBufferJson = resBuffer.toString(CONTENT_TYPE_CHARSET); | |||
const resData = JSON.parse(resBufferJson); | |||
expect(resData).toEqual({ | |||
...newData, | |||
@@ -698,7 +709,8 @@ describe('yasumi', () => { | |||
path: `/api/pianos/${data.id}`, | |||
method: 'DELETE', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
}, | |||
}, | |||
(res) => { | |||
@@ -728,7 +740,8 @@ describe('yasumi', () => { | |||
path: '/api/pianos/2', | |||
method: 'DELETE', | |||
headers: { | |||
'Accept': `${ACCEPT}; charset="${ACCEPT_CHARSET}"`, | |||
'Accept': ACCEPT, | |||
'Accept-Language': ACCEPT_LANGUAGE, | |||
}, | |||
}, | |||
(res) => { | |||