Browse Source

Fix emplace logic

Properly return data updated from data source.
master
TheoryOfNekomata 9 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,
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({


+ 35
- 53
src/core.ts View File

@@ -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<T = object> {
initialize(): Promise<unknown>;
getTotalCount?(): Promise<number>;
getMultiple(): Promise<T[]>;
getSingle(id: string): Promise<T | null>;
create(data: Partial<T>): Promise<T>;
create(data: T): Promise<T>;
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>;
}

@@ -38,24 +36,26 @@ export interface ApplicationParams {
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;
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;
name(n: string): this;
collection(n: string): this;
@@ -76,10 +76,11 @@ interface GenerationStrategy {
(dataSource: DataSource, ...args: unknown[]): Promise<string | number | unknown>;
}

interface IdParams {
interface ResourceIdConfig<T extends BaseSchema> {
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) => {
@@ -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<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 = {
fullTextAttrs: new Set<string>(),
canCreate: false,
@@ -136,13 +120,13 @@ export const resource = <T extends BaseSchema>(schema: T): Resource<T> => {
canPatch: false,
canEmplace: false,
canDelete: false,
} as Partial<ResourceState>;
} as Partial<ResourceState<Id>>;

return {
get state(): ResourceState {
get state(): ResourceState<Id> {
return Object.freeze({
...resourceState
}) as unknown as ResourceState;
}) as unknown as ResourceState<Id>;
},
canFetchCollection(b = true) {
resourceState.canFetchCollection = b;
@@ -168,15 +152,13 @@ export const resource = <T extends BaseSchema>(schema: T): Resource<T> => {
resourceState.canDelete = b;
return this;
},
id(newIdAttr: string, params: IdParams) {
id(newIdAttr: string, params: ResourceIdConfig<Id>) {
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<string, EncodingPair>(),
};

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
},


+ 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];
}

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<T extends Record<string, string>> implements DataSourceI
return null;
}

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

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<T extends Record<string, string>> 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<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 = {
...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<T extends Record<string, string>> 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<T>) {
const existing = await this.getSingle(id);

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

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

return newItem as T;
}
}

+ 2
- 1
src/handlers.ts View File

@@ -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<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);


+ 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,
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<void>((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<void>((resolve, reject) => {
const req = request(


Loading…
Cancel
Save