Compare commits

..

14 Commits

Author SHA1 Message Date
Ali BARIN
321019d36a sample js usage in ts 2023-12-22 12:24:32 +00:00
QAComet
d070e976b0 test(e2e-tests): run only on relevant changes in pull requests (#1495) 2023-12-15 18:51:58 +01:00
Ali BARIN
0caf6bfabb Merge pull request #1494 from automatisch/aut-551
feat: hide notifications page in mation instances
2023-12-15 18:00:08 +01:00
Ali BARIN
b842d7938f feat: hide notifications page in mation instances 2023-12-15 16:28:41 +00:00
Ali BARIN
cebbf84375 Merge pull request #1491 from automatisch/aut-548
feat: apply conditional mation styling
2023-12-15 11:40:12 +01:00
Ali BARIN
8608431490 feat: add conditional mation logo by default 2023-12-14 15:59:37 +00:00
Ali BARIN
78ba18b176 feat: apply conditional mation styling 2023-12-14 14:28:07 +00:00
Ali BARIN
f8c30c8526 Merge pull request #1475 from automatisch/aut-547
feat(queries/getAutomatischInfo): add mation
2023-12-14 11:15:35 +01:00
QAComet
693c9b85a5 test: run UI workflow only on changes outside of backend apps (#1462) 2023-12-14 10:45:15 +01:00
Ali BARIN
70bb7defd1 feat(queries/getAutomatischInfo): add mation 2023-12-12 17:37:27 +00:00
Ali BARIN
160377ca31 Merge pull request #1473 from automatisch/aut-545
docs(salesforce): update connection steps
2023-12-12 18:17:07 +01:00
Ali BARIN
2c0ce77a4e docs(salesforce): update connection steps 2023-12-12 17:08:15 +00:00
Ali BARIN
77fbb0c9da Merge pull request #1470 from automatisch/aut-538
fix(odoo): introduce secure connection option
2023-12-11 16:44:11 +01:00
Ali BARIN
5971425d23 fix(odoo): introduce secure connection option 2023-12-11 15:36:50 +00:00
62 changed files with 358 additions and 1293 deletions

View File

@@ -4,6 +4,11 @@ on:
branches:
- main
pull_request:
paths:
- 'packages/backend/**'
- 'packages/e2e-tests/**'
- 'packages/web/**'
- '!packages/backend/src/apps/**'
workflow_dispatch:
env:

View File

@@ -59,8 +59,8 @@
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.1",
"jsonwebtoken": "^9.0.0",
"knex": "^2.4.0",
"libphonenumber-js": "^1.10.48",
"knex": "^2.5.1",
"lodash.get": "^4.4.2",
"luxon": "2.5.2",
"memory-cache": "^0.2.0",
@@ -69,7 +69,7 @@
"node-html-markdown": "^1.3.0",
"nodemailer": "6.7.0",
"oauth-1.0a": "^2.2.6",
"objection": "^3.1.1",
"objection": "^3.0.0",
"passport": "^0.6.0",
"pg": "^8.7.1",
"php-serialize": "^4.0.2",

View File

@@ -11,7 +11,7 @@ export default {
readOnly: false,
value: null,
placeholder: null,
description: 'Host name of your Odoo Server',
description: 'Host name of your Odoo Server (e.g. sub.domain.com without the protocol)',
clickToCopy: false,
},
{
@@ -25,6 +25,27 @@ export default {
description: 'Port that the host is running on, defaults to 443 (HTTPS)',
clickToCopy: false,
},
{
key: 'secure',
label: 'Secure',
type: 'dropdown' as const,
required: true,
readOnly: false,
value: 'true',
description: 'True if the host communicates via secure protocol.',
variables: false,
clickToCopy: false,
options: [
{
label: 'True',
value: 'true',
},
{
label: 'False',
value: 'false',
},
],
},
{
key: 'databaseName',
label: 'Database Name',
@@ -40,7 +61,7 @@ export default {
key: 'email',
label: 'Email Address',
type: 'string' as const,
requires: true,
required: true,
readOnly: false,
value: null,
placeholder: null,

View File

@@ -32,8 +32,10 @@ export const asyncMethodCall = async <T = number>($: IGlobalVariable, { method,
export const getClient = ($: IGlobalVariable, { path = 'common' }) => {
const host = $.auth.data.host as string;
const port = Number($.auth.data.port as string);
const secure = $.auth.data.secure === 'true';
const createClientFunction = secure ? xmlrpc.createSecureClient : xmlrpc.createClient;
return xmlrpc.createClient(
return createClientFunction(
{
host,
port,

View File

@@ -49,6 +49,7 @@ type AppConfig = {
smtpPassword: string;
fromEmail: string;
isCloud: boolean;
isMation: boolean;
isSelfHosted: boolean;
paddleVendorId: number;
paddleVendorAuthCode: string;
@@ -127,6 +128,7 @@ const appConfig: AppConfig = {
fromEmail: process.env.FROM_EMAIL,
isCloud: process.env.AUTOMATISCH_CLOUD === 'true',
isSelfHosted: process.env.AUTOMATISCH_CLOUD !== 'true',
isMation: process.env.MATION === 'true',
paddleVendorId: Number(process.env.PADDLE_VENDOR_ID),
paddleVendorAuthCode: process.env.PADDLE_VENDOR_AUTH_CODE,
paddlePublicKey: process.env.PADDLE_PUBLIC_KEY,

View File

@@ -1,15 +0,0 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.createTable('shared_connections', (table) => {
table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()'));
table.uuid('connection_id').notNullable().references('id').inTable('connections');
table.uuid('role_id').notNullable().references('id').inTable('roles');
table.timestamps(true, true);
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTable('shared_connections');
}

View File

@@ -19,7 +19,6 @@ 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 shareConnection from './mutations/share-connection.ee';
import updateAppAuthClient from './mutations/update-app-auth-client.ee';
import updateAppConfig from './mutations/update-app-config.ee';
import updateConfig from './mutations/update-config.ee';
@@ -56,7 +55,6 @@ const mutationResolvers = {
registerUser,
resetConnection,
resetPassword,
shareConnection,
updateAppAuthClient,
updateAppConfig,
updateConfig,

View File

@@ -28,11 +28,11 @@ const createFlow = async (
});
if (connectionId) {
const connection = await context.currentUser
.relatedConnectionsQuery()
const hasConnection = await context.currentUser
.$relatedQuery('connections')
.findById(connectionId);
if (!connection) {
if (!hasConnection) {
throw new Error('The connection does not exist!');
}
}

View File

@@ -1,5 +1,4 @@
import Context from '../../types/express/context';
import Connection from '../../models/connection';
type Params = {
input: {
@@ -12,13 +11,10 @@ const deleteConnection = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('delete', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const baseQuery = conditions.isCreator ? userConnections : allConnections;
context.currentUser.can('delete', 'Connection');
await baseQuery
.clone()
await context.currentUser
.$relatedQuery('connections')
.delete()
.findOne({
id: params.input.id,

View File

@@ -4,7 +4,6 @@ import deleteUserQueue from '../../queues/delete-user.ee';
import flowQueue from '../../queues/flow';
import Flow from '../../models/flow';
import Execution from '../../models/execution';
import User from '../../models/user';
import ExecutionStep from '../../models/execution-step';
import appConfig from '../../config/app';
@@ -15,87 +14,51 @@ const deleteCurrentUser = async (
) => {
const id = context.currentUser.id;
try {
await User.transaction(async (trx) => {
const flows = await context.currentUser
.$relatedQuery('flows', trx)
.where({
active: true,
});
const flows = await context.currentUser.$relatedQuery('flows').where({
active: true,
});
const { count } = await context.currentUser
.$relatedQuery('connections', trx)
.joinRelated('sharedConnections')
.joinRelated('steps')
.join('flows', function () {
this
.on(
'flows.id', '=', 'steps.flow_id'
)
.andOnVal(
'flows.user_id', '<>', id
)
.andOnVal(
'flows.active', '=', true
)
})
.count()
.first();
const repeatableJobs = await flowQueue.getRepeatableJobs();
if (count) {
throw new Error('The shared connections must be removed first!');
}
for (const flow of flows) {
const job = repeatableJobs.find((job) => job.id === flow.id);
const executionIds = (
await context.currentUser
.$relatedQuery('executions', trx)
.select('executions.id')
).map((execution: Execution) => execution.id);
const flowIds = flows.map((flow) => flow.id);
await ExecutionStep.query(trx).delete().whereIn('execution_id', executionIds);
await context.currentUser.$relatedQuery('executions', trx).delete();
await context.currentUser.$relatedQuery('steps', trx).delete();
await Flow.query(trx).whereIn('id', flowIds).delete();
await context.currentUser.$relatedQuery('connections', trx).delete();
await context.currentUser.$relatedQuery('identities', trx).delete();
if (appConfig.isCloud) {
await context.currentUser.$relatedQuery('subscriptions', trx).delete();
await context.currentUser.$relatedQuery('usageData', trx).delete();
}
await context.currentUser.$query(trx).delete();
const jobName = `Delete user - ${id}`;
const jobPayload = { id };
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
const jobOptions = {
delay: millisecondsFor30Days,
};
// must be done as the last action as this cannot be reverted via the transaction!
const repeatableJobs = await flowQueue.getRepeatableJobs();
for (const flow of flows) {
const job = repeatableJobs.find((job) => job.id === flow.id);
if (job) {
await flowQueue.removeRepeatableByKey(job.key);
}
}
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
});
return true;
} catch (err) {
if (err instanceof Error) {
throw err;
if (job) {
await flowQueue.removeRepeatableByKey(job.key);
}
throw new Error('The user deletion has failed!');
}
const executionIds = (
await context.currentUser
.$relatedQuery('executions')
.select('executions.id')
).map((execution: Execution) => execution.id);
const flowIds = flows.map((flow) => flow.id);
await ExecutionStep.query().delete().whereIn('execution_id', executionIds);
await context.currentUser.$relatedQuery('executions').delete();
await context.currentUser.$relatedQuery('steps').delete();
await Flow.query().whereIn('id', flowIds).delete();
await context.currentUser.$relatedQuery('connections').delete();
await context.currentUser.$relatedQuery('identities').delete();
if (appConfig.isCloud) {
await context.currentUser.$relatedQuery('subscriptions').delete();
await context.currentUser.$relatedQuery('usageData').delete();
}
await context.currentUser.$query().delete();
const jobName = `Delete user - ${id}`;
const jobPayload = { id };
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
const jobOptions = {
delay: millisecondsFor30Days,
};
await deleteUserQueue.add(jobName, jobPayload, jobOptions);
return true;
};
export default deleteCurrentUser;

View File

@@ -1,5 +1,4 @@
import Context from '../../types/express/context';
import Connection from '../../models/connection';
type Params = {
input: {
@@ -12,13 +11,10 @@ const resetConnection = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('update', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const baseQuery = conditions.isCreator ? userConnections : allConnections;
context.currentUser.can('create', 'Connection');
let connection = await baseQuery
.clone()
let connection = await context.currentUser
.$relatedQuery('connections')
.findOne({
id: params.input.id,
})

View File

@@ -1,55 +0,0 @@
import Context from '../../types/express/context';
import Connection from '../../models/connection';
import SharedConnection from '../../models/shared-connection';
type Params = {
input: {
id: string;
roleIds: string[];
};
};
const shareConnection = async (
_parent: unknown,
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('update', 'Connection');
if (conditions.isCreator) return;
const {
id,
roleIds,
} = params.input;
const connection = await Connection
.query()
.findById(id)
.throwIfNotFound();
try {
const updatedConnection = await Connection.transaction(async (trx) => {
await connection.$relatedQuery('sharedConnections', trx).delete();
if (roleIds?.length) {
const sharedConnections = roleIds.map((roleId) => ({
roleId,
connectionId: connection.id,
}));
await SharedConnection.query().insert(sharedConnections);
}
return await Connection
.query(trx)
.findById(id);
});
return updatedConnection;
} catch (err) {
throw new Error('The connection sharing preferences could not be updated!');
}
};
export default shareConnection;

View File

@@ -1,7 +1,6 @@
import { IJSONObject } from '@automatisch/types';
import Context from '../../types/express/context';
import AppAuthClient from '../../models/app-auth-client';
import Connection from '../../models/connection';
type Params = {
input: {
@@ -16,13 +15,10 @@ const updateConnection = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('update', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const baseQuery = conditions.isCreator ? userConnections : allConnections;
context.currentUser.can('create', 'Connection');
let connection = await baseQuery
.clone()
let connection = await context.currentUser
.$relatedQuery('connections')
.findOne({
id: params.input.id,
})

View File

@@ -45,11 +45,10 @@ const updateStep = async (
canSeeAllConnections = !conditions.isCreator;
} catch {
// The user does not have permission to read any connections!
throw new Error('The connection does not exist!');
// void
}
const userConnections = context.currentUser.relatedConnectionsQuery();
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const baseConnectionsQuery = canSeeAllConnections ? allConnections : userConnections;

View File

@@ -9,55 +9,28 @@ type Params = {
const getApp = async (_parent: unknown, params: Params, context: Context) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
const app = await App.findOneByKey(params.key);
if (context.currentUser) {
const userConnections = context.currentUser.relatedConnectionsQuery();
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
const connections = await Connection.query()
.with('connections', connectionBaseQuery)
.with(
'connections_with_flow_count',
Connection.query()
.clearSelect()
.select('connections.id')
.leftJoinRelated('steps')
.leftJoin('flows', function () {
this
.on(
'flows.id',
'=',
'steps.flow_id',
)
if (conditions.isCreator) {
this.andOnVal(
'flows.user_id',
'=',
context.currentUser.id
)
}
})
.where({
'connections.key': params.key,
'connections.draft': false,
})
.countDistinct('steps.flow_id as flowCount')
.groupBy('connections.id')
)
.select(
'connections.*',
'connections_with_flow_count.flowCount as flowCount'
)
.from('connections')
const connections = await connectionBaseQuery
.clone()
.select('connections.*')
.withGraphFetched({
appConfig: true,
appAuthClient: true
})
.joinRaw('join connections_with_flow_count on connections.id = connections_with_flow_count.id')
.orderBy('connections.created_at', 'desc');
.fullOuterJoinRelated('steps')
.where({
'connections.key': params.key,
'connections.draft': false,
})
.countDistinct('steps.flow_id as flowCount')
.groupBy('connections.id')
.orderBy('created_at', 'desc');
return {
...app,

View File

@@ -9,6 +9,7 @@ describe('graphQL getAutomatischInfo query', () => {
query {
getAutomatischInfo {
isCloud
isMation
license {
id
name
@@ -24,6 +25,7 @@ describe('graphQL getAutomatischInfo query', () => {
jest.spyOn(license, 'getLicense').mockResolvedValue(false);
jest.replaceProperty(appConfig, 'isCloud', false);
jest.replaceProperty(appConfig, 'isMation', false);
});
it('should return empty license data', async () => {
@@ -36,6 +38,7 @@ describe('graphQL getAutomatischInfo query', () => {
data: {
getAutomatischInfo: {
isCloud: false,
isMation: false,
license: {
id: null,
name: null,
@@ -77,6 +80,7 @@ describe('graphQL getAutomatischInfo query', () => {
data: {
getAutomatischInfo: {
isCloud: true,
isMation: false,
license: {
expireAt: '2025-08-09T10:56:54.144Z',
id: '123123',
@@ -105,6 +109,69 @@ describe('graphQL getAutomatischInfo query', () => {
const expectedResponsePayload = {
data: {
getAutomatischInfo: {
isCloud: false,
isMation: false,
license: {
expireAt: '2025-08-09T10:56:54.144Z',
id: '123123',
name: 'Test License',
verified: true,
},
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with mation flag enabled', () => {
beforeEach(async () => {
jest.replaceProperty(appConfig, 'isCloud', false);
jest.replaceProperty(appConfig, 'isMation', true);
});
it('should return all license data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getAutomatischInfo: {
isCloud: false,
isMation: true,
license: {
expireAt: '2025-08-09T10:56:54.144Z',
id: '123123',
name: 'Test License',
verified: true,
},
},
},
};
expect(response.body).toEqual(expectedResponsePayload);
});
});
describe('and with mation flag disabled', () => {
beforeEach(async () => {
jest.replaceProperty(appConfig, 'isCloud', false);
jest.replaceProperty(appConfig, 'isMation', false);
});
it('should return all license data', async () => {
const response = await request(app)
.post('/graphql')
.send({ query })
.expect(200);
const expectedResponsePayload = {
data: {
getAutomatischInfo: {
isMation: false,
isCloud: false,
license: {
expireAt: '2025-08-09T10:56:54.144Z',

View File

@@ -13,6 +13,7 @@ const getAutomatischInfo = async () => {
return {
isCloud: appConfig.isCloud,
isMation: appConfig.isMation,
license: computedLicense,
};
};

View File

@@ -15,7 +15,7 @@ const getConnectedApps = async (
) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.relatedConnectionsQuery();
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
@@ -25,9 +25,8 @@ const getConnectedApps = async (
let apps = await App.findAll(params.name);
const connections = await Connection
.query()
.with('connections', connectionBaseQuery)
const connections = await connectionBaseQuery
.clone()
.select('connections.key')
.where({ draft: false })
.count('connections.id as count')

View File

@@ -1,29 +0,0 @@
import Context from '../../types/express/context';
import Connection from '../../models/connection';
type Params = {
id: string;
};
const getSharedConnectionRoleIds = async (
_parent: unknown,
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('update', 'Connection');
if (conditions.isCreator) return;
const connection = await Connection
.query()
.findById(params.id)
.throwIfNotFound();
const sharedConnections = await connection.$relatedQuery('sharedConnections');
const roleIds = sharedConnections.map(({ roleId }) => roleId);
return roleIds;
};
export default getSharedConnectionRoleIds;

View File

@@ -0,0 +1,10 @@
import appConfig from '../../config/app';
const getUseJsFile = async () => {
return {
canInvoke: true,
appConfig,
};
};
export default getUseJsFile;

View File

@@ -13,15 +13,15 @@ const testConnection = async (
params: Params,
context: Context
) => {
const conditions = context.currentUser.can('read', 'Connection');
const userConnections = context.currentUser.relatedConnectionsQuery();
const conditions = context.currentUser.can('update', 'Connection');
const userConnections = context.currentUser.$relatedQuery('connections');
const allConnections = Connection.query();
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
let connection = await connectionBaseQuery
.clone()
.findOne({
'connections.id': params.id,
id: params.id,
})
.throwIfNotFound();

View File

@@ -6,6 +6,7 @@ import getApps from './queries/get-apps';
import getAutomatischInfo from './queries/get-automatisch-info';
import getBillingAndUsage from './queries/get-billing-and-usage.ee';
import getConfig from './queries/get-config.ee';
import getUseJsFile from './queries/get-use-js-file.js';
import getConnectedApps from './queries/get-connected-apps';
import getCurrentUser from './queries/get-current-user';
import getDynamicData from './queries/get-dynamic-data';
@@ -24,7 +25,6 @@ import getRole from './queries/get-role.ee';
import getRoles from './queries/get-roles.ee';
import getSamlAuthProviderRoleMappings from './queries/get-saml-auth-provider-role-mappings.ee';
import getSamlAuthProvider from './queries/get-saml-auth-provider.ee';
import getSharedConnectionRoleIds from './queries/get-shared-connection-role-ids.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';
@@ -61,7 +61,6 @@ const queryResolvers = {
getRoles,
getSamlAuthProvider,
getSamlAuthProviderRoleMappings,
getSharedConnectionRoleIds,
getStepWithTestExecutions,
getSubscriptionStatus,
getTrialStatus,
@@ -70,6 +69,7 @@ const queryResolvers = {
healthcheck,
listSamlAuthProviders,
testConnection,
getUseJsFile,
};
export default queryResolvers;

View File

@@ -11,6 +11,7 @@ type Query {
getConnectedApps(name: String): [App]
testConnection(id: String!): Connection
getFlow(id: String!): Flow
getUseJsFile: JSONObject
getFlows(
limit: Int!
offset: Int!
@@ -53,7 +54,6 @@ type Query {
getNotifications: [Notification]
getSamlAuthProvider: SamlAuthProvider
getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping]
getSharedConnectionRoleIds(id: String!): [String]
getSubscriptionStatus: GetSubscriptionStatus
getTrialStatus: GetTrialStatus
getUser(id: String!): User
@@ -84,7 +84,6 @@ type Mutation {
registerUser(input: RegisterUserInput): User
resetConnection(input: ResetConnectionInput): Connection
resetPassword(input: ResetPasswordInput): Boolean
shareConnection(input: ShareConnectionInput): Connection
updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient
updateAppConfig(input: UpdateAppConfigInput): AppConfig
updateConfig(input: JSONObject): JSONObject
@@ -246,7 +245,6 @@ type AuthLink {
type Connection {
id: String
key: String
shared: Boolean
reconnectable: Boolean
appAuthClientId: String
formattedData: ConnectionData
@@ -649,6 +647,7 @@ type AppHealth {
type GetAutomatischInfo {
isCloud: Boolean
isMation: Boolean
license: License
}
@@ -813,11 +812,6 @@ input ExecutionFiltersInput {
status: String
}
input ShareConnectionInput {
id: String!
roleIds: [String]
}
schema {
query: Query
mutation: Mutation

View File

@@ -1,18 +1,18 @@
import { IJSONObject, IRequest } from '@automatisch/types';
import { AES, enc } from 'crypto-js';
import { QueryContext, ModelOptions } from 'objection';
import type { RelationMappings } from 'objection';
import { ModelOptions, QueryContext } from 'objection';
import appConfig from '../config/app';
import globalVariable from '../helpers/global-variable';
import Telemetry from '../helpers/telemetry';
import { AES, enc } from 'crypto-js';
import { IRequest } from '@automatisch/types';
import App from './app';
import AppAuthClient from './app-auth-client';
import AppConfig from './app-config';
import AppAuthClient from './app-auth-client';
import Base from './base';
import ExtendedQueryBuilder from './query-builder';
import SharedConnection from './shared-connection';
import Step from './step';
import User from './user';
import Step from './step';
import ExtendedQueryBuilder from './query-builder';
import appConfig from '../config/app';
import { IJSONObject } from '@automatisch/types';
import Telemetry from '../helpers/telemetry';
import globalVariable from '../helpers/global-variable';
class Connection extends Base {
id!: string;
@@ -24,9 +24,6 @@ class Connection extends Base {
draft: boolean;
count?: number;
flowCount?: number;
sharedConnections?: SharedConnection[];
// computed via `User.relevantConnectionsQuery`
shared?: boolean;
user?: User;
steps?: Step[];
triggerSteps?: Step[];
@@ -49,7 +46,6 @@ class Connection extends Base {
appAuthClientId: { type: 'string', format: 'uuid' },
verified: { type: 'boolean', default: false },
draft: { type: 'boolean' },
shared: { type: 'boolean', readOnly: true, },
deletedAt: { type: 'string' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
@@ -104,14 +100,6 @@ class Connection extends Base {
to: 'app_auth_clients.id',
},
},
sharedConnections: {
relation: Base.HasManyRelation,
modelClass: SharedConnection,
join: {
from: 'connections.id',
to: 'shared_connections.connection_id',
},
},
});
get reconnectable() {

View File

@@ -1,45 +0,0 @@
import Base from './base';
import Role from './role';
import User from './user';
class SharedConnection extends Base {
id!: string;
roleId!: string;
connectionId!: string;
static tableName = 'shared_connections';
static jsonSchema = {
type: 'object',
required: ['roleId', 'connectionId'],
properties: {
id: { type: 'string', format: 'uuid' },
roleId: { type: 'string', format: 'uuid' },
connectionId: { type: 'string', format: 'uuid' },
createdAt: { type: 'string' },
updatedAt: { type: 'string' },
},
};
static relationMappings = () => ({
roles: {
relation: Base.HasManyRelation,
modelClass: Role,
join: {
from: 'shared_connections.role_id',
to: 'roles.id',
},
},
users: {
relation: Base.HasManyRelation,
modelClass: User,
join: {
from: 'shared_connections.role_id',
to: 'users.role_id',
},
},
});
}
export default SharedConnection;

View File

@@ -1,7 +1,7 @@
import bcrypt from 'bcrypt';
import { DateTime } from 'luxon';
import crypto from 'node:crypto';
import { raw, ModelOptions, QueryContext } from 'objection';
import { ModelOptions, QueryContext } from 'objection';
import appConfig from '../config/app';
import { hasValidLicense } from '../helpers/license.ee';
@@ -28,7 +28,6 @@ class User extends Base {
resetPasswordTokenSentAt: string;
trialExpiryDate: string;
connections?: Connection[];
sharedConnections?: Connection[];
flows?: Flow[];
steps?: Step[];
executions?: Execution[];
@@ -70,18 +69,6 @@ class User extends Base {
to: 'connections.user_id',
},
},
sharedConnections: {
relation: Base.ManyToManyRelation,
modelClass: Connection,
join: {
from: 'users.role_id',
through: {
from: 'shared_connections.role_id',
to: 'shared_connections.connection_id',
},
to: 'connections.id',
},
},
flows: {
relation: Base.HasManyRelation,
modelClass: Flow,
@@ -178,40 +165,6 @@ class User extends Base {
},
});
relatedConnectionsQuery() {
return Connection
.query()
.select('connections.*', raw('shared_connections.role_id IS NOT NULL as shared'))
.leftJoin(
'shared_connections',
'connections.id',
'=',
'shared_connections.connection_id'
)
.join(
'users',
function () {
this
.on(
'users.id',
'=',
'connections.user_id',
)
.orOn(
'users.role_id',
'=',
'shared_connections.role_id'
)
},
)
.where(
'users.id',
'=',
this.id
)
.groupBy('connections.id', 'shared_connections.role_id');
}
login(password: string) {
return bcrypt.compare(password, this.password);
}

View File

@@ -2,11 +2,12 @@
"compilerOptions": {
"baseUrl": ".",
"declaration": true,
"allowJs": true,
"esModuleInterop": true,
"lib": ["es2021"],
"module": "commonjs",
"moduleResolution": "node",
"noImplicitAny": true,
"noImplicitAny": false,
"outDir": "dist",
"paths": {
"*": ["../../node_modules/*", "node_modules/*", "src/types/*"]

View File

@@ -12,7 +12,9 @@ connection in Automatisch. If any of the steps are outdated, please let us know!
1. Enter necessary information in the form.
1. Check **Enable OAuth Settings** checkbox.
1. Copy **OAuth Redirect URL** from Automatisch and paste it to the **Callback URL** field.
1. Add any scopes you plan to use in the **Selected OAuth Scopes** section.
1. Add any scopes you plan to use in the **Selected OAuth Scopes** section. We suggest `full` and `refresh_token, offline_access` scopes.
1. Uncheck "Require Proof Key for Code Exchange (PKCE) Extension for Supported Authorization Flows" checkbox.
1. Check "Enable Authorization Code and Credentials Flow" checkbox
1. Click on the **Save** button at the bottom of the page.
1. Acknowledge the information and click on the **Continue** button.
1. In the **API (Enable OAuth Settings)** section, click the **Manager Consumer Details** button.

View File

@@ -23,7 +23,6 @@ export interface IConnection {
formattedData?: IJSONObject;
userId: string;
verified: boolean;
shared?: boolean;
count?: number;
flowCount?: number;
appData?: IApp;

View File

@@ -1,168 +0,0 @@
import type { IApp, IField, IJSONObject } from '@automatisch/types';
import LoadingButton from '@mui/lab/LoadingButton';
import Alert from '@mui/material/Alert';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import * as React from 'react';
import { FieldValues, SubmitHandler } from 'react-hook-form';
import { useNavigate, useSearchParams } from 'react-router-dom';
import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee';
import InputCreator from 'components/InputCreator';
import * as URLS from 'config/urls';
import useAuthenticateApp from 'hooks/useAuthenticateApp.ee';
import useFormatMessage from 'hooks/useFormatMessage';
import { generateExternalLink } from '../../helpers/translationValues';
import { Form } from './style';
type AdminApplicationConnectionCreateProps = {
onClose: (response: Record<string, unknown>) => void;
application: IApp;
connectionId?: string;
};
export default function AdminApplicationConnectionCreate(
props: AdminApplicationConnectionCreateProps
): React.ReactElement {
const { application, connectionId, onClose } = props;
const { name, authDocUrl, key, auth } = application;
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const formatMessage = useFormatMessage();
const [error, setError] = React.useState<IJSONObject | null>(null);
const [inProgress, setInProgress] = React.useState(false);
const hasConnection = Boolean(connectionId);
const useShared = searchParams.get('shared') === 'true';
const appAuthClientId = searchParams.get('appAuthClientId') || undefined;
const { authenticate } = useAuthenticateApp({
appKey: key,
connectionId,
appAuthClientId,
useShared: !!appAuthClientId,
});
React.useEffect(function relayProviderData() {
if (window.opener) {
window.opener.postMessage({
source: 'automatisch',
payload: { search: window.location.search, hash: window.location.hash },
});
window.close();
}
}, []);
React.useEffect(
function initiateSharedAuthenticationForGivenAuthClient() {
if (!appAuthClientId) return;
if (!authenticate) return;
const asyncAuthenticate = async () => {
await authenticate();
navigate(URLS.ADMIN_APP_CONNECTIONS(key));
};
asyncAuthenticate();
},
[appAuthClientId, authenticate]
);
const handleClientClick = (appAuthClientId: string) =>
navigate(
URLS.ADMIN_APP_CONNECTIONS_CREATE_WITH_AUTH_CLIENT_ID(
key,
appAuthClientId
)
);
const handleAuthClientsDialogClose = () =>
navigate(URLS.ADMIN_APP_CONNECTIONS(key));
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
async (data) => {
if (!authenticate) return;
setInProgress(true);
try {
const response = await authenticate({
fields: data,
});
onClose(response as Record<string, unknown>);
} catch (err) {
const error = err as IJSONObject;
console.log(error);
setError((error.graphQLErrors as IJSONObject[])?.[0]);
} finally {
setInProgress(false);
}
},
[authenticate]
);
if (useShared)
return (
<AppAuthClientsDialog
appKey={key}
onClose={handleAuthClientsDialogClose}
onClientClick={handleClientClick}
/>
);
if (appAuthClientId) return <React.Fragment />;
return (
<Dialog open={true} onClose={onClose}>
<DialogTitle>
{hasConnection
? formatMessage('adminAppsConnections.reconnectConnection')
: formatMessage('adminAppsConnections.createConnection')}
</DialogTitle>
{authDocUrl && (
<Alert severity="info" sx={{ fontWeight: 300 }}>
{formatMessage('adminAppsConnections.callToDocs', {
appName: name,
docsLink: generateExternalLink(authDocUrl),
})}
</Alert>
)}
{error && (
<Alert
severity="error"
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
>
{error.message}
{error.details && (
<pre style={{ whiteSpace: 'pre-wrap' }}>
{JSON.stringify(error.details, null, 2)}
</pre>
)}
</Alert>
)}
<DialogContent>
<DialogContentText tabIndex={-1} component="div">
<Form onSubmit={submitHandler}>
{auth?.fields?.map((field: IField) => (
<InputCreator key={field.key} schema={field} />
))}
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2 }}
loading={inProgress}
>
{formatMessage('adminAppsConnections.submit')}
</LoadingButton>
</Form>
</DialogContentText>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,9 +0,0 @@
import { styled } from '@mui/material/styles';
import BaseForm from 'components/Form';
export const Form = styled(BaseForm)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
paddingTop: theme.spacing(1),
}));

View File

@@ -1,70 +0,0 @@
import { useFieldArray, useFormContext } from 'react-hook-form';
import FormControlLabel from '@mui/material/FormControlLabel';
import Divider from '@mui/material/Divider';
import Checkbox from '@mui/material/Checkbox';
import useFormatMessage from 'hooks/useFormatMessage';
import ControlledCheckbox from 'components/ControlledCheckbox';
import { Stack } from '@mui/material';
type Roles = { id: string; name: string; checked: boolean }[];
function RolesFieldArray() {
const formatMessage = useFormatMessage();
const { control, watch, setValue } = useFormContext();
const fieldArrayData = useFieldArray({
control,
name: 'roles',
});
const fields = fieldArrayData.fields as Roles;
const watchedFields = watch('roles') as Roles;
const allFieldsSelected = watchedFields.every((field) => field.checked);
const allFieldsDeselected = watchedFields.every((field) => !field.checked);
const handleSelectAllClick = () => {
setValue(
'roles',
watchedFields.map((field) => ({ ...field, checked: !allFieldsSelected })),
{ shouldDirty: true }
);
};
return (
<Stack direction="column" spacing={1}>
<FormControlLabel
control={
<Checkbox
color="primary"
indeterminate={!(allFieldsSelected || allFieldsDeselected)}
checked={allFieldsSelected}
onChange={handleSelectAllClick}
/>
}
label={
allFieldsSelected
? formatMessage('adminAppsConnections.deselectAll')
: formatMessage('adminAppsConnections.selectAll')
}
sx={{ margin: 0 }}
/>
<Divider />
{fields.map((role, index) => {
return (
<FormControlLabel
key={role.id}
control={
<ControlledCheckbox
name={`roles.${index}.checked`}
defaultValue={role.checked}
/>
}
label={role.name}
/>
);
})}
</Stack>
);
}
export default RolesFieldArray;

View File

@@ -1,140 +0,0 @@
import * as React from 'react';
import { useParams } from 'react-router-dom';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import LoadingButton from '@mui/lab/LoadingButton';
import Stack from '@mui/material/Stack';
import Alert from '@mui/material/Alert';
import { CircularProgress } from '@mui/material';
import { useMutation } from '@apollo/client';
import { IApp, IRole } from '@automatisch/types';
import { FieldValues, SubmitHandler } from 'react-hook-form';
import { SHARE_CONNECTION } from 'graphql/mutations/share-connection';
import useFormatMessage from 'hooks/useFormatMessage';
import useSharedConnectionRoleIds from 'hooks/useSharedConnectionRoleIds';
import useRoles from 'hooks/useRoles.ee';
import RolesFieldArray from './RolesFieldArray';
import { Form } from './style';
type AdminApplicationConnectionShareProps = {
onClose: (response: Record<string, unknown>) => void;
application: IApp;
};
type Params = {
connectionId: string;
};
function generateRolesData(roles: IRole[], roleIds: string[]) {
return roles.map(({ id, name }) => ({
id,
name,
checked: roleIds.includes(id),
}));
}
export default function AdminApplicationConnectionShare(
props: AdminApplicationConnectionShareProps
): React.ReactElement {
const { onClose } = props;
const { connectionId } = useParams() as Params;
const formatMessage = useFormatMessage();
const [
shareConnection,
{ loading: loadingShareConnection, error: shareConnectionError },
] = useMutation(SHARE_CONNECTION, {
context: { autoSnackbar: false },
});
const {
roleIds,
loading: roleIdsLoading,
error: roleIdsError,
} = useSharedConnectionRoleIds(connectionId, {
context: { autoSnackbar: false },
});
const { roles, loading: rolesLoading, error: rolesError } = useRoles();
const error = shareConnectionError || roleIdsError || rolesError;
const showDialogContent =
!roleIdsLoading && !rolesLoading && !roleIdsError && !rolesError;
const submitHandler: SubmitHandler<FieldValues> = React.useCallback(
async (data) => {
const roles = data.roles as {
id: string;
name: string;
checked: boolean;
}[];
const response = await shareConnection({
variables: {
input: {
id: connectionId,
roleIds: roles
.filter((role) => role.checked)
.map((role) => role.id),
},
},
});
onClose(response as Record<string, unknown>);
},
[]
);
const defaultValues = React.useMemo(
() => ({
roles: generateRolesData(roles, roleIds),
}),
[roles, roleIds]
);
return (
<Dialog open={true} onClose={onClose}>
<DialogTitle>
{formatMessage('adminAppsConnections.shareConnection')}
</DialogTitle>
{error && (
<Alert
severity="error"
sx={{ mt: 1, fontWeight: 500, wordBreak: 'break-all' }}
>
{error.message}
</Alert>
)}
{(roleIdsLoading || rolesLoading) && (
<CircularProgress sx={{ display: 'block', margin: '20px auto' }} />
)}
{showDialogContent && (
<DialogContent sx={{ pt: '0px !important' }}>
<DialogContentText tabIndex={-1} component="div">
<Form
defaultValues={defaultValues}
onSubmit={submitHandler}
render={({ formState: { isDirty } }) => {
return (
<Stack direction="column">
<RolesFieldArray />
<LoadingButton
type="submit"
variant="contained"
color="primary"
sx={{ boxShadow: 2, mt: 5 }}
disabled={!isDirty}
loading={loadingShareConnection}
>
{formatMessage('adminAppsConnections.submit')}
</LoadingButton>
</Stack>
);
}}
></Form>
</DialogContentText>
</DialogContent>
)}
</Dialog>
);
}

View File

@@ -1,9 +0,0 @@
import { styled } from '@mui/material/styles';
import BaseForm from 'components/Form';
export const Form = styled(BaseForm)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
paddingTop: theme.spacing(1),
}));

View File

@@ -1,85 +0,0 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import Menu from '@mui/material/Menu';
import type { PopoverProps } from '@mui/material/Popover';
import MenuItem from '@mui/material/MenuItem';
import type { IConnection } from '@automatisch/types';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
type Action = {
type: 'test' | 'reconnect' | 'delete' | 'shareConnection';
};
type ContextMenuProps = {
appKey: string;
connection: IConnection;
onClose: () => void;
onMenuItemClick: (event: React.MouseEvent, action: Action) => void;
anchorEl: PopoverProps['anchorEl'];
disableReconnection: boolean;
};
export default function ContextMenu(
props: ContextMenuProps
): React.ReactElement {
const {
appKey,
connection,
onClose,
onMenuItemClick,
anchorEl,
disableReconnection,
} = props;
const formatMessage = useFormatMessage();
const createActionHandler = React.useCallback(
(action: Action) => {
return function clickHandler(event: React.MouseEvent) {
onMenuItemClick(event, action);
onClose();
};
},
[onMenuItemClick, onClose]
);
return (
<Menu
open={true}
onClose={onClose}
hideBackdrop={false}
anchorEl={anchorEl}
>
<MenuItem onClick={createActionHandler({ type: 'test' })}>
{formatMessage('adminAppsConnections.testConnection')}
</MenuItem>
<MenuItem
component={Link}
disabled={disableReconnection}
to={URLS.ADMIN_APP_RECONNECT_CONNECTION(
appKey,
connection.id,
connection.appAuthClientId
)}
onClick={createActionHandler({ type: 'reconnect' })}
>
{formatMessage('adminAppsConnections.reconnect')}
</MenuItem>
<MenuItem
component={Link}
to={URLS.ADMIN_APP_SHARE_CONNECTION(appKey, connection.id)}
onClick={createActionHandler({ type: 'shareConnection' })}
>
{formatMessage('adminAppsConnections.shareConnection')}
</MenuItem>
<MenuItem onClick={createActionHandler({ type: 'delete' })}>
{formatMessage('adminAppsConnections.delete')}
</MenuItem>
</Menu>
);
}

View File

@@ -1,155 +0,0 @@
import type { IConnection } from '@automatisch/types';
import { useLazyQuery, useMutation } from '@apollo/client';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from '@mui/icons-material/Error';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import CardActionArea from '@mui/material/CardActionArea';
import CircularProgress from '@mui/material/CircularProgress';
import Stack from '@mui/material/Stack';
import { DateTime } from 'luxon';
import * as React from 'react';
import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection';
import { TEST_CONNECTION } from 'graphql/queries/test-connection';
import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar';
import useFormatMessage from 'hooks/useFormatMessage';
import ConnectionContextMenu from '../AppConnectionContextMenu';
import { CardContent, Typography } from './style';
type AppConnectionRowProps = {
connection: IConnection;
};
function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
const enqueueSnackbar = useEnqueueSnackbar();
const [verificationVisible, setVerificationVisible] = React.useState(false);
const [testConnection, { called: testCalled, loading: testLoading }] =
useLazyQuery(TEST_CONNECTION, {
fetchPolicy: 'network-only',
onCompleted: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
onError: () => {
setTimeout(() => setVerificationVisible(false), 3000);
},
});
const [deleteConnection] = useMutation(DELETE_CONNECTION);
const formatMessage = useFormatMessage();
const { id, key, formattedData, verified, createdAt, reconnectable, shared } =
props.connection;
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
const [anchorEl, setAnchorEl] = React.useState<SVGSVGElement | null>(null);
const handleClose = () => {
setAnchorEl(null);
};
const onContextMenuClick = () => setAnchorEl(contextButtonRef.current);
const onContextMenuAction = React.useCallback(
async (event, action: { [key: string]: string }) => {
if (action.type === 'delete') {
await deleteConnection({
variables: { input: { id } },
update: (cache) => {
const connectionCacheId = cache.identify({
__typename: 'Connection',
id,
});
cache.evict({
id: connectionCacheId,
});
},
});
enqueueSnackbar(formatMessage('adminAppsConnections.deletedMessage'), {
variant: 'success',
});
} else if (action.type === 'test') {
setVerificationVisible(true);
testConnection({ variables: { id } });
}
},
[deleteConnection, id, testConnection, formatMessage, enqueueSnackbar]
);
const relativeCreatedAt = DateTime.fromMillis(
parseInt(createdAt, 10)
).toRelative();
return (
<>
<Card sx={{ my: 2 }}>
<CardActionArea onClick={onContextMenuClick}>
<CardContent>
<Stack justifyContent="center" alignItems="flex-start" spacing={1}>
<Typography variant="h6" sx={{ textAlign: 'left' }}>
{formattedData?.screenName} {shared && 'shared'}
</Typography>
<Typography variant="caption">
{formatMessage('adminAppsConnections.addedAt', {
datetime: relativeCreatedAt,
})}
</Typography>
</Stack>
<Box>
<Stack direction="row" alignItems="center" spacing={1}>
{verificationVisible && testCalled && testLoading && (
<>
<CircularProgress size={16} />
<Typography variant="caption">
{formatMessage('adminAppsConnections.testing')}
</Typography>
</>
)}
{verificationVisible && testCalled && !testLoading && verified && (
<>
<CheckCircleIcon fontSize="small" color="success" />
<Typography variant="caption">
{formatMessage('adminAppsConnections.testSuccessful')}
</Typography>
</>
)}
{verificationVisible &&
testCalled &&
!testLoading &&
!verified && (
<>
<ErrorIcon fontSize="small" color="error" />
<Typography variant="caption">
{formatMessage('adminAppsConnections.testFailed')}
</Typography>
</>
)}
</Stack>
</Box>
<Box>
<MoreHorizIcon ref={contextButtonRef} />
</Box>
</CardContent>
</CardActionArea>
</Card>
{anchorEl && (
<ConnectionContextMenu
appKey={key}
connection={props.connection}
disableReconnection={!reconnectable}
onClose={handleClose}
onMenuItemClick={onContextMenuAction}
anchorEl={anchorEl}
/>
)}
</>
);
}
export default AppConnectionRow;

View File

@@ -1,16 +0,0 @@
import { styled } from '@mui/material/styles';
import MuiCardContent from '@mui/material/CardContent';
import MuiTypography from '@mui/material/Typography';
export const CardContent = styled(MuiCardContent)(({ theme }) => ({
display: 'grid',
gridTemplateRows: 'auto',
gridTemplateColumns: '1fr auto auto auto',
gridColumnGap: theme.spacing(2),
alignItems: 'center',
}));
export const Typography = styled(MuiTypography)(() => ({
textAlign: 'center',
display: 'inline-block',
}));

View File

@@ -1,55 +0,0 @@
import { Link } from 'react-router-dom';
import { useQuery } from '@apollo/client';
import CircularProgress from '@mui/material/CircularProgress';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import type { IConnection } from '@automatisch/types';
import { GET_APP_CONNECTIONS } from 'graphql/queries/get-app-connections';
import * as URLS from 'config/urls';
import useFormatMessage from 'hooks/useFormatMessage';
import NoResultFound from 'components/NoResultFound';
import AppConnectionRow from './AppConnectionRow';
type AdminApplicationConnectionsProps = { appKey: string };
function AdminApplicationConnections(
props: AdminApplicationConnectionsProps
): React.ReactElement {
const { appKey } = props;
const formatMessage = useFormatMessage();
const { data, loading } = useQuery(GET_APP_CONNECTIONS, {
variables: { key: appKey },
});
const appConnections: IConnection[] = data?.getApp?.connections || [];
if (loading)
return <CircularProgress sx={{ display: 'block', margin: '20px auto' }} />;
if (appConnections.length === 0) {
return (
<NoResultFound
to={URLS.ADMIN_APP_CONNECTIONS_CREATE(appKey)}
text={formatMessage('adminAppsConnections.noConnections')}
/>
);
}
return (
<div>
{appConnections.map((appConnection) => (
<AppConnectionRow key={appConnection.id} connection={appConnection} />
))}
<Stack justifyContent="flex-end" direction="row">
<Link to={URLS.ADMIN_APP_CONNECTIONS_CREATE(appKey)}>
<Button variant="contained" sx={{ mt: 2 }} component="div">
{formatMessage('adminAppsConnections.createConnection')}
</Button>
</Link>
</Stack>
</div>
);
}
export default AdminApplicationConnections;

View File

@@ -53,7 +53,6 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
createdAt,
flowCount,
reconnectable,
shared,
} = props.connection;
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
@@ -106,7 +105,7 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
<CardContent>
<Stack justifyContent="center" alignItems="flex-start" spacing={1}>
<Typography variant="h6" sx={{ textAlign: 'left' }}>
{formattedData?.screenName} {shared && 'shared'}
{formattedData?.screenName}
</Typography>
<Typography variant="caption">

View File

@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
export const LogoImage = styled('img')(() => ({
maxWidth: 200,
maxHeight: 50,
maxHeight: 22,
width: '100%',
height: 'auto',
}));

View File

@@ -0,0 +1,22 @@
import Typography from '@mui/material/Typography';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import MationLogo from 'components/MationLogo';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
const DefaultLogo = () => {
const { isMation, loading } = useAutomatischInfo();
if (loading) return <React.Fragment />;
if (isMation) return <MationLogo />;
return (
<Typography variant="h6" component="h1" data-test="typography-logo" noWrap>
<FormattedMessage id="brandText" />
</Typography>
);
};
export default DefaultLogo;

View File

@@ -12,6 +12,7 @@ import * as URLS from 'config/urls';
import useVersion from 'hooks/useVersion';
import AppBar from 'components/AppBar';
import Drawer from 'components/Drawer';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
type PublicLayoutProps = {
children: React.ReactNode;
@@ -38,19 +39,36 @@ const drawerLinks = [
},
];
const generateDrawerBottomLinks = ({ notificationBadgeContent = 0 }) => [
{
Icon: NotificationsIcon,
primary: 'settingsDrawer.notifications',
to: URLS.UPDATES,
badgeContent: notificationBadgeContent,
},
];
type GenerateDrawerBottomLinksOptions = {
isMation: boolean;
loading: boolean;
notificationBadgeContent: number;
};
const generateDrawerBottomLinks = ({
isMation,
loading,
notificationBadgeContent = 0,
}: GenerateDrawerBottomLinksOptions) => {
if (loading || isMation) {
return [];
}
return [
{
Icon: NotificationsIcon,
primary: 'settingsDrawer.notifications',
to: URLS.UPDATES,
badgeContent: notificationBadgeContent,
},
];
};
export default function PublicLayout({
children,
}: PublicLayoutProps): React.ReactElement {
const version = useVersion();
const { isMation, loading } = useAutomatischInfo();
const theme = useTheme();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg'));
const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens);
@@ -60,6 +78,8 @@ export default function PublicLayout({
const drawerBottomLinks = generateDrawerBottomLinks({
notificationBadgeContent: version.newVersionCount,
loading,
isMation,
});
return (

View File

@@ -1,8 +1,7 @@
import Typography from '@mui/material/Typography';
import * as React from 'react';
import { FormattedMessage } from 'react-intl';
import CustomLogo from 'components/CustomLogo/index.ee';
import DefaultLogo from 'components/DefaultLogo';
import useConfig from 'hooks/useConfig';
const Logo = () => {
@@ -13,11 +12,7 @@ const Logo = () => {
if (logoSvgData) return <CustomLogo />;
return (
<Typography variant="h6" component="h1" data-test="typography-logo" noWrap>
<FormattedMessage id="brandText" />
</Typography>
);
return <DefaultLogo />;
};
export default Logo;

View File

@@ -0,0 +1,3 @@
<svg width="115" height="22" viewBox="0 0 411 77" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.991 15.3555C281.231 15.3555 284.669 11.918 284.669 7.67773C284.669 3.43744 281.231 0 276.991 0C272.751 0 269.313 3.43744 269.313 7.67773C269.313 11.918 272.751 15.3555 276.991 15.3555ZM367.497 50.0315C367.497 41.5346 374.385 34.6464 382.882 34.6464C391.379 34.6464 398.267 41.5346 398.267 50.0315V71.2076H409.851V50.0315C409.851 35.1371 397.777 23.0627 382.882 23.0627C367.988 23.0627 355.914 35.1371 355.914 50.0315V71.2076H367.497V50.0315ZM271.199 71.2071V28.8539H282.783V71.2071H271.199ZM237.933 34.6464V71.2076H249.517V34.6464H259.608V23.0627H249.517V7.67718H237.933V23.0627H227.843V34.6464H237.933ZM176.899 50.0296C176.899 58.5265 183.787 65.4146 192.284 65.4146V76.9983C177.389 76.9983 165.315 64.924 165.315 50.0296C165.315 35.1351 177.389 23.0608 192.284 23.0608C207.178 23.0608 219.252 35.1359 219.252 50.0303L219.253 71.2068H207.669L207.669 50.0303C207.669 41.5334 200.781 34.6445 192.284 34.6445C183.787 34.6445 176.899 41.5326 176.899 50.0296ZM71.0145 50.0315C71.0145 41.5346 77.9026 34.6464 86.3995 34.6464C94.8965 34.6464 101.785 41.5346 101.785 50.0315V71.2071H113.368V50.0315C113.368 41.5346 120.256 34.6464 128.753 34.6464C137.25 34.6464 144.138 41.5346 144.138 50.0315L144.138 71.2071H155.722L155.722 50.0315C155.722 35.1371 143.647 23.0627 128.753 23.0627C120.165 23.0627 112.515 27.0767 107.576 33.3308C102.637 27.0767 94.9873 23.0627 86.3995 23.0627C71.5051 23.0627 59.4308 35.1371 59.4308 50.0315V71.2071H71.0145V50.0315ZM44.0459 65.4162V76.9999H1.69178V65.4162H44.0459ZM292.376 50.0305C292.376 64.925 304.45 76.9993 319.345 76.9993C334.239 76.9993 346.313 64.9257 346.313 50.0313C346.313 35.1369 334.239 23.0618 319.345 23.0618C304.45 23.0618 292.376 35.1361 292.376 50.0305ZM319.345 65.4157C310.848 65.4157 303.96 58.5276 303.96 50.0306C303.96 41.5337 310.848 34.6456 319.345 34.6456C327.842 34.6456 334.729 41.5345 334.729 50.0314C334.729 58.5283 327.842 65.4157 319.345 65.4157Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,8 @@
import * as React from 'react';
import { ReactComponent as MationLogoSvg } from './assets/mation-logo.svg';
const MationLogo = () => {
return <MationLogoSvg />;
};
export default MationLogo;

View File

@@ -7,19 +7,20 @@ import * as React from 'react';
import { IJSONObject } from '@automatisch/types';
import useConfig from 'hooks/useConfig';
import theme from 'styles/theme';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
import { defaultTheme, mationTheme } from 'styles/theme';
type ThemeProviderProps = {
children: React.ReactNode;
};
const customizeTheme = (defaultTheme: typeof theme, config: IJSONObject) => {
const customizeTheme = (theme: typeof defaultTheme, config: IJSONObject) => {
// `clone` is needed so that the new theme reference triggers re-render
const shallowDefaultTheme = clone(defaultTheme);
const shallowDefaultTheme = clone(theme);
for (const key in config) {
const value = config[key];
const exists = get(defaultTheme, key);
const exists = get(theme, key);
if (exists) {
set(shallowDefaultTheme, key, value);
@@ -33,18 +34,21 @@ const ThemeProvider = ({
children,
...props
}: ThemeProviderProps): React.ReactElement => {
const { config, loading } = useConfig();
const { isMation, loading: automatischInfoLoading } = useAutomatischInfo();
const { config, loading: configLoading } = useConfig();
const customTheme = React.useMemo(() => {
if (!config) return theme;
const installationTheme = isMation ? mationTheme : defaultTheme;
const customTheme = customizeTheme(theme, config);
if (configLoading || automatischInfoLoading) return installationTheme;
const customTheme = customizeTheme(installationTheme, config || {});
return customTheme;
}, [config]);
}, [configLoading, config, isMation, automatischInfoLoading]);
// TODO: maybe a global loading state for the custom theme?
if (loading) return <></>;
if (automatischInfoLoading || configLoading) return <></>;
return (
<BaseThemeProvider theme={customTheme} {...props}>

View File

@@ -103,13 +103,6 @@ export const ADMIN_APP_AUTH_CLIENTS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/au
export const ADMIN_APP_CONNECTIONS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/connections`;
export const ADMIN_APP_CONNECTIONS = (appKey: string) =>
`${ADMIN_SETTINGS}/apps/${appKey}/connections`;
export const ADMIN_APP_CONNECTIONS_CREATE = (appKey: string, shared = false) =>
`${ADMIN_SETTINGS}/apps/${appKey}/connections/create?shared=${shared}`;
export const ADMIN_APP_CONNECTIONS_CREATE_WITH_AUTH_CLIENT_ID = (
appKey: string,
appAuthClientId: string
) =>
`${ADMIN_SETTINGS}/apps/${appKey}/connections/create?appAuthClientId=${appAuthClientId}`;
export const ADMIN_APP_SETTINGS = (appKey: string) =>
`${ADMIN_SETTINGS}/apps/${appKey}/settings`;
export const ADMIN_APP_AUTH_CLIENTS = (appKey: string) =>
@@ -118,23 +111,6 @@ export const ADMIN_APP_AUTH_CLIENT = (appKey: string, id: string) =>
`${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/${id}`;
export const ADMIN_APP_AUTH_CLIENTS_CREATE = (appKey: string) =>
`${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/create`;
export const ADMIN_APP_RECONNECT_CONNECTION = (
appKey: string,
connectionId: string,
appAuthClientId?: string
) => {
const path = `${ADMIN_SETTINGS}/apps/${appKey}/connections/${connectionId}/reconnect`;
if (appAuthClientId) {
return `${path}?appAuthClientId=${appAuthClientId}`;
}
return path;
};
export const ADMIN_APP_SHARE_CONNECTION = (
appKey: string,
connectionId: string
) => `${ADMIN_SETTINGS}/apps/${appKey}/connections/${connectionId}/share`;
export const DASHBOARD = FLOWS;

View File

@@ -5,11 +5,15 @@ import { GET_AUTOMATISCH_INFO } from 'graphql/queries/get-automatisch-info';
export type AutomatischInfoContextParams = {
isCloud: boolean;
isMation: boolean;
loading: boolean;
};
export const AutomatischInfoContext =
React.createContext<AutomatischInfoContextParams>({
isCloud: false,
isMation: false,
loading: true,
});
type AutomatischInfoProviderProps = {
@@ -23,13 +27,15 @@ export const AutomatischInfoProvider = (
const { data, loading } = useQuery(GET_AUTOMATISCH_INFO);
const isCloud = data?.getAutomatischInfo?.isCloud;
const isMation = data?.getAutomatischInfo?.isMation;
const value = React.useMemo(() => {
return {
isCloud,
loading
isMation,
loading,
};
}, [isCloud, loading]);
}, [isCloud, isMation, loading]);
return (
<AutomatischInfoContext.Provider value={value}>

View File

@@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const SHARE_CONNECTION = gql`
mutation ShareConnection($input: ShareConnectionInput) {
shareConnection(input: $input) {
id
}
}
`;

View File

@@ -7,7 +7,6 @@ export const GET_APP_CONNECTIONS = gql`
connections {
id
key
shared
reconnectable
appAuthClientId
verified

View File

@@ -4,6 +4,7 @@ export const GET_AUTOMATISCH_INFO = gql`
query GetAutomatischInfo {
getAutomatischInfo {
isCloud
isMation
}
}
`;

View File

@@ -1,7 +0,0 @@
import { gql } from '@apollo/client';
export const GET_SHARED_CONNECTION_ROLE_IDS = gql`
query GetSharedConnectionRoleIds($id: String!) {
getSharedConnectionRoleIds(id: $id)
}
`;

View File

@@ -3,6 +3,8 @@ import { AutomatischInfoContext } from 'contexts/AutomatischInfo';
type UseAutomatischInfoReturn = {
isCloud: boolean;
isMation: boolean;
loading: boolean;
};
export default function useAutomatischInfo(): UseAutomatischInfoReturn {
@@ -10,5 +12,7 @@ export default function useAutomatischInfo(): UseAutomatischInfoReturn {
return {
isCloud: automatischInfoContext.isCloud,
isMation: automatischInfoContext.isMation,
loading: automatischInfoContext.loading,
};
}

View File

@@ -5,16 +5,13 @@ import { GET_ROLES } from 'graphql/queries/get-roles.ee';
type QueryResponse = {
getRoles: IRole[];
};
}
export default function useRoles() {
const { data, loading, error } = useQuery<QueryResponse>(GET_ROLES, {
context: { autoSnackbar: false },
});
const { data, loading } = useQuery<QueryResponse>(GET_ROLES, { context: { autoSnackbar: false } });
return {
roles: data?.getRoles || [],
loading,
error,
loading
};
}

View File

@@ -1,32 +0,0 @@
import * as React from 'react';
import { LazyQueryHookOptions, useLazyQuery } from '@apollo/client';
import { GET_SHARED_CONNECTION_ROLE_IDS } from 'graphql/queries/get-shared-connection-role-ids';
type QueryResponse = {
getSharedConnectionRoleIds: string[];
};
export default function useSharedConnectionRoleIds(
connectionId: string,
options?: LazyQueryHookOptions
) {
const [getSharedConnectionRoleIds, { data, loading, error }] =
useLazyQuery<QueryResponse>(GET_SHARED_CONNECTION_ROLE_IDS, options);
React.useEffect(() => {
if (connectionId) {
getSharedConnectionRoleIds({
variables: {
id: connectionId,
},
});
}
}, [connectionId]);
return {
roleIds: data?.getSharedConnectionRoleIds || [],
loading,
error,
};
}

View File

@@ -265,21 +265,5 @@
"authClient.buttonSubmit": "Submit",
"authClient.inputName": "Name",
"authClient.inputActive": "Active",
"updateAuthClient.title": "Update auth client",
"adminAppsConnections.noConnections": "You don't have any connections yet.",
"adminAppsConnections.createConnection": "Create connection",
"adminAppsConnections.deletedMessage": "The connection has been deleted.",
"adminAppsConnections.addedAt": "added {datetime}",
"adminAppsConnections.testing": "Testing...",
"adminAppsConnections.testSuccessful": "Test successful",
"adminAppsConnections.testFailed": "Test failed",
"adminAppsConnections.testConnection": "Test connection",
"adminAppsConnections.delete": "Delete",
"adminAppsConnections.reconnect": "Reconnect",
"adminAppsConnections.shareConnection": "Share connection",
"adminAppsConnections.reconnectConnection": "Reconnect connection",
"adminAppsConnections.callToDocs": "Visit <docsLink>our documentation</docsLink> to see how to add connection for {appName}.",
"adminAppsConnections.submit": "Submit",
"adminAppsConnections.selectAll": "Select all roles",
"adminAppsConnections.deselectAll": "Deselect all roles"
"updateAuthClient.title": "Update auth client"
}

View File

@@ -27,26 +27,9 @@ import AdminApplicationSettings from 'components/AdminApplicationSettings';
import AdminApplicationAuthClients from 'components/AdminApplicationAuthClients';
import AdminApplicationCreateAuthClient from 'components/AdminApplicationCreateAuthClient';
import AdminApplicationUpdateAuthClient from 'components/AdminApplicationUpdateAuthClient';
import AdminApplicationConnections from 'components/AdminApplicationConnections';
import AdminApplicationConnectionCreate from 'components/AdminApplicationConnectionCreate';
import AdminApplicationConnectionShare from 'components/AdminApplicationConnectionShare';
type AdminApplicationParams = {
appKey: string;
connectionId?: string;
};
const ReconnectConnection = (props: any): React.ReactElement => {
const { application, onClose } = props;
const { connectionId } = useParams() as AdminApplicationParams;
return (
<AdminApplicationConnectionCreate
onClose={onClose}
application={application}
connectionId={connectionId}
/>
);
};
export default function AdminApplication(): React.ReactElement | null {
@@ -74,7 +57,6 @@ export default function AdminApplication(): React.ReactElement | null {
const app = data?.getApp || {};
const goToAuthClientsPage = () => navigate('auth-clients');
const goToConnectionsPage = () => navigate('connections');
if (loading) return null;
@@ -138,7 +120,7 @@ export default function AdminApplication(): React.ReactElement | null {
/>
<Route
path={`/connections/*`}
element={<AdminApplicationConnections appKey={appKey} />}
element={<div>App connections</div>}
/>
<Route
path="/"
@@ -171,33 +153,6 @@ export default function AdminApplication(): React.ReactElement | null {
/>
}
/>
<Route
path="/connections/create"
element={
<AdminApplicationConnectionCreate
onClose={goToConnectionsPage}
application={app}
/>
}
/>
<Route
path="/connections/:connectionId/reconnect"
element={
<ReconnectConnection
application={app}
onClose={goToConnectionsPage}
/>
}
/>
<Route
path="/connections/:connectionId/share"
element={
<AdminApplicationConnectionShare
onClose={goToConnectionsPage}
application={app}
/>
}
/>
</Routes>
</>
);

View File

@@ -65,8 +65,8 @@ function RoleMappings({ provider, providerLoading }: RoleMappingsProps) {
enqueueSnackbar(formatMessage('roleMappingsForm.successfullySaved'), {
variant: 'success',
SnackbarProps: {
'data-test': 'snackbar-update-role-mappings-success',
},
'data-test': 'snackbar-update-role-mappings-success'
}
});
}
} catch (error) {

View File

@@ -5,12 +5,13 @@ import Stack from '@mui/material/Stack';
import DeleteIcon from '@mui/icons-material/Delete';
import IconButton from '@mui/material/IconButton';
import Button from '@mui/material/Button';
import { Divider, Typography } from '@mui/material';
import useRoles from 'hooks/useRoles.ee';
import useFormatMessage from 'hooks/useFormatMessage';
import ControlledAutocomplete from 'components/ControlledAutocomplete';
import TextField from 'components/TextField';
import { Divider, Typography } from '@mui/material';
function generateRoleOptions(roles: IRole[]) {
return roles?.map(({ name: label, id: value }) => ({ label, value }));

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import Box from '@mui/material/Box';
import Stack from '@mui/material/Stack';
@@ -7,6 +8,8 @@ import Container from 'components/Container';
import NotificationCard from 'components/NotificationCard';
import PageTitle from 'components/PageTitle';
import useFormatMessage from 'hooks/useFormatMessage';
import useAutomatischInfo from 'hooks/useAutomatischInfo';
import * as URLS from 'config/urls';
interface INotification {
name: string;
@@ -16,8 +19,19 @@ interface INotification {
}
export default function Updates(): React.ReactElement {
const navigate = useNavigate();
const formatMessage = useFormatMessage();
const { notifications } = useNotifications();
const { isMation, loading } = useAutomatischInfo();
React.useEffect(
function redirectToHomepageInMation() {
if (!loading && isMation) {
navigate(URLS.DASHBOARD);
}
},
[loading, isMation]
);
return (
<Box sx={{ py: 3 }}>

View File

@@ -1,3 +1,5 @@
import { deepmerge } from '@mui/utils';
import type { Theme } from '@mui/material/styles';
import { createTheme, alpha } from '@mui/material/styles';
import { cardActionAreaClasses } from '@mui/material/CardActionArea';
@@ -6,7 +8,7 @@ export const primaryMainColor = '#0059F7';
export const primaryLightColor = '#4286FF';
export const primaryDarkColor = '#001F52';
const extendedTheme = createTheme({
export const defaultTheme = createTheme({
palette: {
primary: {
main: primaryMainColor,
@@ -280,4 +282,24 @@ const extendedTheme = createTheme({
},
});
export default extendedTheme;
export const mationTheme = createTheme(deepmerge(defaultTheme, {
palette: {
primary: {
main: '#2962FF',
light: '#448AFF',
dark: '#2962FF',
contrastText: '#fff',
},
},
components: {
MuiAppBar: {
styleOverrides: {
root: ({ theme }: { theme: Theme }) => ({
zIndex: theme.zIndex.drawer + 1,
}),
},
},
},
}));
export default defaultTheme;

View File

@@ -7106,11 +7106,6 @@ commander@7.1.0:
resolved "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz"
integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg==
commander@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz"
@@ -7126,7 +7121,7 @@ commander@^8.3.0:
resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
commander@^9.0.0:
commander@^9.0.0, commander@^9.1.0:
version "9.5.0"
resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz"
integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
@@ -12266,13 +12261,13 @@ klona@^2.0.4, klona@^2.0.5:
resolved "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz"
integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==
knex@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/knex/-/knex-2.5.1.tgz#a6c6b449866cf4229f070c17411f23871ba52ef9"
integrity sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA==
knex@^2.4.0:
version "2.4.0"
resolved "https://registry.npmjs.org/knex/-/knex-2.4.0.tgz"
integrity sha512-i0GWwqYp1Hs2yvc2rlDO6nzzkLhwdyOZKRdsMTB8ZxOs2IXQyL5rBjSbS1krowCh6V65T4X9CJaKtuIfkaPGSA==
dependencies:
colorette "2.0.19"
commander "^10.0.0"
commander "^9.1.0"
debug "4.3.4"
escalade "^3.1.1"
esm "^3.2.25"
@@ -12280,7 +12275,7 @@ knex@^2.5.1:
getopts "2.3.0"
interpret "^2.2.0"
lodash "^4.17.21"
pg-connection-string "2.6.1"
pg-connection-string "2.5.0"
rechoir "^0.8.0"
resolve-from "^5.0.0"
tarn "^3.0.2"
@@ -13864,13 +13859,12 @@ object.values@^1.1.0, object.values@^1.1.5:
define-properties "^1.1.3"
es-abstract "^1.19.1"
objection@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/objection/-/objection-3.1.1.tgz#b744d4ff13c01863d6edec773f1315c964442510"
integrity sha512-v8dqQrFwZm9gRN3ZF4abF+hL6Jm5EbcUjOxVDan0lheOev0sggGGHBP8jgesZ68I0XXBjDFjGXCjTPZsWDu49A==
objection@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/objection/-/objection-3.0.1.tgz"
integrity sha512-rqNnyQE+C55UHjdpTOJEKQHJGZ/BGtBBtgxdUpKG4DQXRUmqxfmgS/MhPWxB9Pw0mLSVLEltr6soD4c0Sddy0Q==
dependencies:
ajv "^8.6.2"
ajv-formats "^2.1.1"
db-errors "^0.2.3"
obuf@^1.0.0, obuf@^1.1.2:
@@ -14391,12 +14385,7 @@ performance-now@^2.1.0:
resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
pg-connection-string@2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb"
integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==
pg-connection-string@^2.5.0:
pg-connection-string@2.5.0, pg-connection-string@^2.5.0:
version "2.5.0"
resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz"
integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==