Provide utility types for resource schema validation. Also handle each step for POST and PUT handlers.master
@@ -1,7 +1,7 @@ | |||||
import { | import { | ||||
application, | application, | ||||
resource, | resource, | ||||
valibot as v, | |||||
validation as v, | |||||
serializers, | serializers, | ||||
encodings, | encodings, | ||||
} from '../../src'; | } from '../../src'; | ||||
@@ -38,14 +38,14 @@ const User = resource(v.object( | |||||
}, | }, | ||||
v.never() | v.never() | ||||
)) | )) | ||||
.name('User') | |||||
.fullText('bio') | |||||
.id('id', { | |||||
.id('id' as const, { | |||||
generationStrategy: autoIncrement, | generationStrategy: autoIncrement, | ||||
serialize: (id) => id?.toString() ?? '0', | serialize: (id) => id?.toString() ?? '0', | ||||
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, | deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, | ||||
schema: v.number(), | schema: v.number(), | ||||
}); | |||||
}) | |||||
.name('User') | |||||
.fullText('bio'); | |||||
const app = application({ | const app = application({ | ||||
name: 'piano-service', | name: 'piano-service', | ||||
@@ -2,7 +2,7 @@ import * as http from 'http'; | |||||
import * as https from 'https'; | import * as https from 'https'; | ||||
import { constants } from 'http2'; | import { constants } from 'http2'; | ||||
import { pluralize } from 'inflection'; | import { pluralize } from 'inflection'; | ||||
import {BaseSchema, ObjectSchema, Output} from 'valibot'; | |||||
import * as v from 'valibot'; | |||||
import { SerializerPair } from './serializers'; | import { SerializerPair } from './serializers'; | ||||
import { | import { | ||||
handleCreateItem, handleDeleteItem, handleEmplaceItem, | handleCreateItem, handleDeleteItem, handleEmplaceItem, | ||||
@@ -20,11 +20,12 @@ import * as applicationJson from './serializers/application/json'; | |||||
// TODO separate frontend and backend factory methods | // TODO separate frontend and backend factory methods | ||||
export interface DataSource<T = object> { | |||||
export interface DataSource<T = object, Q = object> { | |||||
initialize(): Promise<unknown>; | initialize(): Promise<unknown>; | ||||
getTotalCount?(): Promise<number>; | |||||
getMultiple(): Promise<T[]>; | |||||
getSingle(id: string): Promise<T | null>; | |||||
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>; | create(data: T): Promise<T>; | ||||
delete(id: string): Promise<unknown>; | delete(id: string): Promise<unknown>; | ||||
emplace(id: string, data: T): Promise<[T, boolean]>; | emplace(id: string, data: T): Promise<[T, boolean]>; | ||||
@@ -36,12 +37,12 @@ export interface ApplicationParams { | |||||
dataSource?: (resource: Resource) => DataSource; | dataSource?: (resource: Resource) => DataSource; | ||||
} | } | ||||
interface ResourceState<T extends BaseSchema> { | |||||
idAttr: string; | |||||
interface ResourceState<IdAttr extends string = string, IdSchema extends v.BaseSchema = v.BaseSchema> { | |||||
idAttr: IdAttr; | |||||
itemName: string; | itemName: string; | ||||
collectionName: string; | collectionName: string; | ||||
routeName: string; | routeName: string; | ||||
idConfig: ResourceIdConfig<T>; | |||||
idConfig: ResourceIdConfig<IdSchema>; | |||||
fullTextAttrs: Set<string>; | fullTextAttrs: Set<string>; | ||||
canCreate: boolean; | canCreate: boolean; | ||||
canFetchCollection: boolean; | canFetchCollection: boolean; | ||||
@@ -51,11 +52,18 @@ interface ResourceState<T extends BaseSchema> { | |||||
canDelete: boolean; | canDelete: boolean; | ||||
} | } | ||||
export interface Resource<T extends BaseSchema = any, IdSchema extends BaseSchema = any> { | |||||
export interface Resource< | |||||
ResourceSchema extends v.BaseSchema = v.BaseSchema, | |||||
IdAttr extends string = string, | |||||
IdSchema extends v.BaseSchema = v.BaseSchema | |||||
> { | |||||
newId(dataSource: DataSource): string | number | unknown; | newId(dataSource: DataSource): string | number | unknown; | ||||
schema: T; | |||||
state: ResourceState<IdSchema>; | |||||
id(newIdAttr: string, params: ResourceIdConfig<IdSchema>): this; | |||||
schema: ResourceSchema; | |||||
state: ResourceState<IdAttr, IdSchema>; | |||||
id<NewIdAttr extends IdAttr, TheIdSchema extends v.BaseSchema>( | |||||
newIdAttr: NewIdAttr, | |||||
params: ResourceIdConfig<TheIdSchema> | |||||
): Resource<ResourceSchema, NewIdAttr, TheIdSchema>; | |||||
fullText(fullTextAttr: string): this; | fullText(fullTextAttr: string): this; | ||||
name(n: string): this; | name(n: string): this; | ||||
collection(n: string): this; | collection(n: string): this; | ||||
@@ -68,7 +76,7 @@ export interface Resource<T extends BaseSchema = any, IdSchema extends BaseSchem | |||||
canDelete(b?: boolean): this; | canDelete(b?: boolean): this; | ||||
} | } | ||||
export interface ResourceWithDataSource<T extends BaseSchema = any> extends Resource<T> { | |||||
export interface ResourceWithDataSource<T extends v.BaseSchema> extends Resource<T> { | |||||
dataSource: DataSource; | dataSource: DataSource; | ||||
} | } | ||||
@@ -76,14 +84,14 @@ interface GenerationStrategy { | |||||
(dataSource: DataSource, ...args: unknown[]): Promise<string | number | unknown>; | (dataSource: DataSource, ...args: unknown[]): Promise<string | number | unknown>; | ||||
} | } | ||||
interface ResourceIdConfig<T extends BaseSchema> { | |||||
interface ResourceIdConfig<IdSchema extends v.BaseSchema> { | |||||
generationStrategy: GenerationStrategy; | generationStrategy: GenerationStrategy; | ||||
serialize: (id: unknown) => string; | serialize: (id: unknown) => string; | ||||
deserialize: (id: string) => Output<T>; | |||||
schema: T; | |||||
deserialize: (id: string) => v.Output<IdSchema>; | |||||
schema: IdSchema; | |||||
} | } | ||||
const getAllowedMiddlewares = (resource: Resource, mainResourceId: string) => { | |||||
const getAllowedMiddlewares = <T extends v.BaseSchema>(resource: Resource<T>, mainResourceId: string) => { | |||||
const middlewares = [] as [string, Middleware][]; | const middlewares = [] as [string, Middleware][]; | ||||
if (mainResourceId === '') { | if (mainResourceId === '') { | ||||
if (resource.state.canFetchCollection) { | if (resource.state.canFetchCollection) { | ||||
@@ -111,7 +119,11 @@ const getAllowedMiddlewares = (resource: Resource, mainResourceId: string) => { | |||||
return middlewares; | return middlewares; | ||||
}; | }; | ||||
export const resource = <T extends BaseSchema, Id extends BaseSchema = any>(schema: T): Resource<T, Id> => { | |||||
export const resource = < | |||||
ResourceSchema extends v.BaseSchema, | |||||
IdAttr extends string = string, | |||||
IdSchema extends v.BaseSchema = v.BaseSchema | |||||
>(schema: ResourceSchema): Resource<ResourceSchema, IdAttr, IdSchema> => { | |||||
const resourceState = { | const resourceState = { | ||||
fullTextAttrs: new Set<string>(), | fullTextAttrs: new Set<string>(), | ||||
canCreate: false, | canCreate: false, | ||||
@@ -120,13 +132,13 @@ export const resource = <T extends BaseSchema, Id extends BaseSchema = any>(sche | |||||
canPatch: false, | canPatch: false, | ||||
canEmplace: false, | canEmplace: false, | ||||
canDelete: false, | canDelete: false, | ||||
} as Partial<ResourceState<Id>>; | |||||
} as ResourceState<IdAttr, IdSchema>; | |||||
return { | return { | ||||
get state(): ResourceState<Id> { | |||||
get state(): ResourceState<IdAttr, IdSchema> { | |||||
return Object.freeze({ | return Object.freeze({ | ||||
...resourceState | ...resourceState | ||||
}) as unknown as ResourceState<Id>; | |||||
}) as unknown as ResourceState<IdAttr, IdSchema>; | |||||
}, | }, | ||||
canFetchCollection(b = true) { | canFetchCollection(b = true) { | ||||
resourceState.canFetchCollection = b; | resourceState.canFetchCollection = b; | ||||
@@ -152,10 +164,10 @@ export const resource = <T extends BaseSchema, Id extends BaseSchema = any>(sche | |||||
resourceState.canDelete = b; | resourceState.canDelete = b; | ||||
return this; | return this; | ||||
}, | }, | ||||
id(newIdAttr: string, params: ResourceIdConfig<Id>) { | |||||
id<NewIdAttr extends IdAttr, NewIdSchema extends IdSchema>(newIdAttr: NewIdAttr, params: ResourceIdConfig<NewIdSchema>) { | |||||
resourceState.idAttr = newIdAttr; | resourceState.idAttr = newIdAttr; | ||||
resourceState.idConfig = params; | resourceState.idConfig = params; | ||||
return this; | |||||
return this as Resource<ResourceSchema, NewIdAttr, NewIdSchema>; | |||||
}, | }, | ||||
newId(dataSource: DataSource) { | newId(dataSource: DataSource) { | ||||
return resourceState?.idConfig?.generationStrategy?.(dataSource); | return resourceState?.idConfig?.generationStrategy?.(dataSource); | ||||
@@ -164,8 +176,8 @@ export const resource = <T extends BaseSchema, Id extends BaseSchema = any>(sche | |||||
if ( | if ( | ||||
schema.type === 'object' | schema.type === 'object' | ||||
&& ( | && ( | ||||
schema as unknown as ObjectSchema< | |||||
Record<string, BaseSchema>, | |||||
schema as unknown as v.ObjectSchema< | |||||
Record<string, v.BaseSchema>, | |||||
undefined, | undefined, | ||||
Record<string, string> | Record<string, string> | ||||
> | > | ||||
@@ -207,8 +219,8 @@ export const resource = <T extends BaseSchema, Id extends BaseSchema = any>(sche | |||||
}, | }, | ||||
get schema() { | get schema() { | ||||
return schema; | return schema; | ||||
} | |||||
} as Resource; | |||||
}, | |||||
} as Resource<ResourceSchema, IdAttr, IdSchema>; | |||||
}; | }; | ||||
interface CreateServerParams { | interface CreateServerParams { | ||||
@@ -232,7 +244,7 @@ interface HandlerState { | |||||
} | } | ||||
export interface ApplicationState { | export interface ApplicationState { | ||||
resources: Set<ResourceWithDataSource>; | |||||
resources: Set<ResourceWithDataSource<any>>; | |||||
languages: Map<string, MessageCollection>; | languages: Map<string, MessageCollection>; | ||||
serializers: Map<string, SerializerPair>; | serializers: Map<string, SerializerPair>; | ||||
encodings: Map<string, EncodingPair>; | encodings: Map<string, EncodingPair>; | ||||
@@ -256,6 +268,8 @@ export interface MessageCollection { | |||||
resourceFetched(resource: Resource): string; | resourceFetched(resource: Resource): string; | ||||
resourceNotFound(resource: Resource): string; | resourceNotFound(resource: Resource): string; | ||||
deleteNonExistingResource(resource: Resource): string; | deleteNonExistingResource(resource: Resource): string; | ||||
unableToGenerateIdFromResourceDataSource(resource: Resource): string; | |||||
unableToEmplaceResource(resource: Resource): string; | |||||
unableToSerializeResponse(): string; | unableToSerializeResponse(): string; | ||||
unableToEncodeResponse(): string; | unableToEncodeResponse(): string; | ||||
unableToDeleteResource(resource: Resource): string; | unableToDeleteResource(resource: Resource): string; | ||||
@@ -293,7 +307,7 @@ export interface BackendState { | |||||
showTotalItemCountOnCreateItem: boolean; | showTotalItemCountOnCreateItem: boolean; | ||||
} | } | ||||
interface MiddlewareArgs { | |||||
interface MiddlewareArgs<T extends v.BaseSchema> { | |||||
handlerState: HandlerState; | handlerState: HandlerState; | ||||
backendState: BackendState; | backendState: BackendState; | ||||
appState: ApplicationState; | appState: ApplicationState; | ||||
@@ -305,13 +319,13 @@ interface MiddlewareArgs { | |||||
errorResponseBodyLanguage: [string, MessageCollection]; | errorResponseBodyLanguage: [string, MessageCollection]; | ||||
errorResponseBodyEncoding: [string, EncodingPair]; | errorResponseBodyEncoding: [string, EncodingPair]; | ||||
errorResponseBodyMediaType: [string, SerializerPair]; | errorResponseBodyMediaType: [string, SerializerPair]; | ||||
resource: ResourceWithDataSource; | |||||
resource: ResourceWithDataSource<T>; | |||||
resourceId: string; | resourceId: string; | ||||
query: URLSearchParams; | query: URLSearchParams; | ||||
} | } | ||||
export interface Middleware { | export interface Middleware { | ||||
(args: MiddlewareArgs): RequestListenerWithReturn<HandlerState | Promise<HandlerState>> | |||||
<T extends v.BaseSchema = v.BaseSchema>(args: MiddlewareArgs<T>): RequestListenerWithReturn<HandlerState | Promise<HandlerState>> | |||||
} | } | ||||
export interface Backend { | export interface Backend { | ||||
@@ -332,14 +346,14 @@ export interface Application { | |||||
contentType(mimeTypePrefix: string, serializerPair: SerializerPair): this; | contentType(mimeTypePrefix: string, serializerPair: SerializerPair): this; | ||||
language(languageCode: string, messageCollection: MessageCollection): this; | language(languageCode: string, messageCollection: MessageCollection): this; | ||||
encoding(encoding: string, encodingPair: EncodingPair): this; | encoding(encoding: string, encodingPair: EncodingPair): this; | ||||
resource(resRaw: Partial<Resource>): this; | |||||
resource<T extends v.BaseSchema>(resRaw: Partial<Resource<T>>): this; | |||||
createBackend(): Backend; | createBackend(): Backend; | ||||
createClient(): Client; | createClient(): Client; | ||||
} | } | ||||
export const application = (appParams: ApplicationParams): Application => { | export const application = (appParams: ApplicationParams): Application => { | ||||
const appState: ApplicationState = { | const appState: ApplicationState = { | ||||
resources: new Set<ResourceWithDataSource>(), | |||||
resources: new Set<ResourceWithDataSource<any>>(), | |||||
languages: new Map<string, MessageCollection>(), | languages: new Map<string, MessageCollection>(), | ||||
serializers: new Map<string, SerializerPair>(), | serializers: new Map<string, SerializerPair>(), | ||||
encodings: new Map<string, EncodingPair>(), | encodings: new Map<string, EncodingPair>(), | ||||
@@ -362,13 +376,13 @@ export const application = (appParams: ApplicationParams): Application => { | |||||
appState.languages.set(languageCode, messageCollection); | appState.languages.set(languageCode, messageCollection); | ||||
return this; | return this; | ||||
}, | }, | ||||
resource(resRaw: Partial<Resource>) { | |||||
const res = resRaw as Partial<ResourceWithDataSource>; | |||||
resource<T extends v.BaseSchema>(resRaw: Partial<Resource<T>>) { | |||||
const res = resRaw as Partial<ResourceWithDataSource<T>>; | |||||
res.dataSource = res.dataSource ?? appParams.dataSource?.(res as Resource); | res.dataSource = res.dataSource ?? appParams.dataSource?.(res as Resource); | ||||
if (typeof res.dataSource === 'undefined') { | if (typeof res.dataSource === 'undefined') { | ||||
throw new Error(`Resource ${res.state!.itemName} must have a data source.`); | throw new Error(`Resource ${res.state!.itemName} must have a data source.`); | ||||
} | } | ||||
appState.resources.add(res as ResourceWithDataSource); | |||||
appState.resources.add(res as ResourceWithDataSource<T>); | |||||
return this; | return this; | ||||
}, | }, | ||||
createClient(): Client { | createClient(): Client { | ||||
@@ -411,7 +425,6 @@ export const application = (appParams: ApplicationParams): Application => { | |||||
showTotalItemCountOnCreateItem: false, | showTotalItemCountOnCreateItem: false, | ||||
throws404OnDeletingNotFound: false, | throws404OnDeletingNotFound: false, | ||||
checksSerializersOnDelete: false, | checksSerializersOnDelete: false, | ||||
}; | }; | ||||
return { | return { | ||||
@@ -466,6 +479,7 @@ export const application = (appParams: ApplicationParams): Application => { | |||||
const errorEncodingKey = backendState.errorHeaders.encoding ?? backendState.fallback.encoding; | const errorEncodingKey = backendState.errorHeaders.encoding ?? backendState.fallback.encoding; | ||||
const errorEncoding = appState.encodings.get(errorEncodingKey) ?? fallbackEncoding; | const errorEncoding = appState.encodings.get(errorEncodingKey) ?? fallbackEncoding; | ||||
// TODO refactor | |||||
const [currentLanguageCode, currentLanguageMessages] = availableLanguages.find(([code]) => code === languageCandidate) ?? []; | const [currentLanguageCode, currentLanguageMessages] = availableLanguages.find(([code]) => code === languageCandidate) ?? []; | ||||
if (typeof currentLanguageCode === 'undefined' || typeof currentLanguageMessages === 'undefined') { | if (typeof currentLanguageCode === 'undefined' || typeof currentLanguageMessages === 'undefined') { | ||||
const data = errorMessageCollection.bodies.languageNotAcceptable(); | const data = errorMessageCollection.bodies.languageNotAcceptable(); | ||||
@@ -513,7 +527,7 @@ export const application = (appParams: ApplicationParams): Application => { | |||||
return; | return; | ||||
} | } | ||||
const middlewareArgs: Omit<MiddlewareArgs, 'resource' | 'resourceId'> = { | |||||
const middlewareArgs: Omit<MiddlewareArgs<never>, 'resource' | 'resourceId'> = { | |||||
handlerState: { | handlerState: { | ||||
handled: false | handled: false | ||||
}, | }, | ||||
@@ -530,13 +544,13 @@ export const application = (appParams: ApplicationParams): Application => { | |||||
errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], | errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], | ||||
}; | }; | ||||
const methodAndUrl = await handleHasMethodAndUrl(middlewareArgs as MiddlewareArgs)(req, res); | |||||
const methodAndUrl = await handleHasMethodAndUrl(middlewareArgs as MiddlewareArgs<never>)(req, res); | |||||
if (methodAndUrl.handled) { | if (methodAndUrl.handled) { | ||||
return; | return; | ||||
} | } | ||||
if (url === '/') { | if (url === '/') { | ||||
const middlewareState = await handleGetRoot(middlewareArgs as MiddlewareArgs)(req, res); | |||||
const middlewareState = await handleGetRoot(middlewareArgs as MiddlewareArgs<never>)(req, res); | |||||
if (middlewareState.handled) { | if (middlewareState.handled) { | ||||
return; | return; | ||||
} | } | ||||
@@ -29,7 +29,7 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI | |||||
return [...this.data]; | return [...this.data]; | ||||
} | } | ||||
async getSingle(idSerialized: string) { | |||||
async getById(idSerialized: string) { | |||||
const id = this.resource.state.idConfig.deserialize(idSerialized); | const id = this.resource.state.idConfig.deserialize(idSerialized); | ||||
const foundData = this.data.find((s) => s[this.resource.state.idAttr as string] === id); | const foundData = this.data.find((s) => s[this.resource.state.idAttr as string] === id); | ||||
@@ -73,7 +73,7 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI | |||||
} | } | ||||
async emplace(idSerialized: string, dataWithId: T) { | async emplace(idSerialized: string, dataWithId: T) { | ||||
const existing = await this.getSingle(idSerialized); | |||||
const existing = await this.getById(idSerialized); | |||||
const id = this.resource.state.idConfig.deserialize(idSerialized); | const id = this.resource.state.idConfig.deserialize(idSerialized); | ||||
const { [this.resource.state.idAttr]: idFromResource, ...data } = dataWithId; | const { [this.resource.state.idAttr]: idFromResource, ...data } = dataWithId; | ||||
const dataToEmplace = { | const dataToEmplace = { | ||||
@@ -100,7 +100,7 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI | |||||
} | } | ||||
async patch(idSerialized: string, data: Partial<T>) { | async patch(idSerialized: string, data: Partial<T>) { | ||||
const existing = await this.getSingle(idSerialized); | |||||
const existing = await this.getById(idSerialized); | |||||
if (!existing) { | if (!existing) { | ||||
return null; | return null; | ||||
} | } | ||||
@@ -43,13 +43,13 @@ export const handleGetRoot: Middleware = ({ | |||||
responseBodyEncoding: [encodingKey, encoding], | responseBodyEncoding: [encodingKey, encoding], | ||||
errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], | errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], | ||||
}) => (_req: IncomingMessage, res: ServerResponse) => { | }) => (_req: IncomingMessage, res: ServerResponse) => { | ||||
const singleResDatum = { | |||||
const data = { | |||||
name: appParams.name | name: appParams.name | ||||
}; | }; | ||||
let serialized; | let serialized; | ||||
try { | try { | ||||
serialized = responseBodySerializerPair.serialize(singleResDatum); | |||||
serialized = responseBodySerializerPair.serialize(data); | |||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
'Content-Language': errorLanguageCode, | 'Content-Language': errorLanguageCode, | ||||
@@ -61,9 +61,9 @@ export const handleGetRoot: Middleware = ({ | |||||
}; | }; | ||||
} | } | ||||
let theFormatted; | |||||
let encoded; | |||||
try { | try { | ||||
theFormatted = encoding.encode(serialized); | |||||
encoded = encoding.encode(serialized); | |||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
'Content-Language': errorLanguageCode, | 'Content-Language': errorLanguageCode, | ||||
@@ -99,7 +99,7 @@ export const handleGetRoot: Middleware = ({ | |||||
} | } | ||||
res.writeHead(constants.HTTP_STATUS_OK, theHeaders); | res.writeHead(constants.HTTP_STATUS_OK, theHeaders); | ||||
res.statusMessage = responseBodyMessageCollection.statusMessages.ok(); | res.statusMessage = responseBodyMessageCollection.statusMessages.ok(); | ||||
res.end(theFormatted); | |||||
res.end(encoded); | |||||
return { | return { | ||||
handled: true | handled: true | ||||
}; | }; | ||||
@@ -112,6 +112,7 @@ export const handleGetCollection: Middleware = ({ | |||||
responseBodyEncoding: [encodingKey, encoding], | responseBodyEncoding: [encodingKey, encoding], | ||||
errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], | errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection], | ||||
backendState, | backendState, | ||||
query, | |||||
}) => async (_req: IncomingMessage, res: ServerResponse) => { | }) => async (_req: IncomingMessage, res: ServerResponse) => { | ||||
try { | try { | ||||
await resource.dataSource.initialize(); | await resource.dataSource.initialize(); | ||||
@@ -126,13 +127,13 @@ export const handleGetCollection: Middleware = ({ | |||||
}; | }; | ||||
} | } | ||||
let resData: Object[]; | |||||
let data: v.Output<typeof resource.schema>[]; | |||||
let totalItemCount: number | undefined; | let totalItemCount: number | undefined; | ||||
try { | try { | ||||
// TODO querying mechanism | // TODO querying mechanism | ||||
resData = await resource.dataSource.getMultiple(); // TODO paginated responses per resource | |||||
data = await resource.dataSource.getMultiple(query); // TODO paginated responses per resource | |||||
if (backendState.showTotalItemCountOnGetCollection && typeof resource.dataSource.getTotalCount === 'function') { | if (backendState.showTotalItemCountOnGetCollection && typeof resource.dataSource.getTotalCount === 'function') { | ||||
totalItemCount = await resource.dataSource.getTotalCount(); | |||||
totalItemCount = await resource.dataSource.getTotalCount(query); | |||||
} | } | ||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
@@ -147,7 +148,7 @@ export const handleGetCollection: Middleware = ({ | |||||
let serialized; | let serialized; | ||||
try { | try { | ||||
serialized = responseBodySerializerPair.serialize(resData); | |||||
serialized = responseBodySerializerPair.serialize(data); | |||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
'Content-Language': errorLanguageCode, | 'Content-Language': errorLanguageCode, | ||||
@@ -159,9 +160,9 @@ export const handleGetCollection: Middleware = ({ | |||||
}; | }; | ||||
} | } | ||||
let theFormatted; | |||||
let encoded; | |||||
try { | try { | ||||
theFormatted = encoding.encode(serialized); | |||||
encoded = encoding.encode(serialized); | |||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
'Content-Language': errorLanguageCode, | 'Content-Language': errorLanguageCode, | ||||
@@ -185,7 +186,7 @@ export const handleGetCollection: Middleware = ({ | |||||
res.writeHead(constants.HTTP_STATUS_OK, headers); | res.writeHead(constants.HTTP_STATUS_OK, headers); | ||||
res.statusMessage = responseBodyMessageCollection.statusMessages.resourceCollectionFetched(resource); | res.statusMessage = responseBodyMessageCollection.statusMessages.resourceCollectionFetched(resource); | ||||
res.end(theFormatted); | |||||
res.end(encoded); | |||||
return { | return { | ||||
handled: true | handled: true | ||||
}; | }; | ||||
@@ -212,9 +213,9 @@ export const handleGetItem: Middleware = ({ | |||||
}; | }; | ||||
} | } | ||||
let singleResDatum: Object | null = null; | |||||
let data: v.Output<typeof resource.schema> | null = null; | |||||
try { | try { | ||||
singleResDatum = await resource.dataSource.getSingle(resourceId); | |||||
data = await resource.dataSource.getById(resourceId); | |||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
'Content-Language': errorLanguageCode, | 'Content-Language': errorLanguageCode, | ||||
@@ -228,7 +229,7 @@ export const handleGetItem: Middleware = ({ | |||||
let serialized: string | null; | let serialized: string | null; | ||||
try { | try { | ||||
serialized = singleResDatum === null ? null : responseBodySerializerPair.serialize(singleResDatum); | |||||
serialized = data === null ? null : responseBodySerializerPair.serialize(data); | |||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
'Content-Language': errorLanguageCode, | 'Content-Language': errorLanguageCode, | ||||
@@ -240,9 +241,9 @@ export const handleGetItem: Middleware = ({ | |||||
}; | }; | ||||
} | } | ||||
let theFormatted; | |||||
let encoded; | |||||
try { | try { | ||||
theFormatted = serialized === null ? null : encoding.encode(serialized); | |||||
encoded = serialized === null ? null : encoding.encode(serialized); | |||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
'Content-Language': errorLanguageCode, | 'Content-Language': errorLanguageCode, | ||||
@@ -254,14 +255,14 @@ export const handleGetItem: Middleware = ({ | |||||
}; | }; | ||||
} | } | ||||
if (theFormatted) { | |||||
if (encoded) { | |||||
res.writeHead(constants.HTTP_STATUS_OK, { | res.writeHead(constants.HTTP_STATUS_OK, { | ||||
'Content-Type': responseBodyMediaType, | 'Content-Type': responseBodyMediaType, | ||||
'Content-Language': languageCode, | 'Content-Language': languageCode, | ||||
'Content-Encoding': encodingKey, | 'Content-Encoding': encodingKey, | ||||
}); | }); | ||||
res.statusMessage = responseBodyMessageCollection.statusMessages.resourceFetched(resource) | res.statusMessage = responseBodyMessageCollection.statusMessages.resourceFetched(resource) | ||||
res.end(theFormatted); | |||||
res.end(encoded); | |||||
return { | return { | ||||
handled: true | handled: true | ||||
}; | }; | ||||
@@ -298,50 +299,50 @@ export const handleDeleteItem: Middleware = ({ | |||||
}; | }; | ||||
} | } | ||||
let response; | |||||
let existing: unknown | null; | |||||
try { | try { | ||||
response = await resource.dataSource.delete(resourceId); | |||||
existing = await resource.dataSource.getById(resourceId); | |||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
'Content-Language': errorLanguageCode, | 'Content-Language': errorLanguageCode, | ||||
}); | }); | ||||
res.statusMessage = errorMessageCollection.statusMessages.unableToDeleteResource(resource); | |||||
res.statusMessage = errorMessageCollection.statusMessages.unableToFetchResource(resource); | |||||
res.end(); | res.end(); | ||||
return { | return { | ||||
handled: true | handled: true | ||||
}; | }; | ||||
} | } | ||||
const throwOnNotFound = !response && backendState.throws404OnDeletingNotFound; | |||||
res.writeHead( | |||||
throwOnNotFound | |||||
? constants.HTTP_STATUS_NOT_FOUND | |||||
: constants.HTTP_STATUS_NO_CONTENT, | |||||
throwOnNotFound | |||||
? { | |||||
'Content-Language': errorLanguageCode, | |||||
// TODO provide more details | |||||
} | |||||
: { | |||||
'Content-Language': languageCode, | |||||
} | |||||
); | |||||
res.statusMessage = ( | |||||
throwOnNotFound | |||||
? errorMessageCollection.statusMessages.deleteNonExistingResource(resource) | |||||
: responseBodyMessageCollection.statusMessages.resourceDeleted(resource) | |||||
); | |||||
if (!existing && backendState.throws404OnDeletingNotFound) { | |||||
res.writeHead(constants.HTTP_STATUS_NOT_FOUND, { | |||||
'Content-Language': errorLanguageCode, | |||||
}); | |||||
res.statusMessage = errorMessageCollection.statusMessages.deleteNonExistingResource(resource); | |||||
res.end(); | |||||
return { | |||||
handled: true | |||||
}; | |||||
} | |||||
if (throwOnNotFound) { | |||||
// TODO provide error message | |||||
try { | |||||
if (existing) { | |||||
await resource.dataSource.delete(resourceId); | |||||
} | |||||
} catch { | |||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | |||||
'Content-Language': errorLanguageCode, | |||||
}); | |||||
res.statusMessage = errorMessageCollection.statusMessages.unableToDeleteResource(resource); | |||||
res.end(); | res.end(); | ||||
return { | return { | ||||
handled: true | handled: true | ||||
}; | }; | ||||
} | } | ||||
res.writeHead(constants.HTTP_STATUS_NO_CONTENT, { | |||||
'Content-Language': languageCode, | |||||
}); | |||||
res.statusMessage = responseBodyMessageCollection.statusMessages.resourceDeleted(resource); | |||||
res.end(); | res.end(); | ||||
return { | return { | ||||
handled: true | handled: true | ||||
@@ -384,9 +385,9 @@ export const handlePatchItem: Middleware = ({ | |||||
}; | }; | ||||
} | } | ||||
let existing: object | null; | |||||
let existing: unknown | null; | |||||
try { | try { | ||||
existing = await resource.dataSource.getSingle(resourceId); | |||||
existing = await resource.dataSource.getById(resourceId); | |||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
'Content-Language': errorLanguageCode, | 'Content-Language': errorLanguageCode, | ||||
@@ -411,7 +412,7 @@ export const handlePatchItem: Middleware = ({ | |||||
let bodyDeserialized: unknown; | let bodyDeserialized: unknown; | ||||
try { | try { | ||||
const schema = resource.schema.type === 'object' ? resource.schema as v.ObjectSchema<any> : resource.schema | |||||
const schema = resource.schema.type === 'object' ? resource.schema as unknown as v.ObjectSchema<any> : resource.schema | |||||
bodyDeserialized = await getBody( | bodyDeserialized = await getBody( | ||||
req, | req, | ||||
requestBodyDeserializerPair, | requestBodyDeserializerPair, | ||||
@@ -444,22 +445,21 @@ export const handlePatchItem: Middleware = ({ | |||||
`${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` | `${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` | ||||
)), | )), | ||||
); | ); | ||||
const theFormatted = errorEncoding.encode(serialized); | |||||
const encoded = errorEncoding.encode(serialized); | |||||
res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, { | res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, { | ||||
...headers, | ...headers, | ||||
'Content-Type': errorMediaType, | 'Content-Type': errorMediaType, | ||||
'Content-Encoding': errorEncodingKey, | 'Content-Encoding': errorEncodingKey, | ||||
}) | }) | ||||
res.statusMessage = errorMessageCollection.statusMessages.invalidResourcePatch(resource); | res.statusMessage = errorMessageCollection.statusMessages.invalidResourcePatch(resource); | ||||
res.end(theFormatted); | |||||
res.end(encoded); | |||||
return { | return { | ||||
handled: true, | handled: true, | ||||
}; | }; | ||||
} | } | ||||
const params = bodyDeserialized as Record<string, unknown>; | const params = bodyDeserialized as Record<string, unknown>; | ||||
let newObject: object | null; | |||||
let newObject: v.Output<typeof resource.schema> | null; | |||||
try { | try { | ||||
newObject = await resource.dataSource.patch(resourceId, params); | newObject = await resource.dataSource.patch(resourceId, params); | ||||
} catch { | } catch { | ||||
@@ -487,9 +487,9 @@ export const handlePatchItem: Middleware = ({ | |||||
}; | }; | ||||
} | } | ||||
let theFormatted; | |||||
let encoded; | |||||
try { | try { | ||||
theFormatted = encoding.encode(serialized); | |||||
encoded = encoding.encode(serialized); | |||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
'Content-Language': errorLanguageCode, | 'Content-Language': errorLanguageCode, | ||||
@@ -507,7 +507,7 @@ export const handlePatchItem: Middleware = ({ | |||||
'Content-Encoding': encodingKey, | 'Content-Encoding': encodingKey, | ||||
}); | }); | ||||
res.statusMessage = responseBodyMessageCollection.statusMessages.resourcePatched(resource); | res.statusMessage = responseBodyMessageCollection.statusMessages.resourcePatched(resource); | ||||
res.end(theFormatted); | |||||
res.end(encoded); | |||||
return { | return { | ||||
handled: true | handled: true | ||||
}; | }; | ||||
@@ -567,14 +567,14 @@ export const handleCreateItem: Middleware = ({ | |||||
`${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` | `${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` | ||||
)), | )), | ||||
); | ); | ||||
const theFormatted = errorEncoding.encode(serialized); | |||||
const encoded = errorEncoding.encode(serialized); | |||||
res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, { | res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, { | ||||
...headers, | ...headers, | ||||
'Content-Type': errorMediaType, | 'Content-Type': errorMediaType, | ||||
'Content-Encoding': errorEncodingKey, | 'Content-Encoding': errorEncodingKey, | ||||
}) | }) | ||||
res.statusMessage = errorMessageCollection.statusMessages.invalidResource(resource); | res.statusMessage = errorMessageCollection.statusMessages.invalidResource(resource); | ||||
res.end(theFormatted); | |||||
res.end(encoded); | |||||
return { | return { | ||||
handled: true, | handled: true, | ||||
}; | }; | ||||
@@ -593,37 +593,79 @@ export const handleCreateItem: Middleware = ({ | |||||
}; | }; | ||||
} | } | ||||
//v.Output<typeof resource.schema> | |||||
let newId; | |||||
let params: v.Output<typeof resource.schema>; | |||||
try { | try { | ||||
// TODO error handling for each process | |||||
const newId = await resource.newId(resource.dataSource); | |||||
const params = bodyDeserialized as Record<string, unknown>; | |||||
newId = await resource.newId(resource.dataSource); | |||||
params = bodyDeserialized as Record<string, unknown>; | |||||
params[resource.state.idAttr] = newId; | params[resource.state.idAttr] = newId; | ||||
const newObject = await resource.dataSource.create(params); | |||||
let totalItemCount: number | undefined; | |||||
} catch { | |||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | |||||
'Content-Language': errorLanguageCode, | |||||
}); | |||||
res.statusMessage = errorMessageCollection.statusMessages.unableToGenerateIdFromResourceDataSource(resource); | |||||
res.end(); | |||||
return { | |||||
handled: true, | |||||
}; | |||||
// noop | |||||
// TODO | |||||
} | |||||
let newObject; | |||||
let totalItemCount: number | undefined; | |||||
try { | |||||
newObject = await resource.dataSource.create(params); | |||||
if (backendState.showTotalItemCountOnCreateItem && typeof resource.dataSource.getTotalCount === 'function') { | if (backendState.showTotalItemCountOnCreateItem && typeof resource.dataSource.getTotalCount === 'function') { | ||||
totalItemCount = await resource.dataSource.getTotalCount(); | totalItemCount = await resource.dataSource.getTotalCount(); | ||||
} | } | ||||
const headers: Record<string, string> = { | |||||
'Content-Type': responseBodyMediaType, | |||||
'Content-Language': languageCode, | |||||
'Content-Encoding': encodingKey, | |||||
'Location': `${serverParams.baseUrl}/${resource.state.routeName}/${newId}` | |||||
} catch { | |||||
// noop | |||||
// TODO | |||||
} | |||||
let serialized; | |||||
try { | |||||
serialized = responseBodySerializerPair.serialize(newObject); | |||||
} catch { | |||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | |||||
'Content-Language': errorLanguageCode, | |||||
}); | |||||
res.statusMessage = errorMessageCollection.statusMessages.unableToSerializeResponse(); | |||||
res.end(); | |||||
return { | |||||
handled: true, | |||||
}; | }; | ||||
if (typeof totalItemCount !== 'undefined') { | |||||
headers['X-Resource-Total-Item-Count'] = totalItemCount.toString(); | |||||
} | |||||
const serialized = responseBodySerializerPair.serialize(newObject); | |||||
const theFormatted = encoding.encode(serialized); | |||||
res.writeHead(constants.HTTP_STATUS_CREATED, headers); | |||||
res.statusMessage = responseBodyMessageCollection.statusMessages.resourceCreated(resource); | |||||
res.end(theFormatted); | |||||
} | |||||
let encoded; | |||||
try { | |||||
encoded = encoding.encode(serialized); | |||||
} catch { | } catch { | ||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | ||||
'Content-Language': errorLanguageCode, | 'Content-Language': errorLanguageCode, | ||||
}) | |||||
res.statusMessage = `Could Not Return ${resource.state.itemName}`; | |||||
}); | |||||
res.statusMessage = errorMessageCollection.statusMessages.unableToEncodeResponse(); | |||||
res.end(); | res.end(); | ||||
return { | |||||
handled: true, | |||||
}; | |||||
} | } | ||||
const headers: Record<string, string> = { | |||||
'Content-Type': responseBodyMediaType, | |||||
'Content-Language': languageCode, | |||||
'Content-Encoding': encodingKey, | |||||
'Location': `${serverParams.baseUrl}/${resource.state.routeName}/${newId}` | |||||
}; | |||||
if (typeof totalItemCount !== 'undefined') { | |||||
headers['X-Resource-Total-Item-Count'] = totalItemCount.toString(); | |||||
} | |||||
res.writeHead(constants.HTTP_STATUS_CREATED, headers); | |||||
res.statusMessage = responseBodyMessageCollection.statusMessages.resourceCreated(resource); | |||||
res.end(encoded); | |||||
return { | return { | ||||
handled: true | handled: true | ||||
}; | }; | ||||
@@ -656,7 +698,7 @@ export const handleEmplaceItem: Middleware = ({ | |||||
let bodyDeserialized: unknown; | let bodyDeserialized: unknown; | ||||
try { | try { | ||||
const schema = resource.schema.type === 'object' ? resource.schema as v.ObjectSchema<any> : resource.schema | |||||
const schema = resource.schema.type === 'object' ? resource.schema as unknown as v.ObjectSchema<any> : resource.schema | |||||
bodyDeserialized = await getBody( | bodyDeserialized = await getBody( | ||||
req, | req, | ||||
requestBodyDeserializerPair, | requestBodyDeserializerPair, | ||||
@@ -694,14 +736,14 @@ export const handleEmplaceItem: Middleware = ({ | |||||
`${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` | `${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` | ||||
)), | )), | ||||
); | ); | ||||
const theFormatted = errorEncoding.encode(serialized); | |||||
const encoded = errorEncoding.encode(serialized); | |||||
res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, { | res.writeHead(constants.HTTP_STATUS_BAD_REQUEST, { | ||||
...headers, | ...headers, | ||||
'Content-Type': errorMediaType, | 'Content-Type': errorMediaType, | ||||
'Content-Encoding': errorEncodingKey, | 'Content-Encoding': errorEncodingKey, | ||||
}) | }) | ||||
res.statusMessage = errorMessageCollection.statusMessages.invalidResource(resource); | res.statusMessage = errorMessageCollection.statusMessages.invalidResource(resource); | ||||
res.end(theFormatted); | |||||
res.end(encoded); | |||||
return { | return { | ||||
handled: true, | handled: true, | ||||
}; | }; | ||||
@@ -720,40 +762,73 @@ export const handleEmplaceItem: Middleware = ({ | |||||
}; | }; | ||||
} | } | ||||
let newObject: v.Output<typeof resource.schema>; | |||||
let isCreated: boolean; | |||||
try { | try { | ||||
// TODO error handling for each process | |||||
const params = bodyDeserialized as Record<string, unknown>; | const params = bodyDeserialized as Record<string, unknown>; | ||||
params[resource.state.idAttr] = resource.state.idConfig.deserialize(params[resource.state.idAttr] as string); | params[resource.state.idAttr] = resource.state.idConfig.deserialize(params[resource.state.idAttr] as string); | ||||
const [newObject, isCreated] = await resource.dataSource.emplace(resourceId, params); | |||||
const serialized = responseBodySerializerPair.serialize(newObject); | |||||
const theFormatted = encoding.encode(serialized); | |||||
const headers: Record<string, string> = { | |||||
'Content-Type': responseBodyMediaType, | |||||
'Content-Language': languageCode, | |||||
'Content-Encoding': encodingKey, | |||||
[newObject, isCreated] = await resource.dataSource.emplace(resourceId, params); | |||||
} catch { | |||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | |||||
'Content-Language': errorLanguageCode, | |||||
}); | |||||
res.statusMessage = errorMessageCollection.statusMessages.unableToEmplaceResource(resource); | |||||
res.end(); | |||||
return { | |||||
handled: true | |||||
}; | }; | ||||
let totalItemCount: number | undefined; | |||||
if (backendState.showTotalItemCountOnCreateItem && typeof resource.dataSource.getTotalCount === 'function') { | |||||
totalItemCount = await resource.dataSource.getTotalCount(); | |||||
} | |||||
if (isCreated) { | |||||
headers['Location'] = `${serverParams.baseUrl}/${resource.state.routeName}/${resourceId}`; | |||||
if (typeof totalItemCount !== 'undefined') { | |||||
headers['X-Resource-Total-Item-Count'] = totalItemCount.toString(); | |||||
} | |||||
} | |||||
res.writeHead(isCreated ? constants.HTTP_STATUS_CREATED : constants.HTTP_STATUS_OK, headers); | |||||
res.statusMessage = ( | |||||
isCreated | |||||
? responseBodyMessageCollection.statusMessages.resourceCreated(resource) | |||||
: responseBodyMessageCollection.statusMessages.resourceReplaced(resource) | |||||
); | |||||
res.end(theFormatted); | |||||
} | |||||
let serialized; | |||||
try { | |||||
serialized = responseBodySerializerPair.serialize(newObject); | |||||
} catch { | } catch { | ||||
res.statusCode = constants.HTTP_STATUS_INTERNAL_SERVER_ERROR; | |||||
res.statusMessage = `Could Not Return ${resource.state.itemName}`; | |||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | |||||
'Content-Language': errorLanguageCode, | |||||
}); | |||||
res.statusMessage = errorMessageCollection.statusMessages.unableToSerializeResponse(); | |||||
res.end(); | res.end(); | ||||
return { | |||||
handled: true, | |||||
}; | |||||
} | } | ||||
let encoded; | |||||
try { | |||||
encoded = encoding.encode(serialized); | |||||
} catch { | |||||
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, { | |||||
'Content-Language': errorLanguageCode, | |||||
}); | |||||
res.statusMessage = errorMessageCollection.statusMessages.unableToEncodeResponse(); | |||||
res.end(); | |||||
return { | |||||
handled: true, | |||||
}; | |||||
} | |||||
const headers: Record<string, string> = { | |||||
'Content-Type': responseBodyMediaType, | |||||
'Content-Language': languageCode, | |||||
'Content-Encoding': encodingKey, | |||||
}; | |||||
let totalItemCount: number | undefined; | |||||
if (backendState.showTotalItemCountOnCreateItem && typeof resource.dataSource.getTotalCount === 'function') { | |||||
totalItemCount = await resource.dataSource.getTotalCount(); | |||||
} | |||||
if (isCreated) { | |||||
headers['Location'] = `${serverParams.baseUrl}/${resource.state.routeName}/${resourceId}`; | |||||
if (typeof totalItemCount !== 'undefined') { | |||||
headers['X-Resource-Total-Item-Count'] = totalItemCount.toString(); | |||||
} | |||||
} | |||||
res.writeHead(isCreated ? constants.HTTP_STATUS_CREATED : constants.HTTP_STATUS_OK, headers); | |||||
res.statusMessage = ( | |||||
isCreated | |||||
? responseBodyMessageCollection.statusMessages.resourceCreated(resource) | |||||
: responseBodyMessageCollection.statusMessages.resourceReplaced(resource) | |||||
); | |||||
res.end(encoded); | |||||
return { | return { | ||||
handled: true | handled: true | ||||
}; | }; | ||||
@@ -1,5 +1,5 @@ | |||||
export * from './core'; | export * from './core'; | ||||
export * as valibot from './validation'; | |||||
export * as validation from './validation'; | |||||
export * as dataSources from './data-sources'; | export * as dataSources from './data-sources'; | ||||
export * as serializers from './serializers'; | export * as serializers from './serializers'; | ||||
export * as encodings from './encodings'; | export * as encodings from './encodings'; |
@@ -79,6 +79,12 @@ export const messages: MessageCollection = { | |||||
}, | }, | ||||
resourceReplaced(resource: Resource): string { | resourceReplaced(resource: Resource): string { | ||||
return `${resource.state.itemName} Replaced`; | return `${resource.state.itemName} Replaced`; | ||||
}, | |||||
unableToGenerateIdFromResourceDataSource(resource: Resource): string { | |||||
return `Unable To Generate ID From ${resource.state.itemName} Data Source`; | |||||
}, | |||||
unableToEmplaceResource(resource: Resource): string { | |||||
return `Unable To Emplace ${resource.state.itemName}`; | |||||
} | } | ||||
}, | }, | ||||
bodies: { | bodies: { | ||||
@@ -1,5 +1,6 @@ | |||||
import * as v from 'valibot'; | import * as v from 'valibot'; | ||||
export * from 'valibot'; | export * from 'valibot'; | ||||
import { Resource } from './core'; | |||||
export const datelike = () => v.transform( | export const datelike = () => v.transform( | ||||
v.union([ | v.union([ | ||||
@@ -11,3 +12,7 @@ export const datelike = () => v.transform( | |||||
(value) => new Date(value).toISOString(), | (value) => new Date(value).toISOString(), | ||||
v.string([v.isoTimestamp()]) | v.string([v.isoTimestamp()]) | ||||
); | ); | ||||
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']>>; |
@@ -6,6 +6,7 @@ import { | |||||
describe, | describe, | ||||
expect, | expect, | ||||
it, | it, | ||||
test, | |||||
} from 'vitest'; | } from 'vitest'; | ||||
import { | import { | ||||
tmpdir | tmpdir | ||||
@@ -26,7 +27,7 @@ import { | |||||
Resource, | Resource, | ||||
resource, | resource, | ||||
serializers, | serializers, | ||||
valibot as v, | |||||
validation as v, | |||||
} from '../../src'; | } from '../../src'; | ||||
import {request, Server} from 'http'; | import {request, Server} from 'http'; | ||||
import {constants} from 'http2'; | import {constants} from 'http2'; | ||||
@@ -79,7 +80,7 @@ describe('yasumi', () => { | |||||
v.never() | v.never() | ||||
)) | )) | ||||
.name('Piano') | .name('Piano') | ||||
.id('id', { | |||||
.id('id' as const, { | |||||
generationStrategy: autoIncrement, | generationStrategy: autoIncrement, | ||||
serialize: (id) => id?.toString() ?? '0', | serialize: (id) => id?.toString() ?? '0', | ||||
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, | deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, | ||||
@@ -204,6 +205,7 @@ describe('yasumi', () => { | |||||
it('returns data', () => { | it('returns data', () => { | ||||
return new Promise<void>((resolve, reject) => { | return new Promise<void>((resolve, reject) => { | ||||
// TODO all responses should have serialized ids | |||||
const req = request( | const req = request( | ||||
{ | { | ||||
host: HOST, | host: HOST, | ||||
@@ -660,4 +662,13 @@ describe('yasumi', () => { | |||||
}); | }); | ||||
}); | }); | ||||
}); | }); | ||||
// https://github.com/mayajs/maya/blob/main/test/index.test.ts | |||||
// | |||||
// peak unit test | |||||
describe("Contribute to see a unit test", () => { | |||||
test("should have a unit test", () => { | |||||
expect("Is this a unit test?").not.toEqual("Yes this is a unit test."); | |||||
}); | |||||
}); | |||||
}); | }); |