Skip to content

Commit e00c212

Browse files
authored
Merge pull request #5 from codemix/map-filter-chaining
Add FilterPredicateStep and related methods to ValueTraversal
2 parents 7155793 + 977c868 commit e00c212

5 files changed

Lines changed: 112 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@codemix/graph": minor
3+
---
4+
5+
Add support for `.map()` and `.filter()` on `ValueTraversal`, so value pipelines can transform and filter extracted values after steps like `values()` and `unfold()`.

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ This is the knowledge graph database for the [codemix](https://codemix.com/) pro
1010

1111
## Packages
1212

13-
| Package | Version | Description |
14-
| -------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------- |
15-
| [`@codemix/graph`](./packages/graph) | 0.0.1 | Core graph database: Cypher queries + type-safe Gremlin-style traversals (`GraphTraversal`) |
16-
| [`@codemix/text-search`](./packages/text-search) | 0.0.1 | BM25-based full-text search with English stemming |
17-
| [`@codemix/y-graph-storage`](./packages/y-graph-storage) | 0.0.1 | [Yjs](https://yjs.dev/) CRDT storage adapter for collaborative/offline-first use |
13+
| Package | Description |
14+
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
15+
| [`@codemix/graph`](./packages/graph) | Core graph database: Cypher queries + type-safe Gremlin-style traversals (`GraphTraversal`) |
16+
| [`@codemix/text-search`](./packages/text-search) | BM25-based full-text search with English stemming |
17+
| [`@codemix/y-graph-storage`](./packages/y-graph-storage) | [Yjs](https://yjs.dev/) CRDT storage adapter for collaborative/offline-first use |
1818

1919
---
2020

packages/graph/src/Steps.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2876,6 +2876,46 @@ export class MapElementsStep<TInput> extends Step<MapElementsStepConfig<TInput>>
28762876
}
28772877
}
28782878

2879+
export interface FilterPredicateStepConfig<TInput> extends StepConfig {
2880+
/**
2881+
* The predicate used to decide whether to keep each element.
2882+
*/
2883+
predicate: (value: TInput) => boolean;
2884+
}
2885+
2886+
export class FilterPredicateStep<TInput> extends Step<FilterPredicateStepConfig<TInput>> {
2887+
public get name() {
2888+
return "FilterPredicate";
2889+
}
2890+
2891+
public *traverse(
2892+
_source: GraphSource<any>,
2893+
input: Iterable<TInput>,
2894+
_context?: QueryContext,
2895+
): IterableIterator<unknown> {
2896+
const { predicate } = this.config;
2897+
for (const value of input) {
2898+
this.traversed++;
2899+
if (predicate(value as TInput)) {
2900+
this.emitted++;
2901+
yield value;
2902+
}
2903+
}
2904+
}
2905+
2906+
public override clone(partial?: Partial<FilterPredicateStepConfig<TInput>>) {
2907+
const { config } = this;
2908+
return new FilterPredicateStep({
2909+
predicate: partial?.predicate ?? config.predicate,
2910+
stepLabels: partial?.stepLabels ?? (config.stepLabels ? [...config.stepLabels] : undefined),
2911+
});
2912+
}
2913+
2914+
public override toJSON(): [string, FilterPredicateStepConfig<TInput>, unknown?] {
2915+
throw new Error("Cannot convert FilterPredicateStep to JSON.");
2916+
}
2917+
}
2918+
28792919
export interface GroupByStepConfig extends StepConfig {
28802920
/**
28812921
* The items to group by (variable, property, function like labels/type).
@@ -7360,6 +7400,7 @@ const KnownSteps = {
73607400
Unwind: UnwindStep,
73617401
Call: CallStep,
73627402
MapElements: MapElementsStep,
7403+
FilterPredicate: FilterPredicateStep,
73637404
Range: RangeStep,
73647405
Order: OrderStep,
73657406
FilterElements: FilterElementsStep,
@@ -7394,6 +7435,7 @@ export type KnownSteps =
73947435
| RepeatStep<any>
73957436
| DedupStep
73967437
| MapElementsStep<any>
7438+
| FilterPredicateStep<any>
73977439
| RangeStep
73987440
| OrderStep
73997441
| SumStep

packages/graph/src/Traversals.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
FetchEdgesStep,
1919
FetchVerticesStep,
2020
FilterElementsStep,
21+
FilterPredicateStep,
2122
IntersectStep,
2223
MapElementsStep,
2324
OrderDirection,
@@ -1105,6 +1106,32 @@ export class ValueTraversal<const TSchema extends GraphSchema, const TValue> ext
11051106
TSchema,
11061107
TValue
11071108
> {
1109+
/**
1110+
* Map each value in the traversal to a new value.
1111+
* @param mapper A function that maps the current value to a new value.
1112+
*/
1113+
public map<const TNextValue>(mapper: (value: TValue) => TNextValue) {
1114+
return new ValueTraversal<TSchema, TNextValue>(this.graph, [
1115+
...this.steps,
1116+
new MapElementsStep<TValue>({
1117+
mapper,
1118+
}),
1119+
]);
1120+
}
1121+
1122+
/**
1123+
* Filter the values in the traversal using a predicate.
1124+
* @param predicate A function that returns true for values to keep.
1125+
*/
1126+
public filter(predicate: (value: TValue) => boolean) {
1127+
return new ValueTraversal<TSchema, TValue>(this.graph, [
1128+
...this.steps,
1129+
new FilterPredicateStep<TValue>({
1130+
predicate,
1131+
}),
1132+
]);
1133+
}
1134+
11081135
/**
11091136
* Unfold each value in the traversal.
11101137
*/

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

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

58+
test("ValueTraversal Operations - map() operation - map() after values() transforms extracted vertices", () => {
59+
const names = Array.from(
60+
g
61+
.V()
62+
.hasLabel("Person")
63+
.limit(3)
64+
.values()
65+
.map((vertex) => vertex.get("name"))
66+
.order()
67+
.by(),
68+
);
69+
70+
expect(names).toEqual(["Alice", "Bob", "Charlie"]);
71+
});
72+
73+
test("ValueTraversal Operations - filter() operation - filter() after values() keeps matching vertices", () => {
74+
const names = Array.from(
75+
g
76+
.V()
77+
.hasLabel("Person")
78+
.values()
79+
.filter((vertex) => vertex.get("age") >= 50)
80+
.map((vertex) => vertex.get("name"))
81+
.order()
82+
.by(),
83+
);
84+
85+
expect(names).toEqual(["Fiona", "George"]);
86+
});
87+
5888
test("ValueTraversal Operations - order() operation - order().by() sorts primitive values", () => {
5989
const names = Array.from(
6090
g
@@ -243,10 +273,12 @@ test("ValueTraversal Operations - unfold() operation - unfold() with subsequent
243273
.V(alice.id)
244274
.map(() => [10, 20, 30, 40])
245275
.unfold()
276+
.filter((value) => value >= 20)
277+
.map((value) => value / 10)
246278
.values(),
247279
);
248280

249-
expect(results).toEqual([10, 20, 30, 40]);
281+
expect(results).toEqual([2, 3, 4]);
250282
});
251283

252284
test("ValueTraversal Operations - Combined values() and unfold() operations - Select multiple labels, unfold, and extract values", () => {

0 commit comments

Comments
 (0)