Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fb24c55
Add the option to set a custom_id for users with admin right only
lesagi Mar 10, 2021
3b08252
Merge branch 'release/0.2.0' of https://github.com/mini-services/mini…
lesagi Mar 10, 2021
67ef7b9
Merge branch 'release/0.2.0' of github.com:SnirShechter/miniurl into …
SnirShechter Mar 13, 2021
8712965
Update src/services/storage/drivers/relational/index.ts
lesagi Mar 13, 2021
f3cca37
Update src/routes/url.ts
lesagi Mar 13, 2021
10839e3
Update src/services/storage/drivers/relational/index.ts
lesagi Mar 13, 2021
a7f4980
Update src/services/storage/drivers/relational/index.ts
lesagi Mar 13, 2021
5bf4b3c
Unauthorized Error on Custom ID
lesagi Mar 13, 2021
ff59372
Use postgres Error number to identify id duplicates
lesagi Mar 15, 2021
ceb52c3
Check if custom_id already exists before saving
lesagi Mar 20, 2021
5119db4
Merge remote-tracking branch 'origin/release/0.2.0' into release/0.2.0
lesagi Mar 23, 2021
f5d58ab
Add 'deletedAt' property to StoredUrl type
lesagi Mar 23, 2021
eb1c327
Replace hard delete with soft delete for InMemory Driver
lesagi Mar 23, 2021
a1f5d2f
Store ISOString in the 'deletedAt' field instead of UTCString
lesagi Mar 23, 2021
274e13e
Prevent unauthorized user to fetch soft deleted url
lesagi Mar 23, 2021
2f0c51d
Make the hard delete optional along with soft delete
lesagi Mar 23, 2021
e3707fa
Add the option for soft delete for Relational DB
lesagi Mar 23, 2021
1fafdc8
Relational: Prevent unauthorized user to fetch soft deleted url
lesagi Mar 23, 2021
0039269
Update Soft Delete
lesagi Apr 1, 2021
0350ad8
Remove unecessary import
lesagi Apr 1, 2021
d9195ff
Refactor softDelete function location in InMemoryStorage
lesagi Apr 1, 2021
df3172f
Add a call to storage.softDelete in Overdue
lesagi Apr 2, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/routes/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion src/services/auth/drivers/bearerToken/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 29 additions & 10 deletions src/services/storage/drivers/inMemory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, StoredUrl>; urlInformation: Map<string, UrlInformation> } = {
urls: new Map(),
urlInformation: new Map(),
}
private async softDelete(storedUrl: StoredUrl): Promise<void> {
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
Expand All @@ -22,9 +32,9 @@ export class InMemoryStorage implements StorageDriver {
return id
}

public async get(id: string, options = { withInfo: false }): Promise<StoredUrl | UrlWithInformation> {
public async get(id: string, options = { withInfo: false, includeDeleted: true }): Promise<StoredUrl | UrlWithInformation> {
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
Expand All @@ -34,19 +44,27 @@ export class InMemoryStorage implements StorageDriver {
return { ...storedUrl, ...urlInfo }
}

public async delete(id: string): Promise<void> {
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<void> {
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<number> {
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++
}
})
Expand Down Expand Up @@ -114,4 +132,5 @@ export class InMemoryStorage implements StorageDriver {
// eslint-disable-next-line
constructor(public config: InMemoryStorageConfig) {
}

}
27 changes: 16 additions & 11 deletions src/services/storage/drivers/relational/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StoredUrl | UrlWithInformation> {
public async get(id: string, options = { withInfo: false, includeDeleted: true }): Promise<StoredUrl | UrlWithInformation> {
let storedUrl, urlInfo
storedUrl = await this.storage.db
.table<StoredUrl & { serial?: number }>('urls')
.select('*')
.where('id', id)
.first()
if (storedUrl?.deletedAt) throw new NotFoundError();
if (!options.withInfo) {
storedUrl = await this.storage.db
.table<StoredUrl & { serial?: number }>('urls')
.select('*')
.where('id', id)
.first()

delete storedUrl?.serial
} else {
urlInfo = await this.storage.db
Expand All @@ -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<void> {
await this.storage.db.table<StoredUrl>('urls').where('id', id).delete()
return
public async delete(id: string, options: { softDelete: true }): Promise<void> {
if (!await this.storage.db.table<StoredUrl>('urls').where('id', id)) throw new NotFoundError();

if (options.softDelete) {
await this.storage.db.table<StoredUrl>('urls').where('id', id).delete()
} else {
await this.storage.db.table<StoredUrl>('urls').update({ deletedAt: new Date().toISOString() }).where('id', id)
}
}

public async deleteOverdue(timespanMs: number): Promise<number> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about deleteOverdue? We need it to soft-delete too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a deleteOverdue + migration to support the soft delete in postgres

const deleteBefore = new Date(new Date().getTime() - timespanMs)
return await this.storage.db.table<StoredUrl>('urls').where('updatedAt', '<', deleteBefore).delete()
return await this.storage.db.table<StoredUrl>('urls').update({ deletedAt: new Date().toISOString() }).where('updatedAt', '<', deleteBefore);
}

public async edit(id: string, url: string): Promise<StoredUrl> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as Knex from "knex";


export async function up(knex: Knex): Promise<void> {
return await knex.schema.table('urls', table => {
table.string('deletedAt');
})

}


export async function down(knex: Knex): Promise<void> {
return knex.schema.table('urls', table => {
table.dropColumn('deletedAt');
})
}

13 changes: 8 additions & 5 deletions src/services/storage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -56,7 +59,7 @@ export class Storage implements StorageDriver {
get driver() {
return this.storage._driver
}
public async get(id: string, options = { withInfo: false }): Promise<StoredUrl | UrlWithInformation> {
public async get(id: string, options = { withInfo: false, includeDeleted: true }): Promise<StoredUrl | UrlWithInformation> {
try {
logger.debug(`Running Storage.url.get with ${id}`)
return await this.driver.url.get(id, options)
Expand All @@ -66,10 +69,10 @@ export class Storage implements StorageDriver {
}
}

public async delete(id: string): Promise<void> {
public async delete(id: string, options: { softDelete: boolean }): Promise<void> {
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')
Expand Down
5 changes: 3 additions & 2 deletions src/services/storage/types/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StoredUrl | UrlWithInformation>
get(id: string, options: { withInfo: boolean, includeDeleted: boolean }): Promise<StoredUrl | UrlWithInformation>

save(url: UrlRequestData): Promise<StoredUrl>

edit(id: string, url: string): Promise<StoredUrl>

delete(id: string): Promise<void>
delete(id: string, options: { softDelete: boolean }): Promise<void>

deleteOverdue(timespanMs: number): Promise<number>

Expand Down