UNPKG

9.01 kBPlain TextView Raw
1if (+process.versions.node.replace(/\.\d+$/, '') < 12)
2 throw new Error(`Required version of node: >=12, current: ${process.versions.node}`);
3
4import dotenv from 'dotenv';
5export const ENV = process.env.NODE_ENV || 'development';
6const envFiles = ['.env', '.env.local', '.env.' + ENV, '.env.' + ENV + '.local'];
7envFiles.forEach(path => Object.assign(process.env, dotenv.config({ path }).parsed));
8
9import cors from 'cors';
10import 'deps-check';
11import Express from 'express';
12import graphqlHTTP from 'express-graphql';
13import session, { SessionOptions } from 'express-session';
14import { GraphQLError, validateSchema } from 'graphql';
15import { dirname } from 'path';
16import { createSchema } from 'ts2graphql';
17import { dbInit, DBOptions } from './dbInit';
18import { graphQLBigintTypeFactory } from './graphQLUtils';
19import { BaseDB, SchemaConstraint } from './Orm/PostgresqlDriver';
20import * as bodyparser from 'body-parser';
21import serveStatic from 'serve-static';
22import { readFileSync, writeFileSync, existsSync } from 'fs';
23import https from 'https';
24import http from 'http';
25import { ClientException, logger, Exception } from './logger';
26import { Pool } from 'pg';
27import findUp from 'find-up';
28import { sleep } from './utils';
29// import * as diskusage from 'diskusage';
30
31export * from './di';
32export * from './graphQLUtils';
33export * from './Orm/PostgresqlDriver';
34export * from './request';
35export * from './testUtils';
36export * from './utils';
37export * from './dateUtils';
38export * from './assert';
39export * from './logger';
40export const bodyParser = bodyparser;
41
42export const PRODUCTION = ENV === 'production';
43
44interface Options {
45 https?: {
46 privateKeyFile: string;
47 certificateFile: string;
48 port?: number;
49 };
50 session?: SessionOptions;
51 db?: DBOptions;
52 graphql: {
53 schema: string;
54 resolver: object;
55 };
56 static?: {
57 rootDir: string;
58 options?: serveStatic.ServeStaticOptions;
59 };
60 parcel?: {
61 indexFilename: string;
62 };
63 errors: {
64 unknown: unknown;
65 };
66 port: number;
67}
68
69interface Result<DBSchema extends SchemaConstraint> {
70 server: https.Server | http.Server;
71 express: Express.Express;
72 projectDir: string;
73 db: BaseDB<DBSchema>;
74 dbPool: Pool;
75}
76
77let EXITING = false;
78export async function createGraphqApp<DBSchema extends SchemaConstraint>(
79 options: Options,
80 runMiddlewares?: (app: Result<DBSchema>) => Promise<void>,
81): Promise<Result<DBSchema>> {
82 let db: BaseDB<DBSchema> | undefined;
83 let dbPool: Pool | undefined;
84 try {
85 logger.info('------------------------ START PROGRAM ----------------------', { pid: process.pid });
86 logger.info('ENV', { ENV });
87
88 if (options.db) {
89 const dbRes = await dbInit<DBSchema>(projectDir, options.db);
90 db = dbRes.db;
91 dbPool = dbRes.pool;
92 }
93 const express = Express();
94 express.disable('x-powered-by');
95 express.use((_req, res, next) => {
96 if (EXITING) {
97 res.status(503);
98 res.send({ status: 'error', error: { message: 'Service unavailable' } });
99 return;
100 }
101 next();
102 });
103
104 if (options.session) {
105 express.use(
106 session({
107 name: 'sid',
108 resave: true,
109 saveUninitialized: true,
110 ...options.session,
111 }),
112 );
113 }
114
115 if (options.static) {
116 express.use(serveStatic(options.static.rootDir, options.static.options));
117 }
118
119 if (!PRODUCTION) {
120 express.use(cors());
121 }
122 if (options.parcel) {
123 const Bundler = require('parcel-bundler');
124 const bundler = new Bundler(options.parcel.indexFilename, { cache: false }) as {
125 middleware(): Express.RequestHandler;
126 };
127 express.use(bundler.middleware());
128 }
129
130 const schema = createSchema(options.graphql.schema, {
131 customScalarFactory: type =>
132 type.type === 'string' && type.rawType !== undefined ? graphQLBigintTypeFactory(type.rawType) : undefined,
133 });
134 // console.log(printSchema(schema));
135 validateSchema(schema).forEach(err => {
136 throw err;
137 });
138
139 function handleError(error: Error) {
140 logger.error(error);
141 if (error instanceof ClientException) {
142 return { error: error.name, status: 400 };
143 }
144 debugger;
145 /* istanbul ignore next */
146 return { error: options.errors.unknown, status: 500 };
147 }
148
149 // console.log(printSchema(schema));
150 express.get(
151 '/api/graphql',
152 graphqlHTTP({
153 schema: schema,
154 rootValue: options.graphql.resolver,
155 graphiql: true,
156 }),
157 );
158 express.post(
159 '/api/graphql',
160 (_req, res, next) => {
161 const sendJson = res.json.bind(res);
162 res.json = (json: { errors?: unknown[] }) => {
163 if (json && json.errors) {
164 json.errors = (json.errors as { originalError?: Error }[]).map(graphqlError => {
165 const originalError = graphqlError.originalError || (graphqlError as Error);
166 if (originalError instanceof GraphQLError) {
167 return originalError;
168 }
169 const { error, status } = handleError(originalError);
170 res.statusCode = status;
171 return error;
172 });
173 }
174 return sendJson(json);
175 };
176 next();
177 },
178 graphqlHTTP({
179 schema: schema,
180 rootValue: options.graphql.resolver,
181 ...{ customFormatErrorFn: (err: Error) => err },
182 }),
183 );
184
185 const server = options.https
186 ? https.createServer(
187 {
188 key: readFileSync(options.https.privateKeyFile, 'utf8'),
189 cert: readFileSync(options.https.certificateFile, 'utf8'),
190 },
191 express,
192 )
193 : http.createServer(express);
194
195 const port = options.https ? options.https.port || 4443 : options.port;
196 server.listen(port, () => logger.info(`server starts on port`, { port }));
197 const result = {
198 server,
199 express,
200 projectDir,
201 db: db!,
202 dbPool: dbPool!,
203 };
204
205 if (runMiddlewares) {
206 await runMiddlewares(result);
207 }
208
209 /* istanbul ignore next */
210 express.use((err: any, _: Express.Request, res: Express.Response, next: Express.NextFunction) => {
211 const { error, status } = handleError(err);
212 if (res.headersSent) {
213 return next(err);
214 }
215 res.status(status);
216 res.send({ status: 'error', error: error });
217 });
218
219 return result;
220 } catch (err) {
221 if (dbPool) {
222 await dbPool.end();
223 }
224 throw err;
225 }
226}
227
228const packageJsonFile = findUp.sync('package.json', { cwd: require.main!.filename });
229if (!packageJsonFile) throw new Exception('package.json is not found');
230const projectDir = dirname(packageJsonFile);
231
232const initFile = projectDir + '/.status';
233
234let activeThreadsCount = 0;
235export function asyncThread(fn: (req: Express.Request, res: Express.Response) => Promise<unknown>): Express.Handler {
236 return (req, res, next) => {
237 activeThreadsCount++;
238 fn(req, res)
239 .then(ret => res.send(ret || { status: 'ok' }), next)
240 .finally(() => activeThreadsCount--);
241 };
242}
243
244let lastExitRequestTime = 0;
245[`SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
246 process.on(eventType as 'exit', async code => {
247 // console.log('exit', {now: Date.now(), lastExitRequestTime, EXITING});
248 if (EXITING && Date.now() - lastExitRequestTime < 10) return;
249 if (EXITING) {
250 logger.warn('Force Exit Double SIGINT', { activeThreadsCount });
251 writeFileSync(initFile, 'ok');
252 process.exit();
253 }
254 lastExitRequestTime = Date.now();
255 logger.info('Exit requested', { eventType, code, activeThreadsCount });
256 EXITING = true;
257 let softExit = false;
258 for (let i = 0; i < 300; i++) {
259 if (activeThreadsCount === 0) {
260 softExit = true;
261 break;
262 }
263 await sleep(100);
264 }
265 if (softExit) {
266 logger.info('Exit');
267 } else {
268 logger.warn('Force Exit', { activeThreadsCount });
269 }
270 writeFileSync(initFile, 'ok');
271 process.exit();
272 });
273});
274
275function round(val: number, round: number) {
276 return Math.round(val / round) * round;
277}
278let prevCpuUsage = process.cpuUsage();
279const SYSTEM_HEALTH_INTERVAL = 600_000;
280setInterval(() => {
281 const mem = process.memoryUsage();
282 const cpu = process.cpuUsage();
283 const cpuSum = cpu.system - prevCpuUsage.system + (cpu.user - prevCpuUsage.user);
284 const cpuUsage = round((cpuSum / (SYSTEM_HEALTH_INTERVAL * 1000)) * 100, 1) + '%';
285 const headUsage = round(mem.heapUsed / 1024 ** 2, 50) + ' MB';
286 const rss = round(mem.rss / 1024 ** 2, 50) + ' MB';
287 logger.info('System health', { headUsage, rss, cpuUsage });
288 prevCpuUsage = cpu;
289}, SYSTEM_HEALTH_INTERVAL).unref();
290
291// const MIN_AVAILABLE_DISK_SPACE = 1024 ** 3;
292
293function checkFreeSpace() {
294 // diskusage
295 // .check('/')
296 // .then(res => {
297 // if (res.available < MIN_AVAILABLE_DISK_SPACE) {
298 // const availableSpace = round(res.available / 1024 ** 2, 50) + ' MB';
299 // logger.warn('Low available disk space', { availableSpace });
300 // }
301 // })
302 // .catch(err => logger.error(err));
303 // setTimeout(checkFreeSpace, 600_000).unref();
304}
305checkFreeSpace();
306
307if (existsSync(initFile) && readFileSync(initFile, 'utf8') !== 'ok') {
308 setTimeout(() => {
309 logger.warn('Last program was killed');
310 });
311}
312writeFileSync(initFile, '');
313
314process.on('unhandledRejection', reason => logger.warn('Unhandled Promise rejection', { reason }));
315process.on('uncaughtException', err => logger.error('UncaughtException', err));
316process.on('warning', warning => logger.warn('Warning', { warning }));