diff --git a/src/routes/url.ts b/src/routes/url.ts index 9d02904..095b6ff 100644 --- a/src/routes/url.ts +++ b/src/routes/url.ts @@ -57,8 +57,8 @@ const retrieveUrl: Route<{ Params: { id: string } }> = { async handler(request) { if (request.validationError) throw new NotFoundError() - const withInfo = await this.auth.isAuthorized(request) - const storedUrl = await this.storage.url.get(request.params.id, { withInfo }) + const isAuthorized = await this.auth.isAuthorized(request) + const storedUrl = await this.storage.url.get(request.params.id, { withInfo: isAuthorized, includeDeleted: isAuthorized }) if (typeof storedUrl === 'undefined') throw new NotFoundError() try { diff --git a/src/services/auth/drivers/bearerToken/index.ts b/src/services/auth/drivers/bearerToken/index.ts index 7da9d0d..b1607cb 100644 --- a/src/services/auth/drivers/bearerToken/index.ts +++ b/src/services/auth/drivers/bearerToken/index.ts @@ -2,7 +2,6 @@ import { AuthDriver } from '../../types' import { BearerTokenDriverConfig } from './types' import { FastifyRequest } from 'fastify' import { UnauthorizedError } from '../../../../errors/unauthorized.js' -import { logger } from '../../../logger/logger.js' export class BearerTokenAuth implements AuthDriver { private token: string diff --git a/src/services/storage/drivers/inMemory/index.ts b/src/services/storage/drivers/inMemory/index.ts index ad95eaa..aa3c473 100644 --- a/src/services/storage/drivers/inMemory/index.ts +++ b/src/services/storage/drivers/inMemory/index.ts @@ -4,14 +4,24 @@ import { NotFoundError } from '../../../../errors/notFound.js' import { InMemoryStorageConfig } from '../../types/config.js' import type { StorageDriver } from '../../types/index.js' import type { StoredUrl, UrlWithInformation, UrlRequestData, UrlInformation } from '../../types/url.js' - export class InMemoryStorage implements StorageDriver { data: { urls: Map; urlInformation: Map } = { urls: new Map(), urlInformation: new Map(), } + private async softDelete(storedUrl: StoredUrl): Promise { + const { id, url, createdAt, updatedAt } = storedUrl; + const newStoredUrl: StoredUrl = { + id, + url, + createdAt, + updatedAt, + deletedAt: new Date().toISOString() + } + this.data.urls.set(id, newStoredUrl); + } url = new (class InMemoryUrlStorage { - constructor(public storage: InMemoryStorage) {} + constructor(public storage: InMemoryStorage) { } public uuid() { let id @@ -22,9 +32,9 @@ export class InMemoryStorage implements StorageDriver { return id } - public async get(id: string, options = { withInfo: false }): Promise { + public async get(id: string, options = { withInfo: false, includeDeleted: true }): Promise { const storedUrl = this.storage.data.urls.get(id) - if (typeof storedUrl === 'undefined') throw new NotFoundError() + if (typeof storedUrl === 'undefined' || storedUrl.deletedAt) throw new NotFoundError() if (!options.withInfo) { return storedUrl @@ -34,19 +44,27 @@ export class InMemoryStorage implements StorageDriver { return { ...storedUrl, ...urlInfo } } - public async delete(id: string): Promise { - if (typeof this.storage.data.urls.get(id) === 'undefined') throw new NotFoundError() - this.storage.data.urls.delete(id) + + public async delete(id: string, options: { softDelete: boolean }): Promise { + const storedUrl = this.storage.data.urls.get(id); + if (typeof storedUrl === 'undefined') throw new NotFoundError() + + if (!options.softDelete) { + this.storage.data.urls.delete(id); + } else { + this.storage.softDelete(storedUrl); + } } + public async deleteOverdue(timespanMs: number): Promise { const deleteBefore = new Date().getTime() - timespanMs let deletedCount = 0 this.storage.data.urls.forEach((storedUrl) => { - const updatedAt = new Date(storedUrl.updatedAt).getTime() - if (updatedAt <= deleteBefore) { - this.storage.data.urls.delete(storedUrl.id) + const updatedAtDate = new Date(storedUrl.updatedAt).getTime() + if (!storedUrl.deletedAt && updatedAtDate <= deleteBefore) { + this.storage.softDelete(storedUrl); deletedCount++ } }) @@ -114,4 +132,5 @@ export class InMemoryStorage implements StorageDriver { // eslint-disable-next-line constructor(public config: InMemoryStorageConfig) { } + } diff --git a/src/services/storage/drivers/relational/index.ts b/src/services/storage/drivers/relational/index.ts index f21017c..94d4a77 100644 --- a/src/services/storage/drivers/relational/index.ts +++ b/src/services/storage/drivers/relational/index.ts @@ -69,15 +69,15 @@ export class RelationalStorage implements StorageDriver { url = new (class RelationalUrlStorage { constructor(public storage: RelationalStorage) { } - public async get(id: string, options = { withInfo: false }): Promise { + public async get(id: string, options = { withInfo: false, includeDeleted: true }): Promise { let storedUrl, urlInfo + storedUrl = await this.storage.db + .table('urls') + .select('*') + .where('id', id) + .first() + if (storedUrl?.deletedAt) throw new NotFoundError(); if (!options.withInfo) { - storedUrl = await this.storage.db - .table('urls') - .select('*') - .where('id', id) - .first() - delete storedUrl?.serial } else { urlInfo = await this.storage.db @@ -93,14 +93,19 @@ export class RelationalStorage implements StorageDriver { //All Info associated with that Urls also get deleted, automatically. //https://stackoverflow.com/questions/53859207/deleting-data-from-associated-tables-using-knex-js - public async delete(id: string): Promise { - await this.storage.db.table('urls').where('id', id).delete() - return + public async delete(id: string, options: { softDelete: true }): Promise { + if (!await this.storage.db.table('urls').where('id', id)) throw new NotFoundError(); + + if (options.softDelete) { + await this.storage.db.table('urls').where('id', id).delete() + } else { + await this.storage.db.table('urls').update({ deletedAt: new Date().toISOString() }).where('id', id) + } } public async deleteOverdue(timespanMs: number): Promise { const deleteBefore = new Date(new Date().getTime() - timespanMs) - return await this.storage.db.table('urls').where('updatedAt', '<', deleteBefore).delete() + return await this.storage.db.table('urls').update({ deletedAt: new Date().toISOString() }).where('updatedAt', '<', deleteBefore); } public async edit(id: string, url: string): Promise { diff --git a/src/services/storage/drivers/relational/migrations/20210401143602_softDelete.ts b/src/services/storage/drivers/relational/migrations/20210401143602_softDelete.ts new file mode 100644 index 0000000..954973f --- /dev/null +++ b/src/services/storage/drivers/relational/migrations/20210401143602_softDelete.ts @@ -0,0 +1,17 @@ +import * as Knex from "knex"; + + +export async function up(knex: Knex): Promise { + return await knex.schema.table('urls', table => { + table.string('deletedAt'); + }) + +} + + +export async function down(knex: Knex): Promise { + return knex.schema.table('urls', table => { + table.dropColumn('deletedAt'); + }) +} + diff --git a/src/services/storage/index.ts b/src/services/storage/index.ts index f82e287..d91f4b0 100644 --- a/src/services/storage/index.ts +++ b/src/services/storage/index.ts @@ -35,9 +35,12 @@ export class Storage implements StorageDriver { logger.debug(`Running Storage.initialize`) // Waits for 1 minute (6 * 10,000ms) before failing await runWithRetries(this._driver.initialize.bind(this._driver), { retries: 6, retryTime: 10 * 1000 }) - await this.url.deleteOverdue(this.config.lifetimeMs) + + const { lifetimeMs } = this.config; + await this.url.deleteOverdue(lifetimeMs) + this._intervalToken = setInterval( - () => this.url.deleteOverdue(this.config.lifetimeMs), + () => this.url.deleteOverdue(lifetimeMs), this.config.cleanupIntervalMs, ) } catch (err) { @@ -56,7 +59,7 @@ export class Storage implements StorageDriver { get driver() { return this.storage._driver } - public async get(id: string, options = { withInfo: false }): Promise { + public async get(id: string, options = { withInfo: false, includeDeleted: true }): Promise { try { logger.debug(`Running Storage.url.get with ${id}`) return await this.driver.url.get(id, options) @@ -66,10 +69,10 @@ export class Storage implements StorageDriver { } } - public async delete(id: string): Promise { + public async delete(id: string, options: { softDelete: boolean }): Promise { try { logger.debug(`Running Storage.url.delete with ${id}`) - return await this.driver.url.delete(id) + return await this.driver.url.delete(id, options) } catch (err) { logger.error(`Storage.url.delete failed: ${err}`) throw new GeneralError('Could not delete url') diff --git a/src/services/storage/types/url.ts b/src/services/storage/types/url.ts index 31c9ea0..12c67e0 100644 --- a/src/services/storage/types/url.ts +++ b/src/services/storage/types/url.ts @@ -3,16 +3,17 @@ export interface StoredUrl { url: string createdAt: string updatedAt: string + deletedAt?: string } export interface UrlStorageDriver { - get(id: string, options: { withInfo: boolean }): Promise + get(id: string, options: { withInfo: boolean, includeDeleted: boolean }): Promise save(url: UrlRequestData): Promise edit(id: string, url: string): Promise - delete(id: string): Promise + delete(id: string, options: { softDelete: boolean }): Promise deleteOverdue(timespanMs: number): Promise