Skip to content

Commit 67af8e7

Browse files
committed
Adding controller for Rental from scratch.
1 parent 81c27c5 commit 67af8e7

File tree

7 files changed

+315
-6
lines changed

7 files changed

+315
-6
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import Router from 'express-promise-router';
2+
import KSUID from 'ksuid';
3+
import { Request, Response } from 'express';
4+
import { DI } from '../server';
5+
import { Rental } from '../model/Rental';
6+
import { RentalMapping } from '../mapping/RentalMapping';
7+
8+
const router = Router();
9+
10+
// Create.
11+
12+
router.post('/for-user/:id/:bookId', async (req: Request<{ id: string, bookId: string }>, res: Response) => {
13+
const id = req.params.id;
14+
const bookId = req.params.bookId;
15+
const payload = req.body;
16+
17+
RentalMapping.validateIdentifiers(id, bookId);
18+
const mapping = RentalMapping.validateAndConstructFromPayload(payload);
19+
20+
const entity = Rental.fromMapping(KSUID.parse(id), KSUID.parse(bookId), mapping);
21+
22+
await DI.database.rentals.put(entity);
23+
24+
res.json(entity.toMapping());
25+
});
26+
27+
// Read (All).
28+
29+
router.get('/', async (req: Request, res: Response) => {
30+
// TODO: Pagination.
31+
const collection = await DI.database.rentals.query()
32+
.partitionKey('type')
33+
.eq(Rental.name)
34+
.run();
35+
36+
res.json(collection.items.map((entity: Rental) => entity.toMapping()));
37+
});
38+
39+
// Read (One).
40+
41+
router.get('/for-user/:id/:bookId', async (req: Request<{ id: string, bookId: string }>, res: Response) => {
42+
const id = req.params.id;
43+
const bookId = req.params.bookId;
44+
RentalMapping.validateIdentifiers(id, bookId);
45+
46+
const entity = await DI.database.rentals.get(Rental.getPrimaryKey(id, bookId));
47+
48+
res.json(entity.toMapping());
49+
});
50+
51+
// Update.
52+
53+
router.put('/for-user/:id/:bookId', async (req: Request<{ id: string, bookId: string }>, res: Response) => {
54+
const userId = req.params.id;
55+
const bookId = req.params.bookId;
56+
const payload = req.body;
57+
const mapping = RentalMapping.validateAndConstructFromPayload({ userId, bookId, ...payload });
58+
59+
const entity = Rental.fromCompleteMapping(mapping);
60+
const primaryKey = Rental.getPrimaryKey(mapping.userId!, mapping.bookId!);
61+
const updatedEntity = await DI.database.rentals.update(primaryKey, entity.toUpdateStructure());
62+
63+
res.json(updatedEntity.toMapping());
64+
});
65+
66+
// Delete.
67+
68+
router.delete('/for-user/:id/:bookId', async (req: Request<{ id: string, bookId: string }>, res: Response) => {
69+
const id = req.params.id;
70+
const bookId = req.params.bookId;
71+
RentalMapping.validateIdentifiers(id, bookId);
72+
73+
await DI.database.rentals.delete(Rental.getPrimaryKey(id, bookId));
74+
75+
res.json(RentalMapping.emptyMapping(id, bookId));
76+
});
77+
78+
export const RentalController = router;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { BaseMapping } from './base/BaseMapping';
2+
import { MappingValidationError } from './exceptions/MappingValidationError';
3+
import { Rental } from '../model/Rental';
4+
5+
type RentalMappingAllowedFields = {
6+
userId?: string;
7+
bookId?: string;
8+
name: string;
9+
email: string;
10+
status?: string;
11+
comment?: string;
12+
}
13+
14+
export class RentalMapping extends BaseMapping {
15+
public readonly userId?: string;
16+
public readonly bookId?: string;
17+
public readonly name: string;
18+
public readonly email: string;
19+
public readonly status?: string;
20+
public readonly comment?: string;
21+
22+
protected constructor(payload: RentalMappingAllowedFields) {
23+
super();
24+
25+
this.userId = payload.userId;
26+
this.bookId = payload.bookId;
27+
this.name = payload.name;
28+
this.email = payload.email;
29+
this.status = payload.status;
30+
this.comment = payload.comment;
31+
}
32+
33+
static fromEntity(entity: Rental): RentalMapping {
34+
return new RentalMapping({
35+
userId: entity.resourceId,
36+
bookId: entity.subResourceId,
37+
name: entity.name,
38+
email: entity.email,
39+
status: entity.status,
40+
comment: entity.comment
41+
});
42+
}
43+
44+
static fromPayload(payload: RentalMappingAllowedFields) {
45+
return new RentalMapping(payload);
46+
}
47+
48+
static validateAndConstructFromPayload(payload: RentalMappingAllowedFields) {
49+
RentalMapping.validatePayload(payload);
50+
return RentalMapping.fromPayload(payload);
51+
}
52+
53+
static validatePayload(payload: RentalMappingAllowedFields) {
54+
BaseMapping.validateIdentifiers(payload.userId, payload.bookId);
55+
56+
if (!payload.name) {
57+
throw new MappingValidationError('For Rental, name is a required field that is a non-empty string');
58+
}
59+
60+
if (!payload.email) {
61+
throw new MappingValidationError('For Rental, email is a required field that is a non-empty string');
62+
}
63+
}
64+
65+
static emptyMapping(userId: string, bookId: string) {
66+
return { userId, bookId };
67+
}
68+
}

examples/01-from-crud-to-cqrs/step-00-crud/src/mapping/base/BaseMapping.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { MappingValidationError } from '../exceptions/MappingValidationError';
44
export abstract class BaseMapping {
55
public id?: string;
66

7-
static emptyMapping(identifier: string) {
8-
return { id: identifier };
7+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8+
static emptyMapping(primaryIdentifier: string, secondaryIdentifier?: string): any {
9+
return { id: primaryIdentifier };
910
}
1011

1112
static validateIdentifiers(primaryIdentifier?: string, secondaryIdentifier?: string) {

examples/01-from-crud-to-cqrs/step-00-crud/src/model/Rental.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import attribute from 'dynamode/decorators';
2+
import KSUID from "ksuid";
23
import { LibraryTable, LibraryTablePrimaryKey, LibraryTableProps } from './base/LibraryTable';
34
import { User } from "./User";
5+
import { RentalMapping } from "../mapping/RentalMapping";
6+
import { IMappable } from "../mapping/interfaces/IMappable";
7+
import { IUpdateable } from "../mapping/interfaces/IUpdateable";
48

5-
type RentalProps = LibraryTableProps & {
9+
type RentalFields = {
610
name: string;
711
email: string;
812
status: RentalStatus;
913
comment: string;
1014
};
1115

12-
export class Rental extends LibraryTable {
16+
type RentalProps = LibraryTableProps & RentalFields;
17+
18+
export class Rental extends LibraryTable implements IMappable, IUpdateable {
1319
@attribute.partitionKey.string({ prefix: User.name }) // `User#${userId}`
1420
resourceId!: string;
1521

@@ -37,6 +43,35 @@ export class Rental extends LibraryTable {
3743
this.comment = props.comment;
3844
}
3945

46+
toMapping(): RentalMapping {
47+
return RentalMapping.fromEntity(this);
48+
}
49+
50+
toUpdateStructure(): { set: RentalFields } {
51+
return {
52+
set: {
53+
name: this.name,
54+
email: this.email,
55+
status: this.status,
56+
comment: this.comment
57+
}
58+
};
59+
}
60+
61+
static fromMapping(userId: KSUID, bookId: KSUID, mapping: RentalMapping): Rental {
62+
return new Rental({
63+
...Rental.getPrimaryKey(userId.string, bookId.string),
64+
name: mapping.name,
65+
email: mapping.email,
66+
status: RentalStatus[(mapping.status ?? RentalStatus.BORROWED) as keyof typeof RentalStatus],
67+
comment: mapping.comment ?? ""
68+
})
69+
}
70+
71+
static fromCompleteMapping(mapping: RentalMapping): Rental {
72+
return Rental.fromMapping(KSUID.parse(mapping.userId!), KSUID.parse(mapping.bookId!), mapping);
73+
}
74+
4075
static getPrimaryKey(userId: string, bookId: string): LibraryTablePrimaryKey {
4176
return {
4277
resourceId: userId,

examples/01-from-crud-to-cqrs/step-00-crud/src/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import logger from 'morgan';
77
import { AuthorController } from './controllers/author';
88
import { BookController } from './controllers/book';
99
import { UserController } from './controllers/user';
10+
import { RentalController } from './controllers/rental';
1011
import { DatabaseProvider } from "./providers/DatabaseProvider";
1112

1213
import * as settings from '../package.json';
@@ -34,6 +35,7 @@ export const init = (async () => {
3435

3536
app.use('/author', AuthorController);
3637
app.use('/book', BookController);
38+
app.use('/rental', RentalController);
3739
app.use('/user', UserController);
3840

3941
app.use((req, res) => {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { RentalStatus } from "../../src/model/Rental";
2+
import { getAgent, getFakeBookId, getFakeUserWithId, start, stop } from '../helpers/common';
3+
4+
describe('CRUD Controller: /rental', () => {
5+
beforeAll(start);
6+
afterAll(stop);
7+
8+
const agent = getAgent();
9+
10+
const user = getFakeUserWithId();
11+
const bookId = getFakeBookId();
12+
13+
it('Verifying validation during creation', async () => {
14+
await agent
15+
.post('/rental/for-user/' + user.id + '/' + bookId)
16+
.set('Content-Type', 'application/json')
17+
.send({})
18+
.then(res => {
19+
expect(res.status).toBe(400);
20+
});
21+
});
22+
23+
it('Create', async () => {
24+
await agent
25+
.post('/rental/for-user/' + user.id + '/' + bookId)
26+
.set('Content-Type', 'application/json')
27+
.send(user)
28+
.then(res => {
29+
expect(res.status).toBe(200);
30+
expect(res.body.userId).toBe(user.id);
31+
expect(res.body.bookId).toBe(bookId);
32+
expect(res.body.name).toBe(user.name);
33+
expect(res.body.email).toBe(user.email);
34+
expect(res.body.status).toBe(RentalStatus.BORROWED);
35+
expect(res.body.comment).toBe("");
36+
});
37+
});
38+
39+
it('Read all', async () => {
40+
await agent
41+
.get('/rental')
42+
.then(res => {
43+
expect(res.status).toBe(200);
44+
expect(res.body).toHaveLength(1);
45+
expect(res.body[0].userId).toBe(user.id);
46+
expect(res.body[0].bookId).toBe(bookId);
47+
expect(res.body[0].name).toBe(user.name);
48+
expect(res.body[0].email).toBe(user.email);
49+
expect(res.body[0].status).toBe(RentalStatus.BORROWED);
50+
expect(res.body[0].comment).toBe("");
51+
});
52+
});
53+
54+
it('Read', async () => {
55+
await agent
56+
.get('/rental/for-user/' + user.id + '/' + bookId)
57+
.then(res => {
58+
expect(res.status).toBe(200);
59+
expect(res.body.userId).toBe(user.id);
60+
expect(res.body.bookId).toBe(bookId);
61+
expect(res.body.name).toBe(user.name);
62+
expect(res.body.email).toBe(user.email);
63+
expect(res.body.status).toBe(RentalStatus.BORROWED);
64+
expect(res.body.comment).toBe("");
65+
});
66+
});
67+
68+
it('Update', async () => {
69+
const rentalUpdate = {
70+
name: 'Jane Doe',
71+
email: 'jane@example.com',
72+
status: 'RETURNED',
73+
comment: 'This is a comment'
74+
};
75+
76+
await agent
77+
.put('/rental/for-user/' + user.id + '/' + bookId)
78+
.send(rentalUpdate)
79+
.then(res => {
80+
expect(res.status).toBe(200);
81+
expect(res.body.userId).toBe(user.id);
82+
expect(res.body.bookId).toBe(bookId);
83+
expect(res.body.name).toBe(rentalUpdate.name);
84+
expect(res.body.email).toBe(rentalUpdate.email);
85+
expect(res.body.status).toBe(RentalStatus.RETURNED);
86+
expect(res.body.comment).toBe(rentalUpdate.comment);
87+
});
88+
});
89+
90+
it('Delete', async () => {
91+
await agent
92+
.delete('/rental/for-user/' + user.id + '/' + bookId)
93+
.then(res => {
94+
expect(res.status).toBe(200);
95+
expect(res.body.userId).toBe(user.id);
96+
expect(res.body.bookId).toBe(bookId);
97+
});
98+
});
99+
100+
it('Read all, but this time empty collection', async () => {
101+
await agent
102+
.get('/rental')
103+
.then(res => {
104+
expect(res.status).toBe(200);
105+
expect(res.body).toHaveLength(0);
106+
});
107+
});
108+
});

examples/01-from-crud-to-cqrs/step-00-crud/test/helpers/common.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ export const stop = async () => {
1313
DI.server.close();
1414
};
1515

16+
const getKSUID = () => {
17+
return KSUID.randomSync().string;
18+
}
19+
20+
export const getFakeAuthorId = () => {
21+
return getKSUID();
22+
}
23+
24+
export const getFakeBookId = () => {
25+
return getKSUID();
26+
}
27+
1628
export const getAgent = () => request(app);
1729

1830
export const getFakeAuthor = () => {
@@ -29,8 +41,13 @@ export const getFakeUser = () => {
2941
}
3042
};
3143

32-
export const getFakeAuthorId = () => {
33-
return KSUID.randomSync().string;
44+
export const getFakeUserWithId = () => {
45+
const fakeUser = getFakeUser();
46+
47+
return {
48+
id: getKSUID(),
49+
...fakeUser
50+
}
3451
}
3552

3653
export const getFakeBook = () => {

0 commit comments

Comments
 (0)