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

feat(performance): support for Map as dataset #93

Closed
wants to merge 3 commits into from
Closed
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
38 changes: 36 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,42 @@ The function will throw `GroqSyntaxError` if there's a syntax error in the query

## `evaluate`

`evaluate` accepts a node returned by [`parse`](#parse) and evaluates the query. Example:

```typescript
let tree = parse('*[_type == "user"]{name}')

let dataset = [
{_type: 'user', name: 'Michael'},
{_type: 'company', name: 'Bluth Company'},
]

// Evaluate a tree against a dataset
let value = await evaluate(tree, {dataset})
```

`evaluate` accepts the `dataset` parameter in the following formats:

**List of documents**

```typescript
let dataset = [
{_type: 'user', name: 'Michael'},
{_type: 'company', name: 'Bluth Company'},
]
```

**ID-Document Map**

```typescript
let dataset = new Map([
['abc123', {_id: 'abc123', _type: 'user', name: 'Michael'}],
['def456', {_id: 'def456', _type: 'company', name: 'Bluth Company'},
])
```

Other options:

```typescript
interface EvaluateOptions {
// The value that will be available as `@` in GROQ.
Expand Down Expand Up @@ -67,5 +103,3 @@ interface EvaluateOptions {

declare async function evaluate(node: ExprNode, options: EvaluateOptions = {})
```

`evaluate` accepts a node returned by [`parse`](#parse) and evaluates the query.
25 changes: 20 additions & 5 deletions src/evaluator/evaluate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
fromJS,
fromNumber,
NULL_VALUE,
ObjectValue,
StreamValue,
TRUE_VALUE,
Value,
Expand Down Expand Up @@ -142,7 +143,7 @@ const EXECUTORS: ExecutorMap = {

async Filter({base, expr}, scope, execute) {
const baseValue = await execute(base, scope)
if (!baseValue.isArray()) {
if (!baseValue.isArray() && !baseValue.isMap()) {
return NULL_VALUE
}
return new StreamValue(async function* () {
Expand Down Expand Up @@ -242,7 +243,7 @@ const EXECUTORS: ExecutorMap = {
async Deref({base}, scope, execute) {
const value = await execute(base, scope)

if (!scope.source.isArray()) {
if (!scope.source.isArray() && !scope.source.isMap()) {
return NULL_VALUE
}

Expand All @@ -255,9 +256,23 @@ const EXECUTORS: ExecutorMap = {
return NULL_VALUE
}

for await (const doc of scope.source) {
if (doc.type === 'object' && id === doc.data._id) {
return doc
if (scope.source.isArray()) {
for await (const doc of scope.source) {
if (doc.type === 'object' && id === doc.data._id) {
return doc
}
}
}

if (scope.source.isMap()) {
const map = (await scope.source.get()) as Map<any, any>
const doc = map.get(id)

if (doc) {
return {
type: 'object',
data: doc,
} as ObjectValue
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/evaluator/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ global.coalesce = async function coalesce(args, scope, execute) {

global.count = async function count(args, scope, execute) {
const inner = await execute(args[0], scope)
if (!inner.isArray()) {
if (!inner.isArray() && !inner.isMap()) {
return NULL_VALUE
}

Expand Down
5 changes: 5 additions & 0 deletions src/values/StreamValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export class StreamValue {
return true
}

// eslint-disable-next-line class-methods-use-this
isMap(): boolean {
return true
}

async get(): Promise<any> {
const result = []
for await (const value of this) {
Expand Down
3 changes: 3 additions & 0 deletions src/values/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type GroqType =
| 'number'
| 'string'
| 'array'
| 'map'
| 'object'
| 'path'
| 'datetime'
Expand All @@ -28,6 +29,7 @@ export type DateTimeValue = StaticValue<DateTime, 'datetime'>
export type PathValue = StaticValue<Path, 'path'>
export type ObjectValue = StaticValue<Record<string, unknown>, 'object'>
export type ArrayValue = StaticValue<unknown[], 'array'>
export type MapValue = StaticValue<Map<string, Value>, 'map'>

export type AnyStaticValue =
| StringValue
Expand All @@ -37,4 +39,5 @@ export type AnyStaticValue =
| DateTimeValue
| ObjectValue
| ArrayValue
| MapValue
| PathValue
17 changes: 17 additions & 0 deletions src/values/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export class StaticValue<P, T extends GroqType> {
return this.type === 'array'
}

isMap(): boolean {
return this.type === 'map'
}

// eslint-disable-next-line require-await
async get(): Promise<any> {
return this.data
Expand All @@ -29,6 +33,16 @@ export class StaticValue<P, T extends GroqType> {
}
})(this.data)
}

if (this.data instanceof Map) {
return (function* (data) {
// This can be simplified when using es2015 target
for (const [id, value] of Array.from(data.entries())) {
yield fromJS(value)
}
})(this.data)
}

throw new Error(`Cannot iterate over: ${this.type}`)
}
}
Expand Down Expand Up @@ -133,5 +147,8 @@ export function getType(data: any): GroqType {
if (data instanceof DateTime) {
return 'datetime'
}
if (data instanceof Map) {
return 'map'
}
return typeof data as GroqType
}
60 changes: 60 additions & 0 deletions test/evaluate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ t.test('Basic parsing', async (t) => {
t.same(data, [{name: 'T-shirt'}, {name: 'Pants'}])
})

t.test('Example query with Map as dataset', async (t) => {
const dataset = new Map([
['a1', {_id: 'a1', _type: 'product', name: 'T-shirt'}],
['b2', {_id: 'b2', _type: 'product', name: 'Pants'}],
['c3', {_id: 'c3', _type: 'user', name: 'Bob'}],
])
const query = `*[_type == "product"]{name}`
const tree = parse(query)

const value = await evaluate(tree, {dataset})
const data = await value.get()
t.same(data, [{name: 'T-shirt'}, {name: 'Pants'}])
})

t.test('String function', async (t) => {
const dataset = [
{_type: 'color', color: 'red', shade: 500, rgb: {r: 255, g: 0, b: 0}},
Expand Down Expand Up @@ -103,6 +117,52 @@ t.test('Basic parsing', async (t) => {
t.same(data, ['Michael'])
})

t.test('Referencing with Map as dataset', async (t) => {
const dataset = new Map([
['a', {_id: 'a', _type: 'person', name: 'Michael'}],
['b', {_id: 'b', _type: 'person', name: 'George Michael', father: {_ref: 'a'}}],
])

const query = `*[_type == "person"]{_id, name, "father": father->name}`
const tree = parse(query)
const value = await evaluate(tree, {dataset})
const data = await value.get()
t.same(data, [
{_id: 'a', name: 'Michael', father: null},
{_id: 'b', name: 'George Michael', father: 'Michael'},
])
})

t.test('Count function', async (t) => {
t.test('Count function with array dataset', async (t) => {
const dataset = [
{_type: 'product', name: 'T-shirt'},
{_type: 'product', name: 'Pants'},
{_type: 'user', name: 'Bob'},
]
const query = `count(*)`
const tree = parse(query)

const value = await evaluate(tree, {dataset})
const data = await value.get()
t.same(data, 3)
})

t.test('Count function with map dataset', async (t) => {
const dataset = new Map([
['a1', {_id: 'a1', _type: 'product', name: 'T-shirt'}],
['b2', {_id: 'b2', _type: 'product', name: 'Pants'}],
['c3', {_id: 'c3', _type: 'user', name: 'Bob'}],
])
const query = `count(*)`
const tree = parse(query)

const value = await evaluate(tree, {dataset})
const data = await value.get()
t.same(data, 3)
})
})

t.test('Non-array documents', async (t) => {
const dataset = {data: [{person: {_ref: 'b'}}]}

Expand Down