create-epic-graphql-server 0.6.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/README.md +1 -0
- package/package.json +5 -3
- package/specs/schema.spec.ts +24 -8
- package/src/index.ts +34 -0
- package/src/schema/schema.ts +35 -15
- package/src/typing/interfaces.ts +5 -0
- package/src/utilities/getUserFromReq.ts +38 -0
- package/tsconfig.json +2 -1
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-epic-graphql-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "a configured graphql server",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -50,12 +50,14 @@
|
|
|
50
50
|
"dotenv-cli": "8.0.0",
|
|
51
51
|
"dotenv-flow": "4.1.0",
|
|
52
52
|
"express": "5.1.0",
|
|
53
|
+
"express-session": "1.18.2",
|
|
53
54
|
"graphql": "16.11.0",
|
|
54
55
|
"graphql-http": "1.22.4",
|
|
55
56
|
"helmet": "8.1.0",
|
|
56
57
|
"husky": "9.1.7",
|
|
57
58
|
"json-server": "1.0.0-beta.3",
|
|
58
|
-
"
|
|
59
|
+
"memorystore": "1.6.7",
|
|
60
|
+
"ruru": "2.0.0-beta.30",
|
|
59
61
|
"winston": "3.17.0",
|
|
60
62
|
"winston-daily-rotate-file": "5.0.0",
|
|
61
63
|
"yargs": "17.7.2",
|
|
@@ -66,6 +68,7 @@
|
|
|
66
68
|
"@types/chalk": "0.4.31",
|
|
67
69
|
"@types/dotenv-flow": "3.3.3",
|
|
68
70
|
"@types/express": "5.0.2",
|
|
71
|
+
"@types/express-session": "1.18.2",
|
|
69
72
|
"@types/mocha": "10.0.10",
|
|
70
73
|
"@types/node": "22.15.18",
|
|
71
74
|
"@types/sinon": "17.0.4",
|
|
@@ -75,7 +78,6 @@
|
|
|
75
78
|
"chai": "4.3.10",
|
|
76
79
|
"eslint": "8.57.1",
|
|
77
80
|
"mocha": "11.7.2",
|
|
78
|
-
"npm": "11.6.0",
|
|
79
81
|
"sinon": "21.0.0",
|
|
80
82
|
"supertest": "7.1.1",
|
|
81
83
|
"ts-node": "10.9.2",
|
package/specs/schema.spec.ts
CHANGED
|
@@ -4,26 +4,23 @@ const sinon = require('sinon')
|
|
|
4
4
|
const express = require('express')
|
|
5
5
|
const { createHandler } = require('graphql-http/lib/use/express')
|
|
6
6
|
const { schema } = require('../src/schema/schema')
|
|
7
|
-
|
|
8
|
-
import type { SinonStub } from 'sinon'
|
|
9
|
-
|
|
10
|
-
const app = express()
|
|
11
|
-
app.use('/graphql', createHandler({ schema }))
|
|
7
|
+
import type { SinonStub } from 'sinon'
|
|
12
8
|
|
|
13
9
|
describe('GraphQL Schema', () => {
|
|
14
10
|
let saveStub: SinonStub
|
|
15
11
|
let findStub: SinonStub
|
|
12
|
+
let app
|
|
16
13
|
|
|
17
14
|
beforeEach(() => {
|
|
18
|
-
//
|
|
19
|
-
saveStub = sinon.stub(
|
|
15
|
+
// create fresh stubs for each test
|
|
16
|
+
saveStub = sinon.stub().resolves({
|
|
20
17
|
id: '1',
|
|
21
18
|
name: 'Alice',
|
|
22
19
|
email: 'alice@example.com',
|
|
23
20
|
phone: '123-456',
|
|
24
21
|
})
|
|
25
22
|
|
|
26
|
-
findStub = sinon.stub(
|
|
23
|
+
findStub = sinon.stub().resolves([
|
|
27
24
|
{
|
|
28
25
|
id: '1',
|
|
29
26
|
name: 'Alice',
|
|
@@ -31,6 +28,23 @@ describe('GraphQL Schema', () => {
|
|
|
31
28
|
phone: '123-456',
|
|
32
29
|
},
|
|
33
30
|
])
|
|
31
|
+
|
|
32
|
+
// build express app, inject stubbed db into context
|
|
33
|
+
app = express()
|
|
34
|
+
app.use(
|
|
35
|
+
'/graphql',
|
|
36
|
+
createHandler({
|
|
37
|
+
schema,
|
|
38
|
+
context: () => ({
|
|
39
|
+
db: {
|
|
40
|
+
customers: {
|
|
41
|
+
save: saveStub,
|
|
42
|
+
find: findStub,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
)
|
|
34
48
|
})
|
|
35
49
|
|
|
36
50
|
afterEach(() => {
|
|
@@ -64,6 +78,8 @@ describe('GraphQL Schema', () => {
|
|
|
64
78
|
email: 'alice@example.com',
|
|
65
79
|
phone: '123-456',
|
|
66
80
|
})
|
|
81
|
+
|
|
82
|
+
expect(findStub.calledOnce).to.be.true
|
|
67
83
|
})
|
|
68
84
|
|
|
69
85
|
it('adds a customer', async () => {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const express = require('express')
|
|
2
|
+
const session = require('express-session')
|
|
3
|
+
const MemoryStore = require('memorystore')(session)
|
|
2
4
|
const graphqlRouter = express.Router()
|
|
3
5
|
const cors = require('cors')
|
|
4
6
|
const helmet = require('helmet')
|
|
@@ -6,20 +8,44 @@ const chalk = require('chalk')
|
|
|
6
8
|
const { createHandler } = require('graphql-http/lib/use/express')
|
|
7
9
|
const { ruruHTML } = require('ruru/server')
|
|
8
10
|
const routes = require('./routes')
|
|
11
|
+
const db = require('./config/db')
|
|
9
12
|
const { schema } = require('./schema/schema')
|
|
10
13
|
const logger = require('./utilities/logger')
|
|
14
|
+
const { getUserFromReq } = require('./utilities/getUserFromReq')
|
|
11
15
|
import type { Application, Request, Response } from 'express'
|
|
12
16
|
import { NodeEnv } from './typing/enums'
|
|
17
|
+
import type { User } from './typing/interfaces'
|
|
13
18
|
|
|
14
19
|
require('dotenv-flow').config()
|
|
15
20
|
|
|
21
|
+
export interface Context {
|
|
22
|
+
user: User,
|
|
23
|
+
db: typeof db,
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
const { production, development } = NodeEnv
|
|
17
27
|
|
|
18
28
|
const PORT = process.env.PORT || 3000
|
|
19
29
|
const NODE_ENV = process.env.NODE_ENV || development
|
|
30
|
+
const SESSION_SECRET = process.env.SESSION_SECRET || 'secret'
|
|
20
31
|
|
|
21
32
|
const app: Application = express()
|
|
22
33
|
|
|
34
|
+
app.use(session({
|
|
35
|
+
cookie: {
|
|
36
|
+
maxAge: 24*60*60*1000, // 24 hrs
|
|
37
|
+
},
|
|
38
|
+
// it's recommended that compatible, robust npm modules be
|
|
39
|
+
// explored before choosing a production app's memory store:
|
|
40
|
+
// https://www.npmjs.com/package/express-session#compatible-session-stores
|
|
41
|
+
store: new MemoryStore({
|
|
42
|
+
checkPeriod: 24*60*60*1000,
|
|
43
|
+
}),
|
|
44
|
+
resave: false,
|
|
45
|
+
secret: SESSION_SECRET,
|
|
46
|
+
saveUninitialized: false,
|
|
47
|
+
}))
|
|
48
|
+
|
|
23
49
|
app.use(cors())
|
|
24
50
|
|
|
25
51
|
if (NODE_ENV === production) {
|
|
@@ -37,6 +63,14 @@ app.use(routes)
|
|
|
37
63
|
graphqlRouter.all('/',
|
|
38
64
|
createHandler({
|
|
39
65
|
schema,
|
|
66
|
+
context: (req: Request): Context => {
|
|
67
|
+
const user = getUserFromReq({ req })
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
user,
|
|
71
|
+
db,
|
|
72
|
+
}
|
|
73
|
+
},
|
|
40
74
|
})
|
|
41
75
|
)
|
|
42
76
|
|
package/src/schema/schema.ts
CHANGED
|
@@ -7,8 +7,10 @@ const {
|
|
|
7
7
|
GraphQLList,
|
|
8
8
|
GraphQLNonNull,
|
|
9
9
|
GraphQLEnumType,
|
|
10
|
+
// GraphQLResolveInfo,
|
|
11
|
+
// SelectionSetNode,
|
|
10
12
|
} = require('graphql')
|
|
11
|
-
|
|
13
|
+
import type { Context } from '../index'
|
|
12
14
|
import type { Customer } from '../models/Customer'
|
|
13
15
|
import type { Order } from '../models/Order'
|
|
14
16
|
import { Product, Status } from '../typing/enums'
|
|
@@ -35,7 +37,7 @@ const orderType = new GraphQLObjectType({
|
|
|
35
37
|
// NOTE creates relationship with other type
|
|
36
38
|
customer: {
|
|
37
39
|
type: customerType,
|
|
38
|
-
resolve(parent: Order, args: Customer) {
|
|
40
|
+
resolve(parent: Order, args: Customer, { db }: Context) {
|
|
39
41
|
return db.customers.findById(parent.customerId)
|
|
40
42
|
},
|
|
41
43
|
},
|
|
@@ -68,7 +70,7 @@ const queryType = new GraphQLObjectType({
|
|
|
68
70
|
args: {
|
|
69
71
|
filter: { type: orderFilterInput },
|
|
70
72
|
},
|
|
71
|
-
resolve(parent: unknown, { filter }: { filter: Order }) {
|
|
73
|
+
resolve(parent: unknown, { filter }: { filter: Order }, { db }: Context) {
|
|
72
74
|
return db.orders.find({ filter })
|
|
73
75
|
},
|
|
74
76
|
// NOTE:
|
|
@@ -79,13 +81,31 @@ const queryType = new GraphQLObjectType({
|
|
|
79
81
|
// the example resolver implementation shown below
|
|
80
82
|
// may avoid over fetching from a database or API
|
|
81
83
|
//
|
|
82
|
-
// resolve(
|
|
83
|
-
//
|
|
84
|
+
// resolve(
|
|
85
|
+
// parent: unknown,
|
|
86
|
+
// { filter }: { filter: Order },
|
|
87
|
+
// { db }: Context,
|
|
88
|
+
// info: typeof GraphQLResolveInfo
|
|
89
|
+
// ) {
|
|
90
|
+
//
|
|
91
|
+
// const selections = info
|
|
84
92
|
// .fieldNodes[0]
|
|
85
93
|
// .selectionSet
|
|
86
|
-
//
|
|
87
|
-
//
|
|
94
|
+
// ?.selections || []
|
|
95
|
+
//
|
|
96
|
+
// const fields = selections.reduce((
|
|
97
|
+
// acc: Record<string, number>,
|
|
98
|
+
// selection: typeof SelectionSetNode
|
|
99
|
+
// ) => {
|
|
100
|
+
// if (selection.kind === 'Field') {
|
|
101
|
+
// acc[selection.name.value] = 1
|
|
102
|
+
// }
|
|
103
|
+
//
|
|
104
|
+
// return acc
|
|
105
|
+
// }, {})
|
|
88
106
|
//
|
|
107
|
+
// // filter = query criteria (docs, rows, etc.)
|
|
108
|
+
// // fields = projection (fields, columns, etc.)
|
|
89
109
|
// return db.orders.find(filter, fields)
|
|
90
110
|
// },
|
|
91
111
|
},
|
|
@@ -94,7 +114,7 @@ const queryType = new GraphQLObjectType({
|
|
|
94
114
|
args: {
|
|
95
115
|
id: { type: new GraphQLNonNull(GraphQLID) },
|
|
96
116
|
},
|
|
97
|
-
resolve: (parent: unknown, { id }: Order) => {
|
|
117
|
+
resolve: (parent: unknown, { id }: Order, { db }: Context) => {
|
|
98
118
|
return db.orders.findById(id)
|
|
99
119
|
},
|
|
100
120
|
},
|
|
@@ -103,7 +123,7 @@ const queryType = new GraphQLObjectType({
|
|
|
103
123
|
args: {
|
|
104
124
|
filter: { type: customerFilterInput },
|
|
105
125
|
},
|
|
106
|
-
resolve(parent: unknown, { filter }: { filter: Customer }) {
|
|
126
|
+
resolve(parent: unknown, { filter }: { filter: Customer }, { db }: Context) {
|
|
107
127
|
return db.customers.find({ filter })
|
|
108
128
|
},
|
|
109
129
|
},
|
|
@@ -112,7 +132,7 @@ const queryType = new GraphQLObjectType({
|
|
|
112
132
|
args: {
|
|
113
133
|
id: { type: new GraphQLNonNull(GraphQLID) },
|
|
114
134
|
},
|
|
115
|
-
resolve: (parent: unknown, { id }: Customer) => {
|
|
135
|
+
resolve: (parent: unknown, { id }: Customer, { db }: Context) => {
|
|
116
136
|
return db.customers.findById(id)
|
|
117
137
|
},
|
|
118
138
|
},
|
|
@@ -130,7 +150,7 @@ const mutation = new GraphQLObjectType({
|
|
|
130
150
|
email: { type: new GraphQLNonNull(GraphQLString) },
|
|
131
151
|
phone: { type: new GraphQLNonNull(GraphQLString) },
|
|
132
152
|
},
|
|
133
|
-
resolve(parent: unknown, args: Customer) {
|
|
153
|
+
resolve(parent: unknown, args: Customer, { db }: Context) {
|
|
134
154
|
return db.customers.save(args)
|
|
135
155
|
},
|
|
136
156
|
},
|
|
@@ -140,7 +160,7 @@ const mutation = new GraphQLObjectType({
|
|
|
140
160
|
args: {
|
|
141
161
|
id: { type: new GraphQLNonNull(GraphQLID) },
|
|
142
162
|
},
|
|
143
|
-
async resolve(parent: unknown, args: Customer) {
|
|
163
|
+
async resolve(parent: unknown, args: Customer, { db }: Context) {
|
|
144
164
|
const orders = await db.orders.find({
|
|
145
165
|
filter: { customerId: args.id },
|
|
146
166
|
})
|
|
@@ -181,7 +201,7 @@ const mutation = new GraphQLObjectType({
|
|
|
181
201
|
},
|
|
182
202
|
customerId: { type: new GraphQLNonNull(GraphQLID) },
|
|
183
203
|
},
|
|
184
|
-
resolve(parent: unknown, args: Order) {
|
|
204
|
+
resolve(parent: unknown, args: Order, { db }: Context) {
|
|
185
205
|
return db.orders.save(args)
|
|
186
206
|
},
|
|
187
207
|
},
|
|
@@ -203,7 +223,7 @@ const mutation = new GraphQLObjectType({
|
|
|
203
223
|
}),
|
|
204
224
|
},
|
|
205
225
|
},
|
|
206
|
-
resolve(parent: unknown, { id, ...update }: Order) {
|
|
226
|
+
resolve(parent: unknown, { id, ...update }: Order, { db }: Context) {
|
|
207
227
|
return db.orders.findByIdAndUpdate(id, { update })
|
|
208
228
|
},
|
|
209
229
|
},
|
|
@@ -213,7 +233,7 @@ const mutation = new GraphQLObjectType({
|
|
|
213
233
|
args: {
|
|
214
234
|
id: { type: new GraphQLNonNull(GraphQLID) },
|
|
215
235
|
},
|
|
216
|
-
resolve(parent: unknown, { id }: Order) {
|
|
236
|
+
resolve(parent: unknown, { id }: Order, { db }: Context) {
|
|
217
237
|
return db.orders.findByIdAndRemove(id)
|
|
218
238
|
},
|
|
219
239
|
},
|
package/src/typing/interfaces.ts
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Request } from 'express'
|
|
2
|
+
import type { User } from 'typing/interfaces'
|
|
3
|
+
|
|
4
|
+
// NOTE this is a DUMMY pattern to demo how
|
|
5
|
+
// one might pass request info into GraphQL
|
|
6
|
+
// via a callback like "getUserFromReq"
|
|
7
|
+
|
|
8
|
+
const setDummySession = ({
|
|
9
|
+
req,
|
|
10
|
+
}:{
|
|
11
|
+
req: Request,
|
|
12
|
+
}) => {
|
|
13
|
+
// in a production app user info might
|
|
14
|
+
// be set upstream via a security model
|
|
15
|
+
|
|
16
|
+
const { session } = req
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
...session,
|
|
20
|
+
user: {
|
|
21
|
+
id: 'user123',
|
|
22
|
+
roles: ['readWrite'],
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const getUserFromReq = ({
|
|
28
|
+
req,
|
|
29
|
+
}:{
|
|
30
|
+
req: Request,
|
|
31
|
+
}): User => {
|
|
32
|
+
const session = setDummySession({ req })
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
id: session.user.id,
|
|
36
|
+
roles: session.user.roles,
|
|
37
|
+
}
|
|
38
|
+
}
|