-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(storage): implement expiration date for user attributes
- Loading branch information
Showing
12 changed files
with
286 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { Table } from 'core/database/interfaces' | ||
|
||
export class DataRetentionTable extends Table { | ||
name: string = 'data_retention' | ||
|
||
async bootstrap() { | ||
let created = false | ||
|
||
await this.knex.createTableIfNotExists(this.name, table => { | ||
table.text('channel').notNullable() | ||
table.text('user_id').notNullable() | ||
table.text('field_path').notNullable() | ||
table.timestamp('expiry_date').notNullable() | ||
table.timestamp('created_on').notNullable() | ||
created = true | ||
}) | ||
return created | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { Logger } from 'botpress/sdk' | ||
import { UserRepository } from 'core/repositories' | ||
import { inject, injectable, tagged } from 'inversify' | ||
import _ from 'lodash' | ||
import { Memoize } from 'lodash-decorators' | ||
|
||
import { BotpressConfig } from '../../config/botpress.config' | ||
import { ConfigProvider } from '../../config/config-loader' | ||
import { TYPES } from '../../types' | ||
import { Janitor } from '../janitor' | ||
|
||
import { DataRetentionService } from './service' | ||
|
||
@injectable() | ||
export class DataRetentionJanitor extends Janitor { | ||
constructor( | ||
@inject(TYPES.Logger) | ||
@tagged('name', 'RetentionJanitor') | ||
protected logger: Logger, | ||
@inject(TYPES.ConfigProvider) private configProvider: ConfigProvider, | ||
@inject(TYPES.DataRetentionService) private dataRetentionService: DataRetentionService, | ||
@inject(TYPES.UserRepository) private userRepo: UserRepository | ||
) { | ||
super(logger) | ||
} | ||
|
||
@Memoize | ||
private async getBotpressConfig(): Promise<BotpressConfig> { | ||
return this.configProvider.getBotpressConfig() | ||
} | ||
|
||
protected async getInterval(): Promise<string> { | ||
const config = await this.getBotpressConfig() | ||
return (config.dataRetention && config.dataRetention.janitorInterval) || '15m' | ||
} | ||
|
||
protected async runTask(): Promise<void> { | ||
const expiredData = await this.dataRetentionService.getExpired() | ||
|
||
for (const expired of expiredData) { | ||
const { channel, user_id, field_path } = expired | ||
const { result: user } = await this.userRepo.getOrCreate(channel, user_id) | ||
|
||
await this.userRepo.updateAttributes(channel, user.id, _.omit(user.attributes, field_path)) | ||
await this.dataRetentionService.delete(channel, user_id, field_path) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { RetentionPolicy } from 'core/config/botpress.config' | ||
import { ConfigProvider } from 'core/config/config-loader' | ||
import Database from 'core/database' | ||
import { TYPES } from 'core/types' | ||
import diff from 'deep-diff' | ||
import { inject, injectable } from 'inversify' | ||
import _ from 'lodash' | ||
import moment from 'moment' | ||
import ms from 'ms' | ||
|
||
import { getPaths } from './util' | ||
|
||
export interface ExpiredData { | ||
channel: string | ||
user_id: string | ||
field_path: string | ||
} | ||
|
||
@injectable() | ||
export class DataRetentionService { | ||
private readonly tableName = 'data_retention' | ||
private policies: RetentionPolicy[] | undefined | ||
private DELETED_ATTR = 'D' | ||
|
||
constructor( | ||
@inject(TYPES.ConfigProvider) private configProvider: ConfigProvider, | ||
@inject(TYPES.Database) private database: Database | ||
) {} | ||
|
||
async initialize(): Promise<void> { | ||
const config = await this.configProvider.getBotpressConfig() | ||
this.policies = config.dataRetention && config.dataRetention.policies | ||
} | ||
|
||
async hasPolicy() { | ||
return this.policies | ||
} | ||
|
||
async checkChanges(channel: string, user_id: string, beforeAttributes: any, afterAttributes: any) { | ||
const differences = diff(getPaths(beforeAttributes), getPaths(afterAttributes)) | ||
if (!differences || !this.policies) { | ||
return | ||
} | ||
|
||
const changedPaths = _.flatten(differences.filter(diff => diff.kind != this.DELETED_ATTR).map(diff => diff.path)) | ||
if (!changedPaths.length) { | ||
return | ||
} | ||
|
||
for (const field in this.policies) { | ||
if (changedPaths.indexOf(field) > -1) { | ||
const expiry = moment() | ||
.add(ms(this.policies[field]), 'ms') | ||
.toDate() | ||
|
||
if (await this.get(channel, user_id, field)) { | ||
await this.update(channel, user_id, field, expiry) | ||
} else { | ||
await this.insert(channel, user_id, field, expiry) | ||
} | ||
} | ||
} | ||
} | ||
|
||
private async get(channel: string, user_id: string, field_path: string) { | ||
return await this.database | ||
.knex(this.tableName) | ||
.where({ channel, user_id, field_path }) | ||
.limit(1) | ||
.select('expiry_date') | ||
.first() | ||
} | ||
|
||
private async insert(channel: string, user_id: string, field_path: string, expiry_date: Date) { | ||
await this.database.knex(this.tableName).insert({ | ||
channel, | ||
user_id, | ||
field_path, | ||
expiry_date: this.database.knex.date.format(expiry_date), | ||
created_on: this.database.knex.date.now() | ||
}) | ||
} | ||
|
||
private async update(channel: string, user_id: string, field_path: string, expiry_date: Date) { | ||
await this.database | ||
.knex(this.tableName) | ||
.update({ expiry_date: this.database.knex.date.format(expiry_date) }) | ||
.where({ channel, user_id, field_path }) | ||
} | ||
|
||
async delete(channel: string, user_id: string, field_path: string): Promise<void> { | ||
await this.database | ||
.knex(this.tableName) | ||
.where({ channel, user_id, field_path }) | ||
.del() | ||
} | ||
|
||
async getExpired(): Promise<ExpiredData[]> { | ||
return await this.database | ||
.knex(this.tableName) | ||
.andWhere(this.database.knex.date.isBefore('expiry_date', new Date())) | ||
.select('channel', 'user_id', 'field_path') | ||
.limit(250) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import _ from 'lodash' | ||
|
||
interface Nodes { | ||
obj: Object | ||
path: string[] | ||
} | ||
|
||
/** | ||
* Returns the full path and value of every keys and subkeys of an object. | ||
* Can be used to easily check the difference between two objects. | ||
* | ||
* const myObject = { | ||
* id: 1, | ||
* profile: { | ||
* name: 'test', | ||
* age: 15 | ||
* } | ||
* } | ||
* Result: { | ||
* 'id': 1, | ||
* 'profile.name': 'test', | ||
* 'profile.age': 15 | ||
* } | ||
* | ||
* @param root Any kind of object | ||
*/ | ||
export function getPaths(sourceObject: any): any { | ||
const nodes: Nodes[] = [{ obj: sourceObject, path: [] }] | ||
const result: any = {} | ||
|
||
while (nodes.length > 0) { | ||
const node = nodes.pop() | ||
|
||
node && | ||
Object.keys(node.obj).forEach(key => { | ||
const value = node.obj[key] | ||
const currentPath = node.path.concat(key) | ||
|
||
if (Array.isArray(value) || typeof value !== 'object') { | ||
result[currentPath.join('.')] = value | ||
} else { | ||
nodes.unshift({ obj: value, path: currentPath }) | ||
} | ||
}) | ||
} | ||
|
||
return result | ||
} |
Oops, something went wrong.