@balena/pinejs 15.0.0-true-boolean-7896b116c446d891d7a0d5e4085c02a13bc9c725 → 15.0.1-build-migrations-clarify-marking-sbvr-optional-d6d0ded8eccc6eadb2492f4697918cf0afd00215-1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (145) hide show
  1. package/.dockerignore +4 -0
  2. package/.github/workflows/flowzone.yml +21 -0
  3. package/.husky/pre-commit +4 -0
  4. package/.pinejs-cache.json +1 -0
  5. package/.resinci.yml +1 -0
  6. package/.versionbot/CHANGELOG.yml +9678 -2002
  7. package/CHANGELOG.md +2976 -2
  8. package/Dockerfile +14 -0
  9. package/Gruntfile.ts +3 -6
  10. package/README.md +10 -1
  11. package/VERSION +1 -0
  12. package/build/browser.ts +1 -1
  13. package/build/config.ts +0 -1
  14. package/docker-compose.npm-test.yml +11 -0
  15. package/docs/AdvancedUsage.md +77 -63
  16. package/docs/GettingStarted.md +90 -41
  17. package/docs/Migrations.md +102 -1
  18. package/docs/ProjectConfig.md +12 -21
  19. package/docs/Testing.md +7 -0
  20. package/out/bin/abstract-sql-compiler.js +17 -17
  21. package/out/bin/abstract-sql-compiler.js.map +1 -1
  22. package/out/bin/odata-compiler.js +23 -20
  23. package/out/bin/odata-compiler.js.map +1 -1
  24. package/out/bin/sbvr-compiler.js +22 -22
  25. package/out/bin/sbvr-compiler.js.map +1 -1
  26. package/out/bin/utils.d.ts +2 -2
  27. package/out/bin/utils.js +3 -3
  28. package/out/bin/utils.js.map +1 -1
  29. package/out/config-loader/config-loader.d.ts +9 -8
  30. package/out/config-loader/config-loader.js +135 -78
  31. package/out/config-loader/config-loader.js.map +1 -1
  32. package/out/config-loader/env.d.ts +41 -16
  33. package/out/config-loader/env.js +46 -2
  34. package/out/config-loader/env.js.map +1 -1
  35. package/out/data-server/sbvr-server.d.ts +2 -19
  36. package/out/data-server/sbvr-server.js +44 -38
  37. package/out/data-server/sbvr-server.js.map +1 -1
  38. package/out/database-layer/db.d.ts +32 -14
  39. package/out/database-layer/db.js +120 -41
  40. package/out/database-layer/db.js.map +1 -1
  41. package/out/express-emulator/express.js +10 -11
  42. package/out/express-emulator/express.js.map +1 -1
  43. package/out/http-transactions/transactions.d.ts +2 -18
  44. package/out/http-transactions/transactions.js +29 -21
  45. package/out/http-transactions/transactions.js.map +1 -1
  46. package/out/migrator/async.d.ts +7 -0
  47. package/out/migrator/async.js +168 -0
  48. package/out/migrator/async.js.map +1 -0
  49. package/out/migrator/migrations.sbvr +43 -0
  50. package/out/migrator/sync.d.ts +9 -0
  51. package/out/migrator/sync.js +106 -0
  52. package/out/migrator/sync.js.map +1 -0
  53. package/out/migrator/utils.d.ts +78 -0
  54. package/out/migrator/utils.js +283 -0
  55. package/out/migrator/utils.js.map +1 -0
  56. package/out/odata-metadata/odata-metadata-generator.js +10 -13
  57. package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
  58. package/out/passport-pinejs/passport-pinejs.d.ts +1 -1
  59. package/out/passport-pinejs/passport-pinejs.js +8 -7
  60. package/out/passport-pinejs/passport-pinejs.js.map +1 -1
  61. package/out/pinejs-session-store/pinejs-session-store.d.ts +1 -1
  62. package/out/pinejs-session-store/pinejs-session-store.js +20 -6
  63. package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
  64. package/out/sbvr-api/abstract-sql.d.ts +3 -2
  65. package/out/sbvr-api/abstract-sql.js +9 -9
  66. package/out/sbvr-api/abstract-sql.js.map +1 -1
  67. package/out/sbvr-api/cached-compile.js +1 -1
  68. package/out/sbvr-api/cached-compile.js.map +1 -1
  69. package/out/sbvr-api/common-types.d.ts +6 -5
  70. package/out/sbvr-api/control-flow.d.ts +8 -1
  71. package/out/sbvr-api/control-flow.js +36 -9
  72. package/out/sbvr-api/control-flow.js.map +1 -1
  73. package/out/sbvr-api/errors.d.ts +47 -40
  74. package/out/sbvr-api/errors.js +78 -77
  75. package/out/sbvr-api/errors.js.map +1 -1
  76. package/out/sbvr-api/express-extension.d.ts +4 -0
  77. package/out/sbvr-api/hooks.d.ts +16 -15
  78. package/out/sbvr-api/hooks.js +74 -48
  79. package/out/sbvr-api/hooks.js.map +1 -1
  80. package/out/sbvr-api/odata-response.d.ts +2 -2
  81. package/out/sbvr-api/odata-response.js +28 -30
  82. package/out/sbvr-api/odata-response.js.map +1 -1
  83. package/out/sbvr-api/permissions.d.ts +17 -16
  84. package/out/sbvr-api/permissions.js +369 -304
  85. package/out/sbvr-api/permissions.js.map +1 -1
  86. package/out/sbvr-api/sbvr-utils.d.ts +33 -15
  87. package/out/sbvr-api/sbvr-utils.js +397 -235
  88. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  89. package/out/sbvr-api/translations.d.ts +6 -0
  90. package/out/sbvr-api/translations.js +150 -0
  91. package/out/sbvr-api/translations.js.map +1 -0
  92. package/out/sbvr-api/uri-parser.d.ts +23 -17
  93. package/out/sbvr-api/uri-parser.js +33 -27
  94. package/out/sbvr-api/uri-parser.js.map +1 -1
  95. package/out/sbvr-api/user.sbvr +2 -0
  96. package/out/server-glue/module.d.ts +6 -6
  97. package/out/server-glue/module.js +4 -2
  98. package/out/server-glue/module.js.map +1 -1
  99. package/out/server-glue/server.js +5 -5
  100. package/out/server-glue/server.js.map +1 -1
  101. package/package.json +89 -73
  102. package/pinejs.png +0 -0
  103. package/repo.yml +9 -9
  104. package/src/bin/abstract-sql-compiler.ts +5 -7
  105. package/src/bin/odata-compiler.ts +11 -13
  106. package/src/bin/sbvr-compiler.ts +11 -17
  107. package/src/bin/utils.ts +3 -5
  108. package/src/config-loader/config-loader.ts +167 -53
  109. package/src/config-loader/env.ts +106 -6
  110. package/src/data-server/sbvr-server.js +44 -38
  111. package/src/database-layer/db.ts +205 -64
  112. package/src/express-emulator/express.js +10 -11
  113. package/src/http-transactions/transactions.js +29 -21
  114. package/src/migrator/async.ts +323 -0
  115. package/src/migrator/migrations.sbvr +43 -0
  116. package/src/migrator/sync.ts +152 -0
  117. package/src/migrator/utils.ts +458 -0
  118. package/src/odata-metadata/odata-metadata-generator.ts +12 -15
  119. package/src/passport-pinejs/passport-pinejs.ts +9 -7
  120. package/src/pinejs-session-store/pinejs-session-store.ts +15 -1
  121. package/src/sbvr-api/abstract-sql.ts +17 -14
  122. package/src/sbvr-api/common-types.ts +2 -1
  123. package/src/sbvr-api/control-flow.ts +45 -11
  124. package/src/sbvr-api/errors.ts +82 -77
  125. package/src/sbvr-api/express-extension.ts +6 -1
  126. package/src/sbvr-api/hooks.ts +123 -50
  127. package/src/sbvr-api/odata-response.ts +23 -28
  128. package/src/sbvr-api/permissions.ts +548 -415
  129. package/src/sbvr-api/sbvr-utils.ts +581 -259
  130. package/src/sbvr-api/translations.ts +248 -0
  131. package/src/sbvr-api/uri-parser.ts +63 -49
  132. package/src/sbvr-api/user.sbvr +2 -0
  133. package/src/server-glue/module.ts +16 -10
  134. package/src/server-glue/server.ts +5 -5
  135. package/tsconfig.dev.json +1 -0
  136. package/tsconfig.json +1 -2
  137. package/typings/lf-to-abstract-sql.d.ts +6 -9
  138. package/typings/memoizee.d.ts +1 -1
  139. package/.github/CODEOWNERS +0 -1
  140. package/circle.yml +0 -37
  141. package/docs/todo.txt +0 -22
  142. package/out/migrator/migrator.d.ts +0 -20
  143. package/out/migrator/migrator.js +0 -188
  144. package/out/migrator/migrator.js.map +0 -1
  145. package/src/migrator/migrator.ts +0 -286
@@ -1,8 +1,24 @@
1
1
  import type * as Express from 'express';
2
- import type { AbstractSqlModel } from '@balena/abstract-sql-compiler';
2
+ import type {
3
+ AbstractSqlModel,
4
+ Definition,
5
+ } from '@balena/abstract-sql-compiler';
3
6
  import type { Database } from '../database-layer/db';
4
- import type { Migration } from '../migrator/migrator';
5
- import type { AnyObject, Resolvable } from '../sbvr-api/common-types';
7
+ import type {
8
+ AnyObject,
9
+ Dictionary,
10
+ RequiredField,
11
+ Resolvable,
12
+ } from '../sbvr-api/common-types';
13
+
14
+ import {
15
+ Migration,
16
+ Migrations,
17
+ defaultMigrationCategory,
18
+ MigrationCategories,
19
+ isSyncMigration,
20
+ isAsyncMigration,
21
+ } from '../migrator/utils';
6
22
 
7
23
  import * as fs from 'fs';
8
24
  import * as _ from 'lodash';
@@ -11,6 +27,7 @@ import * as path from 'path';
11
27
  import * as sbvrUtils from '../sbvr-api/sbvr-utils';
12
28
 
13
29
  import * as permissions from '../sbvr-api/permissions';
30
+ import { AliasValidNodeType } from '../sbvr-api/translations';
14
31
 
15
32
  export type SetupFunction = (
16
33
  app: Express.Application,
@@ -25,9 +42,7 @@ export interface Model {
25
42
  modelText?: string;
26
43
  abstractSql?: AbstractSqlModel;
27
44
  migrationsPath?: string;
28
- migrations?: {
29
- [index: string]: Migration;
30
- };
45
+ migrations?: Migrations;
31
46
  initSqlPath?: string;
32
47
  initSql?: string;
33
48
  customServerCode?:
@@ -36,11 +51,15 @@ export interface Model {
36
51
  setup: SetupFunction;
37
52
  };
38
53
  logging?: { [key in keyof Console | 'default']?: boolean };
54
+ translateTo?: Model['apiRoot'];
55
+ translations?: Dictionary<
56
+ Definition | Dictionary<string | AliasValidNodeType>
57
+ >;
39
58
  }
40
59
  export interface User {
41
60
  username: string;
42
61
  password: string;
43
- permissions: string[];
62
+ permissions?: string[];
44
63
  }
45
64
  export interface Config {
46
65
  models: Model[];
@@ -77,7 +96,7 @@ const getOrCreatePermission = async (
77
96
  ) => {
78
97
  try {
79
98
  return await getOrCreate(authApiTx, 'permission', { name: permissionName });
80
- } catch (e) {
99
+ } catch (e: any) {
81
100
  e.message = `Could not create or find permission "${permissionName}": ${e.message}`;
82
101
  throw e;
83
102
  }
@@ -85,8 +104,8 @@ const getOrCreatePermission = async (
85
104
 
86
105
  // Setup function
87
106
  export const setup = (app: Express.Application) => {
88
- const loadConfig = (data: Config): Promise<void> =>
89
- sbvrUtils.db.transaction(async (tx) => {
107
+ const loadConfig = async (data: Config): Promise<void> => {
108
+ await sbvrUtils.db.transaction(async (tx) => {
90
109
  const authApiTx = sbvrUtils.api.Auth.clone({
91
110
  passthrough: {
92
111
  tx,
@@ -99,20 +118,17 @@ export const setup = (app: Express.Application) => {
99
118
  const permissionsCache: {
100
119
  [index: string]: Promise<number>;
101
120
  } = {};
102
- users.forEach((user) => {
121
+ for (const user of users) {
103
122
  if (user.permissions == null) {
104
- return;
123
+ continue;
105
124
  }
106
- user.permissions.forEach((permissionName) => {
107
- if (permissionsCache[permissionName] != null) {
108
- return;
109
- }
110
- permissionsCache[permissionName] = getOrCreatePermission(
125
+ for (const permissionName of user.permissions) {
126
+ permissionsCache[permissionName] ??= getOrCreatePermission(
111
127
  authApiTx,
112
128
  permissionName,
113
129
  );
114
- });
115
- });
130
+ }
131
+ }
116
132
 
117
133
  await Promise.all(
118
134
  users.map(async (user) => {
@@ -138,7 +154,7 @@ export const setup = (app: Express.Application) => {
138
154
  }),
139
155
  );
140
156
  }
141
- } catch (e) {
157
+ } catch (e: any) {
142
158
  e.message = `Could not create or find user "${user.username}": ${e.message}`;
143
159
  throw e;
144
160
  }
@@ -146,44 +162,76 @@ export const setup = (app: Express.Application) => {
146
162
  );
147
163
  }
148
164
 
149
- await Promise.all(
150
- data.models.map(async (model) => {
151
- if (
165
+ const modelPromises: Dictionary<Promise<void>> = _(data.models)
166
+ .filter(
167
+ (model): model is RequiredField<typeof model, 'apiRoot'> =>
152
168
  (model.abstractSql != null || model.modelText != null) &&
153
- model.apiRoot != null
154
- ) {
155
- try {
156
- await sbvrUtils.executeModel(
157
- tx,
158
- model as sbvrUtils.ExecutableModel,
169
+ model.apiRoot != null,
170
+ )
171
+ .keyBy((model) => model.apiRoot)
172
+ .mapValues(async (model: Model) => {
173
+ try {
174
+ const { translateTo, translations } = model;
175
+ if (translateTo != null) {
176
+ // We add an async boundary here so that `mapValues` can complete and assign the
177
+ // waiting promises to `modelPromises` before we try to access it
178
+ await null;
179
+ if (modelPromises[translateTo] == null) {
180
+ throw new Error(
181
+ `Cannot translate to non-existent version '${translateTo}'`,
182
+ );
183
+ }
184
+ // Wait on the `translateTo` version since it needs to already be available
185
+ await modelPromises[translateTo];
186
+ } else if (translations != null) {
187
+ throw new Error(
188
+ 'Cannot have translations without a translateTo target',
159
189
  );
190
+ }
160
191
 
161
- const apiRoute = `/${model.apiRoot}/*`;
162
- app.options(apiRoute, (_req, res) => res.sendStatus(200));
163
- app.all(apiRoute, sbvrUtils.handleODataRequest);
192
+ await sbvrUtils.executeModel(
193
+ tx,
194
+ model as sbvrUtils.ExecutableModel,
195
+ );
164
196
 
165
- console.info(
166
- 'Successfully executed ' + model.modelName + ' model.',
167
- );
168
- } catch (err) {
169
- const message = `Failed to execute ${model.modelName} model from ${model.modelFile}`;
170
- if (_.isError(err)) {
171
- err.message = message;
172
- throw err;
173
- }
174
- throw new Error(message);
197
+ const apiRoute = `/${model.apiRoot}/*`;
198
+ app
199
+ .route(apiRoute)
200
+ .options((_req, res) => res.status(200).end())
201
+ .get(sbvrUtils.handleODataRequest)
202
+ .put(sbvrUtils.handleODataRequest)
203
+ .post(sbvrUtils.handleODataRequest)
204
+ .patch(sbvrUtils.handleODataRequest)
205
+ .merge(sbvrUtils.handleODataRequest)
206
+ .delete(sbvrUtils.handleODataRequest);
207
+
208
+ console.info(
209
+ 'Successfully executed ' + model.modelName + ' model.',
210
+ );
211
+ } catch (err: any) {
212
+ const message = `Failed to execute '${model.modelName}' model from '${model.modelFile}'`;
213
+ if (_.isError(err)) {
214
+ err.message = `${message} due to: ${err.message}`;
215
+ throw err;
175
216
  }
217
+ throw new Error(message);
176
218
  }
219
+ })
220
+ .value();
221
+ await Promise.all(Object.values(modelPromises));
222
+
223
+ await Promise.all(
224
+ data.models.map(async (model) => {
177
225
  if (model.customServerCode != null) {
178
226
  let customCode: SetupFunction;
179
227
  if (typeof model.customServerCode === 'string') {
180
228
  try {
181
229
  customCode = nodeRequire(model.customServerCode).setup;
182
- } catch (e) {
230
+ } catch (e: any) {
183
231
  e.message = `Error loading custom server code: '${e.message}'`;
184
232
  throw e;
185
233
  }
186
- } else if (_.isObject(model.customServerCode)) {
234
+ } else if (typeof model.customServerCode === 'object') {
187
235
  customCode = model.customServerCode.setup;
188
236
  } else {
189
237
  throw new Error(
@@ -200,7 +248,7 @@ export const setup = (app: Express.Application) => {
200
248
  }),
201
249
  );
202
250
  });
203
-
251
+ };
204
252
  const loadConfigFile = async (configPath: string): Promise<Config> => {
205
253
  console.info('Loading config:', configPath);
206
254
  return await import(configPath);
@@ -217,7 +265,7 @@ export const setup = (app: Express.Application) => {
217
265
  } else if (typeof config === 'string') {
218
266
  root = path.dirname(config);
219
267
  configObj = await loadConfigFile(config);
220
- } else if (_.isObject(config)) {
268
+ } else if (typeof config === 'object') {
221
269
  root = process.cwd();
222
270
  configObj = config;
223
271
  } else {
@@ -251,18 +299,84 @@ export const setup = (app: Express.Application) => {
251
299
  await Promise.all(
252
300
  fileNames.map(async (filename) => {
253
301
  const filePath = path.join(migrationsPath, filename);
302
+ const fileNameParts = filename.split('.', 3);
303
+ const fileExtension = path.extname(filename);
254
304
  const [migrationKey] = filename.split('-', 1);
305
+ let migrationCategory = defaultMigrationCategory;
255
306
 
256
- switch (path.extname(filename)) {
307
+ if (fileNameParts.length === 3) {
308
+ if (fileNameParts[1] in MigrationCategories) {
309
+ migrationCategory = fileNameParts[1] as MigrationCategories;
310
+ } else {
311
+ console.error(
312
+ `Unrecognized migration file category ${
313
+ fileNameParts[1]
314
+ }, skipping: ${path.extname(filename)}`,
315
+ );
316
+ return;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * helper to assign migrations with category level to model
322
+ * example migration file names:
323
+ *
324
+ * key0-name.ts ==> defaults startup migration
325
+ * key1-name1.sql ==> defaults startup migration
326
+ * key2-name2.sync.sql ==> explicit synchrony migration
327
+ * key3-name3.async.ts ==> async migration (async datafiller)
328
+ * key4-name4.async.sql ==> async migration (async datafiller)
329
+ *
330
+ */
331
+ const assignMigrationWithCategory = (
332
+ newMigrationKey: string,
333
+ newMigration: Migration,
334
+ ) => {
335
+ const catMigrations = migrations[migrationCategory] || {};
336
+ if (
337
+ typeof newMigration === 'object' &&
338
+ migrationCategory === MigrationCategories.async
339
+ ) {
340
+ newMigration.type = MigrationCategories.async;
341
+ }
342
+ if (typeof catMigrations === 'object') {
343
+ migrations[migrationCategory] = {
344
+ [newMigrationKey]: newMigration,
345
+ ...catMigrations,
346
+ };
347
+ }
348
+ };
349
+
350
+ switch (fileExtension) {
257
351
  case '.coffee':
258
352
  case '.ts':
259
353
  case '.js':
260
- migrations[migrationKey] = nodeRequire(filePath);
354
+ const loadeMigration = nodeRequire(filePath);
355
+ const migration = loadeMigration.default ?? loadeMigration;
356
+
357
+ if (
358
+ !isAsyncMigration(migration) &&
359
+ !isSyncMigration(migration)
360
+ ) {
361
+ throw new Error(
362
+ `loaded migraton file at ${filePath} is neither a synchron nor a asyncron migration definition`,
363
+ );
364
+ }
365
+
366
+ assignMigrationWithCategory(migrationKey, migration);
261
367
  break;
262
368
  case '.sql':
263
- migrations[migrationKey] = await fs.promises.readFile(
264
- filePath,
265
- 'utf8',
369
+ if (migrationCategory === MigrationCategories.async) {
370
+ console.error(
371
+ `Plain async migration sql statement not supported, needs to be an object, skipping: ${path.extname(
372
+ filename,
373
+ )}`,
374
+ );
375
+ break;
376
+ }
377
+ assignMigrationWithCategory(
378
+ migrationKey,
379
+ await fs.promises.readFile(filePath, 'utf8'),
266
380
  );
267
381
  break;
268
382
  default:
@@ -282,8 +396,8 @@ export const setup = (app: Express.Application) => {
282
396
  }),
283
397
  );
284
398
  await loadConfig(configObj);
285
- } catch (err) {
286
- console.error('Error loading application config', err, err.stack);
399
+ } catch (err: any) {
400
+ console.error('Error loading application config', err);
287
401
  process.exit(1);
288
402
  }
289
403
  };
@@ -1,21 +1,81 @@
1
- export const { DEBUG } = process.env;
1
+ const { PINEJS_DEBUG } = process.env;
2
+ if (![undefined, '', '0', '1'].includes(PINEJS_DEBUG)) {
3
+ throw new Error(`Invalid value for PINEJS_DEBUG '${PINEJS_DEBUG}'`);
4
+ }
5
+ export const DEBUG = PINEJS_DEBUG === '1';
6
+
7
+ type CacheFnOpts<T extends (...args: any[]) => any> =
8
+ | {
9
+ primitive?: true;
10
+ promise?: true;
11
+ normalizer?: memoizeWeak.MemoizeWeakOptions<T>['normalizer'];
12
+ weak: true;
13
+ }
14
+ | {
15
+ primitive?: true;
16
+ promise?: true;
17
+ normalizer?: memoize.Options<T>['normalizer'];
18
+ weak?: undefined;
19
+ };
20
+ export type CacheFn = <T extends (...args: any[]) => any>(
21
+ fn: T,
22
+ opts?: CacheFnOpts<T>,
23
+ ) => T;
24
+ export type CacheOpts =
25
+ | {
26
+ max?: number;
27
+ }
28
+ | CacheFn
29
+ | false;
2
30
 
3
31
  export const cache = {
4
32
  permissionsLookup: {
5
33
  max: 5000,
6
- },
34
+ } as CacheOpts,
7
35
  parsePermissions: {
8
36
  max: 100000,
9
- },
37
+ } as CacheOpts,
10
38
  parseOData: {
11
39
  max: 100000,
12
- },
40
+ } as CacheOpts,
13
41
  odataToAbstractSql: {
14
42
  max: 10000,
15
- },
43
+ } as CacheOpts,
16
44
  abstractSqlCompiler: {
17
45
  max: 10000,
18
- },
46
+ } as CacheOpts,
47
+ userPermissions: false as CacheOpts,
48
+ apiKeyPermissions: false as CacheOpts,
49
+ apiKeyActorId: false as CacheOpts,
50
+ };
51
+
52
+ import { boolVar } from '@balena/env-parsing';
53
+ import * as memoize from 'memoizee';
54
+ import memoizeWeak = require('memoizee/weak');
55
+ export const createCache = <T extends (...args: any[]) => any>(
56
+ cacheName: keyof typeof cache,
57
+ fn: T,
58
+ // TODO: Mark this as optional once TS is able to infer the `normalizer` types
59
+ // when the `weak` differentiating property is not provided.
60
+ opts: CacheFnOpts<T>,
61
+ ) => {
62
+ const cacheOpts = cache[cacheName];
63
+ if (cacheOpts === false) {
64
+ return fn;
65
+ }
66
+ if (typeof cacheOpts === 'function') {
67
+ return cacheOpts(fn, opts);
68
+ }
69
+ if (opts?.weak === true) {
70
+ return memoizeWeak(fn, {
71
+ ...cacheOpts,
72
+ ...opts,
73
+ });
74
+ }
75
+ return memoize(fn, {
76
+ ...cacheOpts,
77
+ ...opts,
78
+ });
19
79
  };
20
80
 
21
81
  let timeoutMS: number;
@@ -39,10 +99,50 @@ export const db = {
39
99
  keepAlive: true as boolean | undefined,
40
100
  rollbackTimeout: 30000,
41
101
  timeoutMS,
102
+ maxUses: Infinity,
103
+ maxLifetimeSeconds: 0,
104
+ /**
105
+ * Check that queries in read-only TXs only contain `SELECT` statements, doing so adds a cost to each query
106
+ * in a read-only TX and is unnecessary if it is part of a read-only database transaction. The only time a
107
+ * writable transaction should be used with a read-only TX is during a read-only hook within a writable request
108
+ * and so should only be able to catch cases of hooks that are incorrectly marked as read-only
109
+ *
110
+ * Defaults to true when in DEBUG mode, false otherwise
111
+ */
112
+ checkReadOnlyQueries: DEBUG,
42
113
  };
43
114
 
115
+ export const PINEJS_ADVISORY_LOCK = {
116
+ namespaceKey: 'pinejs_advisory_lock_namespace',
117
+ namespaceId: -1,
118
+ };
119
+ // for better readability of logging
120
+ export const booleanToEnabledString = (input: boolean) =>
121
+ input ? 'enabled' : 'disabled';
122
+
44
123
  export const migrator = {
45
124
  lockTimeout: 5 * 60 * 1000,
46
125
  // Used to delay the failure on lock taking, to avoid spam taking
47
126
  lockFailDelay: 20 * 1000,
127
+ asyncMigrationDefaultDelayMS: 1000,
128
+ asyncMigrationDefaultBackoffDelayMS: 60000,
129
+ asyncMigrationDefaultErrorThreshold: 10,
130
+ asyncMigrationDefaultBatchSize: 1000,
131
+
132
+ /**
133
+ * @param asyncMigrationIsEnabled Switch on/off the execution of async migrations
134
+ * Example implementation with listening on SIGUSR2 to toggle the AsyncMigrationExecution enable switch
135
+ * For runtime switching the async migration execution the SIGUSR2 signal is interpreted as toggle.
136
+ * When the process receives a SIGUSR2 signal the async migrations will toggle to be enabled or disabled.
137
+ * @example
138
+ * process.on('SIGUSR2', () => {
139
+ * console.info(
140
+ * `Received SIGUSR2 to toggle async migration execution enabled
141
+ * from ${migrator.asyncMigrationIsEnabled}
142
+ * to ${!migrator.asyncMigrationIsEnabled} `,
143
+ * );
144
+ * migrator.asyncMigrationIsEnabled = !migrator.asyncMigrationIsEnabled;
145
+ * });
146
+ */
147
+ asyncMigrationIsEnabled: boolVar('PINEJS_ASYNC_MIGRATION_ENABLED', true),
48
148
  };
@@ -47,6 +47,7 @@ const serverIsOnAir = async (_req, _res, next) => {
47
47
  }
48
48
  };
49
49
 
50
+ /** @type {import('../config-loader/config-loader').Config} */
50
51
  export let config = {
51
52
  models: [
52
53
  {
@@ -59,17 +60,22 @@ export let config = {
59
60
  ALTER TABLE "textarea"
60
61
  ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;\
61
62
  `,
62
- '15.0.0-true-boolean': {
63
- mysql: `\
64
- ALTER TABLE "textarea"
65
- MODIFY "is disabled" BOOLEAN NOT NULL;\
66
- `,
67
- postgres: `\
68
- ALTER TABLE "textarea"
69
- ALTER COLUMN "is disabled" SET DATA TYPE BOOLEAN USING b::BOOLEAN;\
70
- `,
71
- // No need to migrate for websql
72
- websql: '',
63
+ '15.0.0-data-types': async (tx, sbvrUtils) => {
64
+ switch (sbvrUtils.db.engine) {
65
+ case 'mysql':
66
+ await tx.executeSql(`\
67
+ ALTER TABLE "textarea"
68
+ MODIFY "is disabled" BOOLEAN NOT NULL;`);
69
+ break;
70
+ case 'postgres':
71
+ await tx.executeSql(`\
72
+ ALTER TABLE "textarea"
73
+ ALTER COLUMN "is disabled" DROP DEFAULT,
74
+ ALTER COLUMN "is disabled" SET DATA TYPE BOOLEAN USING "is disabled"::BOOLEAN,
75
+ ALTER COLUMN "is disabled" SET DEFAULT FALSE;`);
76
+ break;
77
+ // No need to migrate for websql
78
+ }
73
79
  },
74
80
  },
75
81
  },
@@ -134,7 +140,7 @@ export async function setup(app, sbvrUtils, db) {
134
140
  const instance = result[0];
135
141
  await sbvrUtils.executeModel(tx, {
136
142
  apiRoot: instance.is_of__vocabulary,
137
- modelText: instance.model_value,
143
+ modelText: instance.model_value.value,
138
144
  });
139
145
  });
140
146
  await isServerOnAir(true);
@@ -153,7 +159,7 @@ export async function setup(app, sbvrUtils, db) {
153
159
  permissions.checkPermissionsMiddleware('all'),
154
160
  serverIsOnAir,
155
161
  (_req, res) => {
156
- res.sendStatus(404);
162
+ res.status(404).end();
157
163
  },
158
164
  );
159
165
 
@@ -199,7 +205,7 @@ export async function setup(app, sbvrUtils, db) {
199
205
  });
200
206
  });
201
207
  await isServerOnAir(true);
202
- res.sendStatus(200);
208
+ res.status(200).end();
203
209
  } catch (err) {
204
210
  await isServerOnAir(false);
205
211
  res.status(404).json(err);
@@ -215,7 +221,7 @@ export async function setup(app, sbvrUtils, db) {
215
221
  res.json(results);
216
222
  } catch (err) {
217
223
  console.log('Error validating', err);
218
- res.sendStatus(404);
224
+ res.status(404).end();
219
225
  }
220
226
  },
221
227
  );
@@ -239,10 +245,10 @@ export async function setup(app, sbvrUtils, db) {
239
245
  await sbvrUtils.executeModels(tx, exports.config.models);
240
246
  await setupModels(tx);
241
247
  });
242
- res.sendStatus(200);
243
- } catch (err) {
244
- console.error('Error clearing db', err, err.stack);
245
- res.sendStatus(503);
248
+ res.status(200).end();
249
+ } catch (/** @type any */ err) {
250
+ console.error('Error clearing db', err);
251
+ res.status(503).end();
246
252
  }
247
253
  },
248
254
  );
@@ -266,10 +272,10 @@ export async function setup(app, sbvrUtils, db) {
266
272
  }
267
273
  }
268
274
  });
269
- res.sendStatus(200);
270
- } catch (err) {
271
- console.error('Error importing db', err, err.stack);
272
- res.sendStatus(404);
275
+ res.status(200).end();
276
+ } catch (/** @type any */ err) {
277
+ console.error('Error importing db', err);
278
+ res.status(404).end();
273
279
  }
274
280
  },
275
281
  );
@@ -290,11 +296,11 @@ export async function setup(app, sbvrUtils, db) {
290
296
  'SELECT * FROM "' + tableName + '";',
291
297
  );
292
298
  let insQuery = '';
293
- result.rows.forEach((currRow) => {
299
+ for (const currRow of result.rows) {
294
300
  let notFirst = false;
295
301
  insQuery += 'INSERT INTO "' + tableName + '" (';
296
302
  let valQuery = '';
297
- for (let propName of Object.keys(currRow)) {
303
+ for (const propName of Object.keys(currRow)) {
298
304
  if (notFirst) {
299
305
  insQuery += ',';
300
306
  valQuery += ',';
@@ -305,15 +311,15 @@ export async function setup(app, sbvrUtils, db) {
305
311
  valQuery += "'" + currRow[propName] + "'";
306
312
  }
307
313
  insQuery += ') values (' + valQuery + ');\n';
308
- });
314
+ }
309
315
  exported += insQuery;
310
316
  }),
311
317
  );
312
318
  });
313
319
  res.json(exported);
314
- } catch (err) {
315
- console.error('Error exporting db', err, err.stack);
316
- res.sendStatus(503);
320
+ } catch (/** @type any */ err) {
321
+ console.error('Error exporting db', err);
322
+ res.status(503).end();
317
323
  }
318
324
  },
319
325
  );
@@ -340,10 +346,10 @@ export async function setup(app, sbvrUtils, db) {
340
346
  }),
341
347
  );
342
348
  });
343
- res.sendStatus(200);
344
- } catch (err) {
345
- console.error('Error backing up db', err, err.stack);
346
- res.sendStatus(404);
349
+ res.status(200).end();
350
+ } catch (/** @type any */ err) {
351
+ console.error('Error backing up db', err);
352
+ res.status(404).end();
347
353
  }
348
354
  },
349
355
  );
@@ -369,10 +375,10 @@ export async function setup(app, sbvrUtils, db) {
369
375
  }),
370
376
  );
371
377
  });
372
- res.sendStatus(200);
373
- } catch (err) {
374
- console.error('Error restoring db', err, err.stack);
375
- res.sendStatus(404);
378
+ res.status(200).end();
379
+ } catch (/** @type any */ err) {
380
+ console.error('Error restoring db', err);
381
+ res.status(404).end();
376
382
  }
377
383
  },
378
384
  );
@@ -398,7 +404,7 @@ export async function setup(app, sbvrUtils, db) {
398
404
  sbvrUtils.deleteModel('data'),
399
405
  ]);
400
406
  await isServerOnAir(false);
401
- res.sendStatus(200);
407
+ res.status(200).end();
402
408
  });
403
409
 
404
410
  await db.transaction(setupModels);