@stamhoofd/backend 2.85.4 → 2.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.nvmrc CHANGED
@@ -1 +1 @@
1
- 20.12
1
+ 22
package/index.ts CHANGED
@@ -1,237 +1,15 @@
1
1
  import backendEnv from '@stamhoofd/backend-env';
2
- backendEnv.load({ service: 'api' });
3
2
 
4
- import { Column, Database, Migration } from '@simonbackx/simple-database';
5
- import { CORSPreflightEndpoint, Router, RouterServer } from '@simonbackx/simple-endpoints';
6
- import { CORSMiddleware, LogMiddleware, VersionMiddleware } from '@stamhoofd/backend-middleware';
7
- import { Email } from '@stamhoofd/email';
8
- import { loadLogger } from '@stamhoofd/logging';
9
- import { Version } from '@stamhoofd/structures';
10
- import { sleep } from '@stamhoofd/utility';
11
-
12
- import { startCrons, stopCrons, waitForCrons } from '@stamhoofd/crons';
13
- import { Platform } from '@stamhoofd/models';
14
- import { resumeEmails } from './src/helpers/EmailResumer';
15
- import { GlobalHelper } from './src/helpers/GlobalHelper';
16
- import { SetupStepUpdater } from './src/helpers/SetupStepUpdater';
17
- import { ContextMiddleware } from './src/middleware/ContextMiddleware';
18
- import { AuditLogService } from './src/services/AuditLogService';
19
- import { BalanceItemService } from './src/services/BalanceItemService';
20
- import { DocumentService } from './src/services/DocumentService';
21
- import { FileSignService } from './src/services/FileSignService';
22
- import { PlatformMembershipService } from './src/services/PlatformMembershipService';
23
- import { UniqueUserService } from './src/services/UniqueUserService';
24
- import { QueueHandler } from '@stamhoofd/queues';
25
- import { SimpleError } from '@simonbackx/simple-errors';
26
-
27
- process.on('unhandledRejection', (error: Error) => {
28
- console.error('unhandledRejection');
29
- console.error(error.message, error.stack);
3
+ backendEnv.load({ service: 'api' }).catch((error) => {
4
+ console.error('Failed to load environment:', error);
30
5
  process.exit(1);
31
- });
32
-
33
- // Set version of saved structures
34
- Column.setJSONVersion(Version);
35
-
36
- // Set timezone
37
- process.env.TZ = 'UTC';
38
-
39
- // Quick check
40
- if (new Date().getTimezoneOffset() !== 0) {
41
- throw new Error('Process should always run in UTC timezone');
42
- }
43
-
44
- const seeds = async () => {
45
- try {
46
- // Internal
47
- await AuditLogService.disable(async () => {
48
- await Migration.runAll(__dirname + '/src/seeds');
49
- });
50
- }
51
- catch (e) {
52
- console.error('Failed to run seeds:');
53
- console.error(e);
6
+ }).then(async () => {
7
+ if (STAMHOOFD.environment === 'development') {
8
+ const { run } = await import('./src/migrate');
9
+ await run();
54
10
  }
55
- };
56
- const bootTime = process.hrtime();
57
-
58
- const start = async () => {
59
- console.log('Running server at v' + Version);
60
- loadLogger();
61
-
62
- await GlobalHelper.load();
63
- await UniqueUserService.check();
64
-
65
- // Init platform shared struct: otherwise permissions won't work with missing responsibilities
66
- console.log('Loading platform...');
67
- await Platform.loadCaches();
68
-
69
- const router = new Router();
70
-
71
- console.log('Loading endpoints...');
72
-
73
- // Note: we should load endpoints one by once to have a reliable order of url matching
74
- await router.loadAllEndpoints(__dirname + '/src/endpoints/global/*');
75
- await router.loadAllEndpoints(__dirname + '/src/endpoints/admin/*');
76
- await router.loadAllEndpoints(__dirname + '/src/endpoints/auth');
77
- await router.loadAllEndpoints(__dirname + '/src/endpoints/organization/dashboard/*');
78
- await router.loadAllEndpoints(__dirname + '/src/endpoints/organization/registration');
79
- await router.loadAllEndpoints(__dirname + '/src/endpoints/organization/webshops');
80
- await router.loadAllEndpoints(__dirname + '/src/endpoints/organization/shared');
81
- await router.loadAllEndpoints(__dirname + '/src/endpoints/organization/shared/*');
82
-
83
- router.endpoints.push(new CORSPreflightEndpoint());
84
-
85
- console.log('Creating router...');
86
- const routerServer = new RouterServer(router);
87
- routerServer.verbose = false;
88
-
89
- // Log requests and errors
90
- routerServer.addRequestMiddleware(LogMiddleware);
91
- routerServer.addResponseMiddleware(LogMiddleware);
92
-
93
- routerServer.addResponseMiddleware(FileSignService);
94
- routerServer.addRequestMiddleware(FileSignService);
95
-
96
- // Contexts
97
- routerServer.addRequestMiddleware(ContextMiddleware);
98
-
99
- // Add version headers and minimum version
100
- const versionMiddleware = new VersionMiddleware({
101
- latestVersions: {
102
- android: STAMHOOFD.LATEST_ANDROID_VERSION,
103
- ios: STAMHOOFD.LATEST_IOS_VERSION,
104
- web: Version,
105
- },
106
- minimumVersion: 331,
107
- });
108
- routerServer.addRequestMiddleware(versionMiddleware);
109
- routerServer.addResponseMiddleware(versionMiddleware);
110
-
111
- // Add CORS headers
112
- routerServer.addResponseMiddleware(CORSMiddleware);
113
-
114
- console.log('Loading loaders...');
115
-
116
- // Register Excel loaders
117
- await import('./src/excel-loaders');
118
-
119
- // Register Email Recipient loaders
120
- await import('./src/email-recipient-loaders/members');
121
- await import('./src/email-recipient-loaders/registrations');
122
- await import('./src/email-recipient-loaders/orders');
123
- await import('./src/email-recipient-loaders/receivable-balances');
124
- await import('./src/excel-loaders/registrations');
125
-
126
- console.log('Opening port...');
127
- routerServer.listen(STAMHOOFD.PORT ?? 9090);
128
-
129
- const hrend = process.hrtime(bootTime);
130
- console.log('🟢 HTTP server started in ' + Math.ceil(hrend[0] * 1000 + hrend[1] / 1000000) + 'ms');
131
-
132
- resumeEmails().catch(console.error);
133
-
134
- if (routerServer.server) {
135
- // Default timeout is a bit too short
136
- routerServer.server.timeout = 61000;
137
- }
138
-
139
- let shuttingDown = false;
140
- const shutdown = async () => {
141
- if (shuttingDown) {
142
- return;
143
- }
144
- shuttingDown = true;
145
- console.log('Shutting down...');
146
- // Disable keep alive
147
- routerServer.defaultHeaders = Object.assign(routerServer.defaultHeaders, { Connection: 'close' });
148
- if (routerServer.server) {
149
- routerServer.server.headersTimeout = 5000;
150
- routerServer.server.keepAliveTimeout = 1;
151
- }
152
-
153
- stopCrons();
154
-
155
- if (STAMHOOFD.environment === 'development') {
156
- setTimeout(() => {
157
- console.error('Forcing exit after 5 seconds');
158
- process.exit(1);
159
- }, 5000);
160
- }
161
-
162
- try {
163
- await routerServer.close();
164
- console.log('HTTP server stopped');
165
- }
166
- catch (err) {
167
- console.error('Failed to stop HTTP server:');
168
- console.error(err);
169
- }
170
-
171
- await BalanceItemService.flushAll();
172
- await waitForCrons();
173
- QueueHandler.abortAll(
174
- new SimpleError({
175
- code: 'SHUTDOWN',
176
- message: 'Shutting down',
177
- statusCode: 503,
178
- }),
179
- );
180
- await QueueHandler.awaitAll();
181
-
182
- try {
183
- while (Email.currentQueue.length > 0) {
184
- console.log(`${Email.currentQueue.length} emails still in queue. Waiting 500ms...`);
185
- await sleep(500);
186
- }
187
- }
188
- catch (err) {
189
- console.error('Failed to wait for emails to finish:');
190
- console.error(err);
191
- }
192
-
193
- try {
194
- await Database.end();
195
- console.log('MySQL connections closed');
196
- }
197
- catch (err) {
198
- console.error('Failed to close MySQL connection:');
199
- console.error(err);
200
- }
201
-
202
- // Should not be needed, but added for security as sometimes a promise hangs somewhere
203
- process.exit(0);
204
- };
205
-
206
- process.on('SIGTERM', () => {
207
- console.info('SIGTERM signal received.');
208
- shutdown().catch((e) => {
209
- console.error(e);
210
- process.exit(1);
211
- });
212
- });
213
-
214
- process.on('SIGINT', () => {
215
- console.info('SIGINT signal received.');
216
- shutdown().catch((e) => {
217
- console.error(e);
218
- process.exit(1);
219
- });
220
- });
221
-
222
- // Register crons
223
- await import('./src/crons');
224
-
225
- AuditLogService.listen();
226
- PlatformMembershipService.listen();
227
- DocumentService.listen();
228
- SetupStepUpdater.listen();
229
-
230
- startCrons();
231
- seeds().catch(console.error);
232
- };
233
-
234
- start().catch((error) => {
235
- console.error('unhandledRejection', error);
11
+ await import('./src/boot');
12
+ }).catch((error) => {
13
+ console.error('Failed to start the API:', error);
236
14
  process.exit(1);
237
15
  });
package/migrations.ts CHANGED
@@ -1,35 +1,13 @@
1
1
  import backendEnv from '@stamhoofd/backend-env';
2
- backendEnv.load();
3
2
 
4
- import { Column, Migration } from '@simonbackx/simple-database';
5
- import { Version } from '@stamhoofd/structures';
6
- import path from 'path';
7
-
8
- Column.setJSONVersion(Version);
9
- process.env.TZ = 'UTC';
10
-
11
- const emailPath = require.resolve('@stamhoofd/email');
12
- const modelsPath = require.resolve('@stamhoofd/models');
13
-
14
- // Validate UTC timezone
15
- if (new Date().getTimezoneOffset() !== 0) {
16
- throw new Error('Process should always run in UTC timezone');
17
- }
18
-
19
- const start = async () => {
20
- // External migrations
21
- await Migration.runAll(path.dirname(modelsPath) + '/migrations');
22
- await Migration.runAll(path.dirname(emailPath) + '/migrations');
23
-
24
- // Internal
25
- await Migration.runAll(__dirname + '/src/migrations');
26
- };
27
-
28
- start()
29
- .catch((error) => {
30
- console.error('unhandledRejection', error);
31
- process.exit(1);
32
- })
33
- .finally(() => {
34
- process.exit();
35
- });
3
+ backendEnv.load({ service: 'api' }).catch((error) => {
4
+ console.error('Failed to load environment:', error);
5
+ process.exit(1);
6
+ }).then(async () => {
7
+ const { run } = await import('./src/migrate');
8
+ await run();
9
+ process.exit(0);
10
+ }).catch((error) => {
11
+ console.error('Failed to run migrations:', error);
12
+ process.exit(1);
13
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stamhoofd/backend",
3
- "version": "2.85.4",
3
+ "version": "2.87.0",
4
4
  "main": "./dist/index.js",
5
5
  "exports": {
6
6
  ".": {
@@ -9,24 +9,25 @@
9
9
  },
10
10
  "license": "UNLICENCED",
11
11
  "scripts": {
12
- "dev": "echo 'Waiting for shared backend packages...' && wait-on ../../shared/middleware/dist/index.js && echo 'Start building backend API' && concurrently -r 'yarn build --watch --preserveWatchOutput' \"wait-on ./dist/index.js && nodemon --quiet --inspect=5858 --watch dist --watch '../../../shared/*/dist/' --watch '../../shared/*/dist/' --ext .ts,.json,.sql,.js --watch .env.json --delay 1000ms --exec 'node --enable-source-maps ./dist/index.js' --signal SIGTERM\"",
13
- "dev:backend": "yarn dev",
12
+ "dev": "wait-on ../../shared/middleware/dist/index.js && concurrently -r 'yarn -s build --watch --preserveWatchOutput' \"yarn -s dev:watch\"",
13
+ "dev:watch": "wait-on ./dist/index.js && nodemon --quiet --inspect=5858 --watch dist --watch '../../../shared/*/dist/' --watch '../../shared/*/dist/' --ext .ts,.json,.sql,.js --delay 2000ms --exec 'node --enable-source-maps ./dist/index.js' --signal SIGTERM",
14
+ "dev:backend": "yarn -s dev",
14
15
  "build": "rm -rf ./dist/src/migrations && rm -rf ./dist/src/seeds && tsc -b",
15
- "build:full": "yarn clear && yarn build",
16
+ "build:full": "yarn -s clear && yarn -s build",
16
17
  "clear": "rm -rf ./dist",
17
- "start": "yarn build && node --enable-source-maps ./dist/index.js",
18
+ "start": "yarn -s build && node --enable-source-maps ./dist/index.js",
18
19
  "test": "jest --runInBand",
19
20
  "test:build": "yarn -s build:full && yarn -s test",
20
- "test:reset": "yarn build:full && jest --runInBand",
21
- "migrations": "yarn build:full && node ./dist/migrations.js",
21
+ "test:reset": "yarn -s build:full && jest --runInBand",
22
+ "migrations": "yarn -s build:full && node ./dist/migrations.js",
22
23
  "lint": "eslint"
23
24
  },
24
25
  "devDependencies": {
25
- "@types/cookie": "^0.5.1",
26
+ "@types/cookie": "^0.6.0",
26
27
  "@types/luxon": "3.4.2",
27
28
  "@types/mailparser": "3.4.4",
28
29
  "@types/mysql": "^2.15.20",
29
- "@types/node": "^20.12",
30
+ "@types/node": "^22",
30
31
  "nock": "^13.5.1",
31
32
  "qs": "^6.11.2",
32
33
  "sinon": "^18.0.0"
@@ -39,22 +40,22 @@
39
40
  "@aws-sdk/s3-request-presigner": "3.823.0",
40
41
  "@bwip-js/node": "^4.5.1",
41
42
  "@mollie/api-client": "3.7.0",
42
- "@simonbackx/simple-database": "1.32.0",
43
+ "@simonbackx/simple-database": "1.33.0",
43
44
  "@simonbackx/simple-encoding": "2.22.0",
44
45
  "@simonbackx/simple-endpoints": "1.20.1",
45
46
  "@simonbackx/simple-logging": "^1.0.1",
46
- "@stamhoofd/backend-i18n": "2.85.4",
47
- "@stamhoofd/backend-middleware": "2.85.4",
48
- "@stamhoofd/email": "2.85.4",
49
- "@stamhoofd/models": "2.85.4",
50
- "@stamhoofd/queues": "2.85.4",
51
- "@stamhoofd/sql": "2.85.4",
52
- "@stamhoofd/structures": "2.85.4",
53
- "@stamhoofd/utility": "2.85.4",
47
+ "@stamhoofd/backend-i18n": "2.87.0",
48
+ "@stamhoofd/backend-middleware": "2.87.0",
49
+ "@stamhoofd/email": "2.87.0",
50
+ "@stamhoofd/models": "2.87.0",
51
+ "@stamhoofd/queues": "2.87.0",
52
+ "@stamhoofd/sql": "2.87.0",
53
+ "@stamhoofd/structures": "2.87.0",
54
+ "@stamhoofd/utility": "2.87.0",
54
55
  "archiver": "^7.0.1",
55
- "axios": "1.6.8",
56
- "cookie": "^0.5.0",
57
- "formidable": "3.5.1",
56
+ "axios": "^1.8.2",
57
+ "cookie": "^0.7.0",
58
+ "formidable": "3.5.4",
58
59
  "handlebars": "^4.7.7",
59
60
  "jsonwebtoken": "9.0.0",
60
61
  "luxon": "3.4.4",
@@ -63,11 +64,10 @@
63
64
  "mysql2": "^3.14.1",
64
65
  "node-rsa": "1.1.1",
65
66
  "openid-client": "^5.4.0",
66
- "postmark": "^4.0.5",
67
67
  "stripe": "^16.6.0"
68
68
  },
69
69
  "publishConfig": {
70
70
  "access": "public"
71
71
  },
72
- "gitHead": "ecceacc395d167a6761f1425e77cfc06a31fe8a9"
72
+ "gitHead": "372713e1e4b626d6b4ce4f3d148f14948528312e"
73
73
  }
package/src/boot.ts ADDED
@@ -0,0 +1,240 @@
1
+ import { Column, Database, Migration } from '@simonbackx/simple-database';
2
+ import { CORSPreflightEndpoint, Router, RouterServer } from '@simonbackx/simple-endpoints';
3
+ import { CORSMiddleware, LogMiddleware, VersionMiddleware } from '@stamhoofd/backend-middleware';
4
+ import { Email } from '@stamhoofd/email';
5
+ import { loadLogger } from '@stamhoofd/logging';
6
+ import { Version } from '@stamhoofd/structures';
7
+ import { sleep } from '@stamhoofd/utility';
8
+
9
+ import { startCrons, stopCrons, waitForCrons } from '@stamhoofd/crons';
10
+ import { Platform } from '@stamhoofd/models';
11
+ import { resumeEmails } from './helpers/EmailResumer';
12
+ import { GlobalHelper } from './helpers/GlobalHelper';
13
+ import { SetupStepUpdater } from './helpers/SetupStepUpdater';
14
+ import { ContextMiddleware } from './middleware/ContextMiddleware';
15
+ import { AuditLogService } from './services/AuditLogService';
16
+ import { BalanceItemService } from './services/BalanceItemService';
17
+ import { DocumentService } from './services/DocumentService';
18
+ import { FileSignService } from './services/FileSignService';
19
+ import { PlatformMembershipService } from './services/PlatformMembershipService';
20
+ import { UniqueUserService } from './services/UniqueUserService';
21
+ import { QueueHandler } from '@stamhoofd/queues';
22
+ import { SimpleError } from '@simonbackx/simple-errors';
23
+
24
+ process.on('unhandledRejection', (error: Error) => {
25
+ console.error('unhandledRejection');
26
+ console.error(error.message, error.stack);
27
+ process.exit(1);
28
+ });
29
+
30
+ // Set version of saved structures
31
+ Column.setJSONVersion(Version);
32
+
33
+ // Set timezone
34
+ process.env.TZ = 'UTC';
35
+
36
+ // Quick check
37
+ if (new Date().getTimezoneOffset() !== 0) {
38
+ throw new Error('Process should always run in UTC timezone');
39
+ }
40
+
41
+ const seeds = async () => {
42
+ try {
43
+ // Internal
44
+ await AuditLogService.disable(async () => {
45
+ await Migration.runAll(__dirname + '/seeds');
46
+ });
47
+ }
48
+ catch (e) {
49
+ console.error('Failed to run seeds:');
50
+ console.error(e);
51
+ }
52
+ };
53
+ const bootTime = process.hrtime();
54
+
55
+ function productionLog(message: string) {
56
+ if (STAMHOOFD.environment !== 'development') {
57
+ console.log(message);
58
+ }
59
+ }
60
+
61
+ const start = async () => {
62
+ productionLog('Running server at v' + Version);
63
+ loadLogger();
64
+
65
+ await GlobalHelper.load();
66
+ await UniqueUserService.check();
67
+
68
+ // Init platform shared struct: otherwise permissions won't work with missing responsibilities
69
+ productionLog('Loading platform...');
70
+ await Platform.loadCaches();
71
+
72
+ const router = new Router();
73
+
74
+ productionLog('Loading endpoints...');
75
+
76
+ // Note: we should load endpoints one by once to have a reliable order of url matching
77
+ await router.loadAllEndpoints(__dirname + '/endpoints/global/*');
78
+ await router.loadAllEndpoints(__dirname + '/endpoints/admin/*');
79
+ await router.loadAllEndpoints(__dirname + '/endpoints/auth');
80
+ await router.loadAllEndpoints(__dirname + '/endpoints/organization/dashboard/*');
81
+ await router.loadAllEndpoints(__dirname + '/endpoints/organization/registration');
82
+ await router.loadAllEndpoints(__dirname + '/endpoints/organization/webshops');
83
+ await router.loadAllEndpoints(__dirname + '/endpoints/organization/shared');
84
+ await router.loadAllEndpoints(__dirname + '/endpoints/organization/shared/*');
85
+
86
+ router.endpoints.push(new CORSPreflightEndpoint());
87
+
88
+ productionLog('Creating router...');
89
+ const routerServer = new RouterServer(router);
90
+ routerServer.verbose = false;
91
+
92
+ // Log requests and errors
93
+ routerServer.addRequestMiddleware(LogMiddleware);
94
+ routerServer.addResponseMiddleware(LogMiddleware);
95
+
96
+ routerServer.addResponseMiddleware(FileSignService);
97
+ routerServer.addRequestMiddleware(FileSignService);
98
+
99
+ // Contexts
100
+ routerServer.addRequestMiddleware(ContextMiddleware);
101
+
102
+ // Add version headers and minimum version
103
+ const versionMiddleware = new VersionMiddleware({
104
+ latestVersions: {
105
+ android: STAMHOOFD.LATEST_ANDROID_VERSION,
106
+ ios: STAMHOOFD.LATEST_IOS_VERSION,
107
+ web: Version,
108
+ },
109
+ minimumVersion: 331,
110
+ });
111
+ routerServer.addRequestMiddleware(versionMiddleware);
112
+ routerServer.addResponseMiddleware(versionMiddleware);
113
+
114
+ // Add CORS headers
115
+ routerServer.addResponseMiddleware(CORSMiddleware);
116
+
117
+ productionLog('Loading loaders...');
118
+
119
+ // Register Excel loaders
120
+ await import('./excel-loaders');
121
+
122
+ // Register Email Recipient loaders
123
+ await import('./email-recipient-loaders/members');
124
+ await import('./email-recipient-loaders/registrations');
125
+ await import('./email-recipient-loaders/orders');
126
+ await import('./email-recipient-loaders/receivable-balances');
127
+ await import('./excel-loaders/registrations');
128
+
129
+ productionLog('Opening port...');
130
+ routerServer.listen(STAMHOOFD.PORT ?? 9090);
131
+
132
+ const hrend = process.hrtime(bootTime);
133
+ productionLog('🟢 HTTP server started in ' + Math.ceil(hrend[0] * 1000 + hrend[1] / 1000000) + 'ms');
134
+
135
+ resumeEmails().catch(console.error);
136
+
137
+ if (routerServer.server) {
138
+ // Default timeout is a bit too short
139
+ routerServer.server.timeout = 61000;
140
+ }
141
+
142
+ let shuttingDown = false;
143
+ const shutdown = async () => {
144
+ if (shuttingDown) {
145
+ return;
146
+ }
147
+ shuttingDown = true;
148
+ productionLog('Shutting down...');
149
+ // Disable keep alive
150
+ routerServer.defaultHeaders = Object.assign(routerServer.defaultHeaders, { Connection: 'close' });
151
+ if (routerServer.server) {
152
+ routerServer.server.headersTimeout = 5000;
153
+ routerServer.server.keepAliveTimeout = 1;
154
+ }
155
+
156
+ stopCrons();
157
+
158
+ if (STAMHOOFD.environment === 'development') {
159
+ setTimeout(() => {
160
+ console.error('Forcing exit after 5 seconds');
161
+ process.exit(1);
162
+ }, 5000);
163
+ }
164
+
165
+ try {
166
+ await routerServer.close();
167
+ productionLog('HTTP server stopped');
168
+ }
169
+ catch (err) {
170
+ console.error('Failed to stop HTTP server:');
171
+ console.error(err);
172
+ }
173
+
174
+ await BalanceItemService.flushAll();
175
+ await waitForCrons();
176
+ QueueHandler.abortAll(
177
+ new SimpleError({
178
+ code: 'SHUTDOWN',
179
+ message: 'Shutting down',
180
+ statusCode: 503,
181
+ }),
182
+ );
183
+ await QueueHandler.awaitAll();
184
+
185
+ try {
186
+ while (Email.currentQueue.length > 0) {
187
+ console.log(`${Email.currentQueue.length} emails still in queue. Waiting 500ms...`);
188
+ await sleep(500);
189
+ }
190
+ }
191
+ catch (err) {
192
+ console.error('Failed to wait for emails to finish:');
193
+ console.error(err);
194
+ }
195
+
196
+ try {
197
+ await Database.end();
198
+ productionLog('MySQL connections closed');
199
+ }
200
+ catch (err) {
201
+ console.error('Failed to close MySQL connection:');
202
+ console.error(err);
203
+ }
204
+
205
+ // Should not be needed, but added for security as sometimes a promise hangs somewhere
206
+ process.exit(0);
207
+ };
208
+
209
+ process.on('SIGTERM', () => {
210
+ productionLog('SIGTERM signal received.');
211
+ shutdown().catch((e) => {
212
+ console.error(e);
213
+ process.exit(1);
214
+ });
215
+ });
216
+
217
+ process.on('SIGINT', () => {
218
+ productionLog('SIGINT signal received.');
219
+ shutdown().catch((e) => {
220
+ console.error(e);
221
+ process.exit(1);
222
+ });
223
+ });
224
+
225
+ // Register crons
226
+ await import('./crons');
227
+
228
+ AuditLogService.listen();
229
+ PlatformMembershipService.listen();
230
+ DocumentService.listen();
231
+ SetupStepUpdater.listen();
232
+
233
+ startCrons();
234
+ seeds().catch(console.error);
235
+ };
236
+
237
+ start().catch((error) => {
238
+ console.error('unhandledRejection', error);
239
+ process.exit(1);
240
+ });
@@ -131,7 +131,10 @@ describe('clearExcelCacheHelper', () => {
131
131
  dir.isDirectory = () => true;
132
132
  });
133
133
 
134
- fsMock.readdir.mockReturnValue(Promise.resolve([...directories, file1]));
134
+ fsMock.readdir.mockReturnValue(
135
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
136
+ Promise.resolve([...directories, file1]) as any,
137
+ );
135
138
  // #endregion
136
139
 
137
140
  // act
@@ -0,0 +1,37 @@
1
+ import { registerCron } from '@stamhoofd/crons';
2
+ import { Email } from '@stamhoofd/models';
3
+ import { EmailStatus } from '@stamhoofd/structures';
4
+
5
+ let lastRunDate: number | null = null;
6
+
7
+ registerCron('deleteOldEmailDrafts', deleteOldEmailDrafts);
8
+
9
+ /**
10
+ * Run every night at 5 AM.
11
+ */
12
+ export async function deleteOldEmailDrafts() {
13
+ const now = new Date();
14
+
15
+ if (now.getDate() === lastRunDate) {
16
+ return;
17
+ }
18
+
19
+ const hour = now.getHours();
20
+
21
+ // between 5 and 6 AM
22
+ if (hour !== 5 && STAMHOOFD.environment !== 'development') {
23
+ return;
24
+ }
25
+
26
+ // Clear old email drafts older than 7 days
27
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
28
+ const result = await Email.delete()
29
+ .where('status', EmailStatus.Draft)
30
+ .where('createdAt', '<', sevenDaysAgo)
31
+ .where('updatedAt', '<', sevenDaysAgo)
32
+ .where('sentAt', null);
33
+
34
+ console.log(`Deleted ${result.affectedRows} old email drafts.`);
35
+
36
+ lastRunDate = now.getDate();
37
+ }