|
|
@@ -1,6 +1,6 @@ |
|
|
|
import { constants } from 'http2'; |
|
|
|
import Negotiator from 'negotiator'; |
|
|
|
import { ValiError } from 'valibot'; |
|
|
|
import * as v from 'valibot'; |
|
|
|
import {Middleware} from './core'; |
|
|
|
import { getBody, getMethod, getUrl } from './utils'; |
|
|
|
|
|
|
@@ -296,88 +296,127 @@ export const handleDeleteItem: Middleware = ({ |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
// 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 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(); |
|
|
|
const existing = await theResource.dataSource.getSingle(mainResourceId); |
|
|
|
if (!existing) { |
|
|
|
res.statusCode = constants.HTTP_STATUS_NOT_FOUND; |
|
|
|
res.statusMessage = `${theResource.itemName} Not Found`; |
|
|
|
res.end(); |
|
|
|
return { |
|
|
|
handled: true |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
let bodyDeserialized: unknown; |
|
|
|
try { |
|
|
|
const schema = theResource.schema.type === 'object' ? theResource.schema as v.ObjectSchema<any> : theResource.schema |
|
|
|
bodyDeserialized = await getBody( |
|
|
|
req, |
|
|
|
theDeserializer, |
|
|
|
schema.type === 'object' |
|
|
|
? v.partial( |
|
|
|
schema as v.ObjectSchema<any>, |
|
|
|
(schema as v.ObjectSchema<any>).rest, |
|
|
|
(schema as v.ObjectSchema<any>).pipe |
|
|
|
) |
|
|
|
: schema |
|
|
|
); |
|
|
|
} catch (errRaw) { |
|
|
|
const err = errRaw as v.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 |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
const params = bodyDeserialized as Record<string, unknown>; |
|
|
|
await theResource.dataSource.initialize(); |
|
|
|
const newObject = await theResource.dataSource.patch(mainResourceId, params); |
|
|
|
const theFormatted = theSerializerPair.serialize(newObject); |
|
|
|
res.writeHead(constants.HTTP_STATUS_OK, { |
|
|
|
'Content-Type': theMediaType, |
|
|
|
}); |
|
|
|
res.end(theFormatted); |
|
|
|
} catch { |
|
|
|
res.statusCode = constants.HTTP_STATUS_INTERNAL_SERVER_ERROR; |
|
|
|
res.statusMessage = `Could Not Return ${theResource.itemName}`; |
|
|
|
res.end(); |
|
|
|
} |
|
|
|
return { |
|
|
|
handled: true |
|
|
|
}; |
|
|
|
}; |
|
|
|
|
|
|
|
export const handleCreateItem: Middleware = ({ |
|
|
|
appState, |
|
|
@@ -435,13 +474,13 @@ export const handleCreateItem: Middleware = ({ |
|
|
|
handled: true |
|
|
|
}; |
|
|
|
} |
|
|
|
// TODO determine serializer pair before running the middlewares |
|
|
|
|
|
|
|
await theResource.dataSource.initialize(); |
|
|
|
let bodyDeserialized: unknown; |
|
|
|
try { |
|
|
|
bodyDeserialized = await getBody(req, theDeserializer, theResource.schema); |
|
|
|
} catch (errRaw) { |
|
|
|
const err = errRaw as ValiError; |
|
|
|
const err = errRaw as v.ValiError; |
|
|
|
res.statusCode = constants.HTTP_STATUS_BAD_REQUEST; |
|
|
|
res.statusMessage = `Invalid ${theResource.itemName}`; |
|
|
|
|
|
|
@@ -465,6 +504,7 @@ export const handleCreateItem: Middleware = ({ |
|
|
|
const newId = await theResource.newId(theResource.dataSource); |
|
|
|
const params = bodyDeserialized as Record<string, unknown>; |
|
|
|
params[theResource.idAttr] = newId; |
|
|
|
await theResource.dataSource.initialize(); |
|
|
|
const newObject = await theResource.dataSource.create(params); |
|
|
|
const theFormatted = theSerializerPair.serialize(newObject); |
|
|
|
res.writeHead(constants.HTTP_STATUS_OK, { |
|
|
@@ -481,3 +521,124 @@ export const handleCreateItem: Middleware = ({ |
|
|
|
handled: true |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
export const handleEmplaceItem: Middleware = ({ |
|
|
|
appState, |
|
|
|
serverParams |
|
|
|
}) => async (req, res) => { |
|
|
|
const method = getMethod(req); |
|
|
|
if (method !== 'PUT') { |
|
|
|
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 |
|
|
|
}; |
|
|
|
} |
|
|
|
// TODO determine serializer pair before running the middlewares |
|
|
|
|
|
|
|
let bodyDeserialized: unknown; |
|
|
|
try { |
|
|
|
const schema = theResource.schema.type === 'object' ? theResource.schema as v.ObjectSchema<any> : theResource.schema |
|
|
|
//console.log(schema); |
|
|
|
bodyDeserialized = await getBody( |
|
|
|
req, |
|
|
|
theDeserializer, |
|
|
|
schema.type === 'object' |
|
|
|
? v.merge([ |
|
|
|
schema as v.ObjectSchema<any>, |
|
|
|
v.object({ |
|
|
|
[theResource.idAttr]: v.transform( |
|
|
|
v.any(), |
|
|
|
input => input.toString(), // TODO serialize/deserialize ID values |
|
|
|
v.literal(mainResourceId) |
|
|
|
) |
|
|
|
}) |
|
|
|
]) |
|
|
|
: schema |
|
|
|
); |
|
|
|
} catch (errRaw) { |
|
|
|
const err = errRaw as v.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 |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
const newId = await theResource.newId(theResource.dataSource); |
|
|
|
const params = bodyDeserialized as Record<string, unknown>; |
|
|
|
params[theResource.idAttr] = newId; |
|
|
|
await theResource.dataSource.initialize(); |
|
|
|
const newObject = await theResource.dataSource.emplace(mainResourceId, params); |
|
|
|
const theFormatted = theSerializerPair.serialize(newObject); |
|
|
|
res.writeHead(constants.HTTP_STATUS_OK, { |
|
|
|
'Content-Type': theMediaType, |
|
|
|
'Location': `${serverParams.baseUrl}/${theResource.routeName}/${newId}` |
|
|
|
}); |
|
|
|
res.end(theFormatted); |
|
|
|
} catch { |
|
|
|
res.statusCode = constants.HTTP_STATUS_INTERNAL_SERVER_ERROR; |
|
|
|
res.statusMessage = `Could Not Return ${theResource.itemName}`; |
|
|
|
res.end(); |
|
|
|
} |
|
|
|
return { |
|
|
|
handled: true |
|
|
|
}; |
|
|
|
} |