@next-k8s/tickets 1.0.25 → 1.0.28
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +24 -0
- package/coverage/clover.xml +180 -3
- package/coverage/coverage-final.json +13 -1
- package/coverage/lcov-report/events/listeners/_queue-group-name.ts.html +88 -0
- package/coverage/lcov-report/events/listeners/index.html +131 -0
- package/coverage/lcov-report/events/listeners/order-created.ts.html +157 -0
- package/coverage/lcov-report/events/publishers/index.html +116 -0
- package/coverage/lcov-report/events/publishers/updated.ts.html +106 -0
- package/coverage/lcov-report/index.html +101 -11
- package/coverage/lcov-report/models/index.html +116 -0
- package/coverage/lcov-report/models/ticket.ts.html +235 -0
- package/coverage/lcov-report/src/app.ts.html +17 -17
- package/coverage/lcov-report/src/events/listeners/_queue-group-name.ts.html +88 -0
- package/coverage/lcov-report/src/events/listeners/index.html +146 -0
- package/coverage/lcov-report/src/events/listeners/order-cancelled.ts.html +157 -0
- package/coverage/lcov-report/src/events/listeners/order-created.ts.html +157 -0
- package/coverage/lcov-report/src/events/publishers/created.ts.html +106 -0
- package/coverage/lcov-report/src/events/publishers/index.html +131 -0
- package/coverage/lcov-report/src/events/publishers/updated.ts.html +106 -0
- package/coverage/lcov-report/src/index.html +1 -1
- package/coverage/lcov-report/src/models/index.html +5 -5
- package/coverage/lcov-report/src/models/ticket.ts.html +35 -14
- package/coverage/lcov-report/src/routes/create.ts.html +18 -18
- package/coverage/lcov-report/src/routes/find.ts.html +1 -1
- package/coverage/lcov-report/src/routes/get.ts.html +25 -10
- package/coverage/lcov-report/src/routes/index.html +12 -27
- package/coverage/lcov-report/src/routes/index.ts.html +8 -11
- package/coverage/lcov-report/src/routes/update.ts.html +19 -19
- package/coverage/lcov-report/src/test/index.html +1 -1
- package/coverage/lcov-report/src/test/utils.ts.html +4 -4
- package/coverage/lcov-report/ticket.ts.html +220 -0
- package/coverage/lcov.info +282 -0
- package/package.json +4 -3
- package/pnpm-lock.yaml +334 -18
- package/src/events/listeners/__test__/order-cancelled.test.ts +50 -0
- package/src/events/listeners/__test__/order-created.test.ts +50 -0
- package/src/events/listeners/_queue-group-name.ts +1 -0
- package/src/events/listeners/order-cancelled.ts +24 -0
- package/src/events/listeners/order-created.ts +24 -0
- package/src/index.ts +5 -0
- package/src/models/__test__/ticket.test.ts +37 -0
- package/src/models/ticket.ts +10 -3
- package/src/routes/__test__/get.test.ts +15 -0
- package/src/routes/__test__/update.test.ts +15 -0
- package/src/routes/create.ts +2 -2
- package/src/routes/get.ts +5 -0
- package/src/routes/index.ts +1 -2
- package/src/routes/update.ts +3 -3
- package/src/routes/__test__/find.test.ts +0 -22
- package/src/routes/find.ts +0 -11
@@ -0,0 +1,50 @@
|
|
1
|
+
import mongoose from 'mongoose'
|
2
|
+
import { Message } from 'node-nats-streaming'
|
3
|
+
import { OrderCancelledEvent, OrderStatus } from '@next-k8s/common'
|
4
|
+
|
5
|
+
import Ticket from '../../../models/ticket'
|
6
|
+
import natsClient from '../../../nats-client'
|
7
|
+
import OrderCancelledListener from '../order-cancelled'
|
8
|
+
|
9
|
+
const setup = async () => {
|
10
|
+
const owner = new mongoose.Types.ObjectId().toHexString()
|
11
|
+
const orderId = new mongoose.Types.ObjectId().toHexString()
|
12
|
+
const listener = new OrderCancelledListener(natsClient.client)
|
13
|
+
const ticket = new Ticket({ title: 'Test Ticket', price: 21000, owner })
|
14
|
+
|
15
|
+
ticket.set({ orderId })
|
16
|
+
await ticket.save()
|
17
|
+
|
18
|
+
const data: OrderCancelledEvent['data'] = {
|
19
|
+
id: new mongoose.Types.ObjectId().toHexString(),
|
20
|
+
version: 0,
|
21
|
+
ticket: {
|
22
|
+
id: ticket.id
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
// @ts-ignore
|
27
|
+
const message: Message = { ack: jest.fn() }
|
28
|
+
return { data, listener, message, orderId, ticket }
|
29
|
+
}
|
30
|
+
|
31
|
+
describe('[Order Cancelled] Listener', () => {
|
32
|
+
it('should clear the orderId of the ticket', async () => {
|
33
|
+
const { data, listener, message, ticket } = await setup()
|
34
|
+
await listener.onMessage(data, message)
|
35
|
+
const updatedTicket = await Ticket.findById(ticket.id)
|
36
|
+
expect(updatedTicket.orderId).not.toBeDefined()
|
37
|
+
})
|
38
|
+
|
39
|
+
it('should ack the message', async () =>{
|
40
|
+
const { data, listener, message } = await setup()
|
41
|
+
await listener.onMessage(data, message)
|
42
|
+
expect(message.ack).toHaveBeenCalled()
|
43
|
+
})
|
44
|
+
|
45
|
+
it('should publish an order:updated event', async () => {
|
46
|
+
const { data, listener, message } = await setup()
|
47
|
+
await listener.onMessage(data, message)
|
48
|
+
expect(natsClient.client.publish).toHaveBeenCalled()
|
49
|
+
})
|
50
|
+
})
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import mongoose from 'mongoose'
|
2
|
+
import { Message } from 'node-nats-streaming'
|
3
|
+
import { OrderCreatedEvent, OrderStatus } from '@next-k8s/common'
|
4
|
+
|
5
|
+
import Ticket from '../../../models/ticket'
|
6
|
+
import natsClient from '../../../nats-client'
|
7
|
+
import OrderCreatedListener from '../order-created'
|
8
|
+
|
9
|
+
const setup = async () => {
|
10
|
+
const listener = new OrderCreatedListener(natsClient.client)
|
11
|
+
const ticket = new Ticket({ title: 'Test Ticket', price: 21000, owner: new mongoose.Types.ObjectId().toHexString() })
|
12
|
+
await ticket.save()
|
13
|
+
|
14
|
+
const data: OrderCreatedEvent['data'] = {
|
15
|
+
id: new mongoose.Types.ObjectId().toHexString(),
|
16
|
+
version: 0,
|
17
|
+
status: OrderStatus.Created,
|
18
|
+
owner: new mongoose.Types.ObjectId().toHexString(),
|
19
|
+
expiresAt: new Date().toUTCString(),
|
20
|
+
ticket: {
|
21
|
+
id: ticket.id,
|
22
|
+
price: ticket.price
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
26
|
+
// @ts-ignore
|
27
|
+
const message: Message = { ack: jest.fn() }
|
28
|
+
return { data, listener, message, ticket }
|
29
|
+
}
|
30
|
+
|
31
|
+
describe('[Order Created] Listener', () => {
|
32
|
+
it('should set the ticket orderId', async () => {
|
33
|
+
const { data, listener, message, ticket } = await setup()
|
34
|
+
await listener.onMessage(data, message)
|
35
|
+
const updatedTicket = await Ticket.findById(ticket.id)
|
36
|
+
expect(updatedTicket.orderId).toBe(data.id)
|
37
|
+
})
|
38
|
+
|
39
|
+
it('should ack the message', async () =>{
|
40
|
+
const { data, listener, message } = await setup()
|
41
|
+
await listener.onMessage(data, message)
|
42
|
+
expect(message.ack).toHaveBeenCalled()
|
43
|
+
})
|
44
|
+
|
45
|
+
it('should publish a ticket:updated event', async () => {
|
46
|
+
const { data, listener, message } = await setup()
|
47
|
+
await listener.onMessage(data, message)
|
48
|
+
expect(natsClient.client.publish).toHaveBeenCalled()
|
49
|
+
})
|
50
|
+
})
|
@@ -0,0 +1 @@
|
|
1
|
+
export default 'tickets-service'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { Listener, NotFoundError, OrderCancelledEvent, Subjects } from '@next-k8s/common'
|
2
|
+
import { Message } from 'node-nats-streaming'
|
3
|
+
import Ticket from '../../models/ticket'
|
4
|
+
import TicketUpdatedPublisher from '../publishers/updated'
|
5
|
+
|
6
|
+
import queueGroupName from './_queue-group-name'
|
7
|
+
|
8
|
+
export class OrderCancelledListener extends Listener<OrderCancelledEvent> {
|
9
|
+
readonly subject = Subjects.OrderCancelled
|
10
|
+
queueGroupName = queueGroupName
|
11
|
+
|
12
|
+
async onMessage (data: OrderCancelledEvent['data'], msg: Message) {
|
13
|
+
const ticket = await Ticket.findById(data.ticket.id)
|
14
|
+
if (!ticket) throw new NotFoundError('Ticket not found')
|
15
|
+
ticket.set({ orderId: undefined })
|
16
|
+
await ticket.save()
|
17
|
+
|
18
|
+
const { id, version, price, title, owner, orderId } = ticket
|
19
|
+
await new TicketUpdatedPublisher(this.client).publish({ id, version, price, title, owner, orderId })
|
20
|
+
msg.ack()
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
export default OrderCancelledListener
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { Listener, NotFoundError, OrderCreatedEvent, Subjects } from '@next-k8s/common'
|
2
|
+
import { Message } from 'node-nats-streaming'
|
3
|
+
import Ticket from '../../models/ticket'
|
4
|
+
import TicketUpdatedPublisher from '../publishers/updated'
|
5
|
+
|
6
|
+
import queueGroupName from './_queue-group-name'
|
7
|
+
|
8
|
+
export class OrderCreatedListener extends Listener<OrderCreatedEvent> {
|
9
|
+
readonly subject = Subjects.OrderCreated
|
10
|
+
queueGroupName = queueGroupName
|
11
|
+
|
12
|
+
async onMessage (data: OrderCreatedEvent['data'], msg: Message) {
|
13
|
+
const ticket = await Ticket.findById(data.ticket.id)
|
14
|
+
if (!ticket) throw new NotFoundError('Ticket not found')
|
15
|
+
ticket.set({ orderId: data.id })
|
16
|
+
await ticket.save()
|
17
|
+
|
18
|
+
const { id, version, price, title, owner, orderId } = ticket
|
19
|
+
await new TicketUpdatedPublisher(this.client).publish({ id, version, price, title, owner, orderId })
|
20
|
+
msg.ack()
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
export default OrderCreatedListener
|
package/src/index.ts
CHANGED
@@ -2,6 +2,8 @@ import mongoose from 'mongoose'
|
|
2
2
|
|
3
3
|
import natsClient from './nats-client'
|
4
4
|
import app from './app'
|
5
|
+
import OrderCreatedListener from './events/listeners/order-created'
|
6
|
+
import OrderCancelledListener from './events/listeners/order-cancelled'
|
5
7
|
|
6
8
|
const start = async () => {
|
7
9
|
if (!process.env.JWT_KEY) throw new Error('JWT_KEY is undefined')
|
@@ -20,6 +22,9 @@ const start = async () => {
|
|
20
22
|
process.on('SIGINT', () => natsClient.client.close())
|
21
23
|
process.on('SIGTERM', () => natsClient.client.close())
|
22
24
|
|
25
|
+
new OrderCreatedListener(natsClient.client).listen()
|
26
|
+
new OrderCancelledListener(natsClient.client).listen()
|
27
|
+
|
23
28
|
await mongoose.connect(process.env.MONGO_URI)
|
24
29
|
console.log('Database connected!')
|
25
30
|
const port = process.env.PORT || 3000
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import mongoose from 'mongoose'
|
2
|
+
import Ticket from '../ticket'
|
3
|
+
|
4
|
+
describe('[Models] Ticket Model', () => {
|
5
|
+
it('implements optimistic concurrency control', async () => {
|
6
|
+
const ticket = new Ticket({ title: 'Test Ticket', price: 20000, owner: new mongoose.Types.ObjectId().toHexString() })
|
7
|
+
await ticket.save()
|
8
|
+
|
9
|
+
const copies = [
|
10
|
+
await Ticket.findById(ticket.id),
|
11
|
+
await Ticket.findById(ticket.id)
|
12
|
+
]
|
13
|
+
|
14
|
+
copies[0].set({ price: 15000 })
|
15
|
+
copies[1].set({ price: 23000 })
|
16
|
+
|
17
|
+
await copies[0].save()
|
18
|
+
|
19
|
+
try {
|
20
|
+
await copies[1].save()
|
21
|
+
} catch {
|
22
|
+
return
|
23
|
+
}
|
24
|
+
|
25
|
+
throw new Error('Ticket was saved when it should not have been')
|
26
|
+
})
|
27
|
+
|
28
|
+
it('increments the version number on update', async () => {
|
29
|
+
const ticket = new Ticket({ title: 'Test Ticket', price: 20000, owner: new mongoose.Types.ObjectId().toHexString() })
|
30
|
+
await ticket.save()
|
31
|
+
expect(ticket.version).toEqual(0)
|
32
|
+
await ticket.save()
|
33
|
+
expect(ticket.version).toEqual(1)
|
34
|
+
await ticket.save()
|
35
|
+
expect(ticket.version).toEqual(2)
|
36
|
+
})
|
37
|
+
})
|
package/src/models/ticket.ts
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
import mongoose, { ObjectId } from 'mongoose'
|
2
|
+
import { updateIfCurrentPlugin } from 'mongoose-update-if-current'
|
2
3
|
|
3
4
|
interface TicketAttributes {
|
4
|
-
title:
|
5
|
-
price:
|
5
|
+
title: string;
|
6
|
+
price: number;
|
6
7
|
owner: string;
|
7
8
|
createdAt?: Date;
|
8
9
|
updatedAt?: Date;
|
@@ -22,10 +23,13 @@ const ticketSchema = new mongoose.Schema({
|
|
22
23
|
owner: {
|
23
24
|
type: mongoose.Schema.Types.ObjectId,
|
24
25
|
required: true
|
26
|
+
},
|
27
|
+
|
28
|
+
orderId: {
|
29
|
+
type: String
|
25
30
|
}
|
26
31
|
}, {
|
27
32
|
toJSON: {
|
28
|
-
versionKey: false,
|
29
33
|
transform (doc, ret) {
|
30
34
|
ret.id = ret._id
|
31
35
|
delete ret._id
|
@@ -34,6 +38,9 @@ const ticketSchema = new mongoose.Schema({
|
|
34
38
|
}
|
35
39
|
})
|
36
40
|
|
41
|
+
ticketSchema.set('versionKey', 'version')
|
42
|
+
ticketSchema.plugin(updateIfCurrentPlugin)
|
43
|
+
|
37
44
|
export const TicketModel = mongoose.model('Ticket', ticketSchema)
|
38
45
|
export default class Ticket extends TicketModel {
|
39
46
|
constructor(attributes: TicketAttributes) {
|
@@ -5,6 +5,21 @@ import { getTokenCookie } from '@next-k8s/common'
|
|
5
5
|
import app from '../../app'
|
6
6
|
import { createTicket } from '../../test/utils'
|
7
7
|
|
8
|
+
describe('[List Tickets] Route: GET /api/tickets', () => {
|
9
|
+
it('should return a list of tickets', async () => {
|
10
|
+
const cookie = await getTokenCookie({ id: new mongoose.Types.ObjectId().toHexString() })
|
11
|
+
await createTicket(app, cookie)
|
12
|
+
await createTicket(app, cookie, 'Test Event 2', 40000)
|
13
|
+
const list = await request(app)
|
14
|
+
.get('/api/tickets')
|
15
|
+
.send()
|
16
|
+
.expect(200)
|
17
|
+
|
18
|
+
expect(list.body.tickets).toBeDefined()
|
19
|
+
expect(list.body.tickets.length).toEqual(2)
|
20
|
+
})
|
21
|
+
})
|
22
|
+
|
8
23
|
describe('[Get Ticket] Route: GET /api/tickets/:id', () => {
|
9
24
|
it('should throw a BadRequestError if ticket ID is invalid', async () => {
|
10
25
|
await request(app)
|
@@ -5,6 +5,7 @@ import { getTokenCookie } from '@next-k8s/common'
|
|
5
5
|
import app from '../../app'
|
6
6
|
import { createTicket } from '../../test/utils'
|
7
7
|
import natsClient from '../../nats-client'
|
8
|
+
import Ticket from '../../models/ticket'
|
8
9
|
|
9
10
|
describe('[Update Ticket] Route: PUT /api/tickets/:id', () => {
|
10
11
|
it('should throw a NotFoundError if the ticket does not exist', async () => {
|
@@ -100,4 +101,18 @@ describe('[Update Ticket] Route: PUT /api/tickets/:id', () => {
|
|
100
101
|
await createTicket(app, cookie)
|
101
102
|
expect(natsClient.client.publish).toHaveBeenCalled()
|
102
103
|
})
|
104
|
+
|
105
|
+
it('should reject updates to a reserved ticket', async () => {
|
106
|
+
const cookie = await getTokenCookie({ id: new mongoose.Types.ObjectId().toHexString() })
|
107
|
+
const response = await createTicket(app, cookie)
|
108
|
+
const ticket = await Ticket.findById(response.body.ticket.id)
|
109
|
+
ticket.set({ orderId: new mongoose.Types.ObjectId().toHexString() })
|
110
|
+
await ticket.save()
|
111
|
+
|
112
|
+
await request(app)
|
113
|
+
.put(`/api/tickets/${response.body.ticket.id}`)
|
114
|
+
.set('Cookie', [cookie])
|
115
|
+
.send({ title: 'Test Event 2', price: 33000 })
|
116
|
+
.expect(400)
|
117
|
+
})
|
103
118
|
})
|
package/src/routes/create.ts
CHANGED
@@ -3,7 +3,7 @@ import { body } from 'express-validator'
|
|
3
3
|
import { requireAuth, validateRequest } from '@next-k8s/common'
|
4
4
|
|
5
5
|
import Ticket from '../models/ticket'
|
6
|
-
import
|
6
|
+
import TicketCreatedPublisher from '../events/publishers/created'
|
7
7
|
import natsClient from '../nats-client'
|
8
8
|
|
9
9
|
const router = express.Router()
|
@@ -17,7 +17,7 @@ router.post('/api/tickets', requireAuth, validateInput, validateRequest, async (
|
|
17
17
|
const { title, price } = req.body
|
18
18
|
const ticket = new Ticket({ title, price, owner: req.currentUser!.id })
|
19
19
|
await ticket.save()
|
20
|
-
new TicketCreatedPublisher(natsClient.client).publish({ id: ticket.id, title: ticket.title, price: ticket.price, owner: ticket.owner })
|
20
|
+
new TicketCreatedPublisher(natsClient.client).publish({ id: ticket.id, version: ticket.version, title: ticket.title, price: ticket.price, owner: ticket.owner })
|
21
21
|
res.status(201).send({ ticket })
|
22
22
|
})
|
23
23
|
|
package/src/routes/get.ts
CHANGED
@@ -6,6 +6,11 @@ import Ticket from '../models/ticket'
|
|
6
6
|
|
7
7
|
const router = express.Router()
|
8
8
|
|
9
|
+
router.get('/api/tickets', async (req: Request, res: Response) => {
|
10
|
+
const tickets = await Ticket.find({})
|
11
|
+
res.send({ tickets })
|
12
|
+
})
|
13
|
+
|
9
14
|
router.get('/api/tickets/:id', async (req: Request, res: Response) => {
|
10
15
|
if (!isValidObjectId(req.params.id)) throw new BadRequestError('Invalid Ticket ID')
|
11
16
|
const ticket = await Ticket.findById(req.params.id)
|
package/src/routes/index.ts
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
import getTicket from './get'
|
2
|
-
import findTickets from './find'
|
3
2
|
import createTicket from './create'
|
4
3
|
import updateTicket from './update'
|
5
4
|
|
6
|
-
export default [
|
5
|
+
export default [getTicket, createTicket, updateTicket]
|
package/src/routes/update.ts
CHANGED
@@ -2,9 +2,8 @@ import { BadRequestError, NotFoundError, UnauthorizedError, validateRequest, req
|
|
2
2
|
import express, { Request, Response } from 'express'
|
3
3
|
import { body } from 'express-validator'
|
4
4
|
import { isValidObjectId } from 'mongoose'
|
5
|
-
|
6
5
|
import Ticket from '../models/ticket'
|
7
|
-
import TicketUpdatedPublisher from '../events/publishers/
|
6
|
+
import TicketUpdatedPublisher from '../events/publishers/updated'
|
8
7
|
import natsClient from '../nats-client'
|
9
8
|
|
10
9
|
const router = express.Router()
|
@@ -19,10 +18,11 @@ router.put('/api/tickets/:id', requireAuth, validateInput, validateRequest, asyn
|
|
19
18
|
const ticket = await Ticket.findById(req.params.id)
|
20
19
|
if (!ticket) throw new NotFoundError()
|
21
20
|
if (ticket.owner.toHexString() !== req.currentUser!.id) throw new UnauthorizedError()
|
21
|
+
if (ticket.orderId) throw new BadRequestError('Ticket is reserved')
|
22
22
|
const { title, price } = req.body
|
23
23
|
ticket.set({ title, price })
|
24
24
|
await ticket.save()
|
25
|
-
await new TicketUpdatedPublisher(natsClient.client).publish({ id: ticket.id, title: ticket.title, price: ticket.price, owner: ticket.owner })
|
25
|
+
await new TicketUpdatedPublisher(natsClient.client).publish({ id: ticket.id, version: ticket.version, title: ticket.title, price: ticket.price, owner: ticket.owner })
|
26
26
|
res.json(ticket)
|
27
27
|
})
|
28
28
|
|
@@ -1,22 +0,0 @@
|
|
1
|
-
|
2
|
-
import mongoose from 'mongoose'
|
3
|
-
import request from 'supertest'
|
4
|
-
import { getTokenCookie } from '@next-k8s/common'
|
5
|
-
|
6
|
-
import app from '../../app'
|
7
|
-
import { createTicket } from '../../test/utils'
|
8
|
-
|
9
|
-
describe('[List Tickets] Route: GET /api/tickets', () => {
|
10
|
-
it('should return a list of tickets', async () => {
|
11
|
-
const cookie = await getTokenCookie({ id: new mongoose.Types.ObjectId().toHexString() })
|
12
|
-
await createTicket(app, cookie)
|
13
|
-
await createTicket(app, cookie, 'Test Event 2', 40000)
|
14
|
-
const list = await request(app)
|
15
|
-
.get('/api/tickets')
|
16
|
-
.send()
|
17
|
-
.expect(200)
|
18
|
-
|
19
|
-
expect(list.body.tickets).toBeDefined()
|
20
|
-
expect(list.body.tickets.length).toEqual(2)
|
21
|
-
})
|
22
|
-
})
|
package/src/routes/find.ts
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
import express, { Request, Response } from 'express'
|
2
|
-
import Ticket from '../models/ticket'
|
3
|
-
|
4
|
-
const router = express.Router()
|
5
|
-
|
6
|
-
router.get('/api/tickets', async (req: Request, res: Response) => {
|
7
|
-
const tickets = await Ticket.find({})
|
8
|
-
res.send({ tickets })
|
9
|
-
})
|
10
|
-
|
11
|
-
export default router
|