@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.
- package/lib/entity-to-type.js +70 -22
- package/lib/relationship.js +9 -6
- package/lib/subscriptions.js +10 -5
- package/lib/utils.js +2 -0
- package/package.json +3 -3
- package/test/composite.test.js +331 -0
- package/test/helper.js +5 -0
- package/test/simple.test.js +57 -1
- package/test/subscription.test.js +1 -1
- package/test/where.test.js +288 -0
package/lib/entity-to-type.js
CHANGED
|
@@ -16,7 +16,7 @@ const ascDesc = new graphql.GraphQLEnumType({
|
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
function constructGraph (app, entity, opts) {
|
|
19
|
-
const
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "${
|
|
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 =
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/relationship.js
CHANGED
|
@@ -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 =
|
|
40
|
-
|
|
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]
|
package/lib/subscriptions.js
CHANGED
|
@@ -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
|
-
[
|
|
59
|
-
type: meta.fields[
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/sql-graphql",
|
|
3
|
-
"version": "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.
|
|
18
|
-
"@platformatic/sql-mapper": "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) {
|
package/test/simple.test.js
CHANGED
|
@@ -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
|
+
})
|
package/test/where.test.js
CHANGED
|
@@ -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
|
+
})
|