ソースを参照

Add type safety, provide granular error handling

Provide utility types for resource schema validation.

Also handle each step for POST and PUT handlers.
master
TheoryOfNekomata 8ヶ月前
コミット
3942b1efca
8個のファイルの変更269行の追加158行の削除
  1. +5
    -5
      examples/basic/server.ts
  2. +54
    -40
      src/core.ts
  3. +3
    -3
      src/data-sources/file-jsonl.ts
  4. +182
    -107
      src/handlers.ts
  5. +1
    -1
      src/index.ts
  6. +6
    -0
      src/languages/en/index.ts
  7. +5
    -0
      src/validation.ts
  8. +13
    -2
      test/e2e/default.test.ts

+ 5
- 5
examples/basic/server.ts ファイルの表示

@@ -1,7 +1,7 @@
import {
application,
resource,
valibot as v,
validation as v,
serializers,
encodings,
} from '../../src';
@@ -38,14 +38,14 @@ const User = resource(v.object(
},
v.never()
))
.name('User')
.fullText('bio')
.id('id', {
.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')
.fullText('bio');

const app = application({
name: 'piano-service',


+ 54
- 40
src/core.ts ファイルの表示

@@ -2,7 +2,7 @@ import * as http from 'http';
import * as https from 'https';
import { constants } from 'http2';
import { pluralize } from 'inflection';
import {BaseSchema, ObjectSchema, Output} from 'valibot';
import * as v from 'valibot';
import { SerializerPair } from './serializers';
import {
handleCreateItem, handleDeleteItem, handleEmplaceItem,
@@ -20,11 +20,12 @@ import * as applicationJson from './serializers/application/json';

// TODO separate frontend and backend factory methods

export interface DataSource<T = object> {
export interface DataSource<T = object, Q = object> {
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>;
delete(id: string): Promise<unknown>;
emplace(id: string, data: T): Promise<[T, boolean]>;
@@ -36,12 +37,12 @@ export interface ApplicationParams {
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;
collectionName: string;
routeName: string;
idConfig: ResourceIdConfig<T>;
idConfig: ResourceIdConfig<IdSchema>;
fullTextAttrs: Set<string>;
canCreate: boolean;
canFetchCollection: boolean;
@@ -51,11 +52,18 @@ interface ResourceState<T extends BaseSchema> {
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;
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;
name(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;
}

export interface ResourceWithDataSource<T extends BaseSchema = any> extends Resource<T> {
export interface ResourceWithDataSource<T extends v.BaseSchema> extends Resource<T> {
dataSource: DataSource;
}

@@ -76,14 +84,14 @@ interface GenerationStrategy {
(dataSource: DataSource, ...args: unknown[]): Promise<string | number | unknown>;
}

interface ResourceIdConfig<T extends BaseSchema> {
interface ResourceIdConfig<IdSchema extends v.BaseSchema> {
generationStrategy: GenerationStrategy;
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][];
if (mainResourceId === '') {
if (resource.state.canFetchCollection) {
@@ -111,7 +119,11 @@ const getAllowedMiddlewares = (resource: Resource, mainResourceId: string) => {
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 = {
fullTextAttrs: new Set<string>(),
canCreate: false,
@@ -120,13 +132,13 @@ export const resource = <T extends BaseSchema, Id extends BaseSchema = any>(sche
canPatch: false,
canEmplace: false,
canDelete: false,
} as Partial<ResourceState<Id>>;
} as ResourceState<IdAttr, IdSchema>;

return {
get state(): ResourceState<Id> {
get state(): ResourceState<IdAttr, IdSchema> {
return Object.freeze({
...resourceState
}) as unknown as ResourceState<Id>;
}) as unknown as ResourceState<IdAttr, IdSchema>;
},
canFetchCollection(b = true) {
resourceState.canFetchCollection = b;
@@ -152,10 +164,10 @@ export const resource = <T extends BaseSchema, Id extends BaseSchema = any>(sche
resourceState.canDelete = b;
return this;
},
id(newIdAttr: string, params: ResourceIdConfig<Id>) {
id<NewIdAttr extends IdAttr, NewIdSchema extends IdSchema>(newIdAttr: NewIdAttr, params: ResourceIdConfig<NewIdSchema>) {
resourceState.idAttr = newIdAttr;
resourceState.idConfig = params;
return this;
return this as Resource<ResourceSchema, NewIdAttr, NewIdSchema>;
},
newId(dataSource: DataSource) {
return resourceState?.idConfig?.generationStrategy?.(dataSource);
@@ -164,8 +176,8 @@ export const resource = <T extends BaseSchema, Id extends BaseSchema = any>(sche
if (
schema.type === 'object'
&& (
schema as unknown as ObjectSchema<
Record<string, BaseSchema>,
schema as unknown as v.ObjectSchema<
Record<string, v.BaseSchema>,
undefined,
Record<string, string>
>
@@ -207,8 +219,8 @@ export const resource = <T extends BaseSchema, Id extends BaseSchema = any>(sche
},
get schema() {
return schema;
}
} as Resource;
},
} as Resource<ResourceSchema, IdAttr, IdSchema>;
};

interface CreateServerParams {
@@ -232,7 +244,7 @@ interface HandlerState {
}

export interface ApplicationState {
resources: Set<ResourceWithDataSource>;
resources: Set<ResourceWithDataSource<any>>;
languages: Map<string, MessageCollection>;
serializers: Map<string, SerializerPair>;
encodings: Map<string, EncodingPair>;
@@ -256,6 +268,8 @@ export interface MessageCollection {
resourceFetched(resource: Resource): string;
resourceNotFound(resource: Resource): string;
deleteNonExistingResource(resource: Resource): string;
unableToGenerateIdFromResourceDataSource(resource: Resource): string;
unableToEmplaceResource(resource: Resource): string;
unableToSerializeResponse(): string;
unableToEncodeResponse(): string;
unableToDeleteResource(resource: Resource): string;
@@ -293,7 +307,7 @@ export interface BackendState {
showTotalItemCountOnCreateItem: boolean;
}

interface MiddlewareArgs {
interface MiddlewareArgs<T extends v.BaseSchema> {
handlerState: HandlerState;
backendState: BackendState;
appState: ApplicationState;
@@ -305,13 +319,13 @@ interface MiddlewareArgs {
errorResponseBodyLanguage: [string, MessageCollection];
errorResponseBodyEncoding: [string, EncodingPair];
errorResponseBodyMediaType: [string, SerializerPair];
resource: ResourceWithDataSource;
resource: ResourceWithDataSource<T>;
resourceId: string;
query: URLSearchParams;
}

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 {
@@ -332,14 +346,14 @@ export interface Application {
contentType(mimeTypePrefix: string, serializerPair: SerializerPair): this;
language(languageCode: string, messageCollection: MessageCollection): this;
encoding(encoding: string, encodingPair: EncodingPair): this;
resource(resRaw: Partial<Resource>): this;
resource<T extends v.BaseSchema>(resRaw: Partial<Resource<T>>): this;
createBackend(): Backend;
createClient(): Client;
}

export const application = (appParams: ApplicationParams): Application => {
const appState: ApplicationState = {
resources: new Set<ResourceWithDataSource>(),
resources: new Set<ResourceWithDataSource<any>>(),
languages: new Map<string, MessageCollection>(),
serializers: new Map<string, SerializerPair>(),
encodings: new Map<string, EncodingPair>(),
@@ -362,13 +376,13 @@ export const application = (appParams: ApplicationParams): Application => {
appState.languages.set(languageCode, messageCollection);
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);
if (typeof res.dataSource === 'undefined') {
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;
},
createClient(): Client {
@@ -411,7 +425,6 @@ export const application = (appParams: ApplicationParams): Application => {
showTotalItemCountOnCreateItem: false,
throws404OnDeletingNotFound: false,
checksSerializersOnDelete: false,

};

return {
@@ -466,6 +479,7 @@ export const application = (appParams: ApplicationParams): Application => {
const errorEncodingKey = backendState.errorHeaders.encoding ?? backendState.fallback.encoding;
const errorEncoding = appState.encodings.get(errorEncodingKey) ?? fallbackEncoding;

// TODO refactor
const [currentLanguageCode, currentLanguageMessages] = availableLanguages.find(([code]) => code === languageCandidate) ?? [];
if (typeof currentLanguageCode === 'undefined' || typeof currentLanguageMessages === 'undefined') {
const data = errorMessageCollection.bodies.languageNotAcceptable();
@@ -513,7 +527,7 @@ export const application = (appParams: ApplicationParams): Application => {
return;
}

const middlewareArgs: Omit<MiddlewareArgs, 'resource' | 'resourceId'> = {
const middlewareArgs: Omit<MiddlewareArgs<never>, 'resource' | 'resourceId'> = {
handlerState: {
handled: false
},
@@ -530,13 +544,13 @@ export const application = (appParams: ApplicationParams): Application => {
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) {
return;
}

if (url === '/') {
const middlewareState = await handleGetRoot(middlewareArgs as MiddlewareArgs)(req, res);
const middlewareState = await handleGetRoot(middlewareArgs as MiddlewareArgs<never>)(req, res);
if (middlewareState.handled) {
return;
}


+ 3
- 3
src/data-sources/file-jsonl.ts ファイルの表示

@@ -29,7 +29,7 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI
return [...this.data];
}

async getSingle(idSerialized: 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);

@@ -73,7 +73,7 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI
}

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 { [this.resource.state.idAttr]: idFromResource, ...data } = dataWithId;
const dataToEmplace = {
@@ -100,7 +100,7 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI
}

async patch(idSerialized: string, data: Partial<T>) {
const existing = await this.getSingle(idSerialized);
const existing = await this.getById(idSerialized);
if (!existing) {
return null;
}


+ 182
- 107
src/handlers.ts ファイルの表示

@@ -43,13 +43,13 @@ export const handleGetRoot: Middleware = ({
responseBodyEncoding: [encodingKey, encoding],
errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection],
}) => (_req: IncomingMessage, res: ServerResponse) => {
const singleResDatum = {
const data = {
name: appParams.name
};

let serialized;
try {
serialized = responseBodySerializerPair.serialize(singleResDatum);
serialized = responseBodySerializerPair.serialize(data);
} catch {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': errorLanguageCode,
@@ -61,9 +61,9 @@ export const handleGetRoot: Middleware = ({
};
}

let theFormatted;
let encoded;
try {
theFormatted = encoding.encode(serialized);
encoded = encoding.encode(serialized);
} catch {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': errorLanguageCode,
@@ -99,7 +99,7 @@ export const handleGetRoot: Middleware = ({
}
res.writeHead(constants.HTTP_STATUS_OK, theHeaders);
res.statusMessage = responseBodyMessageCollection.statusMessages.ok();
res.end(theFormatted);
res.end(encoded);
return {
handled: true
};
@@ -112,6 +112,7 @@ export const handleGetCollection: Middleware = ({
responseBodyEncoding: [encodingKey, encoding],
errorResponseBodyLanguage: [errorLanguageCode, errorMessageCollection],
backendState,
query,
}) => async (_req: IncomingMessage, res: ServerResponse) => {
try {
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;
try {
// 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') {
totalItemCount = await resource.dataSource.getTotalCount();
totalItemCount = await resource.dataSource.getTotalCount(query);
}
} catch {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
@@ -147,7 +148,7 @@ export const handleGetCollection: Middleware = ({

let serialized;
try {
serialized = responseBodySerializerPair.serialize(resData);
serialized = responseBodySerializerPair.serialize(data);
} catch {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': errorLanguageCode,
@@ -159,9 +160,9 @@ export const handleGetCollection: Middleware = ({
};
}

let theFormatted;
let encoded;
try {
theFormatted = encoding.encode(serialized);
encoded = encoding.encode(serialized);
} catch {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': errorLanguageCode,
@@ -185,7 +186,7 @@ export const handleGetCollection: Middleware = ({

res.writeHead(constants.HTTP_STATUS_OK, headers);
res.statusMessage = responseBodyMessageCollection.statusMessages.resourceCollectionFetched(resource);
res.end(theFormatted);
res.end(encoded);
return {
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 {
singleResDatum = await resource.dataSource.getSingle(resourceId);
data = await resource.dataSource.getById(resourceId);
} catch {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': errorLanguageCode,
@@ -228,7 +229,7 @@ export const handleGetItem: Middleware = ({

let serialized: string | null;
try {
serialized = singleResDatum === null ? null : responseBodySerializerPair.serialize(singleResDatum);
serialized = data === null ? null : responseBodySerializerPair.serialize(data);
} catch {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': errorLanguageCode,
@@ -240,9 +241,9 @@ export const handleGetItem: Middleware = ({
};
}

let theFormatted;
let encoded;
try {
theFormatted = serialized === null ? null : encoding.encode(serialized);
encoded = serialized === null ? null : encoding.encode(serialized);
} catch {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': errorLanguageCode,
@@ -254,14 +255,14 @@ export const handleGetItem: Middleware = ({
};
}

if (theFormatted) {
if (encoded) {
res.writeHead(constants.HTTP_STATUS_OK, {
'Content-Type': responseBodyMediaType,
'Content-Language': languageCode,
'Content-Encoding': encodingKey,
});
res.statusMessage = responseBodyMessageCollection.statusMessages.resourceFetched(resource)
res.end(theFormatted);
res.end(encoded);
return {
handled: true
};
@@ -298,50 +299,50 @@ export const handleDeleteItem: Middleware = ({
};
}

let response;
let existing: unknown | null;
try {
response = await resource.dataSource.delete(resourceId);
existing = await resource.dataSource.getById(resourceId);
} catch {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': errorLanguageCode,
});
res.statusMessage = errorMessageCollection.statusMessages.unableToDeleteResource(resource);
res.statusMessage = errorMessageCollection.statusMessages.unableToFetchResource(resource);
res.end();
return {
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();
return {
handled: true
};
}

res.writeHead(constants.HTTP_STATUS_NO_CONTENT, {
'Content-Language': languageCode,
});
res.statusMessage = responseBodyMessageCollection.statusMessages.resourceDeleted(resource);
res.end();
return {
handled: true
@@ -384,9 +385,9 @@ export const handlePatchItem: Middleware = ({
};
}

let existing: object | null;
let existing: unknown | null;
try {
existing = await resource.dataSource.getSingle(resourceId);
existing = await resource.dataSource.getById(resourceId);
} catch {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': errorLanguageCode,
@@ -411,7 +412,7 @@ export const handlePatchItem: Middleware = ({

let bodyDeserialized: unknown;
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(
req,
requestBodyDeserializerPair,
@@ -444,22 +445,21 @@ export const handlePatchItem: Middleware = ({
`${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, {
...headers,
'Content-Type': errorMediaType,
'Content-Encoding': errorEncodingKey,
})
res.statusMessage = errorMessageCollection.statusMessages.invalidResourcePatch(resource);
res.end(theFormatted);
res.end(encoded);
return {
handled: true,
};
}

const params = bodyDeserialized as Record<string, unknown>;

let newObject: object | null;
let newObject: v.Output<typeof resource.schema> | null;
try {
newObject = await resource.dataSource.patch(resourceId, params);
} catch {
@@ -487,9 +487,9 @@ export const handlePatchItem: Middleware = ({
};
}

let theFormatted;
let encoded;
try {
theFormatted = encoding.encode(serialized);
encoded = encoding.encode(serialized);
} catch {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': errorLanguageCode,
@@ -507,7 +507,7 @@ export const handlePatchItem: Middleware = ({
'Content-Encoding': encodingKey,
});
res.statusMessage = responseBodyMessageCollection.statusMessages.resourcePatched(resource);
res.end(theFormatted);
res.end(encoded);
return {
handled: true
};
@@ -567,14 +567,14 @@ export const handleCreateItem: Middleware = ({
`${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, {
...headers,
'Content-Type': errorMediaType,
'Content-Encoding': errorEncodingKey,
})
res.statusMessage = errorMessageCollection.statusMessages.invalidResource(resource);
res.end(theFormatted);
res.end(encoded);
return {
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 {
// 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;
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') {
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 {
res.writeHead(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, {
'Content-Language': errorLanguageCode,
})
res.statusMessage = `Could Not Return ${resource.state.itemName}`;
});
res.statusMessage = errorMessageCollection.statusMessages.unableToEncodeResponse();
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 {
handled: true
};
@@ -656,7 +698,7 @@ export const handleEmplaceItem: Middleware = ({

let bodyDeserialized: unknown;
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(
req,
requestBodyDeserializerPair,
@@ -694,14 +736,14 @@ export const handleEmplaceItem: Middleware = ({
`${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, {
...headers,
'Content-Type': errorMediaType,
'Content-Encoding': errorEncodingKey,
})
res.statusMessage = errorMessageCollection.statusMessages.invalidResource(resource);
res.end(theFormatted);
res.end(encoded);
return {
handled: true,
};
@@ -720,40 +762,73 @@ export const handleEmplaceItem: Middleware = ({
};
}

let newObject: v.Output<typeof resource.schema>;
let isCreated: boolean;
try {
// TODO error handling for each process
const params = bodyDeserialized as Record<string, unknown>;
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 {
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();
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 {
handled: true
};


+ 1
- 1
src/index.ts ファイルの表示

@@ -1,5 +1,5 @@
export * from './core';
export * as valibot from './validation';
export * as validation from './validation';
export * as dataSources from './data-sources';
export * as serializers from './serializers';
export * as encodings from './encodings';

+ 6
- 0
src/languages/en/index.ts ファイルの表示

@@ -79,6 +79,12 @@ export const messages: MessageCollection = {
},
resourceReplaced(resource: Resource): string {
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: {


+ 5
- 0
src/validation.ts ファイルの表示

@@ -1,5 +1,6 @@
import * as v from 'valibot';
export * from 'valibot';
import { Resource } from './core';

export const datelike = () => v.transform(
v.union([
@@ -11,3 +12,7 @@ export const datelike = () => v.transform(
(value) => new Date(value).toISOString(),
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']>>;

+ 13
- 2
test/e2e/default.test.ts ファイルの表示

@@ -6,6 +6,7 @@ import {
describe,
expect,
it,
test,
} from 'vitest';
import {
tmpdir
@@ -26,7 +27,7 @@ import {
Resource,
resource,
serializers,
valibot as v,
validation as v,
} from '../../src';
import {request, Server} from 'http';
import {constants} from 'http2';
@@ -79,7 +80,7 @@ describe('yasumi', () => {
v.never()
))
.name('Piano')
.id('id', {
.id('id' as const, {
generationStrategy: autoIncrement,
serialize: (id) => id?.toString() ?? '0',
deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0,
@@ -204,6 +205,7 @@ describe('yasumi', () => {

it('returns data', () => {
return new Promise<void>((resolve, reject) => {
// TODO all responses should have serialized ids
const req = request(
{
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.");
});
});
});

読み込み中…
キャンセル
保存