@subsquid/openreader 5.4.0-beta.11e779 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dialect/opencrud/schema.d.ts.map +1 -1
- package/lib/dialect/opencrud/schema.js +16 -2
- package/lib/dialect/opencrud/schema.js.map +1 -1
- package/lib/dialect/opencrud/tree.d.ts.map +1 -1
- package/lib/dialect/opencrud/tree.js +17 -0
- package/lib/dialect/opencrud/tree.js.map +1 -1
- package/lib/model.d.ts +1 -0
- package/lib/model.d.ts.map +1 -1
- package/lib/model.schema.d.ts.map +1 -1
- package/lib/model.schema.js +25 -1
- package/lib/model.schema.js.map +1 -1
- package/lib/server.d.ts +1 -1
- package/lib/server.d.ts.map +1 -1
- package/lib/server.js +4 -2
- package/lib/server.js.map +1 -1
- package/lib/sql/cursor.d.ts.map +1 -1
- package/lib/sql/cursor.js +8 -1
- package/lib/sql/cursor.js.map +1 -1
- package/lib/test/basic.test.js +1 -0
- package/lib/test/basic.test.js.map +1 -1
- package/lib/test/disableForeignKeyConstraint.test.d.ts +2 -0
- package/lib/test/disableForeignKeyConstraint.test.d.ts.map +1 -0
- package/lib/test/disableForeignKeyConstraint.test.js +332 -0
- package/lib/test/disableForeignKeyConstraint.test.js.map +1 -0
- package/lib/util/execute.js +1 -1
- package/lib/util/execute.js.map +1 -1
- package/lib/util/limit.js +1 -1
- package/lib/util/limit.js.map +1 -1
- package/lib/util/resolve-tree.js +1 -1
- package/lib/util/resolve-tree.js.map +1 -1
- package/lib/util/util.d.ts +3 -0
- package/lib/util/util.d.ts.map +1 -1
- package/lib/util/util.js +11 -0
- package/lib/util/util.js.map +1 -1
- package/package.json +6 -6
- package/src/dialect/opencrud/schema.ts +21 -3
- package/src/dialect/opencrud/tree.ts +18 -1
- package/src/model.schema.ts +31 -1
- package/src/model.ts +1 -0
- package/src/server.ts +5 -4
- package/src/sql/cursor.ts +8 -2
- package/src/test/basic.test.ts +1 -0
- package/src/test/disableForeignKeyConstraint.test.ts +363 -0
- package/src/util/execute.ts +1 -1
- package/src/util/limit.ts +1 -1
- package/src/util/resolve-tree.ts +1 -1
- package/src/util/util.ts +16 -0
package/src/server.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import {Logger} from '@subsquid/logger'
|
|
2
2
|
import {listen, ListeningServer} from '@subsquid/util-internal-http-server'
|
|
3
|
-
import {KeyValueCache, PluginDefinition} from 'apollo-server-core'
|
|
4
|
-
import {ApolloServer} from 'apollo-server-express'
|
|
3
|
+
import {KeyValueCache, PluginDefinition} from '@subsquid/apollo-server-core'
|
|
4
|
+
import {ApolloServer} from '@subsquid/apollo-server-express'
|
|
5
5
|
import express from 'express'
|
|
6
|
-
import { validate } from 'graphql'
|
|
7
6
|
import fs from 'fs'
|
|
8
7
|
import {ExecutionArgs, GraphQLSchema} from 'graphql'
|
|
9
8
|
import {useServer as useWsServer} from 'graphql-ws/lib/use/ws'
|
|
@@ -156,7 +155,9 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
|
|
|
156
155
|
stopOnTerminationSignals: false,
|
|
157
156
|
allowBatchedHttpRequests: false,
|
|
158
157
|
dangerouslyDisableValidation: false,
|
|
159
|
-
|
|
158
|
+
validateOptions: {
|
|
159
|
+
maxErrors: validationMaxErrors
|
|
160
|
+
},
|
|
160
161
|
executor: async req => {
|
|
161
162
|
return execute({
|
|
162
163
|
schema,
|
package/src/sql/cursor.ts
CHANGED
|
@@ -4,7 +4,7 @@ import assert from "assert"
|
|
|
4
4
|
import {DbType} from "../db"
|
|
5
5
|
import {Entity, JsonObject, Model, ObjectPropType, Prop, UnionPropType} from "../model"
|
|
6
6
|
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"
|
|
8
8
|
import {AliasSet, escapeIdentifier, JoinSet} from "./util"
|
|
9
9
|
|
|
10
10
|
|
|
@@ -62,7 +62,13 @@ export class EntityCursor implements Cursor {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
prop(field: string): Prop {
|
|
65
|
-
|
|
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`)
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
output(field: string): string {
|
package/src/test/basic.test.ts
CHANGED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import expect from 'expect'
|
|
2
|
+
import {parse} from 'graphql'
|
|
3
|
+
import {buildModel, buildSchema, SchemaError} from '../model.schema'
|
|
4
|
+
import {useDatabase, useServer} from './setup'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
function model(schema: string) {
|
|
8
|
+
return buildModel(buildSchema(parse(schema)))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
describe('@disableForeignKeyConstraint', function () {
|
|
13
|
+
describe('schema validation', function () {
|
|
14
|
+
it('parses valid usage and sets disableConstraint flag', function () {
|
|
15
|
+
let m = model(`
|
|
16
|
+
type Account @entity {
|
|
17
|
+
id: ID!
|
|
18
|
+
}
|
|
19
|
+
type Transfer @entity {
|
|
20
|
+
id: ID!
|
|
21
|
+
from: Account @disableForeignKeyConstraint
|
|
22
|
+
}
|
|
23
|
+
`)
|
|
24
|
+
expect(m.Transfer).toBeDefined()
|
|
25
|
+
expect(m.Transfer.kind).toBe('entity')
|
|
26
|
+
if (m.Transfer.kind === 'entity') {
|
|
27
|
+
let fromProp = m.Transfer.properties.from
|
|
28
|
+
expect(fromProp.type).toEqual({
|
|
29
|
+
kind: 'fk',
|
|
30
|
+
entity: 'Account',
|
|
31
|
+
disableConstraint: true,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('rejects non-nullable FK field', function () {
|
|
37
|
+
expect(() =>
|
|
38
|
+
model(`
|
|
39
|
+
type Account @entity {
|
|
40
|
+
id: ID!
|
|
41
|
+
}
|
|
42
|
+
type Transfer @entity {
|
|
43
|
+
id: ID!
|
|
44
|
+
from: Account! @disableForeignKeyConstraint
|
|
45
|
+
}
|
|
46
|
+
`)
|
|
47
|
+
).toThrow(SchemaError)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('rejects directive on scalar field', function () {
|
|
51
|
+
expect(() =>
|
|
52
|
+
model(`
|
|
53
|
+
type Account @entity {
|
|
54
|
+
id: ID!
|
|
55
|
+
name: String @disableForeignKeyConstraint
|
|
56
|
+
}
|
|
57
|
+
`)
|
|
58
|
+
).toThrow(SchemaError)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('rejects directive on enum field', function () {
|
|
62
|
+
expect(() =>
|
|
63
|
+
model(`
|
|
64
|
+
enum Status { ACTIVE INACTIVE }
|
|
65
|
+
type Account @entity {
|
|
66
|
+
id: ID!
|
|
67
|
+
status: Status @disableForeignKeyConstraint
|
|
68
|
+
}
|
|
69
|
+
`)
|
|
70
|
+
).toThrow(SchemaError)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('rejects directive on list (derivedFrom) field', function () {
|
|
74
|
+
expect(() =>
|
|
75
|
+
model(`
|
|
76
|
+
type Account @entity {
|
|
77
|
+
id: ID!
|
|
78
|
+
transfers: [Transfer!] @derivedFrom(field: "from") @disableForeignKeyConstraint
|
|
79
|
+
}
|
|
80
|
+
type Transfer @entity {
|
|
81
|
+
id: ID!
|
|
82
|
+
from: Account
|
|
83
|
+
}
|
|
84
|
+
`)
|
|
85
|
+
).toThrow(SchemaError)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('rejects directive on @derivedFrom lookup field', function () {
|
|
89
|
+
expect(() =>
|
|
90
|
+
model(`
|
|
91
|
+
type Account @entity {
|
|
92
|
+
id: ID!
|
|
93
|
+
profile: Profile @derivedFrom(field: "account") @disableForeignKeyConstraint
|
|
94
|
+
}
|
|
95
|
+
type Profile @entity {
|
|
96
|
+
id: ID!
|
|
97
|
+
account: Account! @unique
|
|
98
|
+
}
|
|
99
|
+
`)
|
|
100
|
+
).toThrow(SchemaError)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('rejects directive on non-entity type', function () {
|
|
104
|
+
expect(() =>
|
|
105
|
+
model(`
|
|
106
|
+
type Account @entity {
|
|
107
|
+
id: ID!
|
|
108
|
+
}
|
|
109
|
+
type Metadata {
|
|
110
|
+
ref: Account @disableForeignKeyConstraint
|
|
111
|
+
}
|
|
112
|
+
type Transfer @entity {
|
|
113
|
+
id: ID!
|
|
114
|
+
meta: Metadata
|
|
115
|
+
}
|
|
116
|
+
`)
|
|
117
|
+
).toThrow(SchemaError)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('runtime (opencrud)', function () {
|
|
122
|
+
// Database setup: transfer table has from_id and to_id columns
|
|
123
|
+
// with NO foreign key constraints. Some IDs reference accounts
|
|
124
|
+
// that don't exist, simulating @disableForeignKeyConstraint behavior.
|
|
125
|
+
useDatabase([
|
|
126
|
+
`create table account (id text primary key, name text)`,
|
|
127
|
+
`create table transfer (id text primary key, from_id text, to_id text, amount numeric)`,
|
|
128
|
+
// account '1' exists, account '2' exists, but account 'missing' does NOT exist
|
|
129
|
+
`insert into account (id, name) values ('1', 'Alice')`,
|
|
130
|
+
`insert into account (id, name) values ('2', 'Bob')`,
|
|
131
|
+
`insert into transfer (id, from_id, to_id, amount) values ('t1', '1', '2', 100)`,
|
|
132
|
+
`insert into transfer (id, from_id, to_id, amount) values ('t2', '2', 'missing', 50)`,
|
|
133
|
+
`insert into transfer (id, from_id, to_id, amount) values ('t3', 'missing', '1', 75)`,
|
|
134
|
+
`insert into transfer (id, from_id, to_id, amount) values ('t4', null, '1', 25)`,
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
const client = useServer(`
|
|
138
|
+
type Account @entity {
|
|
139
|
+
id: ID!
|
|
140
|
+
name: String
|
|
141
|
+
transfersFrom: [Transfer!] @derivedFrom(field: "from")
|
|
142
|
+
transfersTo: [Transfer!] @derivedFrom(field: "to")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type Transfer @entity {
|
|
146
|
+
id: ID!
|
|
147
|
+
from: Account @disableForeignKeyConstraint
|
|
148
|
+
to: Account @disableForeignKeyConstraint
|
|
149
|
+
amount: Int!
|
|
150
|
+
}
|
|
151
|
+
`)
|
|
152
|
+
|
|
153
|
+
it('returns null for FK relation when referenced row is missing, but exposes raw ID via {field}Id', function () {
|
|
154
|
+
return client.test(`
|
|
155
|
+
query {
|
|
156
|
+
transfers(orderBy: [id_ASC]) {
|
|
157
|
+
id
|
|
158
|
+
from { id name }
|
|
159
|
+
fromId
|
|
160
|
+
to { id name }
|
|
161
|
+
toId
|
|
162
|
+
amount
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
`, {
|
|
166
|
+
transfers: [
|
|
167
|
+
{id: 't1', from: {id: '1', name: 'Alice'}, fromId: '1', to: {id: '2', name: 'Bob'}, toId: '2', amount: 100},
|
|
168
|
+
{id: 't2', from: {id: '2', name: 'Bob'}, fromId: '2', to: null, toId: 'missing', amount: 50},
|
|
169
|
+
{id: 't3', from: null, fromId: 'missing', to: {id: '1', name: 'Alice'}, toId: '1', amount: 75},
|
|
170
|
+
{id: 't4', from: null, fromId: null, to: {id: '1', name: 'Alice'}, toId: '1', amount: 25},
|
|
171
|
+
]
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('can query only {field}Id without the relation', function () {
|
|
176
|
+
return client.test(`
|
|
177
|
+
query {
|
|
178
|
+
transfers(orderBy: [id_ASC]) {
|
|
179
|
+
id
|
|
180
|
+
fromId
|
|
181
|
+
toId
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`, {
|
|
185
|
+
transfers: [
|
|
186
|
+
{id: 't1', fromId: '1', toId: '2'},
|
|
187
|
+
{id: 't2', fromId: '2', toId: 'missing'},
|
|
188
|
+
{id: 't3', fromId: 'missing', toId: '1'},
|
|
189
|
+
{id: 't4', fromId: null, toId: '1'},
|
|
190
|
+
]
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('supports {field}Id_eq filter', function () {
|
|
195
|
+
return client.test(`
|
|
196
|
+
query {
|
|
197
|
+
transfers(where: {fromId_eq: "missing"}, orderBy: [id_ASC]) {
|
|
198
|
+
id
|
|
199
|
+
fromId
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
`, {
|
|
203
|
+
transfers: [
|
|
204
|
+
{id: 't3', fromId: 'missing'},
|
|
205
|
+
]
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('supports {field}Id_not_eq filter', function () {
|
|
210
|
+
return client.test(`
|
|
211
|
+
query {
|
|
212
|
+
transfers(where: {fromId_not_eq: "missing"}, orderBy: [id_ASC]) {
|
|
213
|
+
id
|
|
214
|
+
fromId
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
`, {
|
|
218
|
+
transfers: [
|
|
219
|
+
{id: 't1', fromId: '1'},
|
|
220
|
+
{id: 't2', fromId: '2'},
|
|
221
|
+
]
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('supports {field}Id_in filter', function () {
|
|
226
|
+
return client.test(`
|
|
227
|
+
query {
|
|
228
|
+
transfers(where: {fromId_in: ["1", "missing"]}, orderBy: [id_ASC]) {
|
|
229
|
+
id
|
|
230
|
+
fromId
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
`, {
|
|
234
|
+
transfers: [
|
|
235
|
+
{id: 't1', fromId: '1'},
|
|
236
|
+
{id: 't3', fromId: 'missing'},
|
|
237
|
+
]
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('supports {field}Id_not_in filter', function () {
|
|
242
|
+
return client.test(`
|
|
243
|
+
query {
|
|
244
|
+
transfers(where: {fromId_not_in: ["1", "missing"]}, orderBy: [id_ASC]) {
|
|
245
|
+
id
|
|
246
|
+
fromId
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
`, {
|
|
250
|
+
transfers: [
|
|
251
|
+
{id: 't2', fromId: '2'},
|
|
252
|
+
]
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('supports {field}Id_isNull filter', function () {
|
|
257
|
+
return client.test(`
|
|
258
|
+
query {
|
|
259
|
+
transfers(where: {fromId_isNull: true}, orderBy: [id_ASC]) {
|
|
260
|
+
id
|
|
261
|
+
fromId
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
`, {
|
|
265
|
+
transfers: [
|
|
266
|
+
{id: 't4', fromId: null},
|
|
267
|
+
]
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('supports {field}Id_contains filter', function () {
|
|
272
|
+
return client.test(`
|
|
273
|
+
query {
|
|
274
|
+
transfers(where: {fromId_contains: "iss"}, orderBy: [id_ASC]) {
|
|
275
|
+
id
|
|
276
|
+
fromId
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
`, {
|
|
280
|
+
transfers: [
|
|
281
|
+
{id: 't3', fromId: 'missing'},
|
|
282
|
+
]
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('supports {field}Id_startsWith filter', function () {
|
|
287
|
+
return client.test(`
|
|
288
|
+
query {
|
|
289
|
+
transfers(where: {fromId_startsWith: "mis"}, orderBy: [id_ASC]) {
|
|
290
|
+
id
|
|
291
|
+
fromId
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
`, {
|
|
295
|
+
transfers: [
|
|
296
|
+
{id: 't3', fromId: 'missing'},
|
|
297
|
+
]
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('supports {field}Id_endsWith filter', function () {
|
|
302
|
+
return client.test(`
|
|
303
|
+
query {
|
|
304
|
+
transfers(where: {toId_endsWith: "2"}, orderBy: [id_ASC]) {
|
|
305
|
+
id
|
|
306
|
+
toId
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
`, {
|
|
310
|
+
transfers: [
|
|
311
|
+
{id: 't1', toId: '2'},
|
|
312
|
+
]
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('supports {field}Id_gt and {field}Id_lt filters', function () {
|
|
317
|
+
return client.test(`
|
|
318
|
+
query {
|
|
319
|
+
transfers(where: {fromId_gt: "1", fromId_lt: "missing"}, orderBy: [id_ASC]) {
|
|
320
|
+
id
|
|
321
|
+
fromId
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
`, {
|
|
325
|
+
transfers: [
|
|
326
|
+
{id: 't2', fromId: '2'},
|
|
327
|
+
]
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('can combine {field}Id filter with other filters', function () {
|
|
332
|
+
return client.test(`
|
|
333
|
+
query {
|
|
334
|
+
transfers(where: {fromId_eq: "2", amount_gt: 40}, orderBy: [id_ASC]) {
|
|
335
|
+
id
|
|
336
|
+
fromId
|
|
337
|
+
amount
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
`, {
|
|
341
|
+
transfers: [
|
|
342
|
+
{id: 't2', fromId: '2', amount: 50},
|
|
343
|
+
]
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('can combine {field}Id filter with relation filter in OR', function () {
|
|
348
|
+
return client.test(`
|
|
349
|
+
query {
|
|
350
|
+
transfers(where: {OR: [{fromId_eq: "missing"}, {from: {name_eq: "Alice"}}]}, orderBy: [id_ASC]) {
|
|
351
|
+
id
|
|
352
|
+
fromId
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
`, {
|
|
356
|
+
transfers: [
|
|
357
|
+
{id: 't1', fromId: '1'},
|
|
358
|
+
{id: 't3', fromId: 'missing'},
|
|
359
|
+
]
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
})
|
package/src/util/execute.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {Logger, LogLevel} from '@subsquid/logger'
|
|
2
2
|
import {addErrorContext} from '@subsquid/util-internal'
|
|
3
|
-
import {UserInputError} from 'apollo-server-core'
|
|
3
|
+
import {UserInputError} from '@subsquid/apollo-server-core'
|
|
4
4
|
import {getOperationRootType, GraphQLError, subscribe as graphqlSubscribe} from 'graphql'
|
|
5
5
|
import {ExecutionResult} from 'graphql-ws'
|
|
6
6
|
import {
|
package/src/util/limit.ts
CHANGED
package/src/util/resolve-tree.ts
CHANGED
package/src/util/util.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {toSnakeCase} from "@subsquid/util-naming"
|
|
2
2
|
import assert from "assert"
|
|
3
|
+
import {Prop} from "../model"
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
export function toColumn(gqlFieldName: string): string {
|
|
@@ -37,3 +38,18 @@ export function invalidFormat(type: string, value: string): Error {
|
|
|
37
38
|
export function identity<T>(x: T): T {
|
|
38
39
|
return x
|
|
39
40
|
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export function toFkIdField(fkFieldName: string): string {
|
|
44
|
+
return fkFieldName + 'Id'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export function getFkPropByIdField(
|
|
49
|
+
idFieldName: string,
|
|
50
|
+
properties: Record<string, Prop>
|
|
51
|
+
): Prop | undefined {
|
|
52
|
+
if (!idFieldName.endsWith('Id')) return undefined
|
|
53
|
+
let fkProp = properties[idFieldName.slice(0, -2)]
|
|
54
|
+
return fkProp?.type.kind == 'fk' ? fkProp : undefined
|
|
55
|
+
}
|