@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
@@ -2,14 +2,14 @@
2
2
  import type * as Mysql from 'mysql';
3
3
  import type * as Pg from 'pg';
4
4
  import type * as PgConnectionString from 'pg-connection-string';
5
- import type { Resolvable } from '../sbvr-api/common-types';
5
+ import type { Dictionary, Resolvable } from '../sbvr-api/common-types';
6
6
 
7
7
  import { Engines } from '@balena/abstract-sql-compiler';
8
- import * as Bluebird from 'bluebird';
9
- import * as EventEmitter from 'eventemitter3';
8
+ import { EventEmitter } from 'eventemitter3';
10
9
  import * as _ from 'lodash';
11
10
  import { TypedError } from 'typed-error';
12
11
  import * as env from '../config-loader/env';
12
+ import { fromCallback, timeout } from '../sbvr-api/control-flow';
13
13
 
14
14
  export const metrics = new EventEmitter();
15
15
 
@@ -25,22 +25,18 @@ export interface Row {
25
25
  export interface Result {
26
26
  rows: Row[];
27
27
  rowsAffected: number;
28
- insertId?: number;
28
+ insertId?: number | undefined;
29
29
  }
30
30
 
31
31
  export type Sql = string;
32
32
  export type Bindings = any[];
33
33
 
34
34
  const isSqlError = (value: any): value is SQLError => {
35
- return (
36
- value != null &&
37
- value.constructor != null &&
38
- value.constructor.name === 'SQLError'
39
- );
35
+ return value?.constructor?.name === 'SQLError';
40
36
  };
41
37
 
42
38
  export class DatabaseError extends TypedError {
43
- public code: number | string;
39
+ public code?: number | string;
44
40
  constructor(message?: string | CodedError | SQLError) {
45
41
  if (isSqlError(message)) {
46
42
  // If this is a SQLError we have to handle it specially (since it's not actually an instance of Error)
@@ -104,9 +100,63 @@ export interface Database extends BaseDatabase {
104
100
  readTransaction: TransactionFn;
105
101
  }
106
102
 
107
- export const engines: {
108
- [engine: string]: (connectString: string | object) => Database;
109
- } = {};
103
+ interface EngineParams {
104
+ [engine: string]: (options: unknown) => Database;
105
+ }
106
+ export const engines = {} as EngineParams;
107
+
108
+ const types = {
109
+ integer: {
110
+ min: -2147483648,
111
+ max: 2147483647,
112
+ },
113
+ };
114
+
115
+ const validateTransactionLockParameter = (
116
+ value: number,
117
+ parameterName: string,
118
+ ) => {
119
+ if (
120
+ !Number.isInteger(value) ||
121
+ value < types.integer.min ||
122
+ types.integer.max < value
123
+ ) {
124
+ throw new TypeError(
125
+ `Invalid parameter '${parameterName}' provided for transaction lock`,
126
+ );
127
+ }
128
+ };
129
+
130
+ const transactionLockNamespaceMap: Dictionary<number> = {};
131
+
132
+ /**
133
+ *
134
+ * @param namespaceKey Any string representing a namespace
135
+ * @param namespaceId For application level locks positive values should be used
136
+ * as internal / low level locks are recommended to use negative values
137
+ */
138
+
139
+ export function registerTransactionLockNamespace(
140
+ namespaceKey: string,
141
+ namespaceId: number,
142
+ ) {
143
+ validateTransactionLockParameter(namespaceId, 'namespaceId');
144
+ if (transactionLockNamespaceMap[namespaceKey] != null) {
145
+ throw new Error(
146
+ `Error while registering transaction lock namespace '${namespaceKey}'. Namespace key is already registered.`,
147
+ );
148
+ }
149
+ const existingNamespaceEntry = Object.entries(
150
+ transactionLockNamespaceMap,
151
+ ).find(([, id]) => id === namespaceId);
152
+ if (existingNamespaceEntry != null) {
153
+ throw new Error(
154
+ `Error while registering transaction lock namespace '${namespaceKey}'. Transaction lock namespace id '${namespaceId}' already registered for namespace ${existingNamespaceEntry[0]}.`,
155
+ );
156
+ }
157
+
158
+ transactionLockNamespaceMap[namespaceKey] = namespaceId;
159
+ }
110
160
 
111
161
  const atomicExecuteSql: Database['executeSql'] = async function (
112
162
  sql,
@@ -121,9 +171,7 @@ const asyncTryFn = (fn: () => any) => {
121
171
  Promise.resolve().then(fn);
122
172
  };
123
173
 
124
- type RejectedFunctions = (
125
- message: string,
126
- ) => {
174
+ type RejectedFunctions = (message: string) => {
127
175
  executeSql: Tx['executeSql'];
128
176
  rollback: Tx['rollback'];
129
177
  };
@@ -215,7 +263,6 @@ class AutomaticClose {
215
263
  export abstract class Tx {
216
264
  private closed = false;
217
265
  protected automaticClose: AutomaticClose;
218
- public abstract readonly engine: Engines;
219
266
 
220
267
  constructor(
221
268
  protected readOnly: boolean,
@@ -255,7 +302,11 @@ export abstract class Tx {
255
302
  bindings: Bindings = [],
256
303
  ...args: any[]
257
304
  ): Promise<Result> {
258
- if (this.readOnly && !/^\s*SELECT\s(?:[^;]|;\s*SELECT\s)*$/.test(sql)) {
305
+ if (
306
+ env.db.checkReadOnlyQueries &&
307
+ this.readOnly &&
308
+ !/^\s*SELECT\s(?:[^;]|;\s*SELECT\s)*$/.test(sql)
309
+ ) {
259
310
  throw new ReadOnlyViolationError(
260
311
  `Attempted to run a non-SELECT statement in a read-only tx: ${sql}`,
261
312
  );
@@ -272,7 +323,7 @@ export abstract class Tx {
272
323
  const t0 = Date.now();
273
324
  try {
274
325
  return await this._executeSql(sql, bindings, ...args);
275
- } catch (err) {
326
+ } catch (err: any) {
276
327
  throw wrapDatabaseError(err);
277
328
  } finally {
278
329
  this.automaticClose.decrementPending();
@@ -321,6 +372,9 @@ export abstract class Tx {
321
372
  this.on = onEnd;
322
373
  this.clearListeners();
323
374
  }
375
+ public disableAutomaticClose(): void {
376
+ this.automaticClose.cancelPending();
377
+ }
324
378
 
325
379
  private listeners: {
326
380
  end: Array<() => void>;
@@ -346,6 +400,16 @@ export abstract class Tx {
346
400
  protected abstract _rollback(): Promise<void>;
347
401
  protected abstract _commit(): Promise<void>;
348
402
 
403
+ public async getTxLevelLock(
404
+ _namespaceKey: string,
405
+ _key: number,
406
+ _blocking: boolean = true,
407
+ ): Promise<boolean> {
408
+ throw new Error(
409
+ 'The getTxLevelLock method is not implemented for the current engine.',
410
+ );
411
+ }
412
+
349
413
  public abstract tableList(extraWhereClause?: string): Promise<Result>;
350
414
  public async dropTable(tableName: string, ifExists = true) {
351
415
  if (typeof tableName !== 'string') {
@@ -374,7 +438,7 @@ const createTransaction = (createFunc: CreateTransactionFn): TransactionFn => {
374
438
  let tx;
375
439
  try {
376
440
  tx = await createFunc(stackTraceErr);
377
- } catch (err) {
441
+ } catch (err: any) {
378
442
  throw wrapDatabaseError(err);
379
443
  }
380
444
  if (fn) {
@@ -382,7 +446,7 @@ const createTransaction = (createFunc: CreateTransactionFn): TransactionFn => {
382
446
  const result = await fn(tx);
383
447
  await tx.end();
384
448
  return result;
385
- } catch (err) {
449
+ } catch (err: any) {
386
450
  try {
387
451
  await tx.rollback();
388
452
  } catch {
@@ -403,38 +467,74 @@ try {
403
467
  } catch (e) {
404
468
  // Ignore errors
405
469
  }
470
+ interface EngineParams {
471
+ postgres: (
472
+ options:
473
+ | string
474
+ | Pg.PoolConfig
475
+ | { primary: Pg.PoolConfig; replica?: Pg.PoolConfig },
476
+ ) => Database;
477
+ }
406
478
  if (maybePg != null) {
407
479
  const pg = maybePg;
408
- engines.postgres = (connectString: string | object): Database => {
480
+ engines.postgres = (connectString) => {
409
481
  const PG_UNIQUE_VIOLATION = '23505';
410
482
  const PG_FOREIGN_KEY_VIOLATION = '23503';
411
483
  const PG_CHECK_CONSTRAINT_VIOLATION = '23514';
412
484
  const PG_EXCLUSION_CONSTRAINT_VIOLATION = '23P01';
413
485
 
414
- let config: Pg.PoolConfig;
415
- if (typeof connectString === 'string') {
416
- const pgConnectionString: typeof PgConnectionString = require('pg-connection-string');
417
- // We have to cast because of the use of null vs undefined
418
- config = pgConnectionString.parse(connectString) as Pg.PoolConfig;
419
- } else {
420
- config = connectString;
421
- }
422
- config.max = env.db.poolSize;
423
- config.idleTimeoutMillis = env.db.idleTimeoutMillis;
424
- config.statement_timeout = env.db.statementTimeout;
425
- config.query_timeout = env.db.queryTimeout;
426
- config.connectionTimeoutMillis = env.db.connectionTimeoutMillis;
427
- config.keepAlive = env.db.keepAlive;
428
- const pool = new pg.Pool(config);
429
486
  const { PG_SCHEMA } = process.env;
430
- if (PG_SCHEMA != null) {
431
- pool.on('connect', (client) => {
432
- client.query({ text: `SET search_path TO "${PG_SCHEMA}"` });
487
+ const initPool = (config: Pg.PoolConfig) => {
488
+ config.max ??= env.db.poolSize;
489
+ config.idleTimeoutMillis ??= env.db.idleTimeoutMillis;
490
+ config.statement_timeout ??= env.db.statementTimeout;
491
+ config.query_timeout ??= env.db.queryTimeout;
492
+ config.connectionTimeoutMillis ??= env.db.connectionTimeoutMillis;
493
+ config.keepAlive ??= env.db.keepAlive;
494
+ // @ts-expect-error maxLifetimeSeconds is valid for PgPool but isn't currently in the typings
495
+ config.maxLifetimeSeconds ??= env.db.maxLifetimeSeconds;
496
+ config.maxUses ??= env.db.maxUses;
497
+ const p = new pg.Pool(config);
498
+ if (PG_SCHEMA != null) {
499
+ p.on('connect', (client) => {
500
+ client.query({ text: `SET search_path TO "${PG_SCHEMA}"` });
501
+ });
502
+ }
503
+ p.on('connect', (client) => {
504
+ client.on('error', (err) => {
505
+ try {
506
+ console.error('Releasing client on error:', err);
507
+ client.release(err);
508
+ } catch (e) {
509
+ console.error('Error releasing client on error:', e);
510
+ }
511
+ });
433
512
  });
434
- pool.on('error', (err) => {
513
+ p.on('error', (err) => {
435
514
  console.error('Pool error:', err.message);
436
515
  });
516
+ return p;
517
+ };
518
+
519
+ let pool: Pg.Pool;
520
+ let replica: Pg.Pool;
521
+ if (typeof connectString === 'string') {
522
+ const pgConnectionString: typeof PgConnectionString = require('pg-connection-string');
523
+ // We have to cast because of the use of null vs undefined
524
+ const config = pgConnectionString.parse(connectString) as Pg.PoolConfig;
525
+ pool = initPool(config);
526
+ } else {
527
+ const config = connectString;
528
+ if ('primary' in config) {
529
+ pool = initPool(config.primary);
530
+ if (config.replica) {
531
+ replica = initPool(config.replica);
532
+ }
533
+ } else {
534
+ pool = initPool(config);
535
+ }
437
536
  }
537
+ replica ??= pool;
438
538
 
439
539
  const createResult = ({
440
540
  rowCount,
@@ -450,8 +550,6 @@ if (maybePg != null) {
450
550
  };
451
551
  };
452
552
  class PostgresTx extends Tx {
453
- engine = Engines.postgres;
454
-
455
553
  constructor(
456
554
  private db: Pg.PoolClient,
457
555
  readOnly: boolean,
@@ -479,7 +577,7 @@ if (maybePg != null) {
479
577
  text: sql,
480
578
  values: bindings,
481
579
  });
482
- } catch (err) {
580
+ } catch (err: any) {
483
581
  if (err.code === PG_UNIQUE_VIOLATION) {
484
582
  throw new UniqueConstraintError(err);
485
583
  }
@@ -504,20 +602,21 @@ if (maybePg != null) {
504
602
  const queryQueue = this.db.queryQueue as Pg.Query[];
505
603
  if (queryQueue.length > 0) {
506
604
  const err = new DatabaseError('Rolling back transaction');
507
- queryQueue.forEach((query) => {
605
+ for (const query of queryQueue) {
508
606
  process.nextTick(() => {
509
607
  // @ts-expect-error typings do not include this function
510
608
  query.handleError(err, this.db.connection);
511
609
  });
512
- });
610
+ }
513
611
  queryQueue.length = 0;
514
612
  }
515
- await Bluebird.resolve(this.$executeSql('ROLLBACK;')).timeout(
613
+ await timeout(
614
+ this.$executeSql('ROLLBACK;'),
516
615
  env.db.rollbackTimeout,
517
616
  'Rolling back transaction timed out',
518
617
  );
519
618
  this.db.release();
520
- } catch (err) {
619
+ } catch (err: any) {
521
620
  err = wrapDatabaseError(err);
522
621
  this.db.release(err);
523
622
  throw err;
@@ -528,12 +627,45 @@ if (maybePg != null) {
528
627
  try {
529
628
  await this.$executeSql('COMMIT;');
530
629
  this.db.release();
531
- } catch (err) {
630
+ } catch (err: any) {
532
631
  this.db.release(err);
533
632
  throw err;
534
633
  }
535
634
  }
536
635
 
636
+ public override async getTxLevelLock(
637
+ namespaceKey: string,
638
+ key: number,
639
+ blocking: boolean = true,
640
+ ) {
641
+ validateTransactionLockParameter(key, 'key');
642
+ const namespaceId = transactionLockNamespaceMap[namespaceKey];
643
+ if (namespaceId == null) {
644
+ throw new Error(
645
+ `Transaction lock namespace ${namespaceKey} not registered.`,
646
+ );
647
+ }
648
+ try {
649
+ if (blocking) {
650
+ await this.executeSql(`SELECT pg_advisory_xact_lock($1, $2);`, [
651
+ namespaceId,
652
+ key,
653
+ ]);
654
+ return true;
655
+ } else {
656
+ const { rows } = await this.executeSql(
657
+ `SELECT pg_try_advisory_xact_lock($1, $2);`,
658
+ [namespaceId, key],
659
+ );
660
+ return rows[0].pg_try_advisory_xact_lock === true;
661
+ }
662
+ } catch (err) {
663
+ throw new Error(
664
+ `getTxLevelLock error during getting lock from postgres db layer ${err}`,
665
+ );
666
+ }
667
+ }
668
+
537
669
  public async tableList(extraWhereClause: string = '') {
538
670
  if (extraWhereClause !== '') {
539
671
  extraWhereClause = 'WHERE ' + extraWhereClause;
@@ -558,7 +690,7 @@ if (maybePg != null) {
558
690
  return tx;
559
691
  }),
560
692
  readTransaction: createTransaction(async (stackTraceErr) => {
561
- const client = await pool.connect();
693
+ const client = await replica.connect();
562
694
  const tx = new PostgresTx(client, false, stackTraceErr);
563
695
  tx.executeSql('START TRANSACTION READ ONLY;');
564
696
  return tx.asReadOnly();
@@ -575,19 +707,23 @@ try {
575
707
  } catch (e) {
576
708
  // Ignore errors
577
709
  }
710
+ interface EngineParams {
711
+ mysql: (options: Mysql.PoolConfig) => Database;
712
+ }
578
713
  if (maybeMysql != null) {
579
714
  const mysql = maybeMysql;
580
- engines.mysql = (options: Mysql.PoolConfig): Database => {
715
+ engines.mysql = (options) => {
581
716
  const MYSQL_UNIQUE_VIOLATION = 'ER_DUP_ENTRY';
582
717
  const MYSQL_FOREIGN_KEY_VIOLATION = 'ER_ROW_IS_REFERENCED';
583
718
  const MYSQL_CHECK_CONSTRAINT_VIOLATION = 'ER_CHECK_CONSTRAINT_VIOLATED';
584
719
  const pool = mysql.createPool(options);
585
720
  pool.on('connection', (db) => {
586
- db.query("SET sql_mode='ANSI_QUOTES';");
587
- });
588
- const getConnectionAsync = Bluebird.promisify(pool.getConnection, {
589
- context: pool,
721
+ db.query("SET sql_mode='ANSI';");
590
722
  });
723
+ const getConnectionAsync = () =>
724
+ fromCallback<Mysql.PoolConnection>((callback) => {
725
+ pool.getConnection(callback);
726
+ });
591
727
 
592
728
  interface MysqlRowArray extends Array<Row> {
593
729
  affectedRows: number;
@@ -601,8 +737,6 @@ if (maybeMysql != null) {
601
737
  };
602
738
  };
603
739
  class MySqlTx extends Tx {
604
- engine = Engines.mysql;
605
-
606
740
  constructor(
607
741
  private db: Mysql.Connection,
608
742
  private close: CloseTransactionFn,
@@ -619,10 +753,10 @@ if (maybeMysql != null) {
619
753
  protected async _executeSql(sql: Sql, bindings: Bindings) {
620
754
  let result;
621
755
  try {
622
- result = await Bluebird.fromCallback<MysqlRowArray>((callback) => {
756
+ result = await fromCallback<MysqlRowArray>((callback) => {
623
757
  this.db.query(sql, bindings, callback);
624
758
  });
625
- } catch (err) {
759
+ } catch (err: any) {
626
760
  if (err.code === MYSQL_UNIQUE_VIOLATION) {
627
761
  // We know that the type is an IError for mysql, but typescript doesn't like the catch obj sugar
628
762
  throw new UniqueConstraintError(err as Mysql.MysqlError);
@@ -690,6 +824,9 @@ if (maybeMysql != null) {
690
824
  };
691
825
  }
692
826
 
827
+ interface EngineParams {
828
+ websql: (databaseName: string) => Database;
829
+ }
693
830
  if (typeof window !== 'undefined' && window.openDatabase != null) {
694
831
  interface WebSqlResult {
695
832
  insertId?: number;
@@ -705,7 +842,7 @@ if (typeof window !== 'undefined' && window.openDatabase != null) {
705
842
  SQLStatementCallback,
706
843
  SQLStatementErrorCallback,
707
844
  ];
708
- engines.websql = (databaseName: string): Database => {
845
+ engines.websql = (databaseName) => {
709
846
  const WEBSQL_CONSTRAINT_ERR = 6;
710
847
 
711
848
  const db = window.openDatabase(
@@ -736,8 +873,6 @@ if (typeof window !== 'undefined' && window.openDatabase != null) {
736
873
  };
737
874
 
738
875
  class WebSqlTx extends Tx {
739
- engine = Engines.websql;
740
-
741
876
  constructor(
742
877
  private tx: WebSqlWrapper,
743
878
  readOnly: boolean,
@@ -754,7 +889,7 @@ if (typeof window !== 'undefined' && window.openDatabase != null) {
754
889
  let result;
755
890
  try {
756
891
  result = await this.tx.executeSql(sql, bindings);
757
- } catch (err) {
892
+ } catch (err: any) {
758
893
  if (err.code === WEBSQL_CONSTRAINT_ERR) {
759
894
  throw new ConstraintError('Constraint failed.');
760
895
  }
@@ -878,7 +1013,13 @@ if (typeof window !== 'undefined' && window.openDatabase != null) {
878
1013
  };
879
1014
  }
880
1015
 
881
- export const connect = (databaseOptions: { engine: string; params: {} }) => {
1016
+ export type DatabaseOptions<T extends keyof EngineParams> = {
1017
+ engine: T;
1018
+ params: Parameters<EngineParams[T]>[0];
1019
+ };
1020
+ export const connect = <T extends keyof EngineParams>(
1021
+ databaseOptions: DatabaseOptions<T>,
1022
+ ) => {
882
1023
  if (engines[databaseOptions.engine] == null) {
883
1024
  throw new Error('Unsupported database engine: ' + databaseOptions.engine);
884
1025
  }
@@ -49,16 +49,15 @@ const app = (function () {
49
49
  match,
50
50
  paramName,
51
51
  // Flatten middleware list to handle arrays of middleware in the arg list.
52
- middleware: _.flattenDeep(middleware),
52
+ middleware: middleware.flat(Infinity),
53
53
  });
54
54
  };
55
55
  const process = async function (
56
56
  /** @type string */ method,
57
57
  /** @type string */ uri,
58
58
  /** @type {{[key: string]: any}} */ headers,
59
- /** @type any */ body,
59
+ /** @type any */ body = '',
60
60
  ) {
61
- body ??= '';
62
61
  if (!handlers[method]) {
63
62
  throw [404, null, null];
64
63
  }
@@ -93,22 +92,22 @@ const app = (function () {
93
92
  json(/** @type any */ obj) {
94
93
  // Stringify and parse to emulate passing over network.
95
94
  obj = JSON.parse(JSON.stringify(obj));
96
- if (this.statusCode >= 400) {
97
- reject([this.statusCode, obj, null]);
98
- } else {
99
- resolve([this.statusCode, obj, null]);
100
- }
95
+ this.end(obj);
101
96
  },
102
97
  send(/** @type any */ data) {
103
98
  data = _.cloneDeep(data);
99
+ this.end(data);
100
+ },
101
+ end(/** @type any */ data) {
104
102
  if (this.statusCode >= 400) {
105
103
  reject([this.statusCode, data, null]);
106
104
  } else {
107
105
  resolve([this.statusCode, data, null]);
108
106
  }
109
107
  },
110
- sendStatus(/** @type undefined | number */ statusCode) {
111
- statusCode ??= this.statusCode;
108
+ sendStatus(
109
+ /** @type undefined | number */ statusCode = this.statusCode,
110
+ ) {
112
111
  if (statusCode >= 400) {
113
112
  reject([statusCode, null, null]);
114
113
  } else {
@@ -163,7 +162,7 @@ const app = (function () {
163
162
  checkMethodHandlers();
164
163
  }
165
164
  } else {
166
- res.sendStatus(404);
165
+ res.status(404).end();
167
166
  }
168
167
  };
169
168
  checkMethodHandlers();
@@ -3,6 +3,7 @@ import { odataNameToSqlName } from '@balena/odata-to-abstract-sql';
3
3
  // @ts-ignore
4
4
  const transactionModel = require('./transaction.sbvr');
5
5
 
6
+ /** @type {import('../config-loader/config-loader').Config} */
6
7
  export let config = {
7
8
  models: [
8
9
  {
@@ -24,19 +25,26 @@ ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT N
24
25
  ALTER TABLE "transaction"
25
26
  ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;\
26
27
  `,
27
- '15.0.0-true-boolean': {
28
- mysql: `\
29
- ALTER TABLE "lock"
30
- MODIFY "is exclusive" BOOLEAN NOT NULL,
31
- MODIFY "is under lock" BOOLEAN NOT NULL;\
32
- `,
33
- postgres: `\
34
- ALTER TABLE "lock"
35
- ALTER COLUMN "is exclusive" SET DATA TYPE BOOLEAN USING b::BOOLEAN,
36
- ALTER COLUMN "is under lock" SET DATA TYPE BOOLEAN USING b::BOOLEAN;\
37
- `,
38
- // No need to migrate for websql
39
- websql: '',
28
+ '15.0.0-data-types': async (tx, sbvrUtils) => {
29
+ switch (sbvrUtils.db.engine) {
30
+ case 'mysql':
31
+ await tx.executeSql(`\
32
+ ALTER TABLE "lock"
33
+ MODIFY "is exclusive" BOOLEAN NOT NULL,
34
+ MODIFY "is under lock" BOOLEAN NOT NULL;`);
35
+ break;
36
+ case 'postgres':
37
+ await tx.executeSql(`\
38
+ ALTER TABLE "lock"
39
+ ALTER COLUMN "is exclusive" DROP DEFAULT,
40
+ ALTER COLUMN "is exclusive" SET DATA TYPE BOOLEAN USING "is exclusive"::BOOLEAN,
41
+ ALTER COLUMN "is exclusive" SET DEFAULT FALSE,
42
+ ALTER COLUMN "is under lock" DROP DEFAULT,
43
+ ALTER COLUMN "is under lock" SET DATA TYPE BOOLEAN USING "is under lock"::BOOLEAN,
44
+ ALTER COLUMN "is under lock" SET DEFAULT FALSE;`);
45
+ break;
46
+ // No need to migrate for websql
47
+ }
40
48
  },
41
49
  },
42
50
  },
@@ -68,8 +76,8 @@ SELECT NOT EXISTS(
68
76
  ) AS result;`,
69
77
  [request.resourceName, id],
70
78
  );
71
- } catch (err) {
72
- logger.error('Unable to check resource locks', err, err.stack);
79
+ } catch (/** @type any */ err) {
80
+ logger.error('Unable to check resource locks', err);
73
81
  throw new Error('Unable to check resource locks');
74
82
  }
75
83
  if ([false, 0, '0'].includes(result.rows[0].result)) {
@@ -151,7 +159,7 @@ WHERE "conditional resource"."transaction" = ?;\
151
159
  [transactionID],
152
160
  );
153
161
 
154
- conditionalResources.rows.forEach((conditionalResource) => {
162
+ for (const conditionalResource of conditionalResources.rows) {
155
163
  const { placeholder } = conditionalResource;
156
164
  if (placeholder != null && placeholder.length > 0) {
157
165
  /** @type {Function} */
@@ -165,7 +173,7 @@ WHERE "conditional resource"."transaction" = ?;\
165
173
  // @ts-ignore
166
174
  placeholders[placeholder] = { promise, resolve, reject };
167
175
  }
168
- });
176
+ }
169
177
 
170
178
  // get conditional resources (if exist)
171
179
  await Promise.all(
@@ -263,14 +271,14 @@ WHERE "conditional resource"."transaction" = ?;\
263
271
  app.post('/transaction/execute', async (req, res) => {
264
272
  const id = Number(req.body.id);
265
273
  if (Number.isNaN(id)) {
266
- res.sendStatus(404);
274
+ res.status(404).end();
267
275
  } else {
268
276
  try {
269
277
  await endTransaction(id);
270
278
 
271
- res.sendStatus(200);
272
- } catch (err) {
273
- console.error('Error ending transaction', err, err.stack);
279
+ res.status(200).end();
280
+ } catch (/** @type any */ err) {
281
+ console.error('Error ending transaction', err);
274
282
  res.status(404).json(err);
275
283
  }
276
284
  }