grafeo-ogm 2.3.6 → 2.4.0
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 +27 -0
- package/README.md +7 -0
- package/dist/cjs/utils/cypher-field-projection.d.ts +14 -4
- package/dist/cjs/utils/cypher-field-projection.d.ts.map +1 -1
- package/dist/cjs/utils/cypher-field-projection.js +22 -7
- package/dist/cjs/utils/cypher-field-projection.js.map +1 -1
- package/dist/cjs/utils/cypher-sort-projection.d.ts +5 -12
- package/dist/cjs/utils/cypher-sort-projection.d.ts.map +1 -1
- package/dist/cjs/utils/cypher-sort-projection.js +8 -20
- package/dist/cjs/utils/cypher-sort-projection.js.map +1 -1
- package/dist/esm/utils/cypher-field-projection.d.ts +14 -4
- package/dist/esm/utils/cypher-field-projection.d.ts.map +1 -1
- package/dist/esm/utils/cypher-field-projection.js +22 -7
- package/dist/esm/utils/cypher-field-projection.js.map +1 -1
- package/dist/esm/utils/cypher-sort-projection.d.ts +5 -12
- package/dist/esm/utils/cypher-sort-projection.d.ts.map +1 -1
- package/dist/esm/utils/cypher-sort-projection.js +8 -20
- package/dist/esm/utils/cypher-sort-projection.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.0 (2026-10-18) — `@cypher` fields keep every node exactly once
|
|
4
|
+
|
|
5
|
+
> **Minor release with a behavior change.** Reads that select, filter or sort a `@cypher` field can return a **different number of rows** than in 2.3.x. Nodes that were silently dropped now appear, with `null` for the field, and duplicated nodes now appear once. See *Behavior changes*.
|
|
6
|
+
|
|
7
|
+
### 🐛 A `@cypher` statement with zero or several rows dropped or duplicated the node ([#9](https://github.com/neomodular/grafeo-ogm/issues/9))
|
|
8
|
+
|
|
9
|
+
A top-level `@cypher` field was computed with a plain `CALL { <statement> }`, and a CALL that yields zero rows removes the outer row, while one that yields several multiplies it. The same prelude backs the select, `where` and `options.sort` scopes, so with a statement like `MATCH (this)-[:HAS_STATUS]->(s) RETURN s.name AS status`:
|
|
10
|
+
|
|
11
|
+
| Operation | 2.3.x | 2.4.0 |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `find`, selecting `status` | node without a status **dropped**; node with 2 statuses **returned twice** | each node once: `null` / first status |
|
|
14
|
+
| `where: { status: null }` | **never matched** | matches nodes without a status |
|
|
15
|
+
| `where: { status_NOT: x }` | duplicates | each node once |
|
|
16
|
+
| `options.sort: [{ status }]` | nodes without a status **missing**, others duplicated | every node once; `null` sorts per Neo4j (last on `ASC`) |
|
|
17
|
+
| `count({ where })` on `status` | undercounted / overcounted | agrees with `find` |
|
|
18
|
+
| `create` / `update` / `upsert` selecting `status` | **`[]` (or `null`) even though the write committed** | the written node |
|
|
19
|
+
|
|
20
|
+
Each prelude now runs the statement in an inner `CALL` and reduces its `columnName` column with `head(collect(…))`. A grouping-free aggregation always yields exactly one row, so every node is kept once. That matches how nested selections already projected `@cypher` fields (`head(COLLECT { … })`). The shape works on every Neo4j 5.x, unlike `OPTIONAL CALL` (5.24+), and statements may still return extra columns.
|
|
21
|
+
|
|
22
|
+
### Behavior changes
|
|
23
|
+
|
|
24
|
+
- **Row counts can change** for any read that selects, filters or sorts a top-level `@cypher` field. Code that relied on such nodes being absent, for example as an implicit "has a status" filter, should filter explicitly: `where: { status_NOT: null }`.
|
|
25
|
+
- **With several statement rows, the first one wins.** Add an `ORDER BY` to the `@cypher` statement if it matters which value is picked.
|
|
26
|
+
- **Generated Cypher changes** for queries that use `@cypher` fields. If you pin compiled Cypher in tests, expect the new prelude shape. Queries without `@cypher` fields are byte-identical.
|
|
27
|
+
|
|
28
|
+
A new opt-in live suite, `tests/integration/cypher-field-row-semantics.spec.ts`, pins each row above as a database outcome. Against Neo4j 5.26.32, all 7 cases fail on 2.3.6.
|
|
29
|
+
|
|
3
30
|
## 2.3.6 (2026-10-18) — `searchByPhrase` works
|
|
4
31
|
|
|
5
32
|
### 🐛 `searchByPhrase` called a function as a procedure ([#13](https://github.com/neomodular/grafeo-ogm/issues/13))
|
package/README.md
CHANGED
|
@@ -1100,6 +1100,13 @@ type Book @node {
|
|
|
1100
1100
|
|
|
1101
1101
|
`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.
|
|
1102
1102
|
|
|
1103
|
+
**Row semantics (v2.4.0+).** A `@cypher` field always resolves to **one value per node**, in every scope (select, where, sort, nested selections):
|
|
1104
|
+
|
|
1105
|
+
- If the statement returns **no rows**, the field is `null` and the node is still returned. A filter like `where: { status: null }` matches it, and it sorts as `null`.
|
|
1106
|
+
- If the statement returns **several rows**, the field takes the **first** row's `columnName` value, and the node is returned once. Add an `ORDER BY` to the statement when it matters which row is first.
|
|
1107
|
+
|
|
1108
|
+
Before v2.4.0, top-level fields dropped nodes whose statement returned no rows and duplicated nodes whose statement returned several. See the [2.4.0 changelog](CHANGELOG.md).
|
|
1109
|
+
|
|
1103
1110
|
> **⚠️ Security — `@cypher` SDL is trusted code, not data.** The `statement` argument is interpolated **verbatim** into the compiled Cypher (no parameterisation, no escaping). Treat `@cypher` directives the same way you treat `eval()` in your application code: the string MUST be a constant authored at development time, never built from runtime input. If your `typeDefs` are ever assembled from user input, environment variables, database records, or remote configuration, you have a Cypher-injection vector equivalent to RCE on the database (`MATCH (n) DETACH DELETE n RETURN '' AS x` in a `statement` will wipe the graph). The grafeo-ogm runtime cannot distinguish between developer-authored and user-derived SDL — that boundary is the application's responsibility.
|
|
1104
1111
|
|
|
1105
1112
|
#### Using @cypher in SELECT
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import type { PropertyDefinition } from '../schema/types';
|
|
2
2
|
/**
|
|
3
|
-
* Builds the `CALL { ... }` subquery that projects a `@cypher` field
|
|
4
|
-
*
|
|
5
|
-
* variable inside the subquery, no text substitution.
|
|
3
|
+
* Builds the `CALL { ... }` subquery that projects a `@cypher` field (used
|
|
4
|
+
* by the SELECT, WHERE and sort preludes). `this` is rebound from the
|
|
5
|
+
* outer node variable inside the subquery, no text substitution.
|
|
6
|
+
*
|
|
7
|
+
* Row-preserving: the user's statement runs in an inner CALL and its
|
|
8
|
+
* `columnName` column is reduced with `head(collect(...))`. A grouping-
|
|
9
|
+
* free aggregation always yields exactly ONE row, so the outer row is
|
|
10
|
+
* kept with `null` when the statement returns no rows, and kept once
|
|
11
|
+
* (first value) when it returns several — matching the nested
|
|
12
|
+
* `head(COLLECT { ... })` projection. Pre-2.4.0 the statement was CALLed
|
|
13
|
+
* directly, which dropped the node on zero rows and duplicated it on many.
|
|
14
|
+
* Works on every Neo4j 5.x (unlike `OPTIONAL CALL`, 5.24+) and still lets
|
|
15
|
+
* the statement return extra columns (unlike `COLLECT { }`).
|
|
6
16
|
*/
|
|
7
|
-
export declare function buildCypherFieldCall(statement: string, nodeVar: string): string;
|
|
17
|
+
export declare function buildCypherFieldCall(statement: string, nodeVar: string, columnName: string): string;
|
|
8
18
|
/**
|
|
9
19
|
* Builds an inline `head(COLLECT { ... })` subquery expression for a
|
|
10
20
|
* `@cypher` scalar field. Used inside contexts where a CALL prelude cannot
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cypher-field-projection.d.ts","sourceRoot":"","sources":["../../../src/utils/cypher-field-projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D
|
|
1
|
+
{"version":3,"file":"cypher-field-projection.d.ts","sourceRoot":"","sources":["../../../src/utils/cypher-field-projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,MAAM,CAaR;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,MAAM,CAER;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,gBAAgB;IAWzB,sEAAsE;IACtE,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,2DAA2D;IAC3D,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,cAAc;IAfjC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;;IAG7C,sEAAsE;IACrD,OAAO,EAAE,MAAM;IAChC,2DAA2D;IAC1C,YAAY,GAAE,aAAa,CAAC,MAAM,CAAM;IACzD,4DAA4D;IAC3C,cAAc,GAAE,MAAe;IAKlD;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,MAAM;IAiChE,+CAA+C;IAC/C,MAAM,IAAI,OAAO;IAIjB,sEAAsE;IACtE,IAAI,IAAI,MAAM,EAAE;IAIhB,wDAAwD;IACxD,OAAO,IAAI,MAAM,EAAE;IAInB,0CAA0C;IAC1C,UAAU,IAAI,MAAM;CAGrB"}
|
|
@@ -5,16 +5,31 @@ exports.buildCypherFieldCall = buildCypherFieldCall;
|
|
|
5
5
|
exports.buildInlineCypherFieldExpr = buildInlineCypherFieldExpr;
|
|
6
6
|
const validation_1 = require("./validation");
|
|
7
7
|
/**
|
|
8
|
-
* Builds the `CALL { ... }` subquery that projects a `@cypher` field
|
|
9
|
-
*
|
|
10
|
-
* variable inside the subquery, no text substitution.
|
|
8
|
+
* Builds the `CALL { ... }` subquery that projects a `@cypher` field (used
|
|
9
|
+
* by the SELECT, WHERE and sort preludes). `this` is rebound from the
|
|
10
|
+
* outer node variable inside the subquery, no text substitution.
|
|
11
|
+
*
|
|
12
|
+
* Row-preserving: the user's statement runs in an inner CALL and its
|
|
13
|
+
* `columnName` column is reduced with `head(collect(...))`. A grouping-
|
|
14
|
+
* free aggregation always yields exactly ONE row, so the outer row is
|
|
15
|
+
* kept with `null` when the statement returns no rows, and kept once
|
|
16
|
+
* (first value) when it returns several — matching the nested
|
|
17
|
+
* `head(COLLECT { ... })` projection. Pre-2.4.0 the statement was CALLed
|
|
18
|
+
* directly, which dropped the node on zero rows and duplicated it on many.
|
|
19
|
+
* Works on every Neo4j 5.x (unlike `OPTIONAL CALL`, 5.24+) and still lets
|
|
20
|
+
* the statement return extra columns (unlike `COLLECT { }`).
|
|
11
21
|
*/
|
|
12
|
-
function buildCypherFieldCall(statement, nodeVar) {
|
|
22
|
+
function buildCypherFieldCall(statement, nodeVar, columnName) {
|
|
23
|
+
const col = (0, validation_1.escapeIdentifier)(columnName);
|
|
13
24
|
return [
|
|
14
25
|
`CALL {`,
|
|
15
26
|
` WITH ${nodeVar}`,
|
|
16
|
-
`
|
|
17
|
-
`
|
|
27
|
+
` CALL {`,
|
|
28
|
+
` WITH ${nodeVar}`,
|
|
29
|
+
` WITH ${nodeVar} AS this`,
|
|
30
|
+
` ${statement}`,
|
|
31
|
+
` }`,
|
|
32
|
+
` RETURN head(collect(${col})) AS ${col}`,
|
|
18
33
|
`}`,
|
|
19
34
|
].join('\n');
|
|
20
35
|
}
|
|
@@ -94,7 +109,7 @@ class CypherFieldScope {
|
|
|
94
109
|
(0, validation_1.assertSafeIdentifier)(alias, '@cypher alias');
|
|
95
110
|
const carry = [...this.preserveVars, ...this.carriedAliases];
|
|
96
111
|
const carryStr = carry.length ? `, ${carry.join(', ')}` : '';
|
|
97
|
-
this.callLines.push(buildCypherFieldCall(propDef.cypherStatement, this.nodeVar));
|
|
112
|
+
this.callLines.push(buildCypherFieldCall(propDef.cypherStatement, this.nodeVar, columnName));
|
|
98
113
|
this.callLines.push(`WITH ${this.nodeVar}${carryStr}, ${(0, validation_1.escapeIdentifier)(columnName)} AS ${alias}`);
|
|
99
114
|
this.carriedAliases.push(alias);
|
|
100
115
|
this.aliases.set(fieldName, alias);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cypher-field-projection.js","sourceRoot":"","sources":["../../../src/utils/cypher-field-projection.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"cypher-field-projection.js","sourceRoot":"","sources":["../../../src/utils/cypher-field-projection.ts"],"names":[],"mappings":";;;AAkBA,oDAiBC;AAiBD,gEAKC;AAxDD,6CAAsE;AAEtE;;;;;;;;;;;;;;GAcG;AACH,SAAgB,oBAAoB,CAClC,SAAiB,EACjB,OAAe,EACf,UAAkB;IAElB,MAAM,GAAG,GAAG,IAAA,6BAAgB,EAAC,UAAU,CAAC,CAAC;IACzC,OAAO;QACL,QAAQ;QACR,UAAU,OAAO,EAAE;QACnB,UAAU;QACV,YAAY,OAAO,EAAE;QACrB,YAAY,OAAO,UAAU;QAC7B,OAAO,SAAS,EAAE;QAClB,KAAK;QACL,yBAAyB,GAAG,SAAS,GAAG,EAAE;QAC1C,GAAG;KACJ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,SAAgB,0BAA0B,CACxC,SAAiB,EACjB,OAAe;IAEf,OAAO,uBAAuB,OAAO,eAAe,SAAS,MAAM,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAa,gBAAgB;IAU3B;IACE,sEAAsE;IACrD,OAAe;IAChC,2DAA2D;IAC1C,eAAsC,EAAE;IACzD,4DAA4D;IAC3C,iBAAyB,MAAM;QAJ/B,YAAO,GAAP,OAAO,CAAQ;QAEf,iBAAY,GAAZ,YAAY,CAA4B;QAExC,mBAAc,GAAd,cAAc,CAAiB;QAflD;;;;WAIG;QACc,YAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;QACpC,cAAS,GAAa,EAAE,CAAC;QACzB,mBAAc,GAAa,EAAE,CAAC;QAU7C,IAAA,iCAAoB,EAAC,OAAO,EAAE,uBAAuB,CAAC,CAAC;IACzD,CAAC;IAED;;;;;;OAMG;IACH,QAAQ,CAAC,SAAiB,EAAE,OAA2B;QACrD,IAAA,iCAAoB,EAAC,SAAS,EAAE,eAAe,CAAC,CAAC;QAEjD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,IAAI,CAAC,OAAO,CAAC,eAAe;YAC1B,MAAM,IAAI,KAAK,CACb,oCAAoC,SAAS,4BAA4B,CAC1E,CAAC;QAEJ,MAAM,UAAU,GAAG,OAAO,CAAC,gBAAgB,IAAI,SAAS,CAAC;QACzD,IAAA,iCAAoB,EAAC,UAAU,EAAE,oBAAoB,CAAC,CAAC;QAEvD,MAAM,KAAK,GAAG,GAAG,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,OAAO,IAAI,SAAS,EAAE,CAAC;QACpE,IAAA,iCAAoB,EAAC,KAAK,EAAE,eAAe,CAAC,CAAC;QAE7C,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC;QAC7D,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAE7D,IAAI,CAAC,SAAS,CAAC,IAAI,CACjB,oBAAoB,CAAC,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CACxE,CAAC;QACF,IAAI,CAAC,SAAS,CAAC,IAAI,CACjB,QAAQ,IAAI,CAAC,OAAO,GAAG,QAAQ,KAAK,IAAA,6BAAgB,EAAC,UAAU,CAAC,OAAO,KAAK,EAAE,CAC/E,CAAC;QAEF,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QAEnC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,+CAA+C;IAC/C,MAAM;QACJ,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;IACnC,CAAC;IAED,sEAAsE;IACtE,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7B,CAAC;IAED,wDAAwD;IACxD,OAAO;QACL,OAAO,CAAC,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC;IAClC,CAAC;IAED,0CAA0C;IAC1C,UAAU;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;CACF;AAhFD,4CAgFC"}
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import type { PropertyDefinition } from '../schema/types';
|
|
2
2
|
/**
|
|
3
3
|
* Builds the `CALL { ... }` subquery used to project an `@cypher` field
|
|
4
|
-
* for `ORDER BY
|
|
5
|
-
*
|
|
4
|
+
* for `ORDER BY` — the same row-preserving shape as the SELECT / WHERE
|
|
5
|
+
* preludes (see `buildCypherFieldCall`): a node whose statement returns
|
|
6
|
+
* no rows sorts with `null` instead of vanishing, and one returning
|
|
7
|
+
* several rows sorts once.
|
|
6
8
|
*
|
|
7
9
|
* The caller is expected to follow this CALL with its own `WITH` clause
|
|
8
10
|
* to rename the projected column (`<columnName>`) to a unique alias
|
|
9
11
|
* (e.g. `__sort_<fieldName>`) and carry forward any prior sort aliases.
|
|
10
|
-
*
|
|
11
|
-
* Example output:
|
|
12
|
-
* ```cypher
|
|
13
|
-
* CALL {
|
|
14
|
-
* WITH n
|
|
15
|
-
* WITH n AS this
|
|
16
|
-
* MATCH (this)-[:HAS_STATUS]->(s) RETURN s.name AS statusName
|
|
17
|
-
* }
|
|
18
|
-
* ```
|
|
19
12
|
*/
|
|
20
|
-
export declare function buildCypherSortProjection(statement: string, nodeVar: string): string;
|
|
13
|
+
export declare function buildCypherSortProjection(statement: string, nodeVar: string, columnName: string): string;
|
|
21
14
|
/**
|
|
22
15
|
* Compiles a `sort` array (mixed stored-field + `@cypher`-field entries)
|
|
23
16
|
* into the Cypher fragments needed before and after the `RETURN` clause.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cypher-sort-projection.d.ts","sourceRoot":"","sources":["../../../src/utils/cypher-sort-projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"cypher-sort-projection.d.ts","sourceRoot":"","sources":["../../../src/utils/cypher-sort-projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAU1D;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,MAAM,CAER;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,kBAAkB,GAAG,SAAS,CAAC;IAClE,YAAY,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACtC,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAgDnC"}
|
|
@@ -2,33 +2,21 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildCypherSortProjection = buildCypherSortProjection;
|
|
4
4
|
exports.compileSortClause = compileSortClause;
|
|
5
|
+
const cypher_field_projection_1 = require("./cypher-field-projection");
|
|
5
6
|
const validation_1 = require("./validation");
|
|
6
7
|
/**
|
|
7
8
|
* Builds the `CALL { ... }` subquery used to project an `@cypher` field
|
|
8
|
-
* for `ORDER BY
|
|
9
|
-
*
|
|
9
|
+
* for `ORDER BY` — the same row-preserving shape as the SELECT / WHERE
|
|
10
|
+
* preludes (see `buildCypherFieldCall`): a node whose statement returns
|
|
11
|
+
* no rows sorts with `null` instead of vanishing, and one returning
|
|
12
|
+
* several rows sorts once.
|
|
10
13
|
*
|
|
11
14
|
* The caller is expected to follow this CALL with its own `WITH` clause
|
|
12
15
|
* to rename the projected column (`<columnName>`) to a unique alias
|
|
13
16
|
* (e.g. `__sort_<fieldName>`) and carry forward any prior sort aliases.
|
|
14
|
-
*
|
|
15
|
-
* Example output:
|
|
16
|
-
* ```cypher
|
|
17
|
-
* CALL {
|
|
18
|
-
* WITH n
|
|
19
|
-
* WITH n AS this
|
|
20
|
-
* MATCH (this)-[:HAS_STATUS]->(s) RETURN s.name AS statusName
|
|
21
|
-
* }
|
|
22
|
-
* ```
|
|
23
17
|
*/
|
|
24
|
-
function buildCypherSortProjection(statement, nodeVar) {
|
|
25
|
-
return
|
|
26
|
-
`CALL {`,
|
|
27
|
-
` WITH ${nodeVar}`,
|
|
28
|
-
` WITH ${nodeVar} AS this`,
|
|
29
|
-
` ${statement}`,
|
|
30
|
-
`}`,
|
|
31
|
-
].join('\n');
|
|
18
|
+
function buildCypherSortProjection(statement, nodeVar, columnName) {
|
|
19
|
+
return (0, cypher_field_projection_1.buildCypherFieldCall)(statement, nodeVar, columnName);
|
|
32
20
|
}
|
|
33
21
|
/**
|
|
34
22
|
* Compiles a `sort` array (mixed stored-field + `@cypher`-field entries)
|
|
@@ -73,7 +61,7 @@ function compileSortClause(args) {
|
|
|
73
61
|
const alias = `__sort_${field}`;
|
|
74
62
|
const columnName = propDef.cypherColumnName ?? field;
|
|
75
63
|
(0, validation_1.assertSafeIdentifier)(columnName, '@cypher columnName');
|
|
76
|
-
preLines.push(buildCypherSortProjection(propDef.cypherStatement, nodeVar));
|
|
64
|
+
preLines.push(buildCypherSortProjection(propDef.cypherStatement, nodeVar, columnName));
|
|
77
65
|
const carry = [...preserveVars, ...carriedAliases];
|
|
78
66
|
const carryStr = carry.length ? `, ${carry.join(', ')}` : '';
|
|
79
67
|
preLines.push(`WITH ${nodeVar}${carryStr}, ${(0, validation_1.escapeIdentifier)(columnName)} AS ${alias}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cypher-sort-projection.js","sourceRoot":"","sources":["../../../src/utils/cypher-sort-projection.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"cypher-sort-projection.js","sourceRoot":"","sources":["../../../src/utils/cypher-sort-projection.ts"],"names":[],"mappings":";;AAqBA,8DAMC;AAmBD,8CAqDC;AAlGD,uEAAiE;AACjE,6CAMsB;AAEtB;;;;;;;;;;GAUG;AACH,SAAgB,yBAAyB,CACvC,SAAiB,EACjB,OAAe,EACf,UAAkB;IAElB,OAAO,IAAA,8CAAoB,EAAC,SAAS,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,SAAgB,iBAAiB,CAAC,IAKjC;IACC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC;IAClE,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,MAAM,cAAc,GAAa,EAAE,CAAC;IAEpC,KAAK,MAAM,OAAO,IAAI,IAAI,EAAE,CAAC;QAC3B,uEAAuE;QACvE,kEAAkE;QAClE,IAAA,8BAAiB,EAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QAEzC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAiC,CAAC,CAAC;QAClE,yEAAyE;QACzE,iEAAiE;QACjE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACnC,2EAA2E;QAC3E,qEAAqE;QACrE,KAAK,MAAM,CAAC,GAAG,CAAC,IAAI,OAAO;YAAE,IAAA,iCAAoB,EAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QACrE,IAAA,gCAAmB,EAAC,OAAO,CAAC,CAAC;QAE7B,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACtC,MAAM,cAAc,GAAG,IAAA,gCAAmB,EAAC,SAAS,CAAC,CAAC;QAEtD,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,OAAO,EAAE,QAAQ,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;YACjD,MAAM,KAAK,GAAG,UAAU,KAAK,EAAE,CAAC;YAChC,MAAM,UAAU,GAAG,OAAO,CAAC,gBAAgB,IAAI,KAAK,CAAC;YACrD,IAAA,iCAAoB,EAAC,UAAU,EAAE,oBAAoB,CAAC,CAAC;YAEvD,QAAQ,CAAC,IAAI,CACX,yBAAyB,CAAC,OAAO,CAAC,eAAe,EAAE,OAAO,EAAE,UAAU,CAAC,CACxE,CAAC;YAEF,MAAM,KAAK,GAAG,CAAC,GAAG,YAAY,EAAE,GAAG,cAAc,CAAC,CAAC;YACnD,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,QAAQ,CAAC,IAAI,CACX,QAAQ,OAAO,GAAG,QAAQ,KAAK,IAAA,6BAAgB,EAAC,UAAU,CAAC,OAAO,KAAK,EAAE,CAC1E,CAAC;YACF,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC3B,SAAS,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,cAAc,EAAE,CAAC,CAAC;QAC/C,CAAC;;YACC,SAAS,CAAC,IAAI,CAAC,GAAG,OAAO,IAAI,IAAA,6BAAgB,EAAC,KAAK,CAAC,IAAI,cAAc,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO;QACL,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QACxB,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;KACpE,CAAC;AACJ,CAAC"}
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import type { PropertyDefinition } from '../schema/types';
|
|
2
2
|
/**
|
|
3
|
-
* Builds the `CALL { ... }` subquery that projects a `@cypher` field
|
|
4
|
-
*
|
|
5
|
-
* variable inside the subquery, no text substitution.
|
|
3
|
+
* Builds the `CALL { ... }` subquery that projects a `@cypher` field (used
|
|
4
|
+
* by the SELECT, WHERE and sort preludes). `this` is rebound from the
|
|
5
|
+
* outer node variable inside the subquery, no text substitution.
|
|
6
|
+
*
|
|
7
|
+
* Row-preserving: the user's statement runs in an inner CALL and its
|
|
8
|
+
* `columnName` column is reduced with `head(collect(...))`. A grouping-
|
|
9
|
+
* free aggregation always yields exactly ONE row, so the outer row is
|
|
10
|
+
* kept with `null` when the statement returns no rows, and kept once
|
|
11
|
+
* (first value) when it returns several — matching the nested
|
|
12
|
+
* `head(COLLECT { ... })` projection. Pre-2.4.0 the statement was CALLed
|
|
13
|
+
* directly, which dropped the node on zero rows and duplicated it on many.
|
|
14
|
+
* Works on every Neo4j 5.x (unlike `OPTIONAL CALL`, 5.24+) and still lets
|
|
15
|
+
* the statement return extra columns (unlike `COLLECT { }`).
|
|
6
16
|
*/
|
|
7
|
-
export declare function buildCypherFieldCall(statement: string, nodeVar: string): string;
|
|
17
|
+
export declare function buildCypherFieldCall(statement: string, nodeVar: string, columnName: string): string;
|
|
8
18
|
/**
|
|
9
19
|
* Builds an inline `head(COLLECT { ... })` subquery expression for a
|
|
10
20
|
* `@cypher` scalar field. Used inside contexts where a CALL prelude cannot
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cypher-field-projection.d.ts","sourceRoot":"","sources":["../../../src/utils/cypher-field-projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D
|
|
1
|
+
{"version":3,"file":"cypher-field-projection.d.ts","sourceRoot":"","sources":["../../../src/utils/cypher-field-projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,MAAM,CAaR;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,MAAM,CAER;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,gBAAgB;IAWzB,sEAAsE;IACtE,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,2DAA2D;IAC3D,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,cAAc;IAfjC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;;IAG7C,sEAAsE;IACrD,OAAO,EAAE,MAAM;IAChC,2DAA2D;IAC1C,YAAY,GAAE,aAAa,CAAC,MAAM,CAAM;IACzD,4DAA4D;IAC3C,cAAc,GAAE,MAAe;IAKlD;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,kBAAkB,GAAG,MAAM;IAiChE,+CAA+C;IAC/C,MAAM,IAAI,OAAO;IAIjB,sEAAsE;IACtE,IAAI,IAAI,MAAM,EAAE;IAIhB,wDAAwD;IACxD,OAAO,IAAI,MAAM,EAAE;IAInB,0CAA0C;IAC1C,UAAU,IAAI,MAAM;CAGrB"}
|
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
import { assertSafeIdentifier, escapeIdentifier } from './validation';
|
|
2
2
|
/**
|
|
3
|
-
* Builds the `CALL { ... }` subquery that projects a `@cypher` field
|
|
4
|
-
*
|
|
5
|
-
* variable inside the subquery, no text substitution.
|
|
3
|
+
* Builds the `CALL { ... }` subquery that projects a `@cypher` field (used
|
|
4
|
+
* by the SELECT, WHERE and sort preludes). `this` is rebound from the
|
|
5
|
+
* outer node variable inside the subquery, no text substitution.
|
|
6
|
+
*
|
|
7
|
+
* Row-preserving: the user's statement runs in an inner CALL and its
|
|
8
|
+
* `columnName` column is reduced with `head(collect(...))`. A grouping-
|
|
9
|
+
* free aggregation always yields exactly ONE row, so the outer row is
|
|
10
|
+
* kept with `null` when the statement returns no rows, and kept once
|
|
11
|
+
* (first value) when it returns several — matching the nested
|
|
12
|
+
* `head(COLLECT { ... })` projection. Pre-2.4.0 the statement was CALLed
|
|
13
|
+
* directly, which dropped the node on zero rows and duplicated it on many.
|
|
14
|
+
* Works on every Neo4j 5.x (unlike `OPTIONAL CALL`, 5.24+) and still lets
|
|
15
|
+
* the statement return extra columns (unlike `COLLECT { }`).
|
|
6
16
|
*/
|
|
7
|
-
export function buildCypherFieldCall(statement, nodeVar) {
|
|
17
|
+
export function buildCypherFieldCall(statement, nodeVar, columnName) {
|
|
18
|
+
const col = escapeIdentifier(columnName);
|
|
8
19
|
return [
|
|
9
20
|
`CALL {`,
|
|
10
21
|
` WITH ${nodeVar}`,
|
|
11
|
-
`
|
|
12
|
-
`
|
|
22
|
+
` CALL {`,
|
|
23
|
+
` WITH ${nodeVar}`,
|
|
24
|
+
` WITH ${nodeVar} AS this`,
|
|
25
|
+
` ${statement}`,
|
|
26
|
+
` }`,
|
|
27
|
+
` RETURN head(collect(${col})) AS ${col}`,
|
|
13
28
|
`}`,
|
|
14
29
|
].join('\n');
|
|
15
30
|
}
|
|
@@ -89,7 +104,7 @@ export class CypherFieldScope {
|
|
|
89
104
|
assertSafeIdentifier(alias, '@cypher alias');
|
|
90
105
|
const carry = [...this.preserveVars, ...this.carriedAliases];
|
|
91
106
|
const carryStr = carry.length ? `, ${carry.join(', ')}` : '';
|
|
92
|
-
this.callLines.push(buildCypherFieldCall(propDef.cypherStatement, this.nodeVar));
|
|
107
|
+
this.callLines.push(buildCypherFieldCall(propDef.cypherStatement, this.nodeVar, columnName));
|
|
93
108
|
this.callLines.push(`WITH ${this.nodeVar}${carryStr}, ${escapeIdentifier(columnName)} AS ${alias}`);
|
|
94
109
|
this.carriedAliases.push(alias);
|
|
95
110
|
this.aliases.set(fieldName, alias);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cypher-field-projection.js","sourceRoot":"","sources":["../../../src/utils/cypher-field-projection.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEtE
|
|
1
|
+
{"version":3,"file":"cypher-field-projection.js","sourceRoot":"","sources":["../../../src/utils/cypher-field-projection.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import type { PropertyDefinition } from '../schema/types';
|
|
2
2
|
/**
|
|
3
3
|
* Builds the `CALL { ... }` subquery used to project an `@cypher` field
|
|
4
|
-
* for `ORDER BY
|
|
5
|
-
*
|
|
4
|
+
* for `ORDER BY` — the same row-preserving shape as the SELECT / WHERE
|
|
5
|
+
* preludes (see `buildCypherFieldCall`): a node whose statement returns
|
|
6
|
+
* no rows sorts with `null` instead of vanishing, and one returning
|
|
7
|
+
* several rows sorts once.
|
|
6
8
|
*
|
|
7
9
|
* The caller is expected to follow this CALL with its own `WITH` clause
|
|
8
10
|
* to rename the projected column (`<columnName>`) to a unique alias
|
|
9
11
|
* (e.g. `__sort_<fieldName>`) and carry forward any prior sort aliases.
|
|
10
|
-
*
|
|
11
|
-
* Example output:
|
|
12
|
-
* ```cypher
|
|
13
|
-
* CALL {
|
|
14
|
-
* WITH n
|
|
15
|
-
* WITH n AS this
|
|
16
|
-
* MATCH (this)-[:HAS_STATUS]->(s) RETURN s.name AS statusName
|
|
17
|
-
* }
|
|
18
|
-
* ```
|
|
19
12
|
*/
|
|
20
|
-
export declare function buildCypherSortProjection(statement: string, nodeVar: string): string;
|
|
13
|
+
export declare function buildCypherSortProjection(statement: string, nodeVar: string, columnName: string): string;
|
|
21
14
|
/**
|
|
22
15
|
* Compiles a `sort` array (mixed stored-field + `@cypher`-field entries)
|
|
23
16
|
* into the Cypher fragments needed before and after the `RETURN` clause.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cypher-sort-projection.d.ts","sourceRoot":"","sources":["../../../src/utils/cypher-sort-projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"cypher-sort-projection.d.ts","sourceRoot":"","sources":["../../../src/utils/cypher-sort-projection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAU1D;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,MAAM,CAER;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,kBAAkB,GAAG,SAAS,CAAC;IAClE,YAAY,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACtC,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAgDnC"}
|
|
@@ -1,30 +1,18 @@
|
|
|
1
|
+
import { buildCypherFieldCall } from './cypher-field-projection';
|
|
1
2
|
import { assertPlainObject, assertSafeIdentifier, assertSingleSortKey, assertSortDirection, escapeIdentifier, } from './validation';
|
|
2
3
|
/**
|
|
3
4
|
* Builds the `CALL { ... }` subquery used to project an `@cypher` field
|
|
4
|
-
* for `ORDER BY
|
|
5
|
-
*
|
|
5
|
+
* for `ORDER BY` — the same row-preserving shape as the SELECT / WHERE
|
|
6
|
+
* preludes (see `buildCypherFieldCall`): a node whose statement returns
|
|
7
|
+
* no rows sorts with `null` instead of vanishing, and one returning
|
|
8
|
+
* several rows sorts once.
|
|
6
9
|
*
|
|
7
10
|
* The caller is expected to follow this CALL with its own `WITH` clause
|
|
8
11
|
* to rename the projected column (`<columnName>`) to a unique alias
|
|
9
12
|
* (e.g. `__sort_<fieldName>`) and carry forward any prior sort aliases.
|
|
10
|
-
*
|
|
11
|
-
* Example output:
|
|
12
|
-
* ```cypher
|
|
13
|
-
* CALL {
|
|
14
|
-
* WITH n
|
|
15
|
-
* WITH n AS this
|
|
16
|
-
* MATCH (this)-[:HAS_STATUS]->(s) RETURN s.name AS statusName
|
|
17
|
-
* }
|
|
18
|
-
* ```
|
|
19
13
|
*/
|
|
20
|
-
export function buildCypherSortProjection(statement, nodeVar) {
|
|
21
|
-
return
|
|
22
|
-
`CALL {`,
|
|
23
|
-
` WITH ${nodeVar}`,
|
|
24
|
-
` WITH ${nodeVar} AS this`,
|
|
25
|
-
` ${statement}`,
|
|
26
|
-
`}`,
|
|
27
|
-
].join('\n');
|
|
14
|
+
export function buildCypherSortProjection(statement, nodeVar, columnName) {
|
|
15
|
+
return buildCypherFieldCall(statement, nodeVar, columnName);
|
|
28
16
|
}
|
|
29
17
|
/**
|
|
30
18
|
* Compiles a `sort` array (mixed stored-field + `@cypher`-field entries)
|
|
@@ -69,7 +57,7 @@ export function compileSortClause(args) {
|
|
|
69
57
|
const alias = `__sort_${field}`;
|
|
70
58
|
const columnName = propDef.cypherColumnName ?? field;
|
|
71
59
|
assertSafeIdentifier(columnName, '@cypher columnName');
|
|
72
|
-
preLines.push(buildCypherSortProjection(propDef.cypherStatement, nodeVar));
|
|
60
|
+
preLines.push(buildCypherSortProjection(propDef.cypherStatement, nodeVar, columnName));
|
|
73
61
|
const carry = [...preserveVars, ...carriedAliases];
|
|
74
62
|
const carryStr = carry.length ? `, ${carry.join(', ')}` : '';
|
|
75
63
|
preLines.push(`WITH ${nodeVar}${carryStr}, ${escapeIdentifier(columnName)} AS ${alias}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cypher-sort-projection.js","sourceRoot":"","sources":["../../../src/utils/cypher-sort-projection.ts"],"names":[],"mappings":"AACA,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAEtB
|
|
1
|
+
{"version":3,"file":"cypher-sort-projection.js","sourceRoot":"","sources":["../../../src/utils/cypher-sort-projection.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,cAAc,CAAC;AAEtB;;;;;;;;;;GAUG;AACH,MAAM,UAAU,yBAAyB,CACvC,SAAiB,EACjB,OAAe,EACf,UAAkB;IAElB,OAAO,oBAAoB,CAAC,SAAS,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAKjC;IACC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC;IAClE,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,MAAM,cAAc,GAAa,EAAE,CAAC;IAEpC,KAAK,MAAM,OAAO,IAAI,IAAI,EAAE,CAAC;QAC3B,uEAAuE;QACvE,kEAAkE;QAClE,iBAAiB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QAEzC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAiC,CAAC,CAAC;QAClE,yEAAyE;QACzE,iEAAiE;QACjE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACnC,2EAA2E;QAC3E,qEAAqE;QACrE,KAAK,MAAM,CAAC,GAAG,CAAC,IAAI,OAAO;YAAE,oBAAoB,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QACrE,mBAAmB,CAAC,OAAO,CAAC,CAAC;QAE7B,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACtC,MAAM,cAAc,GAAG,mBAAmB,CAAC,SAAS,CAAC,CAAC;QAEtD,MAAM,OAAO,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QACtC,IAAI,OAAO,EAAE,QAAQ,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;YACjD,MAAM,KAAK,GAAG,UAAU,KAAK,EAAE,CAAC;YAChC,MAAM,UAAU,GAAG,OAAO,CAAC,gBAAgB,IAAI,KAAK,CAAC;YACrD,oBAAoB,CAAC,UAAU,EAAE,oBAAoB,CAAC,CAAC;YAEvD,QAAQ,CAAC,IAAI,CACX,yBAAyB,CAAC,OAAO,CAAC,eAAe,EAAE,OAAO,EAAE,UAAU,CAAC,CACxE,CAAC;YAEF,MAAM,KAAK,GAAG,CAAC,GAAG,YAAY,EAAE,GAAG,cAAc,CAAC,CAAC;YACnD,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,QAAQ,CAAC,IAAI,CACX,QAAQ,OAAO,GAAG,QAAQ,KAAK,gBAAgB,CAAC,UAAU,CAAC,OAAO,KAAK,EAAE,CAC1E,CAAC;YACF,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC3B,SAAS,CAAC,IAAI,CAAC,GAAG,KAAK,IAAI,cAAc,EAAE,CAAC,CAAC;QAC/C,CAAC;;YACC,SAAS,CAAC,IAAI,CAAC,GAAG,OAAO,IAAI,gBAAgB,CAAC,KAAK,CAAC,IAAI,cAAc,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO;QACL,GAAG,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QACxB,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,YAAY,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;KACpE,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grafeo-ogm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Type-safe Neo4j ORM / OGM for TypeScript. Define your graph in GraphQL SDL, get fully typed Prisma-like CRUD, optimized Cypher, and zero-config code generation. Community successor to @neo4j/graphql-ogm.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Alejandro Guzman",
|