@balena/pinejs 15.0.0-true-boolean-911aca4062d3132ad3c34712014739b6849fa13a → 15.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.dockerignore +4 -0
- package/.github/workflows/flowzone.yml +21 -0
- package/.husky/pre-commit +4 -0
- package/.pinejs-cache.json +1 -0
- package/.resinci.yml +1 -0
- package/.versionbot/CHANGELOG.yml +9678 -2001
- package/CHANGELOG.md +2975 -2
- package/Dockerfile +14 -0
- package/Gruntfile.ts +3 -6
- package/README.md +10 -1
- package/VERSION +1 -0
- package/build/browser.ts +1 -1
- package/build/config.ts +0 -1
- package/docker-compose.npm-test.yml +11 -0
- package/docs/AdvancedUsage.md +77 -63
- package/docs/GettingStarted.md +90 -41
- package/docs/Migrations.md +102 -1
- package/docs/ProjectConfig.md +12 -21
- package/docs/Testing.md +7 -0
- package/out/bin/abstract-sql-compiler.js +17 -17
- package/out/bin/abstract-sql-compiler.js.map +1 -1
- package/out/bin/odata-compiler.js +23 -20
- package/out/bin/odata-compiler.js.map +1 -1
- package/out/bin/sbvr-compiler.js +22 -22
- package/out/bin/sbvr-compiler.js.map +1 -1
- package/out/bin/utils.d.ts +2 -2
- package/out/bin/utils.js +3 -3
- package/out/bin/utils.js.map +1 -1
- package/out/config-loader/config-loader.d.ts +9 -8
- package/out/config-loader/config-loader.js +135 -78
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/config-loader/env.d.ts +41 -16
- package/out/config-loader/env.js +46 -2
- package/out/config-loader/env.js.map +1 -1
- package/out/data-server/sbvr-server.d.ts +2 -19
- package/out/data-server/sbvr-server.js +44 -38
- package/out/data-server/sbvr-server.js.map +1 -1
- package/out/database-layer/db.d.ts +32 -14
- package/out/database-layer/db.js +120 -41
- package/out/database-layer/db.js.map +1 -1
- package/out/express-emulator/express.js +10 -11
- package/out/express-emulator/express.js.map +1 -1
- package/out/http-transactions/transactions.d.ts +2 -18
- package/out/http-transactions/transactions.js +29 -21
- package/out/http-transactions/transactions.js.map +1 -1
- package/out/migrator/async.d.ts +7 -0
- package/out/migrator/async.js +168 -0
- package/out/migrator/async.js.map +1 -0
- package/out/migrator/migrations.sbvr +43 -0
- package/out/migrator/sync.d.ts +9 -0
- package/out/migrator/sync.js +106 -0
- package/out/migrator/sync.js.map +1 -0
- package/out/migrator/utils.d.ts +78 -0
- package/out/migrator/utils.js +283 -0
- package/out/migrator/utils.js.map +1 -0
- package/out/odata-metadata/odata-metadata-generator.js +10 -13
- package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
- package/out/passport-pinejs/passport-pinejs.d.ts +1 -1
- package/out/passport-pinejs/passport-pinejs.js +8 -7
- package/out/passport-pinejs/passport-pinejs.js.map +1 -1
- package/out/pinejs-session-store/pinejs-session-store.d.ts +1 -1
- package/out/pinejs-session-store/pinejs-session-store.js +20 -6
- package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
- package/out/sbvr-api/abstract-sql.d.ts +3 -2
- package/out/sbvr-api/abstract-sql.js +9 -9
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/sbvr-api/cached-compile.js +1 -1
- package/out/sbvr-api/cached-compile.js.map +1 -1
- package/out/sbvr-api/common-types.d.ts +6 -5
- package/out/sbvr-api/control-flow.d.ts +8 -1
- package/out/sbvr-api/control-flow.js +36 -9
- package/out/sbvr-api/control-flow.js.map +1 -1
- package/out/sbvr-api/errors.d.ts +47 -40
- package/out/sbvr-api/errors.js +78 -77
- package/out/sbvr-api/errors.js.map +1 -1
- package/out/sbvr-api/express-extension.d.ts +4 -0
- package/out/sbvr-api/hooks.d.ts +16 -15
- package/out/sbvr-api/hooks.js +74 -48
- package/out/sbvr-api/hooks.js.map +1 -1
- package/out/sbvr-api/odata-response.d.ts +2 -2
- package/out/sbvr-api/odata-response.js +28 -30
- package/out/sbvr-api/odata-response.js.map +1 -1
- package/out/sbvr-api/permissions.d.ts +17 -16
- package/out/sbvr-api/permissions.js +369 -304
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +33 -15
- package/out/sbvr-api/sbvr-utils.js +397 -235
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/sbvr-api/translations.d.ts +6 -0
- package/out/sbvr-api/translations.js +150 -0
- package/out/sbvr-api/translations.js.map +1 -0
- package/out/sbvr-api/uri-parser.d.ts +23 -17
- package/out/sbvr-api/uri-parser.js +33 -27
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/out/sbvr-api/user.sbvr +2 -0
- package/out/server-glue/module.d.ts +6 -6
- package/out/server-glue/module.js +4 -2
- package/out/server-glue/module.js.map +1 -1
- package/out/server-glue/server.js +5 -5
- package/out/server-glue/server.js.map +1 -1
- package/package.json +89 -73
- package/pinejs.png +0 -0
- package/repo.yml +9 -9
- package/src/bin/abstract-sql-compiler.ts +5 -7
- package/src/bin/odata-compiler.ts +11 -13
- package/src/bin/sbvr-compiler.ts +11 -17
- package/src/bin/utils.ts +3 -5
- package/src/config-loader/config-loader.ts +167 -53
- package/src/config-loader/env.ts +106 -6
- package/src/data-server/sbvr-server.js +44 -38
- package/src/database-layer/db.ts +205 -64
- package/src/express-emulator/express.js +10 -11
- package/src/http-transactions/transactions.js +29 -21
- package/src/migrator/async.ts +323 -0
- package/src/migrator/migrations.sbvr +43 -0
- package/src/migrator/sync.ts +152 -0
- package/src/migrator/utils.ts +458 -0
- package/src/odata-metadata/odata-metadata-generator.ts +12 -15
- package/src/passport-pinejs/passport-pinejs.ts +9 -7
- package/src/pinejs-session-store/pinejs-session-store.ts +15 -1
- package/src/sbvr-api/abstract-sql.ts +17 -14
- package/src/sbvr-api/common-types.ts +2 -1
- package/src/sbvr-api/control-flow.ts +45 -11
- package/src/sbvr-api/errors.ts +82 -77
- package/src/sbvr-api/express-extension.ts +6 -1
- package/src/sbvr-api/hooks.ts +123 -50
- package/src/sbvr-api/odata-response.ts +23 -28
- package/src/sbvr-api/permissions.ts +548 -415
- package/src/sbvr-api/sbvr-utils.ts +581 -259
- package/src/sbvr-api/translations.ts +248 -0
- package/src/sbvr-api/uri-parser.ts +63 -49
- package/src/sbvr-api/user.sbvr +2 -0
- package/src/server-glue/module.ts +16 -10
- package/src/server-glue/server.ts +5 -5
- package/tsconfig.dev.json +1 -0
- package/tsconfig.json +1 -2
- package/typings/lf-to-abstract-sql.d.ts +6 -9
- package/typings/memoizee.d.ts +1 -1
- package/.github/CODEOWNERS +0 -1
- package/circle.yml +0 -37
- package/docs/todo.txt +0 -22
- package/out/migrator/migrator.d.ts +0 -20
- package/out/migrator/migrator.js +0 -188
- package/out/migrator/migrator.js.map +0 -1
- package/src/migrator/migrator.ts +0 -286
package/src/database-layer/db.ts
CHANGED
@@ -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
|
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
|
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
|
-
|
108
|
-
[engine: string]: (
|
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 (
|
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
|
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
|
-
|
431
|
-
|
432
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
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='
|
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
|
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
|
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
|
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:
|
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
|
-
|
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(
|
111
|
-
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.
|
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-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
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
|
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.
|
274
|
+
res.status(404).end();
|
267
275
|
} else {
|
268
276
|
try {
|
269
277
|
await endTransaction(id);
|
270
278
|
|
271
|
-
res.
|
272
|
-
} catch (err) {
|
273
|
-
console.error('Error ending transaction', err
|
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
|
}
|