@balena/pinejs 15.0.0-build-15-x-4acccbc677d2f8b6c23ace10945cd167339fc8df-1 → 15.0.0-build-15-x-bf3a44dfe299de8966cf9bc201fa13fdf9d4a747-1
Sign up to get free protection for your applications and to get access to all the features.
- package/.pinejs-cache.json +1 -1
- package/.versionbot/CHANGELOG.yml +1383 -66
- package/CHANGELOG.md +507 -2
- package/out/config-loader/config-loader.js +16 -4
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/database-layer/db.d.ts +1 -0
- package/out/database-layer/db.js +3 -0
- package/out/database-layer/db.js.map +1 -1
- package/out/migrator/async.js +14 -8
- package/out/migrator/async.js.map +1 -1
- package/out/migrator/utils.d.ts +3 -4
- package/out/migrator/utils.js +11 -1
- package/out/migrator/utils.js.map +1 -1
- package/out/passport-pinejs/passport-pinejs.d.ts +1 -1
- package/out/passport-pinejs/passport-pinejs.js +3 -3
- package/out/passport-pinejs/passport-pinejs.js.map +1 -1
- package/out/pinejs-session-store/pinejs-session-store.d.ts +1 -1
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/sbvr-api/hooks.js +1 -1
- package/out/sbvr-api/hooks.js.map +1 -1
- package/out/sbvr-api/permissions.js +1 -1
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +3 -3
- package/out/sbvr-api/sbvr-utils.js +28 -5
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/sbvr-api/translations.js +26 -11
- package/out/sbvr-api/translations.js.map +1 -1
- package/out/sbvr-api/uri-parser.d.ts +1 -1
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/package.json +18 -16
- package/src/config-loader/config-loader.ts +24 -7
- package/src/database-layer/db.ts +3 -0
- package/src/migrator/async.ts +22 -12
- package/src/migrator/utils.ts +16 -5
- package/src/passport-pinejs/passport-pinejs.ts +4 -4
- package/src/sbvr-api/abstract-sql.ts +2 -1
- package/src/sbvr-api/hooks.ts +1 -1
- package/src/sbvr-api/permissions.ts +6 -4
- package/src/sbvr-api/sbvr-utils.ts +65 -7
- package/src/sbvr-api/translations.ts +43 -14
- package/src/sbvr-api/uri-parser.ts +3 -1
- package/tsconfig.dev.json +1 -0
package/src/migrator/async.ts
CHANGED
@@ -14,7 +14,6 @@ type InitialMigrationStatus = MigrationStatus &
|
|
14
14
|
import {
|
15
15
|
MigrationTuple,
|
16
16
|
AsyncMigrationFn,
|
17
|
-
setExecutedMigrations,
|
18
17
|
getExecutedMigrations,
|
19
18
|
migratorEnv,
|
20
19
|
lockMigrations,
|
@@ -64,19 +63,10 @@ const $run = async (
|
|
64
63
|
}> = [];
|
65
64
|
|
66
65
|
// get a transaction for setting up the async migrator
|
67
|
-
const modelIsNew = await sbvrUtils.isModelNew(setupTx, modelName);
|
68
|
-
const executedMigrations = await getExecutedMigrations(setupTx, modelName);
|
69
66
|
|
70
|
-
|
71
|
-
(sbvrUtils.api.migrations?.logger.info ?? console.info)(
|
72
|
-
'First time model has executed, skipping async migrations',
|
73
|
-
);
|
67
|
+
const executedMigrations = await getExecutedMigrations(setupTx, modelName);
|
74
68
|
|
75
|
-
|
76
|
-
...executedMigrations,
|
77
|
-
...Object.keys(migrations),
|
78
|
-
]);
|
79
|
-
}
|
69
|
+
// if the model is new, the sync migration parts (marked by finalize=true) are already marked in the sync migration runner.
|
80
70
|
|
81
71
|
/**
|
82
72
|
* preflight check if there are already migrations executed before starting the async scheduler
|
@@ -246,6 +236,26 @@ const $run = async (
|
|
246
236
|
// when it fails it would break the transaction for managing the migration status
|
247
237
|
const migratedRows = await sbvrUtils.db.transaction(
|
248
238
|
async (migrationTx) => {
|
239
|
+
// disable automatic close on the management transaction as the migration transaction consumes up to max autoClose time
|
240
|
+
// disable first here, to let if fail when it takes to long before coming here to actually migrate.
|
241
|
+
tx.disableAutomaticClose();
|
242
|
+
const rollbackMigrationTx = async () => {
|
243
|
+
// if the parent transaction fails for any reason, the actual running migration transaction has to be rolled back to stop parallel unsafe async migrations.
|
244
|
+
try {
|
245
|
+
if (!migrationTx.isClosed()) {
|
246
|
+
await migrationTx.rollback();
|
247
|
+
}
|
248
|
+
} catch (err) {
|
249
|
+
(
|
250
|
+
sbvrUtils.api.migrations?.logger.error ??
|
251
|
+
console.error
|
252
|
+
)(
|
253
|
+
`error rolling back pending async migration tx on mgmt tx end/rollback: ${key}: ${err}`,
|
254
|
+
);
|
255
|
+
}
|
256
|
+
};
|
257
|
+
tx.on('rollback', rollbackMigrationTx);
|
258
|
+
tx.on('end', rollbackMigrationTx);
|
249
259
|
return (
|
250
260
|
(await asyncRunnerMigratorFn?.(migrationTx)) ?? 0
|
251
261
|
);
|
package/src/migrator/utils.ts
CHANGED
@@ -19,11 +19,17 @@ export const migrations: Migrations = {
|
|
19
19
|
await tx.executeSql(`\
|
20
20
|
ALTER TABLE "migration"
|
21
21
|
MODIFY "executed migrations" JSON NOT NULL;`);
|
22
|
+
await tx.executeSql(`\
|
23
|
+
ALTER TABLE "migration status"
|
24
|
+
MODIFY "is backing off" JSON NOT NULL;`);
|
22
25
|
break;
|
23
26
|
case 'postgres':
|
24
27
|
await tx.executeSql(`\
|
25
28
|
ALTER TABLE "migration"
|
26
29
|
ALTER COLUMN "executed migrations" SET DATA TYPE JSONB USING b::JSONB;`);
|
30
|
+
await tx.executeSql(`\
|
31
|
+
ALTER TABLE "migration status"
|
32
|
+
ALTER COLUMN "is backing off" SET DATA TYPE JSONB USING b::JSONB;`);
|
27
33
|
break;
|
28
34
|
// No need to migrate for websql
|
29
35
|
}
|
@@ -53,8 +59,7 @@ export type AsyncMigrationFn = (
|
|
53
59
|
) => Resolvable<Result>;
|
54
60
|
|
55
61
|
type AddFn<T extends {}, x extends 'sync' | 'async'> = T & {
|
56
|
-
syncFn:
|
57
|
-
asyncFn: AsyncMigrationFn;
|
62
|
+
[key in `${x}Fn`]: key extends 'syncFn' ? MigrationFn : AsyncMigrationFn;
|
58
63
|
} & {
|
59
64
|
[key in `${x}Sql`]?: undefined;
|
60
65
|
};
|
@@ -79,13 +84,19 @@ export type AsyncMigration =
|
|
79
84
|
| AddFn<AddSql<BaseAsyncMigration, 'sync'>, 'async'>;
|
80
85
|
|
81
86
|
export function isAsyncMigration(
|
82
|
-
migration: AsyncMigration | RunnableMigrations,
|
87
|
+
migration: string | MigrationFn | AsyncMigration | RunnableMigrations,
|
83
88
|
): migration is AsyncMigration {
|
84
|
-
return (
|
89
|
+
return (
|
90
|
+
((typeof (migration as AsyncMigration).asyncFn === 'function' ||
|
91
|
+
typeof (migration as AsyncMigration).asyncSql === 'string') &&
|
92
|
+
(typeof (migration as AsyncMigration).syncFn === 'function' ||
|
93
|
+
typeof (migration as AsyncMigration).syncSql === 'string')) ||
|
94
|
+
(migration as AsyncMigration).type === MigrationCategories.async
|
95
|
+
);
|
85
96
|
}
|
86
97
|
|
87
98
|
export function isSyncMigration(
|
88
|
-
migration: string | MigrationFn | RunnableMigrations,
|
99
|
+
migration: string | MigrationFn | RunnableMigrations | AsyncMigration,
|
89
100
|
): migration is MigrationFn {
|
90
101
|
return typeof migration === 'function' || typeof migration === 'string';
|
91
102
|
}
|
@@ -10,7 +10,7 @@ import * as permissions from '../sbvr-api/permissions';
|
|
10
10
|
export let login: (
|
11
11
|
fn: (
|
12
12
|
err: any,
|
13
|
-
user: {} | undefined,
|
13
|
+
user: {} | null | false | undefined,
|
14
14
|
req: Express.Request,
|
15
15
|
res: Express.Response,
|
16
16
|
next: Express.NextFunction,
|
@@ -54,15 +54,15 @@ const setup: ConfigLoader.SetupFunction = async (app: Express.Application) => {
|
|
54
54
|
passport.use(new LocalStrategy(checkPassword));
|
55
55
|
|
56
56
|
login = (fn) => (req, res, next) =>
|
57
|
-
passport.authenticate('local', (err, user
|
58
|
-
if (err || user == null) {
|
57
|
+
passport.authenticate('local', ((err, user) => {
|
58
|
+
if (err || user == null || user === false) {
|
59
59
|
fn(err, user, req, res, next);
|
60
60
|
return;
|
61
61
|
}
|
62
62
|
req.login(user, (error) => {
|
63
63
|
fn(error, user, req, res, next);
|
64
64
|
});
|
65
|
-
})(req, res, next);
|
65
|
+
}) as Passport.AuthenticateCallback)(req, res, next);
|
66
66
|
|
67
67
|
logout = (req, _res, next) => {
|
68
68
|
req.logout((error) => {
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import * as _ from 'lodash';
|
2
2
|
|
3
3
|
import * as AbstractSQLCompiler from '@balena/abstract-sql-compiler';
|
4
|
+
import type { BindKey } from '@balena/odata-parser';
|
4
5
|
import {
|
5
6
|
ODataBinds,
|
6
7
|
odataNameToSqlName,
|
@@ -123,7 +124,7 @@ export const getAndCheckBindValues = async (
|
|
123
124
|
throw new Error('Invalid binding');
|
124
125
|
}
|
125
126
|
let dataType;
|
126
|
-
[dataType, value] = odataBinds[bindValue];
|
127
|
+
[dataType, value] = odataBinds[bindValue as BindKey];
|
127
128
|
field = { dataType };
|
128
129
|
} else {
|
129
130
|
throw new Error(`Unknown binding: ${binding}`);
|
package/src/sbvr-api/hooks.ts
CHANGED
@@ -442,7 +442,7 @@ export const runHooks = async <T extends keyof Hooks>(
|
|
442
442
|
await Promise.all(
|
443
443
|
(modelHooks as Array<Hook<HookFn>>).map(async (hook) => {
|
444
444
|
if (hook.readOnlyTx) {
|
445
|
-
modelReadOnlyArgs ??= getReadOnlyArgs(modelName,
|
445
|
+
modelReadOnlyArgs ??= getReadOnlyArgs(modelName, modelArgs);
|
446
446
|
await hook.run(modelReadOnlyArgs);
|
447
447
|
} else {
|
448
448
|
await hook.run(modelArgs);
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import type {
|
2
2
|
AbstractSqlModel,
|
3
3
|
AbstractSqlQuery,
|
4
|
-
AbstractSqlType,
|
5
4
|
AliasNode,
|
5
|
+
AnyTypeNodes,
|
6
6
|
Definition,
|
7
7
|
FieldNode,
|
8
8
|
ReferencedFieldNode,
|
@@ -657,7 +657,7 @@ const generateConstrainedAbstractSql = (
|
|
657
657
|
const select = abstractSqlQuery.find(
|
658
658
|
(v): v is SelectNode => v[0] === 'Select',
|
659
659
|
)!;
|
660
|
-
select[1] = select[1].map((selectField):
|
660
|
+
select[1] = select[1].map((selectField): AnyTypeNodes => {
|
661
661
|
if (selectField[0] === 'Alias') {
|
662
662
|
const sqlName = odataNameToSqlName((selectField as AliasNode<any>)[2]);
|
663
663
|
const maybeField = (
|
@@ -1045,7 +1045,9 @@ const getBoundConstrainedMemoizer = memoizeWeak(
|
|
1045
1045
|
permissionsLookup,
|
1046
1046
|
permissions,
|
1047
1047
|
vocabulary,
|
1048
|
-
sqlNameToODataName(
|
1048
|
+
sqlNameToODataName(
|
1049
|
+
permissionsTable.modifyName ?? permissionsTable.name,
|
1050
|
+
),
|
1049
1051
|
),
|
1050
1052
|
);
|
1051
1053
|
|
@@ -1603,7 +1605,7 @@ const getGuestPermissions = memoize(
|
|
1603
1605
|
|
1604
1606
|
const getReqPermissions = async (
|
1605
1607
|
req: PermissionReq,
|
1606
|
-
odataBinds: ODataBinds = [],
|
1608
|
+
odataBinds: ODataBinds = [] as any as ODataBinds,
|
1607
1609
|
) => {
|
1608
1610
|
const guestPermissions = await (async () => {
|
1609
1611
|
if (
|
@@ -23,6 +23,7 @@ import { version as AbstractSQLCompilerVersion } from '@balena/abstract-sql-comp
|
|
23
23
|
import * as LF2AbstractSQL from '@balena/lf-to-abstract-sql';
|
24
24
|
|
25
25
|
import {
|
26
|
+
ODataBinds,
|
26
27
|
odataNameToSqlName,
|
27
28
|
sqlNameToODataName,
|
28
29
|
SupportedMethod,
|
@@ -372,12 +373,60 @@ export const isModelNew = async (
|
|
372
373
|
return !cachedIsModelNew.has(modelName);
|
373
374
|
};
|
374
375
|
|
376
|
+
const bindsForAffectedIds = (
|
377
|
+
bindings: AbstractSQLCompiler.Binding[],
|
378
|
+
request?: Pick<
|
379
|
+
uriParser.ODataRequest,
|
380
|
+
| 'vocabulary'
|
381
|
+
| 'abstractSqlModel'
|
382
|
+
| 'method'
|
383
|
+
| 'resourceName'
|
384
|
+
| 'affectedIds'
|
385
|
+
>,
|
386
|
+
) => {
|
387
|
+
if (request?.affectedIds == null) {
|
388
|
+
return {};
|
389
|
+
}
|
390
|
+
|
391
|
+
const tableName =
|
392
|
+
getAbstractSqlModel(request).tables[resolveSynonym(request)].name;
|
393
|
+
|
394
|
+
// If we're deleting the affected IDs then we can't narrow our rule to
|
395
|
+
// those IDs that are now missing
|
396
|
+
const isDelete = request.method === 'DELETE';
|
397
|
+
|
398
|
+
const odataBinds: { [key: string]: any } = {};
|
399
|
+
for (const bind of bindings) {
|
400
|
+
if (
|
401
|
+
bind.length !== 2 ||
|
402
|
+
bind[0] !== 'Bind' ||
|
403
|
+
typeof bind[1] !== 'string'
|
404
|
+
) {
|
405
|
+
continue;
|
406
|
+
}
|
407
|
+
|
408
|
+
const bindName = bind[1];
|
409
|
+
if (!isDelete && bindName === tableName) {
|
410
|
+
odataBinds[bindName] = ['Text', `{${request.affectedIds}}`];
|
411
|
+
} else {
|
412
|
+
odataBinds[bindName] = ['Text', '{}'];
|
413
|
+
}
|
414
|
+
}
|
415
|
+
|
416
|
+
return odataBinds;
|
417
|
+
};
|
418
|
+
|
375
419
|
export const validateModel = async (
|
376
420
|
tx: Db.Tx,
|
377
421
|
modelName: string,
|
378
422
|
request?: Pick<
|
379
423
|
uriParser.ODataRequest,
|
380
|
-
|
424
|
+
| 'abstractSqlQuery'
|
425
|
+
| 'modifiedFields'
|
426
|
+
| 'method'
|
427
|
+
| 'vocabulary'
|
428
|
+
| 'resourceName'
|
429
|
+
| 'affectedIds'
|
381
430
|
>,
|
382
431
|
): Promise<void> => {
|
383
432
|
const { sql } = models[modelName];
|
@@ -394,7 +443,16 @@ export const validateModel = async (
|
|
394
443
|
const values = await getAndCheckBindValues(
|
395
444
|
{
|
396
445
|
vocabulary: modelName,
|
397
|
-
|
446
|
+
// TODO: `odataBinds` is of type `ODataBinds`, which is an
|
447
|
+
// array with extra arbitrary fields. `getAndCheckBindValues`
|
448
|
+
// accepts that and also a pure object form for this
|
449
|
+
// argument. Given how arrays have predefined symbols,
|
450
|
+
// `bindsForAffectedIds` cannot return an `ODataBinds`.
|
451
|
+
// Both `ODataBinds` and `getAndCheckBindValues` should be
|
452
|
+
// fixed to accept a pure object with both string and
|
453
|
+
// numerical keys, for named and positional binds
|
454
|
+
// respectively
|
455
|
+
odataBinds: bindsForAffectedIds(rule.bindings, request) as any,
|
398
456
|
values: {},
|
399
457
|
engine: db.engine,
|
400
458
|
},
|
@@ -832,7 +890,7 @@ export const runRule = (() => {
|
|
832
890
|
const values = await getAndCheckBindValues(
|
833
891
|
{
|
834
892
|
vocabulary: vocab,
|
835
|
-
odataBinds: [],
|
893
|
+
odataBinds: [] as any as ODataBinds,
|
836
894
|
values: {},
|
837
895
|
engine: db.engine,
|
838
896
|
},
|
@@ -1479,7 +1537,8 @@ const updateBinds = (
|
|
1479
1537
|
request: uriParser.ODataRequest,
|
1480
1538
|
) => {
|
1481
1539
|
if (request._defer) {
|
1482
|
-
|
1540
|
+
for (let i = 0; i < request.odataBinds.length; i++) {
|
1541
|
+
const [tag, id] = request.odataBinds[i];
|
1483
1542
|
if (tag === 'ContentReference') {
|
1484
1543
|
const ref = changeSetResults.get(id);
|
1485
1544
|
if (
|
@@ -1491,10 +1550,9 @@ const updateBinds = (
|
|
1491
1550
|
'Reference to a non existing resource in Changeset',
|
1492
1551
|
);
|
1493
1552
|
}
|
1494
|
-
|
1553
|
+
request.odataBinds[i] = uriParser.parseId(ref.body.id);
|
1495
1554
|
}
|
1496
|
-
|
1497
|
-
});
|
1555
|
+
}
|
1498
1556
|
}
|
1499
1557
|
return request;
|
1500
1558
|
};
|
@@ -24,47 +24,75 @@ export type AliasValidNodeType =
|
|
24
24
|
| UnknownTypeNodes
|
25
25
|
| NullNode;
|
26
26
|
const aliasFields = (
|
27
|
-
|
28
|
-
|
27
|
+
fromAbstractSqlModel: AbstractSqlModel,
|
28
|
+
toAbstractSqlModel: AbstractSqlModel,
|
29
|
+
fromResourceName: string,
|
30
|
+
toResource: string,
|
29
31
|
aliases: Dictionary<string | AliasValidNodeType>,
|
30
32
|
): SelectNode[1] => {
|
31
|
-
const
|
32
|
-
|
33
|
-
);
|
34
|
-
const nonexistentFields = _.difference(Object.keys(aliases),
|
33
|
+
const fromFieldNames = fromAbstractSqlModel.tables[
|
34
|
+
fromResourceName
|
35
|
+
].fields.map(({ fieldName }) => fieldName);
|
36
|
+
const nonexistentFields = _.difference(Object.keys(aliases), fromFieldNames);
|
35
37
|
if (nonexistentFields.length > 0) {
|
36
38
|
throw new Error(
|
37
39
|
`Tried to alias non-existent fields: '${nonexistentFields.join(', ')}'`,
|
38
40
|
);
|
39
41
|
}
|
40
|
-
|
42
|
+
const toFieldNames = toAbstractSqlModel.tables[toResource].fields.map(
|
43
|
+
({ fieldName }) => fieldName,
|
44
|
+
);
|
45
|
+
const checkToFieldExists = (fromFieldName: string, toFieldName: string) => {
|
46
|
+
if (!toFieldNames.includes(toFieldName)) {
|
47
|
+
throw new Error(
|
48
|
+
`Tried to alias '${fromFieldName}' to the non-existent target field: '${toFieldName}'`,
|
49
|
+
);
|
50
|
+
}
|
51
|
+
};
|
52
|
+
return fromFieldNames.map(
|
41
53
|
(fieldName): AliasNode<AliasValidNodeType> | ReferencedFieldNode => {
|
42
54
|
const alias = aliases[fieldName];
|
43
55
|
if (alias) {
|
44
56
|
if (typeof alias === 'string') {
|
45
|
-
|
57
|
+
checkToFieldExists(fieldName, alias);
|
58
|
+
return [
|
59
|
+
'Alias',
|
60
|
+
['ReferencedField', fromResourceName, alias],
|
61
|
+
fieldName,
|
62
|
+
];
|
46
63
|
}
|
47
64
|
return ['Alias', alias, fieldName];
|
48
65
|
}
|
49
|
-
|
66
|
+
checkToFieldExists(fieldName, fieldName);
|
67
|
+
return ['ReferencedField', fromResourceName, fieldName];
|
50
68
|
},
|
51
69
|
);
|
52
70
|
};
|
53
71
|
|
54
72
|
const aliasResource = (
|
55
|
-
|
56
|
-
|
73
|
+
fromAbstractSqlModel: AbstractSqlModel,
|
74
|
+
toAbstractSqlModel: AbstractSqlModel,
|
75
|
+
fromResourceName: string,
|
57
76
|
toResource: string,
|
58
77
|
aliases: Dictionary<string | AliasValidNodeType>,
|
59
78
|
): Definition => {
|
60
|
-
if (!
|
79
|
+
if (!toAbstractSqlModel.tables[toResource]) {
|
61
80
|
throw new Error(`Tried to alias to a non-existent resource: ${toResource}`);
|
62
81
|
}
|
63
82
|
return {
|
64
83
|
abstractSql: [
|
65
84
|
'SelectQuery',
|
66
|
-
[
|
67
|
-
|
85
|
+
[
|
86
|
+
'Select',
|
87
|
+
aliasFields(
|
88
|
+
fromAbstractSqlModel,
|
89
|
+
toAbstractSqlModel,
|
90
|
+
fromResourceName,
|
91
|
+
toResource,
|
92
|
+
aliases,
|
93
|
+
),
|
94
|
+
],
|
95
|
+
['From', ['Alias', ['Resource', toResource], fromResourceName]],
|
68
96
|
],
|
69
97
|
};
|
70
98
|
};
|
@@ -196,6 +224,7 @@ export const translateAbstractSqlModel = (
|
|
196
224
|
} else {
|
197
225
|
table.definition = aliasResource(
|
198
226
|
fromAbstractSqlModel,
|
227
|
+
toAbstractSqlModel,
|
199
228
|
key,
|
200
229
|
toResource,
|
201
230
|
definition,
|
@@ -124,7 +124,9 @@ export const memoizedParseOdata = (() => {
|
|
124
124
|
},
|
125
125
|
);
|
126
126
|
parsed.tree.options ??= {};
|
127
|
-
for (const key of Object.keys(
|
127
|
+
for (const key of Object.keys(
|
128
|
+
parsedParams.tree,
|
129
|
+
) as ODataParser.BindKey[]) {
|
128
130
|
parsed.tree.options[key] = parsedParams.tree[key];
|
129
131
|
parsed.binds[key] = parsedParams.binds[key];
|
130
132
|
}
|