Merge branch

'2428-feature-federation-implement-multiple-apollo-graphql-endpoints' of
github.com:gradido/gradido into
2428-feature-federation-implement-multiple-apollo-graphql-endpoints
This commit is contained in:
Claus-Peter Hübner 2023-01-13 00:24:08 +01:00
commit de3fdd7c62
17 changed files with 3448 additions and 5484 deletions

View File

@ -4,23 +4,25 @@ module.exports = {
node: true,
// jest: true,
},
parser: "@typescript-eslint/parser",
plugins: ["prettier", "@typescript-eslint" /*, 'jest' */],
parser: '@typescript-eslint/parser',
plugins: ['prettier', '@typescript-eslint' /*, 'jest' */],
extends: [
"standard",
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended",
'standard',
'eslint:recommended',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
],
// add your custom rules here
rules: {
"no-console": ["error"],
"no-debugger": "error",
"prettier/prettier": [
"error",
'no-console': ['error'],
'no-debugger': 'error',
'prettier/prettier': [
'error',
{
htmlWhitespaceSensitivity: "ignore",
htmlWhitespaceSensitivity: 'ignore',
semi: false,
singleQuote: true,
},
],
},
};
}

View File

@ -28,19 +28,13 @@
"reflect-metadata": "^0.1.13",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.1",
"type-graphql": "^1.1.1",
"typescript": "^4.9.3"
"type-graphql": "^1.1.1"
},
"devDependencies": {
"@types/express": "^4.17.12",
"@types/jest": "^27.0.2",
"@types/lodash.clonedeep": "^4.5.7",
"@types/node": "^16.10.3",
"@types/nodemailer": "^6.4.4",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"apollo-server-testing": "^2.25.2",
"@types/lodash.clonedeep": "^4.5.6",
"@types/node": "^16.10.3",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
@ -48,13 +42,8 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0",
"faker": "^5.5.3",
"jest": "^27.2.4",
"nodemon": "^2.0.7",
"prettier": "^2.3.1",
"ts-jest": "^27.0.5",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.1",
"typescript": "^4.9.3"
"typescript": "^4.3.4",
"nodemon": "^2.0.7"
}
}

View File

@ -1,6 +1,6 @@
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
import dotenv from "dotenv";
dotenv.config();
import dotenv from 'dotenv'
dotenv.config()
/*
import Decimal from 'decimal.js-light'
@ -11,35 +11,35 @@ Decimal.set({
*/
const constants = {
DB_VERSION: "0059-add_hide_amount_to_users",
DB_VERSION: '0059-add_hide_amount_to_users',
// DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: "log4js-config.json",
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || "info",
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: {
DEFAULT: "DEFAULT",
EXPECTED: "v1.2023-01-09",
CURRENT: "",
DEFAULT: 'DEFAULT',
EXPECTED: 'v1.2023-01-09',
CURRENT: '',
},
};
}
const server = {
PORT: process.env.PORT || 5000,
// JWT_SECRET: process.env.JWT_SECRET || 'secret123',
// JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
GRAPHIQL: process.env.GRAPHIQL === "true" || false,
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
// GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
PRODUCTION: process.env.NODE_ENV === "production" || false,
};
PRODUCTION: process.env.NODE_ENV === 'production' || false,
}
const database = {
DB_HOST: process.env.DB_HOST || "localhost",
DB_HOST: process.env.DB_HOST || 'localhost',
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
DB_USER: process.env.DB_USER || "root",
DB_PASSWORD: process.env.DB_PASSWORD || "",
DB_DATABASE: process.env.DB_DATABASE || "gradido_community",
DB_USER: process.env.DB_USER || 'root',
DB_PASSWORD: process.env.DB_PASSWORD || '',
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
TYPEORM_LOGGING_RELATIVE_PATH:
process.env.TYPEORM_LOGGING_RELATIVE_PATH || "typeorm.backend.log",
};
process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log',
}
/*
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
@ -62,7 +62,7 @@ const community = {
// Check config version
constants.CONFIG_VERSION.CURRENT =
process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT;
process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
if (
![
constants.CONFIG_VERSION.EXPECTED,
@ -71,16 +71,16 @@ if (
) {
throw new Error(
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`
);
)
}
const federation = {
// FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
// FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
FEDERATION_PORT: process.env.FEDERATION_PORT || 5000,
FEDERATION_API: process.env.FEDERATION_API || "1_0",
FEDERATION_API: process.env.FEDERATION_API || '1_0',
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null,
};
}
const CONFIG = {
...constants,
@ -89,6 +89,6 @@ const CONFIG = {
// ...community,
// ...eventProtocol,
...federation,
};
}
export default CONFIG;
export default CONFIG

View File

@ -1,21 +1,21 @@
import { Field, ObjectType, Query, Resolver } from "type-graphql";
import { federationLogger as logger } from "@/server/logger";
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger'
@ObjectType()
class GetTestApiResult {
constructor(apiVersion: string) {
this.api = `${apiVersion}`;
this.api = `${apiVersion}`
}
@Field(() => String)
api: string;
api: string
}
@Resolver()
export class TestResolver {
@Query(() => GetTestApiResult)
async test(): Promise<GetTestApiResult> {
logger.info(`test api 1_0`);
return new GetTestApiResult("1_0");
logger.info(`test api 1_0`)
return new GetTestApiResult('1_0')
}
}

View File

@ -1,21 +1,21 @@
import { Field, ObjectType, Query, Resolver } from "type-graphql";
import { federationLogger as logger } from "@/server/logger";
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger'
@ObjectType()
class GetTestApiResult {
constructor(apiVersion: string) {
this.api = `${apiVersion}`;
this.api = `${apiVersion}`
}
@Field(() => String)
api: string;
api: string
}
@Resolver()
export class TestResolver {
@Query(() => GetTestApiResult)
async test(): Promise<GetTestApiResult> {
logger.info(`test api 1_1`);
return new GetTestApiResult("1_1");
logger.info(`test api 1_1`)
return new GetTestApiResult('1_1')
}
}

View File

@ -1,21 +1,21 @@
import { Field, ObjectType, Query, Resolver } from "type-graphql";
import { federationLogger as logger } from "@/server/logger";
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger'
@ObjectType()
class GetTestApiResult {
constructor(apiVersion: string) {
this.api = `${apiVersion}`;
this.api = `${apiVersion}`
}
@Field(() => String)
api: string;
api: string
}
@Resolver()
export class TestResolver {
@Query(() => GetTestApiResult)
async test(): Promise<GetTestApiResult> {
logger.info(`test api 2_0`);
return new GetTestApiResult("2_0");
logger.info(`test api 2_0`)
return new GetTestApiResult('2_0')
}
}

View File

@ -1,12 +1,12 @@
import path from "path";
import path from 'path'
// config
import CONFIG from "../../config";
import { federationLogger as logger } from "@/server/logger";
import CONFIG from '../../config'
import { federationLogger as logger } from '@/server/logger'
export const getApiResolvers = (): string => {
logger.info(`getApiResolvers...${CONFIG.FEDERATION_API}`);
logger.info(`getApiResolvers...${CONFIG.FEDERATION_API}`)
return path.join(
__dirname,
`./${CONFIG.FEDERATION_API}/resolver/*Resolver.{ts,js}`
);
};
)
}

View File

@ -1,23 +1,23 @@
import { GraphQLScalarType, Kind } from "graphql";
import Decimal from "decimal.js-light";
import { GraphQLScalarType, Kind } from 'graphql'
import Decimal from 'decimal.js-light'
export default new GraphQLScalarType({
name: "Decimal",
description: "The `Decimal` scalar type to represent currency values",
name: 'Decimal',
description: 'The `Decimal` scalar type to represent currency values',
serialize(value: Decimal) {
return value.toString();
return value.toString()
},
parseValue(value) {
return new Decimal(value);
return new Decimal(value)
},
parseLiteral(ast) {
if (ast.kind !== Kind.STRING) {
throw new TypeError(`${String(ast)} is not a valid decimal value.`);
throw new TypeError(`${String(ast)} is not a valid decimal value.`)
}
return new Decimal(ast.value);
return new Decimal(ast.value)
},
});
})

View File

@ -1,17 +1,17 @@
import { GraphQLSchema } from "graphql";
import { buildSchema } from "type-graphql";
import { GraphQLSchema } from 'graphql'
import { buildSchema } from 'type-graphql'
// import isAuthorized from './directive/isAuthorized'
import DecimalScalar from "./scalar/Decimal";
import Decimal from "decimal.js-light";
import { getApiResolvers } from "./api/schema";
import DecimalScalar from './scalar/Decimal'
import Decimal from 'decimal.js-light'
import { getApiResolvers } from './api/schema'
const schema = async (): Promise<GraphQLSchema> => {
return await buildSchema({
resolvers: [getApiResolvers()],
// authChecker: isAuthorized,
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
});
};
})
}
export default schema;
export default schema

View File

@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import createServer from "./server/createServer"
import createServer from './server/createServer'
// config
import CONFIG from "./config"
import CONFIG from './config'
async function main() {
// eslint-disable-next-line no-console

View File

@ -1,8 +1,8 @@
import cors from "cors";
import cors from 'cors'
const corsOptions = {
origin: "*",
exposedHeaders: ["token"],
};
origin: '*',
exposedHeaders: ['token'],
}
export default cors(corsOptions);
export default cors(corsOptions)

View File

@ -1,29 +1,29 @@
import "reflect-metadata";
import 'reflect-metadata'
import { ApolloServer } from "apollo-server-express";
import express, { Express } from "express";
import { ApolloServer } from 'apollo-server-express'
import express, { Express } from 'express'
// database
import connection from "@/typeorm/connection";
import { checkDBVersion } from "@/typeorm/DBVersion";
import connection from '@/typeorm/connection'
import { checkDBVersion } from '@/typeorm/DBVersion'
// server
import cors from "./cors";
import cors from './cors'
// import serverContext from './context'
import plugins from "./plugins";
import plugins from './plugins'
// config
import CONFIG from "@/config";
import CONFIG from '@/config'
// graphql
import schema from "@/graphql/schema";
import schema from '@/graphql/schema'
// webhooks
// import { elopageWebhook } from '@/webhook/elopage'
import { Connection } from "@dbTools/typeorm";
import { Connection } from '@dbTools/typeorm'
import { apolloLogger } from "./logger";
import { Logger } from "log4js";
import { apolloLogger } from './logger'
import { Logger } from 'log4js'
// i18n
// import { i18n } from './localization'
@ -31,7 +31,7 @@ import { Logger } from "log4js";
// TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection };
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection }
const createServer = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -39,33 +39,33 @@ const createServer = async (
logger: Logger = apolloLogger
// localization: i18n.I18n = i18n,
): Promise<ServerDef> => {
logger.addContext("user", "unknown");
logger.debug("createServer...");
logger.addContext('user', 'unknown')
logger.debug('createServer...')
// open mysql connection
const con = await connection();
const con = await connection()
if (!con || !con.isConnected) {
logger.fatal(`Couldn't open connection to database!`);
throw new Error(`Fatal: Couldn't open connection to database`);
logger.fatal(`Couldn't open connection to database!`)
throw new Error(`Fatal: Couldn't open connection to database`)
}
// check for correct database version
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION);
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
if (!dbVersion) {
logger.fatal("Fatal: Database Version incorrect");
throw new Error("Fatal: Database Version incorrect");
logger.fatal('Fatal: Database Version incorrect')
throw new Error('Fatal: Database Version incorrect')
}
// Express Server
const app = express();
const app = express()
// cors
app.use(cors);
app.use(cors)
// bodyparser json
app.use(express.json());
app.use(express.json())
// bodyparser urlencoded for elopage
app.use(express.urlencoded({ extended: true }));
app.use(express.urlencoded({ extended: true }))
// i18n
// app.use(localization.init)
@ -81,11 +81,11 @@ const createServer = async (
// context,
plugins,
logger,
});
apollo.applyMiddleware({ app, path: "/" });
logger.debug("createServer...successful");
})
apollo.applyMiddleware({ app, path: '/' })
logger.debug('createServer...successful')
return { apollo, app, con };
};
return { apollo, app, con }
}
export default createServer;
export default createServer

View File

@ -1,43 +1,43 @@
import log4js from "log4js";
import CONFIG from "@/config";
import log4js from 'log4js'
import CONFIG from '@/config'
import { readFileSync } from "fs";
import { readFileSync } from 'fs'
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, "utf-8"));
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
options.categories.backend.level = CONFIG.LOG_LEVEL;
options.categories.apollo.level = CONFIG.LOG_LEVEL;
let filename: string = options.appenders.federation.filename;
options.categories.backend.level = CONFIG.LOG_LEVEL
options.categories.apollo.level = CONFIG.LOG_LEVEL
let filename: string = options.appenders.federation.filename
options.appenders.federation.filename = filename
.replace("%v", CONFIG.FEDERATION_API)
.replace("%p", CONFIG.FEDERATION_PORT.toString());
filename = options.appenders.access.filename;
.replace('%v', CONFIG.FEDERATION_API)
.replace('%p', CONFIG.FEDERATION_PORT.toString())
filename = options.appenders.access.filename
options.appenders.access.filename = filename.replace(
"%p",
'%p',
CONFIG.FEDERATION_PORT.toString()
);
filename = options.appenders.apollo.filename;
)
filename = options.appenders.apollo.filename
options.appenders.apollo.filename = filename.replace(
"%p",
'%p',
CONFIG.FEDERATION_PORT.toString()
);
filename = options.appenders.backend.filename;
)
filename = options.appenders.backend.filename
options.appenders.backend.filename = filename.replace(
"%p",
'%p',
CONFIG.FEDERATION_PORT.toString()
);
filename = options.appenders.errorFile.filename;
)
filename = options.appenders.errorFile.filename
options.appenders.errorFile.filename = filename.replace(
"%p",
'%p',
CONFIG.FEDERATION_PORT.toString()
);
)
log4js.configure(options);
log4js.configure(options)
const apolloLogger = log4js.getLogger("apollo");
const apolloLogger = log4js.getLogger('apollo')
// const backendLogger = log4js.getLogger('backend')
const federationLogger = log4js.getLogger("federation");
const federationLogger = log4js.getLogger('federation')
// backendLogger.addContext('user', 'unknown')
export { apolloLogger, federationLogger };
export { apolloLogger, federationLogger }

View File

@ -1,69 +1,68 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import clonedeep from "lodash.clonedeep";
import clonedeep from 'lodash.clonedeep'
const setHeadersPlugin = {
requestDidStart() {
return {
willSendResponse(requestContext: any) {
const { setHeaders = [] } = requestContext.context;
const { setHeaders = [] } = requestContext.context
setHeaders.forEach(({ key, value }: { [key: string]: string }) => {
if (requestContext.response.http.headers.get(key)) {
requestContext.response.http.headers.set(key, value);
requestContext.response.http.headers.set(key, value)
} else {
requestContext.response.http.headers.append(key, value);
requestContext.response.http.headers.append(key, value)
}
});
return requestContext;
})
return requestContext
},
};
}
},
};
}
const filterVariables = (variables: any) => {
const vars = clonedeep(variables);
if (vars.password) vars.password = "***";
if (vars.passwordNew) vars.passwordNew = "***";
return vars;
};
const vars = clonedeep(variables)
if (vars.password) vars.password = '***'
if (vars.passwordNew) vars.passwordNew = '***'
return vars
}
const logPlugin = {
requestDidStart(requestContext: any) {
const { logger } = requestContext;
const { query, mutation, variables, operationName } =
requestContext.request;
if (operationName !== "IntrospectionQuery") {
const { logger } = requestContext
const { query, mutation, variables, operationName } = requestContext.request
if (operationName !== 'IntrospectionQuery') {
logger.info(`Request:
${mutation || query}variables: ${JSON.stringify(
filterVariables(variables),
null,
2
)}`);
)}`)
}
return {
willSendResponse(requestContext: any) {
if (operationName !== "IntrospectionQuery") {
if (operationName !== 'IntrospectionQuery') {
if (requestContext.context.user)
logger.info(`User ID: ${requestContext.context.user.id}`);
logger.info(`User ID: ${requestContext.context.user.id}`)
if (requestContext.response.data) {
logger.info("Response Success!");
logger.info('Response Success!')
logger.trace(`Response-Data:
${JSON.stringify(requestContext.response.data, null, 2)}`);
${JSON.stringify(requestContext.response.data, null, 2)}`)
}
if (requestContext.response.errors)
logger.error(`Response-Errors:
${JSON.stringify(requestContext.response.errors, null, 2)}`);
${JSON.stringify(requestContext.response.errors, null, 2)}`)
}
return requestContext;
return requestContext
},
};
}
},
};
}
const plugins =
process.env.NODE_ENV === "development"
process.env.NODE_ENV === 'development'
? [setHeadersPlugin]
: [setHeadersPlugin, logPlugin];
: [setHeadersPlugin, logPlugin]
export default plugins;
export default plugins

View File

@ -1,27 +1,27 @@
import { Migration } from "@entity/Migration";
import { federationLogger as logger } from "@/server/logger";
import { Migration } from '@entity/Migration'
import { federationLogger as logger } from '@/server/logger'
const getDBVersion = async (): Promise<string | null> => {
try {
const dbVersion = await Migration.findOne({ order: { version: "DESC" } });
return dbVersion ? dbVersion.fileName : null;
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
return dbVersion ? dbVersion.fileName : null
} catch (error) {
logger.error(error);
return null;
logger.error(error)
return null
}
};
}
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
const dbVersion = await getDBVersion();
const dbVersion = await getDBVersion()
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
logger.error(
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
dbVersion || "None"
dbVersion || 'None'
}`
);
return false;
)
return false
}
return true;
};
return true
}
export { checkDBVersion, getDBVersion };
export { checkDBVersion, getDBVersion }

View File

@ -1,14 +1,14 @@
// TODO This is super weird - since the entities are defined in another project they have their own globals.
// We cannot use our connection here, but must use the external typeorm installation
import { Connection, createConnection, FileLogger } from "@dbTools/typeorm";
import CONFIG from "@/config";
import { entities } from "@entity/index";
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm'
import CONFIG from '@/config'
import { entities } from '@entity/index'
const connection = async (): Promise<Connection | null> => {
try {
return createConnection({
name: "default",
type: "mysql",
name: 'default',
type: 'mysql',
host: CONFIG.DB_HOST,
port: CONFIG.DB_PORT,
username: CONFIG.DB_USER,
@ -17,18 +17,18 @@ const connection = async (): Promise<Connection | null> => {
entities,
synchronize: false,
logging: true,
logger: new FileLogger("all", {
logger: new FileLogger('all', {
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
}),
extra: {
charset: "utf8mb4_unicode_ci",
charset: 'utf8mb4_unicode_ci',
},
});
})
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
return null;
console.log(error)
return null
}
};
}
export default connection;
export default connection

File diff suppressed because it is too large Load Diff