grafeo-ogm 1.5.0 → 1.7.0-beta.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 +110 -1
- package/README.md +289 -2
- package/dist/cjs/compilers/mutation.compiler.d.ts +3 -0
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +8 -0
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/selection.compiler.d.ts +31 -1
- package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/selection.compiler.js +158 -34
- package/dist/cjs/compilers/selection.compiler.js.map +1 -1
- package/dist/cjs/compilers/where.compiler.d.ts +57 -0
- package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/where.compiler.js +259 -26
- package/dist/cjs/compilers/where.compiler.js.map +1 -1
- package/dist/cjs/execution/executor.d.ts +20 -0
- package/dist/cjs/execution/executor.d.ts.map +1 -1
- package/dist/cjs/execution/executor.js +28 -4
- package/dist/cjs/execution/executor.js.map +1 -1
- package/dist/cjs/index.d.ts +5 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +11 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/interface-model.d.ts +43 -1
- package/dist/cjs/interface-model.d.ts.map +1 -1
- package/dist/cjs/interface-model.js +231 -12
- package/dist/cjs/interface-model.js.map +1 -1
- package/dist/cjs/model.d.ts +118 -1
- package/dist/cjs/model.d.ts.map +1 -1
- package/dist/cjs/model.js +341 -51
- package/dist/cjs/model.js.map +1 -1
- package/dist/cjs/ogm.d.ts +115 -4
- package/dist/cjs/ogm.d.ts.map +1 -1
- package/dist/cjs/ogm.js +189 -2
- package/dist/cjs/ogm.js.map +1 -1
- package/dist/cjs/policy/errors.d.ts +22 -0
- package/dist/cjs/policy/errors.d.ts.map +1 -0
- package/dist/cjs/policy/errors.js +23 -0
- package/dist/cjs/policy/errors.js.map +1 -0
- package/dist/cjs/policy/resolver.d.ts +42 -0
- package/dist/cjs/policy/resolver.d.ts.map +1 -0
- package/dist/cjs/policy/resolver.js +116 -0
- package/dist/cjs/policy/resolver.js.map +1 -0
- package/dist/cjs/policy/types.d.ts +182 -0
- package/dist/cjs/policy/types.d.ts.map +1 -0
- package/dist/cjs/policy/types.js +130 -0
- package/dist/cjs/policy/types.js.map +1 -0
- package/dist/cjs/utils/cypher-field-projection.d.ts +64 -0
- package/dist/cjs/utils/cypher-field-projection.d.ts.map +1 -0
- package/dist/cjs/utils/cypher-field-projection.js +102 -0
- package/dist/cjs/utils/cypher-field-projection.js.map +1 -0
- package/dist/esm/compilers/mutation.compiler.d.ts +3 -0
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +8 -0
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/selection.compiler.d.ts +31 -1
- package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/selection.compiler.js +158 -34
- package/dist/esm/compilers/selection.compiler.js.map +1 -1
- package/dist/esm/compilers/where.compiler.d.ts +57 -0
- package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/where.compiler.js +259 -26
- package/dist/esm/compilers/where.compiler.js.map +1 -1
- package/dist/esm/execution/executor.d.ts +20 -0
- package/dist/esm/execution/executor.d.ts.map +1 -1
- package/dist/esm/execution/executor.js +28 -4
- package/dist/esm/execution/executor.js.map +1 -1
- package/dist/esm/index.d.ts +5 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interface-model.d.ts +43 -1
- package/dist/esm/interface-model.d.ts.map +1 -1
- package/dist/esm/interface-model.js +231 -12
- package/dist/esm/interface-model.js.map +1 -1
- package/dist/esm/model.d.ts +118 -1
- package/dist/esm/model.d.ts.map +1 -1
- package/dist/esm/model.js +341 -51
- package/dist/esm/model.js.map +1 -1
- package/dist/esm/ogm.d.ts +115 -4
- package/dist/esm/ogm.d.ts.map +1 -1
- package/dist/esm/ogm.js +187 -1
- package/dist/esm/ogm.js.map +1 -1
- package/dist/esm/policy/errors.d.ts +22 -0
- package/dist/esm/policy/errors.d.ts.map +1 -0
- package/dist/esm/policy/errors.js +19 -0
- package/dist/esm/policy/errors.js.map +1 -0
- package/dist/esm/policy/resolver.d.ts +42 -0
- package/dist/esm/policy/resolver.d.ts.map +1 -0
- package/dist/esm/policy/resolver.js +111 -0
- package/dist/esm/policy/resolver.js.map +1 -0
- package/dist/esm/policy/types.d.ts +182 -0
- package/dist/esm/policy/types.d.ts.map +1 -0
- package/dist/esm/policy/types.js +123 -0
- package/dist/esm/policy/types.js.map +1 -0
- package/dist/esm/utils/cypher-field-projection.d.ts +64 -0
- package/dist/esm/utils/cypher-field-projection.d.ts.map +1 -0
- package/dist/esm/utils/cypher-field-projection.js +97 -0
- package/dist/esm/utils/cypher-field-projection.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,114 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.0-beta.1] (2026-05-02)
|
|
4
|
+
|
|
5
|
+
> **BETA — republish of beta.0 with no code changes.** Install with `npm install grafeo-ogm@beta`.
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **CI publish workflow** now passes `--tag` to `npm publish`, derived from the version's pre-release suffix (`beta`, `alpha`, `rc`, etc.), defaulting to `latest` for stable releases. Required because npm refuses to publish prerelease versions without an explicit dist-tag. v1.7.0-beta.0 was tagged on GitHub but never reached npm because of this; v1.7.0-beta.1 is the first beta artifact actually available on the registry.
|
|
10
|
+
|
|
11
|
+
## [1.7.0-beta.0] (2026-05-02)
|
|
12
|
+
|
|
13
|
+
> **BETA — API may change before 1.7.0 final.** Install with `npm install grafeo-ogm@beta` (after the package is published to the `beta` dist-tag). The shape of the public API is settled, but feedback during the beta window may cause minor adjustments.
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
- **Node-Level Security (NLS).** New optional `policies` config on `OGMConfig` plus `OGM.withContext(ctx)` give you a Postgres-RLS-style filter layer that compiles into the existing WHERE pipeline. Policies return `<Node>Where` partials — every operator, quantifier, connection filter, and nested traversal already supported by `WhereCompiler` is automatically available. Three policy kinds: `override` (compile-time short-circuit; admin path is byte-identical to no-policy), `permissive` (OR'd grants with optional `appliesWhen` compile-time gate), `restrictive` (split into read-side and write-side flavors — see "Contract change" below). Vocabulary-neutral — no hardcoded role strings, you compose your own.
|
|
18
|
+
- **Nested-selection enforcement.** `SelectionCompiler` injects every target type's `'read'` policy into pattern comprehensions, connection edges, and union branches. Policies cannot be bypassed by traversal.
|
|
19
|
+
- **Default-deny baseline.** `policyDefaults.onDeny: 'empty'` (default) emits `WHERE false` when no permissive matches; `'throw'` raises `PolicyDeniedError` before the query runs.
|
|
20
|
+
- **Audit metadata.** Every OGM-emitted query when policies are configured attaches `tx.setMetaData({ ogmPolicySetVersion, ctxFingerprint, modelType, operation, policiesEvaluated, bypassed })`. `ctxFingerprint` is a SHA-256 of the SORTED ctx KEYS only — never values, never anything sensitive.
|
|
21
|
+
- **Escape hatches.** `ogm.unsafe.bypassPolicies()` returns a non-policy-aware OGM (logged via `logger.warn`); per-method `unsafe: { bypassPolicies: true }` skips policies for a single call (also logged).
|
|
22
|
+
- **Interface-aware enforcement.** `InterfaceModel.find()` / `aggregate()` emit a CASE-per-label WHERE that AND-combines each implementer's `'read'` policy with the interface-level policy. Concrete-type policies are NOT bypassed when querying through the interface.
|
|
23
|
+
- **`PolicyDeniedError`** — new public error class extending `OGMError`. Carries `typeName`, `operation`, `reason` (`'no-permissive-matched' | 'restrictive-rejected-input' | 'override-failed-validation'`), and optional `policyName`.
|
|
24
|
+
|
|
25
|
+
### Contract change during beta — read/write restrictive split
|
|
26
|
+
|
|
27
|
+
The pre-fix beta invoked `RestrictivePolicy.when` twice on every write path: once at the application layer with `(ctx, input)` (WITH CHECK semantics) and once at WHERE-compile with `(ctx)` only (row predicate). Side-effecting callbacks fired inconsistently and any predicate that legitimately depended on `input` returned `false` at compile time → `WHERE false` → reads silently blocked.
|
|
28
|
+
|
|
29
|
+
**Fix:** `RestrictivePolicy` is now a discriminated union over `operations`.
|
|
30
|
+
|
|
31
|
+
- **`ReadRestrictivePolicy`** — `operations` includes only `read|delete|aggregate|count`. `when(ctx)` returns a `<Node>Where` partial OR `false`. Compiles into the WHERE clause via `WhereCompiler`. Invoked exactly once per read-side query.
|
|
32
|
+
- **`WriteRestrictivePolicy`** — `operations` includes only `create|update`. `when(ctx, input)` returns a boolean (where-partial returns are no longer accepted; if you need a row filter on `update`, register a `ReadRestrictive` on `read`/`delete`). Runs at the application layer ("WITH CHECK"). Invoked exactly once per write op. The `cypher` escape hatch is not supported on write restrictives.
|
|
33
|
+
- **Mixed-operation arrays are rejected** at construction time with `OGMError`. Split a single `restrictive({ operations: ['read', 'create'], … })` into two restrictives — one per kind.
|
|
34
|
+
|
|
35
|
+
The `restrictive()` constructor uses TypeScript overloads so authoring code gets the correct `when` signature inferred from the literal `operations` tuple. New runtime helpers `isReadRestrictive` / `isWriteRestrictive` are exported for users who need to inspect a policy at runtime.
|
|
36
|
+
|
|
37
|
+
This is a contract change inside the beta window — the API has shifted before `1.7.0` final, which is exactly what the beta dist-tag exists for. No public types were removed; `RestrictivePolicy` remains a public type and resolves to the union of the two new flavors.
|
|
38
|
+
|
|
39
|
+
### Public API additions (additive only)
|
|
40
|
+
|
|
41
|
+
- New exports: `override`, `permissive`, `restrictive`, `isReadRestrictive`, `isWriteRestrictive`, `OGMWithContext`, `PolicyDeniedError`.
|
|
42
|
+
- New types: `Policy`, `OverridePolicy`, `PermissivePolicy`, `RestrictivePolicy`, `ReadRestrictivePolicy`, `WriteRestrictivePolicy`, `PolicyContext`, `PolicyContextBundle`, `Operation`, `OperationOrWildcard`, `ReadOperation`, `WriteOperation`, `PoliciesByModel`, `PolicyDefaults`, `ResolvedPolicies`, `UnsafeOptions`.
|
|
43
|
+
- `OGMConfig` gains optional `policies?` and `policyDefaults?`.
|
|
44
|
+
- `OGM` gains `withContext<C>(ctx: C)` and `unsafe.bypassPolicies()`.
|
|
45
|
+
- Every Model / InterfaceModel method's params bag gains optional `unsafe?: { bypassPolicies?: boolean }`.
|
|
46
|
+
- All existing call signatures remain valid. Calling code that doesn't pass policies must compile and run identically — byte-identical Cypher for non-policy calls.
|
|
47
|
+
|
|
48
|
+
### Install command (beta)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install grafeo-ogm@beta
|
|
52
|
+
# or
|
|
53
|
+
pnpm add grafeo-ogm@beta
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Known limits in this beta
|
|
57
|
+
|
|
58
|
+
- **`@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. (Top-level WHERE on the root model still supports `@cypher` filters.)
|
|
59
|
+
- **`upsert` evaluates create- and update-side policies at the application layer.** MERGE has no WHERE we can stitch into; the WHERE-side enforcement only covers the matching path. Documented limit; full MERGE-aware enforcement is deferred to v1.7.1.
|
|
60
|
+
- **Restrictive read/write split — fixed in this iteration.** The dual-invocation contract bug present in earlier beta builds is resolved. `ReadRestrictivePolicy` (`when(ctx)`) is invoked exactly once at WHERE-compile; `WriteRestrictivePolicy` (`when(ctx, input)`) is invoked exactly once at the application layer. See the "Contract change during beta" section above for the new shape and migration notes.
|
|
61
|
+
- **InterfaceModel CASE-per-label fallback.** When an interface has policies registered but a concrete implementer does not, the implementer's branch falls back to interface-level enforcement only (rather than default-denying the unlisted implementer). The OGM emits a `logger.warn` at construction time when this configuration is detected so it never passes silently. Strict per-implementer default-deny is being evaluated for `1.7.0` final.
|
|
62
|
+
- **AsyncLocalStorage opt-in is deferred to v1.7.1.** Beta is explicit `withContext()` only — create one wrapper per request and discard it.
|
|
63
|
+
- **Index requirement declaration deferred to v1.8.0.** No `requires.indexes` config in this beta.
|
|
64
|
+
- **EXPLAIN-in-test mode deferred to v1.8.0.** No dev hook in beta.
|
|
65
|
+
- **No live Neo4j integration test in this beta.** Mock-driver coverage is extensive (1317 specs incl. byte-identical regression of every v1.6.0 emission and the C1 read/write contract proofs), but a live `pnpm run test:integration` against a real Neo4j is a `1.7.0` final blocker.
|
|
66
|
+
|
|
67
|
+
### Unaffected paths (byte-identical to v1.6.0)
|
|
68
|
+
|
|
69
|
+
- An OGM constructed without `policies` emits identical Cypher to v1.6.0 for every covered operation. Verified in `tests/policy/byte-identical.spec.ts`.
|
|
70
|
+
- An OGM with `policies` but invoked via the bare `OGM.model()` path (no `withContext`) emits identical Cypher to v1.6.0.
|
|
71
|
+
- An override match emits identical Cypher to a no-policy query.
|
|
72
|
+
- `ogm.unsafe.bypassPolicies()` and per-call `unsafe: { bypassPolicies: true }` both emit identical Cypher to v1.6.0.
|
|
73
|
+
|
|
74
|
+
### Generated types
|
|
75
|
+
|
|
76
|
+
- Existing `<Node>Where`, `<Node>CreateInput`, `<Node>UpdateInput` are sufficient for typing policy callbacks. The generator's `ModelMap` already exposed `Where`, `CreateInput`, and `UpdateInput` keys per model so `PoliciesByModel<M, C>` can index into them. **No regeneration required when upgrading from v1.6.0 → v1.7.0-beta.0.**
|
|
77
|
+
|
|
78
|
+
### Migration
|
|
79
|
+
|
|
80
|
+
- v1.6.0 → v1.7.0-beta.0 is purely additive — no changes required unless you opt into NLS.
|
|
81
|
+
|
|
82
|
+
## 1.6.0 (2026-05-01)
|
|
83
|
+
|
|
84
|
+
### Features
|
|
85
|
+
|
|
86
|
+
- **`@cypher` scalar fields are now resolved at runtime in `WHERE`** — `find()`, `findFirst()`, `findUnique()`, `count()`, `aggregate()`, `update()`, `updateMany()`, `delete()`, `deleteMany()`, `upsert()`, `setLabels()`, `searchByVector()`, `searchByPhrase()`, and `InterfaceModel.find()` / `aggregate()` now resolve `@cypher` scalar fields when they appear as filter keys (with or without operator suffix — `_EQ`, `_CONTAINS`, `_GT`, `_LT`, `_GTE`, `_LTE`, `_IN`, `_NOT`, `_NOT_IN`, `_STARTS_WITH`, `_ENDS_WITH`, `_MATCHES`, `_NOT_CONTAINS`, etc.). Pre-1.6.0, the typed `<Node>Where` surface emitted these fields and the WHERE compiler would compile `n.<cypherField>` against a property that doesn't exist on the node — predicate always false → silent data omission. Each `@cypher` filter is now compiled into a `CALL { WITH n; WITH n AS this; <user statement> }` prelude before the `WHERE`, with the projected column renamed to a unique `__where_n_<fieldName>` alias the predicate references.
|
|
87
|
+
- **`@cypher` scalar fields are now resolved at runtime in `SELECT`** — both `selectionSet: '...'` (string SDL) and `select: { field: true }` (typed) paths now project `@cypher` scalar fields. Compiled into a `CALL { ... } WITH n, <col> AS __sel_n_<fieldName>` prelude before the `RETURN`, then projected into the map as `<fieldName>: __sel_n_<fieldName>`. Pre-1.6.0 these fields were silently emitted as `.\`<fieldName>\`` and returned NULL.
|
|
88
|
+
- **AND/OR/NOT composition de-dupes references** — a `@cypher` field referenced multiple times at the same scope (e.g. `OR: [{ field_GT: 1 }, { field_LT: 10 }]`) emits a single `CALL` prelude shared by all references.
|
|
89
|
+
- **Relationship-quantifier inner WHERE** — `@cypher` scalar fields on related nodes now work inside `_SOME` / `_NONE` / `_ALL` filters (e.g. `hasStatus_SOME: { statusLowerName_CONTAINS: 'act' }`). The inner `CALL` prelude is stitched directly inside the `EXISTS { MATCH pattern <prelude> WHERE <inner> }` body so each iteration projects the field for its own bound relationship variable.
|
|
90
|
+
- **Combined WHERE + SELECT + sort** — all three preludes can co-exist in the same query. Each uses a disjoint alias namespace (`__where_*`, `__sel_*`, `__sort_*`) and the carry chain is threaded so later `WITH` clauses preserve aliases from earlier preludes.
|
|
91
|
+
- **Mutation projections now project `@cypher` fields** — `update()`, `upsert()`, and `create()` honour `@cypher` fields in their `select` / `selectionSet` projections via the same SELECT prelude pipeline.
|
|
92
|
+
|
|
93
|
+
### Internal
|
|
94
|
+
|
|
95
|
+
- New helper `src/utils/cypher-field-projection.ts` exporting `buildCypherFieldCall()` and `CypherFieldScope`. `CypherFieldScope` is the central state holder used by both `WhereCompiler` and `SelectionCompiler`: it dedupes per-`(nodeVar, fieldName)` registrations, threads carried aliases through every emitted `WITH`, and accepts a `preserveVars` array so callers can keep surrounding pipeline vars (`__typename`, `score`) in scope.
|
|
96
|
+
- `WhereCompiler.compile()` now returns `WhereResult { cypher, params, preludes? }`. Top-level preludes are returned for the caller to stitch between MATCH and WHERE; inner-scope preludes (relationship quantifiers, union members) are stitched directly into the `EXISTS` body. A new `compile(...)` option `preserveVars` lets callers keep `score` (vector / fulltext) or `__typename` (interface) in scope across the prelude `WITH` chain.
|
|
97
|
+
- `MutationCompiler.compileUpdate()`, `compileDelete()`, and `compileSetLabels()` accept `whereResult.preludes` and emit them between MATCH and WHERE.
|
|
98
|
+
- `SelectionCompiler.compile()` accepts an optional `cypherScope` argument. When supplied, top-level `@cypher` scalar fields register there and the caller stitches the scope's emitted lines before the RETURN. When omitted (the default for nested recursive calls), `@cypher` scalar fields throw — pattern comprehensions cannot host CALL subqueries.
|
|
99
|
+
- `Model.compileOptions()` now accepts `preserveVars` so the sort prelude carries forward any aliases already projected by an earlier SELECT prelude.
|
|
100
|
+
|
|
101
|
+
### Notes & Limits
|
|
102
|
+
|
|
103
|
+
- **Scalar return types only.** Mirrors the v1.5.0 sort scope. `@cypher` fields whose declared return type is a node / interface / union are still projection-only (the `where-emitter` already excludes them, and the SELECT pipeline does not synthesize a sub-projection from a `@cypher` statement).
|
|
104
|
+
- **Aggregations are out of scope.** `aggregate()` continues to skip `@cypher` fields when emitting `min(n.<f>) / max / avg`. The aggregation emitters (`<Type>AggregationSelection`, `<Type>EdgeAggregateSelection`, etc.) already exclude `@cypher` from the typed surface.
|
|
105
|
+
- **`_SINGLE` quantifiers reject `@cypher` filters.** Both relationship `_SINGLE` and `Connection_SINGLE` use list comprehensions (`size([(pattern WHERE inner) | 1]) = 1`) which cannot host `CALL { ... }` subqueries. Using a `@cypher` field inside `_SINGLE` throws `OGMError` with a clear message — refactor to `_SOME` + `_NONE`.
|
|
106
|
+
- **Nested-relationship SELECT projection of `@cypher` fields is rejected.** `select: { hasStatus: { select: { statusLowerName: true } } }` (selecting a `@cypher` field on a related node inside a nested selection) throws `OGMError`. Nested relationships use list comprehensions for their pattern, and CALL subqueries cannot be embedded there. Top-level `@cypher` fields on the root model are fully supported. Workaround: query the related node directly via its own model.
|
|
107
|
+
- **Connection `where` does not accept `@cypher` filters.** `select: { hasStatusConnection: { where: { node: { statusLowerName_CONTAINS: 'x' } } } }` throws — same constraint as above.
|
|
108
|
+
- **`select.where` (Prisma-style relationship filtering) does not accept `@cypher` filters.** Same root cause; same error.
|
|
109
|
+
- **Stored-only queries are byte-identical to 1.5.0.** Only queries that reference at least one `@cypher` field gain the new prelude machinery. Every previous test passes without modification (1031 → 1092 tests after this release).
|
|
110
|
+
- **No new generic parameters or breaking type changes.** The `<Node>Where` and `<Node>Select` types are unchanged; if you regenerated against 1.4.x or 1.5.x, those types already exposed scalar `@cypher` fields and now they are correctly resolved at runtime.
|
|
111
|
+
|
|
3
112
|
## 1.5.0 (2026-04-30)
|
|
4
113
|
|
|
5
114
|
### Features
|
|
@@ -23,7 +132,7 @@
|
|
|
23
132
|
|
|
24
133
|
- **Sort-only.** This release implements `@cypher` resolution exclusively for `ORDER BY`. `select`, `selectionSet`, `where`, and `aggregate` continue to skip `@cypher` fields at runtime as in 1.4.0 — those use cases will be addressed in a separate release.
|
|
25
134
|
- **Pre-existing `where-emitter` footgun unchanged**: scalar `@cypher` fields are still emitted in `<Node>Where`, and the WHERE compiler still produces `n.<cypherField>` (which fails at Neo4j). This was already broken in 1.4.x and is out of scope for 1.5.0; track in a separate issue.
|
|
26
|
-
- **Statement convention.** `@cypher` statements must reference the bound node as `this` and end with `RETURN <expr> AS <columnName>` (or `RETURN <expr>` alone — the column name then defaults to the GraphQL field name). Example: `@cypher(statement: "RETURN toLower(this.
|
|
135
|
+
- **Statement convention.** `@cypher` statements must reference the bound node as `this` and end with `RETURN <expr> AS <columnName>` (or `RETURN <expr>` alone — the column name then defaults to the GraphQL field name). Example: `@cypher(statement: "RETURN toLower(this.title) AS insensitiveTitle")`. The OGM never modifies the user's statement — `this` is rebound by the wrapping `CALL` subquery.
|
|
27
136
|
- **Stored-field sorts produce byte-identical Cypher to 1.4.0.** Only sorts that resolve to a `@cypher` field gain the new CALL/WITH machinery; everything else is unchanged.
|
|
28
137
|
- **No new generic parameters or breaking type changes.** `TSort` keeps its 1.4.0 default (`Record<string, 'ASC' | 'DESC'>`) — un-regenerated consumers continue to compile against the older `<Node>Sort` shape.
|
|
29
138
|
|
package/README.md
CHANGED
|
@@ -213,6 +213,24 @@ await ogm.$transaction(async (ctx) => {
|
|
|
213
213
|
|
|
214
214
|
---
|
|
215
215
|
|
|
216
|
+
## Beta features
|
|
217
|
+
|
|
218
|
+
The current beta release exposes one new feature behind opt-in config:
|
|
219
|
+
|
|
220
|
+
- **Node-Level Security (NLS)** — a Postgres-RLS-style filter layer that compiles into the existing WHERE pipeline. See **[Advanced Features → Node-Level Security (Beta)](#node-level-security-beta)**.
|
|
221
|
+
|
|
222
|
+
The beta is purely additive: an OGM constructed without the new `policies` option emits **byte-identical Cypher** to v1.6.0. Install with:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
npm install grafeo-ogm@beta
|
|
226
|
+
# or
|
|
227
|
+
pnpm add grafeo-ogm@beta
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The shape of the public API is settled, but the beta window is for collecting integration feedback. Minor adjustments may land before `1.7.0` final.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
216
234
|
## Features Overview
|
|
217
235
|
|
|
218
236
|
| Feature | Description |
|
|
@@ -250,7 +268,7 @@ grafeo-ogm uses standard GraphQL SDL with Neo4j directives to define your graph
|
|
|
250
268
|
| `@relationshipProperties` | Type | Marks a type as relationship properties (edge data) |
|
|
251
269
|
| `@id` | Field | Auto-generates a UUID on create |
|
|
252
270
|
| `@unique` | Field | Creates a uniqueness constraint in Neo4j |
|
|
253
|
-
| `@cypher` | Field | Computed field resolved via a custom Cypher statement |
|
|
271
|
+
| `@cypher` | Field | Computed field resolved via a custom Cypher statement. Scalar return types are resolved at runtime in `select` / `selectionSet`, `where`, and `options.sort` (see [Computed Fields with @cypher](#computed-fields-with-cypher)); node/interface return types remain projection-only |
|
|
254
272
|
| `@default` | Field | Sets a default value on create |
|
|
255
273
|
| `@fulltext` | Type | Defines fulltext search indexes with `name` and `fields` |
|
|
256
274
|
| `@vector` | Type | Registers one or more Neo4j vector indexes on a node (see [Vector Search](#vector-search)) |
|
|
@@ -814,6 +832,275 @@ const deleteResult = await deleteSubgraph(
|
|
|
814
832
|
);
|
|
815
833
|
```
|
|
816
834
|
|
|
835
|
+
### Computed Fields with @cypher
|
|
836
|
+
|
|
837
|
+
The `@cypher` directive declares a field whose value is produced by a custom Cypher statement at query time. As of v1.6.0, scalar `@cypher` fields are resolved in three scopes — `select` / `selectionSet`, `where`, and `options.sort` — so you can project, filter, and order by them just like stored properties. Pre-1.6.0, references in `select` and `where` silently returned `NULL`.
|
|
838
|
+
|
|
839
|
+
#### Declaration
|
|
840
|
+
|
|
841
|
+
```graphql
|
|
842
|
+
type Book @node {
|
|
843
|
+
id: ID! @id
|
|
844
|
+
title: String!
|
|
845
|
+
published: DateTime
|
|
846
|
+
|
|
847
|
+
# Computed: lowercased title for case-insensitive sort/filter
|
|
848
|
+
insensitiveTitle: String @cypher(
|
|
849
|
+
statement: "RETURN toLower(this.title) AS result"
|
|
850
|
+
columnName: "result"
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
# Computed: count of related reviews
|
|
854
|
+
reviewCount: Int @cypher(
|
|
855
|
+
statement: "MATCH (this)-[:HAS_REVIEW]->(r) RETURN count(r) AS count"
|
|
856
|
+
columnName: "count"
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
# Computed: average review rating (aggregating relationship-property data)
|
|
860
|
+
averageRating: Float @cypher(
|
|
861
|
+
statement: "MATCH (this)-[:HAS_REVIEW]->(r) RETURN avg(r.rating) AS avg"
|
|
862
|
+
columnName: "avg"
|
|
863
|
+
)
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
`statement` is the Cypher body — `this` is bound to the matched node. `columnName` is the alias the OGM reads from the `RETURN` clause; if omitted it defaults to the field name.
|
|
868
|
+
|
|
869
|
+
#### Using @cypher in SELECT
|
|
870
|
+
|
|
871
|
+
```typescript
|
|
872
|
+
const books = await Book.find({
|
|
873
|
+
select: {
|
|
874
|
+
id: true,
|
|
875
|
+
title: true,
|
|
876
|
+
insensitiveTitle: true,
|
|
877
|
+
reviewCount: true,
|
|
878
|
+
averageRating: true,
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
// Or via the legacy selectionSet API:
|
|
883
|
+
const books = await Book.find({
|
|
884
|
+
selectionSet: '{ id title insensitiveTitle reviewCount averageRating }',
|
|
885
|
+
});
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
#### Using @cypher in WHERE
|
|
889
|
+
|
|
890
|
+
```typescript
|
|
891
|
+
// Case-insensitive prefix match
|
|
892
|
+
await Book.find({
|
|
893
|
+
where: { insensitiveTitle_STARTS_WITH: 'the' },
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// Combined with stored fields under AND/OR
|
|
897
|
+
await Book.find({
|
|
898
|
+
where: {
|
|
899
|
+
AND: [
|
|
900
|
+
{ insensitiveTitle_CONTAINS: 'graph' },
|
|
901
|
+
{ reviewCount_GT: 5 },
|
|
902
|
+
],
|
|
903
|
+
},
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
// Nested inside a relationship quantifier
|
|
907
|
+
// Find authors who have at least one book with more than 5 reviews
|
|
908
|
+
await Author.find({
|
|
909
|
+
where: { books_SOME: { reviewCount_GT: 5 } },
|
|
910
|
+
});
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
All standard operator suffixes are supported (`_EQ`, `_NOT`, `_IN`, `_NOT_IN`, `_LT`, `_LTE`, `_GT`, `_GTE`, `_CONTAINS`, `_NOT_CONTAINS`, `_STARTS_WITH`, `_ENDS_WITH`, `_MATCHES`).
|
|
914
|
+
|
|
915
|
+
#### Using @cypher in ORDER BY
|
|
916
|
+
|
|
917
|
+
```typescript
|
|
918
|
+
await Book.find({
|
|
919
|
+
where: { insensitiveTitle_STARTS_WITH: 'g' },
|
|
920
|
+
options: { sort: [{ insensitiveTitle: 'ASC' }] },
|
|
921
|
+
select: { id: true, title: true, reviewCount: true },
|
|
922
|
+
});
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
#### Limitations
|
|
926
|
+
|
|
927
|
+
The following positions throw `OGMError` at compile time with a clear hint, rather than silently misbehaving:
|
|
928
|
+
|
|
929
|
+
- **Connection `where` and `select.where`**: Prisma-style relationship filtering with `@cypher` is unsupported. Workaround: filter on the parent type, or query the related node directly via its own model.
|
|
930
|
+
- **`_SINGLE` / `Connection_SINGLE` quantifiers**: unsupported. Workaround: combine `_SOME` and `_NONE` to express "exactly one match."
|
|
931
|
+
- **Nested-relationship SELECT projection**: `select: { rel: { select: { cypherField: true } } }` is unsupported. Workaround: query the related node directly via its own model and join in application code.
|
|
932
|
+
|
|
933
|
+
These are not OGM bugs — they are Cypher-language constraints. Pattern comprehensions (`[(n)-[:R]->(m) | m { ... }]`) cannot host `CALL` subqueries, so resolving an `@cypher` field inside a projection or relationship-scoped predicate is impossible at the language level.
|
|
934
|
+
|
|
935
|
+
#### Performance
|
|
936
|
+
|
|
937
|
+
Each scope (`where`, `select`, sort) emits its own `CALL { WITH n; WITH n AS this; <statement> }` block per `@cypher` field reference. A query that references the same `@cypher` field in all three scopes emits three `CALL` blocks. This is functionally correct and parameter-isolated, but worth knowing if your statement is expensive (multi-hop traversals, aggregations over large fan-outs). For frequently-accessed computed values, consider materializing as a stored property updated on write instead.
|
|
938
|
+
|
|
939
|
+
### Node-Level Security (Beta)
|
|
940
|
+
|
|
941
|
+
> **Beta — install with `npm install grafeo-ogm@beta`.**
|
|
942
|
+
|
|
943
|
+
Node-Level Security (NLS) is a per-request filter layer that compiles into the existing WHERE pipeline. Policies return `<Node>Where` partials, so every operator, quantifier, connection filter, and nested traversal already supported by `WhereCompiler` is automatically available inside policies — no new DSL.
|
|
944
|
+
|
|
945
|
+
#### Declaration
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
import { OGM, override, permissive, restrictive } from 'grafeo-ogm';
|
|
949
|
+
|
|
950
|
+
interface RequestCtx {
|
|
951
|
+
userId: string;
|
|
952
|
+
capabilities: string[];
|
|
953
|
+
tierIds: string[];
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const ogm = new OGM<ModelMap>({
|
|
957
|
+
typeDefs,
|
|
958
|
+
driver,
|
|
959
|
+
policies: {
|
|
960
|
+
Book: [
|
|
961
|
+
// Compile-time short-circuit. When `when(ctx)` returns true, ALL
|
|
962
|
+
// other policies for `Book.read` are dropped — query is byte-
|
|
963
|
+
// identical to a no-policy query. Zero runtime cost for admins.
|
|
964
|
+
override({
|
|
965
|
+
operations: ['*'],
|
|
966
|
+
when: (c: RequestCtx) => c.capabilities.includes('admin'),
|
|
967
|
+
name: 'admin-bypass',
|
|
968
|
+
}),
|
|
969
|
+
|
|
970
|
+
// OR-grant. Multiple permissives compose with OR — any match grants.
|
|
971
|
+
permissive({
|
|
972
|
+
operations: ['read'],
|
|
973
|
+
when: (c: RequestCtx) => ({ ownerId: c.userId }),
|
|
974
|
+
name: 'owner-can-read',
|
|
975
|
+
}),
|
|
976
|
+
|
|
977
|
+
// Read-side restrictive — AND-row predicate. Compiles into the
|
|
978
|
+
// WHERE clause and applies to read|delete|aggregate|count.
|
|
979
|
+
// `when` receives only `ctx` and returns a where-partial (or
|
|
980
|
+
// `false` for a hard deny).
|
|
981
|
+
restrictive({
|
|
982
|
+
operations: ['read'],
|
|
983
|
+
when: (c: RequestCtx) => ({ tierIds_IN: c.tierIds }),
|
|
984
|
+
name: 'tier-isolation',
|
|
985
|
+
}),
|
|
986
|
+
|
|
987
|
+
// Write-side restrictive — Postgres `WITH CHECK` semantics. Runs
|
|
988
|
+
// at the application layer for create|update only; receives both
|
|
989
|
+
// `ctx` and the user-submitted `input`. MUST return a boolean.
|
|
990
|
+
// The OGM rejects mixed read+write `operations` arrays — split a
|
|
991
|
+
// single restrictive into two if you need both read- and write-
|
|
992
|
+
// side coverage.
|
|
993
|
+
restrictive({
|
|
994
|
+
operations: ['create', 'update'],
|
|
995
|
+
when: (c, input) =>
|
|
996
|
+
(input as { ownerId?: string }).ownerId === c.userId,
|
|
997
|
+
name: 'cannot-set-other-owner',
|
|
998
|
+
}),
|
|
999
|
+
],
|
|
1000
|
+
},
|
|
1001
|
+
policyDefaults: {
|
|
1002
|
+
onDeny: 'empty', // Or 'throw' — raises PolicyDeniedError pre-compile.
|
|
1003
|
+
auditMetadata: true,
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
// Per request:
|
|
1008
|
+
const Books = ogm.withContext({ userId, capabilities, tierIds }).model('Book');
|
|
1009
|
+
const visible = await Books.find({ where: { published_GT: '2024-01-01' } });
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
The Cypher emitted for the call above is roughly:
|
|
1013
|
+
|
|
1014
|
+
```cypher
|
|
1015
|
+
MATCH (n:`Book`)
|
|
1016
|
+
WHERE (n.`published` > $param0) AND ((n.`ownerId` = $param1) AND n.`tierIds` IN $param2)
|
|
1017
|
+
RETURN n { .id, .title, .ownerId, .tierIds, ... }
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
#### Hierarchical ACL example
|
|
1021
|
+
|
|
1022
|
+
Combine permissives to express "owner OR member of an allowed tier":
|
|
1023
|
+
|
|
1024
|
+
```typescript
|
|
1025
|
+
policies: {
|
|
1026
|
+
Book: [
|
|
1027
|
+
permissive({
|
|
1028
|
+
operations: ['read'],
|
|
1029
|
+
when: (c) => ({ ownerId: c.userId }),
|
|
1030
|
+
name: 'owner-can-read',
|
|
1031
|
+
}),
|
|
1032
|
+
permissive({
|
|
1033
|
+
operations: ['read'],
|
|
1034
|
+
// Returns a where-partial that uses the existing _SOME quantifier.
|
|
1035
|
+
when: (c) => ({ tiers_SOME: { id_IN: c.tierIds } }),
|
|
1036
|
+
name: 'tier-can-read',
|
|
1037
|
+
}),
|
|
1038
|
+
],
|
|
1039
|
+
}
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
Both permissives OR together — a row passes if either branch matches.
|
|
1043
|
+
|
|
1044
|
+
#### Interface inheritance
|
|
1045
|
+
|
|
1046
|
+
Policies on an interface AND with policies on every implementing concrete type. Concrete types can only narrow restrictives, never broaden — predictable. `OR` composition for permissives lets concrete types add allow paths their interface didn't anticipate.
|
|
1047
|
+
|
|
1048
|
+
```typescript
|
|
1049
|
+
policies: {
|
|
1050
|
+
Resource: [
|
|
1051
|
+
// Tenant isolation applies to every type that implements Resource.
|
|
1052
|
+
restrictive({ operations: ['read'], when: (c) => ({ tenantId: c.tenantId }) }),
|
|
1053
|
+
],
|
|
1054
|
+
Book: [
|
|
1055
|
+
// Additional grant only for Book — composed via OR with any
|
|
1056
|
+
// Resource-level permissives.
|
|
1057
|
+
permissive({ operations: ['read'], when: (c) => ({ authorId: c.userId }) }),
|
|
1058
|
+
],
|
|
1059
|
+
}
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
#### Enforcement boundary
|
|
1063
|
+
|
|
1064
|
+
> **The OGM enforces policies at the compiler.** Policies do NOT apply to `ogm.$queryRaw`, `ogm.$executeRaw`, or to `@cypher` directive bodies that traverse from a stored field. If you need raw-Cypher enforcement, write the predicate into your raw Cypher manually or layer a Neo4j role/RBAC at the database.
|
|
1065
|
+
|
|
1066
|
+
#### Escape hatches
|
|
1067
|
+
|
|
1068
|
+
```typescript
|
|
1069
|
+
// Disable policies for a single call. Logged via logger.warn.
|
|
1070
|
+
await Books.find({ where: { id: 'b1' }, unsafe: { bypassPolicies: true } });
|
|
1071
|
+
|
|
1072
|
+
// Disable policies for a derived OGM (data migrations, admin scripts).
|
|
1073
|
+
const adminOgm = ogm.unsafe.bypassPolicies();
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
Both paths emit byte-identical Cypher to a no-policy OGM.
|
|
1077
|
+
|
|
1078
|
+
#### Audit metadata
|
|
1079
|
+
|
|
1080
|
+
When `policies` is configured, every OGM-emitted query attaches transaction metadata:
|
|
1081
|
+
|
|
1082
|
+
```typescript
|
|
1083
|
+
{
|
|
1084
|
+
ogmPolicySetVersion: '1.7.0-beta.0',
|
|
1085
|
+
ctxFingerprint: '<sha256 of sorted ctx KEYS — never values>',
|
|
1086
|
+
modelType: 'Book',
|
|
1087
|
+
operation: 'read',
|
|
1088
|
+
policiesEvaluated: ['owner-can-read', 'tier-isolation'],
|
|
1089
|
+
bypassed: false,
|
|
1090
|
+
}
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
Disable via `policyDefaults: { auditMetadata: false }`. The fingerprint is intentionally key-only — no ctx values are leaked.
|
|
1094
|
+
|
|
1095
|
+
#### Beta limits
|
|
1096
|
+
|
|
1097
|
+
- **`@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.
|
|
1098
|
+
- **`upsert`** evaluates create- and update-side policies at the application layer (MERGE has no WHERE). Documented limit; full MERGE-aware enforcement is deferred to v1.7.1.
|
|
1099
|
+
- **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); the dual-invocation contract bug from earlier beta iterations is fixed. Use `isReadRestrictive` / `isWriteRestrictive` if you need to inspect a policy at runtime.
|
|
1100
|
+
- **InterfaceModel CASE-per-label fallback.** Implementers without a registered policy fall back to interface-level enforcement on their branch. The OGM emits a `logger.warn` at construction time when an interface has policies and one of its implementers does not — silence the warning by registering an explicit policy on each implementer.
|
|
1101
|
+
- **AsyncLocalStorage** opt-in is deferred to v1.7.1. Beta is explicit `withContext()` only — create one wrapper per request, discard after.
|
|
1102
|
+
- **Live Neo4j integration tests are not part of the beta.** Mock-driver coverage is extensive but a real-DB suite is a `1.7.0` final blocker.
|
|
1103
|
+
|
|
817
1104
|
### Raw Cypher
|
|
818
1105
|
|
|
819
1106
|
```typescript
|
|
@@ -1299,7 +1586,7 @@ Pure query builders (e.g. `cypher-query-builder`) help you compose Cypher fragme
|
|
|
1299
1586
|
|
|
1300
1587
|
### Is grafeo-ogm production-ready?
|
|
1301
1588
|
|
|
1302
|
-
Yes. The library has
|
|
1589
|
+
Yes. The library has 1,093 unit tests covering compilers, mutations, fulltext, transactions, security, scalar type mapping, and edge cases. It targets Neo4j 5.x and follows semver — see [CHANGELOG.md](CHANGELOG.md).
|
|
1303
1590
|
|
|
1304
1591
|
### Does grafeo-ogm work with Neo4j Aura?
|
|
1305
1592
|
|
|
@@ -28,6 +28,7 @@ export declare class MutationCompiler {
|
|
|
28
28
|
compileUpdate(_where: Record<string, unknown>, update: Record<string, unknown> | undefined, connect: Record<string, unknown> | undefined, disconnect: Record<string, unknown> | undefined, nodeDef: NodeDefinition, whereResult: {
|
|
29
29
|
cypher: string;
|
|
30
30
|
params: Record<string, unknown>;
|
|
31
|
+
preludes?: string[];
|
|
31
32
|
}, labels?: string[], returnMode?: 'node' | 'count'): MutationResult;
|
|
32
33
|
/**
|
|
33
34
|
* Generate DELETE Cypher with optional cascade.
|
|
@@ -35,6 +36,7 @@ export declare class MutationCompiler {
|
|
|
35
36
|
compileDelete(nodeDef: NodeDefinition, whereResult: {
|
|
36
37
|
cypher: string;
|
|
37
38
|
params: Record<string, unknown>;
|
|
39
|
+
preludes?: string[];
|
|
38
40
|
}, deleteInput?: Record<string, unknown>): MutationResult;
|
|
39
41
|
/**
|
|
40
42
|
* Generate MERGE (upsert) Cypher with ON CREATE SET / ON MATCH SET.
|
|
@@ -47,6 +49,7 @@ export declare class MutationCompiler {
|
|
|
47
49
|
compileSetLabels(nodeDef: NodeDefinition, whereResult: {
|
|
48
50
|
cypher: string;
|
|
49
51
|
params: Record<string, unknown>;
|
|
52
|
+
preludes?: string[];
|
|
50
53
|
}, addLabels?: string[], removeLabels?: string[]): MutationResult;
|
|
51
54
|
/**
|
|
52
55
|
* Generate batch CREATE (or MERGE for skipDuplicates) Cypher via UNWIND.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutation.compiler.d.ts","sourceRoot":"","sources":["../../../src/compilers/mutation.compiler.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EAEd,cAAc,EACf,MAAM,iBAAiB,CAAC;AAoBzB,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED;;GAEG;AACH,qBAAa,gBAAgB;IAOf,OAAO,CAAC,MAAM;IAN1B;;;OAGG;IACH,OAAO,CAAC,UAAU,CAA6B;gBAE3B,MAAM,EAAE,cAAc;IAE1C,8EAA8E;IAC9E,WAAW,IAAI,IAAI;IAInB,OAAO,CAAC,oBAAoB;IAQ5B;;;OAGG;IACH,aAAa,CACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACjC,OAAO,EAAE,cAAc,EACvB,MAAM,CAAC,EAAE,MAAM,EAAE,GAChB,cAAc;IAkDjB;;OAEG;IACH,aAAa,CACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC3C,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC5C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC/C,OAAO,EAAE,cAAc,EACvB,WAAW,EAAE;
|
|
1
|
+
{"version":3,"file":"mutation.compiler.d.ts","sourceRoot":"","sources":["../../../src/compilers/mutation.compiler.ts"],"names":[],"mappings":"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"}
|
|
@@ -83,6 +83,10 @@ class MutationCompiler {
|
|
|
83
83
|
}
|
|
84
84
|
else
|
|
85
85
|
lines.push(`MATCH (n:${labelStr})`);
|
|
86
|
+
// CALL preludes for `@cypher` fields referenced in the WHERE — must be
|
|
87
|
+
// emitted between MATCH and WHERE so the projected aliases are in scope.
|
|
88
|
+
if (whereResult.preludes && whereResult.preludes.length > 0)
|
|
89
|
+
lines.push(...whereResult.preludes);
|
|
86
90
|
if (whereResult.cypher)
|
|
87
91
|
lines.push(`WHERE ${whereResult.cypher}`);
|
|
88
92
|
// SET properties
|
|
@@ -127,6 +131,8 @@ class MutationCompiler {
|
|
|
127
131
|
const lines = [];
|
|
128
132
|
const params = { ...whereResult.params };
|
|
129
133
|
lines.push(`MATCH (n:${labelStr})`);
|
|
134
|
+
if (whereResult.preludes && whereResult.preludes.length > 0)
|
|
135
|
+
lines.push(...whereResult.preludes);
|
|
130
136
|
if (whereResult.cypher)
|
|
131
137
|
lines.push(`WHERE ${whereResult.cypher}`);
|
|
132
138
|
if (deleteInput && Object.keys(deleteInput).length > 0) {
|
|
@@ -223,6 +229,8 @@ class MutationCompiler {
|
|
|
223
229
|
const lines = [];
|
|
224
230
|
const params = { ...whereResult.params };
|
|
225
231
|
lines.push(`MATCH (n:${labelStr})`);
|
|
232
|
+
if (whereResult.preludes && whereResult.preludes.length > 0)
|
|
233
|
+
lines.push(...whereResult.preludes);
|
|
226
234
|
if (whereResult.cypher)
|
|
227
235
|
lines.push(`WHERE ${whereResult.cypher}`);
|
|
228
236
|
if (addLabels && addLabels.length > 0)
|