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 CHANGED
@@ -89,6 +89,7 @@ also add any additional variables your project needs
89
89
  # optional but recommended
90
90
  NODE_ENV=development
91
91
  PORT={port number}
92
+ SESSION_SECRET=mySecret!
92
93
  # optional
93
94
  JSON_SERVER_PORT={dev db port, default 3210}
94
95
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-epic-graphql-server",
3
- "version": "0.6.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
- "ruru": "2.0.0-beta.22",
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",
@@ -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
- const db = require('../src/config/db')
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
- // Stub db methods
19
- saveStub = sinon.stub(db.customers, 'save').resolves({
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(db.customers, 'find').resolves([
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
 
@@ -7,8 +7,10 @@ const {
7
7
  GraphQLList,
8
8
  GraphQLNonNull,
9
9
  GraphQLEnumType,
10
+ // GraphQLResolveInfo,
11
+ // SelectionSetNode,
10
12
  } = require('graphql')
11
- const db = require('../config/db')
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(parent, { filter }, context, info) {
83
- // const fields = info
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
- // .selections
87
- // .map(s => s.name.value)
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
  },
@@ -42,3 +42,8 @@ export interface LogErr extends ErrorHandler {
42
42
  meta?: object,
43
43
  level?: enums.LogLevel.warn | enums.LogLevel.error,
44
44
  }
45
+
46
+ export interface User {
47
+ id: string,
48
+ roles: string[],
49
+ }
@@ -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
+ }
package/tsconfig.json CHANGED
@@ -23,7 +23,8 @@
23
23
  "@types/mocha",
24
24
  "@types/node",
25
25
  "@types/sinon",
26
- "@types/supertest"
26
+ "@types/supertest",
27
+ "@types/express-session"
27
28
  ],
28
29
  },
29
30
  "include": [