create-epic-graphql-server 0.5.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.
@@ -0,0 +1,227 @@
1
+ const {
2
+ GraphQLObjectType,
3
+ GraphQLInputObjectType,
4
+ GraphQLID,
5
+ GraphQLString,
6
+ GraphQLSchema,
7
+ GraphQLList,
8
+ GraphQLNonNull,
9
+ GraphQLEnumType,
10
+ } = require('graphql')
11
+ const db = require('../config/db')
12
+ import type { Customer } from '../models/Customer'
13
+ import type { Order } from '../models/Order'
14
+ import { Product, Status } from '../typing/enums'
15
+
16
+ // https://graphql.org/graphql-js/constructing-types/
17
+
18
+ const customerType = new GraphQLObjectType({
19
+ name: 'Customer',
20
+ fields: {
21
+ id: { type: GraphQLID },
22
+ name: { type: GraphQLString },
23
+ email: { type: GraphQLString },
24
+ phone: { type: GraphQLString },
25
+ },
26
+ })
27
+
28
+ const orderType = new GraphQLObjectType({
29
+ name: 'Order',
30
+ fields: () => ({
31
+ id: { type: GraphQLID },
32
+ name: { type: GraphQLString },
33
+ notes: { type: GraphQLString },
34
+ status: { type: GraphQLString },
35
+ // NOTE creates relationship with other type
36
+ customer: {
37
+ type: customerType,
38
+ resolve(parent: Order, args: Customer) {
39
+ return db.customers.findById(parent.customerId)
40
+ },
41
+ },
42
+ }),
43
+ })
44
+
45
+ const orderFilterInput = new GraphQLInputObjectType({
46
+ name: 'OrderFilterInput',
47
+ fields: {
48
+ name: { type: GraphQLString },
49
+ status: { type: GraphQLString },
50
+ customerId: { type: GraphQLID },
51
+ },
52
+ })
53
+
54
+ const customerFilterInput = new GraphQLInputObjectType({
55
+ name: 'CustomerFilterInput',
56
+ fields: {
57
+ name: { type: GraphQLString },
58
+ email: { type: GraphQLString },
59
+ phone: { type: GraphQLString },
60
+ },
61
+ })
62
+
63
+ const queryType = new GraphQLObjectType({
64
+ name: 'Query',
65
+ fields: {
66
+ orders: {
67
+ type: new GraphQLList(orderType),
68
+ args: {
69
+ filter: { type: orderFilterInput },
70
+ },
71
+ resolve(parent: unknown, { filter }: { filter: Order }) {
72
+ return db.orders.find({ filter })
73
+ },
74
+ // NOTE:
75
+ // GraphQL efficiently resolves only the fields
76
+ // requested by the client, but whether it fetches
77
+ // only those fields from the data source depends
78
+ // entirely on how resolvers are implemented...
79
+ // the example resolver implementation shown below
80
+ // may avoid over fetching from a database or API
81
+ //
82
+ // resolve(parent, { filter }, context, info) {
83
+ // const fields = info
84
+ // .fieldNodes[0]
85
+ // .selectionSet
86
+ // .selections
87
+ // .map(s => s.name.value)
88
+ //
89
+ // return db.orders.find(filter, fields)
90
+ // },
91
+ },
92
+ order: {
93
+ type: orderType,
94
+ args: {
95
+ id: { type: new GraphQLNonNull(GraphQLID) },
96
+ },
97
+ resolve: (parent: unknown, { id }: Order) => {
98
+ return db.orders.findById(id)
99
+ },
100
+ },
101
+ customers: {
102
+ type: new GraphQLList(customerType),
103
+ args: {
104
+ filter: { type: customerFilterInput },
105
+ },
106
+ resolve(parent: unknown, { filter }: { filter: Customer }) {
107
+ return db.customers.find({ filter })
108
+ },
109
+ },
110
+ customer: {
111
+ type: customerType,
112
+ args: {
113
+ id: { type: new GraphQLNonNull(GraphQLID) },
114
+ },
115
+ resolve: (parent: unknown, { id }: Customer) => {
116
+ return db.customers.findById(id)
117
+ },
118
+ },
119
+ },
120
+ })
121
+
122
+ const mutation = new GraphQLObjectType({
123
+ name: 'Mutation',
124
+ fields: {
125
+ // Add a customer
126
+ addCustomer: {
127
+ type: customerType,
128
+ args: {
129
+ name: { type: new GraphQLNonNull(GraphQLString) },
130
+ email: { type: new GraphQLNonNull(GraphQLString) },
131
+ phone: { type: new GraphQLNonNull(GraphQLString) },
132
+ },
133
+ resolve(parent: unknown, args: Customer) {
134
+ return db.customers.save(args)
135
+ },
136
+ },
137
+ // Delete a customer
138
+ deleteCustomer: {
139
+ type: customerType,
140
+ args: {
141
+ id: { type: new GraphQLNonNull(GraphQLID) },
142
+ },
143
+ async resolve(parent: unknown, args: Customer) {
144
+ const orders = await db.orders.find({
145
+ filter: { customerId: args.id },
146
+ })
147
+
148
+ orders.forEach(({ id }: Customer) => {
149
+ db.orders.findByIdAndRemove(id)
150
+ })
151
+
152
+ return db.customers.findByIdAndRemove(args.id)
153
+ },
154
+ },
155
+ // Add an order
156
+ addOrder: {
157
+ type: orderType,
158
+ args: {
159
+ name: {
160
+ type: new GraphQLNonNull(new GraphQLEnumType({
161
+ name: 'OrderName',
162
+ values: {
163
+ productOne: { value: Product.productOne },
164
+ productTwo: { value: Product.productTwo },
165
+ productThree: { value: Product.productThree },
166
+ },
167
+ })),
168
+ defaultValue: null,
169
+ },
170
+ notes: { type: GraphQLString },
171
+ status: {
172
+ type: new GraphQLNonNull(new GraphQLEnumType({
173
+ name: 'OrderStatus',
174
+ values: {
175
+ notStarted: { value: Status.notStarted },
176
+ processing: { value: Status.processing },
177
+ sent: { value: Status.sent },
178
+ },
179
+ })),
180
+ defaultValue: Status.notStarted,
181
+ },
182
+ customerId: { type: new GraphQLNonNull(GraphQLID) },
183
+ },
184
+ resolve(parent: unknown, args: Order) {
185
+ return db.orders.save(args)
186
+ },
187
+ },
188
+ // Update an order
189
+ updateOrder: {
190
+ type: orderType,
191
+ args: {
192
+ id: { type: new GraphQLNonNull(GraphQLID) },
193
+ name: { type: GraphQLString },
194
+ notes: { type: GraphQLString },
195
+ status: {
196
+ type: new GraphQLEnumType({
197
+ name: 'OrderStatusUpdate',
198
+ values: {
199
+ notStarted: { value: Status.notStarted },
200
+ processing: { value: Status.processing },
201
+ sent: { value: Status.sent },
202
+ },
203
+ }),
204
+ },
205
+ },
206
+ resolve(parent: unknown, { id, ...update }: Order) {
207
+ return db.orders.findByIdAndUpdate(id, { update })
208
+ },
209
+ },
210
+ // Delete an order
211
+ deleteOrder: {
212
+ type: orderType,
213
+ args: {
214
+ id: { type: new GraphQLNonNull(GraphQLID) },
215
+ },
216
+ resolve(parent: unknown, { id }: Order) {
217
+ return db.orders.findByIdAndRemove(id)
218
+ },
219
+ },
220
+ },
221
+ })
222
+
223
+ export const schema = new GraphQLSchema({
224
+ query: queryType,
225
+ description: 'my schema',
226
+ mutation,
227
+ })
@@ -0,0 +1,37 @@
1
+ export enum NodeEnv {
2
+ production = 'production',
3
+ test = 'test',
4
+ development = 'development',
5
+ }
6
+
7
+ export enum LogLevel {
8
+ error = 'error',
9
+ warn = 'warn',
10
+ info = 'info',
11
+ http = 'http',
12
+ verbose = 'verbose',
13
+ debug = 'debug',
14
+ silly = 'silly',
15
+ }
16
+
17
+ export enum LogFile {
18
+ combined = 'combined',
19
+ error = 'error',
20
+ }
21
+
22
+ export enum Product {
23
+ productOne = 'Product One',
24
+ productTwo = 'Product Two',
25
+ productThree = 'Product Three',
26
+ }
27
+
28
+ export enum Status {
29
+ notStarted = 'Not Started',
30
+ processing = 'Processing',
31
+ sent = 'Sent',
32
+ }
33
+
34
+ export enum DataSet {
35
+ customers = 'customers',
36
+ orders = 'orders',
37
+ }
@@ -0,0 +1,44 @@
1
+ import type { Customer } from '../models/Customer'
2
+ import type { Order } from '../models/Order'
3
+ import type * as enums from './enums'
4
+ import type * as types from './types'
5
+
6
+ interface DataSetOptions {
7
+ dataSet: enums.DataSet,
8
+ }
9
+
10
+ export interface UpdateOptions {
11
+ update: Partial<Customer>|Partial<Order>,
12
+ }
13
+
14
+ interface IdOptions {
15
+ id: types.Id,
16
+ }
17
+
18
+ interface DocOptions {
19
+ doc: Customer | Order,
20
+ }
21
+
22
+ export interface FilterOptions {
23
+ filter?: Record<string, string>,
24
+ }
25
+
26
+ export interface Create extends DataSetOptions, DocOptions {}
27
+
28
+ export interface Read extends DataSetOptions, FilterOptions {}
29
+
30
+ export interface ReadOne extends Read, IdOptions {}
31
+
32
+ export interface Update extends DataSetOptions, IdOptions, UpdateOptions {}
33
+
34
+ export interface Delete extends DataSetOptions, IdOptions {}
35
+
36
+ export interface ErrorHandler {
37
+ header?: types.Msg,
38
+ err: types.Err,
39
+ }
40
+
41
+ export interface LogErr extends ErrorHandler {
42
+ meta?: object,
43
+ level?: enums.LogLevel.warn | enums.LogLevel.error,
44
+ }
@@ -0,0 +1,5 @@
1
+ export type Id = string
2
+
3
+ export type Err = Error | unknown
4
+
5
+ export type Msg = string
@@ -0,0 +1,191 @@
1
+ const path = require('path')
2
+ const chalk = require('chalk')
3
+ const winston = require('winston')
4
+ require('winston-daily-rotate-file')
5
+ const packageJson = require('../../package.json')
6
+ import type { Logger } from 'winston'
7
+ import type { Format } from 'logform'
8
+ import { NodeEnv, LogLevel, LogFile } from '../typing/enums'
9
+ import type { Msg } from '../typing/types'
10
+ import type { ErrorHandler, LogErr } from '../typing/interfaces'
11
+
12
+ require('dotenv-flow').config()
13
+
14
+ const NODE_ENV = process.env.NODE_ENV as NodeEnv || NodeEnv.development
15
+
16
+ // NOTE:
17
+ // 1) production
18
+ // a) write log file in json to enable log analytics
19
+ // b) INFO logging enabled
20
+ // 2) development
21
+ // a) display logs in console in more readable format, and
22
+ // write to log files to align prod and dev behavior
23
+ // b) VERBOSE logging enabled
24
+
25
+ class CustomLogger {
26
+ private dirPath: string
27
+ private locales: string
28
+ private timeZone: string
29
+ private settings: {
30
+ [key in NodeEnv]: {
31
+ level: keyof typeof winston.config.npm.levels,
32
+ }
33
+ }
34
+ log: Logger | null
35
+
36
+ constructor (options?: {
37
+ dirPath: string
38
+ locales: string
39
+ timeZone: string
40
+ }) {
41
+ this.dirPath = options?.dirPath || 'tmp'
42
+ this.locales = options?.locales || 'en-US'
43
+ this.timeZone = options?.timeZone || 'UTC' // 'America/New_York'
44
+
45
+ this.settings = {
46
+ [NodeEnv.production]: {
47
+ level: LogLevel.info,
48
+ },
49
+ [NodeEnv.test]: {
50
+ level: LogLevel.info,
51
+ },
52
+ [NodeEnv.development]: {
53
+ level: LogLevel.verbose,
54
+ },
55
+ }
56
+
57
+ this.log = null
58
+
59
+ this.initWinston()
60
+ }
61
+
62
+ private initWinston() {
63
+ const {
64
+ combine,
65
+ timestamp,
66
+ json,
67
+ simple,
68
+ colorize,
69
+ } = winston.format
70
+
71
+ const transportsOptions = {
72
+ frequency: '24h',
73
+ datePattern: 'YYYY-MM-DD-HH',
74
+ zippedArchive: true,
75
+ maxSize: '20m',
76
+ maxFiles: '30d',
77
+ }
78
+
79
+ const formatFileName = (fileType: LogFile) => {
80
+ const file = `${packageJson.name}_${fileType}_%DATE%.log`
81
+
82
+ return path.join('/', this.dirPath, file)
83
+ }
84
+
85
+ const transports = [
86
+ new winston.transports.DailyRotateFile({
87
+ level: LogLevel.error,
88
+ filename: formatFileName(LogFile.error),
89
+ frequency: transportsOptions.frequency,
90
+ datePattern: transportsOptions.datePattern,
91
+ zippedArchive: transportsOptions.zippedArchive,
92
+ maxSize: transportsOptions.maxSize,
93
+ maxFiles: transportsOptions.maxFiles,
94
+ }),
95
+ new winston.transports.DailyRotateFile({
96
+ filename: formatFileName(LogFile.combined),
97
+ frequency: transportsOptions.frequency,
98
+ datePattern: transportsOptions.datePattern,
99
+ zippedArchive: transportsOptions.zippedArchive,
100
+ maxSize: transportsOptions.maxSize,
101
+ maxFiles: transportsOptions.maxFiles,
102
+ }),
103
+ ]
104
+
105
+ let format: Format
106
+
107
+ if (NODE_ENV === NodeEnv.production) {
108
+ format = combine(timestamp(), json())
109
+ } else {
110
+ format = combine(simple(), colorize())
111
+ transports.push(new winston.transports.Console({ format }))
112
+ }
113
+
114
+ const level = this.settings[NODE_ENV].level
115
+
116
+ return this.log = winston.createLogger({
117
+ level,
118
+ format,
119
+ transports,
120
+ })
121
+ }
122
+
123
+ logTimeStamp = (options?: {
124
+ msg?: string,
125
+ }) => {
126
+ const msg = options?.msg || ''
127
+
128
+ const now = new Date()
129
+
130
+ const time = now.toLocaleString(this.locales, {
131
+ timeZone: this.timeZone,
132
+ timeStyle: 'long',
133
+ dateStyle: 'short',
134
+ hour12: false,
135
+ })
136
+
137
+ const timeStamp = chalk.grey(`[${time}]`)
138
+
139
+ return this.log?.info(`${msg} ${timeStamp}`)
140
+ }
141
+
142
+ logEnv = ({
143
+ msg,
144
+ }: {
145
+ msg: string,
146
+ }) => {
147
+ const env = chalk.grey(`[${NODE_ENV}]`)
148
+
149
+ return this.log?.info(`${msg} ${env}`)
150
+ }
151
+
152
+ private errorHandler = ({
153
+ header,
154
+ err,
155
+ }: ErrorHandler): Msg => {
156
+ const myHeader: string = header
157
+ ? `${header}:`
158
+ : ''
159
+
160
+ if (err instanceof Error) {
161
+ return `${myHeader} ${err.message}`
162
+ } else {
163
+ return `unexpected ${myHeader} ${err}`
164
+ }
165
+ }
166
+
167
+ logErr = ({
168
+ header,
169
+ err,
170
+ meta = {},
171
+ level = LogLevel.error,
172
+ }: LogErr) => {
173
+ const msg = this.errorHandler({
174
+ err,
175
+ header,
176
+ })
177
+
178
+ switch(level) {
179
+ case LogLevel.warn:
180
+ return this.log?.warn(msg, { ...meta })
181
+ case LogLevel.error:
182
+ return this.log?.error(msg, { ...meta })
183
+ default:
184
+ return null
185
+ }
186
+ }
187
+ }
188
+
189
+ const logger = new CustomLogger()
190
+
191
+ export = logger
package/tsconfig.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "ts-node": {
3
+ "transpileOnly": true,
4
+ "files": true,
5
+ "compilerOptions": {}
6
+ },
7
+ "compilerOptions": {
8
+ "module": "commonjs",
9
+ "moduleResolution": "node",
10
+ "esModuleInterop": true,
11
+ "baseUrl": "src",
12
+ "preserveConstEnums": true,
13
+ "target": "es6",
14
+ "pretty": true,
15
+ "sourceMap": true,
16
+ "strict": true,
17
+ "outDir": "./build",
18
+ "types": [
19
+ "@types/chai",
20
+ "@types/chalk",
21
+ "@types/dotenv-flow",
22
+ "@types/express",
23
+ "@types/mocha",
24
+ "@types/node",
25
+ "@types/sinon",
26
+ "@types/supertest"
27
+ ],
28
+ },
29
+ "include": [
30
+ "src/**/*"
31
+ ],
32
+ "exclude": [
33
+ "./.env*",
34
+ "node_modules",
35
+ "bin",
36
+ "build",
37
+ ".vscode",
38
+ "*.json5",
39
+ "**/*.specs.ts",
40
+ "**/*.test.ts"
41
+ ]
42
+ }