diff --git a/graphql/openreader/src/dialect/common.ts b/graphql/openreader/src/dialect/common.ts index 259ef7b87..7e15131c5 100644 --- a/graphql/openreader/src/dialect/common.ts +++ b/graphql/openreader/src/dialect/common.ts @@ -5,7 +5,7 @@ import {OrderBy, Where} from '../ir/args' import assert from 'assert' export enum Dialect { - OpenCRUD = 'opencrud', + OpenCrud = 'opencrud', TheGraph = 'thegraph', } @@ -47,45 +47,3 @@ export function toCondition(op: 'AND' | 'OR', operands: Where[]): Where | undefi return {op, args: operands} } } - - -export function parseWhereKey(key: string): {op: Where['op'], field: string} { - let m = WHERE_KEY_REGEX.exec(key) - if (m) { - return {op: m[2] as Where['op'], field: m[1]} - } else { - return {op: 'REF', field: key} - } -} - - -const WHERE_KEY_REGEX = (() => { - let ops: Where['op'][] = [ - "eq", - "not_eq", - "gt", - "gte", - "lt", - "lte", - "contains", - "not_contains", - "containsInsensitive", - "not_containsInsensitive", - "startsWith", - "not_startsWith", - "endsWith", - "not_endsWith", - "containsAll", - "containsAny", - "containsNone", - "jsonContains", - "jsonHasKey", - "isNull", - "some", - "every", - "none", - "in", - "not_in", - ] - return new RegExp(`^([^_]*)_(${ops.join('|')})$`) -})() \ No newline at end of file diff --git a/graphql/openreader/src/dialect/index.ts b/graphql/openreader/src/dialect/index.ts index bad359fbc..774b38ac7 100644 --- a/graphql/openreader/src/dialect/index.ts +++ b/graphql/openreader/src/dialect/index.ts @@ -6,7 +6,7 @@ export * from './common' export function getSchemaBuilder(options: SchemaOptions & {dialect?: Dialect}): SchemaBuilder { switch (options.dialect) { case undefined: - case Dialect.OpenCRUD: + case Dialect.OpenCrud: return new (require('./opencrud/schema').SchemaBuilder)(options) case Dialect.TheGraph: return new (require('./thegraph/schema').SchemaBuilder)(options) diff --git a/graphql/openreader/src/dialect/opencrud/where.ts b/graphql/openreader/src/dialect/opencrud/where.ts index 10c8667ab..248efb25d 100644 --- a/graphql/openreader/src/dialect/opencrud/where.ts +++ b/graphql/openreader/src/dialect/opencrud/where.ts @@ -2,7 +2,7 @@ import {unexpectedCase} from "@subsquid/util-internal" import assert from "assert" import {Where} from "../../ir/args" import {ensureArray} from "../../util/util" -import {parseWhereKey, toCondition} from '../common' +import {toCondition} from '../common' export function parseWhere(whereArg?: any): Where | undefined { @@ -86,3 +86,44 @@ export function parseWhere(whereArg?: any): Where | undefined { return conjunction } } + +export function parseWhereKey(key: string): {op: Where['op'], field: string} { + let m = WHERE_KEY_REGEX.exec(key) + if (m) { + return {op: m[2] as Where['op'], field: m[1]} + } else { + return {op: 'REF', field: key} + } +} + + +const WHERE_KEY_REGEX = (() => { + let ops: Where['op'][] = [ + "eq", + "not_eq", + "gt", + "gte", + "lt", + "lte", + "contains", + "not_contains", + "containsInsensitive", + "not_containsInsensitive", + "startsWith", + "not_startsWith", + "endsWith", + "not_endsWith", + "containsAll", + "containsAny", + "containsNone", + "jsonContains", + "jsonHasKey", + "isNull", + "some", + "every", + "none", + "in", + "not_in", + ] + return new RegExp(`^([^_]*)_(${ops.join('|')})$`) +})() \ No newline at end of file diff --git a/graphql/openreader/src/dialect/thegraph/orderBy.ts b/graphql/openreader/src/dialect/thegraph/orderBy.ts index fa0891409..99583de72 100644 --- a/graphql/openreader/src/dialect/thegraph/orderBy.ts +++ b/graphql/openreader/src/dialect/thegraph/orderBy.ts @@ -1,19 +1,15 @@ -import assert from "assert" -import type { Model } from "../../model" -import { getUniversalProperties } from '../../model.tools' -import { OrderBy, SortOrder } from "../../ir/args" +import assert from 'assert' +import type {Model} from '../../model' +import {getUniversalProperties} from '../../model.tools' +import {OrderBy, SortOrder} from '../../ir/args' import {mergeOrderBy} from '../common' - export type TheGraphOrderByValue = string - export type TheGraph_OrderBy_List = ReadonlySet - const MAPPING_CACHE = new WeakMap>() - export function getOrderByList(model: Model, typeName: string): TheGraph_OrderBy_List { let cache = MAPPING_CACHE.get(model) if (cache == null) { @@ -21,10 +17,9 @@ export function getOrderByList(model: Model, typeName: string): TheGraph_OrderBy MAPPING_CACHE.set(model, cache) } if (cache[typeName]) return cache[typeName] - return cache[typeName] = buildOrderByList(model, typeName, 2) + return (cache[typeName] = buildOrderByList(model, typeName, 2)) } - function buildOrderByList(model: Model, typeName: string, depth: number): TheGraph_OrderBy_List { if (depth <= 0) return new Set() let properties = getUniversalProperties(model, typeName) @@ -40,22 +35,19 @@ function buildOrderByList(model: Model, typeName: string, depth: number): TheGra break case 'object': case 'union': - for (let [name, spec] of buildOrderByList(model, propType.name, depth - 1)) { + for (let name of buildOrderByList(model, propType.name, depth - 1)) { m.add(key + '__' + name) } break case 'fk': case 'lookup': m.add(key) - for (let [name, spec] of buildOrderByList(model, propType.entity, depth - 1)) { + for (let name of buildOrderByList(model, propType.entity, depth - 1)) { m.add(key + '__' + name) } break } } - if (model[typeName].kind == 'interface') { - m.add('__type') - } return m } @@ -68,14 +60,16 @@ export const ORDER_DIRECTIONS: Record = { desc_nulls_last: 'DESC NULLS LAST', } -export function parseOrderBy(model: Model, typeName: string, input: {orderBy: string, direction?: string}): OrderBy { +export function parseOrderBy(model: Model, typeName: string, input: {orderBy: string; direction?: string}): OrderBy { let list = getOrderByList(model, typeName) assert(list.has(input.orderBy)) - const sortOrder = input.orderBy ? ORDER_DIRECTIONS[input.orderBy] : ORDER_DIRECTIONS['asc'] + const sortOrder = input.direction ? ORDER_DIRECTIONS[input.direction] : ORDER_DIRECTIONS['asc'] assert(sortOrder) - return { - [input.orderBy]: sortOrder - } -} \ No newline at end of file + const keys = input.orderBy.split('__').reverse() + const res = keys.reduce((res: OrderBy | null, key) => ({[key]: res ?? sortOrder}), null) + assert(res) + + return res +} diff --git a/graphql/openreader/src/dialect/thegraph/tree.ts b/graphql/openreader/src/dialect/thegraph/tree.ts index e2afae463..6167bc390 100644 --- a/graphql/openreader/src/dialect/thegraph/tree.ts +++ b/graphql/openreader/src/dialect/thegraph/tree.ts @@ -8,7 +8,7 @@ import {Model} from '../../model' import {getQueryableEntities} from '../../model.tools' import {simplifyResolveTree} from '../../util/resolve-tree' import {parseWhere} from './where' - +import {parseOrderBy} from './orderBy' export function parseObjectTree( model: Model, @@ -19,16 +19,16 @@ export function parseObjectTree( let requests: FieldRequest[] = [] let requestedScalars: Record = {} let object = model[typeName] - assert(object.kind == "entity" || object.kind == "object") + assert(object.kind == 'entity' || object.kind == 'object') let fields = simplifyResolveTree(schema, tree, typeName).fields for (let alias in fields) { let f = fields[alias] let prop = object.properties[f.name] - switch(prop.type.kind) { - case "scalar": - case "enum": - case "list": + switch (prop.type.kind) { + case 'scalar': + case 'enum': + case 'list': if (requestedScalars[f.name] == null) { requestedScalars[f.name] = true requests.push({ @@ -37,11 +37,11 @@ export function parseObjectTree( kind: prop.type.kind, type: prop.type, prop, - index: 0 + index: 0, } as OpaqueRequest) } break - case "object": + case 'object': requests.push({ field: f.name, aliases: [f.alias], @@ -49,12 +49,12 @@ export function parseObjectTree( type: prop.type, prop, index: 0, - children: parseObjectTree(model, prop.type.name, schema, f) + children: parseObjectTree(model, prop.type.name, schema, f), }) break - case "union": { + case 'union': { let union = model[prop.type.name] - assert(union.kind == "union") + assert(union.kind == 'union') let children: FieldRequest[] = [] for (let variant of union.variants) { for (let req of parseObjectTree(model, variant, schema, f)) { @@ -69,11 +69,11 @@ export function parseObjectTree( type: prop.type, prop, index: 0, - children + children, }) break } - case "fk": + case 'fk': requests.push({ field: f.name, aliases: [f.alias], @@ -81,10 +81,10 @@ export function parseObjectTree( type: prop.type, prop, index: 0, - children: parseObjectTree(model, prop.type.entity, schema, f) + children: parseObjectTree(model, prop.type.entity, schema, f), }) break - case "lookup": + case 'lookup': requests.push({ field: f.name, aliases: [f.alias], @@ -92,10 +92,10 @@ export function parseObjectTree( type: prop.type, prop, index: 0, - children: parseObjectTree(model, prop.type.entity, schema, f) + children: parseObjectTree(model, prop.type.entity, schema, f), }) break - case "list-lookup": + case 'list-lookup': requests.push({ field: f.name, aliases: [f.alias], @@ -104,7 +104,7 @@ export function parseObjectTree( prop, index: 0, args: parseSqlArguments(model, prop.type.entity, f.args), - children: parseObjectTree(model, prop.type.entity, schema, f) + children: parseObjectTree(model, prop.type.entity, schema, f), }) break default: @@ -115,7 +115,6 @@ export function parseObjectTree( return requests } - export function parseSqlArguments(model: Model, typeName: string, gqlArgs: any): SqlArguments { let args: SqlArguments = {} @@ -125,25 +124,22 @@ export function parseSqlArguments(model: Model, typeName: string, gqlArgs: any): } if (gqlArgs.orderBy) { - args.orderBy = { - [gqlArgs.orderBy]: gqlArgs.orderDirection === 'asc' ? 'ASC' : 'DESC' - } + args.orderBy = parseOrderBy(model, typeName, {orderBy: gqlArgs.orderBy, direction: gqlArgs.orderDirection}) } if (gqlArgs.skip) { - assert(typeof gqlArgs.skip == "number") + assert(typeof gqlArgs.skip == 'number') args.offset = gqlArgs.skip } if (gqlArgs.first != null) { - assert(typeof gqlArgs.first == "number") + assert(typeof gqlArgs.first == 'number') args.limit = gqlArgs.first } return args } - export function parseQueryableTree( model: Model, queryableName: string, @@ -157,13 +153,7 @@ export function parseQueryableTree( return fields } - -export function parseAnyTree( - model: Model, - typeName: string, - schema: GraphQLSchema, - tree: ResolveTree -): AnyFields { +export function parseAnyTree(model: Model, typeName: string, schema: GraphQLSchema, tree: ResolveTree): AnyFields { if (model[typeName].kind == 'interface') { return parseQueryableTree(model, typeName, schema, tree) } else { diff --git a/graphql/openreader/src/dialect/thegraph/where.ts b/graphql/openreader/src/dialect/thegraph/where.ts index 491457803..2fabc10ee 100644 --- a/graphql/openreader/src/dialect/thegraph/where.ts +++ b/graphql/openreader/src/dialect/thegraph/where.ts @@ -2,7 +2,7 @@ import {unexpectedCase} from '@subsquid/util-internal' import assert from 'assert' import {Where} from '../../ir/args' import {ensureArray} from '../../util/util' -import {parseWhereKey, toCondition} from '../common' +import {toCondition} from '../common' export function parseWhere(whereArg?: any): Where | undefined { if (whereArg == null) return undefined @@ -34,20 +34,47 @@ export function parseWhere(whereArg?: any): Where | undefined { case 'not_in': conj.push({op, field, values: ensureArray(arg)}) break - case 'eq': - case 'not_eq': + case 'not': + conj.push({op: 'not_eq', field, value: arg}) + break case 'gt': case 'gte': case 'lt': case 'lte': case 'contains': case 'not_contains': - case 'containsInsensitive': - case 'not_containsInsensitive': - case 'startsWith': - case 'not_startsWith': - case 'endsWith': - case 'not_endsWith': + conj.push({op, field, value: arg}) + break + case 'contains_nocase': + conj.push({op: 'containsInsensitive', field, value: arg}) + break + case 'not_contains_nocase': + conj.push({op: 'not_containsInsensitive', field, value: arg}) + break + case 'starts_with': + conj.push({op: 'startsWith', field, value: arg}) + break + case 'starts_with_nocase': + conj.push({op: 'startsWithInsensitive', field, value: arg}) + break + case 'not_starts_with': + conj.push({op: 'not_startsWith', field, value: arg}) + break + case 'not_starts_with_nocase': + conj.push({op: 'not_startsWithInsensitive', field, value: arg}) + break + case 'ends_with': + conj.push({op: 'endsWith', field, value: arg}) + break + case 'ends_with_nocase': + conj.push({op: 'endsWithInsensitive', field, value: arg}) + break + case 'not_ends_with': + conj.push({op: 'not_endsWith', field, value: arg}) + break + case 'not_ends_with_nocase': + conj.push({op: 'not_endsWithInsensitive', field, value: arg}) + break case 'jsonHasKey': case 'jsonContains': conj.push({op, field, value: arg}) @@ -92,3 +119,47 @@ export function parseWhere(whereArg?: any): Where | undefined { return conjunction } } + +export function parseWhereKey(key: string): {op: (typeof WHERE_OPS)[number] | 'REF'; field: string} { + let m = WHERE_KEY_REGEX.exec(key) + if (m) { + return {op: m[2] as (typeof WHERE_OPS)[number], field: m[1]} + } else { + return {op: 'REF', field: key} + } +} + +const WHERE_OPS = [ + 'not', + 'gt', + 'gte', + 'lt', + 'lte', + 'in', + 'not_in', + 'contains', + 'contains_nocase', + 'not_contains', + 'not_contains_nocase', + 'starts_with', + 'starts_with_nocase', + 'not_starts_with', + 'not_starts_with_nocase', + 'ends_with', + 'ends_with_nocase', + 'not_ends_with', + 'not_ends_with_nocase', + 'containsAll', + 'containsAny', + 'containsNone', + 'jsonContains', + 'jsonHasKey', + 'isNull', + 'some', + 'every', + 'none', +] as const + +const WHERE_KEY_REGEX = (() => { + return new RegExp(`^([^_]*)_(${WHERE_OPS.join('|')})$`) +})() diff --git a/graphql/openreader/src/ir/args.ts b/graphql/openreader/src/ir/args.ts index ec72a75c8..0dc844b37 100644 --- a/graphql/openreader/src/ir/args.ts +++ b/graphql/openreader/src/ir/args.ts @@ -66,7 +66,9 @@ export type BinaryOp = 'contains' | 'not_contains' | 'containsInsensitive' | 'not_containsInsensitive' | 'startsWith' | 'not_startsWith' | + 'startsWithInsensitive' | 'not_startsWithInsensitive' | 'endsWith' | 'not_endsWith' | + 'endsWithInsensitive' | 'not_endsWithInsensitive' | 'containsAll' | 'containsAny' | 'containsNone' | diff --git a/graphql/openreader/src/sql/printer.ts b/graphql/openreader/src/sql/printer.ts index da414fe83..0b7dc43e8 100644 --- a/graphql/openreader/src/sql/printer.ts +++ b/graphql/openreader/src/sql/printer.ts @@ -92,6 +92,10 @@ export class EntitySqlPrinter { } private populateWhere(cursor: Cursor, where: Where, exps: string[]): void { + function printStr(value: string) { + return !!where.op?.endsWith?.('Insensitive') ? `lower(${value})` : value + } + switch(where.op) { case "AND": for (let cond of where.args) { @@ -178,47 +182,46 @@ export class EntitySqlPrinter { break } case "startsWith": + case "startsWithInsensitive": if (this.dialect == "cockroach") { let f = cursor.native(where.field) let p = this.param(where.value) + "::text" - exps.push(`${f} >= ${p}`) - exps.push(`left(${f}, length(${p})) = ${p}`) + exps.push(`${printStr(f)} >= ${printStr(p)}`) + exps.push(`left(${printStr(f)}, length(${p})) = ${printStr(p)}`) } else { - exps.push(`starts_with(${cursor.native(where.field)}, ${this.param(where.value)})`) + exps.push(`starts_with(${printStr(cursor.native(where.field))}, ${printStr(this.param(where.value))})`) } break case "not_startsWith": + case "not_startsWithInsensitive": if (this.dialect == "cockroach") { let f = cursor.native(where.field) let p = this.param(where.value) + "::text" - exps.push(`(${f} < ${p} OR left(${f}, length(${p})) != ${p})`) + exps.push(`(${printStr(f)} < ${printStr(p)} OR left(${printStr(f)}, length(${printStr(p)})) != ${printStr(p)})`) } else { - exps.push(`NOT starts_with(${cursor.native(where.field)}, ${this.param(where.value)})`) + exps.push(`NOT starts_with(${printStr(cursor.native(where.field))}, ${printStr(this.param(where.value))})`) } break - case "endsWith": { + case "endsWith": + case "not_startsWithInsensitive": { let f = cursor.native(where.field) let p = this.param(where.value) + "::text" - exps.push(`right(${f}, length(${p})) = ${p}`) + exps.push(`right(${printStr(f)}, length(${p})) = ${printStr(p)}`) break } case "not_endsWith": { let f = cursor.native(where.field) let p = this.param(where.value) + "::text" - exps.push(`right(${f}, length(${p})) != ${p}`) + exps.push(`right(${printStr(f)}, length(${p})) != ${printStr(p)}`) break } case "contains": - exps.push(`position(${this.param(where.value)} in ${cursor.native(where.field)}) > 0`) - break - case "not_contains": - exps.push(`position(${this.param(where.value)} in ${cursor.native(where.field)}) = 0`) - break case "containsInsensitive": - exps.push(`position(lower(${this.param(where.value)}) in lower(${cursor.native(where.field)})) > 0`) + exps.push(`position(${printStr(this.param(where.value))} in ${printStr(cursor.native(where.field))}) > 0`) break + case "not_contains": case "not_containsInsensitive": - exps.push(`position(lower(${this.param(where.value)}) in lower(${cursor.native(where.field)})) = 0`) + exps.push(`position(${printStr(this.param(where.value))} in ${printStr(cursor.native(where.field))}) = 0`) break case "every": { let rel = cursor.prop(where.field) @@ -446,3 +449,4 @@ export class QueryableSqlPrinter { return sql } } + diff --git a/graphql/openreader/src/test/basic.test.ts b/graphql/openreader/src/test/basic.test.ts index 124469857..de376b8f1 100644 --- a/graphql/openreader/src/test/basic.test.ts +++ b/graphql/openreader/src/test/basic.test.ts @@ -1,3 +1,4 @@ +import {Dialect} from '../dialect' import {useDatabase, useServer} from "./setup" @@ -16,324 +17,630 @@ describe('basic tests', function() { `insert into historical_balance (id, account_id, balance) values ('3-1', '3', 300)`, ]) - const client = useServer(` - interface HasBalance { - balance: Int! - } - - type Account implements HasBalance @entity { - id: ID! - wallet: String! - balance: Int! - history: [HistoricalBalance!] @derivedFrom(field: "account") - } - - "Historical record of account balance" - type HistoricalBalance implements HasBalance @entity { - "Unique identifier" - id: ID! - - "Related account" - account: Account! - - "Balance" - balance: Int! - } - `) - - it('can fetch all accounts', function() { - return client.test( - `query { - accounts(orderBy: id_ASC) { - id - wallet - balance - history(orderBy: id_ASC) { balance } - } - }`, - { - accounts: [ - {id: '1', wallet: 'a', balance: 100, history: [{balance: 20}, {balance: 80}]}, - {id: '2', wallet: 'b', balance: 200, history: [{balance: 50}, {balance: 90}, {balance: 60}]}, - {id: '3', wallet: 'c', balance: 300, history: [{balance: 300}]}, - ] + describe('opencrud', function() { + const client = useServer(` + interface HasBalance { + balance: Int! } - ) - }) - - it('supports filtering by id', function () { - return client.test( - `query { - accounts(where: {id_eq: "3"}) { - id - wallet - } - }`, - { - accounts: [{id: '3', wallet: 'c'}] + + type Account implements HasBalance @entity { + id: ID! + wallet: String! + balance: Int! + history: [HistoricalBalance!] @derivedFrom(field: "account") } - ) - }) - - it('supports by id query', function () { - return client.test( - `query { - a3: accountById(id: "3") { - id - wallet + + "Historical record of account balance" + type HistoricalBalance implements HasBalance @entity { + "Unique identifier" + id: ID! + + "Related account" + account: Account! + + "Balance" + balance: Int! + } + `) + + it('can fetch all accounts', function() { + return client.test( + `query { + accounts(orderBy: id_ASC) { + id + wallet + balance + history(orderBy: id_ASC) { balance } + } + }`, + { + accounts: [ + {id: '1', wallet: 'a', balance: 100, history: [{balance: 20}, {balance: 80}]}, + {id: '2', wallet: 'b', balance: 200, history: [{balance: 50}, {balance: 90}, {balance: 60}]}, + {id: '3', wallet: 'c', balance: 300, history: [{balance: 300}]}, + ] } - nonexistent: accountById(id: "foo") { - id - wallet + ) + }) + + it('supports filtering by id', function () { + return client.test( + `query { + accounts(where: {id_eq: "3"}) { + id + wallet + } + }`, + { + accounts: [{id: '3', wallet: 'c'}] } - }`, - { - a3: {id: '3', wallet: 'c'}, - nonexistent: null - } - ) - }) - - it('supports by unique input query', function () { - return client.test( - `query { - a2: accountByUniqueInput(where: {id: "2"}) { - id - wallet + ) + }) + + it('supports by id query', function () { + return client.test( + `query { + a3: accountById(id: "3") { + id + wallet + } + nonexistent: accountById(id: "foo") { + id + wallet + } + }`, + { + a3: {id: '3', wallet: 'c'}, + nonexistent: null } - nonexistent: accountByUniqueInput(where: {id: "foo"}) { - id - wallet + ) + }) + + it('supports by unique input query', function () { + return client.test( + `query { + a2: accountByUniqueInput(where: {id: "2"}) { + id + wallet + } + nonexistent: accountByUniqueInput(where: {id: "foo"}) { + id + wallet + } + }`, + { + a2: {id: '2', wallet: 'b'}, + nonexistent: null } - }`, - { - a2: {id: '2', wallet: 'b'}, - nonexistent: null - } - ) - }) - - it('can fetch deep relations', function () { - return client.test( - `query { - accounts(where: {id_eq: "3"}) { - id - history { + ) + }) + + it('can fetch deep relations', function () { + return client.test( + `query { + accounts(where: {id_eq: "3"}) { id - account { - wallet - history { - balance - account { - id + history { + id + account { + wallet + history { + balance + account { + id + } } } } } + }`, + { + accounts: [{ + id: '3', + history: [{ + id: '3-1', + account: { + wallet: 'c', + history: [{ + balance: 300, + account: { + id: '3' + } + }] + } + }] + }] + } + ) + }) + + it('supports *_some filter', function () { + return client.test( + `query { + accounts(where: {history_some: {balance_lt: 50}}) { + id + } + }`, + { + accounts: [{id: '1'}] } - }`, - { - accounts: [{ - id: '3', - history: [{ - id: '3-1', - account: { - wallet: 'c', - history: [{ - balance: 300, - account: { - id: '3' - } - }] + ) + }) + + it('supports *_every filter', function () { + return client.test( + `query { + accounts(where: {history_every: {balance_gt: 20}}) { + wallet + } + }`, + { + accounts: [{wallet: 'b'}, {wallet: 'c'}] + } + ) + }) + + it('supports *_none filter', function () { + return client.test( + `query { + accounts(where: {history_none: {balance_lt: 60}}) { + wallet + } + }`, + { + accounts: [{wallet: 'c'}] + } + ) + }) + + it('supports gql aliases', function () { + return client.test( + `query { + accounts(where: {id_eq: "1"}) { + balance + bag: wallet + purse: wallet + payment1: history(where: {id_eq: "1-1"}) { + balance + } + payment2: history(where: {id_eq: "1-2"}) { + balance } + } + }`, + { + accounts: [{ + balance: 100, + bag: 'a', + purse: 'a', + payment1: [{balance: 20}], + payment2: [{balance: 80}] }] - }] - } - ) - }) - - it('supports *_some filter', function () { - return client.test( - `query { - accounts(where: {history_some: {balance_lt: 50}}) { - id } - }`, - { - accounts: [{id: '1'}] - } - ) - }) - - it('supports *_every filter', function () { - return client.test( - `query { - accounts(where: {history_every: {balance_gt: 20}}) { - wallet + ) + }) + + it('supports gql fragments', function () { + return client.test( + `query { + accounts(where: {id_eq: "1"}) { + ...accountFields + history { + ...historicalBalance + } + } } - }`, - { - accounts: [{wallet: 'b'}, {wallet: 'c'}] - } - ) - }) - - it('supports *_none filter', function () { - return client.test( - `query { - accounts(where: {history_none: {balance_lt: 60}}) { + + fragment accountFields on Account { + id wallet } - }`, - { - accounts: [{wallet: 'c'}] - } - ) - }) - - it('supports gql aliases', function () { - return client.test( - `query { - accounts(where: {id_eq: "1"}) { + + fragment historicalBalance on HistoricalBalance { balance - bag: wallet - purse: wallet - payment1: history(where: {id_eq: "1-1"}) { + }`, + { + accounts: [{ + id: '1', + wallet: 'a', + history: [{balance: 20}, {balance: 80}] + }] + } + ) + }) + + it('supports gql fragments on interfaces', function () { + return client.test( + `query { + accounts(where: {id_eq: "1"}) { + ...balance + history { + ...balance + } + } + } + + fragment balance on HasBalance { + ... on Account { + accountBalance: balance + } + ... on HistoricalBalance { + payment: balance + } + }`, + { + accounts: [{ + accountBalance: 100, + history: [{payment: 20}, {payment: 80}] + }] + } + ) + }) + + it('supports sorting', function () { + return client.test( + `query { + historicalBalances(orderBy: balance_ASC) { balance } - payment2: history(where: {id_eq: "1-2"}) { + }`, + { + historicalBalances: [ + {balance: 20}, + {balance: 50}, + {balance: 60}, + {balance: 80}, + {balance: 90}, + {balance: 300} + ] + } + ) + }) + + it('supports sorting by referenced entity field', function () { + return client.test( + `query { + historicalBalances(orderBy: [account_wallet_ASC, balance_DESC]) { balance } + }`, + { + historicalBalances: [ + {balance: 80}, + {balance: 20}, + {balance: 90}, + {balance: 60}, + {balance: 50}, + {balance: 300} + ] } - }`, - { - accounts: [{ - balance: 100, - bag: 'a', - purse: 'a', - payment1: [{balance: 20}], - payment2: [{balance: 80}] - }] - } - ) - }) - - it('supports gql fragments', function () { - return client.test( - `query { - accounts(where: {id_eq: "1"}) { - ...accountFields - history { - ...historicalBalance + ) + }) + + it('supports descriptions', function () { + return client.test(` + query { + HistoricalBalance: __type(name: "HistoricalBalance") { + description + fields { + description + } } } + `, { + HistoricalBalance: { + description: 'Historical record of account balance', + fields: [ + {description: 'Unique identifier'}, + {description: 'Related account'}, + {description: 'Balance'}, + ] + } + }) + }) + }) + + describe('thegraph', function() { + const client = useServer(` + interface HasBalance { + balance: Int! } - - fragment accountFields on Account { - id - wallet + + type Account implements HasBalance @entity { + id: ID! + wallet: String! + balance: Int! + history: [HistoricalBalance!] @derivedFrom(field: "account") } - - fragment historicalBalance on HistoricalBalance { - balance - }`, - { - accounts: [{ - id: '1', - wallet: 'a', - history: [{balance: 20}, {balance: 80}] - }] + + "Historical record of account balance" + type HistoricalBalance implements HasBalance @entity { + "Unique identifier" + id: ID! + + "Related account" + account: Account! + + "Balance" + balance: Int! } - ) - }) - - it('supports gql fragments on interfaces', function () { - return client.test( - `query { - accounts(where: {id_eq: "1"}) { - ...balance - history { - ...balance + `, {dialect: Dialect.TheGraph}) + + it('can fetch all accounts', function() { + return client.test( + `query { + accounts(orderBy: id, orderDirection: asc) { + id + wallet + balance + history(orderBy: id, orderDirection: asc) { balance } } + }`, + { + accounts: [ + {id: '1', wallet: 'a', balance: 100, history: [{balance: 20}, {balance: 80}]}, + {id: '2', wallet: 'b', balance: 200, history: [{balance: 50}, {balance: 90}, {balance: 60}]}, + {id: '3', wallet: 'c', balance: 300, history: [{balance: 300}]}, + ] } - } - - fragment balance on HasBalance { - ... on Account { - accountBalance: balance - } - ... on HistoricalBalance { - payment: balance - } - }`, - { - accounts: [{ - accountBalance: 100, - history: [{payment: 20}, {payment: 80}] - }] - } - ) - }) - - it('supports sorting', function () { - return client.test( - `query { - historicalBalances(orderBy: balance_ASC) { - balance + ) + }) + + it('supports filtering by id', function () { + return client.test( + `query { + accounts(where: {id: "3"}) { + id + wallet + } + }`, + { + accounts: [{id: '3', wallet: 'c'}] } - }`, - { - historicalBalances: [ - {balance: 20}, - {balance: 50}, - {balance: 60}, - {balance: 80}, - {balance: 90}, - {balance: 300} - ] - } - ) - }) - - it('supports sorting by referenced entity field', function () { - return client.test( - `query { - historicalBalances(orderBy: [account_wallet_ASC, balance_DESC]) { + ) + }) + + it('supports by id query', function () { + return client.test( + `query { + a3: account(id: "3") { + id + wallet + } + nonexistent: account(id: "foo") { + id + wallet + } + }`, + { + a3: {id: '3', wallet: 'c'}, + nonexistent: null + } + ) + }) + + it('can fetch deep relations', function () { + return client.test( + `query { + accounts(where: {id: "3"}) { + id + history { + id + account { + wallet + history { + balance + account { + id + } + } + } + } + } + }`, + { + accounts: [{ + id: '3', + history: [{ + id: '3-1', + account: { + wallet: 'c', + history: [{ + balance: 300, + account: { + id: '3' + } + }] + } + }] + }] + } + ) + }) + + it('supports *_some filter', function () { + return client.test( + `query { + accounts(where: {history_some: {balance_lt: 50}}) { + id + } + }`, + { + accounts: [{id: '1'}] + } + ) + }) + + it('supports *_every filter', function () { + return client.test( + `query { + accounts(where: {history_every: {balance_gt: 20}}) { + wallet + } + }`, + { + accounts: [{wallet: 'b'}, {wallet: 'c'}] + } + ) + }) + + it('supports *_none filter', function () { + return client.test( + `query { + accounts(where: {history_none: {balance_lt: 60}}) { + wallet + } + }`, + { + accounts: [{wallet: 'c'}] + } + ) + }) + + it('supports gql aliases', function () { + return client.test( + `query { + accounts(where: {id: "1"}) { + balance + bag: wallet + purse: wallet + payment1: history(where: {id: "1-1"}) { + balance + } + payment2: history(where: {id: "1-2"}) { + balance + } + } + }`, + { + accounts: [{ + balance: 100, + bag: 'a', + purse: 'a', + payment1: [{balance: 20}], + payment2: [{balance: 80}] + }] + } + ) + }) + + it('supports gql fragments', function () { + return client.test( + `query { + accounts(where: {id: "1"}) { + ...accountFields + history { + ...historicalBalance + } + } + } + + fragment accountFields on Account { + id + wallet + } + + fragment historicalBalance on HistoricalBalance { balance + }`, + { + accounts: [{ + id: '1', + wallet: 'a', + history: [{balance: 20}, {balance: 80}] + }] } - }`, - { - historicalBalances: [ - {balance: 80}, - {balance: 20}, - {balance: 90}, - {balance: 60}, - {balance: 50}, - {balance: 300} - ] - } - ) - }) - - it('supports descriptions', function () { - return client.test(` - query { - HistoricalBalance: __type(name: "HistoricalBalance") { - description - fields { + ) + }) + + it('supports gql fragments on interfaces', function () { + return client.test( + `query { + accounts(where: {id: "1"}) { + ...balance + history { + ...balance + } + } + } + + fragment balance on HasBalance { + ... on Account { + accountBalance: balance + } + ... on HistoricalBalance { + payment: balance + } + }`, + { + accounts: [{ + accountBalance: 100, + history: [{payment: 20}, {payment: 80}] + }] + } + ) + }) + + it('supports sorting', function () { + return client.test( + `query { + historicalBalances(orderBy: balance, orderDirection: asc) { + balance + } + }`, + { + historicalBalances: [ + {balance: 20}, + {balance: 50}, + {balance: 60}, + {balance: 80}, + {balance: 90}, + {balance: 300} + ] + } + ) + }) + + it('supports sorting by referenced entity field', function () { + return client.test( + `query { + historicalBalances(orderBy: account__wallet, orderDirection: asc) { + balance + } + }`, + { + historicalBalances: [ + {balance: 20}, + {balance: 80}, + {balance: 50}, + {balance: 90}, + {balance: 60}, + {balance: 300} + ] + } + ) + }) + + it('supports descriptions', function () { + return client.test(` + query { + HistoricalBalance: __type(name: "HistoricalBalance") { description + fields { + description + } } } - } - `, { - HistoricalBalance: { - description: 'Historical record of account balance', - fields: [ - {description: 'Unique identifier'}, - {description: 'Related account'}, - {description: 'Balance'}, - ] - } + `, { + HistoricalBalance: { + description: 'Historical record of account balance', + fields: [ + {description: 'Unique identifier'}, + {description: 'Related account'}, + {description: 'Balance'}, + ] + } + }) }) }) }) diff --git a/graphql/openreader/src/test/lookup.test.ts b/graphql/openreader/src/test/lookup.test.ts index c6539e51e..c9eaffaa5 100644 --- a/graphql/openreader/src/test/lookup.test.ts +++ b/graphql/openreader/src/test/lookup.test.ts @@ -1,3 +1,4 @@ +import {Dialect} from '../dialect' import {isCockroach, useDatabase, useServer} from "./setup" describe('lookup test', function () { @@ -13,105 +14,211 @@ describe('lookup test', function () { `insert into issue_cancellation (id, issue_id, height) values ('3', '3', 10)`, ]) - const client = useServer(` - type Issue @entity { - id: ID! - payment: IssuePayment @derivedFrom(field: "issue") - cancellation: IssueCancellation @derivedFrom(field: "issue") - } - - type IssuePayment @entity { - id: ID! - issue: Issue! @unique - amount: Int! - } - - type IssueCancellation @entity { - id: ID! - issue: Issue! @unique - height: Int! - } - `) - - it('fetches correctly', function () { - return client.test(` - query { - issues(orderBy: [id_ASC]) { - id - payment { - amount - } - cancellation { - height - issue { - cancellation { - id + describe('opencrud', function () { + const client = useServer(` + type Issue @entity { + id: ID! + payment: IssuePayment @derivedFrom(field: "issue") + cancellation: IssueCancellation @derivedFrom(field: "issue") + } + + type IssuePayment @entity { + id: ID! + issue: Issue! @unique + amount: Int! + } + + type IssueCancellation @entity { + id: ID! + issue: Issue! @unique + height: Int! + } + `) + + it('fetches correctly', function () { + return client.test(` + query { + issues(orderBy: [id_ASC]) { + id + payment { + amount + } + cancellation { + height + issue { + cancellation { + id + } } } } } - } - `, { - issues: [ - { - id: '1', - payment: {amount: 2}, - cancellation: null - }, - { - id: '2', - payment: {amount: 1}, - cancellation: null - }, - { - id: '3', - payment: null, - cancellation: { - height: 10, - issue: { - cancellation: { - id: '3' + `, { + issues: [ + { + id: '1', + payment: {amount: 2}, + cancellation: null + }, + { + id: '2', + payment: {amount: 1}, + cancellation: null + }, + { + id: '3', + payment: null, + cancellation: { + height: 10, + issue: { + cancellation: { + id: '3' + } } } } + ] + }) + }) + + it('supports sorting on lookup fields', function () { + return client.test(` + query { + issues(orderBy: [payment_amount_ASC]) { + id + } } - ] + `, { + issues: isCockroach() + ? [ + {id: '3'}, + {id: '2'}, + {id: '1'} + ] + : [ + {id: '2'}, + {id: '1'}, + {id: '3'} + ] + }) }) - }) - - it('supports sorting on lookup fields', function () { - return client.test(` - query { - issues(orderBy: [payment_amount_ASC]) { - id + + it('supports where conditions', function () { + return client.test(` + query { + issues(where: {payment: {amount_gt: 1}}) { + id + } } - } - `, { - issues: isCockroach() - ? [ - {id: '3'}, - {id: '2'}, + `, { + issues: [ {id: '1'} ] - : [ - {id: '2'}, - {id: '1'}, - {id: '3'} - ] + }) }) }) - it('supports where conditions', function () { - return client.test(` - query { - issues(where: {payment: {amount_gt: 1}}) { - id - } + describe('thegraph', function () { + const client = useServer(` + type Issue @entity { + id: ID! + payment: IssuePayment @derivedFrom(field: "issue") + cancellation: IssueCancellation @derivedFrom(field: "issue") + } + + type IssuePayment @entity { + id: ID! + issue: Issue! @unique + amount: Int! + } + + type IssueCancellation @entity { + id: ID! + issue: Issue! @unique + height: Int! } - `, { - issues: [ - {id: '1'} - ] + `, {dialect: Dialect.TheGraph}) + + it('fetches correctly', function () { + return client.test(` + query { + issues(orderBy: id, orderDirection: asc) { + id + payment { + amount + } + cancellation { + height + issue { + cancellation { + id + } + } + } + } + } + `, { + issues: [ + { + id: '1', + payment: {amount: 2}, + cancellation: null + }, + { + id: '2', + payment: {amount: 1}, + cancellation: null + }, + { + id: '3', + payment: null, + cancellation: { + height: 10, + issue: { + cancellation: { + id: '3' + } + } + } + } + ] + }) + }) + + it('supports sorting on lookup fields', function () { + return client.test(` + query { + issues(orderBy: payment__amount, orderDirection: asc) { + id + } + } + `, { + issues: isCockroach() + ? [ + {id: '3'}, + {id: '2'}, + {id: '1'} + ] + : [ + {id: '2'}, + {id: '1'}, + {id: '3'} + ] + }) + }) + + it('supports where conditions', function () { + return client.test(` + query { + issues(where: {payment: {amount_gt: 1}}) { + id + } + } + `, { + issues: [ + {id: '1'} + ] + }) }) }) }) diff --git a/graphql/openreader/src/test/setup.ts b/graphql/openreader/src/test/setup.ts index ecac4490a..6f0d8449d 100644 --- a/graphql/openreader/src/test/setup.ts +++ b/graphql/openreader/src/test/setup.ts @@ -74,7 +74,7 @@ export function useServer(schema: string, options?: Partial): Cli subscriptionPollInterval: 500, maxRootFields: 10, // log: createLogger('sqd:openreader'), - ...options + ...options, }) client.endpoint = `http://localhost:${info.port}/graphql` }) diff --git a/graphql/openreader/src/test/where.test.ts b/graphql/openreader/src/test/where.test.ts index adb21185f..db58a863f 100644 --- a/graphql/openreader/src/test/where.test.ts +++ b/graphql/openreader/src/test/where.test.ts @@ -1,3 +1,4 @@ +import {Dialect} from '../dialect' import {useDatabase, useServer} from "./setup" describe('AND, OR on entity filters', function () { @@ -11,125 +12,251 @@ describe('AND, OR on entity filters', function () { `insert into item (id, a, b) values ('6', 5, 6)`, ]) - const client = useServer(` - type Item @entity { - id: ID! - a: Int - b: Int - } - `) - - it('{c, and: {c}}', function () { - return client.test(` - query { - items(where: {a_eq: 1, AND: {b_eq: 1}} orderBy: id_ASC) { id } + describe('opencrud', function() { + const client = useServer(` + type Item @entity { + id: ID! + a: Int + b: Int } - `, { - items: [ - {id: '1'} - ] + `) + + it('{c, and: {c}}', function () { + return client.test(` + query { + items(where: {a_eq: 1, AND: {b_eq: 1}} orderBy: id_ASC) { id } + } + `, { + items: [ + {id: '1'} + ] + }) }) - }) - - it('{and: {and: {c}, c}}', function () { - return client.test(` - query { - items(where: {AND: {b_eq: 2, AND: {a_eq: 3}}} orderBy: id_ASC) { id } - } - `, { - items: [ - {id: '3'} - ] + + it('{and: {and: {c}, c}}', function () { + return client.test(` + query { + items(where: {AND: {b_eq: 2, AND: {a_eq: 3}}} orderBy: id_ASC) { id } + } + `, { + items: [ + {id: '3'} + ] + }) }) - }) - - it('{and: [{c}, {c}]}', function () { - return client.test(` - query { - items(where: {AND: [{a_eq: 2}, {b_eq: 2}]} orderBy: id_ASC) { id } - } - `, { - items: [ - {id: '2'} - ] + + it('{and: [{c}, {c}]}', function () { + return client.test(` + query { + items(where: {AND: [{a_eq: 2}, {b_eq: 2}]} orderBy: id_ASC) { id } + } + `, { + items: [ + {id: '2'} + ] + }) }) - }) - - it('{c, {or: {c}}}', function () { - return client.test(` - query { - items(where: {a_eq: 1, OR: {a_eq: 2}} orderBy: id_ASC) { id } - } - `, { - items: [ - {id: '1'}, - {id: '2'} - ] + + it('{c, {or: {c}}}', function () { + return client.test(` + query { + items(where: {a_eq: 1, OR: {a_eq: 2}} orderBy: id_ASC) { id } + } + `, { + items: [ + {id: '1'}, + {id: '2'} + ] + }) }) - }) - - it('{or: [{c}, {c}]}', function () { - return client.test(` - query { - items(where: {OR: [{a_eq: 2}, {a_eq: 3}]} orderBy: id_ASC) { id } - } - `, { - items: [ - {id: '2'}, - {id: '3'} - ] + + it('{or: [{c}, {c}]}', function () { + return client.test(` + query { + items(where: {OR: [{a_eq: 2}, {a_eq: 3}]} orderBy: id_ASC) { id } + } + `, { + items: [ + {id: '2'}, + {id: '3'} + ] + }) }) - }) - - it('{or: {or: {c}, c}}', function () { - return client.test(` - query { - items(where: {OR: {a_eq: 1, OR: {b_eq: 2}}} orderBy: id_ASC) { id } - } - `, { - items: [ - {id: '1'}, - {id: '2'}, - {id: '3'} - ] + + it('{or: {or: {c}, c}}', function () { + return client.test(` + query { + items(where: {OR: {a_eq: 1, OR: {b_eq: 2}}} orderBy: id_ASC) { id } + } + `, { + items: [ + {id: '1'}, + {id: '2'}, + {id: '3'} + ] + }) }) - }) - - it('{and: [{or: {c}, c}, {or: {c}, c}]}', function () { - return client.test(` - query { - items(where: {AND: [{OR: {a_eq: 5}, a_eq: 4}, {OR: {b_eq: 2}, b_eq: 4}]} orderBy: id_ASC) { id } - } - `, { - items: [ - {id: '4'}, - {id: '5'} - ] + + it('{and: [{or: {c}, c}, {or: {c}, c}]}', function () { + return client.test(` + query { + items(where: {AND: [{OR: {a_eq: 5}, a_eq: 4}, {OR: {b_eq: 2}, b_eq: 4}]} orderBy: id_ASC) { id } + } + `, { + items: [ + {id: '4'}, + {id: '5'} + ] + }) }) - }) - - it('{c, and: {c}, or: {c}}', function () { - return client.test(` - query { - items(where: { a_eq: 4, AND: {b_eq: 4}, OR: {b_eq: 6} } orderBy: id_ASC) { id } - } - `, { - items: [ - {id: '4'}, - {id: '6'} - ] + + it('{c, and: {c}, or: {c}}', function () { + return client.test(` + query { + items(where: { a_eq: 4, AND: {b_eq: 4}, OR: {b_eq: 6} } orderBy: id_ASC) { id } + } + `, { + items: [ + {id: '4'}, + {id: '6'} + ] + }) + }) + + it('handles empty wheres', function () { + return client.test(` + query { + items(where: { a_eq: 4, AND: { OR: {}, AND: {} }, OR: { OR: {AND: {} } } } orderBy: id_ASC) { id } + } + `, { + items: [ + {id: '4'} + ] + }) }) }) - - it('handles empty wheres', function () { - return client.test(` - query { - items(where: { a_eq: 4, AND: { OR: {}, AND: {} }, OR: { OR: {AND: {} } } } orderBy: id_ASC) { id } + + describe('thegraph', function() { + const client = useServer(` + type Item @entity { + id: ID! + a: Int + b: Int } - `, { - items: [ - {id: '4'} - ] + `, {dialect: Dialect.TheGraph}) + + it('{c, and: {c}}', function () { + return client.test(` + query { + items(where: {a: 1, and: {b: 1}}, orderBy: id, orderDirection: asc) { id } + } + `, { + items: [ + {id: '1'} + ] + }) + }) + + it('{and: {and: {c}, c}}', function () { + return client.test(` + query { + items(where: {and: {b: 2, and: {a: 3}}}, orderBy: id, orderDirection: asc) { id } + } + `, { + items: [ + {id: '3'} + ] + }) + }) + + it('{and: [{c}, {c}]}', function () { + return client.test(` + query { + items(where: {and: [{a: 2}, {b: 2}]}, orderBy: id, orderDirection: asc) { id } + } + `, { + items: [ + {id: '2'} + ] + }) + }) + + it('{c, {or: {c}}}', function () { + return client.test(` + query { + items(where: {a: 1, or: {a: 2}}, orderBy: id, orderDirection: asc) { id } + } + `, { + items: [ + {id: '1'}, + {id: '2'} + ] + }) + }) + + it('{or: [{c}, {c}]}', function () { + return client.test(` + query { + items(where: {or: [{a: 2}, {a: 3}]}, orderBy: id, orderDirection: asc) { id } + } + `, { + items: [ + {id: '2'}, + {id: '3'} + ] + }) + }) + + it('{or: {or: {c}, c}}', function () { + return client.test(` + query { + items(where: {or: {a: 1, or: {b: 2}}}, orderBy: id, orderDirection: asc) { id } + } + `, { + items: [ + {id: '1'}, + {id: '2'}, + {id: '3'} + ] + }) + }) + + it('{and: [{or: {c}, c}, {or: {c}, c}]}', function () { + return client.test(` + query { + items(where: {and: [{or: {a: 5}, a: 4}, {or: {b: 2}, b: 4}]}, orderBy: id, orderDirection: asc) { id } + } + `, { + items: [ + {id: '4'}, + {id: '5'} + ] + }) + }) + + it('{c, and: {c}, or: {c}}', function () { + return client.test(` + query { + items(where: { a: 4, and: {b: 4}, or: {b: 6} }, orderBy: id, orderDirection: asc) { id } + } + `, { + items: [ + {id: '4'}, + {id: '6'} + ] + }) + }) + + it('handles empty wheres', function () { + return client.test(` + query { + items(where: { a: 4, and: { or: {}, and: {} }, or: { or: {and: {} } } }, orderBy: id, orderDirection: asc) { id } + } + `, { + items: [ + {id: '4'} + ] + }) }) }) }) diff --git a/test/balances/schema.graphql b/test/balances/schema.graphql index 9028fa574..db8a3e853 100644 --- a/test/balances/schema.graphql +++ b/test/balances/schema.graphql @@ -1,301 +1,5 @@ -# --------------------------------------------------------- -# Counts -------------------------------------------------- -# --------------------------------------------------------- - -# thegraph doesn't support count operations, but we need them to paginate results -# This entity is a workaround to this issue, but it's still not enough, as we'd need counts for more complex queries -type Count @entity { - id: ID! - - orderTotal: Int! - orderParcel: Int! - orderEstate: Int! - orderWearable: Int! - orderENS: Int! - parcelTotal: Int! - estateTotal: Int! - wearableTotal: Int! - ensTotal: Int! - started: Int! - salesTotal: Int! - salesManaTotal: BigInt! - creatorEarningsManaTotal: BigInt! - daoEarningsManaTotal: BigInt! -} - -# --------------------------------------------------------- -# Orders -------------------------------------------------- -# --------------------------------------------------------- - -# thegraph doesn't support nested property searches, so we're doing promoting properties -# we need from each NFT type to the Order, in order to search for them, prefixing them with search_[nft]_[prop] -type Order @entity { - id: ID! - marketplaceAddress: String! - category: Category! - nft: NFT - nftAddress: String! - tokenId: BigInt! - txHash: String! - owner: String! - buyer: String - price: BigInt! - status: OrderStatus! - blockNumber: BigInt! - expiresAt: BigInt! - createdAt: BigInt! - updatedAt: BigInt! -} - -# --------------------------------------------------------- -# Bids ---------------------------------------------------- -# --------------------------------------------------------- - -type Bid @entity { - id: ID! - bidAddress: String! - category: Category! - nft: NFT - nftAddress: String! - tokenId: BigInt! - bidder: Bytes - seller: Bytes - price: BigInt! - fingerprint: Bytes - status: OrderStatus! - blockchainId: String! - blockNumber: BigInt! - expiresAt: BigInt! - createdAt: BigInt! - updatedAt: BigInt! -} - -type Wearable @entity { - id: ID! - owner: Account! - representationId: String! - collection: String! - name: String! - description: String! - category: WearableCategory! - rarity: WearableRarity! - bodyShapes: [WearableBodyShape!] - nft: NFT @derivedFrom(field: "wearable") -} - -type ENS @entity { - id: ID! - tokenId: BigInt! - owner: Account! - caller: String - beneficiary: String - labelHash: String - subdomain: String - createdAt: BigInt - nft: NFT @derivedFrom(field: "ens") -} - -type Transfer @entity { - id: ID! - nftId: ID! - network: String! @index - block: Int! @index - # timestamp: DateTime! @index - timestamp: BigInt! - from: String! @index - to: String! @index - txHash: String! @index -} - -type Data @entity { - id: ID! - parcel: Parcel - estate: Estate - version: String! - name: String - description: String - ipns: String -} - -type NFT @entity { - id: ID! - tokenId: BigInt! - contractAddress: Bytes! - category: Category! - owner: Account! - tokenURI: String - - orders: [Order!] @derivedFrom(field: "nft") # History of all orders. Should only ever be ONE open order. all others must be cancelled or sold - bids: [Bid!] @derivedFrom(field: "nft") # History of all bids. - activeOrder: Order - - name: String - image: String - - parcel: Parcel @unique - estate: Estate @unique - wearable: Wearable @unique - ens: ENS @unique - - createdAt: BigInt! - updatedAt: BigInt! - soldAt: BigInt - transferredAt: BigInt! - # analytics - sales: Int! - volume: BigInt! - - # search indexes - searchOrderStatus: OrderStatus - searchOrderPrice: BigInt - searchOrderExpiresAt: BigInt - searchOrderCreatedAt: BigInt - - searchIsLand: Boolean - - searchText: String - - searchParcelIsInBounds: Boolean - searchParcelX: BigInt - searchParcelY: BigInt - searchParcelEstateId: String - searchDistanceToPlaza: Int - searchAdjacentToRoad: Boolean - - searchEstateSize: Int - - searchIsWearableHead: Boolean - searchIsWearableAccessory: Boolean - searchWearableRarity: String # We're using String instead of WearableRarity here so we can later query this field via ()_in - searchWearableCategory: WearableCategory - searchWearableBodyShapes: [WearableBodyShape!] -} - -type Parcel @entity { - id: ID! - tokenId: BigInt! - owner: Account! - x: BigInt! - y: BigInt! - estate: Estate - data: Data - rawData: String - nft: NFT @derivedFrom(field: "parcel") -} - -type Estate @entity { - id: ID! - tokenId: BigInt! - owner: Account! - parcels: [Parcel!]! @derivedFrom(field: "estate") - parcelDistances: [Int!] - adjacentToRoadCount: Int - size: Int - data: Data - rawData: String - nft: NFT @derivedFrom(field: "estate") -} - -# --------------------------------------------------------- -# Account (user) ------------------------------------------- -# --------------------------------------------------------- - -type Account @entity { - id: ID! # ETH addr - address: String! - nfts: [NFT!] @derivedFrom(field: "owner") - # analytics - sales: Int! - purchases: Int! - spent: BigInt! - earned: BigInt! -} - -# --------------------------------------------------------- -# Enums --------------------------------------------------- -# --------------------------------------------------------- - -enum Category { - parcel - estate - wearable - ens -} - -enum OrderStatus { - open - sold - cancelled -} - -enum WearableCategory { - eyebrows - eyes - facial_hair - hair - mouth - upper_body - lower_body - feet - earring - eyewear - hat - helmet - mask - tiara - top_head - skin -} - -enum WearableRarity { - common - uncommon - rare - epic - legendary - mythic - unique - exotic -} - -enum WearableBodyShape { - BaseFemale - BaseMale -} - -# --------------------------------------------------------- -# Sales --------------------------------------------------- -# --------------------------------------------------------- - -# We only track sales from Decentraland's smart contracts - -enum SaleType { - bid - order -} - -type Sale @entity { - id: ID! - type: SaleType! - buyer: String! - seller: String! - price: BigInt! - nft: NFT! - timestamp: BigInt! - txHash: String! - - # search - searchTokenId: BigInt! - searchContractAddress: Bytes! - searchCategory: String! -} - -# Data accumulated and condensed into day stats for all of the Marketplace activity -type AnalyticsDayData @entity { - id: ID! # timestamp rounded to current day by dividing by 86400 - date: Int! - sales: Int! - volume: BigInt! - creatorsEarnings: BigInt! - daoEarnings: BigInt! -} \ No newline at end of file + type Item @entity { + id: ID! + a: Int + b: Int + } \ No newline at end of file