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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@subsquid/openreader",
"comment": "add @disableForeignKeyConstraint directive to suppress FK constraints and expose synthetic {field}Id fields with full filtering support",
"type": "minor"
}
],
"packageName": "@subsquid/openreader"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@subsquid/typeorm-codegen",
"comment": "pass createForeignKeyConstraints: false to TypeORM relation decorators when @disableForeignKeyConstraint is set",
"type": "minor"
}
],
"packageName": "@subsquid/typeorm-codegen"
}
22 changes: 20 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,15 @@ 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] = {
description: prop.description,
type: this.getPropType({type: {kind: 'scalar', name: 'String'}, nullable: prop.nullable}),
}
}
}
}
fields[key] = field
}
Expand Down Expand Up @@ -303,7 +312,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
32 changes: 31 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,30 @@ 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}`
)
let fieldType = asNonNull(f)
let list = unwrapList(fieldType)
if (list.nulls.length > 0) throw new SchemaError(
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but list fields cannot have this directive`
)
if (!isEntityType(list.item)) throw new SchemaError(
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but only foreign key fields can have this directive`
)
if (f.astNode?.directives?.some(d => d.name.value == 'derivedFrom')) throw new SchemaError(
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but @derivedFrom fields cannot have this directive`
)
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
1 change: 1 addition & 0 deletions graphql/openreader/src/test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ describe('basic tests', function() {
fields: [
{description: 'Unique identifier'},
{description: 'Related account'},
{description: 'Related account'},
{description: 'Balance'},
]
}
Expand Down
Loading