From 4be3a7c22f0be44e02ac28ed0155876fb5baec03 Mon Sep 17 00:00:00 2001 From: TheoryOfNekomata Date: Mon, 15 Apr 2024 14:49:05 +0800 Subject: [PATCH] Fix resource validation Exclude ID attribute on applying delta operations. --- src/backend/servers/http/handlers/resource.ts | 19 +- src/common/delta/core.ts | 228 +++++++++++------- src/common/delta/error.ts | 1 - 3 files changed, 161 insertions(+), 87 deletions(-) diff --git a/src/backend/servers/http/handlers/resource.ts b/src/backend/servers/http/handlers/resource.ts index 95e4535..69afa34 100644 --- a/src/backend/servers/http/handlers/resource.ts +++ b/src/backend/servers/http/handlers/resource.ts @@ -156,6 +156,15 @@ export const handlePatchItem: Middleware = async (req, res) => { headers, } = req; + const idAttr = resource.state.shared.get('idAttr'); + assert( + isIdAttributeDefined(idAttr), + new ErrorPlainResponse('resourceIdNotGiven', { + statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, + res, + }) + ); + assert( isResourceIdDefined(resourceId), new ErrorPlainResponse( @@ -203,11 +212,12 @@ export const handlePatchItem: Middleware = async (req, res) => { } case 'delta': { let modifiedObject: Record; + const { [idAttr]: id, ...theExisting } = existing as Record; try { modifiedObject = await applyDelta( + theExisting as Record, + body as Delta[], resource.schema, - existing as Record, - body as Delta[] ); } catch (cause) { throw new ErrorPlainResponse('invalidResourcePatch', { @@ -218,7 +228,10 @@ export const handlePatchItem: Middleware = async (req, res) => { } try { - newObject = await resource.dataSource.patch(resourceId, modifiedObject); + newObject = await resource.dataSource.patch(resourceId, { + ...modifiedObject, + [idAttr]: id, // TODO should ID belong to the resource? + }); } catch (cause) { throw new ErrorPlainResponse('unableToPatchResource', { cause, diff --git a/src/common/delta/core.ts b/src/common/delta/core.ts index 3d1a9a5..a073f5a 100644 --- a/src/common/delta/core.ts +++ b/src/common/delta/core.ts @@ -8,44 +8,158 @@ import { } from './error'; import {append, get, remove, set} from './object'; +const ADD_DELTA_SCHEMA = v.object({ + op: v.literal('add'), + path: v.string(), + value: v.unknown() +}); + +const REMOVE_DELTA_SCHEMA = v.object({ + op: v.literal('remove'), + path: v.string(), +}); + +const REPLACE_DELTA_SCHEMA = v.object({ + op: v.literal('replace'), + path: v.string(), + value: v.unknown() +}); + +const MOVE_DELTA_SCHEMA = v.object({ + op: v.literal('move'), + path: v.string(), + from: v.string(), +}); + +const COPY_DELTA_SCHEMA = v.object({ + op: v.literal('copy'), + path: v.string(), + from: v.string(), +}); + +const TEST_DELTA_SCHEMA = v.object({ + op: v.literal('test'), + path: v.string(), + value: v.unknown() +}); + export const DELTA_SCHEMA = v.union([ - v.object({ - op: v.literal('add'), - path: v.string(), // todo validate if valid path? - value: v.unknown() // todo validate if valid value? - }), - v.object({ - op: v.literal('remove'), - path: v.string(), - }), - v.object({ - op: v.literal('replace'), - path: v.string(), // todo validate if valid path? - value: v.unknown() // todo validate if valid value? - }), - v.object({ - op: v.literal('move'), - path: v.string(), - from: v.string(), - }), - v.object({ - op: v.literal('copy'), - path: v.string(), - from: v.string(), - }), - v.object({ - op: v.literal('test'), - path: v.string(), // todo validate if valid path? - value: v.unknown() // todo validate if valid value? - }), + ADD_DELTA_SCHEMA, + REMOVE_DELTA_SCHEMA, + REPLACE_DELTA_SCHEMA, + MOVE_DELTA_SCHEMA, + COPY_DELTA_SCHEMA, + TEST_DELTA_SCHEMA, ]); export type Delta = v.Output; +const applyReplaceDelta = ( + mutablePreviousObject: Record, + deltaItem: v.Output, + pathSchema: T, +) => { + if (!v.is(pathSchema, deltaItem.value)) { + throw new InvalidPathValueError(); + } + + set(mutablePreviousObject, deltaItem.path, deltaItem.value); +}; + +const applyAddDelta = ( + mutablePreviousObject: Record, + deltaItem: v.Output, + pathSchema: T, +) => { + if (pathSchema.type !== 'array') { + throw new InvalidOperationError(); + } + + const arraySchema = pathSchema as unknown as v.ArraySchema; + if (!v.is(arraySchema.item, deltaItem.value)) { + throw new InvalidPathValueError(); + } + + append(mutablePreviousObject, deltaItem.path, deltaItem.value); +} + +const applyRemoveDelta = ( + mutablePreviousObject: Record, + deltaItem: v.Output, +) => { + remove(mutablePreviousObject, deltaItem.path); +}; + +const applyCopyDelta = ( + mutablePreviousObject: Record, + deltaItem: v.Output, + pathSchema: T, +) => { + const value = get(mutablePreviousObject, deltaItem.from); + if (!v.is(pathSchema, value)) { + throw new InvalidPathValueError(); + } + + set(mutablePreviousObject, deltaItem.path, value); +}; + +const applyMoveDelta = ( + mutablePreviousObject: Record, + deltaItem: v.Output, + pathSchema: T, +) => { + const value = get(mutablePreviousObject, deltaItem.from); + if (!v.is(pathSchema, value)) { + throw new InvalidPathValueError(); + } + + remove(mutablePreviousObject, deltaItem.from) + set(mutablePreviousObject, deltaItem.path, value); +}; + +const applyTestDelta = ( + mutablePreviousObject: Record, + deltaItem: v.Output, +) => { + const value = get(mutablePreviousObject, deltaItem.path); + if (value !== deltaItem.value) { + throw new PathValueTestFailedError(); + } +}; + +type ApplyDeltaFunction = ( + mutablePreviousObject: Record, + deltaItem: any, + pathSchema: T +) => void; + +const OPERATION_FUNCTION_MAP: Record = { + replace: applyReplaceDelta, + add: applyAddDelta, + remove: applyRemoveDelta, + copy: applyCopyDelta, + move: applyMoveDelta, + test: applyTestDelta, +}; + +const mutateObject = ( + mutablePreviousObject: Record, + deltaItem: Delta, + pathSchema: T, +) => { + const { [deltaItem.op]: applyDeltaFn } = OPERATION_FUNCTION_MAP; + + if (typeof applyDeltaFn !== 'function') { + throw new InvalidOperationError(); + } + + applyDeltaFn(mutablePreviousObject, deltaItem, pathSchema); +}; + export const applyDelta = async ( - resourceSchema: T, existing: Record, deltaCollection: Delta[], + resourceSchema: T, ) => { return await deltaCollection.reduce( async (resultObject, deltaItem) => { @@ -61,59 +175,7 @@ export const applyDelta = async ( throw new InvalidSchemaInPathError(); } - switch (deltaItem.op) { - case 'replace': { - if (!v.is(pathSchema, deltaItem.value)) { - throw new InvalidPathValueError(); - } - - set(mutablePreviousObject, deltaItem.path, deltaItem.value); - return mutablePreviousObject; - } - case 'add': { - if (pathSchema.type !== 'array') { - throw new InvalidOperationError(); - } - - const arraySchema = pathSchema as v.ArraySchema; - if (!v.is(arraySchema.item, deltaItem.value)) { - throw new InvalidPathValueError(); - } - - return append(mutablePreviousObject, deltaItem.path, deltaItem.value); - } - case 'remove': { - return remove(mutablePreviousObject, deltaItem.path); - } - case 'copy': { - const value = get(mutablePreviousObject, deltaItem.from); - if (!v.is(pathSchema, value)) { - throw new InvalidPathValueError(); - } - - return set(mutablePreviousObject, deltaItem.path, value); - } - case 'move': { - const value = get(mutablePreviousObject, deltaItem.from); - if (!v.is(pathSchema, value)) { - throw new InvalidPathValueError(); - } - - remove(mutablePreviousObject, deltaItem.from) - return set(mutablePreviousObject, deltaItem.path, value); - } - case 'test': { - const value = get(mutablePreviousObject, deltaItem.path); - if (value !== deltaItem.value) { - throw new PathValueTestFailedError(); - } - - return mutablePreviousObject; - } - default: - break; - } - + mutateObject(mutablePreviousObject, deltaItem, pathSchema); if (!v.is(resourceObjectSchema, mutablePreviousObject)) { throw new InvalidOperationError(); } diff --git a/src/common/delta/error.ts b/src/common/delta/error.ts index 832536f..b79e9c8 100644 --- a/src/common/delta/error.ts +++ b/src/common/delta/error.ts @@ -1,4 +1,3 @@ - export class InvalidSchemaInPathError extends Error {} export class InvalidPathValueError extends Error {}