Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/bright-turkeys-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@codemix/graph": minor
---

Expand traversal parity across value and edge pipelines.

`ValueTraversal` now supports `dedup()`, `skip()`, `limit()`, `range()`, `count()`, `property()`, and `properties()` so extracted values can keep flowing through the same shaping and projection steps as other traversal results.

`EdgeTraversal` now supports direct `skip()`, `limit()`, `range()`, `count()`, `map()`, `property()`, `properties()`, and `order()` operations, making it possible to paginate, transform, project, and sort edges without first converting them to vertices or raw values.
25 changes: 25 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash

set -euo pipefail

repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"

staged_files=()
while IFS= read -r -d '' file; do
case "$file" in
*.cjs|*.css|*.cts|*.html|*.js|*.json|*.jsonc|*.jsx|*.md|*.mdx|*.mjs|*.mts|*.scss|*.ts|*.tsx|*.yaml|*.yml)
if [[ -f "$file" ]]; then
staged_files+=("$file")
fi
;;
esac
done < <(git diff --cached --name-only --diff-filter=ACMR -z)

if (( ${#staged_files[@]} == 0 )); then
exit 0
fi

echo "Formatting staged files with oxfmt"
pnpm exec oxfmt --write -- "${staged_files[@]}"
git add -- "${staged_files[@]}"
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ A fully typed, in-memory graph database. Vertices and edges are strongly typed a
pnpm add @codemix/graph
```

### Development

Running `pnpm install` in the monorepo configures Git to use the tracked hooks in `.githooks`.

The pre-commit hook formats staged supported source files with `oxfmt` and restages them automatically. You can re-run the hook setup at any time with `pnpm run setup:hooks`.

### Quick Start

```typescript
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
"scripts": {
"build": "pnpm run -r build",
"changeset": "changeset",
"prepare": "sh ./scripts/setup-git-hooks.sh",
"version-packages": "changeset version",
"release": "pnpm build && changeset publish",
"setup:hooks": "sh ./scripts/setup-git-hooks.sh",
"typecheck": "pnpm run -r typecheck",
"test": "vitest",
"test:coverage": "vitest run --coverage",
Expand Down
272 changes: 271 additions & 1 deletion packages/graph/src/Traversals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,7 @@ export class VertexTraversal<const TSchema extends GraphSchema, const TPath> ext
[
...this.steps,
new MapElementsStep<any>({
mapper: (path) => path.value.properties[propertyName],
mapper: (path) => path.value.get(propertyName),
}),
],
);
Expand Down Expand Up @@ -1106,6 +1106,53 @@ export class ValueTraversal<const TSchema extends GraphSchema, const TValue> ext
TSchema,
TValue
> {
/**
* Deduplicate the values in the traversal.
*/
public dedup() {
return new ValueTraversal<TSchema, TValue>(this.graph, [...this.steps, new DedupStep({})]);
}

/**
* Skip the first n values in the traversal.
*/
public skip(n: number) {
return new ValueTraversal<TSchema, TValue>(this.graph, [
...this.steps,
new RangeStep({ start: n, end: Infinity }),
]);
}

/**
* Take the first n values in the traversal.
* @param n The number of values to take.
*/
public limit(n: number) {
return new ValueTraversal<TSchema, TValue>(this.graph, [
...this.steps,
new RangeStep({ start: 0, end: n }),
]);
}

/**
* Slice the values in the traversal.
* @param start The index to start slicing
* @param end The index to end slicing
*/
public range(start: number, end: number) {
return new ValueTraversal<TSchema, TValue>(this.graph, [
...this.steps,
new RangeStep({ start, end }),
]);
}

/**
* Count the number of values in the traversal.
*/
public count() {
return new ValueTraversal<TSchema, number>(this.graph, [...this.steps, new CountStep({})]);
}

/**
* Map each value in the traversal to a new value.
* @param mapper A function that maps the current value to a new value.
Expand Down Expand Up @@ -1149,6 +1196,93 @@ export class ValueTraversal<const TSchema extends GraphSchema, const TValue> ext
]);
}

/**
* Select a single property of the values in the traversal.
* Supports both graph elements and plain object values.
* @param propertyName The name of the property to select.
*/
public property<const TPropertyName extends keyof GetValueTraversalProperties<TValue>>(
propertyName: TPropertyName,
) {
return new ValueTraversal<TSchema, GetValueTraversalProperties<TValue>[TPropertyName]>(
this.graph,
[
...this.steps,
new MapElementsStep<TValue>({
mapper: (value) => {
if (value instanceof Element) {
return value.get(propertyName as never);
}
if (typeof value === "object" && value !== null) {
return value[propertyName as keyof typeof value];
}
return undefined as GetValueTraversalProperties<TValue>[TPropertyName];
},
}),
],
);
}

/**
* Select specific properties of the values in the traversal.
* Supports both graph elements and plain object values.
* @param propertyNames The names of the properties to select.
*/
public properties(): ValueTraversal<TSchema, GetValueTraversalProperties<TValue>>;
public properties<
const TPropertyNames extends readonly (keyof GetValueTraversalProperties<TValue>)[],
>(
...propertyNames: TPropertyNames
): ValueTraversal<TSchema, Pick<GetValueTraversalProperties<TValue>, TPropertyNames[number]>>;
public properties<
const TPropertyNames extends readonly (keyof GetValueTraversalProperties<TValue>)[],
>(...propertyNames: TPropertyNames): ValueTraversal<TSchema, any> {
return new ValueTraversal<
TSchema,
Pick<GetValueTraversalProperties<TValue>, TPropertyNames[number]>
>(this.graph, [
...this.steps,
new MapElementsStep<TValue>({
mapper: (value) => {
if (value instanceof Element) {
const storedProps = value[$StoredElement].properties;
if (propertyNames.length === 0) {
return storedProps as Pick<
GetValueTraversalProperties<TValue>,
TPropertyNames[number]
>;
}
const properties = {} as Pick<
GetValueTraversalProperties<TValue>,
TPropertyNames[number]
>;
for (const propertyName of propertyNames) {
properties[propertyName] = value.get(propertyName as never);
}
return properties;
}
if (typeof value === "object" && value !== null) {
if (propertyNames.length === 0) {
return value as Pick<GetValueTraversalProperties<TValue>, TPropertyNames[number]>;
}
const properties = {} as Pick<
GetValueTraversalProperties<TValue>,
TPropertyNames[number]
>;
for (const propertyName of propertyNames) {
properties[propertyName] = value[propertyName as keyof typeof value] as Pick<
GetValueTraversalProperties<TValue>,
TPropertyNames[number]
>[typeof propertyName];
}
return properties;
}
return {} as Pick<GetValueTraversalProperties<TValue>, TPropertyNames[number]>;
},
}),
]);
}

/**
* Order the values in the traversal.
*/
Expand Down Expand Up @@ -1900,6 +2034,46 @@ export class EdgeTraversal<const TSchema extends GraphSchema, const TPath> exten
return new EdgeTraversal<TSchema, TPath>(this.graph, [...this.steps, new DedupStep({})]);
}

/**
* Skip the first n edges in the traversal.
*/
public skip(n: number) {
return new EdgeTraversal<TSchema, TPath>(this.graph, [
...this.steps,
new RangeStep({ start: n, end: Infinity }),
]);
}

/**
* Take the first n edges in the traversal.
* @param n The number of edges to take.
*/
public limit(n: number) {
return new EdgeTraversal<TSchema, TPath>(this.graph, [
...this.steps,
new RangeStep({ start: 0, end: n }),
]);
}

/**
* Slice the edges in the traversal.
* @param start The index to start slicing
* @param end The index to end slicing
*/
public range(start: number, end: number) {
return new EdgeTraversal<TSchema, TPath>(this.graph, [
...this.steps,
new RangeStep({ start, end }),
]);
}

/**
* Count the number of edges in the traversal.
*/
public count() {
return new ValueTraversal<TSchema, number>(this.graph, [...this.steps, new CountStep({})]);
}

/**
* Select the labeled elements in the path
* @param pathLabels The labels to select.
Expand Down Expand Up @@ -1927,4 +2101,100 @@ export class EdgeTraversal<const TSchema extends GraphSchema, const TPath> exten
new ValuesStep({}),
]);
}

/**
* Select a single property of the edges in the traversal.
* @param propertyName The name of the property to select.
*/
public property<const TPropertyName extends keyof GetTraversalPathProperties<TPath>>(
propertyName: TPropertyName,
) {
return new ValueTraversal<TSchema, GetTraversalPathProperties<TPath>[TPropertyName]>(
this.graph,
[
...this.steps,
new MapElementsStep<any>({
mapper: (path) => path.value.get(propertyName),
}),
],
);
}

/**
* Select specific properties of the edges in the traversal.
* @param propertyNames The names of the properties to select.
*/
public properties(): ValueTraversal<TSchema, GetTraversalPathProperties<TPath>>;
public properties<
const TPropertyNames extends readonly (keyof GetTraversalPathProperties<TPath>)[],
>(
...propertyNames: TPropertyNames
): ValueTraversal<TSchema, Pick<GetTraversalPathProperties<TPath>, TPropertyNames[number]>>;
public properties<
const TPropertyNames extends readonly (keyof GetTraversalPathProperties<TPath>)[],
>(...propertyNames: TPropertyNames) {
return new ValueTraversal<
TSchema,
Pick<GetTraversalPathProperties<TPath>, TPropertyNames[number]>
>(this.graph, [
...this.steps,
new MapElementsStep<any>({
mapper: (path) => {
const value = path.value;
const storedProps = value[$StoredElement].properties;
if (propertyNames.length === 0) {
return storedProps;
}
const properties = {} as any;
for (const propertyName of propertyNames) {
properties[propertyName] = storedProps[propertyName as keyof typeof storedProps];
}
return properties;
},
}),
]);
}

/**
* Map each edge in the traversal to a new value.
* @param mapper A function that maps the path to a new value.
*/
public map<const TValue>(mapper: (path: TPath) => TValue) {
return new ValueTraversal<TSchema, TValue>(this.graph, [
...this.steps,
new MapElementsStep<TPath>({
mapper,
}),
]);
}

/**
* Order the edges in the traversal.
*/
public order() {
return new OrderEdgeTraversal<TSchema, TPath>(this.graph, [
...this.steps,
new OrderStep({ directions: [] }),
]);
}
}

export class OrderEdgeTraversal<
const TSchema extends GraphSchema,
const TPath,
> extends EdgeTraversal<TSchema, TPath> {
/**
* Order the edges in the traversal by a property.
* @param propertyName The name of the property to order by.
* @param direction The direction to order by.
*/
public by<const TPropertyName extends keyof GetTraversalPathProperties<TPath> & string>(
propertyName: TPropertyName,
direction: OrderDirection = "asc",
) {
return new OrderEdgeTraversal<TSchema, TPath>(
this.graph,
appendOrderDirection(this.steps, { key: propertyName, direction }),
);
}
}
Loading
Loading