@platformatic/sql-graphql 0.6.1 → 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
@@ -36,7 +36,18 @@ function constructGraph (app, entity, opts) {
36
36
  for (const key of Object.keys(entity.fields)) {
37
37
  const field = entity.fields[key]
38
38
  const meta = { field }
39
- meta.type = sqlTypeToGraphQL(field.sqlType)
39
+
40
+ // sqlite doesn't support enums
41
+ /* istanbul ignore next */
42
+ if (field.enum) {
43
+ const enumValues = field.enum.reduce((acc, enumValue) => {
44
+ acc[enumValue] = { value: enumValue }
45
+ return acc
46
+ }, {})
47
+ meta.type = new graphql.GraphQLEnumType({ name: key, values: enumValues })
48
+ } else {
49
+ meta.type = sqlTypeToGraphQL(field.sqlType)
50
+ }
40
51
  if (field.primaryKey) {
41
52
  meta.primaryKeyType = field.type
42
53
  meta.type = graphql.GraphQLID
@@ -55,7 +66,12 @@ function constructGraph (app, entity, opts) {
55
66
  resolvers.Mutation = resolvers.Mutation || {}
56
67
  loaders.Query = loaders.Query || {}
57
68
 
58
- 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('_'))
59
75
 
60
76
  const whereArgType = new graphql.GraphQLInputObjectType({
61
77
  name: `${entityName}WhereArguments`,
@@ -70,6 +86,7 @@ function constructGraph (app, entity, opts) {
70
86
  gte: { type: fields[field].type },
71
87
  lt: { type: fields[field].type },
72
88
  lte: { type: fields[field].type },
89
+ like: { type: fields[field].type },
73
90
  in: { type: new graphql.GraphQLList(fields[field].type) },
74
91
  nin: { type: new graphql.GraphQLList(fields[field].type) }
75
92
  }
@@ -82,14 +99,21 @@ function constructGraph (app, entity, opts) {
82
99
  queryTopFields[getBy] = {
83
100
  type,
84
101
  args: {
85
- [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
+ }, {}))
86
106
  }
87
107
  }
88
108
  loaders.Query[getBy] = {
89
109
  loader (queries, ctx) {
90
110
  const keys = []
91
111
  for (const query of queries) {
92
- 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)
93
117
  }
94
118
  return loadMany(keys, queries, ctx)
95
119
  },
@@ -127,7 +151,9 @@ function constructGraph (app, entity, opts) {
127
151
 
128
152
  resolvers.Query[plural] = (_, query, ctx, info) => {
129
153
  const requestedFields = info.fieldNodes[0].selectionSet.selections.map((s) => s.name.value)
130
- requestedFields.push(primaryKey)
154
+ for (const primaryKey of primaryKeys) {
155
+ requestedFields.push(primaryKey)
156
+ }
131
157
  return entity.find({ ...query, fields: [...requestedFields, ...relationalFields], ctx })
132
158
  }
133
159
 
@@ -161,7 +187,9 @@ function constructGraph (app, entity, opts) {
161
187
 
162
188
  resolvers.Query[count] = async (_, query, ctx, info) => {
163
189
  const requestedFields = info.fieldNodes[0].selectionSet.selections.map((s) => s.name.value)
164
- requestedFields.push(primaryKey)
190
+ for (const primaryKey of primaryKeys) {
191
+ requestedFields.push(primaryKey)
192
+ }
165
193
  const total = await entity.count({ ...query, fields: [...requestedFields, ...relationalFields], ctx })
166
194
  return { total }
167
195
  }
@@ -209,14 +237,21 @@ function constructGraph (app, entity, opts) {
209
237
 
210
238
  federationReplacements.push({
211
239
  find: new RegExp(`type ${entityName}`),
212
- replace: `type ${entityName} @key(fields: "${primaryKey}")`
240
+ replace: `type ${entityName} @key(fields: "${primaryKeys}")`
213
241
  })
214
242
 
215
243
  if (federationMetadata) {
216
244
  loaders[entityName] = loaders[entityName] || {}
217
245
  loaders[entityName].__resolveReference = {
218
246
  loader (queries, ctx) {
219
- 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
+ }
220
255
  return loadMany(keys, queries, ctx)
221
256
  },
222
257
  opts: {
@@ -235,25 +270,47 @@ function constructGraph (app, entity, opts) {
235
270
 
236
271
  async function loadMany (keys, queries, ctx) {
237
272
  const fields = getFields(queries)
238
- const res = await entity.find({
239
- where: {
240
- [primaryKey]: {
241
- 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
+ }
242
285
  }
243
- },
286
+ return acc
287
+ }, acc)
288
+ return acc
289
+ }, {})
290
+
291
+ const res = await entity.find({
292
+ where,
244
293
  fields,
245
294
  ctx
246
295
  })
247
296
 
248
- const map = {}
249
-
250
- for (const row of res) {
251
- map[row[primaryKey]] = row
252
- }
253
-
254
297
  const output = []
255
- for (const key of keys) {
256
- 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
+ }
257
314
  }
258
315
 
259
316
  return output
@@ -261,7 +318,9 @@ function constructGraph (app, entity, opts) {
261
318
 
262
319
  function getFields (queries) {
263
320
  const fields = new Set([...relationalFields])
264
- fields.add(primaryKey)
321
+ for (const primaryKey of primaryKeys) {
322
+ fields.add(primaryKey)
323
+ }
265
324
  for (const query of queries) {
266
325
  fromSelectionSet(query.info.fieldNodes[0].selectionSet, fields)
267
326
  }
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ /* eslint camelcase: 0 */
4
+
3
5
  const camelcase = require('camelcase')
4
6
  const {
5
7
  fromSelectionSet
@@ -13,28 +15,32 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
13
15
  const entity = entities[key]
14
16
  tablesTypeMap[entity.table] = metaMap.get(entity)
15
17
  }
16
- for (const relation of relations) {
17
- assert(relation.table_name, 'table_name is required')
18
- assert(relation.foreign_table_name, 'foreign_table_name is required')
19
- assert(relation.column_name.toLowerCase().endsWith('id'), 'The column with id reference must be ended with `id` postfix')
18
+ for (const { table_name, foreign_table_name, column_name, foreign_column_name } of relations) {
19
+ const enhanceAssertLogMsg = `(table: "${table_name}", foreign table: "${foreign_table_name}", column: "${column_name}")`
20
+
21
+ assert(table_name, `table_name is required ${enhanceAssertLogMsg}`)
22
+ assert(foreign_table_name, `foreign_table_name is required ${enhanceAssertLogMsg}`)
23
+ assert(column_name.toLowerCase().endsWith('id'), `The column with id reference must be ended with "id" postfix ${enhanceAssertLogMsg}`)
20
24
 
21
- const current = tablesTypeMap[relation.table_name]
22
- const foreign = tablesTypeMap[relation.foreign_table_name]
23
- assert(foreign !== undefined, `No foreign table named "${relation.foreign_table_name}" was found`)
25
+ const current = tablesTypeMap[table_name]
26
+ const foreign = tablesTypeMap[foreign_table_name]
27
+ assert(foreign !== undefined, `No foreign table named "${foreign_table_name}" was found ${enhanceAssertLogMsg}`)
24
28
 
25
29
  // current to foreign
26
30
  {
27
- const lowered = lowerCaseFirst(camelcase(cutOutIdEnding(relation.column_name)))
31
+ const lowered = lowerCaseFirst(camelcase(cutOutIdEnding(column_name)))
28
32
  if (!relationships[current.type] || relationships[current.type][lowered] !== false) {
29
33
  current.fields[lowered] = { type: foreign.type }
30
- const originalField = camelcase(relation.column_name)
34
+ const originalField = camelcase(column_name)
31
35
  delete current.fields[originalField]
32
36
  loaders[current.type] = loaders[current.type] || resolvers[current.type] || {}
37
+ const key = camelcase(foreign_column_name)
33
38
  loaders[current.type][lowered] = {
34
39
  loader (queries, ctx) {
35
- const keys = queries.map(({ obj }) => {
36
- return obj[originalField]
37
- })
40
+ const keys = []
41
+ for (const { obj } of queries) {
42
+ keys.push([{ key, value: obj[originalField].toString() }])
43
+ }
38
44
  return foreign.loadMany(keys, queries, ctx)
39
45
  },
40
46
  opts: {
@@ -44,17 +50,18 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
44
50
  }
45
51
  }
46
52
 
47
- // foreign to current
48
- {
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) {
49
56
  const lowered = lowerCaseFirst(camelcase(current.entity.table))
50
57
  if (!relationships[foreign.type] || relationships[foreign.type][lowered] !== false) {
51
58
  foreign.fields[lowered] = queryTopFields[lowered]
52
59
  resolvers[foreign.type] = resolvers[foreign.type] || {}
53
60
  const resolveRelation = async function (obj, args, ctx, info) {
54
61
  const fields = fromSelectionSet(info.fieldNodes[0].selectionSet, new Set())
55
- const toSearch = { ...args, fields: [...fields, relation.column_name], ctx }
62
+ const toSearch = { ...args, fields: [...fields, column_name], ctx }
56
63
  toSearch.where = toSearch.where || {}
57
- toSearch.where[camelcase(relation.column_name)] = { eq: obj.id }
64
+ toSearch.where[camelcase(column_name)] = { eq: obj.id }
58
65
  return current.entity.find(toSearch)
59
66
  }
60
67
  if (resolvers[foreign.type][lowered] === undefined) {
@@ -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,21 +1,21 @@
1
1
  {
2
2
  "name": "@platformatic/sql-graphql",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Map SQL dbs to GraphQL",
5
5
  "main": "index.js",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "git+https://github.com/plaformatic/platformatic.git"
8
+ "url": "git+https://github.com/platformatic/platformatic.git"
9
9
  },
10
10
  "author": "Matteo Collina <hello@matteocollina.com>",
11
11
  "license": "Apache-2.0",
12
12
  "bugs": {
13
- "url": "https://github.com/plaformatic/platformatic/issues"
13
+ "url": "https://github.com/platformatic/platformatic/issues"
14
14
  },
15
- "homepage": "https://github.com/plaformatic/platformatic#readme",
15
+ "homepage": "https://github.com/platformatic/platformatic#readme",
16
16
  "devDependencies": {
17
- "@platformatic/sql-events": "0.6.1",
18
- "@platformatic/sql-mapper": "0.6.1",
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
+ })
@@ -15,7 +15,9 @@ test('[PG] simple db simple graphql schema', { skip: !isPg }, async ({ pass, tea
15
15
 
16
16
  await clear(db, sql)
17
17
 
18
- await db.query(sql`CREATE TABLE simple_types (
18
+ await db.query(sql`
19
+ CREATE TYPE simple_enum as ENUM ('value1', 'value2');
20
+ CREATE TABLE simple_types (
19
21
  id SERIAL8 PRIMARY KEY,
20
22
  published BOOL,
21
23
  current DOUBLE PRECISION,
@@ -26,7 +28,8 @@ test('[PG] simple db simple graphql schema', { skip: !isPg }, async ({ pass, tea
26
28
  uuid UUID UNIQUE,
27
29
  a_real real,
28
30
  a_smallint smallint,
29
- a_decimal decimal
31
+ a_decimal decimal,
32
+ an_enum simple_enum
30
33
  );`)
31
34
  }
32
35
  })
@@ -55,7 +58,9 @@ test('[PG] simple db simple graphql schema', { skip: !isPg }, async ({ pass, tea
55
58
  uuid: "12345678-1234-1234-1234-123456789012",
56
59
  aReal: 1.2,
57
60
  aSmallint: 42,
58
- aDecimal: 42 }) {
61
+ aDecimal: 42,
62
+ anEnum: value1
63
+ }) {
59
64
  id
60
65
  published
61
66
  current
@@ -67,6 +72,7 @@ test('[PG] simple db simple graphql schema', { skip: !isPg }, async ({ pass, tea
67
72
  aReal
68
73
  aSmallint
69
74
  aDecimal
75
+ anEnum
70
76
  }
71
77
  }
72
78
  `
@@ -86,7 +92,8 @@ test('[PG] simple db simple graphql schema', { skip: !isPg }, async ({ pass, tea
86
92
  uuid: '12345678-1234-1234-1234-123456789012',
87
93
  aReal: 1.2,
88
94
  aSmallint: 42,
89
- aDecimal: 42
95
+ aDecimal: 42,
96
+ anEnum: 'value1'
90
97
  }
91
98
  }
92
99
  }, 'saveSimpleType response')
@@ -111,6 +118,7 @@ test('[PG] simple db simple graphql schema', { skip: !isPg }, async ({ pass, tea
111
118
  aReal
112
119
  aSmallint
113
120
  aDecimal
121
+ anEnum
114
122
  }
115
123
  }
116
124
  `
@@ -130,7 +138,8 @@ test('[PG] simple db simple graphql schema', { skip: !isPg }, async ({ pass, tea
130
138
  uuid: '12345678-1234-1234-1234-123456789012',
131
139
  aReal: 1.2,
132
140
  aSmallint: 42,
133
- aDecimal: 42
141
+ aDecimal: 42,
142
+ anEnum: 'value1'
134
143
  }
135
144
  }
136
145
  }, 'getSimpleTypeById response')
@@ -262,7 +271,8 @@ test('[MySQL] simple db simple graphql schema', { skip: !isMysql }, async ({ pas
262
271
  uuid ${uuidType} UNIQUE,
263
272
  a_real real,
264
273
  a_smallint smallint,
265
- a_decimal decimal
274
+ a_decimal decimal,
275
+ an_enum enum ('value1', 'value2')
266
276
  );`)
267
277
  }
268
278
  })
@@ -293,7 +303,9 @@ test('[MySQL] simple db simple graphql schema', { skip: !isMysql }, async ({ pas
293
303
  uuid: "12345678-1234-1234-1234-123456789012",
294
304
  aReal: 1.2,
295
305
  aSmallint: 42,
296
- aDecimal: 42 }) {
306
+ aDecimal: 42,
307
+ anEnum: value1
308
+ }) {
297
309
  id
298
310
  published
299
311
  current
@@ -305,6 +317,7 @@ test('[MySQL] simple db simple graphql schema', { skip: !isMysql }, async ({ pas
305
317
  aReal
306
318
  aSmallint
307
319
  aDecimal
320
+ anEnum
308
321
  }
309
322
  }
310
323
  `
@@ -324,7 +337,8 @@ test('[MySQL] simple db simple graphql schema', { skip: !isMysql }, async ({ pas
324
337
  uuid: '12345678-1234-1234-1234-123456789012',
325
338
  aReal: 1.2,
326
339
  aSmallint: 42,
327
- aDecimal: 42
340
+ aDecimal: 42,
341
+ anEnum: 'value1'
328
342
  }
329
343
  }
330
344
  }, 'saveSimpleType response')
@@ -349,6 +363,7 @@ test('[MySQL] simple db simple graphql schema', { skip: !isMysql }, async ({ pas
349
363
  aReal
350
364
  aSmallint
351
365
  aDecimal
366
+ anEnum
352
367
  }
353
368
  }
354
369
  `
@@ -368,7 +383,8 @@ test('[MySQL] simple db simple graphql schema', { skip: !isMysql }, async ({ pas
368
383
  uuid: '12345678-1234-1234-1234-123456789012',
369
384
  aReal: 1.2,
370
385
  aSmallint: 42,
371
- aDecimal: 42
386
+ aDecimal: 42,
387
+ anEnum: 'value1'
372
388
  }
373
389
  }
374
390
  }, 'getSimpleTypeById response')
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) {
@@ -86,4 +91,9 @@ module.exports.clear = async function (db, sql) {
86
91
  await db.query(sql`DROP TABLE individuals`)
87
92
  } catch (err) {
88
93
  }
94
+
95
+ try {
96
+ await db.query(sql`DROP Type simple_enum`)
97
+ } catch (err) {
98
+ }
89
99
  }
@@ -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
+ })