Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add array operation nodes #629

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
12 changes: 12 additions & 0 deletions .changeset/pink-roses-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@tokens-studio/graph-engine": minor
---

Added new array operation nodes:
- `unique`: Removes duplicate elements from an array
- `intersection`: Returns common elements between two arrays
- `union`: Combines arrays and removes duplicates
- `difference`: Returns elements in first array not in second array
- `shuffle`: Randomly reorders array elements using Fisher-Yates algorithm

Each node includes comprehensive tests and supports both primitive and object arrays while preserving object references.
47 changes: 47 additions & 0 deletions packages/graph-engine/src/nodes/array/difference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { AnyArraySchema } from '../../schemas/index.js';
import { INodeDefinition, Node } from '../../programmatic/node.js';
import { ToInput, ToOutput } from '../../programmatic/index.js';

export default class NodeDefinition<T> extends Node {
static title = 'Array Difference';
static type = 'studio.tokens.array.difference';
static description =
'Returns elements in first array that are not in second array';

declare inputs: ToInput<{
a: T[];
b: T[];
}>;

declare outputs: ToOutput<{
value: T[];
}>;

constructor(props: INodeDefinition) {
super(props);
this.addInput('a', {
type: AnyArraySchema
});
this.addInput('b', {
type: AnyArraySchema
});
this.addOutput('value', {
type: AnyArraySchema
});
}

execute(): void | Promise<void> {
const { a, b } = this.getAllInputs();

//Verify types
if (this.inputs.a.type.$id !== this.inputs.b.type.$id) {
throw new Error('Array types must match');
}

// Create set from second array for efficient lookup
const setB = new Set(b);
const difference = a.filter(item => !setB.has(item));

this.outputs.value.set(difference, this.inputs.a.type);
}
}
12 changes: 11 additions & 1 deletion packages/graph-engine/src/nodes/array/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,43 @@
import arraySubgraph from './arraySubgraph.js';
import arrify from './arrify.js';
import concat from './concat.js';
import difference from './difference.js';
import filter from './filter.js';
import find from './find.js';
import flatten from './flatten.js';
import indexArray from './indexArray.js';
import inject from './inject.js';
import intersection from './intersection.js';
import length from './length.js';
import push from './push.js';
import remove from './remove.js';
import replace from './replace.js';
import reverse from './reverse.js';
import shuffle from './shuffle.js';
import slice from './slice.js';
import sort from './sort.js';
import union from './union.js';
import unique from './unique.js';

export const nodes = [
arraySubgraph,
arrify,
concat,
difference,
filter,
find,
flatten,
indexArray,
inject,
intersection,
length,
push,
remove,
replace,
reverse,
shuffle,
slice,
sort
sort,
union,
unique
];
46 changes: 46 additions & 0 deletions packages/graph-engine/src/nodes/array/intersection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { AnyArraySchema } from '../../schemas/index.js';
import { INodeDefinition, Node } from '../../programmatic/node.js';
import { ToInput, ToOutput } from '../../programmatic/index.js';

export default class NodeDefinition<T> extends Node {
static title = 'Array Intersection';
static type = 'studio.tokens.array.intersection';
static description = 'Returns common elements between two arrays';

declare inputs: ToInput<{
a: T[];
b: T[];
}>;

declare outputs: ToOutput<{
value: T[];
}>;

constructor(props: INodeDefinition) {
super(props);
this.addInput('a', {
type: AnyArraySchema
});
this.addInput('b', {
type: AnyArraySchema
});
this.addOutput('value', {
type: AnyArraySchema
});
}

execute(): void | Promise<void> {
const { a, b } = this.getAllInputs();

//Verify types
if (this.inputs.a.type.$id !== this.inputs.b.type.$id) {
throw new Error('Array types must match');
}

// Create sets for efficient lookup
const setB = new Set(b);
const intersection = a.filter(item => setB.has(item));

this.outputs.value.set(intersection, this.inputs.a.type);
}
}
43 changes: 43 additions & 0 deletions packages/graph-engine/src/nodes/array/shuffle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { AnyArraySchema } from '../../schemas/index.js';
import { INodeDefinition, Node } from '../../programmatic/node.js';
import { ToInput, ToOutput } from '../../programmatic/index.js';

export default class NodeDefinition<T> extends Node {
static title = 'Shuffle Array';
static type = 'studio.tokens.array.shuffle';
static description =
'Randomly reorders elements in an array using Fisher-Yates algorithm';

declare inputs: ToInput<{
array: T[];
}>;

declare outputs: ToOutput<{
value: T[];
}>;

constructor(props: INodeDefinition) {
super(props);
this.addInput('array', {
type: AnyArraySchema
});
this.addOutput('value', {
type: AnyArraySchema
});
}

execute(): void | Promise<void> {
const { array } = this.getAllInputs();

// Create a copy of the array to avoid mutating the input
const shuffled = [...array];

// Fisher-Yates shuffle algorithm
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}

this.outputs.value.set(shuffled, this.inputs.array.type);
}
}
45 changes: 45 additions & 0 deletions packages/graph-engine/src/nodes/array/union.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AnyArraySchema } from '../../schemas/index.js';
import { INodeDefinition, Node } from '../../programmatic/node.js';
import { ToInput, ToOutput } from '../../programmatic/index.js';

export default class NodeDefinition<T> extends Node {
static title = 'Array Union';
static type = 'studio.tokens.array.union';
static description = 'Combines two arrays and removes duplicates';

declare inputs: ToInput<{
a: T[];
b: T[];
}>;

declare outputs: ToOutput<{
value: T[];
}>;

constructor(props: INodeDefinition) {
super(props);
this.addInput('a', {
type: AnyArraySchema
});
this.addInput('b', {
type: AnyArraySchema
});
this.addOutput('value', {
type: AnyArraySchema
});
}

execute(): void | Promise<void> {
const { a, b } = this.getAllInputs();

//Verify types
if (this.inputs.a.type.$id !== this.inputs.b.type.$id) {
throw new Error('Array types must match');
}

// Combine arrays and remove duplicates using Set
const union = [...new Set([...a, ...b])];

this.outputs.value.set(union, this.inputs.a.type);
}
}
34 changes: 34 additions & 0 deletions packages/graph-engine/src/nodes/array/unique.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { AnyArraySchema } from '../../schemas/index.js';
import { INodeDefinition, Node } from '../../programmatic/node.js';
import { ToInput, ToOutput } from '../../programmatic/index.js';

export default class NodeDefinition<T> extends Node {
static title = 'Unique Array';
static type = 'studio.tokens.array.unique';
static description = 'Removes duplicate elements from an array';

declare inputs: ToInput<{
array: T[];
}>;

declare outputs: ToOutput<{
value: T[];
}>;

constructor(props: INodeDefinition) {
super(props);
this.addInput('array', {
type: AnyArraySchema
});
this.addOutput('value', {
type: AnyArraySchema
});
}

execute(): void | Promise<void> {
const { array } = this.getAllInputs();
// Use Set to remove duplicates and convert back to array
const uniqueArray = [...new Set(array)];
this.outputs.value.set(uniqueArray, this.inputs.array.type);
}
}
77 changes: 77 additions & 0 deletions packages/graph-engine/tests/suites/nodes/array/difference.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Graph } from '../../../../src/graph/graph.js';
import { describe, expect, test } from 'vitest';
import Node from '../../../../src/nodes/array/difference.js';

describe('array/difference', () => {
test('returns elements in first array not in second', async () => {
const graph = new Graph();
const node = new Node({ graph });

const a = [1, 2, 3, 4];
const b = [3, 4, 5, 6];

node.inputs.a.setValue(a);
node.inputs.b.setValue(b);

await node.execute();

const actual = node.outputs.value.value;

expect(actual).to.eql([1, 2]);
// Ensure original arrays weren't modified
expect(a).to.eql([1, 2, 3, 4]);
expect(b).to.eql([3, 4, 5, 6]);
});

test('handles arrays with objects', async () => {
const graph = new Graph();
const node = new Node({ graph });

const obj1 = { id: 1 };
const obj2 = { id: 2 };
const obj3 = { id: 3 };

const a = [obj1, obj2, obj3];
const b = [obj2, obj3];

node.inputs.a.setValue(a);
node.inputs.b.setValue(b);

await node.execute();

const actual = node.outputs.value.value;

expect(actual).to.have.length(1);
expect(actual[0]).to.equal(obj1);
});

test('handles empty arrays', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.a.setValue([1, 2, 3]);
node.inputs.b.setValue([]);

await node.execute();

expect(node.outputs.value.value).to.eql([1, 2, 3]);

// Test with empty first array
node.inputs.a.setValue([]);
node.inputs.b.setValue([1, 2, 3]);

await node.execute();

expect(node.outputs.value.value).to.eql([]);
});

test('throws error when array types do not match', async () => {
const graph = new Graph();
const node = new Node({ graph });

node.inputs.a.setValue([1, 2, 3]);
node.inputs.b.setValue(['a', 'b', 'c']);

await expect(node.execute()).rejects.toThrow('Array types must match');
});
});
Loading
Loading