@platformatic/sql-graphql 0.9.2 → 0.11.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/index.js CHANGED
@@ -118,7 +118,21 @@ async function mapperToGraphql (app, opts) {
118
118
  }
119
119
  }
120
120
 
121
- let sdl = graphql.printSchema(new graphql.GraphQLSchema({ query, mutation, subscription }))
121
+ let sdl = ''
122
+ try {
123
+ sdl = graphql.printSchema(new graphql.GraphQLSchema({ query, mutation, subscription }))
124
+ } catch (error) {
125
+ // The next lines are excluded from test coverage:
126
+ // it's quite hard to test the following lines for all of the DB types
127
+ /* istanbul ignore next */
128
+ app.log.debug({ query, mutation, subscription }, 'GraphQL input schema')
129
+ /* istanbul ignore next */
130
+ const newError = new Error('Error printing the GraphQL schema')
131
+ /* istanbul ignore next */
132
+ newError.cause = error
133
+ /* istanbul ignore next */
134
+ throw newError
135
+ }
122
136
 
123
137
  if (opts.federationMetadata) {
124
138
  for (const replacement of federationReplacements) {
@@ -274,6 +274,7 @@ function constructGraph (app, entity, opts, ignore) {
274
274
  entity,
275
275
  loadMany,
276
276
  getFields,
277
+ relationalFields,
277
278
  fields
278
279
  }
279
280
 
@@ -20,19 +20,20 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
20
20
 
21
21
  assert(table_name, `table_name is required ${enhanceAssertLogMsg}`)
22
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}`)
24
23
 
25
24
  const current = tablesTypeMap[table_name]
26
25
  const foreign = tablesTypeMap[foreign_table_name]
27
26
  assert(foreign !== undefined, `No foreign table named "${foreign_table_name}" was found ${enhanceAssertLogMsg}`)
28
27
 
29
- // current to foreign
30
- {
31
- const lowered = lowerCaseFirst(camelcase(cutOutIdEnding(column_name)))
28
+ // current to foreign, we skip this if the foreign table has a composite primary key
29
+ // TODO implement support for this case
30
+ /* istanbul ignore else */
31
+ if (foreign.entity.primaryKeys.size === 1) {
32
+ const lowered = generateRelationshipName(column_name, foreign)
32
33
  if (!relationships[current.type] || relationships[current.type][lowered] !== false) {
33
- current.fields[lowered] = { type: foreign.type }
34
34
  const originalField = camelcase(column_name)
35
35
  delete current.fields[originalField]
36
+ current.fields[lowered] = { type: foreign.type }
36
37
  loaders[current.type] = loaders[current.type] || resolvers[current.type] || {}
37
38
  const key = camelcase(foreign_column_name)
38
39
  loaders[current.type][lowered] = {
@@ -59,7 +60,7 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
59
60
  resolvers[foreign.type] = resolvers[foreign.type] || {}
60
61
  const resolveRelation = async function (obj, args, ctx, info) {
61
62
  const fields = fromSelectionSet(info.fieldNodes[0].selectionSet, new Set())
62
- const toSearch = { ...args, fields: [...fields, column_name], ctx }
63
+ const toSearch = { ...args, fields: [...fields, column_name, ...current.relationalFields], ctx }
63
64
  toSearch.where = toSearch.where || {}
64
65
  toSearch.where[camelcase(column_name)] = { eq: obj.id }
65
66
  return current.entity.find(toSearch)
@@ -84,9 +85,13 @@ function lowerCaseFirst (str) {
84
85
  return str.charAt(0).toLowerCase() + str.slice(1)
85
86
  }
86
87
 
87
- function cutOutIdEnding (str) {
88
+ function generateRelationshipName (str, foreign) {
89
+ const primaryKey = foreign.entity.primaryKeys.values().next().value
88
90
  str = str.toString()
89
- return str.slice(0, str.toLowerCase().lastIndexOf('id'))
91
+ if (str.endsWith('_' + primaryKey)) {
92
+ str = str.slice(0, -2)
93
+ }
94
+ return lowerCaseFirst(camelcase(str))
90
95
  }
91
96
 
92
97
  function findPrimaryColumnName (camelCasedFields) {
package/lib/utils.js CHANGED
@@ -21,11 +21,13 @@ function sqlTypeToGraphQL (sqlType) {
21
21
  case 'decimal':
22
22
  return graphql.GraphQLInt
23
23
  case 'bigint':
24
- return graphql.GraphQLInt
24
+ return graphql.GraphQLString
25
25
  case 'int2':
26
26
  return graphql.GraphQLInt
27
27
  case 'int4':
28
28
  return graphql.GraphQLInt
29
+ case 'int8':
30
+ return graphql.GraphQLString
29
31
  case 'varchar':
30
32
  return graphql.GraphQLString
31
33
  case 'text':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/sql-graphql",
3
- "version": "0.9.2",
3
+ "version": "0.11.0",
4
4
  "description": "Map SQL dbs to GraphQL",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -14,14 +14,14 @@
14
14
  },
15
15
  "homepage": "https://github.com/platformatic/platformatic#readme",
16
16
  "devDependencies": {
17
- "@platformatic/sql-events": "0.9.2",
18
- "@platformatic/sql-mapper": "0.9.2",
19
17
  "fastify": "^4.6.0",
20
18
  "snazzy": "^9.0.0",
21
19
  "standard": "^17.0.0",
22
20
  "tap": "^16.0.0",
23
21
  "tsd": "^0.25.0",
24
- "ws": "^8.9.0"
22
+ "ws": "^8.9.0",
23
+ "@platformatic/sql-events": "0.11.0",
24
+ "@platformatic/sql-mapper": "0.11.0"
25
25
  },
26
26
  "dependencies": {
27
27
  "camelcase": "^6.0.0",
@@ -0,0 +1,517 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const sqlGraphQL = require('..')
5
+ const sqlMapper = require('@platformatic/sql-mapper')
6
+ const fastify = require('fastify')
7
+ const { clear, connInfo, isSQLite, isMysql } = require('./helper')
8
+ const { setTimeout } = require('timers/promises')
9
+
10
+ async function createBasicPages (db, sql) {
11
+ if (isSQLite) {
12
+ await db.query(sql`CREATE TABLE pages (
13
+ id INTEGER PRIMARY KEY,
14
+ title VARCHAR(42),
15
+ created_at TIMESTAMP,
16
+ updated_at TIMESTAMP
17
+ );`)
18
+ } else if (isMysql) {
19
+ await db.query(sql`CREATE TABLE pages (
20
+ id SERIAL PRIMARY KEY,
21
+ title VARCHAR(42),
22
+ created_at TIMESTAMP NULL DEFAULT NULL,
23
+ updated_at TIMESTAMP NULL DEFAULT NULL
24
+ );`)
25
+ } else {
26
+ await db.query(sql`CREATE TABLE pages (
27
+ id SERIAL PRIMARY KEY,
28
+ title VARCHAR(42),
29
+ created_at TIMESTAMP,
30
+ updated_at TIMESTAMP
31
+ );`)
32
+ }
33
+ }
34
+
35
+ test('created_at updated_at happy path', async ({ pass, teardown, same, equal, not, comment }) => {
36
+ const app = fastify()
37
+ app.register(sqlMapper, {
38
+ ...connInfo,
39
+ async onDatabaseLoad (db, sql) {
40
+ pass('onDatabaseLoad called')
41
+
42
+ await clear(db, sql)
43
+ await createBasicPages(db, sql)
44
+ }
45
+ })
46
+ app.register(sqlGraphQL)
47
+ teardown(app.close.bind(app))
48
+
49
+ await app.ready()
50
+
51
+ let original
52
+
53
+ {
54
+ const res = await app.inject({
55
+ method: 'POST',
56
+ url: '/graphql',
57
+ body: {
58
+ query: `
59
+ mutation {
60
+ savePage(input: { title: "Hello" }) {
61
+ id
62
+ title
63
+ createdAt
64
+ updatedAt
65
+ }
66
+ }
67
+ `
68
+ }
69
+ })
70
+ equal(res.statusCode, 200, 'savePage status code')
71
+ const data = res.json().data
72
+ not(data.savePage.createdAt, null, 'createdAt')
73
+ not(data.savePage.updatedAt, null, 'updatedAt')
74
+ comment(`createdAt: ${data.savePage.createdAt}`)
75
+ comment(`updatedAt: ${data.savePage.updatedAt}`)
76
+ original = data.savePage
77
+ }
78
+
79
+ {
80
+ const res = await app.inject({
81
+ method: 'POST',
82
+ url: '/graphql',
83
+ body: {
84
+ query: `
85
+ query {
86
+ getPageById(id: 1) {
87
+ id
88
+ title
89
+ createdAt
90
+ updatedAt
91
+ }
92
+ }
93
+ `
94
+ }
95
+ })
96
+ equal(res.statusCode, 200, 'pages status code')
97
+ const data = res.json().data
98
+ equal(data.getPageById.createdAt, original.createdAt, 'createdAt')
99
+ equal(data.getPageById.updatedAt, original.updatedAt, 'updatedAt')
100
+ comment(`createdAt: ${data.getPageById.createdAt}`)
101
+ comment(`updatedAt: ${data.getPageById.updatedAt}`)
102
+ }
103
+
104
+ await setTimeout(1000) // await 1s
105
+
106
+ let updated
107
+ {
108
+ const res = await app.inject({
109
+ method: 'POST',
110
+ url: '/graphql',
111
+ body: {
112
+ query: `
113
+ mutation {
114
+ savePage(input: { id: 1, title: "Hello World" }) {
115
+ id
116
+ title
117
+ createdAt
118
+ updatedAt
119
+ }
120
+ }
121
+ `
122
+ }
123
+ })
124
+ const data = res.json().data
125
+ equal(data.savePage.createdAt, original.createdAt, 'createdAt')
126
+ not(data.savePage.updatedAt, original.updatedAt, 'updatedAt')
127
+ updated = data.savePage
128
+ comment(`createdAt: ${data.savePage.createdAt}`)
129
+ comment(`updatedAt: ${data.savePage.updatedAt}`)
130
+ }
131
+
132
+ {
133
+ const res = await app.inject({
134
+ method: 'POST',
135
+ url: '/graphql',
136
+ body: {
137
+ query: `
138
+ query {
139
+ getPageById(id: 1) {
140
+ id
141
+ title
142
+ createdAt
143
+ updatedAt
144
+ }
145
+ }
146
+ `
147
+ }
148
+ })
149
+ equal(res.statusCode, 200, 'pages status code')
150
+ const data = res.json().data
151
+ equal(data.getPageById.createdAt, updated.createdAt, 'createdAt')
152
+ equal(data.getPageById.updatedAt, updated.updatedAt, 'updatedAt')
153
+ comment(`createdAt: ${data.getPageById.createdAt}`)
154
+ comment(`updatedAt: ${data.getPageById.updatedAt}`)
155
+ }
156
+ })
157
+
158
+ test('cannot set created_at', async ({ pass, teardown, same, equal, not, comment }) => {
159
+ const app = fastify()
160
+ app.register(sqlMapper, {
161
+ ...connInfo,
162
+ async onDatabaseLoad (db, sql) {
163
+ pass('onDatabaseLoad called')
164
+
165
+ await clear(db, sql)
166
+ await createBasicPages(db, sql)
167
+ }
168
+ })
169
+ app.register(sqlGraphQL)
170
+ teardown(app.close.bind(app))
171
+
172
+ await app.ready()
173
+
174
+ {
175
+ const res = await app.inject({
176
+ method: 'POST',
177
+ url: '/graphql',
178
+ body: {
179
+ query: `
180
+ mutation {
181
+ savePage(input: { title: "Hello", createdAt: "${new Date().toISOString()}" }) {
182
+ id
183
+ title
184
+ createdAt
185
+ updatedAt
186
+ }
187
+ }
188
+ `
189
+ }
190
+ })
191
+ equal(res.statusCode, 400, 'savePage status code')
192
+ const data = res.json()
193
+ equal(data.errors[0].message, 'Field "createdAt" is not defined by type "PageInput".')
194
+ }
195
+ })
196
+
197
+ test('cannot set updated_at', async ({ pass, teardown, same, equal, not, comment }) => {
198
+ const app = fastify()
199
+ app.register(sqlMapper, {
200
+ ...connInfo,
201
+ async onDatabaseLoad (db, sql) {
202
+ pass('onDatabaseLoad called')
203
+
204
+ await clear(db, sql)
205
+ await createBasicPages(db, sql)
206
+ }
207
+ })
208
+ app.register(sqlGraphQL)
209
+ teardown(app.close.bind(app))
210
+
211
+ await app.ready()
212
+
213
+ {
214
+ const res = await app.inject({
215
+ method: 'POST',
216
+ url: '/graphql',
217
+ body: {
218
+ query: `
219
+ mutation {
220
+ savePage(input: { title: "Hello" }) {
221
+ id
222
+ title
223
+ createdAt
224
+ updatedAt
225
+ }
226
+ }
227
+ `
228
+ }
229
+ })
230
+ equal(res.statusCode, 200, 'savePage status code')
231
+ const data = res.json().data
232
+ not(data.savePage.createdAt, null, 'createdAt')
233
+ not(data.savePage.updatedAt, null, 'updatedAt')
234
+ comment(`createdAt: ${data.savePage.createdAt}`)
235
+ comment(`updatedAt: ${data.savePage.updatedAt}`)
236
+ }
237
+
238
+ {
239
+ const res = await app.inject({
240
+ method: 'POST',
241
+ url: '/graphql',
242
+ body: {
243
+ query: `
244
+ mutation {
245
+ savePage(input: { id: 1, title: "Hello World", updatedAt: "${new Date().toISOString()}" }) {
246
+ id
247
+ title
248
+ createdAt
249
+ updatedAt
250
+ }
251
+ }
252
+ `
253
+ }
254
+ })
255
+ equal(res.statusCode, 400, 'savePage status code')
256
+ const data = res.json()
257
+ equal(data.errors[0].message, 'Field "updatedAt" is not defined by type "PageInput".')
258
+ }
259
+ })
260
+
261
+ test('do not assign created_at updated_at', async ({ pass, teardown, same, equal, not, comment }) => {
262
+ const app = fastify()
263
+ app.register(sqlMapper, {
264
+ ...connInfo,
265
+ autoTimestamp: false,
266
+ async onDatabaseLoad (db, sql) {
267
+ pass('onDatabaseLoad called')
268
+
269
+ await clear(db, sql)
270
+ await createBasicPages(db, sql)
271
+ }
272
+ })
273
+ app.register(sqlGraphQL)
274
+ teardown(app.close.bind(app))
275
+
276
+ await app.ready()
277
+
278
+ {
279
+ const res = await app.inject({
280
+ method: 'POST',
281
+ url: '/graphql',
282
+ body: {
283
+ query: `
284
+ mutation {
285
+ savePage(input: { title: "Hello" }) {
286
+ id
287
+ title
288
+ createdAt
289
+ updatedAt
290
+ }
291
+ }
292
+ `
293
+ }
294
+ })
295
+ equal(res.statusCode, 200, 'savePage status code')
296
+ const data = res.json().data
297
+ equal(data.savePage.createdAt, null, 'createdAt')
298
+ equal(data.savePage.updatedAt, null, 'updatedAt')
299
+ }
300
+
301
+ {
302
+ const res = await app.inject({
303
+ method: 'POST',
304
+ url: '/graphql',
305
+ body: {
306
+ query: `
307
+ query {
308
+ getPageById(id: 1) {
309
+ id
310
+ title
311
+ createdAt
312
+ updatedAt
313
+ }
314
+ }
315
+ `
316
+ }
317
+ })
318
+ equal(res.statusCode, 200, 'pages status code')
319
+ const data = res.json().data
320
+ equal(data.getPageById.createdAt, null, 'createdAt')
321
+ equal(data.getPageById.updatedAt, null, 'updatedAt')
322
+ }
323
+
324
+ {
325
+ const res = await app.inject({
326
+ method: 'POST',
327
+ url: '/graphql',
328
+ body: {
329
+ query: `
330
+ mutation {
331
+ savePage(input: { id: 1, title: "Hello World" }) {
332
+ id
333
+ title
334
+ createdAt
335
+ updatedAt
336
+ }
337
+ }
338
+ `
339
+ }
340
+ })
341
+ const data = res.json().data
342
+ equal(data.savePage.createdAt, null, 'createdAt')
343
+ equal(data.savePage.updatedAt, null, 'updatedAt')
344
+ }
345
+
346
+ {
347
+ const res = await app.inject({
348
+ method: 'POST',
349
+ url: '/graphql',
350
+ body: {
351
+ query: `
352
+ query {
353
+ getPageById(id: 1) {
354
+ id
355
+ title
356
+ createdAt
357
+ updatedAt
358
+ }
359
+ }
360
+ `
361
+ }
362
+ })
363
+ equal(res.statusCode, 200, 'pages status code')
364
+ const data = res.json().data
365
+ equal(data.getPageById.createdAt, null, 'createdAt')
366
+ equal(data.getPageById.updatedAt, null, 'updatedAt')
367
+ }
368
+ })
369
+
370
+ test('bulk insert adds created_at updated_at', async ({ pass, teardown, same, equal, not, comment }) => {
371
+ const app = fastify()
372
+ app.register(sqlMapper, {
373
+ ...connInfo,
374
+ async onDatabaseLoad (db, sql) {
375
+ pass('onDatabaseLoad called')
376
+
377
+ await clear(db, sql)
378
+ await createBasicPages(db, sql)
379
+ }
380
+ })
381
+ app.register(sqlGraphQL)
382
+ teardown(app.close.bind(app))
383
+
384
+ await app.ready()
385
+
386
+ {
387
+ const res = await app.inject({
388
+ method: 'POST',
389
+ url: '/graphql',
390
+ body: {
391
+ query: `
392
+ mutation {
393
+ savePage(input: { title: "Hello" }) {
394
+ id
395
+ title
396
+ createdAt
397
+ updatedAt
398
+ }
399
+ }
400
+ `
401
+ }
402
+ })
403
+ equal(res.statusCode, 200, 'savePage status code')
404
+ const data = res.json().data
405
+ not(data.savePage.createdAt, null, 'createdAt')
406
+ not(data.savePage.updatedAt, null, 'updatedAt')
407
+ }
408
+
409
+ {
410
+ const res = await app.inject({
411
+ method: 'POST',
412
+ url: '/graphql',
413
+ body: {
414
+ query: `
415
+ mutation batch($inputs : [PageInput]!) {
416
+ insertPages (inputs: $inputs) {
417
+ id
418
+ title
419
+ createdAt
420
+ updatedAt
421
+ }
422
+ }
423
+ `,
424
+ variables: {
425
+ inputs: [
426
+ { title: 'Page 1' },
427
+ { title: 'Page 2' },
428
+ { title: 'Page 3' }
429
+ ]
430
+ }
431
+ }
432
+ })
433
+ equal(res.statusCode, 200, 'savePage status code')
434
+ const pages = res.json().data.insertPages
435
+ for (const page of pages) {
436
+ not(page.createdAt, null, 'createdAt')
437
+ not(page.updatedAt, null, 'updatedAt')
438
+ equal(page.createdAt, page.updatedAt, 'createdAt === updatedAt')
439
+ }
440
+ }
441
+
442
+ {
443
+ const res = await app.inject({
444
+ method: 'POST',
445
+ url: '/graphql',
446
+ body: {
447
+ query: `
448
+ query {
449
+ pages {
450
+ id
451
+ title
452
+ createdAt
453
+ updatedAt
454
+ }
455
+ }
456
+ `
457
+ }
458
+ })
459
+ equal(res.statusCode, 200, 'pages status code')
460
+ const pages = res.json().data.pages
461
+ for (const page of pages) {
462
+ not(page.createdAt, null, 'createdAt')
463
+ not(page.updatedAt, null, 'updatedAt')
464
+ equal(page.createdAt, page.updatedAt, 'createdAt === updatedAt')
465
+ }
466
+ }
467
+ })
468
+
469
+ test('bulk insert with autoTimestamp=false do not had created_at updated_at', async ({ pass, teardown, same, equal, not, comment }) => {
470
+ const app = fastify()
471
+ app.register(sqlMapper, {
472
+ ...connInfo,
473
+ autoTimestamp: false,
474
+ async onDatabaseLoad (db, sql) {
475
+ pass('onDatabaseLoad called')
476
+
477
+ await clear(db, sql)
478
+ await createBasicPages(db, sql)
479
+ }
480
+ })
481
+ app.register(sqlGraphQL)
482
+ teardown(app.close.bind(app))
483
+
484
+ await app.ready()
485
+
486
+ {
487
+ const res = await app.inject({
488
+ method: 'POST',
489
+ url: '/graphql',
490
+ body: {
491
+ query: `
492
+ mutation batch($inputs : [PageInput]!) {
493
+ insertPages (inputs: $inputs) {
494
+ id
495
+ title
496
+ createdAt
497
+ updatedAt
498
+ }
499
+ }
500
+ `,
501
+ variables: {
502
+ inputs: [
503
+ { title: 'Page 1' },
504
+ { title: 'Page 2' },
505
+ { title: 'Page 3' }
506
+ ]
507
+ }
508
+ }
509
+ })
510
+ equal(res.statusCode, 200, 'savePage status code')
511
+ const pages = res.json().data.insertPages
512
+ for (const page of pages) {
513
+ equal(page.createdAt, null, 'createdAt')
514
+ equal(page.updatedAt, null, 'updatedAt')
515
+ }
516
+ }
517
+ })
@@ -722,3 +722,82 @@ test('[SQLite] - UUID', { skip: !isSQLite }, async ({ pass, teardown, same, equa
722
722
  }, 'savePage response')
723
723
  }
724
724
  })
725
+
726
+ test('BIGINT!', { skip: isSQLite, only: true }, async ({ pass, teardown, same, equal }) => {
727
+ const app = fastify()
728
+ app.register(sqlMapper, {
729
+ ...connInfo,
730
+ async onDatabaseLoad (db, sql) {
731
+ pass('onDatabaseLoad called')
732
+
733
+ await clear(db, sql)
734
+
735
+ await db.query(sql`
736
+ CREATE TABLE simple_types (
737
+ id SERIAL PRIMARY KEY,
738
+ counter BIGINT
739
+ );`)
740
+ }
741
+ })
742
+ teardown(app.close.bind(app))
743
+
744
+ app.register(sqlGraphQL)
745
+
746
+ await app.ready()
747
+
748
+ const counter = BigInt(Number.MAX_SAFE_INTEGER) + 1000n
749
+
750
+ {
751
+ const res = await app.inject({
752
+ method: 'POST',
753
+ url: '/graphql',
754
+ body: {
755
+ query: `
756
+ mutation {
757
+ saveSimpleType(input: {
758
+ counter: "${counter}"
759
+ }) {
760
+ id
761
+ counter
762
+ }
763
+ }
764
+ `
765
+ }
766
+ })
767
+ equal(res.statusCode, 200, 'saveSimpleType status code')
768
+ same(res.json(), {
769
+ data: {
770
+ saveSimpleType: {
771
+ id: '1',
772
+ counter
773
+ }
774
+ }
775
+ }, 'saveSimpleType response')
776
+ }
777
+
778
+ {
779
+ const res = await app.inject({
780
+ method: 'POST',
781
+ url: '/graphql',
782
+ body: {
783
+ query: `
784
+ query {
785
+ getSimpleTypeById(id: 1) {
786
+ id
787
+ counter
788
+ }
789
+ }
790
+ `
791
+ }
792
+ })
793
+ equal(res.statusCode, 200, 'getSimpleTypeById status code')
794
+ same(res.json(), {
795
+ data: {
796
+ getSimpleTypeById: {
797
+ id: 1,
798
+ counter
799
+ }
800
+ }
801
+ }, 'getSimpleTypeById response')
802
+ }
803
+ })
@@ -0,0 +1,172 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const sqlGraphQL = require('..')
5
+ const sqlMapper = require('@platformatic/sql-mapper')
6
+ const fastify = require('fastify')
7
+ const { connInfo, isPg } = require('./helper')
8
+
9
+ async function runMigrations (db, sql) {
10
+ await db.query(sql`
11
+ create table company (
12
+ id SERIAL primary key,
13
+ name varchar(60)
14
+ );
15
+
16
+ create table social_medias (
17
+ id SERIAL primary key,
18
+ name varchar(60)
19
+ );
20
+
21
+ create table company_social_medias (
22
+ id SERIAL primary key,
23
+ username varchar not null,
24
+ social_medias_id integer not null,
25
+ company_id integer not null,
26
+ constraint "fk_company_id" foreign key ("company_id") references company,
27
+ constraint "fk_social_medias_id" foreign key ("social_medias_id") references social_medias
28
+ );
29
+ `)
30
+ }
31
+
32
+ async function clean (db, sql) {
33
+ await db.query(sql`
34
+ drop table if exists company_social_medias;
35
+ drop table if exists company;
36
+ drop table if exists social_medias;
37
+ `)
38
+ }
39
+
40
+ test('do not crash', { skip: !isPg }, async ({ pass, teardown, same, equal }) => {
41
+ const app = fastify()
42
+ app.register(sqlMapper, {
43
+ ...connInfo,
44
+ async onDatabaseLoad (db, sql) {
45
+ pass('onDatabaseLoad called')
46
+ await clean(db, sql)
47
+
48
+ await runMigrations(db, sql)
49
+ teardown(async () => {
50
+ await clean(db, sql)
51
+ })
52
+ }
53
+ })
54
+ await app.register(sqlGraphQL)
55
+ teardown(app.close.bind(app))
56
+
57
+ await app.ready()
58
+
59
+ {
60
+ const res = await app.inject({
61
+ method: 'POST',
62
+ url: '/graphql',
63
+ body: {
64
+ query: `
65
+ mutation {
66
+ saveCompany(input: { name: "Platformatic" }) {
67
+ id
68
+ name
69
+ }
70
+ }
71
+ `
72
+ }
73
+ })
74
+ equal(res.statusCode, 200, 'saveCompany status code')
75
+ same(res.json(), {
76
+ data: {
77
+ saveCompany: {
78
+ id: 1,
79
+ name: 'Platformatic'
80
+ }
81
+ }
82
+ }, 'saveCompany response')
83
+ }
84
+
85
+ {
86
+ const res = await app.inject({
87
+ method: 'POST',
88
+ url: '/graphql',
89
+ body: {
90
+ query: `
91
+ mutation {
92
+ saveSocialMedia(input: { name: "Twitter" }) {
93
+ id
94
+ name
95
+ }
96
+ }
97
+ `
98
+ }
99
+ })
100
+ equal(res.statusCode, 200, 'saveSocialMedia status code')
101
+ same(res.json(), {
102
+ data: {
103
+ saveSocialMedia: {
104
+ id: 1,
105
+ name: 'Twitter'
106
+ }
107
+ }
108
+ }, 'saveSocialMedia response')
109
+ }
110
+
111
+ {
112
+ const res = await app.inject({
113
+ method: 'POST',
114
+ url: '/graphql',
115
+ body: {
116
+ query: `
117
+ mutation {
118
+ saveCompanySocialMedia(input: { companyId: 1, socialMediasId: 1, username: "platformatic" }) {
119
+ id
120
+ username
121
+ }
122
+ }
123
+ `
124
+ }
125
+ })
126
+ equal(res.statusCode, 200, 'saveCompanySocialMedia status code')
127
+ same(res.json(), {
128
+ data: {
129
+ saveCompanySocialMedia: {
130
+ id: 1,
131
+ username: 'platformatic'
132
+ }
133
+ }
134
+ }, 'saveCompanySocialMedia response')
135
+ }
136
+
137
+ {
138
+ const res = await app.inject({
139
+ method: 'POST',
140
+ url: '/graphql',
141
+ body: {
142
+ query: `
143
+ query {
144
+ company {
145
+ name
146
+ companySocialMedias {
147
+ username
148
+ socialMedias {
149
+ name
150
+ }
151
+ }
152
+ }
153
+ }
154
+ `
155
+ }
156
+ })
157
+ equal(res.statusCode, 200, 'complex status code')
158
+ same(res.json(), {
159
+ data: {
160
+ company: [{
161
+ name: 'Platformatic',
162
+ companySocialMedias: [{
163
+ username: 'platformatic',
164
+ socialMedias: {
165
+ name: 'Twitter'
166
+ }
167
+ }]
168
+ }]
169
+ }
170
+ }, 'complex response')
171
+ }
172
+ })
package/test/helper.js CHANGED
@@ -96,4 +96,19 @@ module.exports.clear = async function (db, sql) {
96
96
  await db.query(sql`DROP Type simple_enum`)
97
97
  } catch (err) {
98
98
  }
99
+
100
+ try {
101
+ await db.query(sql`DROP TABLE "myschema"."mytable"`)
102
+ } catch (err) {
103
+ }
104
+
105
+ try {
106
+ await db.query(sql`DROP TABLE "myschema"."template"`)
107
+ } catch (err) {
108
+ }
109
+
110
+ try {
111
+ await db.query(sql`DROP SCHEMA "myschema"`)
112
+ } catch (err) {
113
+ }
99
114
  }
@@ -36,6 +36,10 @@ test('inserted_at updated_at happy path', async ({ pass, teardown, same, equal,
36
36
  const app = fastify()
37
37
  app.register(sqlMapper, {
38
38
  ...connInfo,
39
+ autoTimestamp: {
40
+ createdAt: 'inserted_at',
41
+ updatedAt: 'updated_at'
42
+ },
39
43
  async onDatabaseLoad (db, sql) {
40
44
  pass('onDatabaseLoad called')
41
45
 
@@ -159,6 +163,10 @@ test('cannot set inserted_at', async ({ pass, teardown, same, equal, not, commen
159
163
  const app = fastify()
160
164
  app.register(sqlMapper, {
161
165
  ...connInfo,
166
+ autoTimestamp: {
167
+ createdAt: 'inserted_at',
168
+ updatedAt: 'updated_at'
169
+ },
162
170
  async onDatabaseLoad (db, sql) {
163
171
  pass('onDatabaseLoad called')
164
172
 
@@ -198,6 +206,10 @@ test('cannot set updated_at', async ({ pass, teardown, same, equal, not, comment
198
206
  const app = fastify()
199
207
  app.register(sqlMapper, {
200
208
  ...connInfo,
209
+ autoTimestamp: {
210
+ createdAt: 'inserted_at',
211
+ updatedAt: 'updated_at'
212
+ },
201
213
  async onDatabaseLoad (db, sql) {
202
214
  pass('onDatabaseLoad called')
203
215
 
@@ -371,6 +383,10 @@ test('bulk insert adds inserted_at updated_at', async ({ pass, teardown, same, e
371
383
  const app = fastify()
372
384
  app.register(sqlMapper, {
373
385
  ...connInfo,
386
+ autoTimestamp: {
387
+ createdAt: 'inserted_at',
388
+ updatedAt: 'updated_at'
389
+ },
374
390
  async onDatabaseLoad (db, sql) {
375
391
  pass('onDatabaseLoad called')
376
392
 
@@ -466,7 +482,7 @@ test('bulk insert adds inserted_at updated_at', async ({ pass, teardown, same, e
466
482
  }
467
483
  })
468
484
 
469
- test('bulk insert with autoTimestamp=false do not hadd inserted_at updated_at', async ({ pass, teardown, same, equal, not, comment }) => {
485
+ test('bulk insert with autoTimestamp=false do not had inserted_at updated_at', async ({ pass, teardown, same, equal, not, comment }) => {
470
486
  const app = fastify()
471
487
  app.register(sqlMapper, {
472
488
  ...connInfo,
@@ -860,3 +860,255 @@ test('nested update', async ({ pass, teardown, same, equal }) => {
860
860
  }, 'savePage response')
861
861
  }
862
862
  })
863
+
864
+ test('nested resolver without `id` suffix', async ({ pass, teardown, same, equal }) => {
865
+ const app = fastify()
866
+ app.register(sqlMapper, {
867
+ ...connInfo,
868
+ async onDatabaseLoad (db, sql) {
869
+ pass('onDatabaseLoad called')
870
+
871
+ await clear(db, sql)
872
+
873
+ if (isMysql) {
874
+ await db.query(sql`
875
+ CREATE TABLE categories (
876
+ id SERIAL PRIMARY KEY,
877
+ name VARCHAR(255)
878
+ );
879
+ CREATE TABLE pages (
880
+ id SERIAL PRIMARY KEY,
881
+ title VARCHAR(42),
882
+ category BIGINT UNSIGNED,
883
+ FOREIGN KEY (category) REFERENCES categories(id) ON DELETE CASCADE
884
+ );
885
+ `)
886
+ } else if (isSQLite) {
887
+ await db.query(sql`
888
+ CREATE TABLE categories (
889
+ id INTEGER PRIMARY KEY,
890
+ name VARCHAR(42)
891
+ );
892
+ `)
893
+ await db.query(sql`
894
+ CREATE TABLE pages (
895
+ id INTEGER PRIMARY KEY,
896
+ title VARCHAR(42),
897
+ category INTEGER REFERENCES categories(id)
898
+ );
899
+ `)
900
+ } else {
901
+ await db.query(sql`
902
+ CREATE TABLE categories (
903
+ id SERIAL PRIMARY KEY,
904
+ name VARCHAR(42)
905
+ );
906
+ CREATE TABLE pages (
907
+ id SERIAL PRIMARY KEY,
908
+ title VARCHAR(42),
909
+ category INTEGER REFERENCES categories(id)
910
+ );
911
+ `)
912
+ }
913
+ }
914
+ })
915
+ app.register(sqlGraphQL)
916
+ teardown(app.close.bind(app))
917
+
918
+ const categories = [{
919
+ name: 'Pets'
920
+ }, {
921
+ name: 'Food'
922
+ }]
923
+
924
+ await app.inject({
925
+ method: 'POST',
926
+ url: '/graphql',
927
+ body: {
928
+ query: `
929
+ mutation batch($inputs: [CategoryInput]!) {
930
+ insertCategories(inputs: $inputs) {
931
+ id
932
+ name
933
+ }
934
+ }
935
+ `,
936
+ variables: {
937
+ inputs: categories
938
+ }
939
+ }
940
+ })
941
+
942
+ {
943
+ const res = await app.inject({
944
+ method: 'POST',
945
+ url: '/graphql',
946
+ body: {
947
+ query: `
948
+ mutation {
949
+ savePage(input: { title: "Hello", category: 1 }) {
950
+ id
951
+ title
952
+ category {
953
+ id
954
+ name
955
+ }
956
+ }
957
+ }
958
+ `
959
+ }
960
+ })
961
+ equal(res.statusCode, 200, 'savePage status code')
962
+ same(res.json(), {
963
+ data: {
964
+ savePage: {
965
+ id: 1,
966
+ title: 'Hello',
967
+ category: {
968
+ id: 1,
969
+ name: 'Pets'
970
+ }
971
+ }
972
+ }
973
+ }, 'savePage response')
974
+ }
975
+
976
+ {
977
+ const res = await app.inject({
978
+ method: 'POST',
979
+ url: '/graphql',
980
+ body: {
981
+ query: `
982
+ query {
983
+ getPageById(id: 1) {
984
+ id
985
+ title
986
+ category {
987
+ id
988
+ name
989
+ pages {
990
+ id
991
+ title
992
+ category {
993
+ id
994
+ name
995
+ }
996
+ }
997
+ }
998
+ }
999
+ }
1000
+ `
1001
+ }
1002
+ })
1003
+ equal(res.statusCode, 200, 'pages status code')
1004
+ same(res.json(), {
1005
+ data: {
1006
+ getPageById: {
1007
+ id: 1,
1008
+ title: 'Hello',
1009
+ category: {
1010
+ id: 1,
1011
+ name: 'Pets',
1012
+ pages: [{
1013
+ id: 1,
1014
+ title: 'Hello',
1015
+ category: {
1016
+ id: 1,
1017
+ name: 'Pets'
1018
+ }
1019
+ }]
1020
+ }
1021
+ }
1022
+ }
1023
+ }, 'pages response')
1024
+ }
1025
+
1026
+ // Without ids
1027
+ {
1028
+ const res = await app.inject({
1029
+ method: 'POST',
1030
+ url: '/graphql',
1031
+ body: {
1032
+ query: `
1033
+ query {
1034
+ getPageById(id: 1) {
1035
+ title
1036
+ category {
1037
+ name
1038
+ pages {
1039
+ title
1040
+ category {
1041
+ name
1042
+ }
1043
+ }
1044
+ }
1045
+ }
1046
+ }
1047
+ `
1048
+ }
1049
+ })
1050
+ equal(res.statusCode, 200, 'pages status code')
1051
+ same(res.json(), {
1052
+ data: {
1053
+ getPageById: {
1054
+ title: 'Hello',
1055
+ category: {
1056
+ name: 'Pets',
1057
+ pages: [{
1058
+ title: 'Hello',
1059
+ category: {
1060
+ name: 'Pets'
1061
+ }
1062
+ }]
1063
+ }
1064
+ }
1065
+ }
1066
+ }, 'pages response')
1067
+ }
1068
+
1069
+ {
1070
+ const res = await app.inject({
1071
+ method: 'POST',
1072
+ url: '/graphql',
1073
+ body: {
1074
+ query: `
1075
+ query {
1076
+ categories {
1077
+ id
1078
+ name
1079
+ pages {
1080
+ id
1081
+ title
1082
+ category {
1083
+ id
1084
+ name
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+ `
1090
+ }
1091
+ })
1092
+ equal(res.statusCode, 200, 'categories.posts status code')
1093
+ same(res.json(), {
1094
+ data: {
1095
+ categories: [{
1096
+ id: 1,
1097
+ name: 'Pets',
1098
+ pages: [{
1099
+ id: 1,
1100
+ title: 'Hello',
1101
+ category: {
1102
+ id: 1,
1103
+ name: 'Pets'
1104
+ }
1105
+ }]
1106
+ }, {
1107
+ id: 2,
1108
+ name: 'Food',
1109
+ pages: []
1110
+ }]
1111
+ }
1112
+ }, 'categories.posts response')
1113
+ }
1114
+ })
@@ -0,0 +1,51 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const sqlGraphQL = require('..')
5
+ const sqlMapper = require('@platformatic/sql-mapper')
6
+ const fastify = require('fastify')
7
+ const { clear, connInfo, isPg } = require('./helper')
8
+
9
+ const connInfoWithSchema = {
10
+ ...connInfo,
11
+ schema: ['public', 'myschema']
12
+ }
13
+
14
+ test('should catch GraphQL print schema errors', { skip: !isPg }, async ({ pass, teardown, same }) => {
15
+ const app = fastify()
16
+ app.register(sqlMapper, {
17
+ ...connInfoWithSchema,
18
+ async onDatabaseLoad (db, sql) {
19
+ pass('onDatabaseLoad called')
20
+
21
+ await clear(db, sql)
22
+
23
+ await db.query(sql`
24
+ CREATE SCHEMA IF NOT EXISTS "myschema";
25
+
26
+ CREATE TABLE IF NOT EXISTS "myschema"."template" (
27
+ "id" uuid NOT NULL,
28
+ PRIMARY KEY ("id")
29
+ );
30
+
31
+ CREATE TABLE IF NOT EXISTS "myschema"."mytable" (
32
+ "id" int4 NOT NULL,
33
+ "template_id" uuid,
34
+ CONSTRAINT "mytable_template" FOREIGN KEY ("template_id") REFERENCES "myschema"."template"("id"),
35
+ PRIMARY KEY ("id")
36
+ );`)
37
+ }
38
+ })
39
+
40
+ app.register(sqlGraphQL)
41
+ teardown(app.close.bind(app))
42
+
43
+ try {
44
+ await app.ready()
45
+ same(true, false, 'We check that this line never runs, since we should catch the previous error')
46
+ } catch (error) {
47
+ same(error.name, 'Error')
48
+ same(error.message, 'Error printing the GraphQL schema')
49
+ same(error.cause, new Error('MyschemaTemplate.mytable field config must be an object.'))
50
+ }
51
+ })