Skip to content

Commit 7155793

Browse files
authored
Merge pull request #4 from codemix/value-traversals-order-by
Support .order().by() on ValueTraversals
2 parents fd7833f + b69d71e commit 7155793

3 files changed

Lines changed: 147 additions & 22 deletions

File tree

packages/graph/src/Steps.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3890,7 +3890,7 @@ export interface OrderStepConfig extends StepConfig {
38903890
* The directions to sort the results by.
38913891
*/
38923892
directions: readonly {
3893-
key: string;
3893+
key?: string;
38943894
direction: OrderDirection;
38953895
nulls?: NullsOrdering;
38963896
}[];
@@ -3903,18 +3903,14 @@ export class OrderStep extends Step<OrderStepConfig> {
39033903

39043904
public *traverse(
39053905
source: GraphSource<any>,
3906-
input: Iterable<TraversalPath<any, any, any>>,
3906+
input: Iterable<unknown>,
39073907
_context?: QueryContext,
3908-
): IterableIterator<TraversalPath<any, any, any>> {
3908+
): IterableIterator<unknown> {
39093909
const { directions } = this.config;
39103910
const sorted = [...input].sort((a, b) => {
39113911
for (const { key, direction, nulls } of directions) {
3912-
// First try to get the value as a bound variable (for aliases like UNWIND x)
3913-
// then fall back to property access (for expressions like ORDER BY n.name)
3914-
const aNode = a.get(key);
3915-
const bNode = b.get(key);
3916-
const aValue = aNode !== undefined ? aNode.value : a.property(key as never);
3917-
const bValue = bNode !== undefined ? bNode.value : b.property(key as never);
3912+
const aValue = resolveOrderValue(a, key);
3913+
const bValue = resolveOrderValue(b, key);
39183914

39193915
// Handle null values according to nulls ordering
39203916
const aIsNull = aValue === null || aValue === undefined;
@@ -3955,6 +3951,24 @@ export class OrderStep extends Step<OrderStepConfig> {
39553951
}
39563952
}
39573953

3954+
function resolveOrderValue(item: unknown, key: string | undefined): unknown {
3955+
if (key === undefined) {
3956+
return item instanceof TraversalPath ? item.value : item;
3957+
}
3958+
3959+
if (item instanceof TraversalPath) {
3960+
// First try to get the value as a bound variable (for aliases like UNWIND x)
3961+
// then fall back to property access (for expressions like ORDER BY n.name)
3962+
return item.get(key)?.value ?? item.property(key as never);
3963+
}
3964+
3965+
if (typeof item === "object" && item !== null) {
3966+
return (item as Record<string, unknown>)[key];
3967+
}
3968+
3969+
return undefined;
3970+
}
3971+
39583972
export interface UnionStepConfig extends StepConfig {}
39593973

39603974
export class UnionStep<const TSteps extends readonly Step<any>[]> extends ContainerStep<

packages/graph/src/Traversals.ts

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,19 +1094,10 @@ export class OrderVertexTraversal<
10941094
propertyName: TPropertyName,
10951095
direction: OrderDirection = "asc",
10961096
) {
1097-
const steps = [...this.steps];
1098-
let orderStep =
1099-
steps.length > 0 && steps[steps.length - 1] instanceof OrderStep
1100-
? steps[steps.length - 1]
1101-
: undefined;
1102-
if (!orderStep) {
1103-
steps.push(new OrderStep({ directions: [{ key: propertyName, direction }] }));
1104-
} else {
1105-
steps[steps.length - 1] = orderStep.clone({
1106-
directions: [...orderStep.config.directions, { key: propertyName, direction }],
1107-
});
1108-
}
1109-
return new OrderVertexTraversal<TSchema, TPath>(this.graph, steps);
1097+
return new OrderVertexTraversal<TSchema, TPath>(
1098+
this.graph,
1099+
appendOrderDirection(this.steps, { key: propertyName, direction }),
1100+
);
11101101
}
11111102
}
11121103

@@ -1130,10 +1121,80 @@ export class ValueTraversal<const TSchema extends GraphSchema, const TValue> ext
11301121
new ValuesStep({}),
11311122
]);
11321123
}
1124+
1125+
/**
1126+
* Order the values in the traversal.
1127+
*/
1128+
public order() {
1129+
return new OrderValueTraversal<TSchema, TValue>(this.graph, [
1130+
...this.steps,
1131+
new OrderStep({ directions: [] }),
1132+
]);
1133+
}
1134+
}
1135+
1136+
export class OrderValueTraversal<
1137+
const TSchema extends GraphSchema,
1138+
const TValue,
1139+
> extends ValueTraversal<TSchema, TValue> {
1140+
/**
1141+
* Order the values in the traversal by their natural value.
1142+
*/
1143+
public by(): OrderValueTraversal<TSchema, TValue>;
1144+
/**
1145+
* Order the values in the traversal by a property on the value.
1146+
* @param propertyName The name of the property to order by.
1147+
* @param direction The direction to order by.
1148+
*/
1149+
public by<const TPropertyName extends keyof GetValueTraversalProperties<TValue> & string>(
1150+
propertyName: TPropertyName,
1151+
direction?: OrderDirection,
1152+
): OrderValueTraversal<TSchema, TValue>;
1153+
public by<const TPropertyName extends keyof GetValueTraversalProperties<TValue> & string>(
1154+
propertyName?: TPropertyName,
1155+
direction: OrderDirection = "asc",
1156+
) {
1157+
return new OrderValueTraversal<TSchema, TValue>(
1158+
this.graph,
1159+
appendOrderDirection(this.steps, { key: propertyName, direction }),
1160+
);
1161+
}
11331162
}
11341163

11351164
type ValueOf<T> = T[keyof T];
11361165

1166+
type GetValueTraversalProperties<TValue> =
1167+
TValue extends Element<any, any, infer TProperties, any>
1168+
? TProperties
1169+
: TValue extends object
1170+
? TValue
1171+
: never;
1172+
1173+
function appendOrderDirection(
1174+
steps: readonly Step<any>[],
1175+
orderDirection: {
1176+
key?: string;
1177+
direction: OrderDirection;
1178+
},
1179+
) {
1180+
const nextSteps = [...steps];
1181+
const orderStep =
1182+
nextSteps.length > 0 && nextSteps[nextSteps.length - 1] instanceof OrderStep
1183+
? nextSteps[nextSteps.length - 1]
1184+
: undefined;
1185+
1186+
if (!orderStep) {
1187+
nextSteps.push(new OrderStep({ directions: [orderDirection] }));
1188+
return nextSteps;
1189+
}
1190+
1191+
nextSteps[nextSteps.length - 1] = orderStep.clone({
1192+
directions: [...orderStep.config.directions, orderDirection],
1193+
});
1194+
1195+
return nextSteps;
1196+
}
1197+
11371198
type ResolveTraversalPathProperty<
11381199
TSchema extends GraphSchema,
11391200
TPath,

packages/graph/src/test/ValueTraversal.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,56 @@ test("ValueTraversal Operations - values() extraction - values() after map extra
5555
expect(names.includes("Alice")).toBe(true);
5656
});
5757

58+
test("ValueTraversal Operations - order() operation - order().by() sorts primitive values", () => {
59+
const names = Array.from(
60+
g
61+
.V()
62+
.hasLabel("Person")
63+
.map((path) => path.value.get("name"))
64+
.order()
65+
.by()
66+
.values(),
67+
);
68+
69+
expect(names.length).toBeGreaterThan(0);
70+
expect(names).toEqual([...names].sort());
71+
});
72+
73+
test("ValueTraversal Operations - order() operation - order().by() sorts unfolded primitive values", () => {
74+
const results = Array.from(
75+
g
76+
.V(alice.id)
77+
.map(() => [3, 1, 2])
78+
.unfold()
79+
.order()
80+
.by()
81+
.values(),
82+
);
83+
84+
expect(results).toEqual([1, 2, 3]);
85+
});
86+
87+
test("ValueTraversal Operations - order() operation - order().by(property) sorts object values", () => {
88+
const results = Array.from(
89+
g
90+
.V()
91+
.hasLabel("Person")
92+
.map((path) => ({
93+
bucket: path.value.get("age") >= 35 ? "older" : "younger",
94+
name: path.value.get("name"),
95+
}))
96+
.order()
97+
.by("bucket")
98+
.by("name")
99+
.values(),
100+
);
101+
102+
expect(results.length).toBeGreaterThan(0);
103+
104+
const keys = results.map(({ bucket, name }) => `${bucket}:${name}`);
105+
expect(keys).toEqual([...keys].sort());
106+
});
107+
58108
test("ValueTraversal Operations - values() extraction - values() on edge traversal", () => {
59109
const edges = Array.from(g.E().values());
60110

0 commit comments

Comments
 (0)