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 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`** evaluates create- and update-side policies at the application layer (MERGE has no WHERE). Documented limit; full MERGE-aware enforcement is not yet implemented.
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[]): MutationResult;
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 (updateSets.length > 0)
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
- if (this.strictMode.strict && labels && labels.length > 0)
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 (labels &&
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)