Merge pull request #10 from AdvancedAlgos/develop

Update to Apollo Server 2
This commit is contained in:
Barry Low authored and GitHub committed 2018-10-17 21:00:41 -07:00
commit df0f3f2ab1
9 files changed
+2071 -285

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env"]
}
+1598 -146
View File
File diff suppressed because it is too large. Load diff
+12 -2
View File
@@ -2,26 +2,36 @@
"name": "aa-team-api",
"scripts": {
"start": "NODE_ENV=production nodemon -e js,graphql -x node -r dotenv/config src/index.js",
"debug": "nodemon -e js,graphql -x node --inspect -r dotenv/config src/index.js",
"debug": "nodemon src/index.js --watch ./src --exec \"node --require @babel/register --require dotenv/config\"",
"playground": "graphql playground --dotenv dotenv/config",
"dev": "npm-run-all --parallel debug playground"
},
"dependencies": {
"@azure/storage-blob": "^10.0.0-preview",
"@babel/cli": "^7.1.2",
"@babel/core": "^7.1.2",
"@babel/node": "^7.0.0",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/register": "^7.0.0",
"apollo-server-express": "^2.1.0",
"axios": "^0.18.0",
"cors": "^2.8.4",
"debug": "^4.1.0",
"dotenv": "^6.1.0",
"express": "^4.16.4",
"express-jwt": "^5.3.1",
"fast-xml-parser": "^3.12.5",
"graphql-import": "^0.6.0",
"graphql-middleware": "^1.7.7",
"graphql-subscriptions": "^1.0.0",
"graphql-tag": "^2.10.0",
"graphql-tools": "^4.0.1",
"graphql-yoga": "^1.16.4",
"jsonwebtoken": "^8.3.0",
"jwks-rsa": "^1.3.0",
"lodash.get": "^4.4.2",
"nodemon": "^1.18.3",
"nodemon": "^1.18.4",
"prisma-binding": "2.1.4",
"winston": "^3.1.0"
},
+131
View File
@@ -0,0 +1,131 @@
import http from 'http'
import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'
import { applyMiddleware as applyGraphQLMiddleware } from 'graphql-middleware'
import { logger } from './logger'
/**
*
* @description Creates an Apollo Server. Setup based on `vue-cli-plugin-apollo`.
* @param {Object} app Express application
* @param {Object} options Apollo options
* @returns {Object} HTTP Server
*/
export default function createApolloServer(
app,
{
// Main options
graphqlEndpoint = '',
typeDefs = {},
resolvers = {},
schemaDirectives = {},
directiveResolvers = {},
graphqlMiddlewares = [],
dataSources = () => ({}),
context = () => ({}),
// Subscriptions
subscriptionsEndpoint = '',
wsMiddlewares = [],
// Mocks
mocks,
// Apollo Engine
engineKey = '',
// HTTP options
cors = true,
timeout = 120000,
// Extra options for Apollo Server
apolloServerOptions = {},
}
) {
const schema = makeExecutableSchema({
typeDefs,
resolvers,
schemaDirectives,
directiveResolvers,
resolverValidationOptions: {
requireResolversForResolveType: false,
},
})
// Apollo server options
const options = {
...apolloServerOptions,
schema: applyGraphQLMiddleware(schema, ...graphqlMiddlewares),
tracing: true,
cacheControl: true,
engine: engineKey ? { apiKey: engineKey } : false,
dataSources,
// Resolvers context in POST requests
context: async ({ req, connection }) => {
let contextData
try {
if (connection) {
contextData = { ...connection.context }
} else {
contextData = await context({ req, request: req })
}
} catch (err) {
logger.error(err)
throw err
}
return contextData
},
// Resolvers context in WebSocket requests
subscriptions: {
path: subscriptionsEndpoint,
onConnect: async (connection, websocket) => {
const { authorization } = connection
let contextData = {}
try {
// Simulate `req` object for auth
const req = { headers: { authorization } }
// Call all middlewares in order and modify `req`
await new Promise((resolve, reject) =>
wsMiddlewares.reduceRight(
(acc, m) => err => (err ? reject(err) : m(req, null, acc)),
err => (err ? reject(err) : resolve())
)()
)
contextData = await context({
connection,
websocket,
request: req,
req,
})
} catch (err) {
if (err.status !== 401) {
logger.error(err)
}
throw err
}
return contextData
},
},
}
// Apollo Server
const server = new ApolloServer(options)
// Express middleware
server.applyMiddleware({
app,
cors,
path: graphqlEndpoint,
// gui: {
// endpoint: graphqlEndpoint,
// subscriptionEndpoint: graphqlSubscriptionsPath,
// },
})
// Create HTTP server and add subscriptions
const httpServer = http.createServer(app)
httpServer.setTimeout(timeout)
server.installSubscriptionHandlers(httpServer)
return httpServer
}
+195
View File
@@ -0,0 +1,195 @@
import { SchemaDirectiveVisitor } from 'apollo-server-express'
import _get from 'lodash.get'
import { logger } from '../logger'
import { AuthenticationError, ForbiddenError } from '../errors'
import { validateIdToken } from '../auth/validateIdToken'
const memberLocationOnContext = 'request.user'
const bearerAccessToken = 'request.headers.authorization'
const ctxMember = ctx => _get(ctx, memberLocationOnContext)
const ctxToken = ctx => _get(ctx, bearerAccessToken)
const createMember = async (ctx, idToken, info) => {
logger.info('createMember', idToken)
const member = await ctx.db.mutation.upsertMember({
where: {
authId: idToken.sub,
},
create: {
authId: idToken.sub,
alias: idToken.nickname
},
update: {
authId: idToken.sub,
alias: idToken.nickname
}
}, info)
return member
}
const isLoggedIn = async ctx => {
logger.info('isLoggedIn 0 : ', ctx.request.headers)
let member = ctxMember(ctx, memberLocationOnContext)
let token = ctxToken(ctx, bearerAccessToken)
let memberToken
logger.info('isLoggedIn: ', member, token)
if (!member && token) {
let scheme, credentials
const tokenParts = token.split(' ')
if (tokenParts.length === 2) {
scheme = tokenParts[0]
credentials = tokenParts[1]
}
if (/^Bearer$/i.test(scheme)) {
token = credentials
//verify token
// console.log('isLoggedIn2 : ', token)
try {
memberToken = await validateIdToken(token)
const authId = memberToken.sub
// console.log('isLoggedIn3 : ', await authId, ctx.db.query.member)
exists = await ctx.db.query.member({ where: { authId: authId } })
// console.log('isLoggedIn4 : ', await exists, createMember )
if (!member && exists === null) {
member = await createMember(ctx, memberToken).then(res => {
// console.log('isLoggedIn5 : ', res)
return res
})
return member
}
return member
} catch (err) {
memberToken = false
}
}
}
// console.log('isLoggedIn5 : ', await member)
if (!member) {
throw new AuthenticationError('Access token is missing or expired')
}
return member
}
const isRequestingMemberAlsoOwner = ({ ctx, memberId, type, typeId }) =>
ctx.db.exists[type]({ slug: typeId, owner: memberId })
const isRequestingMember = ({ ctx, memberId }) => ctx.db.exists.Member({ authId: memberId })
const getUser = ({ req = {}, request = {} }) => req.user || request.user || null
const hasRole = (roles, ctx) => {
const { role = '' } = getUser(ctx) || {}
return role && roles.includes(role)
}
const assertAuth = ctx => {
if (!getUser(ctx)) {
throw new AuthenticationError('Access token is missing or expired')
}
}
// Directive resolvers (apollo v1 syntax)
export const directiveResolvers = {
isAuthenticated: async (next, source, args, ctx) => {
let result = await isLoggedIn(ctx)
logger.info('directive isAuthenticated: ', result)
return next(result)
},
hasRole: (next, source, { roles }, ctx) => {
const { role } = isLoggedIn(ctx)
logger.info('directive hasRole: ', role)
if (roles.includes(role)) {
return next()
}
throw new ForbiddenError('Insufficient permissions')
},
isOwner: async (next, source, { type }, ctx) => {
logger.info('directive isOwner: ', source, type, ctx)
const { slug: typeId } =
source && source.id
? source
: ctx.request.body.variables ? ctx.request.body.variables : { id: null }
const user = await isLoggedIn(ctx)
// console.log('directive isOwner 0: ', typeId, user.sub)
const memberId = user.sub
const isOwner =
type === `Member`
? memberId === typeId
: await isRequestingMemberAlsoOwner({ ctx, memberId, type, typeId })
// console.log('directive isOwner 1: ', source, isOwner, type, typeId, memberId)
if (isOwner) {
return next()
}
throw new ForbiddenError('Insufficient permissions')
},
isOwnerOrHasRole: async (next, source, { roles, type }, ctx, ...p) => {
const { authId: memberId } = await isLoggedIn(ctx)
logger.info('directive isOwnerOrHasRole 1: ', memberId, roles, type)
if(memberId === undefined && role=== undefined) throw new Error(`Not logged in`)
if (roles.includes(role)) {
return next()
}
const { authId: typeId } = ctx.request.body.variables
const isOwner = await isRequestingMemberAlsoOwner({
ctx,
memberId,
type,
typeId
})
logger.info('directive isOwnerOrHasRole 2: ', typeId, memberId, role, isOwner)
if (isOwner) {
return next()
}
const hasRole = await isRequestingMember({
ctx,
memberId,
type,
typeId
})
if(hasRole){
return next()
}
throw new ForbiddenError('Insufficient permissions')
}
}
export const defaultFieldResolver = (source, args, contextValue, info) => {
// ensure source is a value for which property access is acceptable.
if (typeof source === 'object' || typeof source === 'function') {
const property = source[info.fieldName]
if (typeof property === 'function') {
return source[info.fieldName](args, contextValue, info)
}
return property
}
}
// Schema directives (apollo v2 syntax)
export const schemaDirectives = Object.keys(directiveResolvers).reduce(
(acc, directiveName) => ({
...acc,
[directiveName]: class extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const resolver = directiveResolvers[directiveName]
const originalResolver = field.resolve || defaultFieldResolver
const directiveArgs = this.args
field.resolve = (...args) => {
const [source, , context, info] = args
return resolver(
async () => originalResolver.apply(field, args),
source,
directiveArgs,
context,
info
)
}
}
},
}),
{}
)
+28
View File
@@ -0,0 +1,28 @@
import { ApolloError } from 'apollo-server-express'
import { logger } from '../logger'
export {
ApolloError,
AuthenticationError,
ForbiddenError,
SyntaxError,
UserInputError,
ValidationError,
} from 'apollo-server-express'
export function formatError(error) {
// This can be used to send errors to third
// party services like Sentry or Stackdriver
logger.error(error)
return error
}
// Define custom Apollo errors here
export class ExternalServiceError extends ApolloError {
constructor(message) {
super(message, 'EXTERNAL_SERVICE_UNAVAILABLE')
Object.defineProperty(this, 'name', { value: this.constructor.name })
}
}
+75 -102
View File
@@ -1,33 +1,36 @@
import '@babel/polyfill'
require('dotenv').config();
const { GraphQLServer } = require('graphql-yoga')
const {
import express from 'express'
import createApolloServer from './ApolloServer'
import { formatError } from './errors'
import {
Prisma,
extractFragmentReplacements,
forwardTo
} = require('prisma-binding')
const { makeExecutableSchema } = require('graphql-tools')
const { importSchema } = require('graphql-import')
const parser = require('fast-xml-parser')
var axios = require('axios')
} from 'prisma-binding'
import { makeExecutableSchema } from 'graphql-tools'
import { importSchema } from 'graphql-import'
import parser from 'fast-xml-parser'
import axios from 'axios'
const checkJwt = require('./auth/middleware/jwt')
const { getMember } = require('./auth/middleware/getMember')
const { validateIdToken } = require('./auth/validateIdToken')
const { directiveResolvers } = require('./auth/authDirectives')
import checkJwt from './auth/middleware/jwt'
import { getMember } from './auth/middleware/getMember'
import { validateIdToken } from './auth/validateIdToken'
import { schemaDirectives } from './directives'
const Azure = require("@azure/storage-blob")
const { createSASQueryURL } = require('./storage/azure')
const azureAccount = process.env.AZURE_STORAGE_ACCOUNT
const azureKey = process.env.AZURE_STORAGE_ACCESS_KEY
const azureStorageUrl = process.env.AZURE_STORAGE_URL
import { sendTeamMemberInvite, sendTeamCreateConfirmation, verifyInviteToken } from './email/sendgrid'
const { sendTeamMemberInvite, sendTeamCreateConfirmation, verifyInviteToken } = require('./email/sendgrid')
import TEAMS_FRAGMENT from './graphql/TeamsFragment'
import TEAMS_CONNECTIONS_FRAGMENT from './graphql/TeamsConnectionsFragment'
import TEAM_FB_FRAGMENT from './graphql/TeamFBFragment'
const TEAMS_FRAGMENT = require('./graphql/TeamsFragment')
const TEAMS_CONNECTIONS_FRAGMENT = require('./graphql/TeamsConnectionsFragment')
const TEAM_FB_FRAGMENT = require('./graphql/TeamFBFragment')
import pubsub from './pubsub'
import { logger } from './logger'
const logger = require('./logger')
const GRAPHQL_ENDPOINT = '/graphql'
const GRAPHQL_SUBSCRIPTIONS = '/graphql'
const PORT = 4001
const NODE_ENV = 'development'
const createMember = async function (ctx, info, idToken) {
logger.info('createMember', idToken)
@@ -52,25 +55,17 @@ const ctxMember = ctx => ctx.request.user
const resolvers = {
Query: {
async member(parent, arg, ctx, info) {
console.log('member: ', parent, arg)
logger.info('member: ', parent, arg)
return ctx.db.query.member({ where: { authId: arg.authId } }, info)
},
async currentMember(parent, args, ctx, info) {
console.log('currentMember: ', ctx.request.res.req.user, ctx.token)
logger.info('currentMember: ', ctx.request.res.req.user, ctx.token)
const authId = ctx.request.res.req.user.sub
fetch('URL_GOES_HERE', {
method: 'post',
headers: new Headers({
'Authorization': 'Basic '+btoa('username:password'),
'Content-Type': 'application/x-www-form-urlencoded'
}),
body: 'A=1&B=2'
})
return ctx.db.query.member({ where: { authId } }, info)
},
teams(parent, args, ctx, info) {
logger.info('teams: ', ctx.request.res)
return ctx.db.query.teamsConnection({}, TEAMS_CONNECTIONS_FRAGMENT)
},
async teamById(parent, { id }, ctx, info) {
@@ -88,21 +83,21 @@ const resolvers = {
async teamsByRole(parent, args, ctx, info) {
const authId = ctx.request.user.sub
let teamAdmin = await ctx.db.query.teams({where: {members_some: {OR: [{role: 'ADMIN'}, {role: 'OWNER'}], AND: [{member: {authId: authId}}]}}, orderBy: 'updatedAt_DESC'}, TEAMS_FRAGMENT)
console.log('teamsByRole teamAdmin: ', teamAdmin)
logger.info('teamsByRole teamAdmin: ', teamAdmin)
return teamAdmin
},
async fbByTeamMember(parent, args, ctx, info) {
console.log('fbByTeamMember: ', args, ctx.request, ctx.request.res.req.user)
logger.info('fbByTeamMember: ', args, ctx.request, ctx.request.res.req.user)
const authId = ctx.request.user.sub
let teamMemberFB = await ctx.db.query.teams({where: {members_some: {AND: [{member: {authId: authId}}]}}, orderBy: 'updatedAt_DESC'}, TEAM_FB_FRAGMENT)
logger.info('fbByTeamMember response: ', await teamMemberFB[0])
return teamMemberFB[0]
},
async owner(parent, args, ctx, info) {
console.log('resolver.query.owner ctx: ', ctxMember(ctx))
logger.info('resolver.query.owner ctx: ', ctxMember(ctx))
return ctx.db.query.member({ where: { id: ctxMember(ctx).authId } }, info)
.catch((res) => {
console.log('createTeam error: ', res)
.catch(err => {
logger.debug('createTeam error: ', err)
const errors = res.graphQLErrors.map((error) => {
return error.message
})
@@ -114,9 +109,9 @@ const resolvers = {
let memberToken = null
try {
memberToken = await validateIdToken(idToken)
console.log('authenticate.memberToken: ', await memberToken)
logger.info('authenticate.memberToken: ', await memberToken)
} catch (err) {
console.log('authenticat.validateIdToken err: ', err)
logger.debug('authenticat.validateIdToken err: ', err)
throw new Error(err.message)
}
const authId = memberToken.sub
@@ -152,9 +147,9 @@ const resolvers = {
slug: verifiedToken.team
}
}
console.log('verifyTeamInvite.token: ', await verifiedToken)
logger.info('verifyTeamInvite.token: ', await verifiedToken)
} catch (err) {
console.log('verifyTeamInvite. err: ', err)
logger.debug('verifyTeamInvite. err: ', err)
throw new Error(err.message)
}
return team
@@ -165,17 +160,17 @@ const resolvers = {
const encodedAuthId = encodeURI(authId)
const encodedURL = `https://users-api.advancedalgos.net/graphql?query=%7B%0A%20%20userByAuthId(authId%3A%20"${encodedAuthId}")%7B%0A%20%20%20%20id%0A%09%09email%0A%20%20%20%20alias%0A%20%20%7D%0A%7D`
const decodedURL = decodeURI(encodedURL)
console.log('createTeam URLs: ', encodedURL, decodedURL)
logger.info('createTeam URLs: ', encodedURL, decodedURL)
const getUserModuleUser = await axios.get(encodedURL)
.then((result) => {
console.log('getUserModuleUser axios:', result)
logger.info('getUserModuleUser axios:', result)
return result
})
.catch(err =>{
console.log('getUserModuleUser err:', err)
logger.debug('getUserModuleUser err:', err)
})
console.log('getUserModuleUser:', await getUserModuleUser)
logger.info('getUserModuleUser:', await getUserModuleUser)
const alias = await getUserModuleUser.data.data.userByAuthId.alias
const email = getUserModuleUser.data.data.userByAuthId.email
const avatar = 'https://algobotcommstorage.blob.core.windows.net/aateammodule/aa-avatar-default.png'
@@ -186,23 +181,23 @@ const resolvers = {
// const platformUrl = 'http://localhost:3100/AABrowserAPI/teamSetup/'
const createPlatformTeam = await axios.get(`${platformUrl}${createTeamUrl}`)
.then((result) => {
console.log('createPlatformTeam:', result)
logger.info('createPlatformTeam:', result)
if(result.data.message === 'Team Name already taken'){
throw new Error(result.data.message)
}
return result
})
.catch(err =>{
console.log('createPlatformTeam err:', err)
logger.errror('createPlatformTeam err:', err)
throw new Error(err)
})
console.log('createPlatformTeam: ', await createPlatformTeam)
logger.info('createPlatformTeam: ', await createPlatformTeam)
if(createPlatformTeam === 'Error: Team Name already taken'){
return createPlatformTeam
}
const createTeam = await ctx.db.mutation.createTeam({ data: {name: name, slug: slug, owner: authId, members: {create: {member: {connect: {authId: authId}}, role: 'OWNER'}}, profile: {create: {avatar: avatar, banner: banner}}, fb: {create: {name: botName, slug: botSlug, kind:'TRADER', avatar: avatar, status: {create: {status: 'ACTIVE', reason: "Cloned on team creation"}}}}, status: {create: {status: 'ACTIVE', reason:"Team created"}}} }, TEAMS_FRAGMENT)
.catch((err) => {
console.log('createTeam error: ', err)
logger.debug('createTeam error: ', err)
return err
})
@@ -213,21 +208,21 @@ const resolvers = {
async updateTeamProfile(parent, { slug, owner, description, motto, avatar, banner }, ctx, info) {
return ctx.db.mutation.updateTeam({data:{profile: {update: {description: description, motto: motto, avatar: avatar, banner: banner}}}, where:{slug: slug}}, TEAMS_FRAGMENT)
.catch((err) => {
console.log('createTeam error: ', err)
logger.debug('createTeam error: ', err)
return err
})
},
async updateFB(parent, { fbId, avatar}, ctx, info) {
return ctx.db.mutation.updateFinancialBeings({data:{avatar:avatar}, where:{id:fbId}}, info)
.catch((err) => {
console.log('createTeam error: ', err)
logger.debug('createTeam error: ', err)
return err
})
},
async deleteTeam(parent, { slug }, ctx, info) {
return ctx.db.mutation.deleteTeam({ where: { slug } }, info)
.catch((res) => {
console.log('deleteTeam error: ', res)
logger.info('deleteTeam error: ', res)
const errors = res.graphQLErrors.map((error) => {
return error.message
})
@@ -244,25 +239,9 @@ const db = new Prisma({
debug: true
})
const schema = makeExecutableSchema({
typeDefs: importSchema('src/schema.graphql'),
resolvers,
resolverValidationOptions: { requireResolversForResolveType: false },
directiveResolvers
})
const app = express()
const server = new GraphQLServer({
schema,
context: req => ({
token: req.headers,
user: req.user,
db,
...req
}),
})
server.express.use(
server.options.endpoint,
const connectionMiddlewares = [
checkJwt,
function (err, req, res, next) {
logger.info('checkJwt: ', err, req.headers, req.user)
@@ -271,39 +250,11 @@ server.express.use(
} else {
next()
}
}
)
server.express.post(server.options.endpoint, (req, res, done) => getUser(req, res, done, db))
/*
let whitelist = [
'http://localhost:3000',
'http://localhost:3001',
'http://localhost:3002',
'http://localhost:4000',
'http://localhost:4002',
'http://localhost:1337',
'http://localhost:4100',
'https://teams.advancedalgos.net',
'https://users.advancedalgos.net',
'https://users-api.advancedalgos.net',
'https://keyvault.advancedalgos.net',
'https://keyvault-api.advancedalgos.net',
'https://aawebdevelop.azurewebsites.net',
'https://develop.advancedalgos.net'
},
// (req, res, done) => getMember(req, res, done, db)
]
const corsOptionsDelegate = (req, callback) => {
var corsOptions
if (whitelist.indexOf(req.header('Origin')) !== -1) {
corsOptions = { origin: true, credentials: true } // reflect (enable) the requested origin in the CORS response
} else {
corsOptions = { origin: false, credentials: true } // disable CORS for this request
}
callback(null, corsOptions) // callback expects two parameters: error and options
}
*/
app.post(GRAPHQL_ENDPOINT, ...connectionMiddlewares)
const corsOptions = {
"origin": "*",
@@ -320,6 +271,28 @@ const options = {
playground: '/graphiql'
}
server.start(options, () => console.log(`Server is running on http://localhost:4001${server.options.endpoint}`))
const server = createApolloServer(app, {
graphqlEndpoint: GRAPHQL_ENDPOINT,
subscriptionsEndpoint: GRAPHQL_SUBSCRIPTIONS,
wsMiddlewares: connectionMiddlewares,
apolloServerOptions: { formatError },
typeDefs: importSchema('src/schema.graphql'),
resolvers,
resolverValidationOptions: { requireResolversForResolveType: false },
schemaDirectives,
context: req => ({
...req ,
token: req.headers,
user: req.user,
db,
pubsub
})
})
server.listen({ port: PORT }, () => {
logger.info(
`🚀 GraphQL Server is running on http://localhost:${PORT}${GRAPHQL_ENDPOINT} in "${NODE_ENV}" mode\n`
)
})
module.exports= { createMember }
+26 -35
View File
@@ -1,39 +1,30 @@
const { createLogger, format, transports } = require('winston')
const { combine, timestamp, simple } = format
import { createLogger, format, transports } from 'winston'
const options = {
file: {
level: 'info',
filename: '../logs/app.log',
handleExceptions: true,
json: true,
maxsize: 5242880, // 5MB
maxFiles: 5,
colorize: false,
timestamp: true
},
console: {
level: 'debug',
handleExceptions: true,
json: false,
colorize: true,
timestamp: true
class LoggerService {
constructor() {
const winstonTransports = [
new transports.Console({
format: format.combine(
format.colorize(),
format.timestamp({ format: 'YYYY/MM/DD HH:mm' }),
format.printf(
info => `${info.timestamp} - ${info.level}: ${info.message}`
)
),
}),
]
const logger = createLogger({
level: 'info',
transports: winstonTransports,
exceptionHandlers: winstonTransports,
})
return new Proxy(this, {
get: (target, propKey) => logger[propKey],
})
}
}
const logger = createLogger({
level: 'info',
format: combine(
timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
simple()
),
transports: [
new transports.File(options.file),
new transports.Console(options.console)
],
exitOnError: false // do not exit on handled exceptions
})
module.exports= logger
export const logger = new LoggerService() // Default logger
export { LoggerService }
+3
View File
@@ -0,0 +1,3 @@
import { PubSub } from 'graphql-subscriptions'
export default new PubSub()