grafeo-ogm 2.6.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +3 -1
- package/dist/cjs/compilers/mutation.compiler.d.ts +26 -2
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +90 -6
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/model.d.ts +26 -0
- package/dist/cjs/model.d.ts.map +1 -1
- package/dist/cjs/model.js +70 -7
- package/dist/cjs/model.js.map +1 -1
- package/dist/esm/compilers/mutation.compiler.d.ts +26 -2
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +90 -6
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/model.d.ts +26 -0
- package/dist/esm/model.d.ts.map +1 -1
- package/dist/esm/model.js +71 -8
- package/dist/esm/model.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.6.1 (2026-10-18) — security fixes
|
|
4
|
+
|
|
5
|
+
> **Security patch.** Fixes two authorization bypasses in node-level security, both present since policies were introduced: `upsert` ignored the `update` row filter on an existing node, and mutation responses returned nodes the context cannot read. Both affect **1.7.0–2.6.0**. OGMs without `policies` compile the same policy-free Cypher as before. Upgrade if you use `policies`.
|
|
6
|
+
|
|
7
|
+
### Security
|
|
8
|
+
|
|
9
|
+
- **`upsert` rewrote existing nodes the context could not update (high).** The MERGE put the `update` input in `ON MATCH SET` with no policy predicate. Only the update `WITH CHECK` restrictives ran, at the application layer. So a context for which a direct `update` matched 0 rows could still rewrite the node through `upsert`, including the owner field that the `update` policy keys on, and so take ownership of it. With `policies: { Chart: [permissive({ operations: ['update'], when: (c) => ({ ownerId: c.uid }) })] }`, user `u1` could run `Chart.upsert({ where: { id: 'c2' }, create: {…}, update: { ownerId: 'u1' } })` on a chart owned by `u2` and become its owner. The MERGE now applies the type's `update` row filter to the node it matches. That is the same clause a direct `update` puts in its `WHERE`: the applicable `update` permissives OR-ed (inherited interface policies included), and `false` when none applies. The update `WITH CHECK` still runs on the input. `createMany({ skipDuplicates: true, labels })` had the same gap in default mode: the runtime `labels` were `SET` on every existing node the MERGE matched. A matched node is now relabelled only when it passes the `update` row filter (a label change is an update, as `setLabels` enforces). Introduced with policies in 1.7.0 (`90ef75e`, v1.7.0-beta.0), where the upsert MERGE carried no row predicate.
|
|
10
|
+
- **Mutation responses returned nodes the context cannot read (medium).** `create`, `update` and `upsert` returned every node they wrote. Nested selections were filtered by their target types' `read` policies, but the root node was not. A context that could update a node, but whose `read` policy hid it, got the whole node back, including the fields `find` withholds. `update({ where: { id }, update: {} })` was enough to read it. A context that could create a node its read policy hides got it back the same way. The response projection now applies the root type's `read` policy to each node in its post-write state, just as `find` would filter it. Writes are unchanged: an `update` still needs only the `update` policy, as documented for nested writes. Present since 1.7.0, whose mutation projections passed the `read` bundle to nested selections only.
|
|
11
|
+
|
|
12
|
+
### Behavior change
|
|
13
|
+
|
|
14
|
+
- **`upsert` on an existing node** is now updated only when the node passes the type's `update` row filter. If it fails the filter, nothing is written (no update and no create) and `upsert` returns `null`, as a direct `update` that matches 0 rows returns no row. The create path (no node matches) is unchanged, and the matched node does not need to be readable, as with `update`. Under `policyDefaults: { onDeny: 'throw' }`, `upsert` still throws `PolicyDeniedError` before compile when no `update` permissive applies, as `update` does. **`createMany({ skipDuplicates: true, labels })`** in default mode no longer relabels existing nodes that fail the `update` row filter. It never throws for this, because the call is a create. Types without an `update` policy, overrides and bypasses compile the MERGE exactly as before.
|
|
15
|
+
- **Mutation responses** leave out every node the context cannot read after the write. It is missing from `create` / `update`'s `<plural>` array, and `upsert` returns `null`. The write still lands, and `info` counters still count it. Types without a `read` policy, overrides and bypasses compile exactly as before.
|
|
16
|
+
|
|
3
17
|
## 2.6.0 (2026-10-18) — strict mode
|
|
4
18
|
|
|
5
19
|
> **Minor release.** It adds an opt-in `strict` mode with the corrected behaviour for input shapes that 2.x compiles in surprising ways. **There are no behaviour changes without `strict`.** With `strict` unset or `false`, every input compiles to the same Cypher as in 2.5.4 and returns the same results. The one exception is the aggregate fix under Fixes, where a query that used to crash now returns `null` for the affected keys. Apart from that, the only difference is the deprecation warnings described below. Generated types are unchanged.
|
package/README.md
CHANGED
|
@@ -1526,6 +1526,8 @@ Writes follow one principle: **a nested write enforces exactly what the equivale
|
|
|
1526
1526
|
|
|
1527
1527
|
Write restrictives and create default-deny are checked before the query runs, so a rejected nested write throws `PolicyDeniedError` and nothing is written.
|
|
1528
1528
|
|
|
1529
|
+
**Mutation responses are reads.** The nodes that `create`, `update` and `upsert` return are filtered by the root type's `read` policy, checked against the node as it is after the write, just as `find` would filter them. A write to a node the context may update or create but not read still happens, but that node is left out of the response: it is missing from the `<plural>` array, and `upsert` returns `null`. The `info` counters still count it. Nested selections in the response were already filtered by their own target-type policies. Before v2.6.1 the root node came back in full, including fields its `read` policy hides.
|
|
1530
|
+
|
|
1529
1531
|
> **A type without policies is open, but not a gateway.** Before v2.3.0, reading or writing through a type with no policies of its own skipped the policies of the types beyond it, and nested writes enforced no target-type policy at all. If a nested write into a protected type now gets filtered or denied, add the matching policy for that operation on the target type, or perform the write directly on its model.
|
|
1530
1532
|
|
|
1531
1533
|
#### Enforcement boundary
|
|
@@ -1614,7 +1616,7 @@ How it works:
|
|
|
1614
1616
|
#### Limitations
|
|
1615
1617
|
|
|
1616
1618
|
- **`@cypher` scalar fields inside a policy `where`-partial throw when the policy is injected into nested-selection enforcement.** Refactor the policy to use stored properties or a relationship traversal.
|
|
1617
|
-
- **`upsert`**
|
|
1619
|
+
- **`upsert`** enforces exactly what the equivalent direct calls would. When no node matches the `where`, it creates one under the `create` policy (default-deny and `WITH CHECK`). When a node matches, the type's `update` row filter applies to it, the same clause a direct `update` puts in its `WHERE`: the node is updated only if it passes, and if it fails nothing is written and `upsert` returns `null`, just as a direct `update` matches 0 rows. The update input is also checked against the update `WITH CHECK`. As with `update`, the matched node does not have to be readable. With `skipDuplicates`, `createMany` adds default-mode `labels` to an existing node it matches only if that node passes the `update` row filter. Before v2.6.1 only the `WITH CHECK` ran, so `upsert` could rewrite any existing node, including the owner field the update policy keys on.
|
|
1618
1620
|
- **Restrictives are split into read-side and write-side flavors.** A restrictive's `operations` array determines which `when` signature applies. Read-side ops (`read|delete|aggregate|count`) → `when(ctx)` returns a where-partial or boolean. Write-side ops (`create|update`) → `when(ctx, input)` returns a boolean only. Mixed arrays (e.g. `['read', 'create']`) are rejected at construction time — split into two restrictives. Each flavor is invoked exactly once per query (read-side at compile, write-side at the application layer). Use `isReadRestrictive` / `isWriteRestrictive` if you need to inspect a policy at runtime.
|
|
1619
1621
|
- **InterfaceModel branches.** Each implementer's `CASE` branch is exactly the policy clause that implementer's own model enforces, including the interface-level policies it inherits. An implementer with no policies of its own therefore gets only the interface-level ones. The OGM emits a `logger.warn` at construction time when an interface has policies and one of its implementers does not. Silence it by registering an explicit policy on each implementer.
|
|
1620
1622
|
- **`edge` filters in nested-write `where`** (connect, disconnect, nested update and delete) are not supported and throw.
|
|
@@ -7,6 +7,16 @@ export interface MutationResult {
|
|
|
7
7
|
cypher: string;
|
|
8
8
|
params: Record<string, unknown>;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* v2.6.1 — a row filter on `n` that gates the MATCH side of a MERGE (the
|
|
12
|
+
* root type's `update` policy, compiled by `WhereCompiler.compile`). An
|
|
13
|
+
* empty `cypher` means "no filter" and leaves the emitted Cypher unchanged.
|
|
14
|
+
*/
|
|
15
|
+
export interface MatchGuard {
|
|
16
|
+
cypher: string;
|
|
17
|
+
params: Record<string, unknown>;
|
|
18
|
+
preludes?: string[];
|
|
19
|
+
}
|
|
10
20
|
/**
|
|
11
21
|
* Compiles create, update, delete, and label mutations into Cypher + params.
|
|
12
22
|
*/
|
|
@@ -228,8 +238,14 @@ export declare class MutationCompiler {
|
|
|
228
238
|
/**
|
|
229
239
|
* Generate MERGE (upsert) Cypher with ON CREATE SET / ON MATCH SET.
|
|
230
240
|
* Scalar properties only — nested relationship ops are not supported.
|
|
241
|
+
*
|
|
242
|
+
* v2.6.1 — `updateGuard` is the root type's `update` row filter on `n`
|
|
243
|
+
* (compiled by the caller with `preserveVars: ['__matched']`). When it
|
|
244
|
+
* is non-empty, an EXISTING node is updated only when it passes the
|
|
245
|
+
* guard, exactly like a direct `update`; one that fails it is left
|
|
246
|
+
* untouched and no row is returned. See `buildGuardedMerge`.
|
|
231
247
|
*/
|
|
232
|
-
compileMerge(where: Record<string, unknown>, create: Record<string, unknown>, update: Record<string, unknown>, nodeDef: NodeDefinition, labels?: string[]): MutationResult;
|
|
248
|
+
compileMerge(where: Record<string, unknown>, create: Record<string, unknown>, update: Record<string, unknown>, nodeDef: NodeDefinition, labels?: string[], updateGuard?: MatchGuard): MutationResult;
|
|
233
249
|
/**
|
|
234
250
|
* Generate SET/REMOVE labels Cypher.
|
|
235
251
|
*/
|
|
@@ -243,7 +259,15 @@ export declare class MutationCompiler {
|
|
|
243
259
|
* Scalar properties only — no nested relationship operations.
|
|
244
260
|
* Returns count of created nodes.
|
|
245
261
|
*/
|
|
246
|
-
compileCreateMany(data: Record<string, unknown>[], nodeDef: NodeDefinition, skipDuplicates?: boolean, labels?: string[]
|
|
262
|
+
compileCreateMany(data: Record<string, unknown>[], nodeDef: NodeDefinition, skipDuplicates?: boolean, labels?: string[],
|
|
263
|
+
/**
|
|
264
|
+
* v2.6.1 — the root type's `update` row filter on `n`. With
|
|
265
|
+
* `skipDuplicates` and default-mode `labels`, an EXISTING node the
|
|
266
|
+
* MERGE matched is relabelled only when it passes this guard (a label
|
|
267
|
+
* change is an update, as `setLabels` enforces); created nodes get the
|
|
268
|
+
* labels in `ON CREATE SET`.
|
|
269
|
+
*/
|
|
270
|
+
updateGuard?: MatchGuard): MutationResult;
|
|
247
271
|
/**
|
|
248
272
|
* Resolve the PropertyDefinition of an edge (relationship) property so
|
|
249
273
|
* Int/BigInt edge writes get the same Integer coercion as node writes.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutation.compiler.d.ts","sourceRoot":"","sources":["../../../src/compilers/mutation.compiler.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"mutation.compiler.d.ts","sourceRoot":"","sources":["../../../src/compilers/mutation.compiler.ts"],"names":[],"mappings":"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"}
|
|
@@ -603,8 +603,14 @@ class MutationCompiler {
|
|
|
603
603
|
/**
|
|
604
604
|
* Generate MERGE (upsert) Cypher with ON CREATE SET / ON MATCH SET.
|
|
605
605
|
* Scalar properties only — nested relationship ops are not supported.
|
|
606
|
+
*
|
|
607
|
+
* v2.6.1 — `updateGuard` is the root type's `update` row filter on `n`
|
|
608
|
+
* (compiled by the caller with `preserveVars: ['__matched']`). When it
|
|
609
|
+
* is non-empty, an EXISTING node is updated only when it passes the
|
|
610
|
+
* guard, exactly like a direct `update`; one that fails it is left
|
|
611
|
+
* untouched and no row is returned. See `buildGuardedMerge`.
|
|
606
612
|
*/
|
|
607
|
-
compileMerge(where, create, update, nodeDef, labels) {
|
|
613
|
+
compileMerge(where, create, update, nodeDef, labels, updateGuard) {
|
|
608
614
|
const labelStr = this.getCachedLabelString(nodeDef);
|
|
609
615
|
const lines = [];
|
|
610
616
|
const params = {};
|
|
@@ -730,9 +736,15 @@ class MutationCompiler {
|
|
|
730
736
|
updateSets.push(`n.${(0, validation_1.escapeIdentifier)(key)} = ${valueExpr}`);
|
|
731
737
|
params[paramName] = (0, write_coercion_1.coerceWriteValue)(value, propDef);
|
|
732
738
|
}
|
|
733
|
-
if (
|
|
739
|
+
if (updateGuard !== undefined && updateGuard.cypher !== '') {
|
|
740
|
+
const guarded = buildGuardedMerge(labelStr, mergeProps, updateGuard, updateSets, params);
|
|
741
|
+
lines.unshift(...guarded.before);
|
|
742
|
+
lines.push(...guarded.after);
|
|
743
|
+
}
|
|
744
|
+
else if (updateSets.length > 0)
|
|
734
745
|
lines.push(`ON MATCH SET ${updateSets.join(', ')}`);
|
|
735
|
-
// Extra labels
|
|
746
|
+
// Extra labels. v2.6.1 — with an update guard, the rows that reach this
|
|
747
|
+
// SET are the created node and an existing node that passed the guard.
|
|
736
748
|
if (labels && labels.length > 0 && !labelsOnCreate) {
|
|
737
749
|
const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
|
|
738
750
|
lines.push(`SET n:${validatedLabels}`);
|
|
@@ -782,7 +794,15 @@ class MutationCompiler {
|
|
|
782
794
|
* Scalar properties only — no nested relationship operations.
|
|
783
795
|
* Returns count of created nodes.
|
|
784
796
|
*/
|
|
785
|
-
compileCreateMany(data, nodeDef, skipDuplicates, labels
|
|
797
|
+
compileCreateMany(data, nodeDef, skipDuplicates, labels,
|
|
798
|
+
/**
|
|
799
|
+
* v2.6.1 — the root type's `update` row filter on `n`. With
|
|
800
|
+
* `skipDuplicates` and default-mode `labels`, an EXISTING node the
|
|
801
|
+
* MERGE matched is relabelled only when it passes this guard (a label
|
|
802
|
+
* change is an update, as `setLabels` enforces); created nodes get the
|
|
803
|
+
* labels in `ON CREATE SET`.
|
|
804
|
+
*/
|
|
805
|
+
updateGuard) {
|
|
786
806
|
if (data.length === 0)
|
|
787
807
|
return { cypher: 'RETURN 0 AS count', params: {} };
|
|
788
808
|
const labelStr = this.getCachedLabelString(nodeDef);
|
|
@@ -855,6 +875,12 @@ class MutationCompiler {
|
|
|
855
875
|
});
|
|
856
876
|
params.items = sanitizedItems;
|
|
857
877
|
lines.push('UNWIND $items AS item');
|
|
878
|
+
const guardedLabels = Boolean(skipDuplicates) &&
|
|
879
|
+
!this.strictMode.strict &&
|
|
880
|
+
labels !== undefined &&
|
|
881
|
+
labels.length > 0 &&
|
|
882
|
+
updateGuard !== undefined &&
|
|
883
|
+
updateGuard.cypher !== '';
|
|
858
884
|
if (skipDuplicates) {
|
|
859
885
|
// MERGE key: the unique/id fields EVERY item supplies — an item
|
|
860
886
|
// missing one would MERGE on null, which Neo4j rejects.
|
|
@@ -887,7 +913,11 @@ class MutationCompiler {
|
|
|
887
913
|
params[paramName] = bound;
|
|
888
914
|
}
|
|
889
915
|
// v2.6.0 — strict: labels only on the nodes MERGE creates.
|
|
890
|
-
|
|
916
|
+
// v2.6.1 — also with an update guard: the matched nodes are
|
|
917
|
+
// relabelled below, only where the guard passes.
|
|
918
|
+
if ((this.strictMode.strict || guardedLabels) &&
|
|
919
|
+
labels &&
|
|
920
|
+
labels.length > 0)
|
|
891
921
|
onCreateSets.push(`n:${labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':')}`);
|
|
892
922
|
if (onCreateSets.length > 0)
|
|
893
923
|
lines.push(`ON CREATE SET ${onCreateSets.join(', ')}`);
|
|
@@ -908,7 +938,20 @@ class MutationCompiler {
|
|
|
908
938
|
lines.push(`CREATE (n:${labelStr} { ${propParts.join(', ')} })`);
|
|
909
939
|
}
|
|
910
940
|
// Extra labels
|
|
911
|
-
if (
|
|
941
|
+
if (guardedLabels) {
|
|
942
|
+
// v2.6.1 — matched nodes are relabelled only where the update guard
|
|
943
|
+
// passes. Pre-2.6.1 `SET n:<labels>` ran on every node the MERGE
|
|
944
|
+
// returned, so an existing node the context could not update was
|
|
945
|
+
// relabelled. FOREACH keeps one row per item (invariant 2).
|
|
946
|
+
const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
|
|
947
|
+
(0, validation_1.mergeParams)(params, updateGuard.params);
|
|
948
|
+
lines.push('WITH n');
|
|
949
|
+
if (updateGuard.preludes && updateGuard.preludes.length > 0)
|
|
950
|
+
lines.push(...updateGuard.preludes);
|
|
951
|
+
lines.push(`FOREACH (__l IN CASE WHEN ${updateGuard.cypher} THEN [1] ELSE [] END | SET n:${validatedLabels})`);
|
|
952
|
+
this.labelsOnMatchedNodes(nodeDef, 'createMany');
|
|
953
|
+
}
|
|
954
|
+
else if (labels &&
|
|
912
955
|
labels.length > 0 &&
|
|
913
956
|
!(skipDuplicates && this.strictMode.strict)) {
|
|
914
957
|
const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
|
|
@@ -2197,6 +2240,47 @@ function upsertRelationshipError(nodeDef, key, branch) {
|
|
|
2197
2240
|
return new errors_1.OGMError(`upsert does not support relationship fields: "${key}" in "${branch}" (${nodeDef.typeName}). ` +
|
|
2198
2241
|
`Use create() / update() for nested relationship operations.`);
|
|
2199
2242
|
}
|
|
2243
|
+
/**
|
|
2244
|
+
* v2.6.1 — the lines around an upsert MERGE that apply the root type's
|
|
2245
|
+
* `update` row filter to the node it MATCHES, as a direct `update` does:
|
|
2246
|
+
*
|
|
2247
|
+
* OPTIONAL MATCH (__existing:L { key }) ┐ before
|
|
2248
|
+
* WITH count(__existing) > 0 AS __matched ┘
|
|
2249
|
+
* MERGE (n:L { key })
|
|
2250
|
+
* ON CREATE SET …
|
|
2251
|
+
* WITH n, __matched ┐ after
|
|
2252
|
+
* [@cypher preludes of the guard] │
|
|
2253
|
+
* WHERE NOT __matched OR (<guard>) │
|
|
2254
|
+
* FOREACH (__u IN CASE WHEN __matched THEN [1] ELSE [] END | SET …)
|
|
2255
|
+
*
|
|
2256
|
+
* A created node passes untouched by the update sets; an existing node is
|
|
2257
|
+
* updated only when it passes the guard; one that fails it is neither
|
|
2258
|
+
* updated nor returned (and the MERGE created nothing). Pre-2.6.1 the
|
|
2259
|
+
* update went in `ON MATCH SET` with no row filter, so any context the
|
|
2260
|
+
* WITH CHECK admitted rewrote any existing node — even its owner field.
|
|
2261
|
+
*
|
|
2262
|
+
* `count()` keeps one row before the MERGE whether 0, 1 or several nodes
|
|
2263
|
+
* match (invariant 2); FOREACH needs no subquery after the write
|
|
2264
|
+
* (invariant 1). Under a race, a node created between the OPTIONAL MATCH
|
|
2265
|
+
* and the MERGE is matched with `__matched = false`: its properties are
|
|
2266
|
+
* not updated (default-mode runtime `labels`, SET after this, still are).
|
|
2267
|
+
*/
|
|
2268
|
+
function buildGuardedMerge(labelStr, mergeProps, guard, updateSets, params) {
|
|
2269
|
+
(0, validation_1.mergeParams)(params, guard.params);
|
|
2270
|
+
const after = ['WITH n, __matched'];
|
|
2271
|
+
if (guard.preludes && guard.preludes.length > 0)
|
|
2272
|
+
after.push(...guard.preludes);
|
|
2273
|
+
after.push(`WHERE NOT __matched OR (${guard.cypher})`);
|
|
2274
|
+
if (updateSets.length > 0)
|
|
2275
|
+
after.push(`FOREACH (__u IN CASE WHEN __matched THEN [1] ELSE [] END | SET ${updateSets.join(', ')})`);
|
|
2276
|
+
return {
|
|
2277
|
+
before: [
|
|
2278
|
+
`OPTIONAL MATCH (__existing:${labelStr} { ${mergeProps.join(', ')} })`,
|
|
2279
|
+
'WITH count(__existing) > 0 AS __matched',
|
|
2280
|
+
],
|
|
2281
|
+
after,
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2200
2284
|
/** `null` / `{}`: the whole-relationship (blanket) form, never per member. */
|
|
2201
2285
|
function isBlanketSpec(spec) {
|
|
2202
2286
|
if (spec == null)
|