Skip to content

Commit 285acbb

Browse files
authored
Merge pull request #10 from codemix/improvements
Enhance TraversalPath with new helper methods and improve ORDER BY su…
2 parents 5fdc619 + 1cd3e27 commit 285acbb

File tree

9 files changed

+495
-61
lines changed

9 files changed

+495
-61
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"typescript.tsdk": "node_modules/typescript/lib"
3+
}

packages/graph/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
### Minor Changes
1616

17+
- Improve path ergonomics with `TraversalPath.nodes()`, `relationships()`, `length()`, and `sum()`, and add runtime support for `ORDER BY` expressions that reference projected aliases before `RETURN`/`WITH`.
1718
- 848690d: Add support for `.map()` and `.filter()` on `ValueTraversal`, so value pipelines can transform and filter extracted values after steps like `values()` and `unfold()`.
1819

1920
## 0.0.2

packages/graph/README.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -404,13 +404,7 @@ g.V()
404404
### Shortest Path
405405

406406
```ts
407-
g.V()
408-
.hasLabel("Person")
409-
.shortestPath(
410-
(t) => t.out("knows"), // expansion
411-
targetVertex, // destination vertex or id
412-
{ maxDepth: 10 },
413-
);
407+
g.V(alice.id).shortestPath().to(george.id).through("knows").direction("out");
414408
```
415409

416410
### Union and Intersection

packages/graph/src/Steps.ts

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3931,6 +3931,7 @@ export interface OrderStepConfig extends StepConfig {
39313931
*/
39323932
directions: readonly {
39333933
key?: string;
3934+
expression?: ConditionValue;
39343935
direction: OrderDirection;
39353936
nulls?: NullsOrdering;
39363937
}[];
@@ -3944,13 +3945,13 @@ export class OrderStep extends Step<OrderStepConfig> {
39443945
public *traverse(
39453946
source: GraphSource<any>,
39463947
input: Iterable<unknown>,
3947-
_context?: QueryContext,
3948+
context?: QueryContext,
39483949
): IterableIterator<unknown> {
39493950
const { directions } = this.config;
39503951
const sorted = [...input].sort((a, b) => {
3951-
for (const { key, direction, nulls } of directions) {
3952-
const aValue = resolveOrderValue(a, key);
3953-
const bValue = resolveOrderValue(b, key);
3952+
for (const { key, expression, direction, nulls } of directions) {
3953+
const aValue = resolveOrderValue(a, key, expression, context);
3954+
const bValue = resolveOrderValue(b, key, expression, context);
39543955

39553956
// Handle null values according to nulls ordering
39563957
const aIsNull = aValue === null || aValue === undefined;
@@ -3991,7 +3992,19 @@ export class OrderStep extends Step<OrderStepConfig> {
39913992
}
39923993
}
39933994

3994-
function resolveOrderValue(item: unknown, key: string | undefined): unknown {
3995+
function resolveOrderValue(
3996+
item: unknown,
3997+
key: string | undefined,
3998+
expression?: ConditionValue,
3999+
context?: QueryContext,
4000+
): unknown {
4001+
if (expression !== undefined) {
4002+
if (item instanceof TraversalPath) {
4003+
return resolveConditionValue(item, expression, context);
4004+
}
4005+
return undefined;
4006+
}
4007+
39954008
if (key === undefined) {
39964009
return item instanceof TraversalPath ? item.value : item;
39974010
}
@@ -6639,7 +6652,8 @@ export interface WithStepConfig extends StepConfig {
66396652
* Optional ORDER BY within the WITH clause.
66406653
*/
66416654
orderBy?: readonly {
6642-
key: string;
6655+
key?: string;
6656+
expression?: ConditionValue;
66436657
direction: OrderDirection;
66446658
nulls?: NullsOrdering;
66456659
}[];
@@ -6791,13 +6805,15 @@ export class WithStep extends Step<WithStepConfig> {
67916805
// Apply ORDER BY if present
67926806
if (orderBy && orderBy.length > 0) {
67936807
results.sort((a, b) => {
6794-
for (const { key, direction, nulls } of orderBy) {
6795-
// First try to get the value as a bound variable (for aliases like `WITH a.num AS val`)
6796-
// then fall back to property access (for expressions like `ORDER BY a.num`)
6797-
const aNode = a.get(key);
6798-
const bNode = b.get(key);
6799-
const aValue = aNode !== undefined ? aNode.value : a.property(key as never);
6800-
const bValue = bNode !== undefined ? bNode.value : b.property(key as never);
6808+
for (const { key, expression, direction, nulls } of orderBy) {
6809+
const aValue =
6810+
expression !== undefined
6811+
? resolveConditionValue(a, expression, context)
6812+
: resolveProjectedOrderValue(a, key);
6813+
const bValue =
6814+
expression !== undefined
6815+
? resolveConditionValue(b, expression, context)
6816+
: resolveProjectedOrderValue(b, key);
68016817

68026818
const aIsNull = aValue === null || aValue === undefined;
68036819
const bIsNull = bValue === null || bValue === undefined;
@@ -7088,6 +7104,20 @@ export class WithStep extends Step<WithStepConfig> {
70887104
}
70897105
}
70907106

7107+
function resolveProjectedOrderValue(
7108+
path: TraversalPath<any, any, any>,
7109+
key: string | undefined,
7110+
): unknown {
7111+
if (key === undefined) {
7112+
return path.value;
7113+
}
7114+
7115+
// First try to get the value as a bound variable (for aliases like `WITH a.num AS val`)
7116+
// then fall back to property access (for expressions like `ORDER BY a.num`)
7117+
const pathNode = path.get(key);
7118+
return pathNode !== undefined ? pathNode.value : path.property(key as never);
7119+
}
7120+
70917121
/**
70927122
* Expression type for UNWIND - can be a literal list, null, property access, variable reference, parameter, function call, or general expression.
70937123
*/

packages/graph/src/Traversals.ts

Lines changed: 147 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
VertexProperties,
77
EdgeProperties,
88
AnyEdgePropertyName,
9+
AnyVertexPropertyName,
910
} from "./GraphSchema.js";
1011
import { ElementId } from "./GraphStorage.js";
1112
import {
@@ -108,7 +109,11 @@ export class TraversalPath<
108109
public with<const TValue, const TLabels extends readonly string[] = []>(
109110
value: TValue,
110111
labels: TLabels = [] as unknown as TLabels,
111-
): this extends TraversalPath<any, unknown, any> ? TraversalPath<this, TValue, TLabels> : never {
112+
): this extends TraversalPath<any, unknown, any>
113+
? TLabels extends readonly []
114+
? this
115+
: TraversalPath<this, TValue, TLabels>
116+
: this {
112117
return new TraversalPath(this as any, value, labels) as any;
113118
}
114119

@@ -129,20 +134,96 @@ export class TraversalPath<
129134
return undefined as PropertyOf<TValue>[TPropertyName];
130135
}
131136

137+
/**
138+
* Return the vertices in this path, in traversal order.
139+
* Optionally extract a property from each vertex instead of returning the vertices themselves.
140+
*/
141+
public nodes(): GetTraversalPathVertices<TraversalPath<TParent, TValue, TLabels>>;
142+
public nodes<
143+
const TPropertyName extends AnyVertexPropertyNameOrString<
144+
GetTraversalPathSchema<TraversalPath<TParent, TValue, TLabels>>
145+
>,
146+
>(
147+
propertyName: TPropertyName,
148+
): GetTraversalPathVertexPropertyValues<TraversalPath<TParent, TValue, TLabels>, TPropertyName>;
149+
public nodes(propertyName?: string) {
150+
const nodes: unknown[] = [];
151+
for (const step of this) {
152+
if (!(step.value instanceof Vertex)) {
153+
continue;
154+
}
155+
nodes.push(propertyName ? step.property(propertyName as never) : step.value);
156+
}
157+
return nodes;
158+
}
159+
160+
/**
161+
* Return the edges/relationships in this path, in traversal order.
162+
* Optionally extract a property from each edge instead of returning the edges themselves.
163+
*/
164+
public relationships(): GetTraversalPathEdges<TraversalPath<TParent, TValue, TLabels>>;
165+
public relationships<
166+
const TPropertyName extends AnyEdgePropertyNameOrString<
167+
GetTraversalPathSchema<TraversalPath<TParent, TValue, TLabels>>
168+
>,
169+
>(
170+
propertyName: TPropertyName,
171+
): GetTraversalPathEdgePropertyValues<TraversalPath<TParent, TValue, TLabels>, TPropertyName>;
172+
public relationships(propertyName?: string) {
173+
const relationships: unknown[] = [];
174+
for (const step of this) {
175+
if (!(step.value instanceof Edge)) {
176+
continue;
177+
}
178+
relationships.push(propertyName ? step.property(propertyName as never) : step.value);
179+
}
180+
return relationships;
181+
}
182+
183+
/**
184+
* Return the number of edges/relationships in this path.
185+
* Mirrors Cypher's `length(path)`.
186+
*/
187+
public length(): number {
188+
let edgeCount = 0;
189+
for (const step of this) {
190+
if (step.value instanceof Edge) {
191+
edgeCount++;
192+
}
193+
}
194+
return edgeCount;
195+
}
196+
197+
/**
198+
* Sum a numeric property across all items in the path.
199+
* Non-numeric or missing values are ignored.
200+
*/
201+
public sum(propertyName: string): number {
202+
let total = 0;
203+
for (const step of this) {
204+
const value = step.property(propertyName as never);
205+
if (typeof value === "number") {
206+
total += value;
207+
}
208+
}
209+
return total;
210+
}
211+
132212
/**
133213
* Get a node in the path by label.
134214
* @param label The label to get the node by.
135215
*/
136-
public get<const TLabel extends string>(label: TLabel): GetTraversalPathByLabel<this, TLabel> {
137-
// eslint-disable-next-line @typescript-eslint/no-this-alias
138-
let node: TraversalPath<any, unknown, any> = this;
216+
public get<const TLabel extends string>(
217+
label: TLabel,
218+
): GetTraversalPathByLabel<TraversalPath<TParent, TValue, TLabels>, TLabel> {
219+
let node: TraversalPath<any, any, any> = this;
139220
while (node !== undefined) {
140221
if (node.labels.includes(label)) {
141-
return node as GetTraversalPathByLabel<this, TLabel>;
222+
return node as GetTraversalPathByLabel<TraversalPath<TParent, TValue, TLabels>, TLabel>;
142223
}
143224
node = node.parent;
144225
}
145-
return undefined as GetTraversalPathByLabel<this, TLabel>;
226+
return undefined as GetTraversalPathByLabel<TraversalPath<TParent, TValue, TLabels>, TLabel>;
146227
}
147228

148229
/**
@@ -151,17 +232,16 @@ export class TraversalPath<
151232
*/
152233
public getAll<const TLabel extends string>(
153234
label: TLabel,
154-
): GetAllTraversalPathsByLabel<this, TLabel> {
155-
const nodes = [] as TraversalPath<any, unknown, any>[];
156-
// eslint-disable-next-line @typescript-eslint/no-this-alias
157-
let node: TraversalPath<any, unknown, any> = this;
235+
): GetAllTraversalPathsByLabel<TraversalPath<TParent, TValue, TLabels>, TLabel> {
236+
const nodes = [] as TraversalPath<any, any, any>[];
237+
let node: TraversalPath<any, any, any> = this;
158238
while (node !== undefined) {
159239
if (node.labels.includes(label)) {
160240
nodes.unshift(node);
161241
}
162242
node = node.parent;
163243
}
164-
return nodes as GetAllTraversalPathsByLabel<this, TLabel>;
244+
return nodes as GetAllTraversalPathsByLabel<TraversalPath<TParent, TValue, TLabels>, TLabel>;
165245
}
166246

167247
public toJSON(): TraversalPathJSON {
@@ -224,6 +304,62 @@ export class TraversalPath<
224304

225305
type PropertyOf<T> = T extends Element<any, any, infer TProperties, any> ? TProperties : keyof T;
226306

307+
type GetTraversalPathItems<TPath> =
308+
TPath extends TraversalPath<infer TParent, infer TValue, any>
309+
? [...GetTraversalPathItems<TParent>, TValue]
310+
: [];
311+
312+
type GetTraversalPathSchema<TPath> =
313+
Extract<GetTraversalPathItems<TPath>[number], Element<any, any, any, any>> extends Element<
314+
infer TSchema,
315+
any,
316+
any,
317+
any
318+
>
319+
? TSchema
320+
: GraphSchema;
321+
322+
type AnyVertexPropertyNameOrString<TSchema extends GraphSchema> =
323+
AnyVertexPropertyName<TSchema> extends never ? string : AnyVertexPropertyName<TSchema>;
324+
325+
type AnyEdgePropertyNameOrString<TSchema extends GraphSchema> =
326+
AnyEdgePropertyName<TSchema> extends never ? string : AnyEdgePropertyName<TSchema>;
327+
328+
type AnyVertexPropertyValue<TSchema extends GraphSchema, TPropertyName extends string> = {
329+
[TVertexLabel in VertexLabel<TSchema>]: TPropertyName extends keyof VertexProperties<
330+
TSchema,
331+
TVertexLabel
332+
>
333+
? VertexProperties<TSchema, TVertexLabel>[TPropertyName]
334+
: never;
335+
}[VertexLabel<TSchema>];
336+
337+
type AnyEdgePropertyValue<TSchema extends GraphSchema, TPropertyName extends string> = {
338+
[TEdgeLabel in EdgeLabel<TSchema>]: TPropertyName extends keyof EdgeProperties<
339+
TSchema,
340+
TEdgeLabel
341+
>
342+
? EdgeProperties<TSchema, TEdgeLabel>[TPropertyName]
343+
: never;
344+
}[EdgeLabel<TSchema>];
345+
346+
type GetTraversalPathVertices<TPath> = Extract<
347+
GetTraversalPathItems<TPath>[number],
348+
Vertex<any, any>
349+
>[];
350+
351+
type GetTraversalPathEdges<TPath> = Extract<GetTraversalPathItems<TPath>[number], Edge<any, any>>[];
352+
353+
type GetTraversalPathVertexPropertyValues<
354+
TPath,
355+
TPropertyName extends string,
356+
> = AnyVertexPropertyValue<GetTraversalPathSchema<TPath>, TPropertyName>[];
357+
358+
type GetTraversalPathEdgePropertyValues<TPath, TPropertyName extends string> = AnyEdgePropertyValue<
359+
GetTraversalPathSchema<TPath>,
360+
TPropertyName
361+
>[];
362+
227363
type GetTraversalPathValue<TPath> =
228364
TPath extends TraversalPath<any, infer TValue, any>
229365
? TValue

0 commit comments

Comments
 (0)