Dynamic attributes for metadata are included for createdAt and updatedAt.master
@@ -1,5 +1,12 @@ | |||||
import {BaseSchema} from 'valibot'; | import {BaseSchema} from 'valibot'; | ||||
import {ApplicationState, ContentNegotiation, Language, LanguageStatusMessageMap, Resource} from '../common'; | |||||
import { | |||||
ApplicationState, | |||||
BaseResourceType, | |||||
ContentNegotiation, | |||||
Language, | |||||
LanguageStatusMessageMap, | |||||
Resource, | |||||
} from '../common'; | |||||
import {DataSource} from './data-source'; | import {DataSource} from './data-source'; | ||||
export interface BackendState { | export interface BackendState { | ||||
@@ -45,8 +52,8 @@ export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | ' | |||||
export interface AllowedMiddlewareSpecification<Schema extends BaseSchema = BaseSchema> { | export interface AllowedMiddlewareSpecification<Schema extends BaseSchema = BaseSchema> { | ||||
method: Method; | method: Method; | ||||
middleware: Middleware; | middleware: Middleware; | ||||
constructBodySchema?: (resource: Resource<Schema>, resourceId?: string) => BaseSchema; | |||||
allowed: (resource: Resource<Schema>) => boolean; | |||||
constructBodySchema?: (resource: Resource<BaseResourceType & { schema: Schema }>, resourceId?: string) => BaseSchema; | |||||
allowed: (resource: Resource<BaseResourceType & { schema: Schema }>) => boolean; | |||||
} | } | ||||
export interface Response { | export interface Response { | ||||
@@ -9,7 +9,14 @@ import { | |||||
RequestContext, RequestDecorator, | RequestContext, RequestDecorator, | ||||
Response, | Response, | ||||
} from '../../common'; | } from '../../common'; | ||||
import {CanPatchSpec, DELTA_SCHEMA, PATCH_CONTENT_MAP_TYPE, PatchContentType, Resource} from '../../../common'; | |||||
import { | |||||
BaseResourceType, | |||||
CanPatchSpec, | |||||
DELTA_SCHEMA, | |||||
PATCH_CONTENT_MAP_TYPE, | |||||
PatchContentType, | |||||
Resource, | |||||
} from '../../../common'; | |||||
import { | import { | ||||
handleGetRoot, handleOptions, | handleGetRoot, handleOptions, | ||||
} from './handlers/default'; | } from './handlers/default'; | ||||
@@ -49,11 +56,11 @@ declare module '../../common' { | |||||
} | } | ||||
} | } | ||||
const constructPostSchema = <T extends v.BaseSchema>(resource: Resource<T>) => { | |||||
const constructPostSchema = <T extends v.BaseSchema>(resource: Resource<BaseResourceType & { schema: T }>) => { | |||||
return resource.schema; | return resource.schema; | ||||
}; | }; | ||||
const constructPutSchema = <T extends v.BaseSchema>(resource: Resource<T>, mainResourceId?: string) => { | |||||
const constructPutSchema = <T extends v.BaseSchema>(resource: Resource<BaseResourceType & { schema: T }>, mainResourceId?: string) => { | |||||
if (typeof mainResourceId === 'undefined') { | if (typeof mainResourceId === 'undefined') { | ||||
return resource.schema; | return resource.schema; | ||||
} | } | ||||
@@ -77,7 +84,7 @@ const constructPutSchema = <T extends v.BaseSchema>(resource: Resource<T>, mainR | |||||
); | ); | ||||
}; | }; | ||||
const constructPatchSchema = <T extends v.BaseSchema>(resource: Resource<T>) => { | |||||
const constructPatchSchema = <T extends v.BaseSchema>(resource: Resource<BaseResourceType & { schema: T }>) => { | |||||
const schema = resource.schema.type === 'object' ? resource.schema as unknown as v.ObjectSchema<any> : resource.schema; | const schema = resource.schema.type === 'object' ? resource.schema as unknown as v.ObjectSchema<any> : resource.schema; | ||||
if (resource.schema.type !== 'object') { | if (resource.schema.type !== 'object') { | ||||
@@ -1,4 +1,4 @@ | |||||
import {Resource} from './resource'; | |||||
import {BaseResourceType, Resource} from './resource'; | |||||
import {FALLBACK_LANGUAGE, Language} from './language'; | import {FALLBACK_LANGUAGE, Language} from './language'; | ||||
import {FALLBACK_MEDIA_TYPE, MediaType, PATCH_CONTENT_TYPES} from './media-type'; | import {FALLBACK_MEDIA_TYPE, MediaType, PATCH_CONTENT_TYPES} from './media-type'; | ||||
import {Charset, FALLBACK_CHARSET} from './charset'; | import {Charset, FALLBACK_CHARSET} from './charset'; | ||||
@@ -35,12 +35,8 @@ export interface Application< | |||||
charset<CharsetName extends string = string>(charset: Charset<CharsetName>): Application< | charset<CharsetName extends string = string>(charset: Charset<CharsetName>): Application< | ||||
Resources, MediaTypes, [...Charsets, Charset<CharsetName>], Languages | Resources, MediaTypes, [...Charsets, Charset<CharsetName>], Languages | ||||
>; | >; | ||||
resource< | |||||
Schema extends v.BaseSchema, | |||||
CurrentItemName extends string = string, | |||||
CurrentRouteName extends string = string | |||||
>(resRaw: Resource<Schema, CurrentItemName, CurrentRouteName>): Application< | |||||
[...Resources, Resource<Schema, CurrentItemName, CurrentRouteName>], MediaTypes, Charsets, Languages | |||||
resource<ResourceType extends BaseResourceType = BaseResourceType>(resRaw: Resource<ResourceType>): Application< | |||||
[...Resources, Resource<ResourceType>], MediaTypes, Charsets, Languages | |||||
>; | >; | ||||
createBackend(params: Omit<CreateBackendParams, 'app'>): Backend; | createBackend(params: Omit<CreateBackendParams, 'app'>): Backend; | ||||
createClient(params: Omit<CreateClientParams, 'app'>): ClientBuilder; | createClient(params: Omit<CreateClientParams, 'app'>): ClientBuilder; | ||||
@@ -84,7 +80,7 @@ export const application = (appParams: ApplicationParams): Application => { | |||||
appState.languages.set(language.name, language); | appState.languages.set(language.name, language); | ||||
return this; | return this; | ||||
}, | }, | ||||
resource<T extends v.BaseSchema>(resRaw: Resource<T>) { | |||||
resource<T extends v.BaseSchema>(resRaw: Resource<BaseResourceType & { schema: T }>) { | |||||
appState.resources.add(resRaw); | appState.resources.add(resRaw); | ||||
return this; | return this; | ||||
}, | }, | ||||
@@ -31,36 +31,38 @@ export interface ResourceState< | |||||
type CanPatch = boolean | Partial<CanPatchObject> | CanPatchSpec[]; | type CanPatch = boolean | Partial<CanPatchObject> | CanPatchSpec[]; | ||||
export interface Resource< | |||||
Schema extends v.BaseSchema = v.BaseSchema, | |||||
CurrentName extends string = string, | |||||
CurrentRouteName extends string = string, | |||||
CurrentIdAttr extends string = string, | |||||
IdSchema extends v.BaseSchema = v.BaseSchema | |||||
> { | |||||
schema: Schema; | |||||
state: ResourceState<CurrentName, CurrentRouteName>; | |||||
name<NewName extends CurrentName>(n: NewName): Resource<Schema, NewName, CurrentRouteName, CurrentIdAttr, IdSchema>; | |||||
route<NewRouteName extends CurrentRouteName>(n: NewRouteName): Resource<Schema, CurrentName, NewRouteName, CurrentIdAttr, IdSchema>; | |||||
export interface BaseResourceType { | |||||
schema: v.BaseSchema; | |||||
name: string; | |||||
routeName: string; | |||||
idAttr: string; | |||||
idSchema: v.BaseSchema; | |||||
createdAtAttr: string; | |||||
updatedAtAttr: string; | |||||
} | |||||
export interface Resource<ResourceType extends BaseResourceType = BaseResourceType> { | |||||
schema: ResourceType['schema']; | |||||
state: ResourceState<ResourceType['name'], ResourceType['routeName']>; | |||||
name<NewName extends ResourceType['name']>(n: NewName): Resource<ResourceType & { name: NewName }>; | |||||
route<NewRouteName extends ResourceType['routeName']>(n: NewRouteName): Resource<ResourceType & { routeName: NewRouteName }>; | |||||
canFetchCollection(b?: boolean): this; | canFetchCollection(b?: boolean): this; | ||||
canFetchItem(b?: boolean): this; | canFetchItem(b?: boolean): this; | ||||
canCreate(b?: boolean): this; | canCreate(b?: boolean): this; | ||||
canPatch(b?: CanPatch): this; | canPatch(b?: CanPatch): this; | ||||
canEmplace(b?: boolean): this; | canEmplace(b?: boolean): this; | ||||
canDelete(b?: boolean): this; | canDelete(b?: boolean): this; | ||||
relatedTo<RelatedSchema extends v.BaseSchema>(resource: Resource<RelatedSchema>): this; | |||||
relatedTo<RelatedSchema extends v.BaseSchema>(resource: Resource<ResourceType & { schema: RelatedSchema }>): this; | |||||
dataSource?: DataSource; | dataSource?: DataSource; | ||||
id<NewIdAttr extends CurrentIdAttr, TheIdSchema extends IdSchema>( | |||||
id<NewIdAttr extends ResourceType['idAttr'], TheIdSchema extends ResourceType['idSchema']>( | |||||
newIdAttr: NewIdAttr, | newIdAttr: NewIdAttr, | ||||
params: ResourceIdConfig<TheIdSchema> | params: ResourceIdConfig<TheIdSchema> | ||||
): Resource<Schema, CurrentName, CurrentRouteName, NewIdAttr, TheIdSchema>; | |||||
): Resource<ResourceType & { idAttr: NewIdAttr, idSchema: TheIdSchema }>; | |||||
createdAt<NewCreatedAtAttr extends ResourceType['createdAtAttr']>(n: NewCreatedAtAttr): Resource<ResourceType & { createdAtAttr: NewCreatedAtAttr }>; | |||||
updatedAt<NewUpdatedAtAttr extends ResourceType['updatedAtAttr']>(n: NewUpdatedAtAttr): Resource<ResourceType & { updatedAtAttr: NewUpdatedAtAttr }>; | |||||
} | } | ||||
export const resource = < | |||||
Schema extends v.BaseSchema, | |||||
CurrentName extends string = string, | |||||
CurrentRouteName extends string = string | |||||
>(schema: Schema): Resource<Schema, CurrentName, CurrentRouteName> => { | |||||
export const resource = <ResourceType extends BaseResourceType = BaseResourceType>(schema: ResourceType['schema']): Resource<ResourceType> => { | |||||
const resourceState = { | const resourceState = { | ||||
shared: new Map(), | shared: new Map(), | ||||
relationships: new Set<Resource>(), | relationships: new Set<Resource>(), | ||||
@@ -73,13 +75,11 @@ export const resource = < | |||||
}, | }, | ||||
canEmplace: false, | canEmplace: false, | ||||
canDelete: false, | canDelete: false, | ||||
} as ResourceState<CurrentName, CurrentRouteName>; | |||||
} as ResourceState<ResourceType['name'], ResourceType['routeName']>; | |||||
return { | return { | ||||
get state(): ResourceState<CurrentName, CurrentRouteName> { | |||||
return Object.freeze({ | |||||
...resourceState | |||||
}) as unknown as ResourceState<CurrentName, CurrentRouteName>; | |||||
get state(): ResourceState<ResourceType['name'], ResourceType['routeName']> { | |||||
return Object.freeze(resourceState); | |||||
}, | }, | ||||
canFetchCollection(b = true) { | canFetchCollection(b = true) { | ||||
resourceState.canFetchCollection = b; | resourceState.canFetchCollection = b; | ||||
@@ -135,13 +135,13 @@ export const resource = < | |||||
resourceState.shared.set('fullText', fullTextAttrs); | resourceState.shared.set('fullText', fullTextAttrs); | ||||
return this; | return this; | ||||
}, | }, | ||||
name<NewName extends CurrentName>(n: NewName) { | |||||
name<NewName extends ResourceType['name']>(n: NewName) { | |||||
resourceState.itemName = n; | resourceState.itemName = n; | ||||
return this as Resource<Schema, NewName, CurrentRouteName>; | |||||
return this; | |||||
}, | }, | ||||
route<NewRouteName extends CurrentRouteName>(n: NewRouteName) { | |||||
route<NewRouteName extends ResourceType['routeName']>(n: NewRouteName) { | |||||
resourceState.routeName = n; | resourceState.routeName = n; | ||||
return this as Resource<Schema, CurrentName, NewRouteName>; | |||||
return this; | |||||
}, | }, | ||||
get itemName() { | get itemName() { | ||||
return resourceState.itemName; | return resourceState.itemName; | ||||
@@ -152,11 +152,19 @@ export const resource = < | |||||
get schema() { | get schema() { | ||||
return schema; | return schema; | ||||
}, | }, | ||||
relatedTo<RelatedSchema extends v.BaseSchema>(resource: Resource<RelatedSchema>) { | |||||
relatedTo<RelatedSchema extends v.BaseSchema>(resource: Resource<ResourceType & { schema: RelatedSchema }>) { | |||||
resourceState.relationships.add(resource); | resourceState.relationships.add(resource); | ||||
return this; | return this; | ||||
}, | }, | ||||
} as Resource<Schema, CurrentName, CurrentRouteName>; | |||||
createdAt<NewCreatedAtAttr extends ResourceType['createdAtAttr']>(n: NewCreatedAtAttr) { | |||||
resourceState.shared.set('createdAtAttr', n); | |||||
return this; | |||||
}, | |||||
updatedAt<NewUpdatedAtAttr extends ResourceType['updatedAtAttr']>(n: NewUpdatedAtAttr) { | |||||
resourceState.shared.set('updatedAtAttr', n); | |||||
return this; | |||||
}, | |||||
} as Resource<ResourceType>; | |||||
}; | }; | ||||
export type ResourceType<R extends Resource> = v.Output<R['schema']>; | export type ResourceType<R extends Resource> = v.Output<R['schema']>; |
@@ -1,6 +1,6 @@ | |||||
import { readFile, writeFile } from 'fs/promises'; | import { readFile, writeFile } from 'fs/promises'; | ||||
import { join } from 'path'; | import { join } from 'path'; | ||||
import { Resource, validation as v } from '@modal-sh/yasumi'; | |||||
import { Resource, validation as v, BaseResourceType } from '@modal-sh/yasumi'; | |||||
import { DataSource, ResourceIdConfig } from '@modal-sh/yasumi/backend'; | import { DataSource, ResourceIdConfig } from '@modal-sh/yasumi/backend'; | ||||
import assert from 'assert'; | import assert from 'assert'; | ||||
@@ -16,7 +16,11 @@ export class JsonLinesDataSource< | |||||
> implements DataSource<Data> { | > implements DataSource<Data> { | ||||
private path?: string; | private path?: string; | ||||
private resource?: Resource<Schema, CurrentName, CurrentRouteName>; | |||||
private resource?: Resource<BaseResourceType & { | |||||
schema: Schema, | |||||
name: CurrentName, | |||||
routeName: CurrentRouteName, | |||||
}>; | |||||
data: Data[] = []; | data: Data[] = []; | ||||
@@ -24,15 +28,25 @@ export class JsonLinesDataSource< | |||||
// noop | // noop | ||||
} | } | ||||
prepareResource(resource: Resource<Schema, CurrentName, CurrentRouteName>) { | |||||
prepareResource<Schema extends v.BaseSchema>(resource: Resource<BaseResourceType & { | |||||
schema: Schema, | |||||
name: CurrentName, | |||||
routeName: CurrentRouteName, | |||||
}>) { | |||||
this.path = join(this.baseDir, `${resource.state.routeName}.jsonl`); | this.path = join(this.baseDir, `${resource.state.routeName}.jsonl`); | ||||
resource.dataSource = resource.dataSource ?? this; | resource.dataSource = resource.dataSource ?? this; | ||||
const originalResourceId = resource.id; | const originalResourceId = resource.id; | ||||
resource.id = <NewIdAttr extends string, NewIdSchema extends v.BaseSchema>(newIdAttr: NewIdAttr, params: ResourceIdConfig<NewIdSchema>) => { | |||||
resource.id = <NewIdAttr extends BaseResourceType['idAttr'], NewIdSchema extends BaseResourceType['idSchema']>(newIdAttr: NewIdAttr, params: ResourceIdConfig<NewIdSchema>) => { | |||||
originalResourceId(newIdAttr, params); | originalResourceId(newIdAttr, params); | ||||
return resource as Resource<Schema, CurrentName, CurrentRouteName, NewIdAttr, NewIdSchema>; | |||||
return resource as Resource<BaseResourceType & { | |||||
name: CurrentName, | |||||
routeName: CurrentRouteName, | |||||
schema: Schema, | |||||
idAttr: NewIdAttr, | |||||
idSchema: NewIdSchema, | |||||
}>; | |||||
}; | }; | ||||
this.resource = resource; | |||||
this.resource = resource as any; | |||||
} | } | ||||
async initialize() { | async initialize() { | ||||
@@ -94,12 +108,28 @@ export class JsonLinesDataSource< | |||||
const idConfig = this.resource.state.shared.get('idConfig') as ResourceIdConfig<any> | undefined; | const idConfig = this.resource.state.shared.get('idConfig') as ResourceIdConfig<any> | undefined; | ||||
assert(typeof idConfig !== 'undefined', new ResourceIdNotDesignatedError()); | assert(typeof idConfig !== 'undefined', new ResourceIdNotDesignatedError()); | ||||
let theId: any; | |||||
const { [idAttr]: dataId, ...etcData } = data as Record<string, unknown>; | |||||
if (typeof dataId !== 'undefined') { | |||||
theId = idConfig.deserialize((data as Record<string, string>)[idAttr]); | |||||
} else { | |||||
const newId = await this.newId(); | |||||
theId = idConfig.deserialize(newId); | |||||
} | |||||
const newData = { | const newData = { | ||||
...data | |||||
[idAttr]: theId, | |||||
...etcData | |||||
} as Record<string, unknown>; | } as Record<string, unknown>; | ||||
if (idAttr in newData) { | |||||
newData[idAttr] = idConfig.deserialize(newData[idAttr] as string); | |||||
const now = Date.now(); // TODO how to serialize dates | |||||
const createdAt = this.resource.state.shared.get('createdAtAttr'); | |||||
if (typeof createdAt === 'string') { | |||||
newData[createdAt] = now; | |||||
} | |||||
const updatedAt = this.resource.state.shared.get('updatedAtAttr'); | |||||
if (typeof updatedAt === 'string') { | |||||
newData[updatedAt] = now; | |||||
} | } | ||||
const newCollection = [ | const newCollection = [ | ||||
@@ -109,7 +139,7 @@ export class JsonLinesDataSource< | |||||
await writeFile(this.path, newCollection.map((d) => JSON.stringify(d)).join('\n')); | await writeFile(this.path, newCollection.map((d) => JSON.stringify(d)).join('\n')); | ||||
return data as Data; | |||||
return newData as Data; | |||||
} | } | ||||
async delete(idSerialized: string) { | async delete(idSerialized: string) { | ||||
@@ -148,9 +178,20 @@ export class JsonLinesDataSource< | |||||
const dataToEmplace = { | const dataToEmplace = { | ||||
[idAttr]: id, | [idAttr]: id, | ||||
...data, | ...data, | ||||
} as Data; | |||||
} as Record<string, unknown>; | |||||
if (existing) { | if (existing) { | ||||
const createdAt = this.resource.state.shared.get('createdAtAttr'); | |||||
if (typeof createdAt === 'string') { | |||||
dataToEmplace[createdAt] = (existing as Record<string, unknown>)[createdAt]; | |||||
} | |||||
const now = Date.now(); // TODO how to serialize dates | |||||
const updatedAt = this.resource.state.shared.get('updatedAtAttr'); | |||||
if (typeof updatedAt === 'string') { | |||||
dataToEmplace[updatedAt] = now; | |||||
} | |||||
const newData = this.data.map((d) => { | const newData = this.data.map((d) => { | ||||
if ((d as any)[idAttr] === id) { | if ((d as any)[idAttr] === id) { | ||||
return dataToEmplace; | return dataToEmplace; | ||||
@@ -164,7 +205,7 @@ export class JsonLinesDataSource< | |||||
return [dataToEmplace, false] as [Data, boolean]; | return [dataToEmplace, false] as [Data, boolean]; | ||||
} | } | ||||
const newData = await this.create(dataToEmplace); | |||||
const newData = await this.create(dataToEmplace as Data); | |||||
return [newData, true] as [Data, boolean]; | return [newData, true] as [Data, boolean]; | ||||
} | } | ||||
@@ -186,6 +227,17 @@ export class JsonLinesDataSource< | |||||
const newItem = { | const newItem = { | ||||
...existing, | ...existing, | ||||
...data, | ...data, | ||||
} as Record<string, unknown>; | |||||
const createdAt = this.resource.state.shared.get('createdAtAttr'); | |||||
if (typeof createdAt === 'string') { | |||||
newItem[createdAt] = (existing as Record<string, unknown>)[createdAt]; | |||||
} | |||||
const now = Date.now(); // TODO how to serialize dates | |||||
const updatedAt = this.resource.state.shared.get('updatedAtAttr'); | |||||
if (typeof updatedAt === 'string') { | |||||
newItem[updatedAt] = now; | |||||
} | } | ||||
const id = idConfig.deserialize(idSerialized); | const id = idConfig.deserialize(idSerialized); | ||||
@@ -1,7 +1,7 @@ | |||||
import {describe, it, expect, vi, Mock, beforeAll, beforeEach} from 'vitest'; | import {describe, it, expect, vi, Mock, beforeAll, beforeEach} from 'vitest'; | ||||
import { readFile, writeFile } from 'fs/promises'; | import { readFile, writeFile } from 'fs/promises'; | ||||
import { JsonLinesDataSource } from '../src'; | import { JsonLinesDataSource } from '../src'; | ||||
import { resource, validation as v } from '@modal-sh/yasumi'; | |||||
import { resource, validation as v, BaseResourceType } from '@modal-sh/yasumi'; | |||||
import {DataSource} from '@modal-sh/yasumi/dist/types/backend'; | import {DataSource} from '@modal-sh/yasumi/dist/types/backend'; | ||||
vi.mock('fs/promises'); | vi.mock('fs/promises'); | ||||
@@ -51,7 +51,7 @@ describe('methods', () => { | |||||
generationStrategy: mockGenerationStrategy, | generationStrategy: mockGenerationStrategy, | ||||
schema: v.any(), | schema: v.any(), | ||||
serialize: (id) => id.toString(), | serialize: (id) => id.toString(), | ||||
deserialize: (id) => Number(id.toString()), | |||||
deserialize: (id) => Number(id?.toString() ?? 0), | |||||
}); | }); | ||||
ds = new JsonLinesDataSource<typeof schema>(); | ds = new JsonLinesDataSource<typeof schema>(); | ||||
ds.prepareResource(r); | ds.prepareResource(r); | ||||
@@ -133,7 +133,10 @@ describe('methods', () => { | |||||
expect.any(String), | expect.any(String), | ||||
toJsonl([ | toJsonl([ | ||||
...dummyItems, | ...dummyItems, | ||||
data | |||||
{ | |||||
id: 0, | |||||
...data, | |||||
} | |||||
]) | ]) | ||||
); | ); | ||||
expect(newItem).toEqual(data); | expect(newItem).toEqual(data); | ||||
@@ -0,0 +1,19 @@ | |||||
meta { | |||||
name: Create Post with ID | |||||
type: http | |||||
seq: 7 | |||||
} | |||||
put { | |||||
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2 | |||||
body: json | |||||
auth: none | |||||
} | |||||
body:json { | |||||
{ | |||||
"title": "Emplaced Post", | |||||
"content": "Created post at ID", | |||||
"id": "5fac64d6-d261-42bb-a67b-bc7e1955a7e2" | |||||
} | |||||
} |
@@ -5,7 +5,7 @@ meta { | |||||
} | } | ||||
patch { | patch { | ||||
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2 | |||||
url: http://localhost:6969/api/posts/9ba60691-0cd3-4e8a-9f44-e92b19fcacbc | |||||
body: json | body: json | ||||
auth: none | auth: none | ||||
} | } | ||||
@@ -5,7 +5,7 @@ meta { | |||||
} | } | ||||
patch { | patch { | ||||
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2 | |||||
url: http://localhost:6969/api/posts/9ba60691-0cd3-4e8a-9f44-e92b19fcacbc | |||||
body: json | body: json | ||||
auth: none | auth: none | ||||
} | } | ||||
@@ -1,11 +1,11 @@ | |||||
meta { | meta { | ||||
name: Replace Post | name: Replace Post | ||||
type: http | type: http | ||||
seq: 7 | |||||
seq: 6 | |||||
} | } | ||||
put { | put { | ||||
url: http://localhost:6969/api/posts/5fac64d6-d261-42bb-a67b-bc7e1955a7e2 | |||||
url: http://localhost:6969/api/posts/9ba60691-0cd3-4e8a-9f44-e92b19fcacbc | |||||
body: json | body: json | ||||
auth: none | auth: none | ||||
} | } | ||||
@@ -14,6 +14,6 @@ body:json { | |||||
{ | { | ||||
"title": "Replaced Post", | "title": "Replaced Post", | ||||
"content": "The old content is gone.", | "content": "The old content is gone.", | ||||
"id": "5fac64d6-d261-42bb-a67b-bc7e1955a7e2" | |||||
"id": "9ba60691-0cd3-4e8a-9f44-e92b19fcacbc" | |||||
} | } | ||||
} | } |
@@ -25,14 +25,10 @@ const User = resource( | |||||
.canPatch() | .canPatch() | ||||
.canDelete(); | .canDelete(); | ||||
const now = () => new Date(); | |||||
const Post = resource( | const Post = resource( | ||||
v.object({ | v.object({ | ||||
title: v.string(), | title: v.string(), | ||||
content: v.string(), | content: v.string(), | ||||
createdAt: v.optional(v.datelike(), now), | |||||
updatedAt: v.optional(v.datelike(), now), | |||||
}) | }) | ||||
) | ) | ||||
.name('Post') | .name('Post') | ||||
@@ -43,6 +39,8 @@ const Post = resource( | |||||
serialize: (v: unknown) => v?.toString() ?? '', | serialize: (v: unknown) => v?.toString() ?? '', | ||||
deserialize: (v: string) => v, | deserialize: (v: string) => v, | ||||
}) | }) | ||||
.createdAt('createdAt') | |||||
.updatedAt('updatedAt') | |||||
.canFetchItem() | .canFetchItem() | ||||
.canFetchCollection() | .canFetchCollection() | ||||
.canCreate() | .canCreate() | ||||
@@ -58,7 +56,8 @@ const app = application({ | |||||
const backend = app.createBackend({ | const backend = app.createBackend({ | ||||
dataSource: new JsonLinesDataSource(), | dataSource: new JsonLinesDataSource(), | ||||
}); | |||||
}) | |||||
.throwsErrorOnDeletingNotFound(); | |||||
const server = backend.createHttpServer({ | const server = backend.createHttpServer({ | ||||
basePath: '/api', | basePath: '/api', | ||||