@platformatic/sql-graphql 0.7.0 → 0.8.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.
@@ -16,7 +16,7 @@ const ascDesc = new graphql.GraphQLEnumType({
16
16
  })
17
17
 
18
18
  function constructGraph (app, entity, opts) {
19
- const primaryKey = camelcase(entity.primaryKey)
19
+ const primaryKeys = Array.from(entity.primaryKeys).map((key) => camelcase(key))
20
20
  const relationalFields = entity.relations
21
21
  .map((relation) => relation.column_name)
22
22
  const entityName = entity.name
@@ -66,7 +66,12 @@ function constructGraph (app, entity, opts) {
66
66
  resolvers.Mutation = resolvers.Mutation || {}
67
67
  loaders.Query = loaders.Query || {}
68
68
 
69
- const getBy = camelcase(['get', singular, 'by', primaryKey])
69
+ const getBy = camelcase(['get', singular, ...(primaryKeys.map((key, i) => {
70
+ if (i === 0) {
71
+ return ['by', key]
72
+ }
73
+ return ['and', key]
74
+ }).flat())].join('_'))
70
75
 
71
76
  const whereArgType = new graphql.GraphQLInputObjectType({
72
77
  name: `${entityName}WhereArguments`,
@@ -81,6 +86,7 @@ function constructGraph (app, entity, opts) {
81
86
  gte: { type: fields[field].type },
82
87
  lt: { type: fields[field].type },
83
88
  lte: { type: fields[field].type },
89
+ like: { type: fields[field].type },
84
90
  in: { type: new graphql.GraphQLList(fields[field].type) },
85
91
  nin: { type: new graphql.GraphQLList(fields[field].type) }
86
92
  }
@@ -93,14 +99,21 @@ function constructGraph (app, entity, opts) {
93
99
  queryTopFields[getBy] = {
94
100
  type,
95
101
  args: {
96
- [primaryKey]: { type: new graphql.GraphQLNonNull(fields[primaryKey].type) }
102
+ ...(primaryKeys.reduce((acc, primaryKey) => {
103
+ acc[primaryKey] = { type: new graphql.GraphQLNonNull(fields[primaryKey].type) }
104
+ return acc
105
+ }, {}))
97
106
  }
98
107
  }
99
108
  loaders.Query[getBy] = {
100
109
  loader (queries, ctx) {
101
110
  const keys = []
102
111
  for (const query of queries) {
103
- keys.push(query.params[primaryKey])
112
+ const pairs = []
113
+ for (const key of primaryKeys) {
114
+ pairs.push({ key, value: query.params[key] })
115
+ }
116
+ keys.push(pairs)
104
117
  }
105
118
  return loadMany(keys, queries, ctx)
106
119
  },
@@ -138,7 +151,9 @@ function constructGraph (app, entity, opts) {
138
151
 
139
152
  resolvers.Query[plural] = (_, query, ctx, info) => {
140
153
  const requestedFields = info.fieldNodes[0].selectionSet.selections.map((s) => s.name.value)
141
- requestedFields.push(primaryKey)
154
+ for (const primaryKey of primaryKeys) {
155
+ requestedFields.push(primaryKey)
156
+ }
142
157
  return entity.find({ ...query, fields: [...requestedFields, ...relationalFields], ctx })
143
158
  }
144
159
 
@@ -172,7 +187,9 @@ function constructGraph (app, entity, opts) {
172
187
 
173
188
  resolvers.Query[count] = async (_, query, ctx, info) => {
174
189
  const requestedFields = info.fieldNodes[0].selectionSet.selections.map((s) => s.name.value)
175
- requestedFields.push(primaryKey)
190
+ for (const primaryKey of primaryKeys) {
191
+ requestedFields.push(primaryKey)
192
+ }
176
193
  const total = await entity.count({ ...query, fields: [...requestedFields, ...relationalFields], ctx })
177
194
  return { total }
178
195
  }
@@ -220,14 +237,21 @@ function constructGraph (app, entity, opts) {
220
237
 
221
238
  federationReplacements.push({
222
239
  find: new RegExp(`type ${entityName}`),
223
- replace: `type ${entityName} @key(fields: "${primaryKey}")`
240
+ replace: `type ${entityName} @key(fields: "${primaryKeys}")`
224
241
  })
225
242
 
226
243
  if (federationMetadata) {
227
244
  loaders[entityName] = loaders[entityName] || {}
228
245
  loaders[entityName].__resolveReference = {
229
246
  loader (queries, ctx) {
230
- const keys = queries.map(({ obj }) => obj[primaryKey])
247
+ const keys = []
248
+ for (const { obj } of queries) {
249
+ const pairs = []
250
+ for (const key of primaryKeys) {
251
+ pairs.push({ key, value: obj[key] })
252
+ }
253
+ keys.push(pairs)
254
+ }
231
255
  return loadMany(keys, queries, ctx)
232
256
  },
233
257
  opts: {
@@ -246,25 +270,47 @@ function constructGraph (app, entity, opts) {
246
270
 
247
271
  async function loadMany (keys, queries, ctx) {
248
272
  const fields = getFields(queries)
249
- const res = await entity.find({
250
- where: {
251
- [primaryKey]: {
252
- in: keys
273
+
274
+ // TODO this is inefficient as it might load
275
+ // more data than needed if there are more than
276
+ // one primary key
277
+ const where = keys.reduce((acc, pairs) => {
278
+ pairs.reduce((acc, { key, value }) => {
279
+ if (acc[key]) {
280
+ acc[key].in.push(value)
281
+ } else {
282
+ acc[key] = {
283
+ in: [value]
284
+ }
253
285
  }
254
- },
286
+ return acc
287
+ }, acc)
288
+ return acc
289
+ }, {})
290
+
291
+ const res = await entity.find({
292
+ where,
255
293
  fields,
256
294
  ctx
257
295
  })
258
296
 
259
- const map = {}
260
-
261
- for (const row of res) {
262
- map[row[primaryKey]] = row
263
- }
264
-
265
297
  const output = []
266
- for (const key of keys) {
267
- output.push(map[key])
298
+ // TODO this is extremely inefficient
299
+ // we need a better data structure
300
+ for (const pair of keys) {
301
+ for (const row of res) {
302
+ let target = row
303
+ for (const { key, value } of pair) {
304
+ if (row[key] !== value) {
305
+ target = null
306
+ break
307
+ }
308
+ }
309
+
310
+ if (target) {
311
+ output.push(target)
312
+ }
313
+ }
268
314
  }
269
315
 
270
316
  return output
@@ -272,7 +318,9 @@ function constructGraph (app, entity, opts) {
272
318
 
273
319
  function getFields (queries) {
274
320
  const fields = new Set([...relationalFields])
275
- fields.add(primaryKey)
321
+ for (const primaryKey of primaryKeys) {
322
+ fields.add(primaryKey)
323
+ }
276
324
  for (const query of queries) {
277
325
  fromSelectionSet(query.info.fieldNodes[0].selectionSet, fields)
278
326
  }
@@ -15,7 +15,7 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
15
15
  const entity = entities[key]
16
16
  tablesTypeMap[entity.table] = metaMap.get(entity)
17
17
  }
18
- for (const { table_name, foreign_table_name, column_name } of relations) {
18
+ for (const { table_name, foreign_table_name, column_name, foreign_column_name } of relations) {
19
19
  const enhanceAssertLogMsg = `(table: "${table_name}", foreign table: "${foreign_table_name}", column: "${column_name}")`
20
20
 
21
21
  assert(table_name, `table_name is required ${enhanceAssertLogMsg}`)
@@ -34,11 +34,13 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
34
34
  const originalField = camelcase(column_name)
35
35
  delete current.fields[originalField]
36
36
  loaders[current.type] = loaders[current.type] || resolvers[current.type] || {}
37
+ const key = camelcase(foreign_column_name)
37
38
  loaders[current.type][lowered] = {
38
39
  loader (queries, ctx) {
39
- const keys = queries.map(({ obj }) => {
40
- return obj[originalField]
41
- })
40
+ const keys = []
41
+ for (const { obj } of queries) {
42
+ keys.push([{ key, value: obj[originalField].toString() }])
43
+ }
42
44
  return foreign.loadMany(keys, queries, ctx)
43
45
  },
44
46
  opts: {
@@ -48,8 +50,9 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
48
50
  }
49
51
  }
50
52
 
51
- // foreign to current
52
- {
53
+ // foreign to current, we skip this if the current table has a composite primary key
54
+ // TODO implement support for this case
55
+ if (current.entity.primaryKeys.size === 1) {
53
56
  const lowered = lowerCaseFirst(camelcase(current.entity.table))
54
57
  if (!relationships[foreign.type] || relationships[foreign.type][lowered] !== false) {
55
58
  foreign.fields[lowered] = queryTopFields[lowered]
@@ -9,6 +9,14 @@ function setupSubscriptions (app, metaMap, resolvers, ignores) {
9
9
  if (ignores.indexOf(field.singularName) >= 0) {
10
10
  continue
11
11
  }
12
+ // TODO currently we are not supporting subscriptions for
13
+ // entities that have a composite primary key
14
+ /* istanbul ignore next */
15
+ if (field.primaryKeys.size !== 1) {
16
+ continue
17
+ }
18
+ const entity = app.platformatic.entities[field.singularName]
19
+ const primaryKey = entity.primaryKeys.values().next().value
12
20
  const { type } = meta
13
21
  const saved = `${field.singularName}Saved`
14
22
  fields[saved] = {
@@ -18,11 +26,9 @@ function setupSubscriptions (app, metaMap, resolvers, ignores) {
18
26
  subscribe: async function * (_, query, ctx, info) {
19
27
  const log = ctx.reply.request.log
20
28
  const { pubsub } = ctx
21
- const entity = app.platformatic.entities[field.singularName]
22
29
  const topic = await entity.getSubscriptionTopic({ action: 'save', ctx })
23
30
  log.trace({ topic }, 'subscribed')
24
31
  const res = await pubsub.subscribe(topic)
25
- const primaryKey = entity.primaryKey
26
32
  const fields = meta.getFields([{ info }])
27
33
 
28
34
  for await (const msg of res) {
@@ -55,8 +61,8 @@ function setupSubscriptions (app, metaMap, resolvers, ignores) {
55
61
  type: new graphql.GraphQLObjectType({
56
62
  name: `${field.name}Deleted`,
57
63
  fields: {
58
- [field.primaryKey]: {
59
- type: meta.fields[field.primaryKey].type
64
+ [primaryKey]: {
65
+ type: meta.fields[primaryKey].type
60
66
  }
61
67
  }
62
68
  })
@@ -64,7 +70,6 @@ function setupSubscriptions (app, metaMap, resolvers, ignores) {
64
70
  resolvers.Subscription[deleted] = {
65
71
  subscribe: async (_, query, ctx, info) => {
66
72
  const { pubsub } = ctx
67
- const entity = app.platformatic.entities[field.singularName]
68
73
  const topic = await entity.getSubscriptionTopic({ action: 'delete', ctx })
69
74
  const res = await pubsub.subscribe(topic)
70
75
  ctx.reply.request.log.trace({ topic }, 'subscribed')
package/lib/utils.js CHANGED
@@ -54,6 +54,8 @@ function sqlTypeToGraphQL (sqlType) {
54
54
  return graphql.GraphQLString
55
55
  case 'json':
56
56
  return GraphQLJSONObject
57
+ case 'jsonb':
58
+ return GraphQLJSONObject
57
59
  default:
58
60
  return graphql.GraphQLString
59
61
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/sql-graphql",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Map SQL dbs to GraphQL",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -14,8 +14,8 @@
14
14
  },
15
15
  "homepage": "https://github.com/platformatic/platformatic#readme",
16
16
  "devDependencies": {
17
- "@platformatic/sql-events": "0.7.0",
18
- "@platformatic/sql-mapper": "0.7.0",
17
+ "@platformatic/sql-events": "0.8.0",
18
+ "@platformatic/sql-mapper": "0.8.0",
19
19
  "fastify": "^4.6.0",
20
20
  "snazzy": "^9.0.0",
21
21
  "standard": "^17.0.0",
@@ -0,0 +1,331 @@
1
+ 'use strict'
2
+
3
+ const { clear, connInfo, isSQLite, isMysql, isPg } = require('./helper')
4
+ const { test } = require('tap')
5
+ const fastify = require('fastify')
6
+ const sqlMapper = require('@platformatic/sql-mapper')
7
+ const sqlGraphQL = require('..')
8
+
9
+ test('composite primary keys', async ({ equal, same, teardown, rejects }) => {
10
+ /* https://github.com/platformatic/platformatic/issues/299 */
11
+ async function onDatabaseLoad (db, sql) {
12
+ await clear(db, sql)
13
+
14
+ if (isSQLite) {
15
+ await db.query(sql`CREATE TABLE pages (
16
+ id INTEGER PRIMARY KEY,
17
+ the_title VARCHAR(42)
18
+ );`)
19
+
20
+ await db.query(sql`CREATE TABLE users (
21
+ id INTEGER PRIMARY KEY,
22
+ username VARCHAR(255) NOT NULL
23
+ );`)
24
+
25
+ await db.query(sql`CREATE TABLE editors (
26
+ page_id INTEGER NOT NULL,
27
+ user_id INTEGER NOT NULL,
28
+ role VARCHAR(255) NOT NULL,
29
+ CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
30
+ CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
31
+ PRIMARY KEY (page_id, user_id)
32
+ );`)
33
+ } else if (isPg) {
34
+ await db.query(sql`CREATE TABLE pages (
35
+ id SERIAL PRIMARY KEY,
36
+ the_title VARCHAR(255) NOT NULL
37
+ );`)
38
+
39
+ await db.query(sql`CREATE TABLE users (
40
+ id SERIAL PRIMARY KEY,
41
+ username VARCHAR(255) NOT NULL
42
+ );`)
43
+
44
+ await db.query(sql`CREATE TABLE editors (
45
+ page_id INTEGER NOT NULL,
46
+ user_id INTEGER NOT NULL,
47
+ role VARCHAR(255) NOT NULL,
48
+ CONSTRAINT fk_editor_pages FOREIGN KEY (page_id) REFERENCES pages(id),
49
+ CONSTRAINT fk_editor_users FOREIGN KEY (user_id) REFERENCES users(id),
50
+ PRIMARY KEY (page_id, user_id)
51
+ );`)
52
+ } else if (isMysql) {
53
+ await db.query(sql`CREATE TABLE pages (
54
+ id INTEGER PRIMARY KEY AUTO_INCREMENT,
55
+ the_title VARCHAR(255) NOT NULL
56
+ );`)
57
+
58
+ await db.query(sql`CREATE TABLE users (
59
+ id INTEGER PRIMARY KEY AUTO_INCREMENT,
60
+ username VARCHAR(255) NOT NULL
61
+ );`)
62
+
63
+ await db.query(sql`CREATE TABLE editors (
64
+ page_id INTEGER NOT NULL,
65
+ user_id INTEGER NOT NULL,
66
+ role VARCHAR(255) NOT NULL,
67
+ CONSTRAINT \`fk_editor_pages\` FOREIGN KEY (page_id) REFERENCES pages (id) ON DELETE CASCADE ON UPDATE RESTRICT,
68
+ CONSTRAINT \`fk_editor_users\` FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT,
69
+ PRIMARY KEY (page_id, user_id)
70
+ );`)
71
+ }
72
+ }
73
+
74
+ const app = fastify()
75
+ app.register(sqlMapper, {
76
+ ...connInfo,
77
+ onDatabaseLoad
78
+ })
79
+ app.register(sqlGraphQL)
80
+ teardown(app.close.bind(app))
81
+
82
+ await app.ready()
83
+
84
+ {
85
+ const res = await app.inject({
86
+ method: 'POST',
87
+ url: '/graphql',
88
+ body: {
89
+ query: `
90
+ mutation {
91
+ savePage(input: { theTitle: "foobar" }) {
92
+ id
93
+ theTitle
94
+ }
95
+ }
96
+ `
97
+ }
98
+ })
99
+ equal(res.statusCode, 200, 'savePage status code')
100
+ same(res.json(), {
101
+ data: {
102
+ savePage: {
103
+ id: 1,
104
+ theTitle: 'foobar'
105
+ }
106
+ }
107
+ }, 'savePage response')
108
+ }
109
+
110
+ {
111
+ const res = await app.inject({
112
+ method: 'POST',
113
+ url: '/graphql',
114
+ body: {
115
+ query: `
116
+ mutation {
117
+ saveUser(input: { username: "mcollina" }) {
118
+ id
119
+ username
120
+ }
121
+ }
122
+ `
123
+ }
124
+ })
125
+ equal(res.statusCode, 200, 'saveUser status code')
126
+ same(res.json(), {
127
+ data: {
128
+ saveUser: {
129
+ id: 1,
130
+ username: 'mcollina'
131
+ }
132
+ }
133
+ }, 'saveUser response')
134
+ }
135
+
136
+ {
137
+ const res = await app.inject({
138
+ method: 'POST',
139
+ url: '/graphql',
140
+ body: {
141
+ query: `
142
+ mutation {
143
+ saveUser(input: { username: "lucamaraschi" }) {
144
+ id
145
+ username
146
+ }
147
+ }
148
+ `
149
+ }
150
+ })
151
+ equal(res.statusCode, 200, 'saveUser status code')
152
+ same(res.json(), {
153
+ data: {
154
+ saveUser: {
155
+ id: 2,
156
+ username: 'lucamaraschi'
157
+ }
158
+ }
159
+ }, 'saveUser response')
160
+ }
161
+
162
+ {
163
+ const res = await app.inject({
164
+ method: 'POST',
165
+ url: '/graphql',
166
+ body: {
167
+ query: `
168
+ mutation {
169
+ saveEditor(input: { userId: "1", pageId: "1", role: "admin" }) {
170
+ user {
171
+ id
172
+ username
173
+ }
174
+ page {
175
+ id
176
+ theTitle
177
+ }
178
+ role
179
+ }
180
+ }
181
+ `
182
+ }
183
+ })
184
+ equal(res.statusCode, 200, 'saveEditor status code')
185
+ same(res.json(), {
186
+ data: {
187
+ saveEditor: {
188
+ user: {
189
+ id: 1,
190
+ username: 'mcollina'
191
+ },
192
+ page: {
193
+ id: 1,
194
+ theTitle: 'foobar'
195
+ },
196
+ role: 'admin'
197
+ }
198
+ }
199
+ }, 'saveEditor response')
200
+ }
201
+
202
+ {
203
+ const res = await app.inject({
204
+ method: 'POST',
205
+ url: '/graphql',
206
+ body: {
207
+ query: `
208
+ mutation {
209
+ saveEditor(input: { userId: "2", pageId: "1", role: "author" }) {
210
+ user {
211
+ id
212
+ username
213
+ }
214
+ page {
215
+ id
216
+ theTitle
217
+ }
218
+ role
219
+ }
220
+ }
221
+ `
222
+ }
223
+ })
224
+ equal(res.statusCode, 200, 'saveEditor status code')
225
+ same(res.json(), {
226
+ data: {
227
+ saveEditor: {
228
+ user: {
229
+ id: 2,
230
+ username: 'lucamaraschi'
231
+ },
232
+ page: {
233
+ id: 1,
234
+ theTitle: 'foobar'
235
+ },
236
+ role: 'author'
237
+ }
238
+ }
239
+ }, 'saveEditor response')
240
+ }
241
+
242
+ {
243
+ const res = await app.inject({
244
+ method: 'POST',
245
+ url: '/graphql',
246
+ body: {
247
+ query: `
248
+ mutation {
249
+ saveEditor(input: { userId: "1", pageId: "1", role: "captain" }) {
250
+ user {
251
+ id
252
+ username
253
+ }
254
+ page {
255
+ id
256
+ theTitle
257
+ }
258
+ role
259
+ }
260
+ }
261
+ `
262
+ }
263
+ })
264
+ equal(res.statusCode, 200, 'saveEditor status code')
265
+ same(res.json(), {
266
+ data: {
267
+ saveEditor: {
268
+ user: {
269
+ id: 1,
270
+ username: 'mcollina'
271
+ },
272
+ page: {
273
+ id: 1,
274
+ theTitle: 'foobar'
275
+ },
276
+ role: 'captain'
277
+ }
278
+ }
279
+ }, 'saveEditor response')
280
+ }
281
+
282
+ {
283
+ const res = await app.inject({
284
+ method: 'post',
285
+ url: '/graphql',
286
+ body: {
287
+ query: `
288
+ query {
289
+ editors(orderBy: { field: role, direction: DESC }) {
290
+ user {
291
+ id
292
+ username
293
+ }
294
+ page {
295
+ id
296
+ theTitle
297
+ }
298
+ role
299
+ }
300
+ }
301
+ `
302
+ }
303
+ })
304
+ equal(res.statusCode, 200, 'editors status code')
305
+ same(res.json(), {
306
+ data: {
307
+ editors: [{
308
+ user: {
309
+ id: '1',
310
+ username: 'mcollina'
311
+ },
312
+ page: {
313
+ id: '1',
314
+ theTitle: 'foobar'
315
+ },
316
+ role: 'captain'
317
+ }, {
318
+ user: {
319
+ id: '2',
320
+ username: 'lucamaraschi'
321
+ },
322
+ page: {
323
+ id: '1',
324
+ theTitle: 'foobar'
325
+ },
326
+ role: 'author'
327
+ }]
328
+ }
329
+ }, 'editor response')
330
+ }
331
+ })
package/test/helper.js CHANGED
@@ -30,6 +30,11 @@ if (!process.env.DB || process.env.DB === 'postgresql') {
30
30
  module.exports.connInfo = connInfo
31
31
 
32
32
  module.exports.clear = async function (db, sql) {
33
+ try {
34
+ await db.query(sql`DROP TABLE editors`)
35
+ } catch (err) {
36
+ }
37
+
33
38
  try {
34
39
  await db.query(sql`DROP TABLE pages`)
35
40
  } catch (err) {
@@ -4,7 +4,7 @@ const { test } = require('tap')
4
4
  const sqlGraphQL = require('..')
5
5
  const sqlMapper = require('@platformatic/sql-mapper')
6
6
  const fastify = require('fastify')
7
- const { clear, connInfo, isSQLite, isMysql, isMariaDB } = require('./helper')
7
+ const { clear, connInfo, isSQLite, isMysql, isMariaDB, isPg } = require('./helper')
8
8
 
9
9
  async function createBasicPages (db, sql) {
10
10
  if (isSQLite) {
@@ -901,3 +901,59 @@ test('deserialize JSON columns', { skip: isSQLite }, async (t) => {
901
901
  same(json.data.getPageById.metadata, jsonData)
902
902
  }
903
903
  })
904
+
905
+ test('deserialize JSONB columns', { skip: !isPg }, async (t) => {
906
+ const { pass, teardown, same } = t
907
+ const app = fastify()
908
+ const jsonData = {
909
+ foo: 'bar',
910
+ baz: 42,
911
+ items: ['foo', 'bar'],
912
+ nested: {
913
+ hello: 'world'
914
+ }
915
+ }
916
+ app.register(sqlMapper, {
917
+ ...connInfo,
918
+ async onDatabaseLoad (db, sql) {
919
+ pass('onDatabaseLoad called')
920
+
921
+ await clear(db, sql)
922
+
923
+ await db.query(sql`CREATE TABLE pages (
924
+ id SERIAL PRIMARY KEY,
925
+ title VARCHAR(255) NOT NULL,
926
+ metadata JSONB NOT NULL
927
+ );`)
928
+
929
+ await db.query(sql`INSERT INTO pages (id, title, metadata) VALUES (1, 'Hello World', ${JSON.stringify(jsonData)})`)
930
+ }
931
+ })
932
+ app.register(sqlGraphQL, {
933
+ graphiql: false
934
+ })
935
+ teardown(app.close.bind(app))
936
+
937
+ await app.ready()
938
+ const res = await app.inject({
939
+ method: 'POST',
940
+ url: '/graphql',
941
+ body: {
942
+ query: `
943
+ query {
944
+ getPageById(id: 1) {
945
+ id
946
+ title
947
+ metadata
948
+ }
949
+ }
950
+ `
951
+ }
952
+ })
953
+ const json = res.json()
954
+ if (isMariaDB) {
955
+ same(json.data.getPageById.metadata, JSON.stringify(jsonData))
956
+ } else {
957
+ same(json.data.getPageById.metadata, jsonData)
958
+ }
959
+ })
@@ -33,7 +33,7 @@ function createWebSocketClient (t, app) {
33
33
  return { client, ws }
34
34
  }
35
35
 
36
- test('subscription - crud', { only: true }, async t => {
36
+ test('subscription - crud', async t => {
37
37
  const app = Fastify()
38
38
  t.teardown(() => app.close())
39
39
 
@@ -812,3 +812,291 @@ test('delete all', async ({ pass, teardown, same, equal }) => {
812
812
  }, 'posts response')
813
813
  }
814
814
  })
815
+
816
+ test('like', async ({ pass, teardown, same, equal }) => {
817
+ const app = fastify()
818
+ app.register(sqlMapper, {
819
+ ...connInfo,
820
+ async onDatabaseLoad (db, sql) {
821
+ pass('onDatabaseLoad called')
822
+
823
+ await clear(db, sql)
824
+
825
+ if (isSQLite) {
826
+ await db.query(sql`CREATE TABLE posts (
827
+ id INTEGER PRIMARY KEY,
828
+ title VARCHAR(42),
829
+ long_text TEXT,
830
+ counter INTEGER
831
+ );`)
832
+ } else {
833
+ await db.query(sql`CREATE TABLE posts (
834
+ id SERIAL PRIMARY KEY,
835
+ title VARCHAR(42),
836
+ long_text TEXT,
837
+ counter INTEGER
838
+ );`)
839
+ }
840
+ }
841
+ })
842
+ app.register(sqlGraphQL)
843
+ teardown(app.close.bind(app))
844
+
845
+ await app.ready()
846
+
847
+ const posts = [{
848
+ title: 'Dog',
849
+ longText: 'The dog barks',
850
+ counter: 10
851
+ }, {
852
+ title: 'Cat',
853
+ longText: 'The cat meows',
854
+ counter: 20
855
+ }, {
856
+ title: 'Mouse',
857
+ longText: 'The mouse likes cheese',
858
+ counter: 30
859
+ }, {
860
+ title: 'Duck',
861
+ longText: 'A duck tale',
862
+ counter: 40
863
+ }]
864
+
865
+ await app.inject({
866
+ method: 'POST',
867
+ url: '/graphql',
868
+ body: {
869
+ query: `
870
+ mutation batch($inputs : [PostInput]!) {
871
+ insertPosts(inputs: $inputs) {
872
+ id
873
+ title
874
+ }
875
+ }
876
+ `,
877
+ variables: {
878
+ inputs: posts
879
+ }
880
+ }
881
+ })
882
+
883
+ {
884
+ const res = await app.inject({
885
+ method: 'POST',
886
+ url: '/graphql',
887
+ body: {
888
+ query: `
889
+ query {
890
+ posts {
891
+ id
892
+ title
893
+ longText
894
+ counter
895
+ }
896
+ }
897
+ `
898
+ }
899
+ })
900
+ equal(res.statusCode, 200, 'posts status code')
901
+ same(res.json(), {
902
+ data: {
903
+ posts: [{
904
+ id: '1',
905
+ title: 'Dog',
906
+ longText: 'The dog barks',
907
+ counter: 10
908
+ }, {
909
+ id: '2',
910
+ title: 'Cat',
911
+ longText: 'The cat meows',
912
+ counter: 20
913
+ }, {
914
+ id: '3',
915
+ title: 'Mouse',
916
+ longText: 'The mouse likes cheese',
917
+ counter: 30
918
+ }, {
919
+ id: '4',
920
+ title: 'Duck',
921
+ longText: 'A duck tale',
922
+ counter: 40
923
+ }]
924
+ }
925
+ }, 'posts response')
926
+ }
927
+
928
+ {
929
+ const res = await app.inject({
930
+ method: 'POST',
931
+ url: '/graphql',
932
+ body: {
933
+ query: `
934
+ query {
935
+ posts(where: { longText: { like: "The%" } }) {
936
+ id
937
+ title
938
+ longText
939
+ counter
940
+ }
941
+ }
942
+ `
943
+ }
944
+ })
945
+ equal(res.statusCode, 200, 'where: { longText: { like: "The%" } } status code')
946
+ same(res.json(), {
947
+ data: {
948
+ posts: [{
949
+ id: '1',
950
+ title: 'Dog',
951
+ longText: 'The dog barks',
952
+ counter: 10
953
+ }, {
954
+ id: '2',
955
+ title: 'Cat',
956
+ longText: 'The cat meows',
957
+ counter: 20
958
+ }, {
959
+ id: '3',
960
+ title: 'Mouse',
961
+ longText: 'The mouse likes cheese',
962
+ counter: 30
963
+ }]
964
+ }
965
+ }, 'where: { longText: { like: "The%" } } response')
966
+ }
967
+
968
+ {
969
+ const res = await app.inject({
970
+ method: 'POST',
971
+ url: '/graphql',
972
+ body: {
973
+ query: `
974
+ query {
975
+ posts(where: { longText: { like: "%s" } }) {
976
+ id
977
+ title
978
+ longText
979
+ counter
980
+ }
981
+ }
982
+ `
983
+ }
984
+ })
985
+ equal(res.statusCode, 200, 'where: { longText: { like: "%s" } } status code')
986
+ same(res.json(), {
987
+ data: {
988
+ posts: [{
989
+ id: '1',
990
+ title: 'Dog',
991
+ longText: 'The dog barks',
992
+ counter: 10
993
+ }, {
994
+ id: '2',
995
+ title: 'Cat',
996
+ longText: 'The cat meows',
997
+ counter: 20
998
+ }]
999
+ }
1000
+ }, 'where: { longText: { like: "%s" } } response')
1001
+ }
1002
+
1003
+ {
1004
+ const res = await app.inject({
1005
+ method: 'POST',
1006
+ url: '/graphql',
1007
+ body: {
1008
+ query: `
1009
+ query {
1010
+ posts(where: { longText: { like: "%o%" } }) {
1011
+ id
1012
+ title
1013
+ longText
1014
+ counter
1015
+ }
1016
+ }
1017
+ `
1018
+ }
1019
+ })
1020
+ equal(res.statusCode, 200, 'where: { longText: { like: "%o%" } } status code')
1021
+ same(res.json(), {
1022
+ data: {
1023
+ posts: [{
1024
+ id: '1',
1025
+ title: 'Dog',
1026
+ longText: 'The dog barks',
1027
+ counter: 10
1028
+ }, {
1029
+ id: '2',
1030
+ title: 'Cat',
1031
+ longText: 'The cat meows',
1032
+ counter: 20
1033
+ }, {
1034
+ id: '3',
1035
+ title: 'Mouse',
1036
+ longText: 'The mouse likes cheese',
1037
+ counter: 30
1038
+ }]
1039
+ }
1040
+ }, 'where: { longText: { like: "%o%" } } response')
1041
+ }
1042
+
1043
+ {
1044
+ const res = await app.inject({
1045
+ method: 'POST',
1046
+ url: '/graphql',
1047
+ body: {
1048
+ query: `
1049
+ query {
1050
+ posts(where: { counter: { like: 10 } }) {
1051
+ id
1052
+ title
1053
+ longText
1054
+ counter
1055
+ }
1056
+ }
1057
+ `
1058
+ }
1059
+ })
1060
+ equal(res.statusCode, 200, 'where: { counter: { like: 10 } } status code')
1061
+ same(res.json(), {
1062
+ data: {
1063
+ posts: [{
1064
+ id: '1',
1065
+ title: 'Dog',
1066
+ longText: 'The dog barks',
1067
+ counter: 10
1068
+ }]
1069
+ }
1070
+ }, 'where: { counter: { like: 10 } } response')
1071
+ }
1072
+
1073
+ {
1074
+ const res = await app.inject({
1075
+ method: 'POST',
1076
+ url: '/graphql',
1077
+ body: {
1078
+ query: `
1079
+ query {
1080
+ posts(where: { counter: { like: 40 } }) {
1081
+ id
1082
+ title
1083
+ longText
1084
+ counter
1085
+ }
1086
+ }
1087
+ `
1088
+ }
1089
+ })
1090
+ equal(res.statusCode, 200, 'where: { counter: { like: 40 } } status code')
1091
+ same(res.json(), {
1092
+ data: {
1093
+ posts: [{
1094
+ id: '4',
1095
+ title: 'Duck',
1096
+ longText: 'A duck tale',
1097
+ counter: 40
1098
+ }]
1099
+ }
1100
+ }, 'where: { counter: { like: 40 } } response')
1101
+ }
1102
+ })