Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
19 changes: 17 additions & 2 deletions graphql/openreader/src/dialect/opencrud/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import {customScalars} from '../../scalars'
import {ConnectionQuery, CountQuery, EntityByIdQuery, ListQuery, Query} from '../../sql/query'
import {Limit} from '../../util/limit'
import {getResolveTree, getTreeRequest, hasTreeRequest, simplifyResolveTree} from '../../util/resolve-tree'
import {ensureArray, identity} from '../../util/util'
import {ensureArray, identity, toFkIdField} from '../../util/util'
import {getOrderByMapping, parseOrderBy} from './orderBy'
import {parseAnyTree, parseObjectTree, parseSqlArguments} from './tree'
import {parseWhere} from './where'
Expand Down Expand Up @@ -152,6 +152,12 @@ export class SchemaBuilder {
field.resolve = (source, args, context, info) => source[info.path.key]
break
}
if (prop.type.kind == 'fk') {
let idKey = toFkIdField(key)
if (!object.properties[idKey]) {
fields[idKey] = {type: this.getPropType({type: {kind: 'scalar', name: 'String'}, nullable: prop.nullable})}
}
}
}
fields[key] = field
}
Expand Down Expand Up @@ -303,7 +309,16 @@ export class SchemaBuilder {
fields[`${key}_isNull`] = {type: GraphQLBoolean}
fields[key] = {type: this.getWhere(prop.type.name)}
break
case "fk":
case "fk": {
fields[`${key}_isNull`] = {type: GraphQLBoolean}
fields[key] = {type: this.getWhere(prop.type.entity)}
this.buildPropWhereFilters(
toFkIdField(key),
{type: {kind: 'scalar', name: 'String'}, nullable: prop.nullable},
fields
)
break
}
case "lookup":
fields[`${key}_isNull`] = {type: GraphQLBoolean}
fields[key] = {type: this.getWhere(prop.type.entity)}
Expand Down
19 changes: 18 additions & 1 deletion graphql/openreader/src/dialect/opencrud/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {AnyFields, FieldRequest, FieldsByEntity, OpaqueRequest} from '../../ir/f
import {Model} from '../../model'
import {getQueryableEntities} from '../../model.tools'
import {simplifyResolveTree} from '../../util/resolve-tree'
import {ensureArray} from '../../util/util'
import {ensureArray, getFkPropByIdField} from '../../util/util'
import {parseOrderBy} from './orderBy'
import {parseWhere} from './where'

Expand All @@ -27,6 +27,23 @@ export function parseObjectTree(
for (let alias in fields) {
let f = fields[alias]
let prop = object.properties[f.name]
if (!prop) {
let fkProp = getFkPropByIdField(f.name, object.properties)
if (fkProp) {
if (requestedScalars[f.name] == null) {
requestedScalars[f.name] = true
requests.push({
field: f.name,
aliases: [f.name],
kind: 'scalar',
type: {kind: 'scalar', name: 'String'},
prop: {type: {kind: 'scalar', name: 'String'}, nullable: fkProp.nullable},
index: 0
} as OpaqueRequest)
}
continue
}
}
switch(prop.type.kind) {
case "scalar":
case "enum":
Expand Down
21 changes: 20 additions & 1 deletion graphql/openreader/src/model.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const baseSchema = buildASTSchema(parse(`
directive @fulltext(query: String!) on FIELD_DEFINITION
directive @cardinality(value: Int!) on OBJECT | FIELD_DEFINITION
directive @byteWeight(value: Float!) on FIELD_DEFINITION
directive @disableForeignKeyConstraint on FIELD_DEFINITION
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mo4islona not sure about directive name

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with this name

directive @variant on OBJECT # legacy
directive @jsonField on OBJECT # legacy
scalar ID
Expand Down Expand Up @@ -132,6 +133,7 @@ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType
let derivedFrom = checkDerivedFrom(type, f)
let index = checkFieldIndex(type, f)
let unique = index?.unique || false
let fkConstraint = checkDisableForeignKeyConstraint(type, f)
let limits = {
...checkByteWeightDirective(type, f),
...checkCardinalityLimitDirective(type, f)
Expand Down Expand Up @@ -199,10 +201,14 @@ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType
description
}
} else {
if (fkConstraint.disableConstraint && !nullable) {
throw new SchemaError(`Property ${propName} must be nullable when @disableForeignKeyConstraint is applied`)
}
properties[key] = {
type: {
kind: 'fk',
entity: fieldType.name
entity: fieldType.name,
...fkConstraint
},
nullable,
unique,
Expand Down Expand Up @@ -509,6 +515,19 @@ function checkCardinalityLimitDirective(type: GraphQLNamedType, f: GraphQLField<
}


function checkDisableForeignKeyConstraint(type: GraphQLNamedType, f: GraphQLField<any, any>): {disableConstraint?: boolean} {
let directives = f.astNode?.directives?.filter(d => d.name.value == 'disableForeignKeyConstraint') || []
if (directives.length == 0) return {}
if (!isEntityType(type)) throw new SchemaError(
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but only entity fields can have this directive`
)
if (directives.length > 1) throw new SchemaError(
`Multiple @disableForeignKeyConstraint directives were applied to ${type.name}.${f.name}`
)
return {disableConstraint: true}
}


function checkByteWeightDirective(type: GraphQLNamedType, f: GraphQLField<any, any>): {byteWeight?: number} {
let directives = f.astNode?.directives?.filter(d => d.name.value == 'byteWeight') || []
if (directives.length > 1) throw new SchemaError(
Expand Down
1 change: 1 addition & 0 deletions graphql/openreader/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export interface ListPropType {
export interface FkPropType {
kind: 'fk'
entity: Name
disableConstraint?: boolean
}


Expand Down
10 changes: 8 additions & 2 deletions graphql/openreader/src/sql/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import assert from "assert"
import {DbType} from "../db"
import {Entity, JsonObject, Model, ObjectPropType, Prop, UnionPropType} from "../model"
import {getEntity, getFtsQuery, getObject, getUnionProps} from "../model.tools"
import {toColumn, toFkColumn, toTable} from "../util/util"
import {getFkPropByIdField, toColumn, toFkColumn, toTable} from "../util/util"
import {AliasSet, escapeIdentifier, JoinSet} from "./util"


Expand Down Expand Up @@ -62,7 +62,13 @@ export class EntityCursor implements Cursor {
}

prop(field: string): Prop {
return assertNotNull(this.entity.properties[field], `property ${field} is missing`)
let p = this.entity.properties[field]
if (p) return p
let fkProp = getFkPropByIdField(field, this.entity.properties)
if (fkProp) {
return {type: {kind: 'scalar', name: 'String'}, nullable: fkProp.nullable}
}
return assertNotNull(p, `property ${field} is missing`)
}

output(field: string): string {
Expand Down
16 changes: 16 additions & 0 deletions graphql/openreader/src/util/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {toSnakeCase} from "@subsquid/util-naming"
import assert from "assert"
import {Prop} from "../model"


export function toColumn(gqlFieldName: string): string {
Expand Down Expand Up @@ -37,3 +38,18 @@ export function invalidFormat(type: string, value: string): Error {
export function identity<T>(x: T): T {
return x
}


export function toFkIdField(fkFieldName: string): string {
return fkFieldName + 'Id'
}


export function getFkPropByIdField(
idFieldName: string,
properties: Record<string, Prop>
): Prop | undefined {
if (!idFieldName.endsWith('Id')) return undefined
let fkProp = properties[idFieldName.slice(0, -2)]
return fkProp?.type.kind == 'fk' ? fkProp : undefined
}
11 changes: 0 additions & 11 deletions test/erc20-transfers/db/migrations/1682961487386-Data.js

This file was deleted.

17 changes: 17 additions & 0 deletions test/erc20-transfers/db/migrations/1770630835290-Data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = class Data1770630835290 {
name = 'Data1770630835290'

async up(db) {
await db.query(`CREATE TABLE "transfer" ("id" character varying NOT NULL, "block_number" integer NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "tx" text NOT NULL, "amount" numeric NOT NULL, "from_id" character varying, "to_id" character varying, CONSTRAINT "PK_fd9ddbdd49a17afcbe014401295" PRIMARY KEY ("id"))`)
await db.query(`CREATE INDEX "IDX_76bdfed1a7eb27c6d8ecbb7349" ON "transfer" ("from_id") `)
await db.query(`CREATE INDEX "IDX_0751309c66e97eac9ef1149362" ON "transfer" ("to_id") `)
await db.query(`CREATE TABLE "account" ("id" character varying NOT NULL, CONSTRAINT "PK_54115ee388cdb6d86bb4bf5b2ea" PRIMARY KEY ("id"))`)
}

async down(db) {
await db.query(`DROP TABLE "transfer"`)
await db.query(`DROP INDEX "public"."IDX_76bdfed1a7eb27c6d8ecbb7349"`)
await db.query(`DROP INDEX "public"."IDX_0751309c66e97eac9ef1149362"`)
await db.query(`DROP TABLE "account"`)
}
}
10 changes: 8 additions & 2 deletions test/erc20-transfers/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
type Account @entity {
id: ID!
transfersFrom: [Transfer!] @derivedFrom(field: "from")
transfersTo: [Transfer!] @derivedFrom(field: "to")
}

type Transfer @entity {
id: ID!
blockNumber: Int!
timestamp: DateTime!
tx: String!
from: String!
to: String!
from: Account @disableForeignKeyConstraint
to: Account @disableForeignKeyConstraint
amount: BigInt!
}
18 changes: 18 additions & 0 deletions test/erc20-transfers/src/model/generated/account.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, OneToMany as OneToMany_} from "@subsquid/typeorm-store"
import {Transfer} from "./transfer.model"

@Entity_()
export class Account {
constructor(props?: Partial<Account>) {
Object.assign(this, props)
}

@PrimaryColumn_()
id!: string

@OneToMany_(() => Transfer, e => e.from)
transfersFrom!: Transfer[]

@OneToMany_(() => Transfer, e => e.to)
transfersTo!: Transfer[]
}
1 change: 1 addition & 0 deletions test/erc20-transfers/src/model/generated/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./account.model"
export * from "./transfer.model"
13 changes: 8 additions & 5 deletions test/erc20-transfers/src/model/generated/transfer.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, IntColumn as IntColumn_, DateTimeColumn as DateTimeColumn_, StringColumn as StringColumn_, BigIntColumn as BigIntColumn_} from "@subsquid/typeorm-store"
import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, IntColumn as IntColumn_, DateTimeColumn as DateTimeColumn_, StringColumn as StringColumn_, ManyToOne as ManyToOne_, Index as Index_, BigIntColumn as BigIntColumn_} from "@subsquid/typeorm-store"
import {Account} from "./account.model"

@Entity_()
export class Transfer {
Expand All @@ -18,11 +19,13 @@ export class Transfer {
@StringColumn_({nullable: false})
tx!: string

@StringColumn_({nullable: false})
from!: string
@Index_()
@ManyToOne_(() => Account, {nullable: true, createForeignKeyConstraints: false})
from!: Account | undefined | null

@StringColumn_({nullable: false})
to!: string
@Index_()
@ManyToOne_(() => Account, {nullable: true, createForeignKeyConstraints: false})
to!: Account | undefined | null

@BigIntColumn_({nullable: false})
amount!: bigint
Expand Down
7 changes: 3 additions & 4 deletions test/erc20-transfers/src/processor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {EvmBatchProcessor} from '@subsquid/evm-processor'
import {TypeormDatabase} from '@subsquid/typeorm-store'
import * as erc20 from './abi/erc20'
import {Transfer} from './model'
import {Account, Transfer} from './model'


const CONTRACT = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9'.toLowerCase()
Expand All @@ -25,7 +25,6 @@ const processor = new EvmBatchProcessor()
processor.run(new TypeormDatabase({supportHotBlocks: true}), async ctx => {
let transfers: Transfer[] = []


for (let block of ctx.blocks) {
for (let log of block.logs) {
if (log.address == CONTRACT && erc20.events.Transfer.is(log)) {
Expand All @@ -35,8 +34,8 @@ processor.run(new TypeormDatabase({supportHotBlocks: true}), async ctx => {
blockNumber: block.header.height,
timestamp: new Date(block.header.timestamp),
tx: log.transactionHash,
from,
to,
from: new Account({id: from}),
to: new Account({id: to}),
amount: value
}))
}
Expand Down
10 changes: 7 additions & 3 deletions typeorm/typeorm-codegen/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,15 @@ export function generateOrmModels(model: Model, dir: OutDir): void {
)}, nullable: ${prop.nullable}})`
)
break
case 'fk':
case 'fk': {
const fkOptions = prop.type.disableConstraint
? ', createForeignKeyConstraints: false'
: ''
if (getFieldIndex(entity, key)?.unique) {
imports.useTypeormStore('OneToOne', 'Index', 'JoinColumn')
out.line(`@Index_({unique: true})`)
out.line(
`@OneToOne_(() => ${prop.type.entity}, {nullable: true})`
`@OneToOne_(() => ${prop.type.entity}, {nullable: true${fkOptions}})`
)
out.line(`@JoinColumn_()`)
} else {
Expand All @@ -90,10 +93,11 @@ export function generateOrmModels(model: Model, dir: OutDir): void {
}
// Make foreign entity references always nullable
out.line(
`@ManyToOne_(() => ${prop.type.entity}, {nullable: true})`
`@ManyToOne_(() => ${prop.type.entity}, {nullable: true${fkOptions}})`
)
}
break
}
case 'lookup':
imports.useTypeormStore('OneToOne')
out.line(
Expand Down
Loading