|
@@ -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(); |
|
|
} |
|
|
} |
|
|