@balena/pinejs 15.0.0-build-15-x-4681209f5dd8d896491fb5ed64aea47df511c14d-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 +1479 -103
- package/CHANGELOG.md +523 -2
- package/Dockerfile +1 -1
- package/out/bin/odata-compiler.js +4 -1
- package/out/bin/odata-compiler.js.map +1 -1
- package/out/config-loader/config-loader.d.ts +5 -2
- package/out/config-loader/config-loader.js +46 -18
- 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 +2 -1
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/sbvr-api/hooks.d.ts +3 -3
- package/out/sbvr-api/hooks.js +44 -29
- package/out/sbvr-api/hooks.js.map +1 -1
- package/out/sbvr-api/permissions.js +6 -4
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +11 -5
- package/out/sbvr-api/sbvr-utils.js +151 -61
- 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 +5 -2
- package/out/sbvr-api/uri-parser.js +2 -0
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/package.json +19 -17
- package/src/bin/odata-compiler.ts +4 -2
- package/src/config-loader/config-loader.ts +84 -29
- 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 +4 -2
- package/src/sbvr-api/hooks.ts +80 -33
- package/src/sbvr-api/permissions.ts +15 -7
- package/src/sbvr-api/sbvr-utils.ts +258 -84
- package/src/sbvr-api/translations.ts +248 -0
- package/src/sbvr-api/uri-parser.ts +9 -2
- package/tsconfig.dev.json +1 -0
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,
|
@@ -95,7 +96,8 @@ export const getAndCheckBindValues = async (
|
|
95
96
|
|
96
97
|
const sqlTableName = odataNameToSqlName(tableName);
|
97
98
|
const sqlFieldName = odataNameToSqlName(fieldName);
|
98
|
-
const
|
99
|
+
const table = sqlModelTables[sqlTableName];
|
100
|
+
const maybeField = (table.modifyFields ?? table.fields).find(
|
99
101
|
(f) => f.fieldName === sqlFieldName,
|
100
102
|
);
|
101
103
|
if (maybeField == null) {
|
@@ -122,7 +124,7 @@ export const getAndCheckBindValues = async (
|
|
122
124
|
throw new Error('Invalid binding');
|
123
125
|
}
|
124
126
|
let dataType;
|
125
|
-
[dataType, value] = odataBinds[bindValue];
|
127
|
+
[dataType, value] = odataBinds[bindValue as BindKey];
|
126
128
|
field = { dataType };
|
127
129
|
} else {
|
128
130
|
throw new Error(`Unknown binding: ${binding}`);
|
package/src/sbvr-api/hooks.ts
CHANGED
@@ -42,7 +42,9 @@ export interface Hooks {
|
|
42
42
|
PREPARSE?: (options: Omit<HookArgs, 'request' | 'api'>) => HookResponse;
|
43
43
|
POSTPARSE?: (options: HookArgs) => HookResponse;
|
44
44
|
PRERUN?: (options: HookArgs & { tx: Tx }) => HookResponse;
|
45
|
+
/** These are run in reverse translation order from newest to oldest */
|
45
46
|
POSTRUN?: (options: HookArgs & { tx: Tx; result: any }) => HookResponse;
|
47
|
+
/** These are run in reverse translation order from newest to oldest */
|
46
48
|
PRERESPOND?: (
|
47
49
|
options: HookArgs & {
|
48
50
|
tx: Tx;
|
@@ -53,6 +55,7 @@ export interface Hooks {
|
|
53
55
|
data?: any;
|
54
56
|
},
|
55
57
|
) => HookResponse;
|
58
|
+
/** These are run in reverse translation order from newest to oldest */
|
56
59
|
'POSTRUN-ERROR'?: (
|
57
60
|
options: HookArgs & { error: TypedError | any },
|
58
61
|
) => HookResponse;
|
@@ -126,13 +129,13 @@ class SideEffectHook<T extends HookFn> extends Hook<T> {
|
|
126
129
|
|
127
130
|
// The execution order of rollback actions is unspecified
|
128
131
|
export const rollbackRequestHooks = <T extends InstantiatedHooks>(
|
129
|
-
hooks: T | undefined,
|
132
|
+
hooksList: Array<[modelName: string, hooks: T]> | undefined,
|
130
133
|
): void => {
|
131
|
-
if (
|
134
|
+
if (hooksList == null) {
|
132
135
|
return;
|
133
136
|
}
|
134
|
-
const sideEffectHooks =
|
135
|
-
.flatMap((v): Array<Hook<HookFn>> => v)
|
137
|
+
const sideEffectHooks = hooksList
|
138
|
+
.flatMap(([, v]): Array<Hook<HookFn>> => Object.values(v).flat())
|
136
139
|
.filter(
|
137
140
|
(hook): hook is SideEffectHook<HookFn> => hook instanceof SideEffectHook,
|
138
141
|
);
|
@@ -177,21 +180,37 @@ const getResourceHooks = (vocabHooks: VocabHooks, resourceName?: string) => {
|
|
177
180
|
const getVocabHooks = (
|
178
181
|
methodHooks: MethodHooks,
|
179
182
|
vocabulary: string,
|
180
|
-
resourceName
|
183
|
+
resourceName: string | undefined,
|
184
|
+
includeAllVocab: boolean,
|
181
185
|
) => {
|
182
186
|
if (methodHooks == null) {
|
183
187
|
return {};
|
184
188
|
}
|
189
|
+
const vocabHooks = getResourceHooks(methodHooks[vocabulary], resourceName);
|
190
|
+
if (!includeAllVocab) {
|
191
|
+
// Do not include `vocabulary='all'` hooks, useful for translated vocabularies
|
192
|
+
return vocabHooks;
|
193
|
+
}
|
185
194
|
return mergeHooks(
|
186
|
-
|
195
|
+
vocabHooks,
|
187
196
|
getResourceHooks(methodHooks['all'], resourceName),
|
188
197
|
);
|
189
198
|
};
|
190
199
|
const getMethodHooks = memoize(
|
191
|
-
(
|
200
|
+
(
|
201
|
+
method: SupportedMethod,
|
202
|
+
vocabulary: string,
|
203
|
+
resourceName: string | undefined,
|
204
|
+
includeAllVocab: boolean,
|
205
|
+
) =>
|
192
206
|
mergeHooks(
|
193
|
-
getVocabHooks(
|
194
|
-
|
207
|
+
getVocabHooks(
|
208
|
+
apiHooks[method],
|
209
|
+
vocabulary,
|
210
|
+
resourceName,
|
211
|
+
includeAllVocab,
|
212
|
+
),
|
213
|
+
getVocabHooks(apiHooks['all'], vocabulary, resourceName, includeAllVocab),
|
195
214
|
),
|
196
215
|
{ primitive: true },
|
197
216
|
);
|
@@ -200,6 +219,7 @@ export const getHooks = (
|
|
200
219
|
OptionalField<ParsedODataRequest, 'resourceName'>,
|
201
220
|
'resourceName' | 'method' | 'vocabulary'
|
202
221
|
>,
|
222
|
+
includeAllVocab: boolean,
|
203
223
|
): InstantiatedHooks => {
|
204
224
|
let { resourceName } = request;
|
205
225
|
if (resourceName != null) {
|
@@ -208,10 +228,17 @@ export const getHooks = (
|
|
208
228
|
ParsedODataRequest,
|
209
229
|
'resourceName' | 'method' | 'vocabulary'
|
210
230
|
>,
|
211
|
-
)
|
231
|
+
)
|
232
|
+
// Remove version suffixes
|
233
|
+
.replace(/\$.*$/, '');
|
212
234
|
}
|
213
235
|
return instantiateHooks(
|
214
|
-
getMethodHooks(
|
236
|
+
getMethodHooks(
|
237
|
+
request.method,
|
238
|
+
request.vocabulary,
|
239
|
+
resourceName,
|
240
|
+
includeAllVocab,
|
241
|
+
),
|
215
242
|
);
|
216
243
|
};
|
217
244
|
getHooks.clear = () => getMethodHooks.clear();
|
@@ -343,12 +370,11 @@ export const addPureHook = (
|
|
343
370
|
});
|
344
371
|
};
|
345
372
|
|
346
|
-
const defineApi = (args: HookArgs) => {
|
347
|
-
const {
|
348
|
-
const { vocabulary } = request;
|
373
|
+
const defineApi = (modelName: string, args: HookArgs) => {
|
374
|
+
const { req, tx } = args;
|
349
375
|
Object.defineProperty(args, 'api', {
|
350
376
|
get: _.once(() =>
|
351
|
-
api[
|
377
|
+
api[modelName].clone({
|
352
378
|
passthrough: { req, tx },
|
353
379
|
}),
|
354
380
|
),
|
@@ -360,6 +386,7 @@ type RunHookArgs<T extends keyof Hooks> = Omit<
|
|
360
386
|
'api'
|
361
387
|
>;
|
362
388
|
const getReadOnlyArgs = <T extends keyof Hooks>(
|
389
|
+
modelName: string,
|
363
390
|
args: RunHookArgs<T>,
|
364
391
|
): RunHookArgs<T> => {
|
365
392
|
if (args.tx == null || args.tx.isReadOnly()) {
|
@@ -369,38 +396,58 @@ const getReadOnlyArgs = <T extends keyof Hooks>(
|
|
369
396
|
let readOnlyArgs: typeof args;
|
370
397
|
readOnlyArgs = { ...args, tx: args.tx.asReadOnly() };
|
371
398
|
if ((args as HookArgs).request != null) {
|
372
|
-
defineApi(readOnlyArgs as HookArgs);
|
399
|
+
defineApi(modelName, readOnlyArgs as HookArgs);
|
373
400
|
}
|
374
401
|
return readOnlyArgs;
|
375
402
|
};
|
376
403
|
|
377
404
|
export const runHooks = async <T extends keyof Hooks>(
|
378
405
|
hookName: T,
|
379
|
-
|
406
|
+
/**
|
407
|
+
* A list of modelName/hooks to run in order, which will be reversed for hooks after the "RUN" stage,
|
408
|
+
* ie POSTRUN/PRERESPOND/POSTRUN-ERROR
|
409
|
+
*/
|
410
|
+
hooksList: Array<[modelName: string, hooks: InstantiatedHooks]> | undefined,
|
380
411
|
args: RunHookArgs<T>,
|
381
412
|
) => {
|
382
413
|
if (hooksList == null) {
|
383
414
|
return;
|
384
415
|
}
|
385
|
-
const hooks = hooksList
|
386
|
-
|
416
|
+
const hooks = hooksList
|
417
|
+
.map(([modelName, $hooks]): [string, InstantiatedHooks[T] | undefined] => [
|
418
|
+
modelName,
|
419
|
+
$hooks[hookName],
|
420
|
+
])
|
421
|
+
.filter(
|
422
|
+
(v): v is [string, InstantiatedHooks[T]] =>
|
423
|
+
v[1] != null && v[1].length > 0,
|
424
|
+
);
|
425
|
+
if (hooks.length === 0) {
|
387
426
|
return;
|
388
427
|
}
|
389
|
-
|
390
|
-
|
391
|
-
|
428
|
+
if (['POSTRUN', 'PRERESPOND', 'POSTRUN-ERROR'].includes(hookName)) {
|
429
|
+
// Any hooks after we "run" the query are executed in reverse order from newest to oldest
|
430
|
+
// as they'll be translating the query results from "latest" backwards to the model that
|
431
|
+
// was actually requested
|
432
|
+
hooks.reverse();
|
392
433
|
}
|
393
434
|
|
394
|
-
|
435
|
+
for (const [modelName, modelHooks] of hooks) {
|
436
|
+
const modelArgs = { ...args };
|
437
|
+
let modelReadOnlyArgs: typeof modelArgs;
|
438
|
+
if ((args as HookArgs).request != null) {
|
439
|
+
defineApi(modelName, modelArgs as HookArgs);
|
440
|
+
}
|
395
441
|
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
442
|
+
await Promise.all(
|
443
|
+
(modelHooks as Array<Hook<HookFn>>).map(async (hook) => {
|
444
|
+
if (hook.readOnlyTx) {
|
445
|
+
modelReadOnlyArgs ??= getReadOnlyArgs(modelName, modelArgs);
|
446
|
+
await hook.run(modelReadOnlyArgs);
|
447
|
+
} else {
|
448
|
+
await hook.run(modelArgs);
|
449
|
+
}
|
450
|
+
}),
|
451
|
+
);
|
452
|
+
}
|
406
453
|
};
|
@@ -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,
|
@@ -140,7 +140,7 @@ const $parsePermissions = env.createCache(
|
|
140
140
|
},
|
141
141
|
);
|
142
142
|
|
143
|
-
const
|
143
|
+
const rewriteODataBinds = (
|
144
144
|
{ tree, extraBinds }: { tree: ODataQuery; extraBinds: ODataBinds },
|
145
145
|
odataBinds: ODataBinds,
|
146
146
|
): ODataQuery => {
|
@@ -163,7 +163,7 @@ const parsePermissions = (
|
|
163
163
|
odataBinds: ODataBinds,
|
164
164
|
): ODataQuery => {
|
165
165
|
const odata = $parsePermissions(filter);
|
166
|
-
return
|
166
|
+
return rewriteODataBinds(odata, odataBinds);
|
167
167
|
};
|
168
168
|
|
169
169
|
// Traverses all values in `check`, actions for the following data types:
|
@@ -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,9 +1045,12 @@ 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
|
);
|
1053
|
+
|
1051
1054
|
return permissionsTable;
|
1052
1055
|
},
|
1053
1056
|
},
|
@@ -1602,7 +1605,7 @@ const getGuestPermissions = memoize(
|
|
1602
1605
|
|
1603
1606
|
const getReqPermissions = async (
|
1604
1607
|
req: PermissionReq,
|
1605
|
-
odataBinds: ODataBinds = [],
|
1608
|
+
odataBinds: ODataBinds = [] as any as ODataBinds,
|
1606
1609
|
) => {
|
1607
1610
|
const guestPermissions = await (async () => {
|
1608
1611
|
if (
|
@@ -1636,7 +1639,8 @@ export const addPermissions = async (
|
|
1636
1639
|
req: PermissionReq,
|
1637
1640
|
request: ODataRequest & { permissionType?: PermissionCheck },
|
1638
1641
|
): Promise<void> => {
|
1639
|
-
const {
|
1642
|
+
const { resourceName, odataQuery, odataBinds } = request;
|
1643
|
+
const vocabulary = _.last(request.translateVersions)!;
|
1640
1644
|
let abstractSqlModel = sbvrUtils.getAbstractSqlModel(request);
|
1641
1645
|
|
1642
1646
|
let { permissionType } = request;
|
@@ -1759,6 +1763,10 @@ export const setup = () => {
|
|
1759
1763
|
0,
|
1760
1764
|
-'#canAccess'.length,
|
1761
1765
|
);
|
1766
|
+
request.originalResourceName = request.originalResourceName.slice(
|
1767
|
+
0,
|
1768
|
+
-'#canAccess'.length,
|
1769
|
+
);
|
1762
1770
|
const resourceName = sbvrUtils.resolveSynonym(request);
|
1763
1771
|
const resourceTable = abstractSqlModel.tables[resourceName];
|
1764
1772
|
if (resourceTable == null) {
|