refactor: remove whole graphql implementation

This commit is contained in:
Ali BARIN
2024-09-26 13:29:46 +00:00
parent a043a044ca
commit 0b7591edce
28 changed files with 28 additions and 1181 deletions

View File

@@ -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",

View File

@@ -1,3 +0,0 @@
const mutationResolvers = {};
export default mutationResolvers;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,7 +0,0 @@
import mutationResolvers from './mutation-resolvers.js';
const resolvers = {
Mutation: mutationResolvers,
};
export default resolvers;

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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);
}
});
});
});
});

View File

@@ -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;

View File

@@ -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 }
);

View File

@@ -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,
});

View File

@@ -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);