@platformatic/sql-graphql 0.4.0 → 0.5.1
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 +19 -2
- package/lib/entity-to-type.js +1 -0
- package/lib/relationship.js +2 -2
- package/lib/subscriptions.js +89 -0
- package/package.json +6 -4
- package/test/helper.js +9 -0
- package/test/relations.test.js +145 -0
- package/test/subscription.test.js +336 -0
- package/.nyc_output/0ebb7c74-e9d0-4427-873f-707c97b71e3a.json +0 -1
- package/.nyc_output/1705f757-0633-4d57-bdd8-74da5e87c7f2.json +0 -1
- package/.nyc_output/18dca461-a16b-4426-8125-755b1714aa40.json +0 -1
- package/.nyc_output/1f6f25f8-9751-4755-94d9-99047f9e7f95.json +0 -1
- package/.nyc_output/25c88717-db5b-4879-a56e-cf27a8748dc0.json +0 -1
- package/.nyc_output/354b1308-2042-4a6c-9cf3-07be876efbb3.json +0 -1
- package/.nyc_output/415ccadc-7e96-410e-ae66-6ca11ce92b8c.json +0 -1
- package/.nyc_output/5f643acb-71a2-42a1-8e2c-3c830382e6c3.json +0 -1
- package/.nyc_output/7cc4fa46-505d-4868-802f-b22374f1aa0f.json +0 -1
- package/.nyc_output/90d66f5a-41c4-4c50-95f5-078e68e2ee4d.json +0 -1
- package/.nyc_output/a5752887-ebcd-4670-bc8e-8d75a3c1ae51.json +0 -1
- package/.nyc_output/a9d0b301-bd1a-4076-bdd5-e311adb96683.json +0 -1
- package/.nyc_output/cb3b5287-a592-492a-bf10-ecb456437154.json +0 -1
- package/.nyc_output/d13b13a2-ef5a-4e88-949e-77c62f98abd2.json +0 -1
- package/.nyc_output/ea305ecd-1796-4c6b-a9e4-503ca63f2d42.json +0 -1
- package/.nyc_output/fc62d83a-4179-44bf-9bbb-b306531568e9.json +0 -1
- package/.nyc_output/processinfo/0ebb7c74-e9d0-4427-873f-707c97b71e3a.json +0 -1
- package/.nyc_output/processinfo/1705f757-0633-4d57-bdd8-74da5e87c7f2.json +0 -1
- package/.nyc_output/processinfo/18dca461-a16b-4426-8125-755b1714aa40.json +0 -1
- package/.nyc_output/processinfo/1f6f25f8-9751-4755-94d9-99047f9e7f95.json +0 -1
- package/.nyc_output/processinfo/25c88717-db5b-4879-a56e-cf27a8748dc0.json +0 -1
- package/.nyc_output/processinfo/354b1308-2042-4a6c-9cf3-07be876efbb3.json +0 -1
- package/.nyc_output/processinfo/415ccadc-7e96-410e-ae66-6ca11ce92b8c.json +0 -1
- package/.nyc_output/processinfo/5f643acb-71a2-42a1-8e2c-3c830382e6c3.json +0 -1
- package/.nyc_output/processinfo/7cc4fa46-505d-4868-802f-b22374f1aa0f.json +0 -1
- package/.nyc_output/processinfo/90d66f5a-41c4-4c50-95f5-078e68e2ee4d.json +0 -1
- package/.nyc_output/processinfo/a5752887-ebcd-4670-bc8e-8d75a3c1ae51.json +0 -1
- package/.nyc_output/processinfo/a9d0b301-bd1a-4076-bdd5-e311adb96683.json +0 -1
- package/.nyc_output/processinfo/cb3b5287-a592-492a-bf10-ecb456437154.json +0 -1
- package/.nyc_output/processinfo/d13b13a2-ef5a-4e88-949e-77c62f98abd2.json +0 -1
- package/.nyc_output/processinfo/ea305ecd-1796-4c6b-a9e4-503ca63f2d42.json +0 -1
- package/.nyc_output/processinfo/fc62d83a-4179-44bf-9bbb-b306531568e9.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const constructGraph = require('./lib/entity-to-type')
|
|
|
5
5
|
const mercurius = require('mercurius')
|
|
6
6
|
const graphql = require('graphql')
|
|
7
7
|
const establishRelations = require('./lib/relationship')
|
|
8
|
+
const setupSubscriptions = require('./lib/subscriptions')
|
|
8
9
|
const scalars = require('graphql-scalars')
|
|
9
10
|
|
|
10
11
|
async function mapperToGraphql (app, opts) {
|
|
@@ -24,6 +25,8 @@ async function mapperToGraphql (app, opts) {
|
|
|
24
25
|
federationReplacements,
|
|
25
26
|
federationMetadata: opts.federationMetadata
|
|
26
27
|
}
|
|
28
|
+
const metaMap = new Map()
|
|
29
|
+
|
|
27
30
|
if (Object.keys(mapper.entities).length === 0) {
|
|
28
31
|
// no schema
|
|
29
32
|
queryTopFields.hello = { type: graphql.GraphQLString }
|
|
@@ -31,7 +34,6 @@ async function mapperToGraphql (app, opts) {
|
|
|
31
34
|
hello: () => 'Hello Platformatic!'
|
|
32
35
|
}
|
|
33
36
|
} else {
|
|
34
|
-
const metaMap = new Map()
|
|
35
37
|
for (const entity of Object.values(mapper.entities)) {
|
|
36
38
|
relations.push(...entity.relations)
|
|
37
39
|
const meta = constructGraph(app, entity, graphOpts)
|
|
@@ -85,7 +87,21 @@ async function mapperToGraphql (app, opts) {
|
|
|
85
87
|
delete resolvers.Mutation
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
let
|
|
90
|
+
let subscription
|
|
91
|
+
if (app.platformatic.mq) {
|
|
92
|
+
const entitiesList = Object.keys(app.platformatic.entities)
|
|
93
|
+
const ignoreList = opts.subscriptionIgnore || []
|
|
94
|
+
|
|
95
|
+
if (entitiesList.length - ignoreList.length > 0) {
|
|
96
|
+
opts.subscription = {
|
|
97
|
+
emitter: app.platformatic.mq
|
|
98
|
+
}
|
|
99
|
+
// TODO support ignoring some of those
|
|
100
|
+
subscription = setupSubscriptions(app, metaMap, resolvers, ignoreList)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let sdl = graphql.printSchema(new graphql.GraphQLSchema({ query, mutation, subscription }))
|
|
89
105
|
|
|
90
106
|
if (opts.federationMetadata) {
|
|
91
107
|
for (const replacement of federationReplacements) {
|
|
@@ -108,6 +124,7 @@ async function mapperToGraphql (app, opts) {
|
|
|
108
124
|
}
|
|
109
125
|
|
|
110
126
|
opts.graphiql = opts.graphiql !== false
|
|
127
|
+
|
|
111
128
|
await app.register(mercurius, {
|
|
112
129
|
...opts,
|
|
113
130
|
schema: sdl,
|
package/lib/entity-to-type.js
CHANGED
package/lib/relationship.js
CHANGED
|
@@ -16,6 +16,7 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
|
|
|
16
16
|
for (const relation of relations) {
|
|
17
17
|
assert(relation.table_name, 'table_name is required')
|
|
18
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')
|
|
19
20
|
|
|
20
21
|
const current = tablesTypeMap[relation.table_name]
|
|
21
22
|
const foreign = tablesTypeMap[relation.foreign_table_name]
|
|
@@ -23,7 +24,6 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
|
|
|
23
24
|
|
|
24
25
|
// current to foreign
|
|
25
26
|
{
|
|
26
|
-
assert(relation.column_name.toLowerCase().endsWith('id'), 'The column with id reference must be ended with `id` postfix')
|
|
27
27
|
const lowered = lowerCaseFirst(camelcase(cutOutIdEnding(relation.column_name)))
|
|
28
28
|
if (!relationships[current.type] || relationships[current.type][lowered] !== false) {
|
|
29
29
|
current.fields[lowered] = { type: foreign.type }
|
|
@@ -79,7 +79,7 @@ function lowerCaseFirst (str) {
|
|
|
79
79
|
|
|
80
80
|
function cutOutIdEnding (str) {
|
|
81
81
|
str = str.toString()
|
|
82
|
-
return str.slice(0, str.toLowerCase().
|
|
82
|
+
return str.slice(0, str.toLowerCase().lastIndexOf('id'))
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
function findPrimaryColumnName (camelCasedFields) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const graphql = require('graphql')
|
|
4
|
+
|
|
5
|
+
function setupSubscriptions (app, metaMap, resolvers, ignores) {
|
|
6
|
+
const fields = {}
|
|
7
|
+
resolvers.Subscription = {}
|
|
8
|
+
for (const [field, meta] of metaMap) {
|
|
9
|
+
if (ignores.indexOf(field.singularName) >= 0) {
|
|
10
|
+
continue
|
|
11
|
+
}
|
|
12
|
+
const { type } = meta
|
|
13
|
+
const saved = `${field.singularName}Saved`
|
|
14
|
+
fields[saved] = {
|
|
15
|
+
type
|
|
16
|
+
}
|
|
17
|
+
resolvers.Subscription[saved] = {
|
|
18
|
+
subscribe: async function * (_, query, ctx, info) {
|
|
19
|
+
const log = ctx.reply.request.log
|
|
20
|
+
const { pubsub } = ctx
|
|
21
|
+
const entity = app.platformatic.entities[field.singularName]
|
|
22
|
+
const topic = await entity.getSubscriptionTopic({ action: 'save', ctx })
|
|
23
|
+
log.trace({ topic }, 'subscribed')
|
|
24
|
+
const res = await pubsub.subscribe(topic)
|
|
25
|
+
const primaryKey = entity.primaryKey
|
|
26
|
+
const fields = meta.getFields([{ info }])
|
|
27
|
+
|
|
28
|
+
for await (const msg of res) {
|
|
29
|
+
// TODO optimize this by not calling find() if the subscriber is only asking for the id.
|
|
30
|
+
try {
|
|
31
|
+
log.trace({ msg, entity: field.singularName }, 'graphql subscription augmenting data')
|
|
32
|
+
// The alternative to augmenting the data is installing a resolver for every type
|
|
33
|
+
// of a given entity that loads up the type via a dataloader.
|
|
34
|
+
const found = await entity.find({ where: { [primaryKey]: { eq: msg[primaryKey] } }, fields, ctx })
|
|
35
|
+
// The following could happen in case of a race condition
|
|
36
|
+
// testing it would be very hard, so we skip it for now
|
|
37
|
+
/* istanbul ignore next */
|
|
38
|
+
if (found.length === 0) {
|
|
39
|
+
log.warn({ ...msg.payload, entity: field.singularName }, 'graphql subscription could not find element')
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
const toYield = { [saved]: found[0] }
|
|
43
|
+
log.trace({ yield: toYield, entity: field.singularName }, 'graphql subscription augmented data')
|
|
44
|
+
yield toYield
|
|
45
|
+
} catch (err) {
|
|
46
|
+
/* istanbul ignore next */
|
|
47
|
+
log.warn({ err, entity: field.singularName }, 'graphql subscription error')
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const deleted = `${field.singularName}Deleted`
|
|
54
|
+
fields[deleted] = {
|
|
55
|
+
type: new graphql.GraphQLObjectType({
|
|
56
|
+
name: `${field.name}Deleted`,
|
|
57
|
+
fields: {
|
|
58
|
+
[field.primaryKey]: {
|
|
59
|
+
type: meta.fields[field.primaryKey].type
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
resolvers.Subscription[deleted] = {
|
|
65
|
+
subscribe: async (_, query, ctx, info) => {
|
|
66
|
+
const { pubsub } = ctx
|
|
67
|
+
const entity = app.platformatic.entities[field.singularName]
|
|
68
|
+
const topic = await entity.getSubscriptionTopic({ action: 'delete', ctx })
|
|
69
|
+
const res = await pubsub.subscribe(topic)
|
|
70
|
+
ctx.reply.request.log.trace({ topic }, 'subscribed')
|
|
71
|
+
return wrap(res, deleted)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const subscription = new graphql.GraphQLObjectType({
|
|
76
|
+
name: 'Subscription',
|
|
77
|
+
fields
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return subscription
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function * wrap (iterator, key) {
|
|
84
|
+
for await (const msg of iterator) {
|
|
85
|
+
yield { [key]: msg }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = setupSubscriptions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/sql-graphql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Map SQL dbs to GraphQL",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://github.com/plaformatic/platformatic#readme",
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@platformatic/sql-
|
|
17
|
+
"@platformatic/sql-events": "0.5.1",
|
|
18
|
+
"@platformatic/sql-mapper": "0.5.1",
|
|
18
19
|
"fastify": "^4.6.0",
|
|
19
20
|
"snazzy": "^9.0.0",
|
|
20
21
|
"standard": "^17.0.0",
|
|
21
22
|
"tap": "^16.0.0",
|
|
22
|
-
"tsd": "^0.24.0"
|
|
23
|
+
"tsd": "^0.24.0",
|
|
24
|
+
"ws": "^8.9.0"
|
|
23
25
|
},
|
|
24
26
|
"dependencies": {
|
|
25
27
|
"camelcase": "^6.0.0",
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
"graphql-scalars": "^1.13.1",
|
|
29
31
|
"graphql-type-json": "^0.3.2",
|
|
30
32
|
"inflected": "^2.1.0",
|
|
31
|
-
"mercurius": "^11.
|
|
33
|
+
"mercurius": "^11.2.0"
|
|
32
34
|
},
|
|
33
35
|
"tsd": {
|
|
34
36
|
"directory": "test/types"
|
package/test/helper.js
CHANGED
|
@@ -77,4 +77,13 @@ module.exports.clear = async function (db, sql) {
|
|
|
77
77
|
await db.query(sql`DROP TABLE authors`)
|
|
78
78
|
} catch (err) {
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await db.query(sql`DROP TABLE organizations`)
|
|
83
|
+
} catch (err) {
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await db.query(sql`DROP TABLE individuals`)
|
|
87
|
+
} catch (err) {
|
|
88
|
+
}
|
|
80
89
|
}
|
package/test/relations.test.js
CHANGED
|
@@ -239,3 +239,148 @@ test('should handle multi references', async ({ pass, teardown, same, equal }) =
|
|
|
239
239
|
}, 'query authors response')
|
|
240
240
|
}
|
|
241
241
|
})
|
|
242
|
+
|
|
243
|
+
test('cut out id exactly from ending when forming a name of relation', async ({ pass, teardown, same, equal }) => {
|
|
244
|
+
const app = fastify()
|
|
245
|
+
app.register(sqlMapper, {
|
|
246
|
+
...connInfo,
|
|
247
|
+
async onDatabaseLoad (db, sql) {
|
|
248
|
+
pass('onDatabaseLoad called')
|
|
249
|
+
|
|
250
|
+
await clear(db, sql)
|
|
251
|
+
|
|
252
|
+
if (isMysql) {
|
|
253
|
+
await db.query(sql`
|
|
254
|
+
CREATE TABLE individuals (
|
|
255
|
+
id SERIAL PRIMARY KEY,
|
|
256
|
+
name VARCHAR(255)
|
|
257
|
+
);
|
|
258
|
+
CREATE TABLE organizations (
|
|
259
|
+
id SERIAL PRIMARY KEY,
|
|
260
|
+
name VARCHAR(42),
|
|
261
|
+
individual_id BIGINT UNSIGNED,
|
|
262
|
+
FOREIGN KEY (individual_id) REFERENCES individuals(id) ON DELETE CASCADE
|
|
263
|
+
);
|
|
264
|
+
`)
|
|
265
|
+
} else if (isSQLite) {
|
|
266
|
+
await db.query(sql`
|
|
267
|
+
CREATE TABLE individuals (
|
|
268
|
+
id INTEGER PRIMARY KEY,
|
|
269
|
+
name VARCHAR(255)
|
|
270
|
+
);
|
|
271
|
+
`)
|
|
272
|
+
await db.query(sql`
|
|
273
|
+
CREATE TABLE organizations (
|
|
274
|
+
id INTEGER PRIMARY KEY,
|
|
275
|
+
name VARCHAR(42),
|
|
276
|
+
individual_id BIGINT UNSIGNED,
|
|
277
|
+
FOREIGN KEY (individual_id) REFERENCES individuals(id) ON DELETE CASCADE
|
|
278
|
+
);
|
|
279
|
+
`)
|
|
280
|
+
} else {
|
|
281
|
+
await db.query(sql`
|
|
282
|
+
CREATE TABLE individuals (
|
|
283
|
+
id SERIAL PRIMARY KEY,
|
|
284
|
+
name VARCHAR(42)
|
|
285
|
+
);
|
|
286
|
+
CREATE TABLE organizations (
|
|
287
|
+
id SERIAL PRIMARY KEY,
|
|
288
|
+
name VARCHAR(42),
|
|
289
|
+
individual_id INTEGER REFERENCES individuals(id)
|
|
290
|
+
);
|
|
291
|
+
`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
app.register(sqlGraphQL)
|
|
296
|
+
teardown(app.close.bind(app))
|
|
297
|
+
|
|
298
|
+
await app.ready()
|
|
299
|
+
|
|
300
|
+
const individuals = [{
|
|
301
|
+
id: 1,
|
|
302
|
+
name: 'Mark'
|
|
303
|
+
}]
|
|
304
|
+
|
|
305
|
+
const organization = [{
|
|
306
|
+
id: 1,
|
|
307
|
+
name: 'Platformatic',
|
|
308
|
+
individualId: 1
|
|
309
|
+
}]
|
|
310
|
+
|
|
311
|
+
{
|
|
312
|
+
const res = await app.inject({
|
|
313
|
+
method: 'POST',
|
|
314
|
+
url: '/graphql',
|
|
315
|
+
body: {
|
|
316
|
+
query: `
|
|
317
|
+
mutation batch($inputs : [IndividualInput]!) {
|
|
318
|
+
insertIndividuals(inputs: $inputs) {
|
|
319
|
+
id
|
|
320
|
+
name
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
`,
|
|
324
|
+
variables: {
|
|
325
|
+
inputs: individuals
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
equal(res.statusCode, 200, 'individuals status code')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
{
|
|
333
|
+
const res = await app.inject({
|
|
334
|
+
method: 'POST',
|
|
335
|
+
url: '/graphql',
|
|
336
|
+
body: {
|
|
337
|
+
query: `
|
|
338
|
+
mutation batch($inputs : [OrganizationInput]!) {
|
|
339
|
+
insertOrganizations(inputs: $inputs) {
|
|
340
|
+
id
|
|
341
|
+
name
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
`,
|
|
345
|
+
variables: {
|
|
346
|
+
inputs: organization
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
equal(res.statusCode, 200, 'organization status code')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
{
|
|
354
|
+
const res = await app.inject({
|
|
355
|
+
method: 'POST',
|
|
356
|
+
url: '/graphql',
|
|
357
|
+
body: {
|
|
358
|
+
query: `
|
|
359
|
+
query {
|
|
360
|
+
organizations {
|
|
361
|
+
id
|
|
362
|
+
name
|
|
363
|
+
individual {
|
|
364
|
+
id
|
|
365
|
+
name
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
`
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
equal(res.statusCode, 200, 'query organization')
|
|
373
|
+
same(res.json(), {
|
|
374
|
+
data: {
|
|
375
|
+
organizations: [{
|
|
376
|
+
id: 1,
|
|
377
|
+
name: 'Platformatic',
|
|
378
|
+
individual: {
|
|
379
|
+
id: 1,
|
|
380
|
+
name: 'Mark'
|
|
381
|
+
}
|
|
382
|
+
}]
|
|
383
|
+
}
|
|
384
|
+
}, 'query organization response')
|
|
385
|
+
}
|
|
386
|
+
})
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('tap')
|
|
4
|
+
const Fastify = require('fastify')
|
|
5
|
+
const WebSocket = require('ws')
|
|
6
|
+
const { once } = require('events')
|
|
7
|
+
const sqlGraphQL = require('..')
|
|
8
|
+
const sqlMapper = require('@platformatic/sql-mapper')
|
|
9
|
+
const sqlEvents = require('@platformatic/sql-events')
|
|
10
|
+
const { clear, connInfo, isSQLite } = require('./helper')
|
|
11
|
+
const stream = require('stream')
|
|
12
|
+
const graphql = require('graphql')
|
|
13
|
+
|
|
14
|
+
async function createBasicPages (db, sql) {
|
|
15
|
+
if (isSQLite) {
|
|
16
|
+
await db.query(sql`CREATE TABLE pages (
|
|
17
|
+
id INTEGER PRIMARY KEY,
|
|
18
|
+
title VARCHAR(42)
|
|
19
|
+
);`)
|
|
20
|
+
} else {
|
|
21
|
+
await db.query(sql`CREATE TABLE pages (
|
|
22
|
+
id SERIAL PRIMARY KEY,
|
|
23
|
+
title VARCHAR(42)
|
|
24
|
+
);`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createWebSocketClient (t, app) {
|
|
29
|
+
const ws = new WebSocket('ws://localhost:' + (app.server.address()).port + '/graphql', 'graphql-ws')
|
|
30
|
+
const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8', objectMode: true })
|
|
31
|
+
t.teardown(client.destroy.bind(client))
|
|
32
|
+
client.setEncoding('utf8')
|
|
33
|
+
return { client, ws }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('subscription - crud', { only: true }, async t => {
|
|
37
|
+
const app = Fastify()
|
|
38
|
+
t.teardown(() => app.close())
|
|
39
|
+
|
|
40
|
+
app.register(sqlMapper, {
|
|
41
|
+
...connInfo,
|
|
42
|
+
async onDatabaseLoad (db, sql) {
|
|
43
|
+
await clear(db, sql)
|
|
44
|
+
await createBasicPages(db, sql)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
app.register(sqlEvents)
|
|
48
|
+
app.register(sqlGraphQL)
|
|
49
|
+
|
|
50
|
+
await app.listen({ port: 0 })
|
|
51
|
+
|
|
52
|
+
const { client } = createWebSocketClient(t, app)
|
|
53
|
+
|
|
54
|
+
client.write(JSON.stringify({
|
|
55
|
+
type: 'connection_init'
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
{
|
|
59
|
+
const query = `subscription {
|
|
60
|
+
pageSaved {
|
|
61
|
+
id
|
|
62
|
+
title
|
|
63
|
+
}
|
|
64
|
+
}`
|
|
65
|
+
client.write(JSON.stringify({
|
|
66
|
+
id: 1,
|
|
67
|
+
type: 'start',
|
|
68
|
+
payload: {
|
|
69
|
+
query
|
|
70
|
+
}
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
const query = `subscription {
|
|
76
|
+
pageDeleted {
|
|
77
|
+
id
|
|
78
|
+
}
|
|
79
|
+
}`
|
|
80
|
+
client.write(JSON.stringify({
|
|
81
|
+
id: 1,
|
|
82
|
+
type: 'start',
|
|
83
|
+
payload: {
|
|
84
|
+
query
|
|
85
|
+
}
|
|
86
|
+
}))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
const [chunk] = await once(client, 'data')
|
|
91
|
+
const data = JSON.parse(chunk)
|
|
92
|
+
t.equal(data.type, 'connection_ack')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
t.comment('sending mutation')
|
|
96
|
+
|
|
97
|
+
await app.inject({
|
|
98
|
+
method: 'POST',
|
|
99
|
+
url: '/graphql',
|
|
100
|
+
body: {
|
|
101
|
+
query: `
|
|
102
|
+
mutation {
|
|
103
|
+
savePage(input: { title: "Hello World" }) {
|
|
104
|
+
id
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
`
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
t.comment('mutation sent')
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
const [chunk] = await once(client, 'data')
|
|
115
|
+
const data = JSON.parse(chunk)
|
|
116
|
+
t.same(data, {
|
|
117
|
+
id: 1,
|
|
118
|
+
type: 'data',
|
|
119
|
+
payload: {
|
|
120
|
+
data: {
|
|
121
|
+
pageSaved: {
|
|
122
|
+
id: '1',
|
|
123
|
+
title: 'Hello World'
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
t.comment('updating entity')
|
|
131
|
+
|
|
132
|
+
await app.inject({
|
|
133
|
+
method: 'POST',
|
|
134
|
+
url: '/graphql',
|
|
135
|
+
body: {
|
|
136
|
+
query: `
|
|
137
|
+
mutation {
|
|
138
|
+
savePage(input: { id: 1, title: "Harry Potter" }) {
|
|
139
|
+
id
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
`
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
t.comment('entity updated')
|
|
147
|
+
|
|
148
|
+
{
|
|
149
|
+
const [chunk] = await once(client, 'data')
|
|
150
|
+
const data = JSON.parse(chunk)
|
|
151
|
+
t.same(data, {
|
|
152
|
+
id: 1,
|
|
153
|
+
type: 'data',
|
|
154
|
+
payload: {
|
|
155
|
+
data: {
|
|
156
|
+
pageSaved: {
|
|
157
|
+
id: '1',
|
|
158
|
+
title: 'Harry Potter'
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
t.comment('deleting entity')
|
|
166
|
+
|
|
167
|
+
await app.inject({
|
|
168
|
+
method: 'POST',
|
|
169
|
+
url: '/graphql',
|
|
170
|
+
body: {
|
|
171
|
+
query: `
|
|
172
|
+
mutation {
|
|
173
|
+
deletePages(where: { id: { eq: 1 } }) {
|
|
174
|
+
id
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
`
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
t.comment('entity deleted')
|
|
182
|
+
|
|
183
|
+
{
|
|
184
|
+
const [chunk] = await once(client, 'data')
|
|
185
|
+
const data = JSON.parse(chunk)
|
|
186
|
+
t.same(data, {
|
|
187
|
+
id: '1',
|
|
188
|
+
type: 'data',
|
|
189
|
+
payload: {
|
|
190
|
+
data: {
|
|
191
|
+
pageDeleted: {
|
|
192
|
+
id: '1'
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
t.comment('sending mutation')
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
const [
|
|
203
|
+
received,
|
|
204
|
+
stored
|
|
205
|
+
] = await Promise.all(([
|
|
206
|
+
(async function () {
|
|
207
|
+
const res = await app.inject({
|
|
208
|
+
method: 'POST',
|
|
209
|
+
url: '/graphql',
|
|
210
|
+
body: {
|
|
211
|
+
query: `
|
|
212
|
+
mutation batch($inputs : [PageInput]!) {
|
|
213
|
+
insertPages (inputs: $inputs) {
|
|
214
|
+
id
|
|
215
|
+
title
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
`,
|
|
219
|
+
variables: {
|
|
220
|
+
inputs: [
|
|
221
|
+
{ title: 'Page 1' },
|
|
222
|
+
{ title: 'Page 2' },
|
|
223
|
+
{ title: 'Page 3' }
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
t.comment('mutation sent')
|
|
229
|
+
const pages = res.json().data.insertPages
|
|
230
|
+
t.comment(JSON.stringify(pages, null, 2))
|
|
231
|
+
return pages
|
|
232
|
+
})(),
|
|
233
|
+
(async function () {
|
|
234
|
+
const pages = []
|
|
235
|
+
const second = new stream.PassThrough({ objectMode: true })
|
|
236
|
+
client.pipe(second)
|
|
237
|
+
for await (const chunk of second) {
|
|
238
|
+
const data = JSON.parse(chunk)
|
|
239
|
+
pages.push(data.payload.data.pageSaved)
|
|
240
|
+
if (pages.length === 3) {
|
|
241
|
+
break
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
t.comment('received all pages', JSON.stringify(pages, null, 2))
|
|
245
|
+
return pages
|
|
246
|
+
})()
|
|
247
|
+
]))
|
|
248
|
+
|
|
249
|
+
t.same(received, stored)
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('subscription - ignore', async t => {
|
|
254
|
+
const app = Fastify()
|
|
255
|
+
t.teardown(() => app.close())
|
|
256
|
+
|
|
257
|
+
app.register(sqlMapper, {
|
|
258
|
+
...connInfo,
|
|
259
|
+
async onDatabaseLoad (db, sql) {
|
|
260
|
+
await clear(db, sql)
|
|
261
|
+
await createBasicPages(db, sql)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
app.register(sqlEvents)
|
|
265
|
+
app.register(sqlGraphQL, {
|
|
266
|
+
subscriptionIgnore: ['page']
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
await app.ready()
|
|
270
|
+
t.equal(graphql.printSchema(app.graphql.schema).indexOf('type Subscription'), -1)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
test('subscription - crud with two schemas and a ignore', async t => {
|
|
274
|
+
const app = Fastify()
|
|
275
|
+
t.teardown(() => app.close())
|
|
276
|
+
|
|
277
|
+
app.register(sqlMapper, {
|
|
278
|
+
...connInfo,
|
|
279
|
+
async onDatabaseLoad (db, sql) {
|
|
280
|
+
await clear(db, sql)
|
|
281
|
+
await createBasicPages(db, sql)
|
|
282
|
+
|
|
283
|
+
if (isSQLite) {
|
|
284
|
+
await db.query(sql`CREATE TABLE categories (
|
|
285
|
+
id INTEGER PRIMARY KEY,
|
|
286
|
+
name VARCHAR(42)
|
|
287
|
+
);`)
|
|
288
|
+
} else {
|
|
289
|
+
await db.query(sql`CREATE TABLE categories (
|
|
290
|
+
id SERIAL PRIMARY KEY,
|
|
291
|
+
name VARCHAR(42)
|
|
292
|
+
);`)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
app.register(sqlEvents)
|
|
297
|
+
app.register(sqlGraphQL, {
|
|
298
|
+
subscriptionIgnore: ['category']
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
await app.listen({ port: 0 })
|
|
302
|
+
|
|
303
|
+
const { client } = createWebSocketClient(t, app)
|
|
304
|
+
t.teardown(() => client.destroy())
|
|
305
|
+
|
|
306
|
+
client.write(JSON.stringify({
|
|
307
|
+
type: 'connection_init'
|
|
308
|
+
}))
|
|
309
|
+
|
|
310
|
+
{
|
|
311
|
+
const [chunk] = await once(client, 'data')
|
|
312
|
+
const data = JSON.parse(chunk)
|
|
313
|
+
t.equal(data.type, 'connection_ack')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
{
|
|
317
|
+
const query = `subscription {
|
|
318
|
+
categorySaved {
|
|
319
|
+
id
|
|
320
|
+
}
|
|
321
|
+
}`
|
|
322
|
+
client.write(JSON.stringify({
|
|
323
|
+
id: 1,
|
|
324
|
+
type: 'start',
|
|
325
|
+
payload: {
|
|
326
|
+
query
|
|
327
|
+
}
|
|
328
|
+
}))
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
{
|
|
332
|
+
const [chunk] = await once(client, 'data')
|
|
333
|
+
const data = JSON.parse(chunk)
|
|
334
|
+
t.equal(data.payload, 'The subscription field "categorySaved" is not defined.')
|
|
335
|
+
}
|
|
336
|
+
})
|