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.
- package/.eslintignore +7 -0
- package/.eslintrc.js +198 -0
- package/.husky/pre-commit +2 -0
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/bin/create-epic-graphql-server.js +81 -0
- package/db-seed.json5 +37 -0
- package/package.json +75 -0
- package/specs/Customer.spec.ts +41 -0
- package/specs/Order.spec.ts +80 -0
- package/specs/logger.spec.ts +92 -0
- package/specs/schema.spec.ts +103 -0
- package/src/config/db.ts +267 -0
- package/src/index.ts +57 -0
- package/src/models/Customer.ts +10 -0
- package/src/models/Order.ts +12 -0
- package/src/routes/check.ts +18 -0
- package/src/routes/index.ts +8 -0
- package/src/schema/schema.ts +227 -0
- package/src/typing/enums.ts +37 -0
- package/src/typing/interfaces.ts +44 -0
- package/src/typing/types.ts +5 -0
- package/src/utilities/logger.ts +191 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { expect } = require('chai')
|
|
2
|
+
import { orderSchema } from '../src/models/Order'
|
|
3
|
+
import { Product, Status } from '../src/typing/enums'
|
|
4
|
+
|
|
5
|
+
describe('orderSchema', () => {
|
|
6
|
+
it('validates a correct order object', () => {
|
|
7
|
+
const input = {
|
|
8
|
+
name: Product.productOne,
|
|
9
|
+
status: Status.processing,
|
|
10
|
+
customerId: 'abc123',
|
|
11
|
+
notes: 'Fragile item',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const result = orderSchema.safeParse(input)
|
|
15
|
+
expect(result.success).to.be.true
|
|
16
|
+
if (result.success) {
|
|
17
|
+
expect(result.data).to.deep.include(input)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('fails if required fields are missing', () => {
|
|
22
|
+
const input = {
|
|
23
|
+
name: Product.productTwo,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = orderSchema.safeParse(input)
|
|
27
|
+
expect(result.success).to.be.false
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
const errors = result.error.flatten().fieldErrors
|
|
30
|
+
expect(errors).to.have.property('status')
|
|
31
|
+
expect(errors).to.have.property('customerId')
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('fails if name or status is not a valid enum', () => {
|
|
36
|
+
const input = {
|
|
37
|
+
name: 'Invalid Product',
|
|
38
|
+
status: 'Invalid Status',
|
|
39
|
+
customerId: 'abc123',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = orderSchema.safeParse(input)
|
|
43
|
+
expect(result.success).to.be.false
|
|
44
|
+
if (!result.success) {
|
|
45
|
+
const errors = result.error.flatten().fieldErrors
|
|
46
|
+
expect(errors.name?.[0]).to.include('Invalid enum value')
|
|
47
|
+
expect(errors.status?.[0]).to.include('Invalid enum value')
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('accepts when optional fields are omitted', () => {
|
|
52
|
+
const input = {
|
|
53
|
+
name: Product.productThree,
|
|
54
|
+
status: Status.sent,
|
|
55
|
+
customerId: 'xyz789',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = orderSchema.safeParse(input)
|
|
59
|
+
expect(result.success).to.be.true
|
|
60
|
+
if (result.success) {
|
|
61
|
+
expect(result.data.notes).to.be.undefined
|
|
62
|
+
expect(result.data.id).to.be.undefined
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('accepts if id is provided but optional', () => {
|
|
67
|
+
const input = {
|
|
68
|
+
id: 'order-001',
|
|
69
|
+
name: Product.productTwo,
|
|
70
|
+
status: Status.notStarted,
|
|
71
|
+
customerId: 'cust001',
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = orderSchema.safeParse(input)
|
|
75
|
+
expect(result.success).to.be.true
|
|
76
|
+
if (result.success) {
|
|
77
|
+
expect(result.data.id).to.equal('order-001')
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
|
|
2
|
+
const { expect } = require('chai')
|
|
3
|
+
const sinon = require('sinon')
|
|
4
|
+
const logger = require('../src/utilities/logger')
|
|
5
|
+
import type { SinonStub, SinonFakeTimers } from 'sinon'
|
|
6
|
+
|
|
7
|
+
describe('CustomLogger', () => {
|
|
8
|
+
let infoStub: SinonStub
|
|
9
|
+
let errorStub: SinonStub
|
|
10
|
+
let warnStub: SinonStub
|
|
11
|
+
let clock: SinonFakeTimers
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
if (!logger.log) {
|
|
15
|
+
throw new Error('logger.log must be initialized before tests')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
infoStub = sinon.stub(logger.log, 'info')
|
|
19
|
+
errorStub = sinon.stub(logger.log, 'error')
|
|
20
|
+
warnStub = sinon.stub(logger.log, 'warn')
|
|
21
|
+
clock = sinon.useFakeTimers(new Date('2025-06-29T12:00:00Z').getTime())
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
infoStub.restore()
|
|
26
|
+
errorStub.restore()
|
|
27
|
+
warnStub.restore()
|
|
28
|
+
clock.restore()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('logs a timestamp with default message', () => {
|
|
32
|
+
logger.logTimeStamp()
|
|
33
|
+
|
|
34
|
+
expect(infoStub.calledOnce).to.be.true
|
|
35
|
+
const logged = infoStub.firstCall.args[0]
|
|
36
|
+
expect(logged).to.match(/\[\d{1,2}\/\d{1,2}\/\d{2}, 12:00:00/)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('logs a timestamp with a custom message', () => {
|
|
40
|
+
logger.logTimeStamp({ msg: 'Started at' })
|
|
41
|
+
|
|
42
|
+
expect(infoStub.calledOnce).to.be.true
|
|
43
|
+
const logged = infoStub.firstCall.args[0]
|
|
44
|
+
expect(logged).to.include('Started at')
|
|
45
|
+
expect(logged).to.match(/\[\d{1,2}\/\d{1,2}\/\d{2}, 12:00:00/)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('logs environment with custom message', () => {
|
|
49
|
+
logger.logEnv({ msg: 'Server started' })
|
|
50
|
+
|
|
51
|
+
expect(infoStub.calledOnce).to.be.true
|
|
52
|
+
const logged = infoStub.firstCall.args[0]
|
|
53
|
+
expect(logged).to.include('Server started')
|
|
54
|
+
expect(logged).to.include('[development]')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('logs an error with a header and Error instance', () => {
|
|
58
|
+
logger.logErr({
|
|
59
|
+
header: 'TestHeader',
|
|
60
|
+
err: new Error('Something went wrong'),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
expect(errorStub.calledOnce).to.be.true
|
|
64
|
+
const [message] = errorStub.firstCall.args
|
|
65
|
+
expect(message).to.equal('TestHeader: Something went wrong')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('logs a warning with custom level', () => {
|
|
69
|
+
logger.logErr({
|
|
70
|
+
header: 'WarnHeader',
|
|
71
|
+
err: new Error('This is a warning'),
|
|
72
|
+
level: 'warn',
|
|
73
|
+
meta: { cause: 'testing' },
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expect(warnStub.calledOnce).to.be.true
|
|
77
|
+
const [message, meta] = warnStub.firstCall.args
|
|
78
|
+
expect(message).to.equal('WarnHeader: This is a warning')
|
|
79
|
+
expect(meta).to.deep.equal({ cause: 'testing' })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('logs unexpected error types gracefully', () => {
|
|
83
|
+
logger.logErr({
|
|
84
|
+
header: 'Oops',
|
|
85
|
+
err: 'some string error',
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(errorStub.calledOnce).to.be.true
|
|
89
|
+
const [message] = errorStub.firstCall.args
|
|
90
|
+
expect(message).to.equal('unexpected Oops: some string error')
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const { expect } = require('chai')
|
|
2
|
+
const request = require('supertest')
|
|
3
|
+
const sinon = require('sinon')
|
|
4
|
+
const express = require('express')
|
|
5
|
+
const { createHandler } = require('graphql-http/lib/use/express')
|
|
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 }))
|
|
12
|
+
|
|
13
|
+
describe('GraphQL Schema', () => {
|
|
14
|
+
let saveStub: SinonStub
|
|
15
|
+
let findStub: SinonStub
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
// Stub db methods
|
|
19
|
+
saveStub = sinon.stub(db.customers, 'save').resolves({
|
|
20
|
+
id: '1',
|
|
21
|
+
name: 'Alice',
|
|
22
|
+
email: 'alice@example.com',
|
|
23
|
+
phone: '123-456',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
findStub = sinon.stub(db.customers, 'find').resolves([
|
|
27
|
+
{
|
|
28
|
+
id: '1',
|
|
29
|
+
name: 'Alice',
|
|
30
|
+
email: 'alice@example.com',
|
|
31
|
+
phone: '123-456',
|
|
32
|
+
},
|
|
33
|
+
])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
sinon.restore()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('returns customers', async () => {
|
|
41
|
+
const query = {
|
|
42
|
+
query: `
|
|
43
|
+
query {
|
|
44
|
+
customers {
|
|
45
|
+
id
|
|
46
|
+
name
|
|
47
|
+
email
|
|
48
|
+
phone
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
`,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const res = await request(app)
|
|
55
|
+
.post('/graphql')
|
|
56
|
+
.send(query)
|
|
57
|
+
.set('Accept', 'application/json')
|
|
58
|
+
|
|
59
|
+
expect(res.statusCode).to.equal(200)
|
|
60
|
+
expect(res.body).to.have.nested.property('data.customers').that.is.an('array')
|
|
61
|
+
expect(res.body.data.customers[0]).to.include({
|
|
62
|
+
id: '1',
|
|
63
|
+
name: 'Alice',
|
|
64
|
+
email: 'alice@example.com',
|
|
65
|
+
phone: '123-456',
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('adds a customer', async () => {
|
|
70
|
+
const mutation = {
|
|
71
|
+
query: `
|
|
72
|
+
mutation {
|
|
73
|
+
addCustomer(name: "Alice", email: "alice@example.com", phone: "123-456") {
|
|
74
|
+
id
|
|
75
|
+
name
|
|
76
|
+
email
|
|
77
|
+
phone
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
`,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const res = await request(app)
|
|
84
|
+
.post('/graphql')
|
|
85
|
+
.send(mutation)
|
|
86
|
+
.set('Accept', 'application/json')
|
|
87
|
+
|
|
88
|
+
expect(res.statusCode).to.equal(200)
|
|
89
|
+
expect(res.body).to.have.nested.property('data.addCustomer').that.includes({
|
|
90
|
+
id: '1',
|
|
91
|
+
name: 'Alice',
|
|
92
|
+
email: 'alice@example.com',
|
|
93
|
+
phone: '123-456',
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
expect(saveStub.calledOnce).to.be.true
|
|
97
|
+
expect(saveStub.firstCall.args[0]).to.include({
|
|
98
|
+
name: 'Alice',
|
|
99
|
+
email: 'alice@example.com',
|
|
100
|
+
phone: '123-456',
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
})
|
package/src/config/db.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const z = require('zod')
|
|
3
|
+
const { log, logErr } = require('../utilities/logger')
|
|
4
|
+
const { customerSchema } = require('../models/Customer')
|
|
5
|
+
const { orderSchema } = require('../models/Order')
|
|
6
|
+
import type { Customer } from '../models/Customer'
|
|
7
|
+
import type { Order } from '../models/Order'
|
|
8
|
+
import { DataSet } from '../typing/enums'
|
|
9
|
+
import type { Id, Err } from '../typing/types'
|
|
10
|
+
import type * as I from '../typing/interfaces'
|
|
11
|
+
|
|
12
|
+
require('dotenv-flow').config()
|
|
13
|
+
|
|
14
|
+
const crud = {
|
|
15
|
+
url: `http://localhost:${process.env.JSON_SERVER_PORT || 3210}`,
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
},
|
|
19
|
+
datasets: { ...DataSet },
|
|
20
|
+
errors: {
|
|
21
|
+
throw({ response }: { response: Response }) {
|
|
22
|
+
throw new Error(`fetch failed with status ${response.status}`)
|
|
23
|
+
},
|
|
24
|
+
handle({ err }: { err: Err }) {
|
|
25
|
+
logErr({ header: 'fetch error', err })
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
async create({ dataSet, doc }: I.Create) {
|
|
29
|
+
const resource = path.join(this.url, dataSet)
|
|
30
|
+
const options = {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: this.headers,
|
|
33
|
+
body: JSON.stringify(doc),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let result
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const response: Response = await fetch(resource, options)
|
|
40
|
+
|
|
41
|
+
if (!response.ok) this.errors.throw({ response })
|
|
42
|
+
|
|
43
|
+
result = await response.json()
|
|
44
|
+
} catch (err: unknown) {
|
|
45
|
+
this.errors.handle({ err })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result
|
|
49
|
+
},
|
|
50
|
+
async read({ dataSet, filter }: I.Read) {
|
|
51
|
+
let resource = path.join(this.url, dataSet)
|
|
52
|
+
|
|
53
|
+
// add "filter" as query params
|
|
54
|
+
if (filter && Object.keys(filter).length) {
|
|
55
|
+
const searchParams = new URLSearchParams(
|
|
56
|
+
Object.entries(filter)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
resource += '?' + searchParams.toString()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let result: Customer[] | Order[]
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const response: Response = await fetch(resource)
|
|
66
|
+
|
|
67
|
+
if (!response.ok) this.errors.throw({ response })
|
|
68
|
+
|
|
69
|
+
result = await response.json()
|
|
70
|
+
|
|
71
|
+
return result
|
|
72
|
+
} catch (err: unknown) {
|
|
73
|
+
this.errors.handle({ err })
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
async readOne({ dataSet, id }: I.ReadOne) {
|
|
77
|
+
const resource = path.join(this.url, dataSet, id)
|
|
78
|
+
|
|
79
|
+
let result: Customer | Order
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const response: Response = await fetch(resource)
|
|
83
|
+
|
|
84
|
+
if (!response.ok) this.errors.throw({ response })
|
|
85
|
+
|
|
86
|
+
result = await response.json()
|
|
87
|
+
|
|
88
|
+
return result
|
|
89
|
+
} catch (err: unknown) {
|
|
90
|
+
this.errors.handle({ err })
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async update({ dataSet, id, update }: I.Update) {
|
|
94
|
+
const resource = path.join(this.url, dataSet, id)
|
|
95
|
+
const options = {
|
|
96
|
+
method: 'PATCH',
|
|
97
|
+
headers: this.headers,
|
|
98
|
+
body: JSON.stringify(update),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let result
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const response: Response = await fetch(resource, options)
|
|
105
|
+
|
|
106
|
+
if (!response.ok) this.errors.throw({ response })
|
|
107
|
+
|
|
108
|
+
result = await response.json()
|
|
109
|
+
} catch (err: unknown) {
|
|
110
|
+
this.errors.handle({ err })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result
|
|
114
|
+
},
|
|
115
|
+
async delete({ dataSet, id }: I.Delete) {
|
|
116
|
+
const resource = path.join(this.url, dataSet, id)
|
|
117
|
+
const options = {
|
|
118
|
+
method: 'DELETE',
|
|
119
|
+
headers: this.headers,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let result
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const response: Response = await fetch(resource, options)
|
|
126
|
+
|
|
127
|
+
if (!response.ok) this.errors.throw({ response })
|
|
128
|
+
|
|
129
|
+
result = await response.json()
|
|
130
|
+
} catch (err: unknown) {
|
|
131
|
+
this.errors.handle({ err })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return result
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export = {
|
|
139
|
+
[crud.datasets.customers]: {
|
|
140
|
+
async save(doc: Customer) {
|
|
141
|
+
let customer: Customer | undefined
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
customer = customerSchema.parse({
|
|
145
|
+
name: doc.name,
|
|
146
|
+
email: doc.email,
|
|
147
|
+
phone: doc.phone,
|
|
148
|
+
})
|
|
149
|
+
} catch (err: typeof z.ZodError | unknown) {
|
|
150
|
+
if(err instanceof z.ZodError) {
|
|
151
|
+
err.issues.forEach((err: typeof z.ZodError, index: number) => {
|
|
152
|
+
log.error(`${err.path[index]} ${err.message}`, { index })
|
|
153
|
+
})
|
|
154
|
+
} else {
|
|
155
|
+
logErr({ header: 'customer parse error', err })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (customer) {
|
|
160
|
+
const result = await crud.create({
|
|
161
|
+
dataSet: crud.datasets.customers,
|
|
162
|
+
doc: customer,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return result
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
async find({ filter }: I.FilterOptions) {
|
|
169
|
+
const result = await crud.read({
|
|
170
|
+
dataSet: crud.datasets.customers,
|
|
171
|
+
filter,
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
return result
|
|
175
|
+
},
|
|
176
|
+
async findById(id: Id) {
|
|
177
|
+
const result = await crud.readOne({
|
|
178
|
+
dataSet: crud.datasets.customers,
|
|
179
|
+
id,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return result
|
|
183
|
+
},
|
|
184
|
+
async findByIdAndUpdate(id: Id, { update }: I.UpdateOptions) {
|
|
185
|
+
const result = await crud.update({
|
|
186
|
+
dataSet: crud.datasets.customers,
|
|
187
|
+
id,
|
|
188
|
+
update,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return result
|
|
192
|
+
},
|
|
193
|
+
async findByIdAndRemove(id: Id) {
|
|
194
|
+
const result = await crud.delete({
|
|
195
|
+
dataSet: crud.datasets.customers,
|
|
196
|
+
id,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return result
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
[crud.datasets.orders]: {
|
|
203
|
+
async save(doc: Order) {
|
|
204
|
+
let order: Order | undefined
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
order = orderSchema.parse({
|
|
208
|
+
name: doc.name,
|
|
209
|
+
...(doc.notes && { notes: doc.notes }),
|
|
210
|
+
status: doc.status,
|
|
211
|
+
customerId: doc.customerId,
|
|
212
|
+
})
|
|
213
|
+
} catch (err: typeof z.ZodError | unknown) {
|
|
214
|
+
if (err instanceof z.ZodError) {
|
|
215
|
+
err.issues.forEach((err: typeof z.ZodError, index: number) => {
|
|
216
|
+
log.error(`${err.path[index]} ${err.message}`, { index })
|
|
217
|
+
})
|
|
218
|
+
} else {
|
|
219
|
+
logErr({ header: 'order parse error', err })
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (order) {
|
|
224
|
+
const result = await crud.create({
|
|
225
|
+
dataSet: crud.datasets.orders,
|
|
226
|
+
doc: order,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
return result
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
async find({ filter }: I.FilterOptions) {
|
|
233
|
+
const result = await crud.read({
|
|
234
|
+
dataSet: crud.datasets.orders,
|
|
235
|
+
filter,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
return result
|
|
239
|
+
},
|
|
240
|
+
async findById(id: Id) {
|
|
241
|
+
console.log(id)
|
|
242
|
+
const result = await crud.readOne({
|
|
243
|
+
dataSet: crud.datasets.orders,
|
|
244
|
+
id,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
return result
|
|
248
|
+
},
|
|
249
|
+
async findByIdAndUpdate(id: Id, { update }: I.UpdateOptions) {
|
|
250
|
+
const result = await crud.update({
|
|
251
|
+
dataSet: crud.datasets.orders,
|
|
252
|
+
id,
|
|
253
|
+
update,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
return result
|
|
257
|
+
},
|
|
258
|
+
async findByIdAndRemove(id: Id) {
|
|
259
|
+
const result = await crud.delete({
|
|
260
|
+
dataSet: crud.datasets.orders,
|
|
261
|
+
id,
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
return result
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const express = require('express')
|
|
2
|
+
const graphqlRouter = express.Router()
|
|
3
|
+
const cors = require('cors')
|
|
4
|
+
const helmet = require('helmet')
|
|
5
|
+
const chalk = require('chalk')
|
|
6
|
+
const { createHandler } = require('graphql-http/lib/use/express')
|
|
7
|
+
const { ruruHTML } = require('ruru/server')
|
|
8
|
+
const routes = require('./routes')
|
|
9
|
+
const { schema } = require('./schema/schema')
|
|
10
|
+
const logger = require('./utilities/logger')
|
|
11
|
+
import type { Application, Request, Response } from 'express'
|
|
12
|
+
import { NodeEnv } from './typing/enums'
|
|
13
|
+
|
|
14
|
+
require('dotenv-flow').config()
|
|
15
|
+
|
|
16
|
+
const { production, development } = NodeEnv
|
|
17
|
+
|
|
18
|
+
const PORT = process.env.PORT || 3000
|
|
19
|
+
const NODE_ENV = process.env.NODE_ENV || development
|
|
20
|
+
|
|
21
|
+
const app: Application = express()
|
|
22
|
+
|
|
23
|
+
app.use(cors())
|
|
24
|
+
|
|
25
|
+
if (NODE_ENV === production) {
|
|
26
|
+
app.use(helmet())
|
|
27
|
+
} else {
|
|
28
|
+
app.use(
|
|
29
|
+
helmet({
|
|
30
|
+
contentSecurityPolicy: false,
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
app.use(routes)
|
|
36
|
+
|
|
37
|
+
graphqlRouter.all('/',
|
|
38
|
+
createHandler({
|
|
39
|
+
schema,
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
app.use('/graphql', graphqlRouter)
|
|
44
|
+
|
|
45
|
+
if (NODE_ENV === development) {
|
|
46
|
+
// ruru GraphiQL runs on the endpoint '/' not '/graphql'
|
|
47
|
+
app.get('/', (req: Request, res: Response) => {
|
|
48
|
+
res.type('html')
|
|
49
|
+
res.end(ruruHTML({
|
|
50
|
+
endpoint: '/graphql',
|
|
51
|
+
}))
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
app.listen(PORT, () => logger.logEnv({
|
|
56
|
+
msg: `running on ${chalk.blue.bold(PORT)}`,
|
|
57
|
+
}))
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const customerSchema = z.object({
|
|
4
|
+
id: z.string().optional(), // optional? let db add id on save
|
|
5
|
+
name: z.string(),
|
|
6
|
+
email: z.string().email(),
|
|
7
|
+
phone: z.string(),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export type Customer = z.infer<typeof customerSchema>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { Product, Status } from '../typing/enums'
|
|
3
|
+
|
|
4
|
+
export const orderSchema = z.object({
|
|
5
|
+
id: z.string().optional(), // optional? let db add id on save
|
|
6
|
+
name: z.nativeEnum(Product),
|
|
7
|
+
notes: z.string().optional(),
|
|
8
|
+
status: z.nativeEnum(Status),
|
|
9
|
+
customerId: z.string(),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export type Order = z.infer<typeof orderSchema>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const express = require('express')
|
|
2
|
+
const router = express.Router()
|
|
3
|
+
import type { Request, Response } from 'express'
|
|
4
|
+
import { NodeEnv } from '../typing/enums'
|
|
5
|
+
|
|
6
|
+
require('dotenv-flow').config()
|
|
7
|
+
|
|
8
|
+
// NOTE remove this if you wish, it won't affect the graphql route,
|
|
9
|
+
// this is just here to test express and typescript live transpile,
|
|
10
|
+
// but also demonstrates a conventional routing pattern if needed
|
|
11
|
+
|
|
12
|
+
if (process.env.NODE_ENV === NodeEnv.development) {
|
|
13
|
+
router.get('/check', (req: Request, res: Response) => {
|
|
14
|
+
res.status(200).send('okay...')
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export = router
|