-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #26 from phenyl-js/validate-update-operation
Validate update operation
- Loading branch information
Showing
4 changed files
with
203 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { | ||
DocumentPath, | ||
createDocumentPath, | ||
parseDocumentPath, | ||
} from "../common/document-path"; | ||
|
||
import { UpdateOperationOrSetOperand } from "./update-operation"; | ||
import { normalizeUpdateOperation } from "./normalize-update-operation"; | ||
|
||
export type UpdateOperationValidationResult = | ||
| { | ||
valid: true; | ||
errors: []; | ||
} | ||
| { | ||
valid: false; | ||
errors: Error[]; | ||
}; | ||
/** | ||
* Check if the given update operation is valid. | ||
*/ | ||
export function validateUpdateOperation( | ||
operation: UpdateOperationOrSetOperand | ||
): UpdateOperationValidationResult { | ||
const errors: Error[] = []; | ||
const normalizedOperation = normalizeUpdateOperation(operation); | ||
const docPaths: { [path: string]: DocumentPath } = Object.values( | ||
normalizedOperation | ||
) | ||
.filter(isNotUndefined) | ||
.map(operand => Object.keys(operand)) | ||
.reduce( | ||
(acc, paths) => { | ||
paths.forEach(path => { | ||
if (acc[path] != null) { | ||
errors.push( | ||
new Error( | ||
`Updating the path '${path}' would create a conflict at '${path}'` | ||
) | ||
); | ||
} | ||
acc[path] = path; | ||
}); | ||
return acc; | ||
}, | ||
{} as { [path: string]: DocumentPath } | ||
); | ||
Object.keys(docPaths).forEach(path => { | ||
const attributes = parseDocumentPath(path); | ||
for (let i = 1; i <= attributes.length - 1; i++) { | ||
const partialPath = createDocumentPath(...attributes.slice(0, i)); | ||
if (docPaths[partialPath] != null) { | ||
errors.push( | ||
new Error( | ||
`Updating the path '${path}' would create a conflict at '${ | ||
docPaths[partialPath] | ||
}'` | ||
) | ||
); | ||
} | ||
} | ||
}); | ||
return errors.length === 0 | ||
? { valid: true, errors: [] } | ||
: { valid: false, errors }; | ||
} | ||
|
||
function isNotUndefined<T>(v: T | undefined): v is T { | ||
return v != null; | ||
} |
127 changes: 127 additions & 0 deletions
127
modules/format/test/updating/validate-update-operation.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
/* eslint-env mocha */ | ||
import assert from "assert"; | ||
import { validateUpdateOperation } from "../../src/updating/validate-update-operation"; | ||
|
||
describe("validateUpdateOperation", () => { | ||
it("returns valid when an operation has no conflicting document paths", () => { | ||
const operation = { | ||
$set: { "foo.bar": 12, bar: { foo: "abc" }, "foo.biz": true }, | ||
$push: { "foo.baz": "xyz" }, | ||
$mul: { abc: 123 }, | ||
$pull: { xyz: "foo" }, | ||
}; | ||
assert.deepEqual(validateUpdateOperation(operation), { | ||
valid: true, | ||
errors: [], | ||
}); | ||
}); | ||
|
||
it("returns invalid when an operation has the same simple document path in two different operators", () => { | ||
const operation = { | ||
$inc: { foo: 12 }, | ||
$addToSet: { foo: "xyz" }, | ||
}; | ||
assert.deepEqual(validateUpdateOperation(operation), { | ||
valid: false, | ||
errors: [ | ||
new Error("Updating the path 'foo' would create a conflict at 'foo'"), | ||
], | ||
}); | ||
}); | ||
|
||
it("returns invalid when an operation has the same dotted document path in two different operators", () => { | ||
const operation = { | ||
$set: { "foo.bar": 12 }, | ||
$push: { "foo.bar": "xyz" }, | ||
}; | ||
assert.deepEqual(validateUpdateOperation(operation), { | ||
valid: false, | ||
errors: [ | ||
new Error( | ||
"Updating the path 'foo.bar' would create a conflict at 'foo.bar'" | ||
), | ||
], | ||
}); | ||
}); | ||
|
||
it("returns invalid when an operation has the same complex document path in two different operators", () => { | ||
const operation = { | ||
$set: { "foo.bar[1].baz": 12 }, | ||
$push: { "foo.bar[1].baz": "xyz" }, | ||
}; | ||
assert.deepEqual(validateUpdateOperation(operation), { | ||
valid: false, | ||
errors: [ | ||
new Error( | ||
"Updating the path 'foo.bar[1].baz' would create a conflict at 'foo.bar[1].baz'" | ||
), | ||
], | ||
}); | ||
}); | ||
|
||
it("returns invalid when an operation has two document paths, one of which is contained by the other", () => { | ||
const operation = { | ||
$set: { foo: [1] }, | ||
$inc: { "foo[0]": 1 }, | ||
}; | ||
assert.deepEqual(validateUpdateOperation(operation), { | ||
valid: false, | ||
errors: [ | ||
new Error( | ||
"Updating the path 'foo[0]' would create a conflict at 'foo'" | ||
), | ||
], | ||
}); | ||
}); | ||
|
||
it("returns invalid when an operation has two document paths, one of which is contained by the other", () => { | ||
const operation = { | ||
$set: { foo: [1] }, | ||
$inc: { "foo[0]": 1 }, | ||
}; | ||
assert.deepEqual(validateUpdateOperation(operation), { | ||
valid: false, | ||
errors: [ | ||
new Error( | ||
"Updating the path 'foo[0]' would create a conflict at 'foo'" | ||
), | ||
], | ||
}); | ||
}); | ||
|
||
it("returns invalid when an operation has two nested document paths, one of which is contained by the other", () => { | ||
const operation = { | ||
$set: { "foo.bar.baz": [1] }, | ||
$inc: { "foo.bar": 1 }, | ||
}; | ||
assert.deepEqual(validateUpdateOperation(operation), { | ||
valid: false, | ||
errors: [ | ||
new Error( | ||
"Updating the path 'foo.bar.baz' would create a conflict at 'foo.bar'" | ||
), | ||
], | ||
}); | ||
}); | ||
it("returns multiple errors when an operation has two or more conflicts", () => { | ||
const operation = { | ||
$set: { foo: { bar: [1] } }, | ||
$inc: { "foo.bar[0]": 1 }, | ||
$addToSet: { "foo.bar": 4 }, | ||
}; | ||
assert.deepEqual(validateUpdateOperation(operation), { | ||
valid: false, | ||
errors: [ | ||
new Error( | ||
"Updating the path 'foo.bar[0]' would create a conflict at 'foo'" | ||
), | ||
new Error( | ||
"Updating the path 'foo.bar[0]' would create a conflict at 'foo.bar'" | ||
), | ||
new Error( | ||
"Updating the path 'foo.bar' would create a conflict at 'foo'" | ||
), | ||
], | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters