diff --git a/src/core.ts b/src/core.ts index a789adc..8a09bd7 100644 --- a/src/core.ts +++ b/src/core.ts @@ -4,11 +4,11 @@ import { pluralize } from 'inflection'; import { BaseSchema, ObjectSchema } from 'valibot'; import { SerializerPair } from './serializers'; import { - handleCreateItem, + handleCreateItem, handleDeleteItem, handleGetCollection, handleGetItem, handleGetRoot, - handleHasMethodAndUrl + handleHasMethodAndUrl, } from './handlers'; export interface DataSource { @@ -34,6 +34,8 @@ export interface Resource { dataSource: DataSource; newId(dataSource: DataSource): string | number | unknown; schema: BaseSchema; + throws404OnDeletingNotFound: boolean; + checksSerializersOnDelete: boolean; } interface GenerationStrategy { @@ -50,9 +52,25 @@ export const resource = (schema: T) => { let theCollectionName: string; let theRouteName: string; let idGenerationStrategy: GenerationStrategy; + let throw404OnDeletingNotFound = true; + let checkSerializersOnDelete = false; const fullTextAttrs = new Set(); return { + shouldCheckSerializersOnDelete(b = true) { + checkSerializersOnDelete = b; + return this; + }, + get checksSerializersOnDelete() { + return checkSerializersOnDelete; + }, + shouldThrow404OnDeletingNotFound(b = true) { + throw404OnDeletingNotFound = b; + return this; + }, + get throws404OnDeletingNotFound() { + return throw404OnDeletingNotFound; + }, id(newIdAttr: string, params: IdParams) { theIdAttr = newIdAttr; idGenerationStrategy = params.generationStrategy; @@ -172,6 +190,7 @@ export const application = (appParams: ApplicationParams) => { handleGetCollection, handleGetItem, handleCreateItem, + handleDeleteItem, ] .reduce( async (currentHandlerStatePromise, middleware) => { diff --git a/src/data-sources/file-jsonl.ts b/src/data-sources/file-jsonl.ts index 8982d2a..1fa8bd6 100644 --- a/src/data-sources/file-jsonl.ts +++ b/src/data-sources/file-jsonl.ts @@ -48,9 +48,13 @@ export class DataSource> implements DataSourceI } async delete(id: string) { + const oldDataLength = this.data.length; + const newData = this.data.filter((s) => !(s[this.resource.idAttr as string].toString() === id)); await writeFile(this.path, newData.map((d) => JSON.stringify(d)).join('\n')); + + return oldDataLength !== newData.length; } async emplace(id: string, data: Partial) { diff --git a/src/handlers.ts b/src/handlers.ts index 08169f0..2d08d9b 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -1,7 +1,7 @@ import { constants } from 'http2'; import Negotiator from 'negotiator'; import { ValiError } from 'valibot'; -import { Middleware } from './core'; +import {Middleware} from './core'; import { getBody, getMethod, getUrl } from './utils'; export const handleHasMethodAndUrl: Middleware = ({}) => (req, res) => { @@ -220,6 +220,165 @@ export const handleGetItem: Middleware = ({ }; }; + +export const handleDeleteItem: Middleware = ({ + appState, + serverParams +}) => async (req, res) => { + const method = getMethod(req); + if (method !== 'DELETE') { + return { + handled: false + }; + } + + const baseUrl = serverParams.baseUrl ?? ''; + const { url } = getUrl(req, baseUrl); + + const [, mainResourceRouteName, mainResourceId = ''] = url.split('/'); + if (mainResourceId === '') { + return { + handled: false + } + } + + const theResource = Array.from(appState.resources).find((r) => r.routeName === mainResourceRouteName); + if (typeof theResource === 'undefined') { + return { + handled: false + }; + } + + if (theResource.checksSerializersOnDelete) { + const negotiator = new Negotiator(req); + const availableMediaTypes = Array.from(appState.serializers.keys()); + const theMediaType = negotiator.mediaType(availableMediaTypes); + if (typeof theMediaType === 'undefined') { + res.statusCode = constants.HTTP_STATUS_NOT_ACCEPTABLE; + res.end(); + return { + handled: true + }; + } + + const theSerializerPair = appState.serializers.get(theMediaType); + if (typeof theSerializerPair === 'undefined') { + res.statusCode = constants.HTTP_STATUS_NOT_ACCEPTABLE; + res.end(); + return { + handled: true + }; + } + } + + await theResource.dataSource.initialize(); + try { + const response = await theResource.dataSource.delete(mainResourceId); + if (typeof response !== 'undefined' && !response && theResource.throws404OnDeletingNotFound) { + res.statusCode = constants.HTTP_STATUS_NOT_FOUND; + res.statusMessage = `${theResource.itemName} Not Found`; + } else { + res.statusCode = constants.HTTP_STATUS_NO_CONTENT; + } + res.end(); + return { + handled: true + }; + } catch { + // TODO error handling + // what if item is already deleted? Should we hide it by returning no content or throw a 404? + } + + res.statusCode = constants.HTTP_STATUS_INTERNAL_SERVER_ERROR; + res.end(); + return { + handled: true + }; +}; + +// export const handlePatchItem: Middleware = ({ +// appState, +// serverParams, +// }) => async (req, res) => { +// const method = getMethod(req); +// if (method !== 'PATCH') { +// return { +// handled: false +// }; +// } +// +// const baseUrl = serverParams.baseUrl ?? ''; +// const { url } = getUrl(req, baseUrl); +// +// const [, mainResourceRouteName, mainResourceId = ''] = url.split('/'); +// if (mainResourceId === '') { +// return { +// handled: false +// } +// } +// +// const theResource = Array.from(appState.resources).find((r) => r.routeName === mainResourceRouteName); +// if (typeof theResource === 'undefined') { +// return { +// handled: false +// }; +// } +// +// const theDeserializer = appState.serializers.get(req.headers['content-type'] ?? 'application/octet-stream'); +// if (typeof theDeserializer === 'undefined') { +// res.statusCode = constants.HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE; +// res.end(); +// return { +// handled: true +// }; +// } +// +// const negotiator = new Negotiator(req); +// const availableMediaTypes = Array.from(appState.serializers.keys()); +// const theMediaType = negotiator.mediaType(availableMediaTypes); +// if (typeof theMediaType === 'undefined') { +// res.statusCode = constants.HTTP_STATUS_NOT_ACCEPTABLE; +// res.end(); +// return { +// handled: true +// }; +// } +// +// const theSerializerPair = appState.serializers.get(theMediaType); +// if (typeof theSerializerPair === 'undefined') { +// res.statusCode = constants.HTTP_STATUS_NOT_ACCEPTABLE; +// res.end(); +// return { +// handled: true +// }; +// } +// +// await theResource.dataSource.initialize(); +// let bodyDeserialized: unknown; +// try { +// bodyDeserialized = await getBody(req, theDeserializer, theResource.schema); +// } catch (errRaw) { +// const err = errRaw as ValiError; +// res.statusCode = constants.HTTP_STATUS_BAD_REQUEST; +// res.statusMessage = `Invalid ${theResource.itemName}`; +// +// if (Array.isArray(err.issues)) { +// // TODO better error reporting, localizable messages +// const theFormatted = theSerializerPair.serialize( +// err.issues.map((i) => ( +// `${i.path?.map((p) => p.key)?.join('.') ?? i.reason}: ${i.message}` +// )) +// ); +// res.end(theFormatted); +// } else { +// res.end(); +// } +// return { +// handled: true +// }; +// } +// }; + export const handleCreateItem: Middleware = ({ appState, serverParams