diff --git a/__tests__/checkExpiry.test.js b/__tests__/checkExpiry.test.js index 1913fd90..9f031448 100644 --- a/__tests__/checkExpiry.test.js +++ b/__tests__/checkExpiry.test.js @@ -7,11 +7,10 @@ const checkExpiry = require('../lambda/checkExpiry/index'); const { REGION, ENDPOINT, TABLE_NAME } = require('./global/settings'); -let dynamoDb; let docClient; async function setupDb() { - dynamoDb = new AWS.DynamoDB({ + new AWS.DynamoDB({ region: REGION, endpoint: ENDPOINT }); @@ -60,43 +59,8 @@ describe('checkExpiryHandler', () => { return setupDb(); }); - test('should not update old passes', async () => { - const oldDate = new Date(); - oldDate.setDate(oldDate.getDate() - 5); - - await docClient - .put({ - TableName: TABLE_NAME, - Item: { - pk: 'pass::Test Park', - sk: '123456710', - facilityName: 'Parking Lot A', - type: 'DAY', - registrationNumber: '123456710', - passStatus: 'active', - date: formatISO(oldDate, { representation: 'date' }) - } - }) - .promise(); - - MockDate.set(new Date('2021-12-08T11:01:30.135Z')); - await checkExpiry.handler(null, {}); - MockDate.reset(); - - const result = await docClient - .get({ - TableName: TABLE_NAME, - Key: { - pk: 'pass::Test Park', - sk: '123456710' - } - }) - .promise(); - expect(result.Item.passStatus).toBe('active'); - }); - - test.each([['PM', '123456711'], ['DAY', '123456712']])('should set %s passes from yesterday to expired', async (passType, sk) => { - const passDate = new Date('2021-12-07T11:07:58.135Z'); + test.each([['AM', '123456710'], ['PM', '123456711'], ['DAY', '123456712']])('should set %s passes from yesterday to expired', async (passType, sk) => { + const passDate = new Date('2021-12-08T20:00:00.000Z'); await docClient .put({ TableName: TABLE_NAME, @@ -107,12 +71,12 @@ describe('checkExpiryHandler', () => { type: passType, registrationNumber: sk, passStatus: 'active', - date: formatISO(passDate, { representation: 'date' }) + date: formatISO(passDate) } }) .promise(); - MockDate.set(new Date('2021-12-08T08:01:58.135Z')); + MockDate.set(new Date('2021-12-19T00:00:00.000Z')); await checkExpiry.handler(null, {}); MockDate.reset(); @@ -129,7 +93,7 @@ describe('checkExpiryHandler', () => { }); test.each([['PM', '123456713'], ['DAY', '123456714']])('should not set %s passes from today to expired', async (passType, sk) => { - const passDate = new Date('2021-12-08T11:02:43.135Z'); + const passDate = new Date('2021-12-08T20:00:00.000Z'); await docClient .put({ TableName: TABLE_NAME, @@ -140,12 +104,12 @@ describe('checkExpiryHandler', () => { type: passType, registrationNumber: sk, passStatus: 'active', - date: formatISO(passDate, { representation: 'date' }) + date: formatISO(passDate) } }) .promise(); - MockDate.set(new Date('2021-12-08T19:01:58.135Z')); + MockDate.set(new Date('2021-12-08T21:00:00.000Z')); await checkExpiry.handler(null, {}); MockDate.reset(); @@ -162,7 +126,7 @@ describe('checkExpiryHandler', () => { }); test('should set AM passes to expired after 12:00', async () => { - const passDate = new Date('2021-12-08T11:01:02.135Z'); + const passDate = new Date('2021-12-08T20:00:00.000Z'); await docClient .put({ TableName: TABLE_NAME, @@ -173,12 +137,12 @@ describe('checkExpiryHandler', () => { type: 'AM', registrationNumber: '123456715', passStatus: 'active', - date: formatISO(passDate, { representation: 'date' }) + date: formatISO(passDate) } }) .promise(); - MockDate.set(new Date('2021-12-08T20:00:00.001Z')); + MockDate.set(new Date('2021-12-08T22:00:00.000Z')); await checkExpiry.handler(null, {}); MockDate.reset(); @@ -195,7 +159,7 @@ describe('checkExpiryHandler', () => { }); test('should set not AM passes to expired before 12:00', async () => { - const passDate = new Date('2021-12-08T11:01:58.135Z'); + const passDate = new Date('2021-12-08T20:00:00.000Z'); await docClient .put({ TableName: TABLE_NAME, @@ -206,12 +170,12 @@ describe('checkExpiryHandler', () => { type: 'AM', registrationNumber: '123456716', passStatus: 'active', - date: formatISO(passDate, { representation: 'date' }) + date: formatISO(passDate) } }) .promise(); - MockDate.set(new Date('2021-12-08T19:59:59.999Z')); + MockDate.set(new Date('2021-12-08T16:00:00.000Z')); await checkExpiry.handler(null, {}); MockDate.reset(); diff --git a/__tests__/global/setup.js b/__tests__/global/setup.js index 9323a35e..bd03b165 100644 --- a/__tests__/global/setup.js +++ b/__tests__/global/setup.js @@ -9,7 +9,7 @@ module.exports = async () => { }); try { - await dynamoDb + let res = await dynamoDb .createTable({ TableName: TABLE_NAME, KeySchema: [ @@ -30,12 +30,85 @@ module.exports = async () => { { AttributeName: 'sk', AttributeType: 'S' + }, + { + AttributeName: 'shortPassDate', + AttributeType: 'S' + }, + { + AttributeName: 'facilityName', + AttributeType: 'S' + }, + { + AttributeName: 'passStatus', + AttributeType: 'S' } ], ProvisionedThroughput: { ReadCapacityUnits: 1, WriteCapacityUnits: 1 - } + }, + GlobalSecondaryIndexes: [ + { + IndexName: 'passStatus-index', + KeySchema: [ + { + AttributeName: 'passStatus', + KeyType: 'HASH' + } + ], + Projection: { + ProjectionType: 'INCLUDE', + NonKeyAttributes: [ + 'type', + 'date', + 'pk', + 'sk' + ] + }, + ProvisionedThroughput: { + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + } + }, + { + IndexName: 'shortPassDate-index', + KeySchema: [ + { + AttributeName: 'shortPassDate', + KeyType: 'HASH' + }, + { + AttributeName: 'facilityName', + KeyType: 'RANGE' + } + ], + Projection: { + ProjectionType: 'INCLUDE', + NonKeyAttributes: [ + 'firstName', + 'searchFirstName', + 'lastName', + 'searchLastName', + 'facilityName', + 'email', + 'date', + 'shortPassDate', + 'type', + 'registrationNumber', + 'numberOfGuests', + 'passStatus', + 'phoneNumber', + 'facilityType', + 'license' + ] + }, + ProvisionedThroughput: { + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 + } + } + ] }) .promise(); } catch (err) { diff --git a/lambda/checkExpiry/index.js b/lambda/checkExpiry/index.js index 641e4af2..ffddbe76 100644 --- a/lambda/checkExpiry/index.js +++ b/lambda/checkExpiry/index.js @@ -1,10 +1,9 @@ -const { formatISO, subDays, getHours } = require('date-fns'); +const { compareAsc, addHours, endOfYesterday, startOfDay } = require('date-fns'); const { utcToZonedTime } = require('date-fns-tz'); - -const { runQuery, setStatus, getParks, TABLE_NAME } = require('../dynamoUtil'); +const { runQuery, setStatus, TABLE_NAME } = require('../dynamoUtil'); const { sendResponse } = require('../responseUtil'); -const TIMEZONE = 'America/Vancouver'; +const timeZone = 'America/Vancouver'; const ACTIVE_STATUS = 'active'; const EXPIRED_STATUS = 'expired'; const PASS_TYPE_EXPIRY_HOURS = { @@ -14,30 +13,36 @@ const PASS_TYPE_EXPIRY_HOURS = { }; exports.handler = async (event, context) => { - console.log('Event', event, context); + console.log('Check Expiry', event, context); try { - const utcNow = Date.now(); - const localNow = utcToZonedTime(utcNow, TIMEZONE); - const localHour = getHours(localNow); - const yesterday = subDays(new Date(localNow), 1); - console.log(`UTC: ${utcNow}; local (${TIMEZONE}): ${localNow}; yesterday: ${yesterday}`); + const endOfYesterdayTime = endOfYesterday(); + const currentTime = utcToZonedTime(new Date(), timeZone); + + let passesToChange = []; + const passes = await getActivePasses(); + console.log("Active Passes:", passes); - for (const passType in PASS_TYPE_EXPIRY_HOURS) { - const expiryHour = PASS_TYPE_EXPIRY_HOURS[passType]; - if (localHour < expiryHour) { - console.log(`${passType} passes don't expire yet`); - continue; + for(pass of passes) { + const zonedPassTime = utcToZonedTime(pass.date, timeZone); + // If it's zoned date is before the end of yesterday, it's definitely expired (AM/PM/DAY) + if (compareAsc(zonedPassTime, endOfYesterdayTime) <= 0) { + console.log("Expiring:", pass); + passesToChange.push(pass); } - // If expiring at midnight, check yesterday's passes. - const expiryDate = expiryHour === 0 ? yesterday : localNow; - const parks = await getParks(); - for (const park of parks) { - const passes = await getExpiredPasses(passType, expiryDate, park.sk); - await setStatus(passes, EXPIRED_STATUS); + // If AM, see if we're currently in the afternoon or later compared to the pass date's noon time. + const noonTime = addHours(startOfDay(zonedPassTime), PASS_TYPE_EXPIRY_HOURS.AM); + if (pass.type === 'AM' && compareAsc(currentTime, noonTime) > 0) { + console.log("Expiring:", pass); + passesToChange.push(pass); } } + // Set passes => expired + if (passesToChange.length !== 0) { + await setStatus(passesToChange, EXPIRED_STATUS); + } + return sendResponse(200, {}, context); } catch (err) { console.error(err); @@ -46,26 +51,26 @@ exports.handler = async (event, context) => { } }; -async function getExpiredPasses(passType, passDate, parkSk) { - const dateSelector = formatISO(passDate, { representation: 'date' }); - - console.log(`Loading ${passType} passes on ${dateSelector} for ${parkSk}`); +async function getActivePasses() { + console.log(`Loading passes`); const passesQuery = { TableName: TABLE_NAME, - KeyConditionExpression: 'pk = :pk', - ExpressionAttributeNames: { - '#dateselector': 'date', - '#passType': 'type' - }, + KeyConditionExpression: 'passStatus = :activeStatus', + IndexName: 'passStatus-index', ExpressionAttributeValues: { - ':pk': { S: `pass::${parkSk}` }, - ':activeDate': { S: dateSelector }, - ':activeStatus': { S: ACTIVE_STATUS }, - ':passType': { S: passType } - }, - FilterExpression: 'begins_with(#dateselector, :activeDate) AND #passType = :passType AND passStatus = :activeStatus' + ':activeStatus': { S: ACTIVE_STATUS } + } }; - return await runQuery(passesQuery); + // Grab all the results, don't skip any. + let results = []; + let passData; + do { + passData = await runQuery(passesQuery, true); + passData.data.forEach((item) => results.push(item)); + passesQuery.ExclusiveStartKey = passData.LastEvaluatedKey; + } while(typeof passData.LastEvaluatedKey !== "undefined"); + + return results; } diff --git a/package.json b/package.json index 4d10ccdb..7c865c3b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "dependencies": { "axios": "^0.24.0", "csvjson": "^5.1.0", - "date-fns": "^2.27.0", + "date-fns": "^2.28.0", "date-fns-tz": "^1.1.6", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^2.0.5", diff --git a/serverless.yml b/serverless.yml index 8c4e2f4e..f786f4fe 100644 --- a/serverless.yml +++ b/serverless.yml @@ -198,6 +198,18 @@ resources: KeyType: RANGE BillingMode: PAY_PER_REQUEST GlobalSecondaryIndexes: + - IndexName: passStatus-index + KeySchema: + - AttributeName: passStatus + KeyType: HASH + Projection: + ProjectionType: INCLUDE + NonKeyAttributes: + - type + - date + - pk + - sk + BillingMode: PAY_PER_REQUEST - IndexName: shortPassDate-index KeySchema: - AttributeName: shortPassDate diff --git a/terraform/src/db.tf b/terraform/src/db.tf index 9cb01829..19b6095c 100644 --- a/terraform/src/db.tf +++ b/terraform/src/db.tf @@ -23,11 +23,16 @@ resource "aws_dynamodb_table" "park_dup_table" { type = "S" } - attribute { + attribute { name = "shortPassDate" type = "S" } + attribute { + name = "passStatus" + type = "S" + } + attribute { name = "facilityName" type = "S" @@ -56,6 +61,20 @@ resource "aws_dynamodb_table" "park_dup_table" { "license" ] } + + global_secondary_index { + name = "passStatus-index" + hash_key = "passStatus" + write_capacity = 1 + read_capacity = 1 + projection_type = "INCLUDE" + non_key_attributes = [ + "type", + "date", + "pk", + "sk" + ] + } } resource "aws_backup_vault" "parksreso_backup_vault" { diff --git a/yarn.lock b/yarn.lock index 4cc8dc2a..aae1a4a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -917,13 +917,13 @@ "version" "0.2.3" "@digitalspace/dynamodb-migrate@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@digitalspace/dynamodb-migrate/-/dynamodb-migrate-1.0.2.tgz#afc45bd2ed81f088d4373fce06226dc4dec03ff6" - integrity sha512-ZlhY9vy9fq23pJpCDoStmrzNs28U53man4Zi6jaCX4uKjBQMdUbVf7TpGMQsZ2VH/aNp1eTdOJD9ueYn+0Qs6A== + "integrity" "sha512-ZlhY9vy9fq23pJpCDoStmrzNs28U53man4Zi6jaCX4uKjBQMdUbVf7TpGMQsZ2VH/aNp1eTdOJD9ueYn+0Qs6A==" + "resolved" "https://registry.npmjs.org/@digitalspace/dynamodb-migrate/-/dynamodb-migrate-1.0.2.tgz" + "version" "1.0.2" dependencies: - aws-sdk "^2.1059.0" - commander "^8.3.0" - luxon "^2.3.0" + "aws-sdk" "^2.1059.0" + "commander" "^8.3.0" + "luxon" "^2.3.0" "@hapi/accept@^5.0.1": "integrity" "sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw==" @@ -2343,15 +2343,15 @@ "sinon" "^11.1.1" "traverse" "^0.6.6" -"aws-sdk@^2.1030.0", "aws-sdk@^2.1032.0", "aws-sdk@^2.7.0", "aws-sdk@^2.928.0": - "integrity" "sha512-BjSGGZIQE/SCLDgj2T4AhtBG4A4NgXhV/Z/I/E7Mst/RpOepTqZGznUbgXTvO+Z3gKqx33jJa6mS7ZxStCb/Wg==" - "resolved" "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1035.0.tgz" - "version" "2.1035.0" +"aws-sdk@^2.1030.0", "aws-sdk@^2.1032.0", "aws-sdk@^2.1059.0", "aws-sdk@^2.7.0", "aws-sdk@^2.928.0": + "integrity" "sha512-Fjp5GOzctLHly5ySBGzASZVWEQi3zHc2TlYkiT5VNwvDiV9Uwv2frm2zgQf0wL6BOkPRS2b1TfOJT7x6Q5aOIw==" + "resolved" "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1071.0.tgz" + "version" "2.1071.0" dependencies: "buffer" "4.9.2" "events" "1.1.1" "ieee754" "1.1.13" - "jmespath" "0.15.0" + "jmespath" "0.16.0" "querystring" "0.2.0" "sax" "1.2.1" "url" "0.10.3" @@ -3072,6 +3072,11 @@ "resolved" "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" "version" "2.20.3" +"commander@^8.3.0": + "integrity" "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" + "resolved" "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" + "version" "8.3.0" + "commander@~4.1.1": "integrity" "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" "resolved" "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz" @@ -3258,7 +3263,7 @@ "resolved" "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.2.2.tgz" "version" "1.2.2" -"date-fns@^2.27.0", "date-fns@>=2.0.0": +"date-fns@^2.28.0", "date-fns@>=2.0.0": "integrity" "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==" "resolved" "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz" "version" "2.28.0" @@ -5555,10 +5560,10 @@ "import-local" "^3.0.2" "jest-cli" "^27.4.0" -"jmespath@0.15.0": - "integrity" "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" - "resolved" "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz" - "version" "0.15.0" +"jmespath@0.16.0": + "integrity" "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" + "resolved" "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz" + "version" "0.16.0" "jose@^2.0.5": "integrity" "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==" @@ -6114,6 +6119,11 @@ "resolved" "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz" "version" "1.28.0" +"luxon@^2.3.0": + "integrity" "sha512-gv6jZCV+gGIrVKhO90yrsn8qXPKD8HYZJtrUDSfEbow8Tkw84T9OnCyJhWvnJIaIF/tBuiAjZuQHUt1LddX2mg==" + "resolved" "https://registry.npmjs.org/luxon/-/luxon-2.3.0.tgz" + "version" "2.3.0" + "make-dir@^1.0.0": "integrity" "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==" "resolved" "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz"