refactor: remove whole graphql implementation
This commit is contained in:
@@ -23,8 +23,6 @@
|
||||
"dependencies": {
|
||||
"@bull-board/express": "^3.10.1",
|
||||
"@casl/ability": "^6.5.0",
|
||||
"@graphql-tools/graphql-file-loader": "^7.3.4",
|
||||
"@graphql-tools/load": "^7.5.2",
|
||||
"@node-saml/passport-saml": "^4.0.4",
|
||||
"@rudderstack/rudder-sdk-node": "^1.1.2",
|
||||
"@sentry/node": "^7.42.0",
|
||||
@@ -41,11 +39,7 @@
|
||||
"express": "~4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"express-graphql": "^0.12.0",
|
||||
"fast-xml-parser": "^4.0.11",
|
||||
"graphql-middleware": "^6.1.15",
|
||||
"graphql-shield": "^7.5.0",
|
||||
"graphql-tools": "^8.2.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"http-errors": "~1.6.3",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
|
@@ -1,3 +0,0 @@
|
||||
const mutationResolvers = {};
|
||||
|
||||
export default mutationResolvers;
|
@@ -1,16 +0,0 @@
|
||||
import AppAuthClient from '../../models/app-auth-client';
|
||||
|
||||
const deleteAppAuthClient = async (_parent, params, context) => {
|
||||
context.currentUser.can('delete', 'App');
|
||||
|
||||
await AppAuthClient.query()
|
||||
.delete()
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export default deleteAppAuthClient;
|
@@ -1,18 +0,0 @@
|
||||
const updateFlow = async (_parent, params, context) => {
|
||||
context.currentUser.can('update', 'Flow');
|
||||
|
||||
let flow = await context.currentUser
|
||||
.$relatedQuery('flows')
|
||||
.findOne({
|
||||
id: params.input.id,
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
flow = await flow.$query().patchAndFetch({
|
||||
name: params.input.name,
|
||||
});
|
||||
|
||||
return flow;
|
||||
};
|
||||
|
||||
export default updateFlow;
|
@@ -1,62 +0,0 @@
|
||||
import Role from '../../models/role.js';
|
||||
import Permission from '../../models/permission.js';
|
||||
import permissionCatalog from '../../helpers/permission-catalog.ee.js';
|
||||
|
||||
const updateRole = async (_parent, params, context) => {
|
||||
context.currentUser.can('update', 'Role');
|
||||
|
||||
const { id, name, description, permissions } = params.input;
|
||||
|
||||
const role = await Role.query().findById(id).throwIfNotFound();
|
||||
|
||||
try {
|
||||
const updatedRole = await Role.transaction(async (trx) => {
|
||||
await role.$relatedQuery('permissions', trx).delete();
|
||||
|
||||
if (permissions?.length) {
|
||||
const sanitizedPermissions = permissions
|
||||
.filter((permission) => {
|
||||
const { action, subject, conditions } = permission;
|
||||
|
||||
const relevantAction = permissionCatalog.actions.find(
|
||||
(actionCatalogItem) => actionCatalogItem.key === action
|
||||
);
|
||||
const validSubject = relevantAction.subjects.includes(subject);
|
||||
const validConditions = conditions.every((condition) => {
|
||||
return !!permissionCatalog.conditions.find(
|
||||
(conditionCatalogItem) => conditionCatalogItem.key === condition
|
||||
);
|
||||
});
|
||||
|
||||
return validSubject && validConditions;
|
||||
})
|
||||
.map((permission) => ({
|
||||
...permission,
|
||||
roleId: role.id,
|
||||
}));
|
||||
|
||||
await Permission.query().insert(sanitizedPermissions);
|
||||
}
|
||||
|
||||
await role.$query(trx).patch({
|
||||
name,
|
||||
description,
|
||||
});
|
||||
|
||||
return await Role.query(trx)
|
||||
.leftJoinRelated({
|
||||
permissions: true,
|
||||
})
|
||||
.withGraphFetched({
|
||||
permissions: true,
|
||||
})
|
||||
.findById(id);
|
||||
});
|
||||
|
||||
return updatedRole;
|
||||
} catch (err) {
|
||||
throw new Error('The role could not be updated!');
|
||||
}
|
||||
};
|
||||
|
||||
export default updateRole;
|
@@ -1,7 +0,0 @@
|
||||
import mutationResolvers from './mutation-resolvers.js';
|
||||
|
||||
const resolvers = {
|
||||
Mutation: mutationResolvers,
|
||||
};
|
||||
|
||||
export default resolvers;
|
@@ -1,324 +0,0 @@
|
||||
type Query {
|
||||
placeholderQuery(name: String): Boolean
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
placeholderQuery(name: String): Boolean
|
||||
}
|
||||
|
||||
type Trigger {
|
||||
name: String
|
||||
key: String
|
||||
description: String
|
||||
showWebhookUrl: Boolean
|
||||
pollInterval: Int
|
||||
type: String
|
||||
substeps: [Substep]
|
||||
}
|
||||
|
||||
type Action {
|
||||
name: String
|
||||
key: String
|
||||
description: String
|
||||
substeps: [Substep]
|
||||
}
|
||||
|
||||
type Substep {
|
||||
key: String
|
||||
name: String
|
||||
arguments: [SubstepArgument]
|
||||
}
|
||||
|
||||
type SubstepArgument {
|
||||
label: String
|
||||
key: String
|
||||
type: String
|
||||
description: String
|
||||
required: Boolean
|
||||
variables: Boolean
|
||||
options: [SubstepArgumentOption]
|
||||
source: SubstepArgumentSource
|
||||
additionalFields: SubstepArgumentAdditionalFields
|
||||
dependsOn: [String]
|
||||
fields: [SubstepArgument]
|
||||
value: JSONObject
|
||||
}
|
||||
|
||||
type SubstepArgumentOption {
|
||||
label: String
|
||||
value: JSONObject
|
||||
}
|
||||
|
||||
type SubstepArgumentSource {
|
||||
type: String
|
||||
name: String
|
||||
arguments: [SubstepArgumentSourceArgument]
|
||||
}
|
||||
|
||||
type SubstepArgumentSourceArgument {
|
||||
name: String
|
||||
value: String
|
||||
}
|
||||
|
||||
type SubstepArgumentAdditionalFields {
|
||||
type: String
|
||||
name: String
|
||||
arguments: [SubstepArgumentAdditionalFieldsArgument]
|
||||
}
|
||||
|
||||
type SubstepArgumentAdditionalFieldsArgument {
|
||||
name: String
|
||||
value: String
|
||||
}
|
||||
|
||||
type App {
|
||||
name: String
|
||||
key: String
|
||||
connectionCount: Int
|
||||
flowCount: Int
|
||||
iconUrl: String
|
||||
docUrl: String
|
||||
authDocUrl: String
|
||||
primaryColor: String
|
||||
supportsConnections: Boolean
|
||||
auth: AppAuth
|
||||
triggers: [Trigger]
|
||||
actions: [Action]
|
||||
connections: [Connection]
|
||||
}
|
||||
|
||||
type AppAuth {
|
||||
fields: [Field]
|
||||
authenticationSteps: [AuthenticationStep]
|
||||
sharedAuthenticationSteps: [AuthenticationStep]
|
||||
reconnectionSteps: [ReconnectionStep]
|
||||
sharedReconnectionSteps: [ReconnectionStep]
|
||||
}
|
||||
|
||||
enum ArgumentEnumType {
|
||||
integer
|
||||
string
|
||||
}
|
||||
|
||||
type AuthenticationStep {
|
||||
type: String
|
||||
name: String
|
||||
arguments: [AuthenticationStepArgument]
|
||||
}
|
||||
|
||||
type AuthenticationStepArgument {
|
||||
name: String
|
||||
value: String
|
||||
type: ArgumentEnumType
|
||||
properties: [AuthenticationStepProperty]
|
||||
}
|
||||
|
||||
type AuthenticationStepProperty {
|
||||
name: String
|
||||
value: String
|
||||
}
|
||||
|
||||
type Connection {
|
||||
id: String
|
||||
key: String
|
||||
reconnectable: Boolean
|
||||
appAuthClientId: String
|
||||
formattedData: ConnectionData
|
||||
verified: Boolean
|
||||
app: App
|
||||
createdAt: String
|
||||
flowCount: Int
|
||||
}
|
||||
|
||||
type ConnectionData {
|
||||
screenName: String
|
||||
}
|
||||
|
||||
type ExecutionStep {
|
||||
id: String
|
||||
executionId: String
|
||||
stepId: String
|
||||
step: Step
|
||||
status: String
|
||||
dataIn: JSONObject
|
||||
dataOut: JSONObject
|
||||
errorDetails: JSONObject
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
}
|
||||
|
||||
type Field {
|
||||
key: String
|
||||
label: String
|
||||
type: String
|
||||
required: Boolean
|
||||
readOnly: Boolean
|
||||
value: String
|
||||
placeholder: String
|
||||
description: String
|
||||
docUrl: String
|
||||
clickToCopy: Boolean
|
||||
options: [SubstepArgumentOption]
|
||||
}
|
||||
|
||||
enum FlowStatus {
|
||||
paused
|
||||
published
|
||||
draft
|
||||
}
|
||||
|
||||
type Flow {
|
||||
id: String
|
||||
name: String
|
||||
active: Boolean
|
||||
steps: [Step]
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
status: FlowStatus
|
||||
}
|
||||
|
||||
type SamlAuthProvidersRoleMapping {
|
||||
id: String
|
||||
samlAuthProviderId: String
|
||||
roleId: String
|
||||
remoteRoleName: String
|
||||
}
|
||||
|
||||
input UserRoleInput {
|
||||
id: String
|
||||
}
|
||||
|
||||
"""
|
||||
The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
|
||||
"""
|
||||
scalar JSONObject
|
||||
|
||||
input PreviousStepInput {
|
||||
id: String
|
||||
}
|
||||
|
||||
type ReconnectionStep {
|
||||
type: String
|
||||
name: String
|
||||
arguments: [ReconnectionStepArgument]
|
||||
}
|
||||
|
||||
type ReconnectionStepArgument {
|
||||
name: String
|
||||
value: String
|
||||
type: ArgumentEnumType
|
||||
properties: [ReconnectionStepProperty]
|
||||
}
|
||||
|
||||
type ReconnectionStepProperty {
|
||||
name: String
|
||||
value: String
|
||||
}
|
||||
|
||||
type Step {
|
||||
id: String
|
||||
previousStepId: String
|
||||
key: String
|
||||
appKey: String
|
||||
iconUrl: String
|
||||
webhookUrl: String
|
||||
type: StepEnumType
|
||||
parameters: JSONObject
|
||||
connection: Connection
|
||||
flow: Flow
|
||||
position: Int
|
||||
status: String
|
||||
executionSteps: [ExecutionStep]
|
||||
}
|
||||
|
||||
input StepConnectionInput {
|
||||
id: String
|
||||
}
|
||||
|
||||
enum StepEnumType {
|
||||
trigger
|
||||
action
|
||||
}
|
||||
|
||||
input StepFlowInput {
|
||||
id: String
|
||||
}
|
||||
|
||||
input StepInput {
|
||||
id: String
|
||||
previousStepId: String
|
||||
key: String
|
||||
appKey: String
|
||||
connection: StepConnectionInput
|
||||
flow: StepFlowInput
|
||||
parameters: JSONObject
|
||||
previousStep: PreviousStepInput
|
||||
}
|
||||
|
||||
type User {
|
||||
id: String
|
||||
fullName: String
|
||||
email: String
|
||||
role: Role
|
||||
permissions: [Permission]
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
}
|
||||
|
||||
type Role {
|
||||
id: String
|
||||
name: String
|
||||
key: String
|
||||
description: String
|
||||
isAdmin: Boolean
|
||||
permissions: [Permission]
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
currentPage: Int!
|
||||
totalPages: Int!
|
||||
}
|
||||
|
||||
type ExecutionStepEdge {
|
||||
node: ExecutionStep
|
||||
}
|
||||
|
||||
type ExecutionStepConnection {
|
||||
edges: [ExecutionStepEdge]
|
||||
pageInfo: PageInfo
|
||||
}
|
||||
|
||||
type License {
|
||||
id: String
|
||||
name: String
|
||||
expireAt: String
|
||||
verified: Boolean
|
||||
}
|
||||
|
||||
type Permission {
|
||||
id: String
|
||||
action: String
|
||||
subject: String
|
||||
conditions: [String]
|
||||
}
|
||||
|
||||
type Action {
|
||||
label: String
|
||||
key: String
|
||||
subjects: [String]
|
||||
}
|
||||
|
||||
type Condition {
|
||||
key: String
|
||||
label: String
|
||||
}
|
||||
|
||||
type Subject {
|
||||
label: String
|
||||
key: String
|
||||
}
|
||||
|
||||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
@@ -1,4 +1,3 @@
|
||||
import { rule, shield } from 'graphql-shield';
|
||||
import User from '../models/user.js';
|
||||
import AccessToken from '../models/access-token.js';
|
||||
|
||||
@@ -47,19 +46,3 @@ export const authenticateUser = async (request, response, next) => {
|
||||
return response.status(401).end();
|
||||
}
|
||||
};
|
||||
|
||||
const isAuthenticatedRule = rule()(isAuthenticated);
|
||||
|
||||
export const authenticationRules = {
|
||||
Mutation: {
|
||||
'*': isAuthenticatedRule,
|
||||
},
|
||||
};
|
||||
|
||||
const authenticationOptions = {
|
||||
allowExternalErrors: true,
|
||||
};
|
||||
|
||||
const authentication = shield(authenticationRules, authenticationOptions);
|
||||
|
||||
export default authentication;
|
||||
|
@@ -1,74 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { allow } from 'graphql-shield';
|
||||
import { isAuthenticated, authenticationRules } from './authentication.js';
|
||||
import { createUser } from '../../test/factories/user.js';
|
||||
import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js';
|
||||
|
||||
describe('isAuthenticated', () => {
|
||||
it('should return false if no token is provided', async () => {
|
||||
const req = { headers: {} };
|
||||
expect(await isAuthenticated(null, null, req)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if token is invalid', async () => {
|
||||
const req = { headers: { authorization: 'invalidToken' } };
|
||||
expect(await isAuthenticated(null, null, req)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true if token is valid and there is a user', async () => {
|
||||
const user = await createUser();
|
||||
const token = await createAuthTokenByUserId(user.id);
|
||||
|
||||
const req = { headers: { authorization: token } };
|
||||
expect(await isAuthenticated(null, null, req)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if token is valid and but there is no user', async () => {
|
||||
const user = await createUser();
|
||||
const token = await createAuthTokenByUserId(user.id);
|
||||
await user.$query().delete();
|
||||
|
||||
const req = { headers: { authorization: token } };
|
||||
expect(await isAuthenticated(null, null, req)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authentication rules', () => {
|
||||
const getQueryAndMutationNames = (rules) => {
|
||||
const queries = Object.keys(rules.Query || {});
|
||||
const mutations = Object.keys(rules.Mutation || {});
|
||||
return { queries, mutations };
|
||||
};
|
||||
|
||||
const { queries, mutations } = getQueryAndMutationNames(authenticationRules);
|
||||
|
||||
if (queries.length) {
|
||||
describe('for queries', () => {
|
||||
queries.forEach((query) => {
|
||||
it(`should apply correct rule for query: ${query}`, () => {
|
||||
const ruleApplied = authenticationRules.Query[query];
|
||||
|
||||
if (query === '*') {
|
||||
expect(ruleApplied.func).toBe(isAuthenticated);
|
||||
} else {
|
||||
expect(ruleApplied).toEqual(allow);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('for mutations', () => {
|
||||
mutations.forEach((mutation) => {
|
||||
it(`should apply correct rule for mutation: ${mutation}`, () => {
|
||||
const ruleApplied = authenticationRules.Mutation[mutation];
|
||||
|
||||
if (mutation === '*') {
|
||||
expect(ruleApplied.func).toBe(isAuthenticated);
|
||||
} else {
|
||||
expect(ruleApplied).toBe(allow);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,53 +0,0 @@
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { graphqlHTTP } from 'express-graphql';
|
||||
import { loadSchemaSync } from '@graphql-tools/load';
|
||||
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
|
||||
import { addResolversToSchema } from '@graphql-tools/schema';
|
||||
import { applyMiddleware } from 'graphql-middleware';
|
||||
|
||||
import appConfig from '../config/app.js';
|
||||
import logger from './logger.js';
|
||||
import authentication from './authentication.js';
|
||||
import * as Sentry from './sentry.ee.js';
|
||||
import resolvers from '../graphql/resolvers.js';
|
||||
import HttpError from '../errors/http.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const schema = loadSchemaSync(join(__dirname, '../graphql/schema.graphql'), {
|
||||
loaders: [new GraphQLFileLoader()],
|
||||
});
|
||||
|
||||
const schemaWithResolvers = addResolversToSchema({
|
||||
schema,
|
||||
resolvers,
|
||||
});
|
||||
|
||||
const graphQLInstance = graphqlHTTP({
|
||||
schema: applyMiddleware(
|
||||
schemaWithResolvers,
|
||||
authentication.generate(schemaWithResolvers)
|
||||
),
|
||||
graphiql: appConfig.isDev,
|
||||
customFormatErrorFn: (error) => {
|
||||
logger.error(error.path + ' : ' + error.message + '\n' + error.stack);
|
||||
|
||||
if (error.originalError instanceof HttpError) {
|
||||
delete error.originalError.response;
|
||||
}
|
||||
|
||||
Sentry.captureException(error, {
|
||||
tags: { graphql: true },
|
||||
extra: {
|
||||
source: error.source?.body,
|
||||
positions: error.positions,
|
||||
path: error.path,
|
||||
},
|
||||
});
|
||||
|
||||
return error;
|
||||
},
|
||||
});
|
||||
|
||||
export default graphQLInstance;
|
@@ -6,18 +6,8 @@ const stream = {
|
||||
logger.http(message.substring(0, message.lastIndexOf('\n'))),
|
||||
};
|
||||
|
||||
const registerGraphQLToken = () => {
|
||||
morgan.token('graphql-query', (req) => {
|
||||
if (req.body.query) {
|
||||
return `\n GraphQL ${req.body.query}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
registerGraphQLToken();
|
||||
|
||||
const morganMiddleware = morgan(
|
||||
':method :url :status :res[content-length] - :response-time ms :graphql-query',
|
||||
':method :url :status :res[content-length] - :response-time ms',
|
||||
{ stream }
|
||||
);
|
||||
|
||||
|
@@ -17,7 +17,6 @@ export function init(app) {
|
||||
integrations: [
|
||||
app && new Sentry.Integrations.Http({ tracing: true }),
|
||||
app && new Tracing.Integrations.Express({ app }),
|
||||
app && new Tracing.Integrations.GraphQL(),
|
||||
].filter(Boolean),
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { Router } from 'express';
|
||||
import graphQLInstance from '../helpers/graphql-instance.js';
|
||||
import webhooksRouter from './webhooks.js';
|
||||
import paddleRouter from './paddle.ee.js';
|
||||
import healthcheckRouter from './healthcheck.js';
|
||||
@@ -23,7 +22,6 @@ import installationUsersRouter from './api/v1/installation/users.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use('/graphql', graphQLInstance);
|
||||
router.use('/webhooks', webhooksRouter);
|
||||
router.use('/paddle', paddleRouter);
|
||||
router.use('/healthcheck', healthcheckRouter);
|
||||
|
Reference in New Issue
Block a user