@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/coverage/clover.xml +180 -3
  3. package/coverage/coverage-final.json +13 -1
  4. package/coverage/lcov-report/events/listeners/_queue-group-name.ts.html +88 -0
  5. package/coverage/lcov-report/events/listeners/index.html +131 -0
  6. package/coverage/lcov-report/events/listeners/order-created.ts.html +157 -0
  7. package/coverage/lcov-report/events/publishers/index.html +116 -0
  8. package/coverage/lcov-report/events/publishers/updated.ts.html +106 -0
  9. package/coverage/lcov-report/index.html +101 -11
  10. package/coverage/lcov-report/models/index.html +116 -0
  11. package/coverage/lcov-report/models/ticket.ts.html +235 -0
  12. package/coverage/lcov-report/src/app.ts.html +17 -17
  13. package/coverage/lcov-report/src/events/listeners/_queue-group-name.ts.html +88 -0
  14. package/coverage/lcov-report/src/events/listeners/index.html +146 -0
  15. package/coverage/lcov-report/src/events/listeners/order-cancelled.ts.html +157 -0
  16. package/coverage/lcov-report/src/events/listeners/order-created.ts.html +157 -0
  17. package/coverage/lcov-report/src/events/publishers/created.ts.html +106 -0
  18. package/coverage/lcov-report/src/events/publishers/index.html +131 -0
  19. package/coverage/lcov-report/src/events/publishers/updated.ts.html +106 -0
  20. package/coverage/lcov-report/src/index.html +1 -1
  21. package/coverage/lcov-report/src/models/index.html +5 -5
  22. package/coverage/lcov-report/src/models/ticket.ts.html +35 -14
  23. package/coverage/lcov-report/src/routes/create.ts.html +18 -18
  24. package/coverage/lcov-report/src/routes/find.ts.html +1 -1
  25. package/coverage/lcov-report/src/routes/get.ts.html +25 -10
  26. package/coverage/lcov-report/src/routes/index.html +12 -27
  27. package/coverage/lcov-report/src/routes/index.ts.html +8 -11
  28. package/coverage/lcov-report/src/routes/update.ts.html +19 -19
  29. package/coverage/lcov-report/src/test/index.html +1 -1
  30. package/coverage/lcov-report/src/test/utils.ts.html +4 -4
  31. package/coverage/lcov-report/ticket.ts.html +220 -0
  32. package/coverage/lcov.info +282 -0
  33. package/package.json +4 -3
  34. package/pnpm-lock.yaml +334 -18
  35. package/src/events/listeners/__test__/order-cancelled.test.ts +50 -0
  36. package/src/events/listeners/__test__/order-created.test.ts +50 -0
  37. package/src/events/listeners/_queue-group-name.ts +1 -0
  38. package/src/events/listeners/order-cancelled.ts +24 -0
  39. package/src/events/listeners/order-created.ts +24 -0
  40. package/src/index.ts +5 -0
  41. package/src/models/__test__/ticket.test.ts +37 -0
  42. package/src/models/ticket.ts +10 -3
  43. package/src/routes/__test__/get.test.ts +15 -0
  44. package/src/routes/__test__/update.test.ts +15 -0
  45. package/src/routes/create.ts +2 -2
  46. package/src/routes/get.ts +5 -0
  47. package/src/routes/index.ts +1 -2
  48. package/src/routes/update.ts +3 -3
  49. package/src/routes/__test__/find.test.ts +0 -22
  50. 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
+ })
@@ -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: String;
5
- price: Number;
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
  })
@@ -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 { TicketCreatedPublisher } from '../events/publishers/tickets/created'
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)
@@ -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 [findTickets, getTicket, createTicket, updateTicket]
5
+ export default [getTicket, createTicket, updateTicket]
@@ -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/tickets/updated'
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
- })
@@ -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