@subsquid/openreader 0.2.0 → 0.3.3

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.
Files changed (128) hide show
  1. package/README.md +2 -15
  2. package/bin/main.js +2 -0
  3. package/dist/db.d.ts +28 -0
  4. package/dist/db.d.ts.map +1 -0
  5. package/dist/db.js +69 -0
  6. package/dist/db.js.map +1 -0
  7. package/dist/gql/opencrud.d.ts +1 -0
  8. package/dist/gql/opencrud.d.ts.map +1 -0
  9. package/dist/gql/opencrud.js +10 -9
  10. package/dist/gql/opencrud.js.map +1 -1
  11. package/dist/gql/schema.d.ts +1 -0
  12. package/dist/gql/schema.d.ts.map +1 -0
  13. package/dist/main.d.ts +2 -3
  14. package/dist/main.d.ts.map +1 -0
  15. package/dist/main.js +6 -30
  16. package/dist/main.js.map +1 -1
  17. package/dist/model.d.ts +1 -0
  18. package/dist/model.d.ts.map +1 -0
  19. package/dist/model.tools.d.ts +1 -0
  20. package/dist/model.tools.d.ts.map +1 -0
  21. package/dist/orderBy.d.ts +1 -0
  22. package/dist/orderBy.d.ts.map +1 -0
  23. package/dist/queryBuilder.d.ts +3 -2
  24. package/dist/queryBuilder.d.ts.map +1 -0
  25. package/dist/queryBuilder.js +28 -27
  26. package/dist/queryBuilder.js.map +1 -1
  27. package/dist/relayConnection.d.ts +1 -0
  28. package/dist/relayConnection.d.ts.map +1 -0
  29. package/dist/requestedFields.d.ts +1 -0
  30. package/dist/requestedFields.d.ts.map +1 -0
  31. package/dist/requestedFields.js +3 -3
  32. package/dist/requestedFields.js.map +1 -1
  33. package/dist/resolver.d.ts +3 -2
  34. package/dist/resolver.d.ts.map +1 -0
  35. package/dist/resolver.js +12 -11
  36. package/dist/resolver.js.map +1 -1
  37. package/dist/scalars.d.ts +2 -2
  38. package/dist/scalars.d.ts.map +1 -0
  39. package/dist/scalars.js +6 -2
  40. package/dist/scalars.js.map +1 -1
  41. package/dist/server.d.ts +5 -11
  42. package/dist/server.d.ts.map +1 -0
  43. package/dist/server.js +13 -31
  44. package/dist/server.js.map +1 -1
  45. package/dist/test/basic.test.d.ts +2 -0
  46. package/dist/test/basic.test.d.ts.map +1 -0
  47. package/dist/test/basic.test.js +286 -0
  48. package/dist/test/basic.test.js.map +1 -0
  49. package/dist/test/connection.test.d.ts +2 -0
  50. package/dist/test/connection.test.d.ts.map +1 -0
  51. package/dist/test/connection.test.js +193 -0
  52. package/dist/test/connection.test.js.map +1 -0
  53. package/dist/test/fts.test.d.ts +2 -0
  54. package/dist/test/fts.test.d.ts.map +1 -0
  55. package/dist/test/fts.test.js +110 -0
  56. package/dist/test/fts.test.js.map +1 -0
  57. package/dist/test/lists.test.d.ts +2 -0
  58. package/dist/test/lists.test.d.ts.map +1 -0
  59. package/dist/test/lists.test.js +266 -0
  60. package/dist/test/lists.test.js.map +1 -0
  61. package/dist/test/lookup.test.d.ts +2 -0
  62. package/dist/test/lookup.test.d.ts.map +1 -0
  63. package/dist/test/lookup.test.js +109 -0
  64. package/dist/test/lookup.test.js.map +1 -0
  65. package/dist/test/scalars.test.d.ts +2 -0
  66. package/dist/test/scalars.test.d.ts.map +1 -0
  67. package/dist/test/scalars.test.js +303 -0
  68. package/dist/test/scalars.test.js.map +1 -0
  69. package/dist/test/tools.test.d.ts +2 -0
  70. package/dist/test/tools.test.d.ts.map +1 -0
  71. package/dist/test/tools.test.js +49 -0
  72. package/dist/test/tools.test.js.map +1 -0
  73. package/dist/test/typed-json.test.d.ts +2 -0
  74. package/dist/test/typed-json.test.d.ts.map +1 -0
  75. package/dist/test/typed-json.test.js +75 -0
  76. package/dist/test/typed-json.test.js.map +1 -0
  77. package/dist/test/unions.test.d.ts +2 -0
  78. package/dist/test/unions.test.d.ts.map +1 -0
  79. package/dist/test/unions.test.js +84 -0
  80. package/dist/test/unions.test.js.map +1 -0
  81. package/dist/test/util/setup.d.ts +7 -0
  82. package/dist/test/util/setup.d.ts.map +1 -0
  83. package/dist/test/util/setup.js +60 -0
  84. package/dist/test/util/setup.js.map +1 -0
  85. package/dist/test/where.test.d.ts +2 -0
  86. package/dist/test/where.test.d.ts.map +1 -0
  87. package/dist/test/where.test.js +127 -0
  88. package/dist/test/where.test.js.map +1 -0
  89. package/dist/tools.d.ts +1 -0
  90. package/dist/tools.d.ts.map +1 -0
  91. package/dist/util.d.ts +1 -13
  92. package/dist/util.d.ts.map +1 -0
  93. package/dist/util.js +6 -79
  94. package/dist/util.js.map +1 -1
  95. package/dist/where.d.ts +1 -0
  96. package/dist/where.d.ts.map +1 -0
  97. package/package.json +26 -20
  98. package/src/db.ts +83 -0
  99. package/src/gql/opencrud.ts +328 -0
  100. package/src/gql/schema.ts +337 -0
  101. package/src/main.ts +51 -0
  102. package/src/model.tools.ts +173 -0
  103. package/src/model.ts +125 -0
  104. package/src/orderBy.ts +105 -0
  105. package/src/queryBuilder.ts +785 -0
  106. package/src/relayConnection.ts +80 -0
  107. package/src/requestedFields.ts +246 -0
  108. package/src/resolver.ts +199 -0
  109. package/src/scalars.ts +247 -0
  110. package/src/server.ts +115 -0
  111. package/src/test/basic.test.ts +339 -0
  112. package/src/test/connection.test.ts +195 -0
  113. package/src/test/fts.test.ts +114 -0
  114. package/src/test/lists.test.ts +278 -0
  115. package/src/test/lookup.test.ts +111 -0
  116. package/src/test/scalars.test.ts +316 -0
  117. package/src/test/tools.test.ts +27 -0
  118. package/src/test/typed-json.test.ts +76 -0
  119. package/src/test/unions.test.ts +85 -0
  120. package/src/test/util/setup.ts +63 -0
  121. package/src/test/where.test.ts +135 -0
  122. package/src/tools.ts +33 -0
  123. package/src/util.ts +39 -0
  124. package/src/where.ts +110 -0
  125. package/CHANGELOG.md +0 -16
  126. package/dist/transaction.d.ts +0 -10
  127. package/dist/transaction.js +0 -47
  128. package/dist/transaction.js.map +0 -1
package/src/scalars.ts ADDED
@@ -0,0 +1,247 @@
1
+ /**
2
+ * The current concept of custom scalars is as follows:
3
+ *
4
+ * Each custom scalar has a canonical string representation which is used almost everywhere:
5
+ * in JSON responses
6
+ * in graphql queries/schemas
7
+ * in jsonb database columns
8
+ * in database results
9
+ *
10
+ * Database must support 2 way coercion between underlying database type and canonical representation
11
+ * of a corresponding scalar.
12
+ *
13
+ * We receive from database canonical strings and use them within our resolvers as is.
14
+ *
15
+ * GraphQL parsing procedures convert canonical string representation to corresponding js type.
16
+ * This is for compatibility with possible extensions which would like to reuse our scalars.
17
+ *
18
+ * In GraphQL serialization procedures we accept both a canonical string representation
19
+ * and corresponding js type.
20
+ */
21
+
22
+ import {GraphQLScalarType} from "graphql"
23
+
24
+
25
+ export interface Scalar {
26
+ gql: GraphQLScalarType
27
+ fromStringCast: (sqlExp: string) => string
28
+ toStringCast: (sqlExp: string) => string
29
+ toStringArrayCast: (sqlExp: string) => string
30
+ }
31
+
32
+
33
+ export const scalars: Record<string, Scalar> = {
34
+ BigInt: {
35
+ gql: new GraphQLScalarType({
36
+ name: 'BigInt',
37
+ description: 'Big number integer',
38
+ serialize(value: number | string | bigint) {
39
+ return ''+value
40
+ },
41
+ parseValue(value: string) {
42
+ if (!isBigInt(value)) throw invalidFormat('BigInt', value)
43
+ return BigInt(value)
44
+ },
45
+ parseLiteral(ast) {
46
+ switch(ast.kind) {
47
+ case "StringValue":
48
+ if (isBigInt(ast.value)) {
49
+ return BigInt(ast.value)
50
+ } else {
51
+ throw invalidFormat('BigInt', ast.value)
52
+ }
53
+ case "IntValue":
54
+ return BigInt(ast.value)
55
+ default:
56
+ return null
57
+ }
58
+ }
59
+ }),
60
+ fromStringCast(exp) {
61
+ return `(${exp})::numeric`
62
+ },
63
+ toStringCast(exp) {
64
+ return `(${exp})::text`
65
+ },
66
+ toStringArrayCast(exp) {
67
+ return `(${exp})::text[]`
68
+ }
69
+ },
70
+ DateTime: {
71
+ gql: new GraphQLScalarType({
72
+ name: 'DateTime',
73
+ description:
74
+ 'A date-time string in simplified extended ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ)',
75
+ serialize(value: Date | string) {
76
+ if (value instanceof Date) {
77
+ return value.toISOString()
78
+ } else {
79
+ if (!isIsoDateTimeString(value)) throw invalidFormat('DateTime', value)
80
+ return value
81
+ }
82
+ },
83
+ parseValue(value: string) {
84
+ return parseDateTime(value)
85
+ },
86
+ parseLiteral(ast) {
87
+ switch(ast.kind) {
88
+ case "StringValue":
89
+ return parseDateTime(ast.value)
90
+ default:
91
+ return null
92
+ }
93
+ }
94
+ }),
95
+ fromStringCast(exp) {
96
+ return `(${exp})::timestamptz`
97
+ },
98
+ toStringCast(exp) {
99
+ return `to_char((${exp}) at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`
100
+ },
101
+ toStringArrayCast(exp) {
102
+ return `array(select to_char(i at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') from unnest(${exp}) as i)`
103
+ }
104
+ },
105
+ Bytes: {
106
+ gql: new GraphQLScalarType({
107
+ name: 'Bytes',
108
+ description: 'Binary data encoded as a hex string always prefixed with 0x',
109
+ serialize(value: string | Buffer) {
110
+ if (typeof value == 'string') {
111
+ if (!isBytesString(value)) throw invalidFormat('Bytes', value)
112
+ return value.toLowerCase()
113
+ } else {
114
+ return '0x' + value.toString('hex')
115
+ }
116
+ },
117
+ parseValue(value: string) {
118
+ return parseBytes(value)
119
+ },
120
+ parseLiteral(ast) {
121
+ switch(ast.kind) {
122
+ case "StringValue":
123
+ return parseBytes(ast.value)
124
+ default:
125
+ return null
126
+ }
127
+ }
128
+ }),
129
+ fromStringCast(exp) {
130
+ return `decode(substr(${exp}, 3), 'hex')`
131
+ },
132
+ toStringCast(exp) {
133
+ return `'0x' || encode(${exp}, 'hex')`
134
+ },
135
+ toStringArrayCast(exp) {
136
+ return `array(select '0x' || encode(i, 'hex') from unnest(${exp}) as i)`
137
+ }
138
+ }
139
+ }
140
+
141
+
142
+ function isBigInt(s: string): boolean {
143
+ return /^[+\-]?\d+$/.test(s)
144
+ }
145
+
146
+
147
+ // credit - https://github.com/Urigo/graphql-scalars/blob/91b4ea8df891be8af7904cf84751930cc0c6613d/src/scalars/iso-date/validator.ts#L122
148
+ const RFC_3339_REGEX = /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60))(\.\d{1,})?([Z])$/
149
+
150
+
151
+ function isIsoDateTimeString(s: string): boolean {
152
+ return RFC_3339_REGEX.test(s)
153
+ }
154
+
155
+
156
+ function parseDateTime(s: string): Date {
157
+ if (!isIsoDateTimeString(s)) throw invalidFormat('DateTime', s)
158
+ let timestamp = Date.parse(s)
159
+ if (isNaN(timestamp)) throw invalidFormat('DateTime', s)
160
+ return new Date(timestamp)
161
+ }
162
+
163
+
164
+ function isBytesString(s: string): boolean {
165
+ return s.length % 2 == 0 && /^0x[a-f0-9]+$/i.test(s)
166
+ }
167
+
168
+
169
+ function parseBytes(s: string): Buffer {
170
+ if (!isBytesString(s)) throw invalidFormat('Bytes', s)
171
+ return Buffer.from(s.slice(2), 'hex')
172
+ }
173
+
174
+
175
+ function invalidFormat(type: string, value: string): Error {
176
+ return new TypeError(`Not a ${type}: ${value}`)
177
+ }
178
+
179
+
180
+ export const scalars_list = ['ID'].concat(Object.keys(scalars))
181
+
182
+
183
+ export function getScalarResolvers(): Record<string, GraphQLScalarType> {
184
+ let resolvers: Record<string, GraphQLScalarType> = {}
185
+ for (let type in scalars) {
186
+ resolvers[type] = scalars[type].gql
187
+ }
188
+ return resolvers
189
+ }
190
+
191
+
192
+ export function toOutputCast(scalarType: string, sqlExp: string): string {
193
+ let s = scalars[scalarType]
194
+ if (s) {
195
+ return s.toStringCast(sqlExp)
196
+ } else {
197
+ return sqlExp
198
+ }
199
+ }
200
+
201
+
202
+ export function fromStringCast(scalarType: string, sqlExp: string): string {
203
+ let s = scalars[scalarType]
204
+ if (s) {
205
+ return s.fromStringCast(sqlExp)
206
+ } else {
207
+ return sqlExp
208
+ }
209
+ }
210
+
211
+
212
+ export function toOutputArrayCast(scalarType: string, sqlExp: string): string {
213
+ let s = scalars[scalarType]
214
+ if (s) {
215
+ return s.toStringArrayCast(sqlExp)
216
+ } else {
217
+ return sqlExp
218
+ }
219
+ }
220
+
221
+
222
+ export function fromJsonCast(scalarType: string, objSqlExp: string, prop: string): string {
223
+ switch(scalarType) {
224
+ case 'Int':
225
+ return `(${objSqlExp}->'${prop}')::integer`
226
+ case 'Float':
227
+ return `(${objSqlExp}->'${prop}')::numeric`
228
+ case 'Boolean':
229
+ return `(${objSqlExp}->'${prop}')::bool`
230
+ default:
231
+ return fromStringCast(scalarType, `${objSqlExp}->>'${prop}'`)
232
+ }
233
+ }
234
+
235
+
236
+ export function fromJsonToOutputCast(scalarType: string, objSqlExp: string, prop: string) {
237
+ switch(scalarType) {
238
+ case 'Int':
239
+ return `(${objSqlExp}->'${prop}')::integer`
240
+ case 'Float':
241
+ return `(${objSqlExp}->'${prop}')::numeric`
242
+ case 'Boolean':
243
+ return `(${objSqlExp}->'${prop}')::bool`
244
+ default:
245
+ return `${objSqlExp}->>'${prop}'`
246
+ }
247
+ }
package/src/server.ts ADDED
@@ -0,0 +1,115 @@
1
+ import {ApolloServerPluginDrainHttpServer} from "apollo-server-core"
2
+ import {ApolloServer} from "apollo-server-express"
3
+ import assert from "assert"
4
+ import express from "express"
5
+ import fs from "fs"
6
+ import http from "http"
7
+ import path from "path"
8
+ import type {Pool} from "pg"
9
+ import {PoolTransaction} from "./db"
10
+ import {buildServerSchema} from "./gql/opencrud"
11
+ import type {Model} from "./model"
12
+ import {buildResolvers} from "./resolver"
13
+
14
+
15
+ export interface ListeningServer {
16
+ readonly port: number
17
+ stop(): Promise<void>
18
+ }
19
+
20
+
21
+ export interface ServerOptions {
22
+ model: Model
23
+ db: Pool
24
+ port: number | string
25
+ graphiqlConsole?: boolean
26
+ }
27
+
28
+
29
+ export async function serve(options: ServerOptions): Promise<ListeningServer> {
30
+ let {model, db} = options
31
+ let resolvers = buildResolvers(model)
32
+ let typeDefs = buildServerSchema(model)
33
+ let app = express()
34
+ let server = http.createServer(app)
35
+
36
+ let apollo = new ApolloServer({
37
+ typeDefs,
38
+ resolvers,
39
+ context: () => ({openReaderTransaction: new PoolTransaction(db)}),
40
+ plugins: [
41
+ {
42
+ async requestDidStart() {
43
+ return {
44
+ willSendResponse(req: any) {
45
+ return req.context.openReaderTransaction.close()
46
+ }
47
+ }
48
+ }
49
+ },
50
+ ApolloServerPluginDrainHttpServer({httpServer: server})
51
+ ]
52
+ })
53
+
54
+ if (options.graphiqlConsole !== false) {
55
+ setupGraphiqlConsole(app)
56
+ }
57
+
58
+ await apollo.start()
59
+ apollo.applyMiddleware({app})
60
+ return listen(apollo, server, options.port)
61
+ }
62
+
63
+
64
+ export function listen(apollo: ApolloServer, server: http.Server, port: number | string): Promise<ListeningServer> {
65
+ return new Promise((resolve, reject) => {
66
+ function onerror(err: Error) {
67
+ cleanup()
68
+ reject(err)
69
+ }
70
+
71
+ function onlistening() {
72
+ cleanup()
73
+ let address = server.address()
74
+ assert(address != null && typeof address == 'object')
75
+ resolve({
76
+ port: address.port,
77
+ stop: () => apollo.stop()
78
+ })
79
+ }
80
+
81
+ function cleanup() {
82
+ server.removeListener('error', onerror)
83
+ server.removeListener('listening', onlistening)
84
+ }
85
+
86
+ server.on('error', onerror)
87
+ server.on('listening', onlistening)
88
+ server.listen(port)
89
+ })
90
+ }
91
+
92
+
93
+ export function setupGraphiqlConsole(app: express.Application): void {
94
+ let assets = path.join(
95
+ require.resolve('@subsquid/graphiql-console/package.json'),
96
+ '../build'
97
+ )
98
+
99
+ let indexHtml = fs.readFileSync(path.join(assets, 'index.html'), 'utf-8')
100
+ .replace(/\/static\//g, 'console/static/')
101
+ .replace('/manifest.json', 'console/manifest.json')
102
+ .replace('${GRAPHQL_API}', 'graphql')
103
+ .replace('${APP_TITLE}', 'Query node playground')
104
+
105
+ app.use('/console', express.static(assets))
106
+
107
+ app.use('/graphql', (req, res, next) => {
108
+ if (req.path != '/') return next()
109
+ if (req.method != 'GET' && req.method != 'HEAD') return next()
110
+ if (req.query['query']) return next()
111
+ res.vary('Accept')
112
+ if (!req.accepts('html')) return next()
113
+ res.type('html').send(indexHtml)
114
+ })
115
+ }
@@ -0,0 +1,339 @@
1
+ import {useDatabase, useServer} from "./util/setup"
2
+
3
+
4
+ describe('basic tests', function() {
5
+ useDatabase([
6
+ `create table account (id text primary key, wallet text, balance numeric)`,
7
+ `create table historical_balance (id text primary key, account_id text references account(id), balance numeric)`,
8
+ `insert into account (id, wallet, balance) values ('1', 'a', 100)`,
9
+ `insert into account (id, wallet, balance) values ('2', 'b', 200)`,
10
+ `insert into account (id, wallet, balance) values ('3', 'c', 300)`,
11
+ `insert into historical_balance (id, account_id, balance) values ('1-1', '1', 20)`,
12
+ `insert into historical_balance (id, account_id, balance) values ('1-2', '1', 80)`,
13
+ `insert into historical_balance (id, account_id, balance) values ('2-1', '2', 50)`,
14
+ `insert into historical_balance (id, account_id, balance) values ('2-2', '2', 90)`,
15
+ `insert into historical_balance (id, account_id, balance) values ('2-3', '2', 60)`,
16
+ `insert into historical_balance (id, account_id, balance) values ('3-1', '3', 300)`,
17
+ ])
18
+
19
+ const client = useServer(`
20
+ interface HasBalance {
21
+ balance: Int!
22
+ }
23
+
24
+ type Account implements HasBalance @entity {
25
+ id: ID!
26
+ wallet: String!
27
+ balance: Int!
28
+ history: [HistoricalBalance!] @derivedFrom(field: "account")
29
+ }
30
+
31
+ "Historical record of account balance"
32
+ type HistoricalBalance implements HasBalance @entity {
33
+ "Unique identifier"
34
+ id: ID!
35
+
36
+ "Related account"
37
+ account: Account!
38
+
39
+ "Balance"
40
+ balance: Int!
41
+ }
42
+ `)
43
+
44
+ it('can fetch all accounts', function() {
45
+ return client.test(
46
+ `query {
47
+ accounts {
48
+ id
49
+ wallet
50
+ balance
51
+ history { balance }
52
+ }
53
+ }`,
54
+ {
55
+ accounts: [
56
+ {id: '1', wallet: 'a', balance: 100, history: [{balance: 20}, {balance: 80}]},
57
+ {id: '2', wallet: 'b', balance: 200, history: [{balance: 50}, {balance: 90}, {balance: 60}]},
58
+ {id: '3', wallet: 'c', balance: 300, history: [{balance: 300}]},
59
+ ]
60
+ }
61
+ )
62
+ })
63
+
64
+ it('supports filtering by id', function () {
65
+ return client.test(
66
+ `query {
67
+ accounts(where: {id_eq: "3"}) {
68
+ id
69
+ wallet
70
+ }
71
+ }`,
72
+ {
73
+ accounts: [{id: '3', wallet: 'c'}]
74
+ }
75
+ )
76
+ })
77
+
78
+ it('supports by id query', function () {
79
+ return client.test(
80
+ `query {
81
+ a3: accountById(id: "3") {
82
+ id
83
+ wallet
84
+ }
85
+ nonexistent: accountById(id: "foo") {
86
+ id
87
+ wallet
88
+ }
89
+ }`,
90
+ {
91
+ a3: {id: '3', wallet: 'c'},
92
+ nonexistent: null
93
+ }
94
+ )
95
+ })
96
+
97
+ it('supports by unique input query', function () {
98
+ return client.test(
99
+ `query {
100
+ a2: accountByUniqueInput(where: {id: "2"}) {
101
+ id
102
+ wallet
103
+ }
104
+ nonexistent: accountByUniqueInput(where: {id: "foo"}) {
105
+ id
106
+ wallet
107
+ }
108
+ }`,
109
+ {
110
+ a2: {id: '2', wallet: 'b'},
111
+ nonexistent: null
112
+ }
113
+ )
114
+ })
115
+
116
+ it('can fetch deep relations', function () {
117
+ return client.test(
118
+ `query {
119
+ accounts(where: {id_eq: "3"}) {
120
+ id
121
+ history {
122
+ id
123
+ account {
124
+ wallet
125
+ history {
126
+ balance
127
+ account {
128
+ id
129
+ }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }`,
135
+ {
136
+ accounts: [{
137
+ id: '3',
138
+ history: [{
139
+ id: '3-1',
140
+ account: {
141
+ wallet: 'c',
142
+ history: [{
143
+ balance: 300,
144
+ account: {
145
+ id: '3'
146
+ }
147
+ }]
148
+ }
149
+ }]
150
+ }]
151
+ }
152
+ )
153
+ })
154
+
155
+ it('supports *_some filter', function () {
156
+ return client.test(
157
+ `query {
158
+ accounts(where: {history_some: {balance_lt: 50}}) {
159
+ id
160
+ }
161
+ }`,
162
+ {
163
+ accounts: [{id: '1'}]
164
+ }
165
+ )
166
+ })
167
+
168
+ it('supports *_every filter', function () {
169
+ return client.test(
170
+ `query {
171
+ accounts(where: {history_every: {balance_gt: 20}}) {
172
+ wallet
173
+ }
174
+ }`,
175
+ {
176
+ accounts: [{wallet: 'b'}, {wallet: 'c'}]
177
+ }
178
+ )
179
+ })
180
+
181
+ it('supports *_none filter', function () {
182
+ return client.test(
183
+ `query {
184
+ accounts(where: {history_none: {balance_lt: 60}}) {
185
+ wallet
186
+ }
187
+ }`,
188
+ {
189
+ accounts: [{wallet: 'c'}]
190
+ }
191
+ )
192
+ })
193
+
194
+ it('supports gql aliases', function () {
195
+ return client.test(
196
+ `query {
197
+ accounts(where: {id_eq: "1"}) {
198
+ balance
199
+ bag: wallet
200
+ purse: wallet
201
+ payment1: history(where: {id_eq: "1-1"}) {
202
+ balance
203
+ }
204
+ payment2: history(where: {id_eq: "1-2"}) {
205
+ balance
206
+ }
207
+ }
208
+ }`,
209
+ {
210
+ accounts: [{
211
+ balance: 100,
212
+ bag: 'a',
213
+ purse: 'a',
214
+ payment1: [{balance: 20}],
215
+ payment2: [{balance: 80}]
216
+ }]
217
+ }
218
+ )
219
+ })
220
+
221
+ it('supports gql fragments', function () {
222
+ return client.test(
223
+ `query {
224
+ accounts(where: {id_eq: "1"}) {
225
+ ...accountFields
226
+ history {
227
+ ...historicalBalance
228
+ }
229
+ }
230
+ }
231
+
232
+ fragment accountFields on Account {
233
+ id
234
+ wallet
235
+ }
236
+
237
+ fragment historicalBalance on HistoricalBalance {
238
+ balance
239
+ }`,
240
+ {
241
+ accounts: [{
242
+ id: '1',
243
+ wallet: 'a',
244
+ history: [{balance: 20}, {balance: 80}]
245
+ }]
246
+ }
247
+ )
248
+ })
249
+
250
+ it('supports gql fragments on interfaces', function () {
251
+ return client.test(
252
+ `query {
253
+ accounts(where: {id_eq: "1"}) {
254
+ ...balance
255
+ history {
256
+ ...balance
257
+ }
258
+ }
259
+ }
260
+
261
+ fragment balance on HasBalance {
262
+ ... on Account {
263
+ accountBalance: balance
264
+ }
265
+ ... on HistoricalBalance {
266
+ payment: balance
267
+ }
268
+ }`,
269
+ {
270
+ accounts: [{
271
+ accountBalance: 100,
272
+ history: [{payment: 20}, {payment: 80}]
273
+ }]
274
+ }
275
+ )
276
+ })
277
+
278
+ it('supports sorting', function () {
279
+ return client.test(
280
+ `query {
281
+ historicalBalances(orderBy: balance_ASC) {
282
+ balance
283
+ }
284
+ }`,
285
+ {
286
+ historicalBalances: [
287
+ {balance: 20},
288
+ {balance: 50},
289
+ {balance: 60},
290
+ {balance: 80},
291
+ {balance: 90},
292
+ {balance: 300}
293
+ ]
294
+ }
295
+ )
296
+ })
297
+
298
+ it('supports sorting by referenced entity field', function () {
299
+ return client.test(
300
+ `query {
301
+ historicalBalances(orderBy: [account_wallet_ASC, balance_DESC]) {
302
+ balance
303
+ }
304
+ }`,
305
+ {
306
+ historicalBalances: [
307
+ {balance: 80},
308
+ {balance: 20},
309
+ {balance: 90},
310
+ {balance: 60},
311
+ {balance: 50},
312
+ {balance: 300}
313
+ ]
314
+ }
315
+ )
316
+ })
317
+
318
+ it('supports descriptions', function () {
319
+ return client.test(`
320
+ query {
321
+ HistoricalBalance: __type(name: "HistoricalBalance") {
322
+ description
323
+ fields {
324
+ description
325
+ }
326
+ }
327
+ }
328
+ `, {
329
+ HistoricalBalance: {
330
+ description: 'Historical record of account balance',
331
+ fields: [
332
+ {description: 'Unique identifier'},
333
+ {description: 'Related account'},
334
+ {description: 'Balance'},
335
+ ]
336
+ }
337
+ })
338
+ })
339
+ })