Browse Source

Fix emplace logic

Properly return data updated from data source.
master
TheoryOfNekomata 8 months ago
parent
commit
e6e61cb22e
6 changed files with 61 additions and 74 deletions
  1. +2
    -0
      examples/basic/server.ts
  2. +35
    -53
      src/core.ts
  3. +20
    -17
      src/data-sources/file-jsonl.ts
  4. +2
    -1
      src/handlers.ts
  5. +1
    -1
      src/languages/en/index.ts
  6. +1
    -2
      test/e2e/default.test.ts

+ 2
- 0
examples/basic/server.ts View File

@@ -19,6 +19,7 @@ const Piano = resource(v.object(
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(),
}) })
.canFetchItem() .canFetchItem()
.canFetchCollection() .canFetchCollection()
@@ -43,6 +44,7 @@ const User = resource(v.object(
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(),
}); });


const app = application({ const app = application({


+ 35
- 53
src/core.ts View File

@@ -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 } from 'valibot';
import {BaseSchema, ObjectSchema, Output} from 'valibot';
import { SerializerPair } from './serializers'; import { SerializerPair } from './serializers';
import { import {
handleCreateItem, handleDeleteItem, handleEmplaceItem, handleCreateItem, handleDeleteItem, handleEmplaceItem,
@@ -18,18 +18,16 @@ import * as en from './languages/en';
import * as utf8 from './encodings/utf-8'; import * as utf8 from './encodings/utf-8';
import * as applicationJson from './serializers/application/json'; import * as applicationJson from './serializers/application/json';


// TODO define ResourceState
// TODO separate frontend and backend factory methods // TODO separate frontend and backend factory methods
// TODO complete content negotiation and default (fallback) messages collection


export interface DataSource<T = object> { export interface DataSource<T = object> {
initialize(): Promise<unknown>; initialize(): Promise<unknown>;
getTotalCount?(): Promise<number>; getTotalCount?(): Promise<number>;
getMultiple(): Promise<T[]>; getMultiple(): Promise<T[]>;
getSingle(id: string): Promise<T | null>; getSingle(id: string): Promise<T | null>;
create(data: Partial<T>): Promise<T>;
create(data: T): Promise<T>;
delete(id: string): Promise<unknown>; delete(id: string): Promise<unknown>;
emplace(id: string, data: Partial<T>): Promise<[T, boolean]>;
emplace(id: string, data: T): Promise<[T, boolean]>;
patch(id: string, data: Partial<T>): Promise<T | null>; patch(id: string, data: Partial<T>): Promise<T | null>;
} }


@@ -38,24 +36,26 @@ export interface ApplicationParams {
dataSource?: (resource: Resource) => DataSource; dataSource?: (resource: Resource) => DataSource;
} }


export interface Resource<T extends BaseSchema = any> {
interface ResourceState<T extends BaseSchema> {
idAttr: string;
itemName: string;
collectionName: string;
routeName: string;
idConfig: ResourceIdConfig<T>;
fullTextAttrs: Set<string>;
canCreate: boolean;
canFetchCollection: boolean;
canFetchItem: boolean;
canPatch: boolean;
canEmplace: boolean;
canDelete: boolean;
}

export interface Resource<T extends BaseSchema = any, IdSchema extends BaseSchema = any> {
newId(dataSource: DataSource): string | number | unknown; newId(dataSource: DataSource): string | number | unknown;
schema: T; schema: T;
state: {
idAttr: string;
itemName?: string;
collectionName?: string;
routeName?: string;
idSerializer: NonNullable<IdParams['serialize']>;
idDeserializer: NonNullable<IdParams['deserialize']>;
canCreate: boolean;
canFetchCollection: boolean;
canFetchItem: boolean;
canPatch: boolean;
canEmplace: boolean;
canDelete: boolean;
};
id(newIdAttr: string, params: IdParams): this;
state: ResourceState<IdSchema>;
id(newIdAttr: string, params: ResourceIdConfig<IdSchema>): this;
fullText(fullTextAttr: string): this; fullText(fullTextAttr: string): this;
name(n: string): this; name(n: string): this;
collection(n: string): this; collection(n: string): this;
@@ -76,10 +76,11 @@ interface GenerationStrategy {
(dataSource: DataSource, ...args: unknown[]): Promise<string | number | unknown>; (dataSource: DataSource, ...args: unknown[]): Promise<string | number | unknown>;
} }


interface IdParams {
interface ResourceIdConfig<T extends BaseSchema> {
generationStrategy: GenerationStrategy; generationStrategy: GenerationStrategy;
serialize?: (id: unknown) => string;
deserialize?: (id: string) => unknown;
serialize: (id: unknown) => string;
deserialize: (id: string) => Output<T>;
schema: T;
} }


const getAllowedMiddlewares = (resource: Resource, mainResourceId: string) => { const getAllowedMiddlewares = (resource: Resource, mainResourceId: string) => {
@@ -110,24 +111,7 @@ const getAllowedMiddlewares = (resource: Resource, mainResourceId: string) => {
return middlewares; return middlewares;
}; };


interface ResourceState {
idAttr: string
itemName: string
collectionName: string
routeName: string
idGenerationStrategy: GenerationStrategy
idSerializer: IdParams['serialize']
idDeserializer: IdParams['deserialize']
fullTextAttrs: Set<string>;
canCreate: boolean;
canFetchCollection: boolean;
canFetchItem: boolean;
canPatch: boolean;
canEmplace: boolean;
canDelete: boolean;
}

export const resource = <T extends BaseSchema>(schema: T): Resource<T> => {
export const resource = <T extends BaseSchema, Id extends BaseSchema = any>(schema: T): Resource<T, Id> => {
const resourceState = { const resourceState = {
fullTextAttrs: new Set<string>(), fullTextAttrs: new Set<string>(),
canCreate: false, canCreate: false,
@@ -136,13 +120,13 @@ export const resource = <T extends BaseSchema>(schema: T): Resource<T> => {
canPatch: false, canPatch: false,
canEmplace: false, canEmplace: false,
canDelete: false, canDelete: false,
} as Partial<ResourceState>;
} as Partial<ResourceState<Id>>;


return { return {
get state(): ResourceState {
get state(): ResourceState<Id> {
return Object.freeze({ return Object.freeze({
...resourceState ...resourceState
}) as unknown as ResourceState;
}) as unknown as ResourceState<Id>;
}, },
canFetchCollection(b = true) { canFetchCollection(b = true) {
resourceState.canFetchCollection = b; resourceState.canFetchCollection = b;
@@ -168,15 +152,13 @@ export const resource = <T extends BaseSchema>(schema: T): Resource<T> => {
resourceState.canDelete = b; resourceState.canDelete = b;
return this; return this;
}, },
id(newIdAttr: string, params: IdParams) {
id(newIdAttr: string, params: ResourceIdConfig<Id>) {
resourceState.idAttr = newIdAttr; resourceState.idAttr = newIdAttr;
resourceState.idGenerationStrategy = params.generationStrategy;
resourceState.idSerializer = params.serialize;
resourceState.idDeserializer = params.deserialize;
resourceState.idConfig = params;
return this; return this;
}, },
newId(dataSource: DataSource) { newId(dataSource: DataSource) {
return resourceState?.idGenerationStrategy?.(dataSource);
return resourceState?.idConfig?.generationStrategy?.(dataSource);
}, },
fullText(fullTextAttr: string) { fullText(fullTextAttr: string) {
if ( if (
@@ -363,7 +345,7 @@ export const application = (appParams: ApplicationParams): Application => {
encodings: new Map<string, EncodingPair>(), encodings: new Map<string, EncodingPair>(),
}; };


appState.languages.set(en.code, en.messages);
appState.languages.set(en.name, en.messages);
appState.encodings.set(utf8.name, utf8); appState.encodings.set(utf8.name, utf8);
appState.serializers.set(applicationJson.name, applicationJson); appState.serializers.set(applicationJson.name, applicationJson);


@@ -393,7 +375,7 @@ export const application = (appParams: ApplicationParams): Application => {
const clientState = { const clientState = {
contentType: applicationJson.name, contentType: applicationJson.name,
encoding: utf8.name, encoding: utf8.name,
language: en.code
language: en.name
}; };


return { return {
@@ -414,7 +396,7 @@ export const application = (appParams: ApplicationParams): Application => {
createBackend(): Backend { createBackend(): Backend {
const backendState: BackendState = { const backendState: BackendState = {
fallback: { fallback: {
language: en.code,
language: en.name,
encoding: utf8.name, encoding: utf8.name,
serializer: applicationJson.name serializer: applicationJson.name
}, },


+ 20
- 17
src/data-sources/file-jsonl.ts View File

@@ -29,8 +29,9 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI
return [...this.data]; 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) { if (foundData) {
return { return {
@@ -41,13 +42,13 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI
return null; return null;
} }


async create(data: Partial<T>) {
async create(data: T) {
const newData = { const newData = {
...data ...data
} as Record<string, unknown>; } as Record<string, unknown>;


if (this.resource.state.idAttr in newData) { 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 = [ const newCollection = [
@@ -60,26 +61,29 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI
return data as T; return data as T;
} }


async delete(id: string) {
async delete(idSerialized: string) {
const oldDataLength = this.data.length; 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')); await writeFile(this.path, newData.map((d) => JSON.stringify(d)).join('\n'));


return oldDataLength !== newData.length; return oldDataLength !== newData.length;
} }


async emplace(id: string, data: Partial<T>) {
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 = { const dataToEmplace = {
...data, ...data,
[this.resource.state.idAttr]: this.resource.state.idDeserializer(id),
};
[this.resource.state.idAttr]: id,
} as T;


if (existing) { if (existing) {
const newData = this.data.map((d) => { 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; return dataToEmplace;
} }


@@ -88,16 +92,15 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI


await writeFile(this.path, newData.map((d) => JSON.stringify(d)).join('\n')); 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); const newData = await this.create(dataToEmplace);
return [newData, true] as [T, boolean]; return [newData, true] as [T, boolean];
} }


async patch(id: string, data: Partial<T>) {
const existing = await this.getSingle(id);

async patch(idSerialized: string, data: Partial<T>) {
const existing = await this.getSingle(idSerialized);
if (!existing) { if (!existing) {
return null; return null;
} }
@@ -107,8 +110,9 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI
...data, ...data,
} }


const id = this.resource.state.idConfig.deserialize(idSerialized);
const newData = this.data.map((d) => { 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; return newItem;
} }


@@ -116,7 +120,6 @@ export class DataSource<T extends Record<string, string>> implements DataSourceI
}); });


await writeFile(this.path, newData.map((d) => JSON.stringify(d)).join('\n')); await writeFile(this.path, newData.map((d) => JSON.stringify(d)).join('\n'));

return newItem as T; return newItem as T;
} }
} }

+ 2
- 1
src/handlers.ts View File

@@ -667,7 +667,7 @@ export const handleEmplaceItem: Middleware = ({
v.object({ v.object({
[resource.state.idAttr]: v.transform( [resource.state.idAttr]: v.transform(
v.any(), v.any(),
input => resource.state.idSerializer(input),
input => resource.state.idConfig.serialize(input),
v.literal(resourceId) v.literal(resourceId)
) )
}) })
@@ -723,6 +723,7 @@ export const handleEmplaceItem: Middleware = ({
try { try {
// TODO error handling for each process // 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);
const [newObject, isCreated] = await resource.dataSource.emplace(resourceId, params); const [newObject, isCreated] = await resource.dataSource.emplace(resourceId, params);
const serialized = responseBodySerializerPair.serialize(newObject); const serialized = responseBodySerializerPair.serialize(newObject);
const theFormatted = encoding.encode(serialized); const theFormatted = encoding.encode(serialized);


+ 1
- 1
src/languages/en/index.ts View File

@@ -94,4 +94,4 @@ export const messages: MessageCollection = {
} }
}; };


export const code = 'en';
export const name = 'en';

+ 1
- 2
test/e2e/default.test.ts View File

@@ -83,6 +83,7 @@ describe('yasumi', () => {
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(),
}) })
}); });


@@ -300,7 +301,6 @@ describe('yasumi', () => {
Piano.canCreate(false); Piano.canCreate(false);
}); });


// FIXME ID de/serialization problems
it('returns data', () => { it('returns data', () => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const req = request( const req = request(
@@ -480,7 +480,6 @@ describe('yasumi', () => {
Piano.canEmplace(false); Piano.canEmplace(false);
}); });


// FIXME IDs not properly being de/serialized
it('returns data for replacement', () => { it('returns data for replacement', () => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const req = request( const req = request(


Loading…
Cancel
Save