From e693798249a02dfb3a5f164e038adc931a7806ec Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Thu, 5 Nov 2020 16:32:19 +0200 Subject: [PATCH 01/23] feat: initial implementation of the versioning logic --- registry/package.json | 2 +- .../server/appRoutes/routes/updateAppRoute.ts | 2 +- registry/server/apps/routes/createApp.ts | 6 +- registry/server/apps/routes/deleteApp.ts | 5 +- registry/server/apps/routes/updateApp.ts | 7 +- registry/server/auth.ts | 8 +- registry/server/db.ts | 61 -------------- registry/server/db/index.ts | 38 +++++++++ registry/server/db/range.ts | 32 ++++++++ registry/server/db/versioning.ts | 80 +++++++++++++++++++ registry/server/db/versioningConfig.ts | 24 ++++++ .../migrations/20201105155522_versioning.ts | 20 +++++ registry/tsconfig.json | 4 +- 13 files changed, 220 insertions(+), 69 deletions(-) delete mode 100644 registry/server/db.ts create mode 100644 registry/server/db/index.ts create mode 100644 registry/server/db/range.ts create mode 100644 registry/server/db/versioning.ts create mode 100644 registry/server/db/versioningConfig.ts create mode 100644 registry/server/migrations/20201105155522_versioning.ts diff --git a/registry/package.json b/registry/package.json index f1ce4426..5f1f1ff5 100644 --- a/registry/package.json +++ b/registry/package.json @@ -51,7 +51,7 @@ "supertest": "4.0.2", "timekeeper": "^2.2.0", "ts-node": "^8.10.2", - "typescript": "3.7.5" + "typescript": "^3.9.7" }, "dependencies": { "axios": "^0.19.0", diff --git a/registry/server/appRoutes/routes/updateAppRoute.ts b/registry/server/appRoutes/routes/updateAppRoute.ts index 985e316b..3cfb6f51 100644 --- a/registry/server/appRoutes/routes/updateAppRoute.ts +++ b/registry/server/appRoutes/routes/updateAppRoute.ts @@ -50,7 +50,7 @@ const updateAppRoute = async (req: Request, res: Re return; } - await db.transaction(async (transaction) => { + await db.versioning(req.user, {type: 'routes', id: appRouteId}, async (transaction) => { await db('routes').where('id', appRouteId).update(appRoute).transacting(transaction); if (!_.isEmpty(appRouteSlots)) { diff --git a/registry/server/apps/routes/createApp.ts b/registry/server/apps/routes/createApp.ts index 2d3cb707..20c9669d 100644 --- a/registry/server/apps/routes/createApp.ts +++ b/registry/server/apps/routes/createApp.ts @@ -23,7 +23,11 @@ const validateRequestBeforeCreateApp = validateRequestFactory([{ const createApp = async (req: Request, res: Response): Promise => { const app = req.body; - await db('apps').insert(stringifyJSON(['dependencies', 'props', 'ssr', 'configSelector'], app)); + await db.versioning(req.user, {type: 'apps', id: app.name}, async (trx) => { + await db('apps') + .insert(stringifyJSON(['dependencies', 'props', 'ssr', 'configSelector'], app)) + .transacting(trx); + }); const [savedApp] = await db.select().from('apps').where('name', app.name); diff --git a/registry/server/apps/routes/deleteApp.ts b/registry/server/apps/routes/deleteApp.ts index b732dc1d..aa2e4f1b 100644 --- a/registry/server/apps/routes/deleteApp.ts +++ b/registry/server/apps/routes/deleteApp.ts @@ -25,7 +25,10 @@ const validateRequestBeforeDeleteApp = validateRequestFactory([{ const deleteApp = async (req: Request, res: Response): Promise => { const appName = req.params.name; - const count = await db('apps').where('name', appName).delete(); + let count; + await db.versioning(req.user, {type: 'apps', id: appName}, async (trx) => { + count = await db('apps').where('name', appName).delete().transacting(trx); + }); if (count) { res.status(204).send(); diff --git a/registry/server/apps/routes/updateApp.ts b/registry/server/apps/routes/updateApp.ts index 59c988bb..a17395d7 100644 --- a/registry/server/apps/routes/updateApp.ts +++ b/registry/server/apps/routes/updateApp.ts @@ -44,7 +44,12 @@ const updateApp = async (req: Request, res: Response): P return; } - await db('apps').where({ name: appName }).update(stringifyJSON(['dependencies', 'props', 'ssr', 'configSelector'], app)); + await db.versioning(req.user, {type: 'apps', id: appName}, async (trx) => { + await db('apps') + .where({ name: appName }) + .update(stringifyJSON(['dependencies', 'props', 'ssr', 'configSelector'], app)) + .transacting(trx); + }); const [updatedApp] = await db.select().from('apps').where('name', appName); diff --git a/registry/server/auth.ts b/registry/server/auth.ts index 8326c143..b414e664 100644 --- a/registry/server/auth.ts +++ b/registry/server/auth.ts @@ -12,6 +12,12 @@ import {SettingsService} from "./settings/services/SettingsService"; import {SettingKeys} from "./settings/interfaces"; import urljoin from 'url-join'; +export interface User { + authEntityId: number; + identifier: string; + role: string; +} + export default (app: Express, settingsService: SettingsService, config: any): RequestHandler => { const SessionKnex = sessionKnex(session); const sessionConfig = Object.assign({ @@ -241,7 +247,7 @@ export default (app: Express, settingsService: SettingsService, config: any): Re }; } -async function getEntityWithCreds(provider: string, identifier: string, secret: string|null):Promise { +async function getEntityWithCreds(provider: string, identifier: string, secret: string|null):Promise { const user = await db.select().from('auth_entities') .first('identifier', 'id', 'role', 'secret') .where({ diff --git a/registry/server/db.ts b/registry/server/db.ts deleted file mode 100644 index 0e9df809..00000000 --- a/registry/server/db.ts +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -import knex from 'knex'; -import config from 'config'; - -const client: string = config.get('database.client'); - -const knexConf: knex.Config = { // after: const knex = require('knex')({client: 'mysql'}); - client: client, - connection: config.get('database.connection'), - /** - * Sqlite does not support inserting default values - * That is why we added it - */ - useNullAsDefault: true, -}; - -if (client === 'mysql') { - knexConf.pool = { - afterCreate: (conn: any, done: Function) => { - conn.query('SET time_zone="+00:00";', (err: Error) => done(err, conn)) - } - }; -} else if (client === 'sqlite3'){ - knexConf.pool = { - afterCreate: (conn: any, done: Function) => { - conn.run('PRAGMA foreign_keys = ON;', (err: Error) => done(err, conn)) - } - }; -} - -knex.QueryBuilder.extend('range', function (this: any, range: string|null|undefined): knex.QueryBuilder { - if (typeof range !== 'string') { - return this.client.transaction(async (trx: any) => { - const res = await this.transacting(trx); - return {data: res, pagination: {total: res.length}} - }); - } - - const input = JSON.parse(range); - - const countQuery = new this.constructor(this.client) - .count('* as total') - .from( - this.clone() - .offset(0) - .as('__count__query__'), - ) - .first(); - - // This will paginate the data itself - this.offset(parseInt(input[0])).limit(parseInt(input[1]) - parseInt(input[0]) + 1); - - return this.client.transaction(async (trx: any) => { - const res = await this.transacting(trx); - const { total } = await countQuery.transacting(trx); - return {data: res, pagination: { total }} - }); -}); - -export default knex(knexConf); diff --git a/registry/server/db/index.ts b/registry/server/db/index.ts new file mode 100644 index 00000000..a59fe385 --- /dev/null +++ b/registry/server/db/index.ts @@ -0,0 +1,38 @@ +'use strict'; + +import knex from 'knex'; +import config from 'config'; +import rangeExtender from './range'; +import addVersioning from './versioning'; + +const client: string = config.get('database.client'); + +const knexConf: knex.Config = { // after: const knex = require('knex')({client: 'mysql'}); + client: client, + connection: config.get('database.connection'), + /** + * Sqlite does not support inserting default values + * That is why we added it + */ + useNullAsDefault: true, +}; + +if (client === 'mysql') { + knexConf.pool = { + afterCreate: (conn: any, done: Function) => { + conn.query('SET time_zone="+00:00";', (err: Error) => done(err, conn)) + } + }; +} else if (client === 'sqlite3'){ + knexConf.pool = { + afterCreate: (conn: any, done: Function) => { + conn.run('PRAGMA foreign_keys = ON;', (err: Error) => done(err, conn)) + } + }; +} + +rangeExtender(knex); + +const knexInstance = knex(knexConf); + +export default addVersioning(knexInstance); diff --git a/registry/server/db/range.ts b/registry/server/db/range.ts new file mode 100644 index 00000000..ff8f2ef8 --- /dev/null +++ b/registry/server/db/range.ts @@ -0,0 +1,32 @@ +import type Knex from 'knex'; + +export default function (knex: typeof Knex) { + knex.QueryBuilder.extend('range', function (this: any, range: string|null|undefined) { + if (typeof range !== 'string') { + return this.client.transaction(async (trx: any) => { + const res = await this.transacting(trx); + return {data: res, pagination: {total: res.length}} + }); + } + + const input = JSON.parse(range); + + const countQuery = new this.constructor(this.client) + .count('* as total') + .from( + this.clone() + .offset(0) + .as('__count__query__'), + ) + .first(); + + // This will paginate the data itself + this.offset(parseInt(input[0])).limit(parseInt(input[1]) - parseInt(input[0]) + 1); + + return this.client.transaction(async (trx: any) => { + const res = await this.transacting(trx); + const { total } = await countQuery.transacting(trx); + return {data: res, pagination: { total }} + }); + }); +} diff --git a/registry/server/db/versioning.ts b/registry/server/db/versioning.ts new file mode 100644 index 00000000..d6236ea0 --- /dev/null +++ b/registry/server/db/versioning.ts @@ -0,0 +1,80 @@ +import type Knex from 'knex'; +import type {Transaction} from 'knex'; +import {User} from '../auth'; +import _ from 'lodash'; +import entitiesConf from './versioningConfig'; + +export interface OperationConf { + type: string; + id: string|number; +} + +interface VersionedKnex extends Knex { + versioning(user: any, conf: OperationConf, callback: (transaction: Transaction) => Promise): void; +} + +export default function (knex: Knex|any): VersionedKnex { + if (knex.versioning) { + return knex; + } + + knex.versioning = async function (user: User, conf: OperationConf, callback: (transaction: Transaction) => Promise) { + if (entitiesConf[conf.type] === undefined) { + throw new Error(`Attempt to log changes to unknown entity: "${conf.type}"`) + } + + await knex.transaction(async (trx: Transaction) => { + const currentData = await getDataSnapshot(knex, trx, conf); + const res = await callback(trx); + const newData = await getDataSnapshot(knex, trx, conf); + + const logRecord = { + entity_type: conf.type, + entity_id: conf.id, + data: JSON.stringify(currentData), + data_after: JSON.stringify(newData), + created_by: user.identifier, + created_at: Math.floor(new Date().getTime() / 1000), + }; + + await knex('versioning').insert(logRecord).transacting(trx); + + return res; + }); + } + + return knex; +} + +async function getDataSnapshot(db: Knex, trx: Transaction, conf: OperationConf) { + const entityConf = entitiesConf[conf.type]; + + const dbRes = await db(conf.type) + .first('*') + .where(entityConf.idColumn, conf.id) + .transacting(trx); + + if (!dbRes) { + return null; + } + + + const res = { + data: _.omit(dbRes, [entityConf.idColumn]), + related: {} as Record, + } + + for (const relation of entityConf.related) { + const dbRes = await db(relation.type) + .select('*') + .where(relation.key, conf.id) + .transacting(trx); + + res.related = dbRes.reduce((acc, v) => { + acc[relation.type] = _.omit(v, [relation.key, relation.idColumn]); + return acc; + }, {}) + } + + return res; +} diff --git a/registry/server/db/versioningConfig.ts b/registry/server/db/versioningConfig.ts new file mode 100644 index 00000000..67da69be --- /dev/null +++ b/registry/server/db/versioningConfig.ts @@ -0,0 +1,24 @@ + +interface RelatedEntities { + type: string; + idColumn: string; + key: string; +} + +interface EntityConf { + idColumn: string; + related: RelatedEntities[], +} + +const entitiesConf: Record = { + apps: { + idColumn: 'name', + related: [], + }, + routes: { + idColumn: 'id', + related: [{ type: 'route_slots', idColumn: 'id', key: 'routeId' }], + } +}; + +export default entitiesConf; diff --git a/registry/server/migrations/20201105155522_versioning.ts b/registry/server/migrations/20201105155522_versioning.ts new file mode 100644 index 00000000..bbcb783f --- /dev/null +++ b/registry/server/migrations/20201105155522_versioning.ts @@ -0,0 +1,20 @@ +import * as Knex from "knex"; + + +export async function up(knex: Knex): Promise { + return knex.schema.createTable('versioning', table => { + table.increments('id'); + table.string('entity_type').notNullable(); + table.string('entity_id').notNullable(); + table.text('data'); + table.text('data_after'); + table.string('created_by').notNullable(); + table.timestamp('created_at').defaultTo(knex.fn.now()); + }); +} + + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable('versioning'); +} + diff --git a/registry/tsconfig.json b/registry/tsconfig.json index 44157b2f..02fad8bd 100644 --- a/registry/tsconfig.json +++ b/registry/tsconfig.json @@ -2,9 +2,9 @@ "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "target": "ES2019", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - // "lib": [], /* Specify library files to be included in the compilation. */ + "lib": ["es2020"], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ From 2587ba098631ca93fdd04ff5a4852b57240c06a1 Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Thu, 5 Nov 2020 16:45:08 +0200 Subject: [PATCH 02/23] chore: code refactoring --- registry/server/db/versioning.ts | 67 +------------- .../config.ts} | 4 +- .../server/versioning/services/Versioning.ts | 89 +++++++++++++++++++ 3 files changed, 94 insertions(+), 66 deletions(-) rename registry/server/{db/versioningConfig.ts => versioning/config.ts} (81%) create mode 100644 registry/server/versioning/services/Versioning.ts diff --git a/registry/server/db/versioning.ts b/registry/server/db/versioning.ts index d6236ea0..3746a638 100644 --- a/registry/server/db/versioning.ts +++ b/registry/server/db/versioning.ts @@ -1,13 +1,7 @@ import type Knex from 'knex'; import type {Transaction} from 'knex'; -import {User} from '../auth'; -import _ from 'lodash'; -import entitiesConf from './versioningConfig'; -export interface OperationConf { - type: string; - id: string|number; -} +import versioningService, {OperationConf} from '../versioning/services/Versioning'; interface VersionedKnex extends Knex { versioning(user: any, conf: OperationConf, callback: (transaction: Transaction) => Promise): void; @@ -18,63 +12,8 @@ export default function (knex: Knex|any): VersionedKnex { return knex; } - knex.versioning = async function (user: User, conf: OperationConf, callback: (transaction: Transaction) => Promise) { - if (entitiesConf[conf.type] === undefined) { - throw new Error(`Attempt to log changes to unknown entity: "${conf.type}"`) - } - - await knex.transaction(async (trx: Transaction) => { - const currentData = await getDataSnapshot(knex, trx, conf); - const res = await callback(trx); - const newData = await getDataSnapshot(knex, trx, conf); - - const logRecord = { - entity_type: conf.type, - entity_id: conf.id, - data: JSON.stringify(currentData), - data_after: JSON.stringify(newData), - created_by: user.identifier, - created_at: Math.floor(new Date().getTime() / 1000), - }; - - await knex('versioning').insert(logRecord).transacting(trx); - - return res; - }); - } + versioningService.setDb(knex); + knex.versioning = versioningService.logOperation; return knex; } - -async function getDataSnapshot(db: Knex, trx: Transaction, conf: OperationConf) { - const entityConf = entitiesConf[conf.type]; - - const dbRes = await db(conf.type) - .first('*') - .where(entityConf.idColumn, conf.id) - .transacting(trx); - - if (!dbRes) { - return null; - } - - - const res = { - data: _.omit(dbRes, [entityConf.idColumn]), - related: {} as Record, - } - - for (const relation of entityConf.related) { - const dbRes = await db(relation.type) - .select('*') - .where(relation.key, conf.id) - .transacting(trx); - - res.related = dbRes.reduce((acc, v) => { - acc[relation.type] = _.omit(v, [relation.key, relation.idColumn]); - return acc; - }, {}) - } - - return res; -} diff --git a/registry/server/db/versioningConfig.ts b/registry/server/versioning/config.ts similarity index 81% rename from registry/server/db/versioningConfig.ts rename to registry/server/versioning/config.ts index 67da69be..fe6af971 100644 --- a/registry/server/db/versioningConfig.ts +++ b/registry/server/versioning/config.ts @@ -10,7 +10,7 @@ interface EntityConf { related: RelatedEntities[], } -const entitiesConf: Record = { +const versioningConf: Record = { apps: { idColumn: 'name', related: [], @@ -21,4 +21,4 @@ const entitiesConf: Record = { } }; -export default entitiesConf; +export default versioningConf; diff --git a/registry/server/versioning/services/Versioning.ts b/registry/server/versioning/services/Versioning.ts new file mode 100644 index 00000000..440ba624 --- /dev/null +++ b/registry/server/versioning/services/Versioning.ts @@ -0,0 +1,89 @@ +import versioningConfig from '../config'; +import Knex, {Transaction} from 'knex'; +import {User} from "../../auth"; +import _ from "lodash"; + +export interface OperationConf { + type: string; + id: string|number; +} + +export class Versioning { + private db?: Knex; + + constructor( + private config: typeof versioningConfig, + ) {} + + public setDb(db: Knex) { + this.db = db; + } + + public logOperation = async (user: User, conf: OperationConf, callback: (transaction: Transaction) => Promise) => { + if (this.config[conf.type] === undefined) { + throw new Error(`Attempt to log changes to unknown entity: "${conf.type}"`); + } + if (!this.db) { + throw new Error(`Attempt to log operation before DB initialization!`); + } + + await this.db.transaction(async (trx: Transaction) => { + const currentData = await this.getDataSnapshot(trx, conf); + const res = await callback(trx); + const newData = await this.getDataSnapshot(trx, conf); + + const logRecord = { + entity_type: conf.type, + entity_id: conf.id, + data: JSON.stringify(currentData), + data_after: JSON.stringify(newData), + created_by: user.identifier, + created_at: Math.floor(new Date().getTime() / 1000), + }; + + await this.db!('versioning').insert(logRecord).transacting(trx); + + return res; + }); + } + + private async getDataSnapshot(trx: Transaction, conf: OperationConf) { + if (!this.db) { + throw new Error(`Attempt to log operation before DB initialization!`); + } + + const entityConf = this.config[conf.type]; + + const dbRes = await this.db(conf.type) + .first('*') + .where(entityConf.idColumn, conf.id) + .transacting(trx); + + if (!dbRes) { + return null; + } + + + const res = { + data: _.omit(dbRes, [entityConf.idColumn]), + related: {} as Record, + } + + for (const relation of entityConf.related) { + const dbRes = await this.db(relation.type) + .select('*') + .where(relation.key, conf.id) + .transacting(trx); + + res.related = dbRes.reduce((acc, v) => { + acc[relation.type] = _.omit(v, [relation.key, relation.idColumn]); + return acc; + }, {}) + } + + return res; + } + +} + +export default new Versioning(versioningConfig); From 10ecef2170fba81af662fbd824b261747bb80675 Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Fri, 6 Nov 2020 12:03:42 +0200 Subject: [PATCH 03/23] fix: Fixed data formation for log record --- registry/server/versioning/services/Versioning.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/registry/server/versioning/services/Versioning.ts b/registry/server/versioning/services/Versioning.ts index 440ba624..633d0d0c 100644 --- a/registry/server/versioning/services/Versioning.ts +++ b/registry/server/versioning/services/Versioning.ts @@ -75,10 +75,7 @@ export class Versioning { .where(relation.key, conf.id) .transacting(trx); - res.related = dbRes.reduce((acc, v) => { - acc[relation.type] = _.omit(v, [relation.key, relation.idColumn]); - return acc; - }, {}) + res.related[relation.type] = dbRes.map(v => _.omit(v, [relation.key, relation.idColumn])); } return res; From fce525124b3f0b03d0975cd72ac88d20a3baa29c Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Fri, 6 Nov 2020 12:16:59 +0200 Subject: [PATCH 04/23] feat: ability to log creation of the record with auto-increment ID --- .../server/appRoutes/routes/createAppRoute.ts | 4 +++- registry/server/db/versioning.ts | 2 +- .../server/versioning/services/Versioning.ts | 22 ++++++++++++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/registry/server/appRoutes/routes/createAppRoute.ts b/registry/server/appRoutes/routes/createAppRoute.ts index eab794e7..308aad70 100644 --- a/registry/server/appRoutes/routes/createAppRoute.ts +++ b/registry/server/appRoutes/routes/createAppRoute.ts @@ -27,7 +27,9 @@ const createAppRoute = async (req: Request, res: Response) => { ...appRoute } = req.body; - const savedAppRouteId = await db.transaction(async (transaction) => { + let savedAppRouteId: number|null = null; + + await db.versioning(req.user, {type: 'routes'}, async (transaction) => { const [appRouteId] = await db('routes').insert(appRoute).transacting(transaction); await db.batchInsert('route_slots', _.compose( diff --git a/registry/server/db/versioning.ts b/registry/server/db/versioning.ts index 3746a638..cb42c333 100644 --- a/registry/server/db/versioning.ts +++ b/registry/server/db/versioning.ts @@ -4,7 +4,7 @@ import type {Transaction} from 'knex'; import versioningService, {OperationConf} from '../versioning/services/Versioning'; interface VersionedKnex extends Knex { - versioning(user: any, conf: OperationConf, callback: (transaction: Transaction) => Promise): void; + versioning(user: any, conf: OperationConf, callback: (transaction: Transaction) => Promise): void; } export default function (knex: Knex|any): VersionedKnex { diff --git a/registry/server/versioning/services/Versioning.ts b/registry/server/versioning/services/Versioning.ts index 633d0d0c..0b102fa8 100644 --- a/registry/server/versioning/services/Versioning.ts +++ b/registry/server/versioning/services/Versioning.ts @@ -5,7 +5,7 @@ import _ from "lodash"; export interface OperationConf { type: string; - id: string|number; + id?: string|number; } export class Versioning { @@ -19,7 +19,7 @@ export class Versioning { this.db = db; } - public logOperation = async (user: User, conf: OperationConf, callback: (transaction: Transaction) => Promise) => { + public logOperation = async (user: User, conf: OperationConf, callback: (transaction: Transaction) => Promise) => { if (this.config[conf.type] === undefined) { throw new Error(`Attempt to log changes to unknown entity: "${conf.type}"`); } @@ -28,8 +28,17 @@ export class Versioning { } await this.db.transaction(async (trx: Transaction) => { - const currentData = await this.getDataSnapshot(trx, conf); - const res = await callback(trx); + let currentData = null; + if (conf.id) { + currentData = await this.getDataSnapshot(trx, conf); + } + + const newRecordId = await callback(trx); + if (conf.id === undefined && newRecordId === undefined) { + throw new Error(`Unable to identify record ID. Received ids: "${conf.id}" & "${newRecordId}"`); + } else if (conf.id === undefined) { + conf.id = newRecordId as number; + } const newData = await this.getDataSnapshot(trx, conf); const logRecord = { @@ -42,8 +51,6 @@ export class Versioning { }; await this.db!('versioning').insert(logRecord).transacting(trx); - - return res; }); } @@ -51,6 +58,9 @@ export class Versioning { if (!this.db) { throw new Error(`Attempt to log operation before DB initialization!`); } + if (!conf.id) { + throw new Error(`Attempt to log operation without passing an ID! Passed ID: "${conf.id}"`); + } const entityConf = this.config[conf.type]; From 780ceb1dd48f0856cbeeaf45b81aa44ac82443d8 Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Fri, 6 Nov 2020 15:27:07 +0200 Subject: [PATCH 05/23] feat: initial implementation of the version revert logic --- registry/server/app.ts | 1 + registry/server/db/versioning.ts | 2 +- registry/server/routes/index.ts | 1 + registry/server/versioning/routes/index.ts | 11 +++ .../server/versioning/routes/revertVersion.ts | 83 +++++++++++++++++++ .../server/versioning/services/Versioning.ts | 11 ++- 6 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 registry/server/versioning/routes/index.ts create mode 100644 registry/server/versioning/routes/revertVersion.ts diff --git a/registry/server/app.ts b/registry/server/app.ts index 02ede77c..7c12b364 100644 --- a/registry/server/app.ts +++ b/registry/server/app.ts @@ -38,6 +38,7 @@ export default (withAuth: boolean = true) => { app.use('/api/v1/route', authMw, routes.appRoutes); app.use('/api/v1/shared_props', authMw, routes.sharedProps); app.use('/api/v1/auth_entities', authMw, routes.authEntities); + app.use('/api/v1/versioning', authMw, routes.versioning); app.use('/api/v1/settings', routes.settings(authMw)); app.use(errorHandler); diff --git a/registry/server/db/versioning.ts b/registry/server/db/versioning.ts index cb42c333..4978a41e 100644 --- a/registry/server/db/versioning.ts +++ b/registry/server/db/versioning.ts @@ -4,7 +4,7 @@ import type {Transaction} from 'knex'; import versioningService, {OperationConf} from '../versioning/services/Versioning'; interface VersionedKnex extends Knex { - versioning(user: any, conf: OperationConf, callback: (transaction: Transaction) => Promise): void; + versioning(user: any, conf: OperationConf, callback: (transaction: Transaction) => Promise): Promise; } export default function (knex: Knex|any): VersionedKnex { diff --git a/registry/server/routes/index.ts b/registry/server/routes/index.ts index b0ab1938..59f381ce 100644 --- a/registry/server/routes/index.ts +++ b/registry/server/routes/index.ts @@ -5,3 +5,4 @@ export { default as appRoutes } from '../appRoutes/routes'; export { default as sharedProps } from '../sharedProps/routes'; export { default as authEntities } from '../authEntities/routes'; export { default as settings } from '../settings/routes'; +export { default as versioning } from '../versioning/routes'; diff --git a/registry/server/versioning/routes/index.ts b/registry/server/versioning/routes/index.ts new file mode 100644 index 00000000..968168db --- /dev/null +++ b/registry/server/versioning/routes/index.ts @@ -0,0 +1,11 @@ +import express from 'express'; + +//import getVersions from './getVersions'; +import revertVersion from './revertVersion'; + +const versioningRouter = express.Router(); + +//versioningRouter.get('/', ...getVersions); +versioningRouter.post('/:id/revert', ...revertVersion); + +export default versioningRouter; diff --git a/registry/server/versioning/routes/revertVersion.ts b/registry/server/versioning/routes/revertVersion.ts new file mode 100644 index 00000000..90101cc9 --- /dev/null +++ b/registry/server/versioning/routes/revertVersion.ts @@ -0,0 +1,83 @@ +import { + Request, + Response, +} from 'express'; + +import db from '../../db'; + +import versioningConfig from '../config'; +import _ from 'lodash'; + +type RequestParams = { + id: string +}; + +const updateApp = async (req: Request, res: Response): Promise => { + const versionId = req.params.id; + + const versionRow = await db('versioning').first('*').where('id', versionId); + if (!versionRow) { + res.status(404).send(); + return; + } + versionRow.data = versionRow.data === null ? null : JSON.parse(versionRow.data); + versionRow.data_after = versionRow.data_after === null ? null : JSON.parse(versionRow.data_after); + if (!await isRevertable(versionRow)) { + res.status(400).send(); + return; + } + + const entityConf = versioningConfig[versionRow.entity_type]; + + const revertVersionId = await db.versioning(req.user, {type: versionRow.entity_type, id: versionRow.entity_id}, async (trx) => { + if (versionRow.data === null) { // We have creation operation, so we delete records to revert it + for (const relation of entityConf.related) { + await db(relation.type).where(relation.key, versionRow.entity_id).delete().transacting(trx); + } + await db(versionRow.entity_type).where(entityConf.idColumn, versionRow.entity_id).delete().transacting(trx); + } else if (versionRow.data_after === null) { // Deletion operation, so we need to create everything the was deleted + const dataToRestore = versionRow.data; + await db(versionRow.entity_type).insert({...dataToRestore.data, [entityConf.idColumn]: versionRow.entity_id}).transacting(trx); + + for (const relation of entityConf.related) { + const relatedItems = dataToRestore.related[relation.type].map((v: any) => ({...v, [relation.key]: versionRow.entity_id})); + await db.batchInsert(relation.type, relatedItems).transacting(trx); + } + } else { // We have an update operation + const dataToRestore = versionRow.data; + for (const relation of entityConf.related) { + await db(relation.type).where(relation.key, versionRow.entity_id).delete().transacting(trx); + + const relatedItems = dataToRestore.related[relation.type].map((v: any) => ({...v, [relation.key]: versionRow.entity_id})); + await db.batchInsert(relation.type, relatedItems).transacting(trx); + } + await db(versionRow.entity_type).where(entityConf.idColumn, versionRow.entity_id).update(dataToRestore.data).transacting(trx); + } + }); + + res.status(200).send({ + status: 'ok', + versionId: revertVersionId, + }); +}; + +async function isRevertable(versionRow: any) { + const lastVersionRow = await db('versioning') + .first('*') + .where(_.pick(versionRow, ['entity_type', 'entity_id'])) + .orderBy('id', 'desc'); + lastVersionRow.data = lastVersionRow.data === null ? null : JSON.parse(lastVersionRow.data); + lastVersionRow.data_after = lastVersionRow.data_after === null ? null : JSON.parse(lastVersionRow.data_after); + + if (lastVersionRow.id === versionRow.id) { + return true; + } + + if (versionRow.data_after === null) { // Deletion operation + return false; // It's possible to revert deletion operations only if it's the last one for selected entity + } else { // We have creation/update operation + return lastVersionRow.data_after !== null; //It's possible to revert creation/update operations only if the last one is not a deletion one + } +} + +export default [updateApp]; diff --git a/registry/server/versioning/services/Versioning.ts b/registry/server/versioning/services/Versioning.ts index 0b102fa8..b14e1925 100644 --- a/registry/server/versioning/services/Versioning.ts +++ b/registry/server/versioning/services/Versioning.ts @@ -19,6 +19,12 @@ export class Versioning { this.db = db; } + /** + * @param user + * @param conf + * @param callback + * @returns - Id of the version record + */ public logOperation = async (user: User, conf: OperationConf, callback: (transaction: Transaction) => Promise) => { if (this.config[conf.type] === undefined) { throw new Error(`Attempt to log changes to unknown entity: "${conf.type}"`); @@ -27,7 +33,7 @@ export class Versioning { throw new Error(`Attempt to log operation before DB initialization!`); } - await this.db.transaction(async (trx: Transaction) => { + return await this.db.transaction(async (trx: Transaction) => { let currentData = null; if (conf.id) { currentData = await this.getDataSnapshot(trx, conf); @@ -50,7 +56,8 @@ export class Versioning { created_at: Math.floor(new Date().getTime() / 1000), }; - await this.db!('versioning').insert(logRecord).transacting(trx); + const [versionID] = await this.db!('versioning').insert(logRecord).transacting(trx); + return versionID; }); } From beea4aa6f5581a2c7b2aca88391d5c8cbdab2e7d Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Fri, 6 Nov 2020 15:39:07 +0200 Subject: [PATCH 06/23] feat: added API to list available versions --- .../server/versioning/routes/getVersions.ts | 26 +++++++++++++++++++ registry/server/versioning/routes/index.ts | 4 +-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 registry/server/versioning/routes/getVersions.ts diff --git a/registry/server/versioning/routes/getVersions.ts b/registry/server/versioning/routes/getVersions.ts new file mode 100644 index 00000000..b502de51 --- /dev/null +++ b/registry/server/versioning/routes/getVersions.ts @@ -0,0 +1,26 @@ +import { + Request, + Response, +} from 'express'; +import _ from 'lodash'; + +import db from '../../db'; + +const getVersions = async (req: Request, res: Response): Promise => { + const filters = req.query.filter ? JSON.parse(req.query.filter as string) : {}; + + const query = db.select().from('versioning').orderBy('id', 'desc'); + if (filters.id) { + query.whereIn('name', [...filters.id]); + } + if (filters.entity_type || filters.entity_id) { + query.where(_.pick(filters, ['entity_type', 'entity_id'])); + } + + const dbRes = await query.range(req.query.range as string | undefined); + + res.setHeader('Content-Range', dbRes.pagination.total); //Stub for future pagination capabilities + res.status(200).send(dbRes.data); +}; + +export default [getVersions]; diff --git a/registry/server/versioning/routes/index.ts b/registry/server/versioning/routes/index.ts index 968168db..fd6d1159 100644 --- a/registry/server/versioning/routes/index.ts +++ b/registry/server/versioning/routes/index.ts @@ -1,11 +1,11 @@ import express from 'express'; -//import getVersions from './getVersions'; +import getVersions from './getVersions'; import revertVersion from './revertVersion'; const versioningRouter = express.Router(); -//versioningRouter.get('/', ...getVersions); +versioningRouter.get('/', ...getVersions); versioningRouter.post('/:id/revert', ...revertVersion); export default versioningRouter; From 353a2ff7570c275c89f11ac168c066f5fb233c47 Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Fri, 6 Nov 2020 16:34:57 +0200 Subject: [PATCH 07/23] chore: code refactoring --- registry/package.json | 1 + registry/server/versioning/errors.ts | 6 ++ .../server/versioning/routes/revertVersion.ts | 79 ++++--------------- .../server/versioning/services/Versioning.ts | 75 +++++++++++++++++- registry/typings/@namecheap/index.d.ts | 24 ++++++ 5 files changed, 120 insertions(+), 65 deletions(-) create mode 100644 registry/server/versioning/errors.ts create mode 100644 registry/typings/@namecheap/index.d.ts diff --git a/registry/package.json b/registry/package.json index 5f1f1ff5..41fb14ea 100644 --- a/registry/package.json +++ b/registry/package.json @@ -54,6 +54,7 @@ "typescript": "^3.9.7" }, "dependencies": { + "@namecheap/error-extender": "^1.1.1", "axios": "^0.19.0", "bcrypt": "^5.0.0", "body-parser": "^1.19.0", diff --git a/registry/server/versioning/errors.ts b/registry/server/versioning/errors.ts new file mode 100644 index 00000000..8b107c96 --- /dev/null +++ b/registry/server/versioning/errors.ts @@ -0,0 +1,6 @@ +import extendError from '@namecheap/error-extender'; + +export const VersioningError = extendError('VersioningError'); +export const NonRevertableError = extendError<{reason: string}>('NonRevertableError', {parent: VersioningError}); +export const NonExistingVersionError = extendError('NonExistingVersionError', {parent: VersioningError}); + diff --git a/registry/server/versioning/routes/revertVersion.ts b/registry/server/versioning/routes/revertVersion.ts index 90101cc9..ab84c036 100644 --- a/registry/server/versioning/routes/revertVersion.ts +++ b/registry/server/versioning/routes/revertVersion.ts @@ -4,9 +4,10 @@ import { } from 'express'; import db from '../../db'; +import versioning from "../services/Versioning"; import versioningConfig from '../config'; -import _ from 'lodash'; +import * as errors from '../errors'; type RequestParams = { id: string @@ -15,69 +16,23 @@ type RequestParams = { const updateApp = async (req: Request, res: Response): Promise => { const versionId = req.params.id; - const versionRow = await db('versioning').first('*').where('id', versionId); - if (!versionRow) { - res.status(404).send(); - return; - } - versionRow.data = versionRow.data === null ? null : JSON.parse(versionRow.data); - versionRow.data_after = versionRow.data_after === null ? null : JSON.parse(versionRow.data_after); - if (!await isRevertable(versionRow)) { - res.status(400).send(); - return; - } - - const entityConf = versioningConfig[versionRow.entity_type]; - - const revertVersionId = await db.versioning(req.user, {type: versionRow.entity_type, id: versionRow.entity_id}, async (trx) => { - if (versionRow.data === null) { // We have creation operation, so we delete records to revert it - for (const relation of entityConf.related) { - await db(relation.type).where(relation.key, versionRow.entity_id).delete().transacting(trx); - } - await db(versionRow.entity_type).where(entityConf.idColumn, versionRow.entity_id).delete().transacting(trx); - } else if (versionRow.data_after === null) { // Deletion operation, so we need to create everything the was deleted - const dataToRestore = versionRow.data; - await db(versionRow.entity_type).insert({...dataToRestore.data, [entityConf.idColumn]: versionRow.entity_id}).transacting(trx); - - for (const relation of entityConf.related) { - const relatedItems = dataToRestore.related[relation.type].map((v: any) => ({...v, [relation.key]: versionRow.entity_id})); - await db.batchInsert(relation.type, relatedItems).transacting(trx); - } - } else { // We have an update operation - const dataToRestore = versionRow.data; - for (const relation of entityConf.related) { - await db(relation.type).where(relation.key, versionRow.entity_id).delete().transacting(trx); - - const relatedItems = dataToRestore.related[relation.type].map((v: any) => ({...v, [relation.key]: versionRow.entity_id})); - await db.batchInsert(relation.type, relatedItems).transacting(trx); - } - await db(versionRow.entity_type).where(entityConf.idColumn, versionRow.entity_id).update(dataToRestore.data).transacting(trx); + try { + const revertVersionId = await versioning.revertOperation(req.user!, parseInt(versionId)); + res.status(200).send({ + status: 'ok', + versionId: revertVersionId, + }); + } catch (err) { + if (err instanceof errors.NonExistingVersionError) { + res.status(404).send(); + return; + } else if (err instanceof errors.NonRevertableError) { + res.status(400).send({ reason: err.data.reason }); + return; } - }); - res.status(200).send({ - status: 'ok', - versionId: revertVersionId, - }); -}; - -async function isRevertable(versionRow: any) { - const lastVersionRow = await db('versioning') - .first('*') - .where(_.pick(versionRow, ['entity_type', 'entity_id'])) - .orderBy('id', 'desc'); - lastVersionRow.data = lastVersionRow.data === null ? null : JSON.parse(lastVersionRow.data); - lastVersionRow.data_after = lastVersionRow.data_after === null ? null : JSON.parse(lastVersionRow.data_after); - - if (lastVersionRow.id === versionRow.id) { - return true; + throw err; } - - if (versionRow.data_after === null) { // Deletion operation - return false; // It's possible to revert deletion operations only if it's the last one for selected entity - } else { // We have creation/update operation - return lastVersionRow.data_after !== null; //It's possible to revert creation/update operations only if the last one is not a deletion one - } -} +}; export default [updateApp]; diff --git a/registry/server/versioning/services/Versioning.ts b/registry/server/versioning/services/Versioning.ts index b14e1925..d368d1d7 100644 --- a/registry/server/versioning/services/Versioning.ts +++ b/registry/server/versioning/services/Versioning.ts @@ -1,7 +1,9 @@ import versioningConfig from '../config'; import Knex, {Transaction} from 'knex'; import {User} from "../../auth"; -import _ from "lodash"; +import _ from 'lodash'; +import * as errors from '../errors'; +import db from "../../db"; export interface OperationConf { type: string; @@ -25,7 +27,7 @@ export class Versioning { * @param callback * @returns - Id of the version record */ - public logOperation = async (user: User, conf: OperationConf, callback: (transaction: Transaction) => Promise) => { + public logOperation = async (user: Express.User|User, conf: OperationConf, callback: (transaction: Transaction) => Promise) => { if (this.config[conf.type] === undefined) { throw new Error(`Attempt to log changes to unknown entity: "${conf.type}"`); } @@ -52,7 +54,7 @@ export class Versioning { entity_id: conf.id, data: JSON.stringify(currentData), data_after: JSON.stringify(newData), - created_by: user.identifier, + created_by: (user as User).identifier, created_at: Math.floor(new Date().getTime() / 1000), }; @@ -61,6 +63,44 @@ export class Versioning { }); } + public async revertOperation(user: Express.User, versionId: number) { + let versionRow = await db('versioning').first('*').where('id', versionId); + if (!versionRow) { + throw new errors.NonExistingVersionError(); + } + versionRow = this.parseVersionData(versionRow); + + await this.checkRevertability(versionRow); + + const entityConf = this.config[versionRow.entity_type]; + + return await this.logOperation(user, {type: versionRow.entity_type, id: versionRow.entity_id}, async (trx) => { + if (versionRow.data === null) { // We have creation operation, so we delete records to revert it + for (const relation of entityConf.related) { + await this.db!(relation.type).where(relation.key, versionRow.entity_id).delete().transacting(trx); + } + await this.db!(versionRow.entity_type).where(entityConf.idColumn, versionRow.entity_id).delete().transacting(trx); + } else if (versionRow.data_after === null) { // Deletion operation, so we need to create everything the was deleted + const dataToRestore = versionRow.data; + await this.db!(versionRow.entity_type).insert({...dataToRestore.data, [entityConf.idColumn]: versionRow.entity_id}).transacting(trx); + + for (const relation of entityConf.related) { + const relatedItems = dataToRestore.related[relation.type].map((v: any) => ({...v, [relation.key]: versionRow.entity_id})); + await this.db!.batchInsert(relation.type, relatedItems).transacting(trx); + } + } else { // We have an update operation + const dataToRestore = versionRow.data; + for (const relation of entityConf.related) { + await this.db!(relation.type).where(relation.key, versionRow.entity_id).delete().transacting(trx); + + const relatedItems = dataToRestore.related[relation.type].map((v: any) => ({...v, [relation.key]: versionRow.entity_id})); + await this.db!.batchInsert(relation.type, relatedItems).transacting(trx); + } + await this.db!(versionRow.entity_type).where(entityConf.idColumn, versionRow.entity_id).update(dataToRestore.data).transacting(trx); + } + }); + } + private async getDataSnapshot(trx: Transaction, conf: OperationConf) { if (!this.db) { throw new Error(`Attempt to log operation before DB initialization!`); @@ -98,6 +138,35 @@ export class Versioning { return res; } + private async checkRevertability(versionRow: any) { + if (!this.db) { + throw new Error(`Attempt to perform operation before DB initialization!`); + } + + let lastVersionRow = await this.db('versioning') + .first('*') + .where(_.pick(versionRow, ['entity_type', 'entity_id'])) + .orderBy('id', 'desc'); + lastVersionRow = this.parseVersionData(lastVersionRow); + + if (lastVersionRow.id === versionRow.id) { + return; + } + + if (versionRow.data_after === null) { // Deletion operation + throw new errors.NonRevertableError({d: {reason: "It's possible to revert deletion operations only if it's the last one for selected entity"}}); + } else if (lastVersionRow.data_after === null) { // We have creation/update operation & last is the delete one + throw new errors.NonRevertableError({d: {reason: "It's possible to revert creation/update operations only if the last one is not a deletion one"}}); + } + } + + private parseVersionData(versionRow: any) { + versionRow.data = versionRow.data === null ? null : JSON.parse(versionRow.data); + versionRow.data_after = versionRow.data_after === null ? null : JSON.parse(versionRow.data_after); + + return versionRow; + } + } export default new Versioning(versioningConfig); diff --git a/registry/typings/@namecheap/index.d.ts b/registry/typings/@namecheap/index.d.ts new file mode 100644 index 00000000..2901ace2 --- /dev/null +++ b/registry/typings/@namecheap/index.d.ts @@ -0,0 +1,24 @@ +declare module '@namecheap/error-extender' { + interface ExtendedErrorConstructConfig { + message?: string; + m?: string; + data?: Partial; + d?: Partial; + cause?: Error; + c?: Error; + } + + export interface ExtendedError extends Error { + new (config?: ExtendedErrorConstructConfig): ExtendedError; + data: DataType; + cause: Error | ExtendedError; + } + + interface ExtendConfig { + defaultMessage?: string; + defaultData?: DataType; + parent?: Error; + } + + export default function (errType: string, config?: ExtendConfig): ExtendedError; +} From 95c2fc0fbfab7c4d9f2c47187da791b122af9ff3 Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Fri, 6 Nov 2020 17:17:13 +0200 Subject: [PATCH 08/23] chore: better typings --- registry/server/versioning/interfaces.ts | 27 +++++++++++++ .../server/versioning/services/Versioning.ts | 38 +++++++++---------- 2 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 registry/server/versioning/interfaces.ts diff --git a/registry/server/versioning/interfaces.ts b/registry/server/versioning/interfaces.ts new file mode 100644 index 00000000..bc357d31 --- /dev/null +++ b/registry/server/versioning/interfaces.ts @@ -0,0 +1,27 @@ + +export enum EntityTypes { + apps = 'apps', + routes = 'routes', +} + +export interface OperationConf { + type: EntityTypes | keyof typeof EntityTypes; + id?: string|number; +} + +export interface VersionRowData { + entity_type: string; + entity_id: string; + data: string|null; + data_after: string|null; + created_by: string; + created_at: number; +} +export interface VersionRow extends VersionRowData { + id: string; +} + +export interface VersionRowParsed extends VersionRow { + data: any|null; + data_after: any|null; +} diff --git a/registry/server/versioning/services/Versioning.ts b/registry/server/versioning/services/Versioning.ts index d368d1d7..94dea365 100644 --- a/registry/server/versioning/services/Versioning.ts +++ b/registry/server/versioning/services/Versioning.ts @@ -2,13 +2,12 @@ import versioningConfig from '../config'; import Knex, {Transaction} from 'knex'; import {User} from "../../auth"; import _ from 'lodash'; -import * as errors from '../errors'; import db from "../../db"; -export interface OperationConf { - type: string; - id?: string|number; -} +import * as errors from '../errors'; +import * as interfaces from '../interfaces'; + +export * from '../interfaces'; export class Versioning { private db?: Knex; @@ -27,7 +26,7 @@ export class Versioning { * @param callback * @returns - Id of the version record */ - public logOperation = async (user: Express.User|User, conf: OperationConf, callback: (transaction: Transaction) => Promise) => { + public logOperation = async (user: Express.User|User, conf: interfaces.OperationConf, callback: (transaction: Transaction) => Promise) => { if (this.config[conf.type] === undefined) { throw new Error(`Attempt to log changes to unknown entity: "${conf.type}"`); } @@ -49,9 +48,9 @@ export class Versioning { } const newData = await this.getDataSnapshot(trx, conf); - const logRecord = { + const logRecord: interfaces.VersionRowData = { entity_type: conf.type, - entity_id: conf.id, + entity_id: conf.id as string, data: JSON.stringify(currentData), data_after: JSON.stringify(newData), created_by: (user as User).identifier, @@ -64,17 +63,17 @@ export class Versioning { } public async revertOperation(user: Express.User, versionId: number) { - let versionRow = await db('versioning').first('*').where('id', versionId); - if (!versionRow) { + let dbRes = await db('versioning').first('*').where('id', versionId); + if (!dbRes) { throw new errors.NonExistingVersionError(); } - versionRow = this.parseVersionData(versionRow); + const versionRow = this.parseVersionData(dbRes); await this.checkRevertability(versionRow); const entityConf = this.config[versionRow.entity_type]; - return await this.logOperation(user, {type: versionRow.entity_type, id: versionRow.entity_id}, async (trx) => { + return await this.logOperation(user, {type: versionRow.entity_type as interfaces.EntityTypes, id: versionRow.entity_id}, async (trx) => { if (versionRow.data === null) { // We have creation operation, so we delete records to revert it for (const relation of entityConf.related) { await this.db!(relation.type).where(relation.key, versionRow.entity_id).delete().transacting(trx); @@ -101,7 +100,7 @@ export class Versioning { }); } - private async getDataSnapshot(trx: Transaction, conf: OperationConf) { + private async getDataSnapshot(trx: Transaction, conf: interfaces.OperationConf) { if (!this.db) { throw new Error(`Attempt to log operation before DB initialization!`); } @@ -143,11 +142,12 @@ export class Versioning { throw new Error(`Attempt to perform operation before DB initialization!`); } - let lastVersionRow = await this.db('versioning') - .first('*') - .where(_.pick(versionRow, ['entity_type', 'entity_id'])) - .orderBy('id', 'desc'); - lastVersionRow = this.parseVersionData(lastVersionRow); + let lastVersionRow = this.parseVersionData( + await this.db('versioning') + .first('*') + .where(_.pick(versionRow, ['entity_type', 'entity_id'])) + .orderBy('id', 'desc') + ) as interfaces.VersionRowParsed; if (lastVersionRow.id === versionRow.id) { return; @@ -160,7 +160,7 @@ export class Versioning { } } - private parseVersionData(versionRow: any) { + private parseVersionData(versionRow: interfaces.VersionRow): interfaces.VersionRowParsed { versionRow.data = versionRow.data === null ? null : JSON.parse(versionRow.data); versionRow.data_after = versionRow.data_after === null ? null : JSON.parse(versionRow.data_after); From e6b173519956e02792871c03b99a9e8412467d87 Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Fri, 6 Nov 2020 17:41:45 +0200 Subject: [PATCH 09/23] Basic UI implementation --- registry/client/src/index.js | 2 + registry/client/src/versioning/List.js | 75 +++++++++++++++++++ .../client/src/versioning/dataTransform.js | 7 ++ registry/client/src/versioning/index.js | 8 ++ 4 files changed, 92 insertions(+) create mode 100644 registry/client/src/versioning/List.js create mode 100644 registry/client/src/versioning/dataTransform.js create mode 100644 registry/client/src/versioning/index.js diff --git a/registry/client/src/index.js b/registry/client/src/index.js index 36483b81..208839bb 100755 --- a/registry/client/src/index.js +++ b/registry/client/src/index.js @@ -13,6 +13,7 @@ import templates from './templates'; import appRoutes from './appRoutes'; import authEntities from './authEntities'; import settings from './settings'; +import versioning from './versioning'; render( , , , + , ]} , document.getElementById('root') diff --git a/registry/client/src/versioning/List.js b/registry/client/src/versioning/List.js new file mode 100644 index 00000000..d87217b8 --- /dev/null +++ b/registry/client/src/versioning/List.js @@ -0,0 +1,75 @@ +import React, {Children, cloneElement} from 'react'; +import {useMediaQuery, makeStyles} from '@material-ui/core'; +import { + List, + Datagrid, + EditButton, + SimpleList, + TextField, + SelectInput, + TextInput, + Filter, +} from 'react-admin'; + +const ListActionsToolbar = ({children, ...props}) => { + const classes = makeStyles({ + toolbar: { + alignItems: 'center', + display: 'flex', + marginTop: -1, + marginBottom: -1, + }, + }); + + return ( +
+ {Children.map(children, button => cloneElement(button, props))} +
+ ); +}; + +const MyFilter = (props) => ( + + + + +); + + +const MyPanel = ({ id, record, resource }) => ( +
+); + +const PostList = props => { + const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm')); + return ( + } + exporter={false} + perPage={25} + bulkActionButtons={false} + > + {isSmall + ? ( + record.key} + secondaryText={record => record.value} + /> + ) : ( + } optimized> + + + + + + + + + + )} + + ); +}; + +export default PostList; diff --git a/registry/client/src/versioning/dataTransform.js b/registry/client/src/versioning/dataTransform.js new file mode 100644 index 00000000..d5369c7c --- /dev/null +++ b/registry/client/src/versioning/dataTransform.js @@ -0,0 +1,7 @@ +export function transformGet(setting) { + +} + +export function transformSet(setting) { + +} diff --git a/registry/client/src/versioning/index.js b/registry/client/src/versioning/index.js new file mode 100644 index 00000000..f2e53f43 --- /dev/null +++ b/registry/client/src/versioning/index.js @@ -0,0 +1,8 @@ +import Icon from '@material-ui/icons/History'; + +import List from './List'; + +export default { + list: List, + icon: Icon, +}; From 2ed616eef4ef57e7d46ca2928265ebe366794960 Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Fri, 6 Nov 2020 21:55:53 +0200 Subject: [PATCH 10/23] feat: versioning UI enhancements --- registry/client/package-lock.json | 250 ++++++++++++++++-- registry/client/package.json | 1 + registry/client/src/utils/json.js | 32 +++ registry/client/src/versioning/List.js | 107 ++++++-- registry/client/src/versioning/index.js | 2 + .../server/versioning/routes/getVersions.ts | 4 +- .../server/versioning/services/Versioning.ts | 4 +- 7 files changed, 348 insertions(+), 52 deletions(-) create mode 100644 registry/client/src/utils/json.js diff --git a/registry/client/package-lock.json b/registry/client/package-lock.json index 74c4ef00..4f5a318d 100644 --- a/registry/client/package-lock.json +++ b/registry/client/package-lock.json @@ -33,7 +33,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", - "dev": true, "requires": { "@babel/highlight": "^7.8.3" } @@ -243,7 +242,6 @@ "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz", "integrity": "sha512-R0Bx3jippsbAEtzkpZ/6FIiuzOURPcMjHp+Z6xPe6DtApDJx+w7UYyOLanZqO8+wKR9G10s/FmHXvxaMd9s6Kg==", - "dev": true, "requires": { "@babel/types": "^7.8.3" } @@ -334,8 +332,7 @@ "@babel/helper-validator-identifier": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", - "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==", - "dev": true + "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==" }, "@babel/helper-wrap-function": { "version": "7.8.3", @@ -364,7 +361,6 @@ "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", @@ -1117,18 +1113,70 @@ "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.0.tgz", "integrity": "sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==", - "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.9.0", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" } }, + "@emotion/cache": { + "version": "10.0.29", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", + "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "requires": { + "@emotion/sheet": "0.9.4", + "@emotion/stylis": "0.8.5", + "@emotion/utils": "0.11.3", + "@emotion/weak-memoize": "0.2.5" + } + }, "@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" }, + "@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + }, + "@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "requires": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "@emotion/sheet": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", + "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==" + }, + "@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==" + }, + "@emotion/weak-memoize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + }, "@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -1424,6 +1472,11 @@ "integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA==", "dev": true }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + }, "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", @@ -1979,6 +2032,38 @@ "object.assign": "^4.1.0" } }, + "babel-plugin-emotion": { + "version": "10.0.33", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz", + "integrity": "sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/serialize": "^0.11.16", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + }, + "babel-plugin-macros": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", + "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", + "requires": { + "@babel/runtime": "^7.7.2", + "cosmiconfig": "^6.0.0", + "resolve": "^1.12.0" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -2389,6 +2474,11 @@ "unset-value": "^1.0.0" } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, "camel-case": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", @@ -2415,7 +2505,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -2702,7 +2791,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -3047,6 +3135,18 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -3057,6 +3157,17 @@ "elliptic": "^6.0.0" } }, + "create-emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-10.0.27.tgz", + "integrity": "sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==", + "requires": { + "@emotion/cache": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + } + }, "create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -3360,6 +3471,11 @@ "resolved": "https://registry.npmjs.org/diacritic/-/diacritic-0.0.2.tgz", "integrity": "sha1-/CqIe1pbwKCoVPthTHwvIJBh7gQ=" }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -3556,6 +3672,15 @@ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true }, + "emotion": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/emotion/-/emotion-10.0.27.tgz", + "integrity": "sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==", + "requires": { + "babel-plugin-emotion": "^10.0.27", + "create-emotion": "^10.0.27" + } + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -3609,6 +3734,14 @@ "prr": "~1.0.1" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-abstract": { "version": "1.17.5", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", @@ -3657,8 +3790,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint-scope": { "version": "4.0.3", @@ -4182,6 +4314,11 @@ "pkg-dir": "^3.0.0" } }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -5052,8 +5189,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.1", @@ -5421,6 +5557,22 @@ "integrity": "sha512-QPMrUVdhTXUII2xcq1pGqXJvIz7qb77TlY9eejQdZJaE2bKMBMBtjkVbDTgovieV591tfZ9fSr6ejCWxzX/gzw==", "dev": true }, + "import-fresh": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.2.tgz", + "integrity": "sha512-cTPNrlvJT6twpYy+YmKUKrTSjWFs3bjYjAhCwm+z4EOCubZxAuO+hHpRN64TqjEaYSHs7tJAE0w1CKMGmsG/lw==", + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + } + } + }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -5563,6 +5715,11 @@ "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", "dev": true }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -5825,6 +5982,11 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -6015,6 +6177,11 @@ "leven": "^3.1.0" } }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + }, "loader-runner": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", @@ -6163,6 +6330,11 @@ } } }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -6941,6 +7113,14 @@ "no-case": "^2.2.0" } }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "requires": { + "callsites": "^3.0.0" + } + }, "parse-asn1": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.5.tgz", @@ -6955,6 +7135,17 @@ "safe-buffer": "^5.1.1" } }, + "parse-json": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.1.0.tgz", + "integrity": "sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", @@ -7012,8 +7203,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-to-regexp": { "version": "1.8.0", @@ -7026,8 +7216,7 @@ "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, "pbkdf2": { "version": "3.0.17", @@ -7583,6 +7772,19 @@ "redux-saga": "^1.0.0" } }, + "react-diff-viewer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/react-diff-viewer/-/react-diff-viewer-3.1.1.tgz", + "integrity": "sha512-rmvwNdcClp6ZWdS11m1m01UnBA4OwYaLG/li0dB781e/bQEzsGyj+qewVd6W5ztBwseQ72pO7nwaCcq5jnlzcw==", + "requires": { + "classnames": "^2.2.6", + "create-emotion": "^10.0.14", + "diff": "^4.0.1", + "emotion": "^10.0.14", + "memoize-one": "^5.0.4", + "prop-types": "^15.6.2" + } + }, "react-dom": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", @@ -7985,7 +8187,6 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", - "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -8100,8 +8301,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex": { "version": "1.1.0", @@ -8494,8 +8694,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-resolve": { "version": "0.5.3", @@ -8911,7 +9110,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -9066,8 +9264,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, "to-object-path": { "version": "0.3.0", @@ -10011,6 +10208,11 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "yaml": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", + "integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==" + }, "yargs": { "version": "13.2.4", "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", diff --git a/registry/client/package.json b/registry/client/package.json index 9b503a2f..2f283022 100755 --- a/registry/client/package.json +++ b/registry/client/package.json @@ -44,6 +44,7 @@ "ra-data-simple-rest": "^3.9.3", "react": "^16.9.0", "react-admin": "^3.9.5", + "react-diff-viewer": "^3.1.1", "react-dom": "^16.9.0" } } diff --git a/registry/client/src/utils/json.js b/registry/client/src/utils/json.js new file mode 100644 index 00000000..b745ec1c --- /dev/null +++ b/registry/client/src/utils/json.js @@ -0,0 +1,32 @@ + +import _ from 'lodash/fp'; + +/** + * Original source code was taken from {@link https://github.com/prototypejs/prototype/blob/5fddd3e/src/prototype/lang/string.js#L702} + */ +const isJSON = (str) => { + if (/^\s*$/.test(str)) return false; + + str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'); + str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'); + str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, ''); + + return (/^[\],:{}\s]*$/).test(str); +}; + +const parse = (value) => { + console.log(value); + if (_.isString(value) && isJSON(value)) { + return JSON.parse(value); + } + + return value; +} + +export function parseJSON(value) { + return _.cond([ + [_.isArray, _.map(_.mapValues(parseJSON))], + [_.isObject, _.mapValues(parseJSON)], + [_.stubTrue, parse] + ])(value); +}; diff --git a/registry/client/src/versioning/List.js b/registry/client/src/versioning/List.js index d87217b8..7561b8b7 100644 --- a/registry/client/src/versioning/List.js +++ b/registry/client/src/versioning/List.js @@ -1,15 +1,20 @@ -import React, {Children, cloneElement} from 'react'; -import {useMediaQuery, makeStyles} from '@material-ui/core'; +import React, {Children, cloneElement, useState, useCallback} from 'react'; +import {makeStyles} from '@material-ui/core'; +import ReactDiffViewer from 'react-diff-viewer'; import { List, Datagrid, - EditButton, - SimpleList, + Button, TextField, SelectInput, TextInput, Filter, + FunctionField, + useRefresh, + useNotify } from 'react-admin'; +import {parseJSON} from '../utils/json'; +import {SettingsBackupRestore} from "@material-ui/icons"; const ListActionsToolbar = ({children, ...props}) => { const classes = makeStyles({ @@ -32,42 +37,96 @@ const MyFilter = (props) => ( + ); +const beautifyJson = v => JSON.stringify(parseJSON(JSON.parse(v)), null, 2); + const MyPanel = ({ id, record, resource }) => ( -
+ ); +const RevertButton = ({ + record, + ...rest +}) => { + const [disabled, setDisabled] = useState(false); + const refresh = useRefresh(); + const notify = useNotify(); + + const handleClick = useCallback(() => { + if (confirm(`Are you sure that you want to revert change with ID "${record.id}"?`)) { + setDisabled(true); + fetch(`/api/v1/versioning/${record.id}/revert`, {method: 'POST'}).then(async res => { + setDisabled(false); + if (!res.ok) { + if (res.status < 500) { + const resInfo = await res.json(); + return notify(resInfo.reason, 'error', { smart_count: 1 }); + } + throw new Error(`Unexpected network error. Returned code "${res.status}"`); + } + notify('Change was successfully reverted', 'info', { smart_count: 1 }); + refresh(); + }).catch(err => { + setDisabled(false); + notify('Oops! Something went wrong.', 'error', { smart_count: 1 }); + console.error(err); + }); + } + }, []); + + return ( + + ); +}; + const PostList = props => { - const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm')); return ( } exporter={false} perPage={25} bulkActionButtons={false} > - {isSmall - ? ( - record.key} - secondaryText={record => record.value} - /> - ) : ( - } optimized> - - - - - - - - - - )} + } > + + + + record.data && record.data_after ? 'UPDATE' : record.data ? 'DELETE' : 'CREATE'} /> + + new Date(record.created_at * 1000).toLocaleString()} /> + + + + ); }; diff --git a/registry/client/src/versioning/index.js b/registry/client/src/versioning/index.js index f2e53f43..d3a24a81 100644 --- a/registry/client/src/versioning/index.js +++ b/registry/client/src/versioning/index.js @@ -1,8 +1,10 @@ import Icon from '@material-ui/icons/History'; +import React, {Children, cloneElement} from 'react'; import List from './List'; export default { list: List, icon: Icon, + options: {label: 'History'}, }; diff --git a/registry/server/versioning/routes/getVersions.ts b/registry/server/versioning/routes/getVersions.ts index b502de51..315b167e 100644 --- a/registry/server/versioning/routes/getVersions.ts +++ b/registry/server/versioning/routes/getVersions.ts @@ -13,8 +13,8 @@ const getVersions = async (req: Request, res: Response): Promise => { if (filters.id) { query.whereIn('name', [...filters.id]); } - if (filters.entity_type || filters.entity_id) { - query.where(_.pick(filters, ['entity_type', 'entity_id'])); + if (filters.entity_type || filters.entity_id || filters.created_by) { + query.where(_.pick(filters, ['entity_type', 'entity_id', 'created_by'])); } const dbRes = await query.range(req.query.range as string | undefined); diff --git a/registry/server/versioning/services/Versioning.ts b/registry/server/versioning/services/Versioning.ts index 94dea365..4a1c5fac 100644 --- a/registry/server/versioning/services/Versioning.ts +++ b/registry/server/versioning/services/Versioning.ts @@ -51,8 +51,8 @@ export class Versioning { const logRecord: interfaces.VersionRowData = { entity_type: conf.type, entity_id: conf.id as string, - data: JSON.stringify(currentData), - data_after: JSON.stringify(newData), + data: currentData === null ? null : JSON.stringify(currentData), + data_after: newData === null ? null : JSON.stringify(newData), created_by: (user as User).identifier, created_at: Math.floor(new Date().getTime() / 1000), }; From a3f887435fe014a79d09175e4f6a84d3a984e3ff Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Mon, 9 Nov 2020 12:54:53 +0200 Subject: [PATCH 11/23] chore: debug logging removed --- registry/client/src/utils/json.js | 1 - 1 file changed, 1 deletion(-) diff --git a/registry/client/src/utils/json.js b/registry/client/src/utils/json.js index b745ec1c..70319d01 100644 --- a/registry/client/src/utils/json.js +++ b/registry/client/src/utils/json.js @@ -15,7 +15,6 @@ const isJSON = (str) => { }; const parse = (value) => { - console.log(value); if (_.isString(value) && isJSON(value)) { return JSON.parse(value); } From 27ad46a3d314830b9f8764fdb0281d704932d439 Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Mon, 9 Nov 2020 14:23:11 +0200 Subject: [PATCH 12/23] chore: Fixed authEntity password update from UI --- .../client/src/authEntities/dataTransform.js | 11 +++++++++ registry/client/src/dataProvider.js | 23 ++++++++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 registry/client/src/authEntities/dataTransform.js diff --git a/registry/client/src/authEntities/dataTransform.js b/registry/client/src/authEntities/dataTransform.js new file mode 100644 index 00000000..7572e672 --- /dev/null +++ b/registry/client/src/authEntities/dataTransform.js @@ -0,0 +1,11 @@ +export function transformGet(entity) { + +} + +export function transformSet(entity, operation) { + if (operation === 'update') { + delete entity.id; + delete entity.identifier; + delete entity.provider; + } +} diff --git a/registry/client/src/dataProvider.js b/registry/client/src/dataProvider.js index 8ca31080..5bbbd050 100755 --- a/registry/client/src/dataProvider.js +++ b/registry/client/src/dataProvider.js @@ -6,6 +6,7 @@ import * as apps from './apps/dataTransform'; import * as templates from './templates/dataTransform'; import * as appRoutes from './appRoutes/dataTransform'; import * as settings from './settings/dataTransform'; +import * as authEntities from './authEntities/dataTransform'; import httpClient from './httpClient'; @@ -42,7 +43,7 @@ const myDataProvider = { update: (resource, params) => { params.id = encodeURIComponent(params.id); - transformSetter(resource, params.data); + transformSetter(resource, params.data, 'update'); delete params.data.name; return dataProvider.update(resource, params).then(v => { @@ -54,7 +55,7 @@ const myDataProvider = { transformSetter(resource, params.data); return dataProvider.create(resource, params).then(v => { - transformGetter(resource, v.data); + transformGetter(resource, v.data, 'create'); return v; }); }, @@ -94,26 +95,32 @@ function transformGetter(resource, data) { case 'settings': settings.transformGet(data); break; + case 'auth_entities': + authEntities.transformGet(data); + break; default: } } -function transformSetter(resource, data) { +function transformSetter(resource, data, operation) { switch (resource) { case 'app': - apps.transformSet(data); + apps.transformSet(data, operation); break; case 'shared_props': - sharedProps.transformSet(data); + sharedProps.transformSet(data, operation); break; case 'template': - templates.transformSet(data); + templates.transformSet(data, operation); break; case 'route': - appRoutes.transformSet(data); + appRoutes.transformSet(data, operation); break; case 'settings': - settings.transformSet(data); + settings.transformSet(data, operation); + break; + case 'auth_entities': + authEntities.transformSet(data, operation); break; default: } From 5996542bed11c9a3efc9aa45651a011d70242784 Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Mon, 9 Nov 2020 14:23:46 +0200 Subject: [PATCH 13/23] feat: added versioning for auth entities --- registry/server/appRoutes/routes/deleteAppRoute.ts | 5 +++-- registry/server/authEntities/routes/create.ts | 9 +++++++-- registry/server/authEntities/routes/deleteRoute.ts | 5 ++++- registry/server/authEntities/routes/update.ts | 5 ++++- registry/server/versioning/config.ts | 6 +++++- registry/server/versioning/interfaces.ts | 1 + 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/registry/server/appRoutes/routes/deleteAppRoute.ts b/registry/server/appRoutes/routes/deleteAppRoute.ts index 35714f79..a3b6dba8 100644 --- a/registry/server/appRoutes/routes/deleteAppRoute.ts +++ b/registry/server/appRoutes/routes/deleteAppRoute.ts @@ -25,9 +25,10 @@ const validateRequestBeforeDeleteAppRoute = validateRequestFactory([{ const deleteAppRoute = async (req: Request, res: Response) => { const appRouteId = req.params.id; - const count = await db.transaction(async (transaction) => { + let count; + await db.versioning(req.user, {type: 'routes', id: appRouteId}, async (transaction) => { await db('route_slots').where('routeId', appRouteId).delete().transacting(transaction); - return await db('routes').where('id', appRouteId).delete().transacting(transaction); + await db('routes').where('id', appRouteId).delete().transacting(transaction); }); if (count) { diff --git a/registry/server/authEntities/routes/create.ts b/registry/server/authEntities/routes/create.ts index e7811f6a..3693964c 100644 --- a/registry/server/authEntities/routes/create.ts +++ b/registry/server/authEntities/routes/create.ts @@ -23,8 +23,13 @@ const createSharedProps = async (req: Request, res: Response): Promise => input.secret = await bcrypt.hash(input.secret, await bcrypt.genSalt()); } - const [recordId] = await db('auth_entities').insert(req.body); - const [savedRecord] = await db.select().from('auth_entities').where('id', recordId); + let recordId: number; + await db.versioning(req.user, {type: 'auth_entities'}, async (trx) => { + [recordId] = await db('auth_entities').insert(req.body).transacting(trx); + return recordId; + }); + + const [savedRecord] = await db.select().from('auth_entities').where('id', recordId!); delete savedRecord.secret; res.status(200).send(preProcessResponse(savedRecord)); diff --git a/registry/server/authEntities/routes/deleteRoute.ts b/registry/server/authEntities/routes/deleteRoute.ts index 6cd93e0a..0c2a6c2a 100644 --- a/registry/server/authEntities/routes/deleteRoute.ts +++ b/registry/server/authEntities/routes/deleteRoute.ts @@ -20,7 +20,10 @@ const validateRequest = validateRequestFactory([{ }]); const deleteRecord = async (req: Request, res: Response): Promise => { - const count = await db('auth_entities').where('id', req.params.id).delete(); + let count; + await db.versioning(req.user, {type: 'auth_entities', id: req.params.id}, async (trx) => { + count = await db('auth_entities').where('id', req.params.id).delete().transacting(trx); + }); if (count) { res.status(204).send(); diff --git a/registry/server/authEntities/routes/update.ts b/registry/server/authEntities/routes/update.ts index e49ec583..2b759098 100644 --- a/registry/server/authEntities/routes/update.ts +++ b/registry/server/authEntities/routes/update.ts @@ -44,7 +44,10 @@ const updateSharedProps = async (req: Request, res: Response): Pr input.secret = await bcrypt.hash(input.secret, await bcrypt.genSalt()); } - await db('auth_entities').where({ id: recordId }).update(input); + await db.versioning(req.user, {type: 'auth_entities', id: recordId}, async (trx) => { + await db('auth_entities').where({ id: recordId }).update(input).transacting(trx); + }); + const [updatedRecord] = await db.select().from('auth_entities').where('id', recordId); delete updatedRecord.secret; diff --git a/registry/server/versioning/config.ts b/registry/server/versioning/config.ts index fe6af971..b8d55a50 100644 --- a/registry/server/versioning/config.ts +++ b/registry/server/versioning/config.ts @@ -18,7 +18,11 @@ const versioningConf: Record = { routes: { idColumn: 'id', related: [{ type: 'route_slots', idColumn: 'id', key: 'routeId' }], - } + }, + auth_entities: { + idColumn: 'id', + related: [], + }, }; export default versioningConf; diff --git a/registry/server/versioning/interfaces.ts b/registry/server/versioning/interfaces.ts index bc357d31..7b31c899 100644 --- a/registry/server/versioning/interfaces.ts +++ b/registry/server/versioning/interfaces.ts @@ -2,6 +2,7 @@ export enum EntityTypes { apps = 'apps', routes = 'routes', + auth_entities = 'auth_entities', } export interface OperationConf { From 7b58b68c2f8ba6c0aac548cede9c884069d52218 Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Mon, 9 Nov 2020 14:39:12 +0200 Subject: [PATCH 14/23] feat: added versioning for settings --- registry/server/settings/routes/updateSetting.ts | 12 ++++++++++-- registry/server/versioning/config.ts | 13 +++++++++---- registry/server/versioning/interfaces.ts | 1 + registry/server/versioning/services/Versioning.ts | 6 +++++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/registry/server/settings/routes/updateSetting.ts b/registry/server/settings/routes/updateSetting.ts index a4806f0e..4bbed26a 100644 --- a/registry/server/settings/routes/updateSetting.ts +++ b/registry/server/settings/routes/updateSetting.ts @@ -32,8 +32,16 @@ const validateRequest = validateRequestFactory([ ]); const updateSetting = async (req: Request, res: Response): Promise => { - await db('settings').where('key', req.params.key).update('value', JSON.stringify(req.body.value)); - const [updated] = await db.select().from('settings').where('key', req.params.key); + const settingKey = req.params.key; + + await db.versioning(req.user, {type: 'settings', id: settingKey}, async (trx) => { + await db('settings') + .where('key', settingKey) + .update('value', JSON.stringify(req.body.value)) + .transacting(trx); + }); + + const [updated] = await db.select().from('settings').where('key', settingKey); res.status(200).send(preProcessResponse(updated)); }; diff --git a/registry/server/versioning/config.ts b/registry/server/versioning/config.ts index b8d55a50..4002302a 100644 --- a/registry/server/versioning/config.ts +++ b/registry/server/versioning/config.ts @@ -1,3 +1,4 @@ +import {EntityTypes} from './interfaces'; interface RelatedEntities { type: string; @@ -10,19 +11,23 @@ interface EntityConf { related: RelatedEntities[], } -const versioningConf: Record = { - apps: { +const versioningConf: Record = { + [EntityTypes.apps]: { idColumn: 'name', related: [], }, - routes: { + [EntityTypes.routes]: { idColumn: 'id', related: [{ type: 'route_slots', idColumn: 'id', key: 'routeId' }], }, - auth_entities: { + [EntityTypes.auth_entities]: { idColumn: 'id', related: [], }, + [EntityTypes.settings]: { + idColumn: 'key', + related: [], + }, }; export default versioningConf; diff --git a/registry/server/versioning/interfaces.ts b/registry/server/versioning/interfaces.ts index 7b31c899..9cd403cd 100644 --- a/registry/server/versioning/interfaces.ts +++ b/registry/server/versioning/interfaces.ts @@ -3,6 +3,7 @@ export enum EntityTypes { apps = 'apps', routes = 'routes', auth_entities = 'auth_entities', + settings = 'settings', } export interface OperationConf { diff --git a/registry/server/versioning/services/Versioning.ts b/registry/server/versioning/services/Versioning.ts index 4a1c5fac..823883f2 100644 --- a/registry/server/versioning/services/Versioning.ts +++ b/registry/server/versioning/services/Versioning.ts @@ -6,6 +6,7 @@ import db from "../../db"; import * as errors from '../errors'; import * as interfaces from '../interfaces'; +import {EntityTypes} from "../interfaces"; export * from '../interfaces'; @@ -71,7 +72,10 @@ export class Versioning { await this.checkRevertability(versionRow); - const entityConf = this.config[versionRow.entity_type]; + const entityConf = this.config[versionRow.entity_type as EntityTypes]; + if (entityConf === undefined) { + throw new Error(`Attempt to revert operation for unknown entity type: "${versionRow.entity_type}"`); + } return await this.logOperation(user, {type: versionRow.entity_type as interfaces.EntityTypes, id: versionRow.entity_id}, async (trx) => { if (versionRow.data === null) { // We have creation operation, so we delete records to revert it From d2f2414b54649e3eda4a7ae73037549c10332569 Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Mon, 9 Nov 2020 14:44:59 +0200 Subject: [PATCH 15/23] feat: added versioning for shared_props --- registry/server/settings/routes/updateSetting.ts | 2 +- registry/server/sharedProps/routes/createSharedProps.ts | 5 ++++- registry/server/sharedProps/routes/deleteSharedProps.ts | 5 ++++- registry/server/sharedProps/routes/updateSharedProps.ts | 8 +++++++- registry/server/versioning/config.ts | 4 ++++ registry/server/versioning/interfaces.ts | 1 + 6 files changed, 21 insertions(+), 4 deletions(-) diff --git a/registry/server/settings/routes/updateSetting.ts b/registry/server/settings/routes/updateSetting.ts index 4bbed26a..23cf3fc0 100644 --- a/registry/server/settings/routes/updateSetting.ts +++ b/registry/server/settings/routes/updateSetting.ts @@ -40,7 +40,7 @@ const updateSetting = async (req: Request, res: Response): Promis .update('value', JSON.stringify(req.body.value)) .transacting(trx); }); - + const [updated] = await db.select().from('settings').where('key', settingKey); res.status(200).send(preProcessResponse(updated)); }; diff --git a/registry/server/sharedProps/routes/createSharedProps.ts b/registry/server/sharedProps/routes/createSharedProps.ts index 6b4510a8..980189a0 100644 --- a/registry/server/sharedProps/routes/createSharedProps.ts +++ b/registry/server/sharedProps/routes/createSharedProps.ts @@ -18,7 +18,10 @@ const validateRequest = validateRequestFactory([{ const createSharedProps = async (req: Request, res: Response): Promise => { const sharedProps = req.body; - await db('shared_props').insert(stringifyJSON(['props'], sharedProps)); + await db.versioning(req.user, {type: 'shared_props', id: sharedProps.name}, async (trx) => { + await db('shared_props').insert(stringifyJSON(['props'], sharedProps)).transacting(trx); + }); + const [savedSharedProps] = await db.select().from('shared_props').where('name', sharedProps.name); res.status(200).send(preProcessResponse(savedSharedProps)); diff --git a/registry/server/sharedProps/routes/deleteSharedProps.ts b/registry/server/sharedProps/routes/deleteSharedProps.ts index ac453db0..6a0fb34a 100644 --- a/registry/server/sharedProps/routes/deleteSharedProps.ts +++ b/registry/server/sharedProps/routes/deleteSharedProps.ts @@ -21,7 +21,10 @@ const validateRequest = validateRequestFactory([{ }]); const deleteSharedProps = async (req: Request, res: Response): Promise => { - const count = await db('shared_props').where('name', req.params.name).delete(); + let count; + await db.versioning(req.user, {type: 'shared_props', id: req.params.name}, async (trx) => { + count = await db('shared_props').where('name', req.params.name).delete().transacting(trx); + }); if (count) { res.status(204).send(); diff --git a/registry/server/sharedProps/routes/updateSharedProps.ts b/registry/server/sharedProps/routes/updateSharedProps.ts index da11e8d6..598265cf 100644 --- a/registry/server/sharedProps/routes/updateSharedProps.ts +++ b/registry/server/sharedProps/routes/updateSharedProps.ts @@ -41,7 +41,13 @@ const updateSharedProps = async (req: Request, res: Response): Pr return; } - await db('shared_props').where({ name: sharedPropsName }).update(stringifyJSON(['props'], sharedProps)); + await db.versioning(req.user, {type: 'shared_props', id: sharedPropsName}, async (trx) => { + await db('shared_props') + .where({ name: sharedPropsName }) + .update(stringifyJSON(['props'], sharedProps)) + .transacting(trx); + }); + const [updatedSharedProps] = await db.select().from('shared_props').where('name', sharedPropsName); res.status(200).send(preProcessResponse(updatedSharedProps)); diff --git a/registry/server/versioning/config.ts b/registry/server/versioning/config.ts index 4002302a..e23aed15 100644 --- a/registry/server/versioning/config.ts +++ b/registry/server/versioning/config.ts @@ -28,6 +28,10 @@ const versioningConf: Record idColumn: 'key', related: [], }, + [EntityTypes.shared_props]: { + idColumn: 'name', + related: [], + }, }; export default versioningConf; diff --git a/registry/server/versioning/interfaces.ts b/registry/server/versioning/interfaces.ts index 9cd403cd..08ee6ff6 100644 --- a/registry/server/versioning/interfaces.ts +++ b/registry/server/versioning/interfaces.ts @@ -4,6 +4,7 @@ export enum EntityTypes { routes = 'routes', auth_entities = 'auth_entities', settings = 'settings', + shared_props = 'shared_props', } export interface OperationConf { From 6e67224956079bfcca7d2ce879f2cbced18f82ab Mon Sep 17 00:00:00 2001 From: Vladlen Fedosov Date: Mon, 9 Nov 2020 14:50:01 +0200 Subject: [PATCH 16/23] feat: added versioning for templates --- registry/server/templates/routes/createTemplate.ts | 4 +++- registry/server/templates/routes/deleteTemplate.ts | 5 ++++- registry/server/templates/routes/updateTemplate.ts | 4 +++- registry/server/versioning/config.ts | 4 ++++ registry/server/versioning/interfaces.ts | 1 + 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/registry/server/templates/routes/createTemplate.ts b/registry/server/templates/routes/createTemplate.ts index a1167916..4844cbdb 100644 --- a/registry/server/templates/routes/createTemplate.ts +++ b/registry/server/templates/routes/createTemplate.ts @@ -18,7 +18,9 @@ const validateRequestBeforeCreateTemplate = validateRequestFactory([{ const createTemplate = async (req: Request, res: Response): Promise => { const template = req.body; - await db('templates').insert(template); + await db.versioning(req.user, {type: 'templates', id: template.name}, async (trx) => { + await db('templates').insert(template).transacting(trx); + }); const [savedTemplate] = await db.select().from