Skip to content

Commit dc71ff1

Browse files
authored
openreader: allow to disable fk contraint (#431)
1 parent aeefef5 commit dc71ff1

File tree

18 files changed

+540
-31
lines changed

18 files changed

+540
-31
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@subsquid/openreader",
5+
"comment": "add @disableForeignKeyConstraint directive to suppress FK constraints and expose synthetic {field}Id fields with full filtering support",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@subsquid/openreader"
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@subsquid/typeorm-codegen",
5+
"comment": "pass createForeignKeyConstraints: false to TypeORM relation decorators when @disableForeignKeyConstraint is set",
6+
"type": "minor"
7+
}
8+
],
9+
"packageName": "@subsquid/typeorm-codegen"
10+
}

graphql/openreader/src/dialect/opencrud/schema.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import {customScalars} from '../../scalars'
3737
import {ConnectionQuery, CountQuery, EntityByIdQuery, ListQuery, Query} from '../../sql/query'
3838
import {Limit} from '../../util/limit'
3939
import {getResolveTree, getTreeRequest, hasTreeRequest, simplifyResolveTree} from '../../util/resolve-tree'
40-
import {ensureArray, identity} from '../../util/util'
40+
import {ensureArray, identity, toFkIdField} from '../../util/util'
4141
import {getOrderByMapping, parseOrderBy} from './orderBy'
4242
import {parseAnyTree, parseObjectTree, parseSqlArguments} from './tree'
4343
import {parseWhere} from './where'
@@ -152,6 +152,15 @@ export class SchemaBuilder {
152152
field.resolve = (source, args, context, info) => source[info.path.key]
153153
break
154154
}
155+
if (prop.type.kind == 'fk') {
156+
let idKey = toFkIdField(key)
157+
if (!object.properties[idKey]) {
158+
fields[idKey] = {
159+
description: prop.description,
160+
type: this.getPropType({type: {kind: 'scalar', name: 'String'}, nullable: prop.nullable}),
161+
}
162+
}
163+
}
155164
}
156165
fields[key] = field
157166
}
@@ -303,7 +312,16 @@ export class SchemaBuilder {
303312
fields[`${key}_isNull`] = {type: GraphQLBoolean}
304313
fields[key] = {type: this.getWhere(prop.type.name)}
305314
break
306-
case "fk":
315+
case "fk": {
316+
fields[`${key}_isNull`] = {type: GraphQLBoolean}
317+
fields[key] = {type: this.getWhere(prop.type.entity)}
318+
this.buildPropWhereFilters(
319+
toFkIdField(key),
320+
{type: {kind: 'scalar', name: 'String'}, nullable: prop.nullable},
321+
fields
322+
)
323+
break
324+
}
307325
case "lookup":
308326
fields[`${key}_isNull`] = {type: GraphQLBoolean}
309327
fields[key] = {type: this.getWhere(prop.type.entity)}

graphql/openreader/src/dialect/opencrud/tree.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {AnyFields, FieldRequest, FieldsByEntity, OpaqueRequest} from '../../ir/f
77
import {Model} from '../../model'
88
import {getQueryableEntities} from '../../model.tools'
99
import {simplifyResolveTree} from '../../util/resolve-tree'
10-
import {ensureArray} from '../../util/util'
10+
import {ensureArray, getFkPropByIdField} from '../../util/util'
1111
import {parseOrderBy} from './orderBy'
1212
import {parseWhere} from './where'
1313

@@ -27,6 +27,23 @@ export function parseObjectTree(
2727
for (let alias in fields) {
2828
let f = fields[alias]
2929
let prop = object.properties[f.name]
30+
if (!prop) {
31+
let fkProp = getFkPropByIdField(f.name, object.properties)
32+
if (fkProp) {
33+
if (requestedScalars[f.name] == null) {
34+
requestedScalars[f.name] = true
35+
requests.push({
36+
field: f.name,
37+
aliases: [f.name],
38+
kind: 'scalar',
39+
type: {kind: 'scalar', name: 'String'},
40+
prop: {type: {kind: 'scalar', name: 'String'}, nullable: fkProp.nullable},
41+
index: 0
42+
} as OpaqueRequest)
43+
}
44+
continue
45+
}
46+
}
3047
switch(prop.type.kind) {
3148
case "scalar":
3249
case "enum":

graphql/openreader/src/model.schema.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const baseSchema = buildASTSchema(parse(`
3232
directive @fulltext(query: String!) on FIELD_DEFINITION
3333
directive @cardinality(value: Int!) on OBJECT | FIELD_DEFINITION
3434
directive @byteWeight(value: Float!) on FIELD_DEFINITION
35+
directive @disableForeignKeyConstraint on FIELD_DEFINITION
3536
directive @variant on OBJECT # legacy
3637
directive @jsonField on OBJECT # legacy
3738
scalar ID
@@ -132,6 +133,7 @@ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType
132133
let derivedFrom = checkDerivedFrom(type, f)
133134
let index = checkFieldIndex(type, f)
134135
let unique = index?.unique || false
136+
let fkConstraint = checkDisableForeignKeyConstraint(type, f)
135137
let limits = {
136138
...checkByteWeightDirective(type, f),
137139
...checkCardinalityLimitDirective(type, f)
@@ -199,10 +201,14 @@ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType
199201
description
200202
}
201203
} else {
204+
if (fkConstraint.disableConstraint && !nullable) {
205+
throw new SchemaError(`Property ${propName} must be nullable when @disableForeignKeyConstraint is applied`)
206+
}
202207
properties[key] = {
203208
type: {
204209
kind: 'fk',
205-
entity: fieldType.name
210+
entity: fieldType.name,
211+
...fkConstraint
206212
},
207213
nullable,
208214
unique,
@@ -509,6 +515,30 @@ function checkCardinalityLimitDirective(type: GraphQLNamedType, f: GraphQLField<
509515
}
510516

511517

518+
function checkDisableForeignKeyConstraint(type: GraphQLNamedType, f: GraphQLField<any, any>): {disableConstraint?: boolean} {
519+
let directives = f.astNode?.directives?.filter(d => d.name.value == 'disableForeignKeyConstraint') || []
520+
if (directives.length == 0) return {}
521+
if (!isEntityType(type)) throw new SchemaError(
522+
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but only entity fields can have this directive`
523+
)
524+
if (directives.length > 1) throw new SchemaError(
525+
`Multiple @disableForeignKeyConstraint directives were applied to ${type.name}.${f.name}`
526+
)
527+
let fieldType = asNonNull(f)
528+
let list = unwrapList(fieldType)
529+
if (list.nulls.length > 0) throw new SchemaError(
530+
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but list fields cannot have this directive`
531+
)
532+
if (!isEntityType(list.item)) throw new SchemaError(
533+
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but only foreign key fields can have this directive`
534+
)
535+
if (f.astNode?.directives?.some(d => d.name.value == 'derivedFrom')) throw new SchemaError(
536+
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but @derivedFrom fields cannot have this directive`
537+
)
538+
return {disableConstraint: true}
539+
}
540+
541+
512542
function checkByteWeightDirective(type: GraphQLNamedType, f: GraphQLField<any, any>): {byteWeight?: number} {
513543
let directives = f.astNode?.directives?.filter(d => d.name.value == 'byteWeight') || []
514544
if (directives.length > 1) throw new SchemaError(

graphql/openreader/src/model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export interface ListPropType {
112112
export interface FkPropType {
113113
kind: 'fk'
114114
entity: Name
115+
disableConstraint?: boolean
115116
}
116117

117118

graphql/openreader/src/sql/cursor.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import assert from "assert"
44
import {DbType} from "../db"
55
import {Entity, JsonObject, Model, ObjectPropType, Prop, UnionPropType} from "../model"
66
import {getEntity, getFtsQuery, getObject, getUnionProps} from "../model.tools"
7-
import {toColumn, toFkColumn, toTable} from "../util/util"
7+
import {getFkPropByIdField, toColumn, toFkColumn, toTable} from "../util/util"
88
import {AliasSet, escapeIdentifier, JoinSet} from "./util"
99

1010

@@ -62,7 +62,13 @@ export class EntityCursor implements Cursor {
6262
}
6363

6464
prop(field: string): Prop {
65-
return assertNotNull(this.entity.properties[field], `property ${field} is missing`)
65+
let p = this.entity.properties[field]
66+
if (p) return p
67+
let fkProp = getFkPropByIdField(field, this.entity.properties)
68+
if (fkProp) {
69+
return {type: {kind: 'scalar', name: 'String'}, nullable: fkProp.nullable}
70+
}
71+
return assertNotNull(p, `property ${field} is missing`)
6672
}
6773

6874
output(field: string): string {

graphql/openreader/src/test/basic.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ describe('basic tests', function() {
314314
fields: [
315315
{description: 'Unique identifier'},
316316
{description: 'Related account'},
317+
{description: 'Related account'},
317318
{description: 'Balance'},
318319
]
319320
}

0 commit comments

Comments
 (0)