Skip to content

Commit 528ce63

Browse files
Merge pull request #123 from o2Labs/ttl-fixes
Fix TTL including milliseconds
2 parents 147136f + 93804b1 commit 528ce63

File tree

12 files changed

+184
-30
lines changed

12 files changed

+184
-30
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.1.0]
9+
10+
- Fix TTL bug resulting in the data retention policy not being followed.
11+
- Add migration to fix TTL on existing database entries.
12+
- Add a basic stats page showing all booking counts per office, per day.
13+
- Improved usability and accessibility of login journey.
14+
- Increase test coverage of critical path to booking.
15+
816
## [2.0.2]
917

1018
- Add check for user before attempting to retrieve office details.

client/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "office-booker",
3-
"version": "0.1.0",
3+
"version": "2.1.0",
44
"private": true,
55
"scripts": {
66
"start": "react-scripts start",
@@ -22,6 +22,7 @@
2222
"@reach/router": "^1.3.3",
2323
"@types/loadable__component": "^5.13.1",
2424
"amazon-cognito-identity-js": "^4.2.2",
25+
"array-fns": "^1.0.0",
2526
"core-js": "^3.6.5",
2627
"date-fns": "^2.12.0",
2728
"lodash": "^4.17.19",
+35-11
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
11
import React from 'react';
2-
import { render, fireEvent, screen } from '@testing-library/react';
2+
import { render, fireEvent, screen, within } from '@testing-library/react';
33
import '@testing-library/jest-dom/extend-expect';
44

55
import { server } from '../../../test/server.mock';
6-
import { createFakeOfficeWithSlots, createFakeSystemAdminUser } from '../../../test/data';
6+
import {
7+
createFakeConfig,
8+
createFakeOfficeWithSlots,
9+
createFakeSystemAdminUser,
10+
} from '../../../test/data';
711
import { TestContext } from '../../../test/TestContext';
8-
import { mockGetBookings, mockGetOffice, mockGetOffices } from '../../../test/handlers';
12+
import {
13+
mockGetBookings,
14+
mockGetConfig,
15+
mockGetOffice,
16+
mockGetOffices,
17+
} from '../../../test/handlers';
918

1019
import Home from './Home';
1120

1221
test('Selecting an office', async () => {
13-
const office = createFakeOfficeWithSlots();
14-
const user = createFakeSystemAdminUser([office]);
15-
server.use(mockGetOffices([office]), mockGetBookings([]), mockGetOffice(office));
22+
const config = createFakeConfig();
23+
const office = createFakeOfficeWithSlots(config);
24+
const user = createFakeSystemAdminUser([office], { quota: 2 });
25+
server.use(
26+
mockGetConfig(config),
27+
mockGetOffices([office]),
28+
mockGetBookings([]),
29+
mockGetOffice(office)
30+
);
1631
render(
1732
<TestContext user={user}>
1833
<Home />
@@ -21,11 +36,20 @@ test('Selecting an office', async () => {
2136

2237
await screen.findByRole('heading', { level: 2, name: 'Select your office' });
2338

24-
fireEvent.click(screen.getByRole('button', { name: office.name }));
39+
const main = within(screen.getByRole('main'));
40+
41+
fireEvent.click(main.getByRole('button', { name: office.name }));
2542

26-
await screen.findByRole('heading', { level: 2, name: office.name });
27-
expect(screen.getByText(/You can make/i)).toHaveTextContent(`You can make 5 booking per week`);
28-
expect(screen.getByText(/daily capacity/i)).toHaveTextContent(
29-
`The Office has a daily capacity of 100 and car park capacity of 100.`
43+
await main.findByRole('heading', { level: 2, name: office.name });
44+
expect(main.getByText(/You can make/i)).toHaveTextContent(
45+
`You can make ${user.quota} booking per week`
46+
);
47+
expect(main.getByText(/daily capacity/i)).toHaveTextContent(
48+
`The Office has a daily capacity of ${office.quota} and car park capacity of ${office.parkingQuota}.`
3049
);
50+
expect(main.getByText(/bookings remaining/i)).toHaveTextContent(
51+
`${user.quota} bookings remaining`
52+
);
53+
54+
await main.findAllByRole('button', { name: 'Book' });
3155
});

client/test/data.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Config } from '../src/context/stores';
22
import { Booking, Office, OfficeWithSlots, User } from '../src/types/api';
3-
import { format } from 'date-fns';
3+
import { addDays, format } from 'date-fns';
4+
import { init } from 'array-fns';
45

56
export const createFakeConfig = (prototype?: Partial<Config>): Config => ({
67
advancedBookingDays: 14,
@@ -18,12 +19,19 @@ export const createFakeOffice = (prototype?: Partial<Office>): Office => ({
1819
});
1920

2021
export const createFakeOfficeWithSlots = (
22+
config: Config,
2123
prototype?: Partial<OfficeWithSlots>
22-
): OfficeWithSlots => ({
23-
...createFakeOffice(prototype),
24-
slots: [],
25-
...prototype,
26-
});
24+
): OfficeWithSlots => {
25+
return {
26+
...createFakeOffice(prototype),
27+
slots: init(config.advancedBookingDays, (i) => ({
28+
date: format(addDays(new Date(), i), 'yyyy-MM-dd'),
29+
booked: 0,
30+
bookedParking: 0,
31+
})),
32+
...prototype,
33+
};
34+
};
2735

2836
export const createFakeUser = (prototype?: Partial<User>): User => ({
2937

client/yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -3002,6 +3002,11 @@ array-flatten@^2.1.0:
30023002
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
30033003
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
30043004

3005+
array-fns@^1.0.0:
3006+
version "1.0.0"
3007+
resolved "https://registry.yarnpkg.com/array-fns/-/array-fns-1.0.0.tgz#bce7b5cc9b95c28187328a320555fd175cb545de"
3008+
integrity sha512-mPqxfuDBey5FYj+f3wy4Jlf1u74eNBOhBuJ26od/UovTLTwOhusNoNgByQN3JI7yEr//CD1XuDtWVQYYfpJRLw==
3009+
30053010
array-includes@^3.1.1:
30063011
version "3.1.1"
30073012
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348"

server/app.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const configureApp = (config: Config) => {
4747
app.get('/api/config', (req, res, next) => {
4848
try {
4949
const clientConfig = {
50-
version: '2.0.0',
50+
version: '2.1.0',
5151
showTestBanner: config.showTestBanner,
5252
auth:
5353
config.authConfig.type === 'cognito'

server/db/bookings.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Config } from '../app-config';
22
import DynamoDB from 'aws-sdk/clients/dynamodb';
33
import { attribute, table, hashKey, rangeKey } from '@aws/dynamodb-data-mapper-annotations';
44
import { DataMapper } from '@aws/dynamodb-data-mapper';
5-
import { format, subDays, addDays } from 'date-fns';
5+
import { format, subDays, addDays, getUnixTime } from 'date-fns';
66
import { AttributePath, FunctionExpression } from '@aws/dynamodb-expressions';
77

88
export interface CreateBookingModel {
@@ -100,7 +100,7 @@ export const createBooking = async (
100100
try {
101101
const created = await mapper.put(
102102
Object.assign(new BookingsModel(), booking, {
103-
ttl: addDays(new Date(booking.date), config.dataRetentionDays).getTime(),
103+
ttl: getUnixTime(addDays(new Date(booking.date), config.dataRetentionDays)),
104104
}),
105105
{
106106
condition: new FunctionExpression('attribute_not_exists', new AttributePath('id')),

server/db/officeBookings.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
UpdateExpression,
1313
FunctionExpression,
1414
} from '@aws/dynamodb-expressions';
15-
import { addDays } from 'date-fns';
15+
import { addDays, getUnixTime } from 'date-fns';
1616

1717
export interface OfficeBooking {
1818
officeId: string;
@@ -57,7 +57,7 @@ export const incrementOfficeBookingCount = async (
5757
date,
5858
bookingCount: 0,
5959
parkingCount: 0,
60-
ttl: addDays(new Date(date), config.dataRetentionDays).getTime(),
60+
ttl: getUnixTime(addDays(new Date(date), config.dataRetentionDays)),
6161
}),
6262
{
6363
condition: {

server/db/userBookings.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
FunctionExpression,
1111
} from '@aws/dynamodb-expressions';
1212
import { Arrays } from 'collection-fns';
13-
import { addDays } from 'date-fns';
13+
import { addDays, getUnixTime } from 'date-fns';
1414

1515
export interface UserBookings {
1616
email: string;
@@ -69,7 +69,7 @@ export const incrementUserBookingCount = async (
6969
email: userEmail,
7070
weekCommencing,
7171
bookingCount: 0,
72-
ttl: addDays(new Date(weekCommencing), config.dataRetentionDays + 7).getTime(),
72+
ttl: getUnixTime(addDays(new Date(weekCommencing), config.dataRetentionDays + 7)),
7373
}),
7474
{
7575
condition: {

server/migrations/4-fix-ttl.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { DataMapper } from '@aws/dynamodb-data-mapper';
2+
import { DynamoDB } from 'aws-sdk';
3+
import { getUnixTime } from 'date-fns';
4+
import { Config } from '../app-config';
5+
import { BookingsModel } from '../db/bookings';
6+
import { OfficeBookingModel } from '../db/officeBookings';
7+
import { UserBookingsModel } from '../db/userBookings';
8+
9+
// Check if TTL is beyond the year 3000
10+
const timestampTooHigh = (ttl: number) => ttl > 32503680000;
11+
12+
async function* getCorrectedTtls(mapper: DataMapper, model: { new (): any }) {
13+
for await (const booking of mapper.scan(model)) {
14+
if (timestampTooHigh(booking.ttl)) {
15+
const ttl = new Date(booking.ttl);
16+
booking.ttl = getUnixTime(ttl);
17+
yield booking;
18+
}
19+
}
20+
}
21+
22+
const scanAndFix = async (mapper: DataMapper, model: { new (): any }) => {
23+
let updated = 0;
24+
for await (const _item of mapper.batchPut(getCorrectedTtls(mapper, model))) {
25+
updated++;
26+
if (updated % 100 === 0) {
27+
console.log('Updated ', updated);
28+
}
29+
}
30+
console.log('Updated ', updated);
31+
};
32+
33+
export const fixTtl = async (config: Config) => {
34+
const mapper = new DataMapper({
35+
client: new DynamoDB(config.dynamoDB),
36+
tableNamePrefix: config.dynamoDBTablePrefix,
37+
});
38+
await scanAndFix(mapper, BookingsModel);
39+
await scanAndFix(mapper, OfficeBookingModel);
40+
await scanAndFix(mapper, UserBookingsModel);
41+
};

server/migrations/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { options } from 'yargs';
22
import { SSM } from 'aws-sdk';
33
import { parseConfigFromEnv, Config } from '../app-config';
4+
import { fixTtl } from './4-fix-ttl';
45

56
/** Collection all migrations that should be applied to the system */
67
type Migrations = {
@@ -30,6 +31,9 @@ const migrations: Migrations = {
3031
'3-move-bookings': {
3132
reasonToFailPreCheck: 'You must deploy version 1.2.0 first.',
3233
},
34+
'4-fix-ttl': {
35+
execute: fixTtl,
36+
},
3337
};
3438

3539
/**

server/tests/migrations.test.ts

+68-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,70 @@
1-
// const { app, resetDb, config } = configureServer('migrations', {
2-
// users: [{ Username: cognitoUsername, UserCreateDate: cognitoUserCreated }],
3-
// });
1+
import { configureServer } from './test-utils';
2+
import { DataMapper } from '@aws/dynamodb-data-mapper';
3+
import { DynamoDB } from 'aws-sdk';
4+
import { UserBookingsModel } from '../db/userBookings';
5+
import { fixTtl } from '../migrations/4-fix-ttl';
6+
import { BookingsModel } from '../db/bookings';
7+
import { OfficeBookingModel } from '../db/officeBookings';
48

5-
// beforeEach(resetDb);
9+
const { resetDb, config } = configureServer('migrations');
610

7-
test('no-active-migration', () => {});
11+
beforeEach(resetDb);
12+
13+
describe('4 - fix TTL', async () => {
14+
it(`fixes bookings ttl`, async () => {
15+
const expires = new Date('2020-03-01');
16+
const mapper = new DataMapper({
17+
client: new DynamoDB(config.dynamoDB),
18+
tableNamePrefix: config.dynamoDBTablePrefix,
19+
});
20+
21+
const userBookingKey = {
22+
23+
weekCommencing: '2020-01-01',
24+
};
25+
const officeBookingKey = {
26+
name: 'the-office',
27+
date: '2020-01-01',
28+
};
29+
const bookingKey = {
30+
id: 'booking-id',
31+
32+
};
33+
await mapper.put(
34+
Object.assign(new UserBookingsModel(), userBookingKey, {
35+
bookingCount: 1,
36+
ttl: expires.getTime(),
37+
})
38+
);
39+
await mapper.put(
40+
Object.assign(new OfficeBookingModel(), officeBookingKey, {
41+
bookingCount: 1,
42+
parkingCount: 0,
43+
ttl: expires.getTime(),
44+
})
45+
);
46+
await mapper.put(
47+
Object.assign(new BookingsModel(), bookingKey, {
48+
date: '2020-01-01',
49+
officeId: 'the-office',
50+
parking: false,
51+
ttl: expires.getTime(),
52+
})
53+
);
54+
55+
await fixTtl(config);
56+
57+
const updatedUserBooking = await mapper.get(
58+
Object.assign(new UserBookingsModel(), userBookingKey)
59+
);
60+
expect(updatedUserBooking.ttl).toBe(Math.floor(expires.getTime() / 1000));
61+
62+
const updatedOfficeBooking = await mapper.get(
63+
Object.assign(new OfficeBookingModel(), officeBookingKey)
64+
);
65+
expect(updatedOfficeBooking.ttl).toBe(Math.floor(expires.getTime() / 1000));
66+
67+
const updatedBooking = await mapper.get(Object.assign(new BookingsModel(), bookingKey));
68+
expect(updatedBooking.ttl).toBe(Math.floor(expires.getTime() / 1000));
69+
});
70+
});

0 commit comments

Comments
 (0)