Browse Source

Fix resource validation

Exclude ID attribute on applying delta operations.
master
TheoryOfNekomata 7 months ago
parent
commit
4be3a7c22f
3 changed files with 161 additions and 87 deletions
  1. +16
    -3
      src/backend/servers/http/handlers/resource.ts
  2. +145
    -83
      src/common/delta/core.ts
  3. +0
    -1
      src/common/delta/error.ts

+ 16
- 3
src/backend/servers/http/handlers/resource.ts View File

@@ -156,6 +156,15 @@ export const handlePatchItem: Middleware = async (req, res) => {
headers, headers,
} = req; } = req;


const idAttr = resource.state.shared.get('idAttr');
assert(
isIdAttributeDefined(idAttr),
new ErrorPlainResponse('resourceIdNotGiven', {
statusCode: constants.HTTP_STATUS_INTERNAL_SERVER_ERROR,
res,
})
);

assert( assert(
isResourceIdDefined(resourceId), isResourceIdDefined(resourceId),
new ErrorPlainResponse( new ErrorPlainResponse(
@@ -203,11 +212,12 @@ export const handlePatchItem: Middleware = async (req, res) => {
} }
case 'delta': { case 'delta': {
let modifiedObject: Record<string, unknown>; let modifiedObject: Record<string, unknown>;
const { [idAttr]: id, ...theExisting } = existing as Record<string, unknown>;
try { try {
modifiedObject = await applyDelta( modifiedObject = await applyDelta(
theExisting as Record<string, unknown>,
body as Delta[],
resource.schema, resource.schema,
existing as Record<string, unknown>,
body as Delta[]
); );
} catch (cause) { } catch (cause) {
throw new ErrorPlainResponse('invalidResourcePatch', { throw new ErrorPlainResponse('invalidResourcePatch', {
@@ -218,7 +228,10 @@ export const handlePatchItem: Middleware = async (req, res) => {
} }


try { 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) { } catch (cause) {
throw new ErrorPlainResponse('unableToPatchResource', { throw new ErrorPlainResponse('unableToPatchResource', {
cause, cause,


+ 145
- 83
src/common/delta/core.ts View File

@@ -8,44 +8,158 @@ import {
} from './error'; } from './error';
import {append, get, remove, set} from './object'; 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([ 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<typeof DELTA_SCHEMA>; export type Delta = v.Output<typeof DELTA_SCHEMA>;


const applyReplaceDelta = <T extends v.BaseSchema = v.BaseSchema>(
mutablePreviousObject: Record<string, unknown>,
deltaItem: v.Output<typeof REPLACE_DELTA_SCHEMA>,
pathSchema: T,
) => {
if (!v.is(pathSchema, deltaItem.value)) {
throw new InvalidPathValueError();
}

set(mutablePreviousObject, deltaItem.path, deltaItem.value);
};

const applyAddDelta = <T extends v.BaseSchema = v.BaseSchema>(
mutablePreviousObject: Record<string, unknown>,
deltaItem: v.Output<typeof ADD_DELTA_SCHEMA>,
pathSchema: T,
) => {
if (pathSchema.type !== 'array') {
throw new InvalidOperationError();
}

const arraySchema = pathSchema as unknown as v.ArraySchema<any>;
if (!v.is(arraySchema.item, deltaItem.value)) {
throw new InvalidPathValueError();
}

append(mutablePreviousObject, deltaItem.path, deltaItem.value);
}

const applyRemoveDelta = (
mutablePreviousObject: Record<string, unknown>,
deltaItem: v.Output<typeof REMOVE_DELTA_SCHEMA>,
) => {
remove(mutablePreviousObject, deltaItem.path);
};

const applyCopyDelta = <T extends v.BaseSchema = v.BaseSchema>(
mutablePreviousObject: Record<string, unknown>,
deltaItem: v.Output<typeof COPY_DELTA_SCHEMA>,
pathSchema: T,
) => {
const value = get(mutablePreviousObject, deltaItem.from);
if (!v.is(pathSchema, value)) {
throw new InvalidPathValueError();
}

set(mutablePreviousObject, deltaItem.path, value);
};

const applyMoveDelta = <T extends v.BaseSchema = v.BaseSchema>(
mutablePreviousObject: Record<string, unknown>,
deltaItem: v.Output<typeof MOVE_DELTA_SCHEMA>,
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<string, unknown>,
deltaItem: v.Output<typeof TEST_DELTA_SCHEMA>,
) => {
const value = get(mutablePreviousObject, deltaItem.path);
if (value !== deltaItem.value) {
throw new PathValueTestFailedError();
}
};

type ApplyDeltaFunction = <T extends v.BaseSchema = v.BaseSchema>(
mutablePreviousObject: Record<string, unknown>,
deltaItem: any,
pathSchema: T
) => void;

const OPERATION_FUNCTION_MAP: Record<Delta['op'], ApplyDeltaFunction> = {
replace: applyReplaceDelta,
add: applyAddDelta,
remove: applyRemoveDelta,
copy: applyCopyDelta,
move: applyMoveDelta,
test: applyTestDelta,
};

const mutateObject = <T extends v.BaseSchema = v.BaseSchema>(
mutablePreviousObject: Record<string, unknown>,
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 <T extends v.BaseSchema = v.BaseSchema>( export const applyDelta = async <T extends v.BaseSchema = v.BaseSchema>(
resourceSchema: T,
existing: Record<string, unknown>, existing: Record<string, unknown>,
deltaCollection: Delta[], deltaCollection: Delta[],
resourceSchema: T,
) => { ) => {
return await deltaCollection.reduce( return await deltaCollection.reduce(
async (resultObject, deltaItem) => { async (resultObject, deltaItem) => {
@@ -61,59 +175,7 @@ export const applyDelta = async <T extends v.BaseSchema = v.BaseSchema>(
throw new InvalidSchemaInPathError(); 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<any>;
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)) { if (!v.is(resourceObjectSchema, mutablePreviousObject)) {
throw new InvalidOperationError(); throw new InvalidOperationError();
} }


+ 0
- 1
src/common/delta/error.ts View File

@@ -1,4 +1,3 @@

export class InvalidSchemaInPathError extends Error {} export class InvalidSchemaInPathError extends Error {}


export class InvalidPathValueError extends Error {} export class InvalidPathValueError extends Error {}


Loading…
Cancel
Save