diff --git a/migrations/20250804092620-add-urls-safebrowsing-expiry.js b/migrations/20250804092620-add-urls-safebrowsing-expiry.js new file mode 100644 index 000000000..b6a2ae42f --- /dev/null +++ b/migrations/20250804092620-add-urls-safebrowsing-expiry.js @@ -0,0 +1,26 @@ +/** @type {import('sequelize-cli').Migration} */ +module.exports = { + async up(queryInterface, Sequelize) { + /** + * Add altering commands here. + * + * Example: + * await queryInterface.createTable('users', { id: Sequelize.INTEGER });. + */ + await queryInterface.addColumn('urls', 'safeBrowsingExpiry', { + type: Sequelize.DATE, + allowNull: true, + defaultValue: null, + }) + }, + + async down(queryInterface) { + /** + * Add reverting commands here. + * + * Example: + * await queryInterface.dropTable('users');. + */ + await queryInterface.removeColumn('urls', 'safeBrowsingExpiry') + }, +} diff --git a/src/server/mappers/UrlMapper.ts b/src/server/mappers/UrlMapper.ts index 5a9e643cd..bc9bd1119 100644 --- a/src/server/mappers/UrlMapper.ts +++ b/src/server/mappers/UrlMapper.ts @@ -32,6 +32,7 @@ export class UrlMapper implements Mapper { contactEmail: urlType.contactEmail, source: urlType.source, tagStrings: urlType.tagStrings, + safeBrowsingExpiry: urlType.safeBrowsingExpiry, } } } diff --git a/src/server/models/index.ts b/src/server/models/index.ts index 88db93845..27539cb18 100644 --- a/src/server/models/index.ts +++ b/src/server/models/index.ts @@ -8,6 +8,7 @@ import { UrlClicks } from './statistics/clicks' import { syncFunctions } from './functions' import { Tag } from './tag' import { Job, JobItem } from './job' +import { DEV_ENV } from '../config' // One user can create many urls but each url can only be mapped to one user. User.hasMany(Url, { as: 'Urls', foreignKey: { allowNull: false } }) @@ -45,6 +46,8 @@ UrlClicks.belongsTo(Url, { foreignKey: 'shortUrl' }) * Initialise the database table. */ export default async () => { - await sequelize.sync() + if (DEV_ENV) { + await sequelize.sync() + } await syncFunctions() } diff --git a/src/server/models/url.ts b/src/server/models/url.ts index 7c34eb836..c02c1c988 100644 --- a/src/server/models/url.ts +++ b/src/server/models/url.ts @@ -24,6 +24,7 @@ export interface UrlBaseType extends IdType { readonly description: string readonly source: StorableUrlSource readonly tagStrings: string + readonly safeBrowsingExpiry: string | null } export interface UrlType extends IdType, UrlBaseType, Sequelize.Model { @@ -229,6 +230,10 @@ export const Url = sequelize.define( allowNull: false, defaultValue: '', }, + safeBrowsingExpiry: { + type: Sequelize.DATE, + allowNull: true, + }, }, { hooks: { diff --git a/src/server/repositories/types.ts b/src/server/repositories/types.ts index 797b531a6..29b51ffe8 100644 --- a/src/server/repositories/types.ts +++ b/src/server/repositories/types.ts @@ -16,6 +16,7 @@ export type StorableUrl = Pick< | 'contactEmail' | 'source' | 'tagStrings' + | 'safeBrowsingExpiry' > & Pick & { tags?: string[] } diff --git a/test/integration/api/user/Urls.test.ts b/test/integration/api/user/Urls.test.ts index d7d1401b6..b19a14137 100644 --- a/test/integration/api/user/Urls.test.ts +++ b/test/integration/api/user/Urls.test.ts @@ -86,6 +86,7 @@ describe('Url integration tests', () => { tagStrings: '', createdAt: expect.stringMatching(DATETIME_REGEX), updatedAt: expect.stringMatching(DATETIME_REGEX), + safeBrowsingExpiry: null, }) }) @@ -108,6 +109,7 @@ describe('Url integration tests', () => { tagStrings: '', createdAt: expect.stringMatching(DATETIME_REGEX), updatedAt: expect.stringMatching(DATETIME_REGEX), + safeBrowsingExpiry: null, }) }) @@ -151,6 +153,7 @@ describe('Url integration tests', () => { tagStrings: '', createdAt: expect.stringMatching(DATETIME_REGEX), updatedAt: expect.stringMatching(DATETIME_REGEX), + safeBrowsingExpiry: null, }) // Should be able to get updated link URL @@ -172,6 +175,7 @@ describe('Url integration tests', () => { tagStrings: '', createdAt: expect.stringMatching(DATETIME_REGEX), updatedAt: expect.stringMatching(DATETIME_REGEX), + safeBrowsingExpiry: null, }, ], count: 1, @@ -203,6 +207,7 @@ describe('Url integration tests', () => { tagStrings: '', createdAt: expect.stringMatching(DATETIME_REGEX), updatedAt: expect.stringMatching(DATETIME_REGEX), + safeBrowsingExpiry: null, }) // Should be able to get updated file URL @@ -224,6 +229,7 @@ describe('Url integration tests', () => { tagStrings: '', createdAt: expect.stringMatching(DATETIME_REGEX), updatedAt: expect.stringMatching(DATETIME_REGEX), + safeBrowsingExpiry: null, }, ], count: 1, diff --git a/test/server/mappers/UrlV1Mapper.test.ts b/test/server/mappers/UrlV1Mapper.test.ts index 4fc88cc0b..14a7c9af5 100644 --- a/test/server/mappers/UrlV1Mapper.test.ts +++ b/test/server/mappers/UrlV1Mapper.test.ts @@ -21,6 +21,7 @@ describe('url v1 mapper', () => { tagStrings: '1;abc;foo-bar', contactEmail: 'bar@open.gov.sg', description: 'this-is-a-description', + safeBrowsingExpiry: null, } const urlV1DTO = urlV1Mapper.persistenceToDto(storableUrl) expect(urlV1DTO).toEqual({ diff --git a/test/server/mocks/repositories/UrlRepository.ts b/test/server/mocks/repositories/UrlRepository.ts index b488d2b7e..74d74c559 100644 --- a/test/server/mocks/repositories/UrlRepository.ts +++ b/test/server/mocks/repositories/UrlRepository.ts @@ -84,6 +84,7 @@ export class UrlRepositoryMock implements UrlRepositoryInterface { clicks: 0, source: StorableUrlSource.Console, tagStrings: '', + safeBrowsingExpiry: null, }, ], count: 0, diff --git a/test/server/repositories/UrlRepository.test.ts b/test/server/repositories/UrlRepository.test.ts index b38688f4f..c04b9d0da 100644 --- a/test/server/repositories/UrlRepository.test.ts +++ b/test/server/repositories/UrlRepository.test.ts @@ -24,6 +24,7 @@ import { import { DirectoryQueryConditions } from '../../../src/server/modules/directory' import TagRepositoryMock from '../mocks/repositories/TagRepository' import { TAG_SEPARATOR } from '../../../src/shared/constants' +import { StorableUrl } from '../../../src/server/repositories/types' jest.mock('../../../src/server/models/url', () => ({ Url: urlModelMock, @@ -75,17 +76,18 @@ describe('UrlRepository', () => { const baseUrlClicks = { clicks: 2, } - const baseTemplate = { + const baseTemplate: Omit = { shortUrl: baseShortUrl, longUrl: baseLongUrl, - state: 'ACTIVE', + state: StorableUrlState.Active, isFile: false, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), description: 'An agency of the Singapore Government', contactEmail: 'contact-us@agency.gov.sg', source: StorableUrlSource.Console, tags: [], + safeBrowsingExpiry: null, } const baseUrl = { ...baseTemplate, diff --git a/test/server/repositories/UserRepository.test.ts b/test/server/repositories/UserRepository.test.ts index 2af0e9876..146e3cae7 100644 --- a/test/server/repositories/UserRepository.test.ts +++ b/test/server/repositories/UserRepository.test.ts @@ -3,6 +3,11 @@ import { UserRepository } from '../../../src/server/repositories/UserRepository' import { UrlMapper } from '../../../src/server/mappers/UrlMapper' import { UserMapper } from '../../../src/server/mappers/UserMapper' import { NotFoundError } from '../../../src/server/util/error' +import { StorableUrl } from '../../../src/server/repositories/types' +import { + StorableUrlSource, + StorableUrlState, +} from '../../../src/server/repositories/enums' jest.mock('../../../src/server/models/user', () => ({ User: userModelMock, @@ -17,16 +22,17 @@ const userRepo = new UserRepository( new UrlMapper(), ) -const baseUrlTemplate = { +const baseUrlTemplate: Omit = { shortUrl: 'short-link', longUrl: 'https://www.agency.gov.sg', - state: 'ACTIVE', + state: StorableUrlState.Active, isFile: false, - createdAt: Date.now(), - updatedAt: Date.now(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), description: 'An agency of the Singapore Government', contactEmail: 'contact-us@agency.gov.sg', - source: 'CONSOLE', + source: StorableUrlSource.Console, + safeBrowsingExpiry: null, } const urlClicks = {