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

feat: introduce dynamic configuration #1205

Merged
merged 6 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/backend/src/db/migrations/20230810124730_create_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Knex } from 'knex';

export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('config', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.string('key').unique().notNullable();
table.jsonb('value').notNullable().defaultTo({});

table.timestamps(true, true);
});
}

export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('config');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Knex } from 'knex';

const getPermissionForRole = (
roleId: string,
subject: string,
actions: string[]
) =>
actions.map((action) => ({
role_id: roleId,
subject,
action,
conditions: [],
}));

export async function up(knex: Knex): Promise<void> {
const role = (await knex('roles')
.first(['id', 'key'])
.where({ key: 'admin' })
.limit(1)) as { id: string; key: string };

await knex('permissions').insert(
getPermissionForRole(role.id, 'Config', [
'update',
])
);
}

export async function down(knex: Knex): Promise<void> {
await knex('permissions').where({ subject: 'Config' }).delete();
}
8 changes: 5 additions & 3 deletions packages/backend/src/graphql/mutation-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ import login from './mutations/login';
import registerUser from './mutations/register-user.ee';
import resetConnection from './mutations/reset-connection';
import resetPassword from './mutations/reset-password.ee';
import updateConfig from './mutations/update-config';
import updateConnection from './mutations/update-connection';
import updateCurrentUser from './mutations/update-current-user';
import updateFlow from './mutations/update-flow';
import updateFlowStatus from './mutations/update-flow-status';
import updateRole from './mutations/update-role.ee';
import updateStep from './mutations/update-step';
import updateUser from './mutations/update-user.ee';
import verifyConnection from './mutations/verify-connection';
import upsertSamlAuthProvider from './mutations/upsert-saml-auth-provider.ee';
import verifyConnection from './mutations/verify-connection';

const mutationResolvers = {
createConnection,
Expand All @@ -47,15 +48,16 @@ const mutationResolvers = {
registerUser,
resetConnection,
resetPassword,
updateConfig,
updateConnection,
updateCurrentUser,
updateUser,
updateFlow,
updateFlowStatus,
updateRole,
updateStep,
verifyConnection,
updateUser,
upsertSamlAuthProvider,
verifyConnection,
};

export default mutationResolvers;
44 changes: 44 additions & 0 deletions packages/backend/src/graphql/mutations/update-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { IJSONValue } from '@automatisch/types';
import Config from '../../models/config';
import Context from '../../types/express/context';

type Params = {
input: {
[index: string]: IJSONValue;
};
};

const updateConfig = async (_parent: unknown, params: Params, context: Context) => {
context.currentUser.can('update', 'Config');

const config = params.input;
const configKeys = Object.keys(config);
const updates = [];

for (const key of configKeys) {
const newValue = config[key];

const entryUpdate = Config
.query()
.insert({
key,
value: {
data: newValue
}
})
.onConflict('key')
.merge({
value: {
data: newValue
}
});

updates.push(entryUpdate);
}

await Promise.all(updates);

return config;
};

export default updateConfig;
31 changes: 31 additions & 0 deletions packages/backend/src/graphql/queries/get-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Context from '../../types/express/context';
import Config from '../../models/config';

type Params = {
keys: string[];
};

const getConfig = async (
_parent: unknown,
params: Params,
context: Context
) => {
const configQuery = Config
.query();

if (Array.isArray(params.keys)) {
configQuery.whereIn('key', params.keys);
}

const config = await configQuery;

return config.reduce((computedConfig, configEntry) => {
const { key, value } = configEntry;

computedConfig[key] = value?.data;

return computedConfig;
}, {} as Record<string, unknown>);
};

export default getConfig;
10 changes: 6 additions & 4 deletions packages/backend/src/graphql/query-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import getApps from './queries/get-apps';
import getAutomatischInfo from './queries/get-automatisch-info';
import getBillingAndUsage from './queries/get-billing-and-usage.ee';
import getConnectedApps from './queries/get-connected-apps';
import getConfig from './queries/get-config';
import getCurrentUser from './queries/get-current-user';
import getDynamicData from './queries/get-dynamic-data';
import getDynamicFields from './queries/get-dynamic-fields';
Expand All @@ -11,27 +12,28 @@ import getExecutionSteps from './queries/get-execution-steps';
import getExecutions from './queries/get-executions';
import getFlow from './queries/get-flow';
import getFlows from './queries/get-flows';
import getUser from './queries/get-user';
import getUsers from './queries/get-users';
import getInvoices from './queries/get-invoices.ee';
import getPaddleInfo from './queries/get-paddle-info.ee';
import getPaymentPlans from './queries/get-payment-plans.ee';
import getPermissionCatalog from './queries/get-permission-catalog.ee';
import getRole from './queries/get-role.ee';
import getRoles from './queries/get-roles.ee';
import listSamlAuthProviders from './queries/list-saml-auth-providers.ee';
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee';
import getStepWithTestExecutions from './queries/get-step-with-test-executions';
import getSubscriptionStatus from './queries/get-subscription-status.ee';
import getTrialStatus from './queries/get-trial-status.ee';
import getUser from './queries/get-user';
import getUsers from './queries/get-users';
import healthcheck from './queries/healthcheck';
import listSamlAuthProviders from './queries/list-saml-auth-providers.ee';
import testConnection from './queries/test-connection';

const queryResolvers = {
getApp,
getApps,
getAutomatischInfo,
getBillingAndUsage,
getConfig,
getConnectedApps,
getCurrentUser,
getDynamicData,
Expand All @@ -47,14 +49,14 @@ const queryResolvers = {
getPermissionCatalog,
getRole,
getRoles,
listSamlAuthProviders,
getSamlAuthProvider,
getStepWithTestExecutions,
getSubscriptionStatus,
getTrialStatus,
getUser,
getUsers,
healthcheck,
listSamlAuthProviders,
testConnection,
};

Expand Down
26 changes: 14 additions & 12 deletions packages/backend/src/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,23 @@ type Query {
key: String!
parameters: JSONObject
): [SubstepArgument]
getCurrentUser: User
getPaymentPlans: [PaymentPlan]
getPaddleInfo: GetPaddleInfo
getAutomatischInfo: GetAutomatischInfo
getBillingAndUsage: GetBillingAndUsage
getCurrentUser: User
getConfig(keys: [String]): JSONObject
getInvoices: [Invoice]
getAutomatischInfo: GetAutomatischInfo
getTrialStatus: GetTrialStatus
getSubscriptionStatus: GetSubscriptionStatus
listSamlAuthProviders: [ListSamlAuthProviders]
getPaddleInfo: GetPaddleInfo
getPaymentPlans: [PaymentPlan]
getPermissionCatalog: PermissionCatalog
getRole(id: String!): Role
getRoles: [Role]
getSamlAuthProvider: SamlAuthProvider
getUsers(limit: Int!, offset: Int!): UserConnection
getSubscriptionStatus: GetSubscriptionStatus
getTrialStatus: GetTrialStatus
getUser(id: String!): User
getRoles: [Role]
getRole(id: String!): Role
getPermissionCatalog: PermissionCatalog
getUsers(limit: Int!, offset: Int!): UserConnection
healthcheck: AppHealth
listSamlAuthProviders: [ListSamlAuthProviders]
}

type Mutation {
Expand All @@ -71,15 +72,16 @@ type Mutation {
registerUser(input: RegisterUserInput): User
resetConnection(input: ResetConnectionInput): Connection
resetPassword(input: ResetPasswordInput): Boolean
updateConfig(input: JSONObject): JSONObject
updateConnection(input: UpdateConnectionInput): Connection
updateCurrentUser(input: UpdateCurrentUserInput): User
updateFlow(input: UpdateFlowInput): Flow
updateFlowStatus(input: UpdateFlowStatusInput): Flow
updateRole(input: UpdateRoleInput): Role
updateStep(input: UpdateStepInput): Step
updateUser(input: UpdateUserInput): User
verifyConnection(input: VerifyConnectionInput): Connection
upsertSamlAuthProvider(input: UpsertSamlAuthProviderInput): SamlAuthProvider
verifyConnection(input: VerifyConnectionInput): Connection
}

"""
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/helpers/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const authentication = shield(
getAutomatischInfo: allow,
listSamlAuthProviders: allow,
healthcheck: allow,
getConfig: allow,
},
Mutation: {
'*': isAuthenticated,
Expand Down
23 changes: 23 additions & 0 deletions packages/backend/src/models/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IJSONValue } from '@automatisch/types';
import Base from './base';

class Config extends Base {
id!: string;
key!: string;
value!: { data: IJSONValue };

static tableName = 'config';

static jsonSchema = {
type: 'object',
required: ['key', 'value'],

properties: {
id: { type: 'string', format: 'uuid' },
key: { type: 'string', minLength: 1 },
value: { type: 'object' },
},
};
}

export default Config;
28 changes: 13 additions & 15 deletions packages/web/src/components/AppBar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import * as React from 'react';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import MenuIcon from '@mui/icons-material/Menu';
import MenuOpenIcon from '@mui/icons-material/MenuOpen';
import MuiAppBar from '@mui/material/AppBar';
import type { ContainerProps } from '@mui/material/Container';
import IconButton from '@mui/material/IconButton';
import Toolbar from '@mui/material/Toolbar';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import MuiAppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import MenuIcon from '@mui/icons-material/Menu';
import MenuOpenIcon from '@mui/icons-material/MenuOpen';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import * as React from 'react';

import * as URLS from 'config/urls';
import AccountDropdownMenu from 'components/AccountDropdownMenu';
import TrialStatusBadge from 'components/TrialStatusBadge/index.ee';
import Container from 'components/Container';
import { FormattedMessage } from 'react-intl';
import Logo from 'components/Logo/index';
import TrialStatusBadge from 'components/TrialStatusBadge/index.ee';
import * as URLS from 'config/urls';

import { Link } from './style';

type AppBarProps = {
Expand Down Expand Up @@ -60,11 +60,9 @@ export default function AppBar(props: AppBarProps): React.ReactElement {
{drawerOpen && matchSmallScreens ? <MenuOpenIcon /> : <MenuIcon />}
</IconButton>

<div style={{ flexGrow: 1 }}>
<div style={{ flexGrow: 1, display: 'flex' }}>
<Link to={URLS.DASHBOARD}>
<Typography variant="h6" component="h1" noWrap>
<FormattedMessage id="brandText" />
</Typography>
<Logo />
</Link>
</div>

Expand Down
15 changes: 15 additions & 0 deletions packages/web/src/components/CustomLogo/index.ee.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import useConfig from 'hooks/useConfig';

const CustomLogo = () => {
const { config, loading } = useConfig(['logo.svgData']);

if (loading || !config?.['logo.svgData']) return null;

const logoSvgData = config['logo.svgData'] as string;

return (
<img src={`data:image/svg+xml;utf8,${encodeURIComponent(logoSvgData)}`} />
);
};

export default CustomLogo;
23 changes: 23 additions & 0 deletions packages/web/src/components/Logo/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Typography from '@mui/material/Typography';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';

import CustomLogo from 'components/CustomLogo/index.ee';
import useConfig from 'hooks/useConfig';

const Logo = () => {
const { config, loading } = useConfig(['logo.svgData']);

const logoSvgData = config?.['logo.svgData'] as string;
if (loading && !logoSvgData) return (<React.Fragment />);

if (logoSvgData) return <CustomLogo />;

return (
<Typography variant="h6" component="h1" noWrap>
<FormattedMessage id="brandText" />
</Typography>
);
};

export default Logo;
8 changes: 8 additions & 0 deletions packages/web/src/components/Logo/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { styled } from '@mui/material/styles';
import { Link as RouterLink } from 'react-router-dom';

export const Link = styled(RouterLink)(() => ({
textDecoration: 'none',
color: 'inherit',
display: 'inline-flex',
}));