@platformatic/sql-graphql 3.0.0-alpha.5 → 3.0.0-alpha.6
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/eslint.config.js +2 -2
- package/index.js +32 -34
- package/lib/entity-to-type.js +116 -112
- package/lib/errors.js +11 -9
- package/lib/relationship.js +25 -17
- package/lib/subscriptions.js +13 -17
- package/lib/telemetry.js +25 -27
- package/lib/utils.js +8 -14
- package/package.json +13 -13
package/eslint.config.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
import neostandard from 'neostandard'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
export default neostandard({ ts: true })
|
package/index.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const scalars = require('graphql-scalars')
|
|
13
|
-
const errors = require('./lib/errors')
|
|
1
|
+
import { mercuriusFederationPlugin } from '@mercuriusjs/federation'
|
|
2
|
+
import { findNearestString } from '@platformatic/foundation'
|
|
3
|
+
import fp from 'fastify-plugin'
|
|
4
|
+
import { GraphQLObjectType, GraphQLSchema, GraphQLString, printSchema } from 'graphql'
|
|
5
|
+
import { GraphQLDate, GraphQLDateTime } from 'graphql-scalars'
|
|
6
|
+
import mercurius from 'mercurius'
|
|
7
|
+
import { constructGraph } from './lib/entity-to-type.js'
|
|
8
|
+
import { ErrorPrintingGraphQLSchema } from './lib/errors.js'
|
|
9
|
+
import { establishRelations } from './lib/relationship.js'
|
|
10
|
+
import { setupSubscriptions } from './lib/subscriptions.js'
|
|
11
|
+
import { setupTelemetry } from './lib/telemetry.js'
|
|
14
12
|
|
|
15
13
|
async function mapperToGraphql (app, opts) {
|
|
16
14
|
const mapper = app.platformatic
|
|
@@ -28,19 +26,18 @@ async function mapperToGraphql (app, opts) {
|
|
|
28
26
|
resolvers,
|
|
29
27
|
loaders,
|
|
30
28
|
federationReplacements,
|
|
31
|
-
federationMetadata: opts.federationMetadata
|
|
29
|
+
federationMetadata: opts.federationMetadata
|
|
32
30
|
}
|
|
33
31
|
const metaMap = new Map()
|
|
34
32
|
|
|
35
33
|
if (Object.keys(mapper.entities).length === 0) {
|
|
36
34
|
// no schema
|
|
37
|
-
queryTopFields.hello = { type:
|
|
35
|
+
queryTopFields.hello = { type: GraphQLString }
|
|
38
36
|
resolvers.Query = {
|
|
39
|
-
hello: () => 'Hello Platformatic!'
|
|
37
|
+
hello: () => 'Hello Platformatic!'
|
|
40
38
|
}
|
|
41
39
|
} else {
|
|
42
|
-
const entitiesNames = Object.values(mapper.entities)
|
|
43
|
-
.map(entity => entity.singularName)
|
|
40
|
+
const entitiesNames = Object.values(mapper.entities).map(entity => entity.singularName)
|
|
44
41
|
|
|
45
42
|
for (const ignoredEntity of Object.keys(ignore)) {
|
|
46
43
|
if (!entitiesNames.includes(ignoredEntity)) {
|
|
@@ -94,17 +91,18 @@ async function mapperToGraphql (app, opts) {
|
|
|
94
91
|
}
|
|
95
92
|
}
|
|
96
93
|
|
|
97
|
-
const query = new
|
|
94
|
+
const query = new GraphQLObjectType({
|
|
98
95
|
name: 'Query',
|
|
99
|
-
fields: queryTopFields
|
|
96
|
+
fields: queryTopFields
|
|
100
97
|
})
|
|
101
98
|
|
|
102
|
-
const mutation =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
const mutation =
|
|
100
|
+
Object.keys(mutationTopFields).length > 0
|
|
101
|
+
? new GraphQLObjectType({
|
|
102
|
+
name: 'Mutation',
|
|
103
|
+
fields: mutationTopFields
|
|
104
|
+
})
|
|
105
|
+
: null
|
|
108
106
|
|
|
109
107
|
if (!mutation) {
|
|
110
108
|
delete resolvers.Mutation
|
|
@@ -130,7 +128,7 @@ async function mapperToGraphql (app, opts) {
|
|
|
130
128
|
|
|
131
129
|
if (entitiesList.length > 0) {
|
|
132
130
|
opts.subscription = {
|
|
133
|
-
emitter: app.platformatic.mq
|
|
131
|
+
emitter: app.platformatic.mq
|
|
134
132
|
}
|
|
135
133
|
// TODO support ignoring some of those
|
|
136
134
|
subscription = setupSubscriptions(app, metaMap, resolvers, ignoreList)
|
|
@@ -139,14 +137,14 @@ async function mapperToGraphql (app, opts) {
|
|
|
139
137
|
|
|
140
138
|
let sdl = ''
|
|
141
139
|
try {
|
|
142
|
-
sdl =
|
|
140
|
+
sdl = printSchema(new GraphQLSchema({ query, mutation, subscription }))
|
|
143
141
|
} catch (error) {
|
|
144
142
|
// The next lines are excluded from test coverage:
|
|
145
143
|
// it's quite hard to test the following lines for all of the DB types
|
|
146
144
|
/* istanbul ignore next */
|
|
147
145
|
app.log.debug({ query, mutation, subscription }, 'GraphQL input schema')
|
|
148
146
|
/* istanbul ignore next */
|
|
149
|
-
const newError = new
|
|
147
|
+
const newError = new ErrorPrintingGraphQLSchema()
|
|
150
148
|
/* istanbul ignore next */
|
|
151
149
|
newError.cause = error
|
|
152
150
|
/* istanbul ignore next */
|
|
@@ -167,10 +165,10 @@ async function mapperToGraphql (app, opts) {
|
|
|
167
165
|
// Ignoriring because SQLite doesn't support dates
|
|
168
166
|
/* istanbul ignore next */
|
|
169
167
|
if (sdl.match(/scalar Date\n/)) {
|
|
170
|
-
resolvers.Date =
|
|
168
|
+
resolvers.Date = GraphQLDate
|
|
171
169
|
}
|
|
172
170
|
if (sdl.indexOf('scalar DateTime') >= 0) {
|
|
173
|
-
resolvers.DateTime =
|
|
171
|
+
resolvers.DateTime = GraphQLDateTime
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
opts.graphiql = opts.graphiql !== false
|
|
@@ -184,7 +182,7 @@ async function mapperToGraphql (app, opts) {
|
|
|
184
182
|
...opts,
|
|
185
183
|
schema: sdl,
|
|
186
184
|
loaders,
|
|
187
|
-
resolvers
|
|
185
|
+
resolvers
|
|
188
186
|
})
|
|
189
187
|
|
|
190
188
|
if (app.openTelemetry) {
|
|
@@ -194,5 +192,5 @@ async function mapperToGraphql (app, opts) {
|
|
|
194
192
|
app.log.debug({ schema: sdl }, 'computed schema')
|
|
195
193
|
}
|
|
196
194
|
|
|
197
|
-
|
|
198
|
-
|
|
195
|
+
export default fp(mapperToGraphql)
|
|
196
|
+
export * as errors from './lib/errors.js'
|
package/lib/entity-to-type.js
CHANGED
|
@@ -1,52 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { findNearestString } from '@platformatic/foundation'
|
|
2
|
+
import camelcase from 'camelcase'
|
|
3
|
+
import {
|
|
4
|
+
GraphQLEnumType,
|
|
5
|
+
GraphQLID,
|
|
6
|
+
GraphQLInputObjectType,
|
|
7
|
+
GraphQLInt,
|
|
8
|
+
GraphQLList,
|
|
9
|
+
GraphQLNonNull,
|
|
10
|
+
GraphQLObjectType,
|
|
11
|
+
GraphQLScalarType
|
|
12
|
+
} from 'graphql'
|
|
13
|
+
import { UnableToGenerateGraphQLEnumTypeError } from './errors.js'
|
|
14
|
+
import { fromSelectionSet, sqlTypeToGraphQL } from './utils.js'
|
|
15
|
+
|
|
16
|
+
const ascDesc = new GraphQLEnumType({
|
|
13
17
|
name: 'OrderByDirection',
|
|
14
18
|
values: {
|
|
15
19
|
ASC: { value: 'ASC' },
|
|
16
|
-
DESC: { value: 'DESC' }
|
|
17
|
-
}
|
|
20
|
+
DESC: { value: 'DESC' }
|
|
21
|
+
}
|
|
18
22
|
})
|
|
19
23
|
|
|
20
|
-
const limitType = new
|
|
24
|
+
const limitType = new GraphQLScalarType({
|
|
21
25
|
name: 'LimitInt',
|
|
22
|
-
description:
|
|
26
|
+
description:
|
|
27
|
+
'Limit will be applied by default if not passed. If the provided value exceeds the maximum allowed value a validation error will be thrown'
|
|
23
28
|
})
|
|
24
29
|
|
|
25
|
-
function constructGraph (app, entity, opts, ignore) {
|
|
26
|
-
const primaryKeys = Array.from(entity.primaryKeys).map(
|
|
27
|
-
const relationalFields = entity.relations
|
|
28
|
-
.map((relation) => relation.column_name)
|
|
30
|
+
export function constructGraph (app, entity, opts, ignore) {
|
|
31
|
+
const primaryKeys = Array.from(entity.primaryKeys).map(key => camelcase(key))
|
|
32
|
+
const relationalFields = entity.relations.map(relation => relation.column_name)
|
|
29
33
|
const entityName = entity.name
|
|
30
34
|
const singular = entity.singularName
|
|
31
35
|
const plural = entity.pluralName
|
|
32
|
-
const {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
resolvers,
|
|
36
|
-
federationReplacements,
|
|
37
|
-
federationMetadata,
|
|
38
|
-
loaders,
|
|
39
|
-
} = opts
|
|
40
|
-
|
|
41
|
-
const entityFieldsNames = Object.values(entity.fields)
|
|
42
|
-
.map(field => field.camelcase)
|
|
36
|
+
const { queryTopFields, mutationTopFields, resolvers, federationReplacements, federationMetadata, loaders } = opts
|
|
37
|
+
|
|
38
|
+
const entityFieldsNames = Object.values(entity.fields).map(field => field.camelcase)
|
|
43
39
|
|
|
44
40
|
for (const ignoredField of Object.keys(ignore)) {
|
|
45
41
|
if (!entityFieldsNames.includes(ignoredField)) {
|
|
46
42
|
const nearestField = findNearestString(entityFieldsNames, ignoredField)
|
|
47
43
|
app.log.warn(
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
`Ignored graphql field "${ignoredField}" not found in entity "${entity.singularName}".` +
|
|
45
|
+
` Did you mean "${nearestField}"?`
|
|
50
46
|
)
|
|
51
47
|
}
|
|
52
48
|
}
|
|
@@ -66,7 +62,7 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
66
62
|
/* istanbul ignore next */
|
|
67
63
|
if (field.isArray) {
|
|
68
64
|
const listType = sqlTypeToGraphQL(field.sqlType)
|
|
69
|
-
meta.type = new
|
|
65
|
+
meta.type = new GraphQLList(listType)
|
|
70
66
|
} else if (field.enum) {
|
|
71
67
|
const enumValues = field.enum.reduce((acc, enumValue, index) => {
|
|
72
68
|
let key = enumValue.replace(/[^\w]/g, '_')
|
|
@@ -85,38 +81,46 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
85
81
|
}, {})
|
|
86
82
|
try {
|
|
87
83
|
const name = camelcase([entityName, key])
|
|
88
|
-
meta.type = new
|
|
84
|
+
meta.type = new GraphQLEnumType({ name, values: enumValues })
|
|
89
85
|
} catch (error) {
|
|
90
86
|
app.log.error({ key, enumValues, entityName, table: entity.table, schema: entity.schema })
|
|
91
|
-
throw new
|
|
87
|
+
throw new UnableToGenerateGraphQLEnumTypeError()
|
|
92
88
|
}
|
|
93
89
|
} else {
|
|
94
90
|
meta.type = sqlTypeToGraphQL(field.sqlType)
|
|
95
91
|
}
|
|
96
92
|
if (field.primaryKey) {
|
|
97
93
|
meta.primaryKeyType = field.type
|
|
98
|
-
meta.type =
|
|
94
|
+
meta.type = GraphQLID
|
|
99
95
|
} else if (field.foreignKey) {
|
|
100
|
-
meta.type =
|
|
96
|
+
meta.type = GraphQLID
|
|
101
97
|
}
|
|
102
98
|
fields[field.camelcase] = meta
|
|
103
99
|
}
|
|
104
100
|
|
|
105
|
-
const type = new
|
|
101
|
+
const type = new GraphQLObjectType({
|
|
106
102
|
name: entityName,
|
|
107
|
-
fields
|
|
103
|
+
fields
|
|
108
104
|
})
|
|
109
105
|
|
|
110
106
|
resolvers.Query = resolvers.Query || {}
|
|
111
107
|
resolvers.Mutation = resolvers.Mutation || {}
|
|
112
108
|
loaders.Query = loaders.Query || {}
|
|
113
109
|
|
|
114
|
-
const getBy = camelcase(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
110
|
+
const getBy = camelcase(
|
|
111
|
+
[
|
|
112
|
+
'get',
|
|
113
|
+
singular,
|
|
114
|
+
...primaryKeys
|
|
115
|
+
.map((key, i) => {
|
|
116
|
+
if (i === 0) {
|
|
117
|
+
return ['by', key]
|
|
118
|
+
}
|
|
119
|
+
return ['and', key]
|
|
120
|
+
})
|
|
121
|
+
.flat()
|
|
122
|
+
].join('_')
|
|
123
|
+
)
|
|
120
124
|
|
|
121
125
|
const whereFields = Object.keys(fields).reduce((acc, field) => {
|
|
122
126
|
let graphqlFields
|
|
@@ -130,50 +134,52 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
130
134
|
lt: { type: fields[field].type },
|
|
131
135
|
lte: { type: fields[field].type },
|
|
132
136
|
like: { type: fields[field].type },
|
|
133
|
-
in: { type: new
|
|
134
|
-
nin: { type: new
|
|
137
|
+
in: { type: new GraphQLList(fields[field].type) },
|
|
138
|
+
nin: { type: new GraphQLList(fields[field].type) }
|
|
135
139
|
}
|
|
136
140
|
} else {
|
|
137
141
|
graphqlFields = {
|
|
138
142
|
any: { type: fields[field].type.ofType },
|
|
139
143
|
all: { type: fields[field].type.ofType },
|
|
140
|
-
contains: { type: new
|
|
141
|
-
contained: { type: new
|
|
142
|
-
overlaps: { type: new
|
|
144
|
+
contains: { type: new GraphQLList(fields[field].type) },
|
|
145
|
+
contained: { type: new GraphQLList(fields[field].type) },
|
|
146
|
+
overlaps: { type: new GraphQLList(fields[field].type) }
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
acc[field] = {
|
|
146
|
-
type: new
|
|
150
|
+
type: new GraphQLInputObjectType({
|
|
147
151
|
name: `${entityName}WhereArguments${field}`,
|
|
148
|
-
fields: graphqlFields
|
|
149
|
-
})
|
|
152
|
+
fields: graphqlFields
|
|
153
|
+
})
|
|
150
154
|
}
|
|
151
155
|
return acc
|
|
152
156
|
}, {})
|
|
153
157
|
|
|
154
|
-
const whereArgType = new
|
|
158
|
+
const whereArgType = new GraphQLInputObjectType({
|
|
155
159
|
name: `${entityName}WhereArguments`,
|
|
156
160
|
fields: {
|
|
157
161
|
...whereFields,
|
|
158
162
|
or: {
|
|
159
|
-
type: new
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
type: new GraphQLList(
|
|
164
|
+
new GraphQLInputObjectType({
|
|
165
|
+
name: `${entityName}WhereArgumentsOr`,
|
|
166
|
+
fields: {
|
|
167
|
+
...whereFields
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
167
173
|
})
|
|
168
174
|
|
|
169
175
|
queryTopFields[getBy] = {
|
|
170
176
|
type,
|
|
171
177
|
args: {
|
|
172
|
-
...
|
|
173
|
-
acc[primaryKey] = { type: new
|
|
178
|
+
...primaryKeys.reduce((acc, primaryKey) => {
|
|
179
|
+
acc[primaryKey] = { type: new GraphQLNonNull(fields[primaryKey].type) }
|
|
174
180
|
return acc
|
|
175
|
-
}, {})
|
|
176
|
-
}
|
|
181
|
+
}, {})
|
|
182
|
+
}
|
|
177
183
|
}
|
|
178
184
|
loaders.Query[getBy] = {
|
|
179
185
|
loader (queries, ctx) {
|
|
@@ -188,49 +194,51 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
188
194
|
return loadMany(keys, queries, ctx)
|
|
189
195
|
},
|
|
190
196
|
opts: {
|
|
191
|
-
cache: false
|
|
192
|
-
}
|
|
197
|
+
cache: false
|
|
198
|
+
}
|
|
193
199
|
}
|
|
194
200
|
|
|
195
|
-
const orderByFields = new
|
|
201
|
+
const orderByFields = new GraphQLEnumType({
|
|
196
202
|
name: `${entityName}OrderByField`,
|
|
197
203
|
values: Object.keys(fields).reduce((acc, field) => {
|
|
198
204
|
/* istanbul ignore else */
|
|
199
205
|
if (!fields[field].isArray) {
|
|
200
206
|
acc[field] = {
|
|
201
|
-
value: field
|
|
207
|
+
value: field
|
|
202
208
|
}
|
|
203
209
|
}
|
|
204
210
|
return acc
|
|
205
|
-
}, {})
|
|
211
|
+
}, {})
|
|
206
212
|
})
|
|
207
213
|
queryTopFields[plural] = {
|
|
208
|
-
type: new
|
|
214
|
+
type: new GraphQLList(type),
|
|
209
215
|
args: {
|
|
210
216
|
limit: { type: limitType },
|
|
211
|
-
offset: { type:
|
|
217
|
+
offset: { type: GraphQLInt },
|
|
212
218
|
orderBy: {
|
|
213
|
-
type: new
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
219
|
+
type: new GraphQLList(
|
|
220
|
+
new GraphQLInputObjectType({
|
|
221
|
+
name: `${entityName}OrderByArguments`,
|
|
222
|
+
fields: {
|
|
223
|
+
field: { type: orderByFields },
|
|
224
|
+
direction: { type: new GraphQLNonNull(ascDesc) }
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
)
|
|
220
228
|
},
|
|
221
|
-
where: { type: whereArgType }
|
|
222
|
-
}
|
|
229
|
+
where: { type: whereArgType }
|
|
230
|
+
}
|
|
223
231
|
}
|
|
224
232
|
|
|
225
233
|
resolvers.Query[plural] = (_, query, ctx, info) => {
|
|
226
|
-
const requestedFields = info.fieldNodes[0].selectionSet.selections.map(
|
|
234
|
+
const requestedFields = info.fieldNodes[0].selectionSet.selections.map(s => s.name.value)
|
|
227
235
|
for (const primaryKey of primaryKeys) {
|
|
228
236
|
requestedFields.push(primaryKey)
|
|
229
237
|
}
|
|
230
238
|
return entity.find({ ...query, fields: [...requestedFields, ...relationalFields], ctx })
|
|
231
239
|
}
|
|
232
240
|
|
|
233
|
-
const inputType = new
|
|
241
|
+
const inputType = new GraphQLInputObjectType({
|
|
234
242
|
name: `${entityName}Input`,
|
|
235
243
|
fields: Object.keys(fields).reduce((acc, field) => {
|
|
236
244
|
const meta = fields[field]
|
|
@@ -239,27 +247,27 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
239
247
|
acc[field] = meta
|
|
240
248
|
}
|
|
241
249
|
return acc
|
|
242
|
-
}, {})
|
|
250
|
+
}, {})
|
|
243
251
|
})
|
|
244
252
|
|
|
245
253
|
const count = camelcase(['count', plural])
|
|
246
254
|
|
|
247
|
-
const countType = new
|
|
255
|
+
const countType = new GraphQLObjectType({
|
|
248
256
|
name: `${plural}Count`,
|
|
249
257
|
fields: {
|
|
250
|
-
total: { type:
|
|
251
|
-
}
|
|
258
|
+
total: { type: GraphQLInt }
|
|
259
|
+
}
|
|
252
260
|
})
|
|
253
261
|
|
|
254
262
|
queryTopFields[count] = {
|
|
255
263
|
type: countType,
|
|
256
264
|
args: {
|
|
257
|
-
where: { type: whereArgType }
|
|
258
|
-
}
|
|
265
|
+
where: { type: whereArgType }
|
|
266
|
+
}
|
|
259
267
|
}
|
|
260
268
|
|
|
261
269
|
resolvers.Query[count] = async (_, query, ctx, info) => {
|
|
262
|
-
const requestedFields = info.fieldNodes[0].selectionSet.selections.map(
|
|
270
|
+
const requestedFields = info.fieldNodes[0].selectionSet.selections.map(s => s.name.value)
|
|
263
271
|
for (const primaryKey of primaryKeys) {
|
|
264
272
|
requestedFields.push(primaryKey)
|
|
265
273
|
}
|
|
@@ -272,8 +280,8 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
272
280
|
mutationTopFields[save] = {
|
|
273
281
|
type,
|
|
274
282
|
args: {
|
|
275
|
-
input: { type: new
|
|
276
|
-
}
|
|
283
|
+
input: { type: new GraphQLNonNull(inputType) }
|
|
284
|
+
}
|
|
277
285
|
}
|
|
278
286
|
|
|
279
287
|
resolvers.Mutation[save] = async (_, { input }, ctx, info) => {
|
|
@@ -284,10 +292,10 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
284
292
|
const insert = camelcase(['insert', plural])
|
|
285
293
|
|
|
286
294
|
mutationTopFields[insert] = {
|
|
287
|
-
type: new
|
|
295
|
+
type: new GraphQLList(type),
|
|
288
296
|
args: {
|
|
289
|
-
inputs: { type: new
|
|
290
|
-
}
|
|
297
|
+
inputs: { type: new GraphQLNonNull(new GraphQLList(inputType)) }
|
|
298
|
+
}
|
|
291
299
|
}
|
|
292
300
|
|
|
293
301
|
resolvers.Mutation[insert] = (_, { inputs }, ctx, info) => {
|
|
@@ -297,20 +305,20 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
297
305
|
|
|
298
306
|
const deleteKey = camelcase(['delete', plural])
|
|
299
307
|
mutationTopFields[deleteKey] = {
|
|
300
|
-
type: new
|
|
308
|
+
type: new GraphQLList(type),
|
|
301
309
|
args: {
|
|
302
|
-
where: { type: whereArgType }
|
|
303
|
-
}
|
|
310
|
+
where: { type: whereArgType }
|
|
311
|
+
}
|
|
304
312
|
}
|
|
305
313
|
|
|
306
314
|
resolvers.Mutation[deleteKey] = (_, args, ctx, info) => {
|
|
307
|
-
const fields = info.fieldNodes[0].selectionSet.selections.map(
|
|
315
|
+
const fields = info.fieldNodes[0].selectionSet.selections.map(s => s.name.value)
|
|
308
316
|
return entity.delete({ ...args, fields: [...fields, ...relationalFields], ctx })
|
|
309
317
|
}
|
|
310
318
|
|
|
311
319
|
federationReplacements.push({
|
|
312
320
|
find: new RegExp(`type ${entityName}`),
|
|
313
|
-
replace: `type ${entityName} @key(fields: "${primaryKeys}")
|
|
321
|
+
replace: `type ${entityName} @key(fields: "${primaryKeys}")`
|
|
314
322
|
})
|
|
315
323
|
|
|
316
324
|
if (federationMetadata) {
|
|
@@ -328,8 +336,8 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
328
336
|
return loadMany(keys, queries, ctx)
|
|
329
337
|
},
|
|
330
338
|
opts: {
|
|
331
|
-
cache: false
|
|
332
|
-
}
|
|
339
|
+
cache: false
|
|
340
|
+
}
|
|
333
341
|
}
|
|
334
342
|
}
|
|
335
343
|
|
|
@@ -339,7 +347,7 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
339
347
|
loadMany,
|
|
340
348
|
getFields,
|
|
341
349
|
relationalFields,
|
|
342
|
-
fields
|
|
350
|
+
fields
|
|
343
351
|
}
|
|
344
352
|
|
|
345
353
|
async function loadMany (keys, queries, ctx) {
|
|
@@ -357,7 +365,7 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
357
365
|
acc[key].in.push(value)
|
|
358
366
|
} else {
|
|
359
367
|
acc[key] = {
|
|
360
|
-
in: [value]
|
|
368
|
+
in: [value]
|
|
361
369
|
}
|
|
362
370
|
}
|
|
363
371
|
return acc
|
|
@@ -369,16 +377,14 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
369
377
|
where,
|
|
370
378
|
fields,
|
|
371
379
|
limit,
|
|
372
|
-
ctx
|
|
380
|
+
ctx
|
|
373
381
|
})
|
|
374
382
|
|
|
375
383
|
const matchedRaws = []
|
|
376
384
|
// TODO this is extremely inefficient
|
|
377
385
|
// we need a better data structure
|
|
378
386
|
for (const pair of keys) {
|
|
379
|
-
const matchedRaw = rows.find((raw)
|
|
380
|
-
pair.every(({ key, value }) => raw[key] === value)
|
|
381
|
-
) || null
|
|
387
|
+
const matchedRaw = rows.find(raw => pair.every(({ key, value }) => raw[key] === value)) || null
|
|
382
388
|
matchedRaws.push(matchedRaw)
|
|
383
389
|
}
|
|
384
390
|
|
|
@@ -397,5 +403,3 @@ function constructGraph (app, entity, opts, ignore) {
|
|
|
397
403
|
return [...fields]
|
|
398
404
|
}
|
|
399
405
|
}
|
|
400
|
-
|
|
401
|
-
module.exports = constructGraph
|
package/lib/errors.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import createError from '@fastify/error'
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
export const ERROR_PREFIX = 'PLT_SQL_GRAPHQL'
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
5
|
+
export const UnableToGenerateGraphQLEnumTypeError = createError(
|
|
6
|
+
`${ERROR_PREFIX}_UNABLE_GENERATE_GRAPHQL_ENUM_TYPE`,
|
|
7
|
+
'Unable to generate GraphQLEnumType'
|
|
8
|
+
)
|
|
9
|
+
export const UnsupportedKindError = createError(`${ERROR_PREFIX}_UNSUPPORTED_KIND`, 'Unsupported kind: %s')
|
|
10
|
+
export const ErrorPrintingGraphQLSchema = createError(
|
|
11
|
+
`${ERROR_PREFIX}_ERROR_PRINTING_GRAPHQL_SCHEMA`,
|
|
12
|
+
'Error printing the GraphQL schema'
|
|
13
|
+
)
|
package/lib/relationship.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
1
|
/* eslint camelcase: 0 */
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} = require('./utils')
|
|
9
|
-
const assert = require('assert')
|
|
3
|
+
import assert from 'assert'
|
|
4
|
+
import camelcase from 'camelcase'
|
|
5
|
+
import { fromSelectionSet } from './utils.js'
|
|
10
6
|
|
|
11
|
-
|
|
7
|
+
export function establishRelations (app, relations, resolvers, loaders, queryTopFields, relationships, metaMap) {
|
|
12
8
|
const entitiesTypeMap = {}
|
|
13
9
|
const entities = app.platformatic.entities
|
|
14
10
|
for (const key of Object.keys(entities)) {
|
|
15
11
|
entitiesTypeMap[key] = metaMap.get(entities[key])
|
|
16
12
|
}
|
|
17
13
|
for (const relation of relations) {
|
|
18
|
-
const {
|
|
14
|
+
const {
|
|
15
|
+
table_name,
|
|
16
|
+
foreign_table_name,
|
|
17
|
+
column_name,
|
|
18
|
+
foreign_column_name,
|
|
19
|
+
entityName,
|
|
20
|
+
foreignEntityName,
|
|
21
|
+
loweredTableWithSchemaName
|
|
22
|
+
} = relation
|
|
19
23
|
const enhanceAssertLogMsg = `(table: "${table_name}", foreign table: "${foreign_table_name}", column: "${column_name}")`
|
|
20
24
|
|
|
21
25
|
assert(table_name, `table_name is required ${enhanceAssertLogMsg}`)
|
|
@@ -23,7 +27,10 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
|
|
|
23
27
|
|
|
24
28
|
const current = entitiesTypeMap[entityName]
|
|
25
29
|
const foreign = entitiesTypeMap[foreignEntityName]
|
|
26
|
-
assert(
|
|
30
|
+
assert(
|
|
31
|
+
foreign !== undefined,
|
|
32
|
+
`No foreign table named "${foreign_table_name}" was found ${enhanceAssertLogMsg}. Relation: ${JSON.stringify(relation, null, 2)}`
|
|
33
|
+
)
|
|
27
34
|
|
|
28
35
|
// current to foreign, we skip this if the foreign table has a composite primary key
|
|
29
36
|
// TODO implement support for this case
|
|
@@ -40,17 +47,15 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
|
|
|
40
47
|
loader (queries, ctx) {
|
|
41
48
|
const keys = []
|
|
42
49
|
for (const { obj } of queries) {
|
|
43
|
-
const value = obj[originalField] != null
|
|
44
|
-
? obj[originalField].toString()
|
|
45
|
-
: null
|
|
50
|
+
const value = obj[originalField] != null ? obj[originalField].toString() : null
|
|
46
51
|
|
|
47
52
|
keys.push([{ key, value }])
|
|
48
53
|
}
|
|
49
54
|
return foreign.loadMany(keys, queries, ctx)
|
|
50
55
|
},
|
|
51
56
|
opts: {
|
|
52
|
-
cache: false
|
|
53
|
-
}
|
|
57
|
+
cache: false
|
|
58
|
+
}
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
}
|
|
@@ -76,7 +81,10 @@ module.exports = function establishRelations (app, relations, resolvers, loaders
|
|
|
76
81
|
resolvers[foreign.type][lowered] = async function (obj, args, ctx, info) {
|
|
77
82
|
const previousResponse = await previousResolve(obj, args, ctx, info)
|
|
78
83
|
const currentResponse = await resolveRelation(obj, args, ctx, info)
|
|
79
|
-
return uniqueBy(
|
|
84
|
+
return uniqueBy(
|
|
85
|
+
[...previousResponse, ...currentResponse],
|
|
86
|
+
findPrimaryColumnName(current.entity.camelCasedFields)
|
|
87
|
+
)
|
|
80
88
|
}
|
|
81
89
|
}
|
|
82
90
|
}
|
|
@@ -109,4 +117,4 @@ function unique (arr) {
|
|
|
109
117
|
function uniqueBy (arr, key) {
|
|
110
118
|
const uniqueValues = unique(arr.map(el => el[key]))
|
|
111
119
|
return uniqueValues.map(value => arr.find(el => el[key] === value))
|
|
112
|
-
}
|
|
120
|
+
}
|
package/lib/subscriptions.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import camelcase from 'camelcase'
|
|
2
|
+
import { GraphQLObjectType } from 'graphql'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
const camelcase = require('camelcase')
|
|
5
|
-
|
|
6
|
-
function setupSubscriptions (app, metaMap, resolvers, ignores) {
|
|
4
|
+
export function setupSubscriptions (app, metaMap, resolvers, ignores) {
|
|
7
5
|
const fields = {}
|
|
8
6
|
resolvers.Subscription = {}
|
|
9
7
|
for (const [field, meta] of metaMap) {
|
|
@@ -21,7 +19,7 @@ function setupSubscriptions (app, metaMap, resolvers, ignores) {
|
|
|
21
19
|
const { type } = meta
|
|
22
20
|
const saved = `${field.singularName}Saved`
|
|
23
21
|
fields[saved] = {
|
|
24
|
-
type
|
|
22
|
+
type
|
|
25
23
|
}
|
|
26
24
|
resolvers.Subscription[saved] = {
|
|
27
25
|
subscribe: async function * (_, query, ctx, info) {
|
|
@@ -54,19 +52,19 @@ function setupSubscriptions (app, metaMap, resolvers, ignores) {
|
|
|
54
52
|
log.warn({ err, entity: field.singularName }, 'graphql subscription error')
|
|
55
53
|
}
|
|
56
54
|
}
|
|
57
|
-
}
|
|
55
|
+
}
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
const deleted = `${field.singularName}Deleted`
|
|
61
59
|
fields[deleted] = {
|
|
62
|
-
type: new
|
|
60
|
+
type: new GraphQLObjectType({
|
|
63
61
|
name: `${field.name}Deleted`,
|
|
64
62
|
fields: {
|
|
65
63
|
[primaryKey]: {
|
|
66
|
-
type: meta.fields[primaryKey].type
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
})
|
|
64
|
+
type: meta.fields[primaryKey].type
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
})
|
|
70
68
|
}
|
|
71
69
|
resolvers.Subscription[deleted] = {
|
|
72
70
|
subscribe: async (_, query, ctx, info) => {
|
|
@@ -75,12 +73,12 @@ function setupSubscriptions (app, metaMap, resolvers, ignores) {
|
|
|
75
73
|
const res = await pubsub.subscribe(topic)
|
|
76
74
|
ctx.reply.request.log.trace({ topic }, 'subscribed')
|
|
77
75
|
return wrap(res, deleted)
|
|
78
|
-
}
|
|
76
|
+
}
|
|
79
77
|
}
|
|
80
78
|
}
|
|
81
|
-
const subscription = new
|
|
79
|
+
const subscription = new GraphQLObjectType({
|
|
82
80
|
name: 'Subscription',
|
|
83
|
-
fields
|
|
81
|
+
fields
|
|
84
82
|
})
|
|
85
83
|
|
|
86
84
|
return subscription
|
|
@@ -91,5 +89,3 @@ async function * wrap (iterator, key) {
|
|
|
91
89
|
yield { [key]: msg }
|
|
92
90
|
}
|
|
93
91
|
}
|
|
94
|
-
|
|
95
|
-
module.exports = setupSubscriptions
|
package/lib/telemetry.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
|
|
1
|
+
function telemetryWrapper (app, fn, operationType, operationName) {
|
|
2
|
+
return async function (...args) {
|
|
3
|
+
const { startSpan, endSpan } = app.openTelemetry
|
|
4
|
+
const context = args[2]
|
|
5
|
+
const request = context?.reply?.request
|
|
6
|
+
const document = JSON.stringify(request?.body)
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
'graphql.document': document,
|
|
12
|
-
'graphql.operation.name': operationName,
|
|
13
|
-
'graphql.operation.type': operationType,
|
|
14
|
-
}
|
|
15
|
-
const spanName = `${operationType} ${operationName}`
|
|
16
|
-
const ctx = request.span?.context
|
|
8
|
+
// GraphQL semantic conventions, see: https://github.com/open-telemetry/semantic-conventions/blob/main/docs/database/graphql.md#semantic-conventions-for-graphql-server
|
|
9
|
+
const telemetryAttributes = {
|
|
10
|
+
'graphql.document': document,
|
|
11
|
+
'graphql.operation.name': operationName,
|
|
12
|
+
'graphql.operation.type': operationType
|
|
13
|
+
}
|
|
14
|
+
const spanName = `${operationType} ${operationName}`
|
|
15
|
+
const ctx = request.span?.context
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
const span = startSpan(spanName, ctx, telemetryAttributes)
|
|
18
|
+
try {
|
|
19
|
+
const result = await fn(...args)
|
|
20
|
+
endSpan(span)
|
|
21
|
+
return result
|
|
22
|
+
// We ignore this because in sqlite it's HARD to have a resolver exception without a schema validation exception first.
|
|
23
|
+
} catch (err) /* istanbul ignore next */ {
|
|
24
|
+
endSpan(span, err)
|
|
25
|
+
throw err
|
|
26
|
+
}
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
export function setupTelemetry (app) {
|
|
31
31
|
/* istanbul ignore next */
|
|
32
32
|
if (!app || !app.graphql) {
|
|
33
33
|
app.log.warn('No graphql plugin found, skipping telemetry setup')
|
|
@@ -52,5 +52,3 @@ const setupTelemetry = app => {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
module.exports = setupTelemetry
|
package/lib/utils.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const { GraphQLJSONObject } = require('graphql-type-json')
|
|
6
|
-
const errors = require('./errors')
|
|
1
|
+
import * as graphql from 'graphql'
|
|
2
|
+
import * as scalars from 'graphql-scalars'
|
|
3
|
+
import { GraphQLJSONObject } from 'graphql-type-json'
|
|
4
|
+
import { UnsupportedKindError } from './errors.js'
|
|
7
5
|
|
|
8
6
|
// The sqlTypeToGraphQL is shared between
|
|
9
7
|
// all database adapters.
|
|
10
|
-
function sqlTypeToGraphQL (sqlType) {
|
|
8
|
+
export function sqlTypeToGraphQL (sqlType) {
|
|
11
9
|
// TODO support more types
|
|
12
10
|
/* istanbul ignore next */
|
|
13
11
|
switch (sqlType) {
|
|
@@ -68,7 +66,7 @@ function sqlTypeToGraphQL (sqlType) {
|
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
function fromSelectionSet (selectionSet, fields = new Set()) {
|
|
69
|
+
export function fromSelectionSet (selectionSet, fields = new Set()) {
|
|
72
70
|
/* istanbul ignore next */
|
|
73
71
|
for (const s of selectionSet.selections) {
|
|
74
72
|
if (s.kind === 'Field') {
|
|
@@ -76,14 +74,10 @@ function fromSelectionSet (selectionSet, fields = new Set()) {
|
|
|
76
74
|
} else if (s.kind === 'InlineFragment') {
|
|
77
75
|
fromSelectionSet(s.selectionSet, fields)
|
|
78
76
|
} else {
|
|
79
|
-
throw new
|
|
77
|
+
throw new UnsupportedKindError(s.kind)
|
|
80
78
|
}
|
|
81
79
|
}
|
|
82
80
|
return fields
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
|
|
86
|
-
sqlTypeToGraphQL,
|
|
87
|
-
fromSelectionSet,
|
|
88
|
-
typeSym: Symbol('graphlType'),
|
|
89
|
-
}
|
|
83
|
+
export const typeSym = Symbol('graphlType')
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformatic/sql-graphql",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.6",
|
|
4
4
|
"description": "Map SQL dbs to GraphQL",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
6
7
|
"types": "index.d.ts",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
@@ -16,9 +17,8 @@
|
|
|
16
17
|
"homepage": "https://github.com/platformatic/platformatic#readme",
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"@matteo.collina/snap": "^0.3.0",
|
|
19
|
-
"@matteo.collina/tspl": "^0.2.0",
|
|
20
20
|
"@mercuriusjs/gateway": "^5.0.0",
|
|
21
|
-
"
|
|
21
|
+
"cleaner-spec-reporter": "^0.5.0",
|
|
22
22
|
"eslint": "9",
|
|
23
23
|
"fastify": "^5.0.0",
|
|
24
24
|
"fastify-tsconfig": "^3.0.0",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"typescript": "^5.5.4",
|
|
28
28
|
"why-is-node-running": "^2.2.2",
|
|
29
29
|
"ws": "^8.16.0",
|
|
30
|
-
"@platformatic/
|
|
31
|
-
"@platformatic/sql-
|
|
32
|
-
"@platformatic/
|
|
30
|
+
"@platformatic/telemetry": "3.0.0-alpha.6",
|
|
31
|
+
"@platformatic/sql-mapper": "3.0.0-alpha.6",
|
|
32
|
+
"@platformatic/sql-events": "3.0.0-alpha.6"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@fastify/error": "^4.0.0",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"graphql-type-json": "^0.3.2",
|
|
42
42
|
"inflected": "^2.1.0",
|
|
43
43
|
"mercurius": "^16.0.0",
|
|
44
|
-
"@platformatic/foundation": "3.0.0-alpha.
|
|
44
|
+
"@platformatic/foundation": "3.0.0-alpha.6"
|
|
45
45
|
},
|
|
46
46
|
"tsd": {
|
|
47
47
|
"directory": "test/types"
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
53
|
"lint": "eslint",
|
|
54
|
-
"test": "npm run
|
|
55
|
-
"test:postgresql": "DB=postgresql
|
|
56
|
-
"test:mariadb": "DB=mariadb
|
|
57
|
-
"test:mysql": "DB=mysql
|
|
58
|
-
"test:mysql8": "DB=mysql8
|
|
59
|
-
"test:sqlite": "DB=sqlite
|
|
54
|
+
"test": "npm run test:typescript && npm run test:postgresql && npm run test:mariadb && npm run test:mysql && npm run test:mysql8 && npm run test:sqlite",
|
|
55
|
+
"test:postgresql": "DB=postgresql node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js",
|
|
56
|
+
"test:mariadb": "DB=mariadb node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js",
|
|
57
|
+
"test:mysql": "DB=mysql node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js",
|
|
58
|
+
"test:mysql8": "DB=mysql8 node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js",
|
|
59
|
+
"test:sqlite": "DB=sqlite node --test --test-reporter=cleaner-spec-reporter --test-concurrency=1 --test-timeout=2000000 test/*.test.js",
|
|
60
60
|
"test:typescript": "tsd"
|
|
61
61
|
}
|
|
62
62
|
}
|