-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add SCIM V2 PATCH compilePath support; fix bug in ABNF file. #7
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
patchPath = (attributeGroup *1("." attributePathSegment)) / attributePath | ||
|
||
filter = infixLogicalExpression / expression | ||
|
||
expression = precedenceGroup / attributeGroup / prefixLogicalExpression / postfixAssertion / infixAssertion | ||
|
@@ -20,7 +22,7 @@ infixAssertion = attributePath SP infixAssertionOperator SP infixAssertionValue | |
infixAssertionOperator = "eq" / "ne" / "co" / "sw" / "ew" / "gt" / "lt" / "ge" / "le" | ||
infixAssertionValue = null / true / false / number / string | ||
|
||
attributePath = [URI ":"] attributePathSegment *("." attributePathSegment) | ||
attributePath = [URI ":"] attributePathSegment *1("." attributePathSegment) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was the bug; please see RFC 7644 3.4.2.2. In short, we need an upper bound of 1 on the number of allowed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed, this was actually a deliberate deviation from the spec for an internal use-case, and I should have documented the difference. However, the use-case has since disappeared so it's probably appropriate to revert to spec-compliant behavior (even if I personally dislike the arbitrary depth limit). Because this deviation was never documented I'll consider this as a "bug fix" and not a major breaking change, as we shouldn't have been depending on this behavior. |
||
attributePathSegment = ALPHA *("-" / "_" / DIGIT / ALPHA) | ||
|
||
; rfc7159 | ||
|
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
{ | ||
"name": "scim-query-filter-parser", | ||
"version": "2.0.4", | ||
"description": "Parser for SCIM Filter Query Strings", | ||
"description": "Parser for SCIM V2 filters and paths", | ||
"main": "dist/index.js", | ||
"scripts": { | ||
"format": "prettier --list-different --write '**/*.{json,yml,md,ts}'", | ||
|
@@ -21,7 +21,12 @@ | |
"scim", | ||
"scimplecloud", | ||
"parser", | ||
"filter" | ||
"filter", | ||
"path", | ||
"patch", | ||
"scimv2", | ||
"rfc7644", | ||
"rfc7643" | ||
], | ||
"author": "Mike Marcacci <[email protected]>", | ||
"license": "MIT", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,23 @@ | ||
type Expression = (data: any) => boolean; | ||
export type Expression = (data: any) => boolean; | ||
|
||
export type SimplePath = { | ||
path: string; | ||
filter: null; | ||
subpath: null; | ||
}; | ||
|
||
export type FilterPath = { | ||
path: string; | ||
filter: Expression; | ||
subpath: string | null; | ||
}; | ||
Comment on lines
+3
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to have to think about this one. Version 2 of this library is built around the paradigm of returning code that "executes the query" rather than returning "information about the query". The reason for this is to maximize the amount of spec compliance this library can provide (traversing the object correctly and safely, for example, is not exactly trivial). Instinctually, I would rather return Would this kind of strategy work for your use-case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How would you feel about an API that compiled a PATCH path into the following: compilePatch(input: string): function<T=unknown, M=unknown>(
data: T,
modifier: (before: M, path: string[]) => M
): T This would allow your modifier to make any path-specific decisions without having to actually traverse the object. The library could also enforce a copy-on-write policy such that the original data object is never modified. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
V3 "Pluggable compilers" could be built around the paradigm of supporting arbitrary paradigms! We could do a minor refactor, then ship this library with some out-of-the-box compilers including: // "safe, avoid footgun, at the expense of performance" paradigm.
compileFilterToExpression() // what V2 does
// less safe, but more versatile
compileFilterToAST() // what V1 did
compileAttributePathToFieldArray() // what V2's parseAttributePath does This would not only support all existing functionality in V1 and V2, but also allow devs like me to implement custom compilers without submitting PRs! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As for your example, I'm not sure what you mean with // simplified for brevity
const myUserResource = getSCIMUserResource(...);
const patchOps = req.body.Operations;
for (const op of patchOps){
try {
const applyPatchOp = compilePatch(op)
applyPatchOp(myUserResource, true) // true to mutate
} catch (err) {
res.status(400).json(SCIM_INVALID_PATCH_OP_ERR)
return;
}
};
// Cool! Our user resource is now patched.
// Update DB, send result to client
updateDB(myUserResource);
res.status(200).json(myUserResource); I'm not sure how much extra work that would be on this PR, but my intuition says it is not trivial because of all of the little rules around what constitutes valid SCIM patch ops, and how they get applied. My current solution is:
|
||
|
||
class TrackMap { | ||
filter = [] as Expression[]; | ||
patchPath = [] as (SimplePath | FilterPath)[]; | ||
expression = [] as Expression[]; | ||
precedenceGroup = [] as Expression[]; | ||
attributeGroup = [] as Expression[]; | ||
attributeGroup = [] as [string[][], Expression][]; | ||
prefixLogicalExpression = [] as Expression[]; | ||
prefixLogicalExpressionOperator = [] as (( | ||
e: Expression, | ||
|
@@ -28,6 +41,7 @@ class TrackMap { | |
|
||
class Stat { | ||
filter = 0 as number; | ||
patchPath = 0 as number; | ||
expression = 0 as number; | ||
precedenceGroup = 0 as number; | ||
attributeGroup = 0 as number; | ||
|
@@ -47,6 +61,7 @@ class Stat { | |
|
||
class StatsMap { | ||
filter = [] as Stat[]; | ||
patchPath = [] as Stat[]; | ||
expression = [] as Stat[]; | ||
precedenceGroup = [] as Stat[]; | ||
attributeGroup = [] as Stat[]; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { ids } from "apg-lib"; | ||
import { Yard } from "./Yard"; | ||
|
||
export function patchPath( | ||
state: typeof ids.SEM_PRE | typeof ids.SEM_POST, | ||
chars: number[], | ||
phraseIndex: number, | ||
phraseLength: number, | ||
yard: Yard | ||
): typeof ids.SEM_OK | typeof ids.SEM_SKIP { | ||
switch (state) { | ||
case ids.SEM_PRE: | ||
yard.pre("patchPath"); | ||
break; | ||
|
||
case ids.SEM_POST: | ||
const { attributePath, attributeGroup, attributePathSegment } = yard.post( | ||
"patchPath" | ||
); | ||
|
||
const children = [...attributePath, ...attributeGroup]; | ||
|
||
if (children.length !== 1) { | ||
throw new Error( | ||
`INVARIANT: Expected 1 patchPath, but got ${children.length};` | ||
); | ||
} | ||
|
||
if (attributePath.length) { | ||
yard.tracks.patchPath.push({ | ||
path: attributePath[0].join("."), | ||
subpath: null, | ||
filter: null | ||
}); | ||
} else { | ||
const nestedAttributePath = attributeGroup[0][0]; | ||
yard.tracks.patchPath.push({ | ||
path: nestedAttributePath[0].join("."), | ||
filter: attributeGroup[0][1], | ||
subpath: attributePathSegment.length | ||
? attributePathSegment.reverse().join(".") | ||
: null | ||
}); | ||
} | ||
|
||
break; | ||
} | ||
|
||
return ids.SEM_OK; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used parenthesis here because apparently
apg-lib
's precedence rules are contradictory to RFC 5234 3.10.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fascinating and great catch on the precedence bug.