Compare commits
4 Commits
AUT-500
...
shared-con
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8f09681771 | ||
![]() |
3801f9cfa0 | ||
![]() |
b6eea0b5fc | ||
![]() |
099a8ea2cf |
@@ -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.0.0",
|
||||
"objection": "^3.1.1",
|
||||
"passport": "^0.6.0",
|
||||
"pg": "^8.7.1",
|
||||
"php-serialize": "^4.0.2",
|
||||
|
@@ -0,0 +1,15 @@
|
||||
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');
|
||||
}
|
@@ -19,6 +19,7 @@ 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';
|
||||
@@ -55,6 +56,7 @@ const mutationResolvers = {
|
||||
registerUser,
|
||||
resetConnection,
|
||||
resetPassword,
|
||||
shareConnection,
|
||||
updateAppAuthClient,
|
||||
updateAppConfig,
|
||||
updateConfig,
|
||||
|
@@ -28,11 +28,11 @@ const createFlow = async (
|
||||
});
|
||||
|
||||
if (connectionId) {
|
||||
const hasConnection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
const connection = await context.currentUser
|
||||
.relatedConnectionsQuery()
|
||||
.findById(connectionId);
|
||||
|
||||
if (!hasConnection) {
|
||||
if (!connection) {
|
||||
throw new Error('The connection does not exist!');
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Connection from '../../models/connection';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
@@ -11,10 +12,13 @@ const deleteConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('delete', 'Connection');
|
||||
const conditions = context.currentUser.can('delete', 'Connection');
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const baseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
|
||||
await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
await baseQuery
|
||||
.clone()
|
||||
.delete()
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
|
@@ -4,6 +4,7 @@ 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';
|
||||
|
||||
@@ -14,51 +15,87 @@ const deleteCurrentUser = async (
|
||||
) => {
|
||||
const id = context.currentUser.id;
|
||||
|
||||
const flows = await context.currentUser.$relatedQuery('flows').where({
|
||||
active: true,
|
||||
});
|
||||
try {
|
||||
await User.transaction(async (trx) => {
|
||||
const flows = await context.currentUser
|
||||
.$relatedQuery('flows', trx)
|
||||
.where({
|
||||
active: true,
|
||||
});
|
||||
|
||||
const repeatableJobs = await flowQueue.getRepeatableJobs();
|
||||
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();
|
||||
|
||||
for (const flow of flows) {
|
||||
const job = repeatableJobs.find((job) => job.id === flow.id);
|
||||
if (count) {
|
||||
throw new Error('The shared connections must be removed first!');
|
||||
}
|
||||
|
||||
if (job) {
|
||||
await flowQueue.removeRepeatableByKey(job.key);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import Context from '../../types/express/context';
|
||||
import Connection from '../../models/connection';
|
||||
|
||||
type Params = {
|
||||
input: {
|
||||
@@ -11,10 +12,13 @@ const resetConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('create', 'Connection');
|
||||
const conditions = context.currentUser.can('update', 'Connection');
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const baseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
|
||||
let connection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
let connection = await baseQuery
|
||||
.clone()
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
})
|
||||
|
@@ -0,0 +1,55 @@
|
||||
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;
|
@@ -1,6 +1,7 @@
|
||||
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: {
|
||||
@@ -15,10 +16,13 @@ const updateConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
context.currentUser.can('create', 'Connection');
|
||||
const conditions = context.currentUser.can('update', 'Connection');
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const allConnections = Connection.query();
|
||||
const baseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
|
||||
let connection = await context.currentUser
|
||||
.$relatedQuery('connections')
|
||||
let connection = await baseQuery
|
||||
.clone()
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
})
|
||||
|
@@ -45,10 +45,11 @@ const updateStep = async (
|
||||
|
||||
canSeeAllConnections = !conditions.isCreator;
|
||||
} catch {
|
||||
// void
|
||||
// The user does not have permission to read any connections!
|
||||
throw new Error('The connection does not exist!');
|
||||
}
|
||||
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const userConnections = context.currentUser.relatedConnectionsQuery();
|
||||
const allConnections = Connection.query();
|
||||
const baseConnectionsQuery = canSeeAllConnections ? allConnections : userConnections;
|
||||
|
||||
|
@@ -9,28 +9,55 @@ 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 connections = await connectionBaseQuery
|
||||
.clone()
|
||||
.select('connections.*')
|
||||
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')
|
||||
.withGraphFetched({
|
||||
appConfig: true,
|
||||
appAuthClient: true
|
||||
})
|
||||
.fullOuterJoinRelated('steps')
|
||||
.where({
|
||||
'connections.key': params.key,
|
||||
'connections.draft': false,
|
||||
})
|
||||
.countDistinct('steps.flow_id as flowCount')
|
||||
.groupBy('connections.id')
|
||||
.orderBy('created_at', 'desc');
|
||||
.joinRaw('join connections_with_flow_count on connections.id = connections_with_flow_count.id')
|
||||
.orderBy('connections.created_at', 'desc');
|
||||
|
||||
return {
|
||||
...app,
|
||||
|
@@ -15,7 +15,7 @@ const getConnectedApps = async (
|
||||
) => {
|
||||
const conditions = context.currentUser.can('read', 'Connection');
|
||||
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const userConnections = context.currentUser.relatedConnectionsQuery();
|
||||
const allConnections = Connection.query();
|
||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
|
||||
@@ -25,8 +25,9 @@ const getConnectedApps = async (
|
||||
|
||||
let apps = await App.findAll(params.name);
|
||||
|
||||
const connections = await connectionBaseQuery
|
||||
.clone()
|
||||
const connections = await Connection
|
||||
.query()
|
||||
.with('connections', connectionBaseQuery)
|
||||
.select('connections.key')
|
||||
.where({ draft: false })
|
||||
.count('connections.id as count')
|
||||
|
@@ -0,0 +1,29 @@
|
||||
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;
|
@@ -13,15 +13,15 @@ const testConnection = async (
|
||||
params: Params,
|
||||
context: Context
|
||||
) => {
|
||||
const conditions = context.currentUser.can('update', 'Connection');
|
||||
const userConnections = context.currentUser.$relatedQuery('connections');
|
||||
const conditions = context.currentUser.can('read', 'Connection');
|
||||
const userConnections = context.currentUser.relatedConnectionsQuery();
|
||||
const allConnections = Connection.query();
|
||||
const connectionBaseQuery = conditions.isCreator ? userConnections : allConnections;
|
||||
|
||||
let connection = await connectionBaseQuery
|
||||
.clone()
|
||||
.findOne({
|
||||
id: params.id,
|
||||
'connections.id': params.id,
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
|
@@ -24,6 +24,7 @@ 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';
|
||||
@@ -60,6 +61,7 @@ const queryResolvers = {
|
||||
getRoles,
|
||||
getSamlAuthProvider,
|
||||
getSamlAuthProviderRoleMappings,
|
||||
getSharedConnectionRoleIds,
|
||||
getStepWithTestExecutions,
|
||||
getSubscriptionStatus,
|
||||
getTrialStatus,
|
||||
|
@@ -53,6 +53,7 @@ type Query {
|
||||
getNotifications: [Notification]
|
||||
getSamlAuthProvider: SamlAuthProvider
|
||||
getSamlAuthProviderRoleMappings(id: String!): [SamlAuthProvidersRoleMapping]
|
||||
getSharedConnectionRoleIds(id: String!): [String]
|
||||
getSubscriptionStatus: GetSubscriptionStatus
|
||||
getTrialStatus: GetTrialStatus
|
||||
getUser(id: String!): User
|
||||
@@ -83,6 +84,7 @@ 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
|
||||
@@ -244,6 +246,7 @@ type AuthLink {
|
||||
type Connection {
|
||||
id: String
|
||||
key: String
|
||||
shared: Boolean
|
||||
reconnectable: Boolean
|
||||
appAuthClientId: String
|
||||
formattedData: ConnectionData
|
||||
@@ -810,6 +813,11 @@ input ExecutionFiltersInput {
|
||||
status: String
|
||||
}
|
||||
|
||||
input ShareConnectionInput {
|
||||
id: String!
|
||||
roleIds: [String]
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import { QueryContext, ModelOptions } from 'objection';
|
||||
import type { RelationMappings } from 'objection';
|
||||
import { IJSONObject, IRequest } from '@automatisch/types';
|
||||
import { AES, enc } from 'crypto-js';
|
||||
import { IRequest } from '@automatisch/types';
|
||||
import App from './app';
|
||||
import AppConfig from './app-config';
|
||||
import AppAuthClient from './app-auth-client';
|
||||
import Base from './base';
|
||||
import User from './user';
|
||||
import Step from './step';
|
||||
import ExtendedQueryBuilder from './query-builder';
|
||||
import type { RelationMappings } from 'objection';
|
||||
import { ModelOptions, QueryContext } from 'objection';
|
||||
import appConfig from '../config/app';
|
||||
import { IJSONObject } from '@automatisch/types';
|
||||
import Telemetry from '../helpers/telemetry';
|
||||
import globalVariable from '../helpers/global-variable';
|
||||
import Telemetry from '../helpers/telemetry';
|
||||
import App from './app';
|
||||
import AppAuthClient from './app-auth-client';
|
||||
import AppConfig from './app-config';
|
||||
import Base from './base';
|
||||
import ExtendedQueryBuilder from './query-builder';
|
||||
import SharedConnection from './shared-connection';
|
||||
import Step from './step';
|
||||
import User from './user';
|
||||
|
||||
class Connection extends Base {
|
||||
id!: string;
|
||||
@@ -24,6 +24,9 @@ class Connection extends Base {
|
||||
draft: boolean;
|
||||
count?: number;
|
||||
flowCount?: number;
|
||||
sharedConnections?: SharedConnection[];
|
||||
// computed via `User.relevantConnectionsQuery`
|
||||
shared?: boolean;
|
||||
user?: User;
|
||||
steps?: Step[];
|
||||
triggerSteps?: Step[];
|
||||
@@ -46,6 +49,7 @@ 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' },
|
||||
@@ -100,6 +104,14 @@ 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() {
|
||||
|
45
packages/backend/src/models/shared-connection.ts
Normal file
45
packages/backend/src/models/shared-connection.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
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;
|
@@ -1,7 +1,7 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import { DateTime } from 'luxon';
|
||||
import crypto from 'node:crypto';
|
||||
import { ModelOptions, QueryContext } from 'objection';
|
||||
import { raw, ModelOptions, QueryContext } from 'objection';
|
||||
|
||||
import appConfig from '../config/app';
|
||||
import { hasValidLicense } from '../helpers/license.ee';
|
||||
@@ -28,6 +28,7 @@ class User extends Base {
|
||||
resetPasswordTokenSentAt: string;
|
||||
trialExpiryDate: string;
|
||||
connections?: Connection[];
|
||||
sharedConnections?: Connection[];
|
||||
flows?: Flow[];
|
||||
steps?: Step[];
|
||||
executions?: Execution[];
|
||||
@@ -69,6 +70,18 @@ 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,
|
||||
@@ -165,6 +178,40 @@ 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);
|
||||
}
|
||||
|
1
packages/types/index.d.ts
vendored
1
packages/types/index.d.ts
vendored
@@ -23,6 +23,7 @@ export interface IConnection {
|
||||
formattedData?: IJSONObject;
|
||||
userId: string;
|
||||
verified: boolean;
|
||||
shared?: boolean;
|
||||
count?: number;
|
||||
flowCount?: number;
|
||||
appData?: IApp;
|
||||
|
@@ -0,0 +1,168 @@
|
||||
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>
|
||||
);
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
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),
|
||||
}));
|
@@ -0,0 +1,70 @@
|
||||
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;
|
@@ -0,0 +1,140 @@
|
||||
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>
|
||||
);
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
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),
|
||||
}));
|
@@ -0,0 +1,85 @@
|
||||
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>
|
||||
);
|
||||
}
|
@@ -0,0 +1,155 @@
|
||||
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;
|
@@ -0,0 +1,16 @@
|
||||
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',
|
||||
}));
|
@@ -0,0 +1,55 @@
|
||||
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;
|
@@ -53,6 +53,7 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
createdAt,
|
||||
flowCount,
|
||||
reconnectable,
|
||||
shared,
|
||||
} = props.connection;
|
||||
|
||||
const contextButtonRef = React.useRef<SVGSVGElement | null>(null);
|
||||
@@ -105,7 +106,7 @@ function AppConnectionRow(props: AppConnectionRowProps): React.ReactElement {
|
||||
<CardContent>
|
||||
<Stack justifyContent="center" alignItems="flex-start" spacing={1}>
|
||||
<Typography variant="h6" sx={{ textAlign: 'left' }}>
|
||||
{formattedData?.screenName}
|
||||
{formattedData?.screenName} {shared && 'shared'}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption">
|
||||
|
@@ -103,6 +103,13 @@ 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) =>
|
||||
@@ -111,6 +118,23 @@ 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;
|
||||
|
||||
|
9
packages/web/src/graphql/mutations/share-connection.ts
Normal file
9
packages/web/src/graphql/mutations/share-connection.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SHARE_CONNECTION = gql`
|
||||
mutation ShareConnection($input: ShareConnectionInput) {
|
||||
shareConnection(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
@@ -7,6 +7,7 @@ export const GET_APP_CONNECTIONS = gql`
|
||||
connections {
|
||||
id
|
||||
key
|
||||
shared
|
||||
reconnectable
|
||||
appAuthClientId
|
||||
verified
|
||||
|
@@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_SHARED_CONNECTION_ROLE_IDS = gql`
|
||||
query GetSharedConnectionRoleIds($id: String!) {
|
||||
getSharedConnectionRoleIds(id: $id)
|
||||
}
|
||||
`;
|
@@ -5,13 +5,16 @@ import { GET_ROLES } from 'graphql/queries/get-roles.ee';
|
||||
|
||||
type QueryResponse = {
|
||||
getRoles: IRole[];
|
||||
}
|
||||
};
|
||||
|
||||
export default function useRoles() {
|
||||
const { data, loading } = useQuery<QueryResponse>(GET_ROLES, { context: { autoSnackbar: false } });
|
||||
const { data, loading, error } = useQuery<QueryResponse>(GET_ROLES, {
|
||||
context: { autoSnackbar: false },
|
||||
});
|
||||
|
||||
return {
|
||||
roles: data?.getRoles || [],
|
||||
loading
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
32
packages/web/src/hooks/useSharedConnectionRoleIds.ts
Normal file
32
packages/web/src/hooks/useSharedConnectionRoleIds.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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,
|
||||
};
|
||||
}
|
@@ -265,5 +265,21 @@
|
||||
"authClient.buttonSubmit": "Submit",
|
||||
"authClient.inputName": "Name",
|
||||
"authClient.inputActive": "Active",
|
||||
"updateAuthClient.title": "Update auth client"
|
||||
"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"
|
||||
}
|
||||
|
@@ -27,9 +27,26 @@ 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 {
|
||||
@@ -57,6 +74,7 @@ export default function AdminApplication(): React.ReactElement | null {
|
||||
const app = data?.getApp || {};
|
||||
|
||||
const goToAuthClientsPage = () => navigate('auth-clients');
|
||||
const goToConnectionsPage = () => navigate('connections');
|
||||
|
||||
if (loading) return null;
|
||||
|
||||
@@ -120,7 +138,7 @@ export default function AdminApplication(): React.ReactElement | null {
|
||||
/>
|
||||
<Route
|
||||
path={`/connections/*`}
|
||||
element={<div>App connections</div>}
|
||||
element={<AdminApplicationConnections appKey={appKey} />}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
@@ -153,6 +171,33 @@ 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>
|
||||
</>
|
||||
);
|
||||
|
@@ -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) {
|
||||
|
@@ -5,13 +5,12 @@ 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 }));
|
||||
|
35
yarn.lock
35
yarn.lock
@@ -7106,6 +7106,11 @@ 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"
|
||||
@@ -7121,7 +7126,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.1.0:
|
||||
commander@^9.0.0:
|
||||
version "9.5.0"
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz"
|
||||
integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==
|
||||
@@ -12261,13 +12266,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.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.npmjs.org/knex/-/knex-2.4.0.tgz"
|
||||
integrity sha512-i0GWwqYp1Hs2yvc2rlDO6nzzkLhwdyOZKRdsMTB8ZxOs2IXQyL5rBjSbS1krowCh6V65T4X9CJaKtuIfkaPGSA==
|
||||
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==
|
||||
dependencies:
|
||||
colorette "2.0.19"
|
||||
commander "^9.1.0"
|
||||
commander "^10.0.0"
|
||||
debug "4.3.4"
|
||||
escalade "^3.1.1"
|
||||
esm "^3.2.25"
|
||||
@@ -12275,7 +12280,7 @@ knex@^2.4.0:
|
||||
getopts "2.3.0"
|
||||
interpret "^2.2.0"
|
||||
lodash "^4.17.21"
|
||||
pg-connection-string "2.5.0"
|
||||
pg-connection-string "2.6.1"
|
||||
rechoir "^0.8.0"
|
||||
resolve-from "^5.0.0"
|
||||
tarn "^3.0.2"
|
||||
@@ -13859,12 +13864,13 @@ object.values@^1.1.0, object.values@^1.1.5:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.19.1"
|
||||
|
||||
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==
|
||||
objection@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/objection/-/objection-3.1.1.tgz#b744d4ff13c01863d6edec773f1315c964442510"
|
||||
integrity sha512-v8dqQrFwZm9gRN3ZF4abF+hL6Jm5EbcUjOxVDan0lheOev0sggGGHBP8jgesZ68I0XXBjDFjGXCjTPZsWDu49A==
|
||||
dependencies:
|
||||
ajv "^8.6.2"
|
||||
ajv-formats "^2.1.1"
|
||||
db-errors "^0.2.3"
|
||||
|
||||
obuf@^1.0.0, obuf@^1.1.2:
|
||||
@@ -14385,7 +14391,12 @@ 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.5.0, pg-connection-string@^2.5.0:
|
||||
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:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz"
|
||||
integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
|
||||
|
Reference in New Issue
Block a user