Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/versioning #225

Merged
merged 23 commits into from
Nov 11, 2020
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 226 additions & 24 deletions registry/client/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions registry/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
11 changes: 11 additions & 0 deletions registry/client/src/authEntities/dataTransform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export function transformGet(entity) {

}

export function transformSet(entity, operation) {
if (operation === 'update') {
Copy link
Contributor

Choose a reason for hiding this comment

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

In my opinion, it would be nice to have one object with operations and use it everywhere
because as I see operations are copy/pasted in each of the files

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

delete entity.id;
delete entity.identifier;
delete entity.provider;
}
}
23 changes: 15 additions & 8 deletions registry/client/src/dataProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 => {
Expand All @@ -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;
});
},
Expand Down Expand Up @@ -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:
}
Expand Down
2 changes: 2 additions & 0 deletions registry/client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Admin
Expand All @@ -29,6 +30,7 @@ render(
<Resource name="route" {...appRoutes} />,
<Resource name="auth_entities" {...authEntities} />,
<Resource name="settings" {...settings} />,
<Resource name="versioning" {...versioning} />,
]}
</Admin>,
document.getElementById('root')
Expand Down
31 changes: 31 additions & 0 deletions registry/client/src/utils/json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

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) => {
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);
};
134 changes: 134 additions & 0 deletions registry/client/src/versioning/List.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import React, {Children, cloneElement, useState, useCallback} from 'react';
import {makeStyles} from '@material-ui/core';
import ReactDiffViewer from 'react-diff-viewer';
import {
List,
Datagrid,
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({
toolbar: {
alignItems: 'center',
display: 'flex',
marginTop: -1,
marginBottom: -1,
},
});

return (
<div className={classes.toolbar}>
{Children.map(children, button => cloneElement(button, props))}
</div>
);
};

const MyFilter = (props) => (
<Filter {...props}>
<SelectInput label="Entity Type" source="entity_type" choices={[{id: 'apps', name: 'Apps'}, {id: 'routes', name: 'Routes'}]} alwaysOn/>
<TextInput label="Entity ID" source="entity_id" alwaysOn />
<TextInput label="Created by" source="created_by" alwaysOn />
</Filter>
);

const beautifyJson = v => JSON.stringify(parseJSON(JSON.parse(v)), null, 2);


const MyPanel = ({ id, record, resource }) => (
<ReactDiffViewer
oldValue={beautifyJson(record.data || record.data_after)}
newValue={beautifyJson(record.data_after || record.data)}
splitView={false}
showDiffOnly={false}
compareMethod={'diffWords'}
styles={{
diffContainer: {
pre: {
lineHeight: '1',
},
},
gutter: {
minWidth: 0,
}
}}
/>
);

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 (
<Button
label="Revert"
disabled={disabled}
onClick={handleClick}
{...rest}
>
<SettingsBackupRestore/>
</Button>
);
};

const PostList = props => {
return (
<List
{...props}
title="History"
filters={<MyFilter/>}
exporter={false}
perPage={25}
bulkActionButtons={false}
>
<Datagrid expand={<MyPanel />} >
<TextField sortable={false} source="id" />
<TextField sortable={false} source="entity_type" />
<TextField sortable={false} source="entity_id" />
<FunctionField label="Operation" render={record => record.data && record.data_after ? 'UPDATE' : record.data ? 'DELETE' : 'CREATE'} />
Copy link
Contributor

Choose a reason for hiding this comment

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

So if you create an object that I mentioned here you could use it here with toUpperCase()

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that using here the same variable is a little bit incorrect since it's View with simple textual information which can be changed. So it's better to not mix presentation and logic inside a single variable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I agree with that

<TextField sortable={false} source="created_by" />
<FunctionField label="Created At" render={record => new Date(record.created_at * 1000).toLocaleString()} />
<ListActionsToolbar>
<RevertButton />
</ListActionsToolbar>
</Datagrid>
</List>
);
};

export default PostList;
7 changes: 7 additions & 0 deletions registry/client/src/versioning/dataTransform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function transformGet(setting) {

}

export function transformSet(setting) {

}
10 changes: 10 additions & 0 deletions registry/client/src/versioning/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +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'},
};
3 changes: 2 additions & 1 deletion registry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@
"supertest": "4.0.2",
"timekeeper": "^2.2.0",
"ts-node": "^8.10.2",
"typescript": "3.7.5"
"typescript": "^3.9.7"
},
"dependencies": {
"@namecheap/error-extender": "^1.2.0",
"axios": "^0.19.0",
"bcrypt": "^5.0.0",
"body-parser": "^1.19.0",
Expand Down
1 change: 1 addition & 0 deletions registry/server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 7 additions & 5 deletions registry/server/appRoutes/routes/createAppRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,27 @@ const createAppRoute = async (req: Request, res: Response) => {
...appRoute
} = req.body;

const savedAppRouteId = await db.transaction(async (transaction) => {
const [appRouteId] = await db('routes').insert(appRoute).transacting(transaction);
let savedAppRouteId: number;

await db.versioning(req.user, {type: 'routes'}, async (transaction) => {
[savedAppRouteId] = await db('routes').insert(appRoute).transacting(transaction);

await db.batchInsert('route_slots', _.compose(
_.map((appRouteSlotName) => _.compose(
stringifyJSON(['props']),
_.assign({ name: appRouteSlotName, routeId: appRouteId }),
_.assign({ name: appRouteSlotName, routeId: savedAppRouteId }),
_.get(appRouteSlotName)
)(appRouteSlots)),
_.keys,
)(appRouteSlots)).transacting(transaction);

return appRouteId;
return savedAppRouteId;
});

const savedAppRoute = await db
.select('routes.id as routeId', 'route_slots.id as routeSlotId', 'routes.*', 'route_slots.*')
.from('routes')
.where('routeId', savedAppRouteId)
.where('routeId', savedAppRouteId!)
.join('route_slots', {
'route_slots.routeId': 'routes.id'
});
Expand Down
14 changes: 7 additions & 7 deletions registry/server/appRoutes/routes/deleteAppRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from 'express';
import Joi from 'joi';
import _ from 'lodash/fp';
import * as httpErrors from '../../errorHandler/httpErrors';

import db from '../../db';
import validateRequestFactory from '../../common/services/validateRequest';
Expand All @@ -25,16 +26,15 @@ const validateRequestBeforeDeleteAppRoute = validateRequestFactory([{
const deleteAppRoute = async (req: Request<DeleteAppRouteRequestParams>, res: Response) => {
const appRouteId = req.params.id;

const count = await db.transaction(async (transaction) => {
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);
const count = await db('routes').where('id', appRouteId).delete().transacting(transaction);
if (!count) {
throw new httpErrors.NotFoundError()
}
});

if (count) {
res.status(204).send();
} else {
res.status(404).send('Not found');
}
res.status(204).send();
};

export default [validateRequestBeforeDeleteAppRoute, deleteAppRoute];
2 changes: 1 addition & 1 deletion registry/server/appRoutes/routes/updateAppRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const updateAppRoute = async (req: Request<UpdateAppRouteRequestParams>, 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)) {
Expand Down
6 changes: 5 additions & 1 deletion registry/server/apps/routes/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ const validateRequestBeforeCreateApp = validateRequestFactory([{
const createApp = async (req: Request, res: Response): Promise<void> => {
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<App>('apps').where('name', app.name);

Expand Down
Loading