1 | if (+process.versions.node.replace(/\.\d+$/, '') < 12)
|
2 | throw new Error(`Required version of node: >=12, current: ${process.versions.node}`);
|
3 |
|
4 | import dotenv from 'dotenv';
|
5 | export const ENV = process.env.NODE_ENV || 'development';
|
6 | const envFiles = ['.env', '.env.local', '.env.' + ENV, '.env.' + ENV + '.local'];
|
7 | envFiles.forEach(path => Object.assign(process.env, dotenv.config({ path }).parsed));
|
8 |
|
9 | import cors from 'cors';
|
10 | import 'deps-check';
|
11 | import Express from 'express';
|
12 | import graphqlHTTP from 'express-graphql';
|
13 | import session, { SessionOptions } from 'express-session';
|
14 | import { GraphQLError, validateSchema } from 'graphql';
|
15 | import { dirname } from 'path';
|
16 | import { createSchema } from 'ts2graphql';
|
17 | import { dbInit, DBOptions } from './dbInit';
|
18 | import { graphQLBigintTypeFactory } from './graphQLUtils';
|
19 | import { BaseDB, SchemaConstraint } from './Orm/PostgresqlDriver';
|
20 | import * as bodyparser from 'body-parser';
|
21 | import serveStatic from 'serve-static';
|
22 | import { readFileSync, writeFileSync, existsSync } from 'fs';
|
23 | import https from 'https';
|
24 | import http from 'http';
|
25 | import { ClientException, logger, Exception } from './logger';
|
26 | import { Pool } from 'pg';
|
27 | import findUp from 'find-up';
|
28 | import { sleep } from './utils';
|
29 |
|
30 |
|
31 | export * from './di';
|
32 | export * from './graphQLUtils';
|
33 | export * from './Orm/PostgresqlDriver';
|
34 | export * from './request';
|
35 | export * from './testUtils';
|
36 | export * from './utils';
|
37 | export * from './dateUtils';
|
38 | export * from './assert';
|
39 | export * from './logger';
|
40 | export const bodyParser = bodyparser;
|
41 |
|
42 | export const PRODUCTION = ENV === 'production';
|
43 |
|
44 | interface 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 |
|
69 | interface 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 |
|
77 | let EXITING = false;
|
78 | export 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 |
|
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 |
|
146 | return { error: options.errors.unknown, status: 500 };
|
147 | }
|
148 |
|
149 |
|
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 |
|
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 |
|
228 | const packageJsonFile = findUp.sync('package.json', { cwd: require.main!.filename });
|
229 | if (!packageJsonFile) throw new Exception('package.json is not found');
|
230 | const projectDir = dirname(packageJsonFile);
|
231 |
|
232 | const initFile = projectDir + '/.status';
|
233 |
|
234 | let activeThreadsCount = 0;
|
235 | export 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 |
|
244 | let lastExitRequestTime = 0;
|
245 | [`SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
|
246 | process.on(eventType as 'exit', async code => {
|
247 |
|
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 |
|
275 | function round(val: number, round: number) {
|
276 | return Math.round(val / round) * round;
|
277 | }
|
278 | let prevCpuUsage = process.cpuUsage();
|
279 | const SYSTEM_HEALTH_INTERVAL = 600_000;
|
280 | setInterval(() => {
|
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 |
|
292 |
|
293 | function checkFreeSpace() {
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 | }
|
305 | checkFreeSpace();
|
306 |
|
307 | if (existsSync(initFile) && readFileSync(initFile, 'utf8') !== 'ok') {
|
308 | setTimeout(() => {
|
309 | logger.warn('Last program was killed');
|
310 | });
|
311 | }
|
312 | writeFileSync(initFile, '');
|
313 |
|
314 | process.on('unhandledRejection', reason => logger.warn('Unhandled Promise rejection', { reason }));
|
315 | process.on('uncaughtException', err => logger.error('UncaughtException', err));
|
316 | process.on('warning', warning => logger.warn('Warning', { warning }));
|