diff --git a/examples/basic/server.ts b/examples/basic/server.ts index 8d2e634..537ba93 100644 --- a/examples/basic/server.ts +++ b/examples/basic/server.ts @@ -19,6 +19,7 @@ const Piano = resource(v.object( generationStrategy: autoIncrement, serialize: (id) => id?.toString() ?? '0', deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, + schema: v.number(), }) .canFetchItem() .canFetchCollection() @@ -43,6 +44,7 @@ const User = resource(v.object( generationStrategy: autoIncrement, serialize: (id) => id?.toString() ?? '0', deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, + schema: v.number(), }); const app = application({ diff --git a/src/core.ts b/src/core.ts index 20fa2f1..70d784d 100644 --- a/src/core.ts +++ b/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 } from 'valibot'; +import {BaseSchema, ObjectSchema, Output} from 'valibot'; import { SerializerPair } from './serializers'; import { handleCreateItem, handleDeleteItem, handleEmplaceItem, @@ -18,18 +18,16 @@ import * as en from './languages/en'; import * as utf8 from './encodings/utf-8'; import * as applicationJson from './serializers/application/json'; -// TODO define ResourceState // TODO separate frontend and backend factory methods -// TODO complete content negotiation and default (fallback) messages collection export interface DataSource { initialize(): Promise; getTotalCount?(): Promise; getMultiple(): Promise; getSingle(id: string): Promise; - create(data: Partial): Promise; + create(data: T): Promise; delete(id: string): Promise; - emplace(id: string, data: Partial): Promise<[T, boolean]>; + emplace(id: string, data: T): Promise<[T, boolean]>; patch(id: string, data: Partial): Promise; } @@ -38,24 +36,26 @@ export interface ApplicationParams { dataSource?: (resource: Resource) => DataSource; } -export interface Resource { +interface ResourceState { + idAttr: string; + itemName: string; + collectionName: string; + routeName: string; + idConfig: ResourceIdConfig; + fullTextAttrs: Set; + canCreate: boolean; + canFetchCollection: boolean; + canFetchItem: boolean; + canPatch: boolean; + canEmplace: boolean; + canDelete: boolean; +} + +export interface Resource { newId(dataSource: DataSource): string | number | unknown; schema: T; - state: { - idAttr: string; - itemName?: string; - collectionName?: string; - routeName?: string; - idSerializer: NonNullable; - idDeserializer: NonNullable; - canCreate: boolean; - canFetchCollection: boolean; - canFetchItem: boolean; - canPatch: boolean; - canEmplace: boolean; - canDelete: boolean; - }; - id(newIdAttr: string, params: IdParams): this; + state: ResourceState; + id(newIdAttr: string, params: ResourceIdConfig): this; fullText(fullTextAttr: string): this; name(n: string): this; collection(n: string): this; @@ -76,10 +76,11 @@ interface GenerationStrategy { (dataSource: DataSource, ...args: unknown[]): Promise; } -interface IdParams { +interface ResourceIdConfig { generationStrategy: GenerationStrategy; - serialize?: (id: unknown) => string; - deserialize?: (id: string) => unknown; + serialize: (id: unknown) => string; + deserialize: (id: string) => Output; + schema: T; } const getAllowedMiddlewares = (resource: Resource, mainResourceId: string) => { @@ -110,24 +111,7 @@ const getAllowedMiddlewares = (resource: Resource, mainResourceId: string) => { return middlewares; }; -interface ResourceState { - idAttr: string - itemName: string - collectionName: string - routeName: string - idGenerationStrategy: GenerationStrategy - idSerializer: IdParams['serialize'] - idDeserializer: IdParams['deserialize'] - fullTextAttrs: Set; - canCreate: boolean; - canFetchCollection: boolean; - canFetchItem: boolean; - canPatch: boolean; - canEmplace: boolean; - canDelete: boolean; -} - -export const resource = (schema: T): Resource => { +export const resource = (schema: T): Resource => { const resourceState = { fullTextAttrs: new Set(), canCreate: false, @@ -136,13 +120,13 @@ export const resource = (schema: T): Resource => { canPatch: false, canEmplace: false, canDelete: false, - } as Partial; + } as Partial>; return { - get state(): ResourceState { + get state(): ResourceState { return Object.freeze({ ...resourceState - }) as unknown as ResourceState; + }) as unknown as ResourceState; }, canFetchCollection(b = true) { resourceState.canFetchCollection = b; @@ -168,15 +152,13 @@ export const resource = (schema: T): Resource => { resourceState.canDelete = b; return this; }, - id(newIdAttr: string, params: IdParams) { + id(newIdAttr: string, params: ResourceIdConfig) { resourceState.idAttr = newIdAttr; - resourceState.idGenerationStrategy = params.generationStrategy; - resourceState.idSerializer = params.serialize; - resourceState.idDeserializer = params.deserialize; + resourceState.idConfig = params; return this; }, newId(dataSource: DataSource) { - return resourceState?.idGenerationStrategy?.(dataSource); + return resourceState?.idConfig?.generationStrategy?.(dataSource); }, fullText(fullTextAttr: string) { if ( @@ -363,7 +345,7 @@ export const application = (appParams: ApplicationParams): Application => { encodings: new Map(), }; - appState.languages.set(en.code, en.messages); + appState.languages.set(en.name, en.messages); appState.encodings.set(utf8.name, utf8); appState.serializers.set(applicationJson.name, applicationJson); @@ -393,7 +375,7 @@ export const application = (appParams: ApplicationParams): Application => { const clientState = { contentType: applicationJson.name, encoding: utf8.name, - language: en.code + language: en.name }; return { @@ -414,7 +396,7 @@ export const application = (appParams: ApplicationParams): Application => { createBackend(): Backend { const backendState: BackendState = { fallback: { - language: en.code, + language: en.name, encoding: utf8.name, serializer: applicationJson.name }, diff --git a/src/data-sources/file-jsonl.ts b/src/data-sources/file-jsonl.ts index b94a2ea..26dd119 100644 --- a/src/data-sources/file-jsonl.ts +++ b/src/data-sources/file-jsonl.ts @@ -29,8 +29,9 @@ export class DataSource> implements DataSourceI return [...this.data]; } - async getSingle(id: string) { - const foundData = this.data.find((s) => this.resource.state.idSerializer(s[this.resource.state.idAttr as string]) === id); + async getSingle(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 (foundData) { return { @@ -41,13 +42,13 @@ export class DataSource> implements DataSourceI return null; } - async create(data: Partial) { + async create(data: T) { const newData = { ...data } as Record; if (this.resource.state.idAttr in newData) { - newData[this.resource.state.idAttr] = this.resource.state.idDeserializer(newData[this.resource.state.idAttr] as string); + newData[this.resource.state.idAttr] = this.resource.state.idConfig.deserialize(newData[this.resource.state.idAttr] as string); } const newCollection = [ @@ -60,26 +61,29 @@ export class DataSource> implements DataSourceI return data as T; } - async delete(id: string) { + async delete(idSerialized: string) { const oldDataLength = this.data.length; - const newData = this.data.filter((s) => !(this.resource.state.idSerializer(s[this.resource.state.idAttr as string]) === id)); + const id = this.resource.state.idConfig.deserialize(idSerialized); + const newData = this.data.filter((s) => !(s[this.resource.state.idAttr] === id)); await writeFile(this.path, newData.map((d) => JSON.stringify(d)).join('\n')); return oldDataLength !== newData.length; } - async emplace(id: string, data: Partial) { - const existing = await this.getSingle(id); + async emplace(idSerialized: string, dataWithId: T) { + const existing = await this.getSingle(idSerialized); + const id = this.resource.state.idConfig.deserialize(idSerialized); + const { [this.resource.state.idAttr]: idFromResource, ...data } = dataWithId; const dataToEmplace = { ...data, - [this.resource.state.idAttr]: this.resource.state.idDeserializer(id), - }; + [this.resource.state.idAttr]: id, + } as T; if (existing) { const newData = this.data.map((d) => { - if (this.resource.state.idSerializer(d[this.resource.state.idAttr as string]) === id) { + if (d[this.resource.state.idAttr] === id) { return dataToEmplace; } @@ -88,16 +92,15 @@ export class DataSource> implements DataSourceI await writeFile(this.path, newData.map((d) => JSON.stringify(d)).join('\n')); - return [data, false] as [T, boolean]; + return [dataToEmplace, false] as [T, boolean]; } const newData = await this.create(dataToEmplace); return [newData, true] as [T, boolean]; } - async patch(id: string, data: Partial) { - const existing = await this.getSingle(id); - + async patch(idSerialized: string, data: Partial) { + const existing = await this.getSingle(idSerialized); if (!existing) { return null; } @@ -107,8 +110,9 @@ export class DataSource> implements DataSourceI ...data, } + const id = this.resource.state.idConfig.deserialize(idSerialized); const newData = this.data.map((d) => { - if (this.resource.state.idSerializer(d[this.resource.state.idAttr as string]) === id) { + if (d[this.resource.state.idAttr as string] === id) { return newItem; } @@ -116,7 +120,6 @@ export class DataSource> implements DataSourceI }); await writeFile(this.path, newData.map((d) => JSON.stringify(d)).join('\n')); - return newItem as T; } } diff --git a/src/handlers.ts b/src/handlers.ts index 26355ab..6453ce4 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -667,7 +667,7 @@ export const handleEmplaceItem: Middleware = ({ v.object({ [resource.state.idAttr]: v.transform( v.any(), - input => resource.state.idSerializer(input), + input => resource.state.idConfig.serialize(input), v.literal(resourceId) ) }) @@ -723,6 +723,7 @@ export const handleEmplaceItem: Middleware = ({ try { // TODO error handling for each process const params = bodyDeserialized as Record; + 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); diff --git a/src/languages/en/index.ts b/src/languages/en/index.ts index dc13506..a315097 100644 --- a/src/languages/en/index.ts +++ b/src/languages/en/index.ts @@ -94,4 +94,4 @@ export const messages: MessageCollection = { } }; -export const code = 'en'; +export const name = 'en'; diff --git a/test/e2e/default.test.ts b/test/e2e/default.test.ts index f26b2d2..f25c4b0 100644 --- a/test/e2e/default.test.ts +++ b/test/e2e/default.test.ts @@ -83,6 +83,7 @@ describe('yasumi', () => { generationStrategy: autoIncrement, serialize: (id) => id?.toString() ?? '0', deserialize: (id) => Number.isFinite(Number(id)) ? Number(id) : 0, + schema: v.number(), }) }); @@ -300,7 +301,6 @@ describe('yasumi', () => { Piano.canCreate(false); }); - // FIXME ID de/serialization problems it('returns data', () => { return new Promise((resolve, reject) => { const req = request( @@ -480,7 +480,6 @@ describe('yasumi', () => { Piano.canEmplace(false); }); - // FIXME IDs not properly being de/serialized it('returns data for replacement', () => { return new Promise((resolve, reject) => { const req = request(