@balena/pinejs 15.0.0-delete-state-default-user-permissions-981931563dc47b2a8b873bc787d7dacfcc6c52e3 → 15.0.0-deprecate-node12-8a99d72ae66d7708293afc56c5d7eb19b39081cd

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. package/.resinci.yml +0 -1
  2. package/.versionbot/CHANGELOG.yml +226 -8
  3. package/CHANGELOG.md +85 -1
  4. package/out/bin/utils.js +1 -1
  5. package/out/bin/utils.js.map +1 -1
  6. package/out/config-loader/config-loader.d.ts +3 -5
  7. package/out/config-loader/config-loader.js +35 -31
  8. package/out/config-loader/config-loader.js.map +1 -1
  9. package/out/data-server/sbvr-server.js +8 -8
  10. package/out/data-server/sbvr-server.js.map +1 -1
  11. package/out/database-layer/db.d.ts +1 -1
  12. package/out/database-layer/db.js +3 -3
  13. package/out/database-layer/db.js.map +1 -1
  14. package/out/express-emulator/express.js +1 -1
  15. package/out/express-emulator/express.js.map +1 -1
  16. package/out/http-transactions/transactions.js +4 -4
  17. package/out/http-transactions/transactions.js.map +1 -1
  18. package/out/migrator/sync.d.ts +9 -0
  19. package/out/migrator/sync.js +121 -0
  20. package/out/migrator/sync.js.map +1 -0
  21. package/out/migrator/utils.d.ts +28 -0
  22. package/out/migrator/utils.js +104 -0
  23. package/out/migrator/utils.js.map +1 -0
  24. package/out/odata-metadata/odata-metadata-generator.js +6 -9
  25. package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
  26. package/out/passport-pinejs/passport-pinejs.js +4 -3
  27. package/out/passport-pinejs/passport-pinejs.js.map +1 -1
  28. package/out/pinejs-session-store/pinejs-session-store.js +1 -1
  29. package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
  30. package/out/sbvr-api/abstract-sql.d.ts +1 -1
  31. package/out/sbvr-api/abstract-sql.js.map +1 -1
  32. package/out/sbvr-api/control-flow.js.map +1 -1
  33. package/out/sbvr-api/hooks.d.ts +6 -3
  34. package/out/sbvr-api/hooks.js +3 -3
  35. package/out/sbvr-api/hooks.js.map +1 -1
  36. package/out/sbvr-api/odata-response.js +5 -5
  37. package/out/sbvr-api/odata-response.js.map +1 -1
  38. package/out/sbvr-api/permissions.js +25 -20
  39. package/out/sbvr-api/permissions.js.map +1 -1
  40. package/out/sbvr-api/sbvr-utils.d.ts +4 -3
  41. package/out/sbvr-api/sbvr-utils.js +71 -48
  42. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  43. package/out/sbvr-api/uri-parser.d.ts +13 -11
  44. package/out/sbvr-api/uri-parser.js +4 -4
  45. package/out/sbvr-api/uri-parser.js.map +1 -1
  46. package/out/server-glue/module.d.ts +2 -2
  47. package/out/server-glue/module.js +2 -1
  48. package/out/server-glue/module.js.map +1 -1
  49. package/package.json +19 -19
  50. package/src/bin/utils.ts +1 -1
  51. package/src/config-loader/config-loader.ts +69 -44
  52. package/src/data-server/sbvr-server.js +8 -8
  53. package/src/database-layer/db.ts +11 -11
  54. package/src/express-emulator/express.js +1 -1
  55. package/src/http-transactions/transactions.js +4 -4
  56. package/src/migrator/sync.ts +169 -0
  57. package/src/migrator/utils.ts +154 -0
  58. package/src/odata-metadata/odata-metadata-generator.ts +8 -11
  59. package/src/passport-pinejs/passport-pinejs.ts +3 -2
  60. package/src/sbvr-api/abstract-sql.ts +6 -3
  61. package/src/sbvr-api/control-flow.ts +2 -2
  62. package/src/sbvr-api/hooks.ts +18 -8
  63. package/src/sbvr-api/odata-response.ts +4 -4
  64. package/src/sbvr-api/permissions.ts +42 -36
  65. package/src/sbvr-api/sbvr-utils.ts +121 -58
  66. package/src/sbvr-api/uri-parser.ts +29 -21
  67. package/src/server-glue/module.ts +4 -3
  68. package/tsconfig.json +1 -3
  69. package/typings/lf-to-abstract-sql.d.ts +6 -9
  70. package/out/migrator/migrator.d.ts +0 -17
  71. package/out/migrator/migrator.js +0 -185
  72. package/out/migrator/migrator.js.map +0 -1
  73. package/src/migrator/migrator.ts +0 -278
@@ -0,0 +1,169 @@
1
+ import {
2
+ modelText,
3
+ MigrationTuple,
4
+ MigrationError,
5
+ defaultMigrationCategory,
6
+ setExecutedMigrations,
7
+ getExecutedMigrations,
8
+ lockMigrations,
9
+ RunnableMigrations,
10
+ } from './utils';
11
+ import type { Tx } from '../database-layer/db';
12
+ import type { Config, Model } from '../config-loader/config-loader';
13
+
14
+ import * as _ from 'lodash';
15
+ import * as sbvrUtils from '../sbvr-api/sbvr-utils';
16
+
17
+ type ApiRootModel = Model & { apiRoot: string };
18
+
19
+ export const postRun = async (tx: Tx, model: ApiRootModel): Promise<void> => {
20
+ const { initSql } = model;
21
+ if (initSql == null) {
22
+ return;
23
+ }
24
+
25
+ const modelName = model.apiRoot;
26
+ const modelIsNew = await sbvrUtils.isModelNew(tx, modelName);
27
+ if (modelIsNew) {
28
+ (sbvrUtils.api.migrations?.logger.info ?? console.info)(
29
+ `First time executing '${modelName}', running init script`,
30
+ );
31
+
32
+ await lockMigrations(tx, modelName, async () => {
33
+ try {
34
+ await tx.executeSql(initSql);
35
+ } catch (err: any) {
36
+ (sbvrUtils.api.migrations?.logger.error ?? console.error)(
37
+ `initSql execution error ${err} `,
38
+ );
39
+ throw new MigrationError(err);
40
+ }
41
+ });
42
+ }
43
+ };
44
+
45
+ export const run = async (tx: Tx, model: ApiRootModel): Promise<void> => {
46
+ const { migrations } = model;
47
+ if (migrations == null || _.isEmpty(migrations)) {
48
+ return;
49
+ }
50
+ const defaultMigrations = migrations[defaultMigrationCategory];
51
+ const runMigrations: RunnableMigrations =
52
+ defaultMigrationCategory in migrations
53
+ ? typeof defaultMigrations === 'object'
54
+ ? defaultMigrations
55
+ : {}
56
+ : migrations;
57
+
58
+ return $run(tx, model, runMigrations);
59
+ };
60
+
61
+ const $run = async (
62
+ tx: Tx,
63
+ model: ApiRootModel,
64
+ migrations: RunnableMigrations,
65
+ ): Promise<void> => {
66
+ const modelName = model.apiRoot;
67
+
68
+ // migrations only run if the model has been executed before,
69
+ // to make changes that can't be automatically applied
70
+ const modelIsNew = await sbvrUtils.isModelNew(tx, modelName);
71
+ if (modelIsNew) {
72
+ (sbvrUtils.api.migrations?.logger.info ?? console.info)(
73
+ `First time model '${modelName}' has executed, skipping migrations`,
74
+ );
75
+
76
+ return await setExecutedMigrations(tx, modelName, Object.keys(migrations));
77
+ }
78
+ await lockMigrations(tx, modelName, async () => {
79
+ try {
80
+ const executedMigrations = await getExecutedMigrations(tx, modelName);
81
+ const pendingMigrations = filterAndSortPendingMigrations(
82
+ migrations,
83
+ executedMigrations,
84
+ );
85
+ if (pendingMigrations.length === 0) {
86
+ return;
87
+ }
88
+
89
+ const newlyExecutedMigrations = await executeMigrations(
90
+ tx,
91
+ pendingMigrations,
92
+ );
93
+ await setExecutedMigrations(tx, modelName, [
94
+ ...executedMigrations,
95
+ ...newlyExecutedMigrations,
96
+ ]);
97
+ } catch (err: any) {
98
+ (sbvrUtils.api.migrations?.logger.error ?? console.error)(
99
+ `Failed to executed synchronous migrations from api root model ${err}`,
100
+ );
101
+ throw new MigrationError(err);
102
+ }
103
+ });
104
+ };
105
+
106
+ // turns {"key1": migration, "key3": migration, "key2": migration}
107
+ // into [["key1", migration], ["key2", migration], ["key3", migration]]
108
+ const filterAndSortPendingMigrations = (
109
+ migrations: NonNullable<RunnableMigrations>,
110
+ executedMigrations: string[],
111
+ ): MigrationTuple[] =>
112
+ (_(migrations).omit(executedMigrations) as _.Object<typeof migrations>)
113
+ .toPairs()
114
+ .sortBy(([migrationKey]) => migrationKey)
115
+ .value();
116
+
117
+ const executeMigrations = async (
118
+ tx: Tx,
119
+ migrations: MigrationTuple[] = [],
120
+ ): Promise<string[]> => {
121
+ try {
122
+ for (const migration of migrations) {
123
+ await executeMigration(tx, migration);
124
+ }
125
+ } catch (err: any) {
126
+ (sbvrUtils.api.migrations?.logger.error ?? console.error)(
127
+ 'Error while executing migrations, rolled back',
128
+ );
129
+ throw new MigrationError(err);
130
+ }
131
+ return migrations.map(([migrationKey]) => migrationKey); // return migration keys
132
+ };
133
+
134
+ const executeMigration = async (
135
+ tx: Tx,
136
+ [key, migration]: MigrationTuple,
137
+ ): Promise<void> => {
138
+ (sbvrUtils.api.migrations?.logger.info ?? console.info)(
139
+ `Running migration ${JSON.stringify(key)}`,
140
+ );
141
+
142
+ if (typeof migration === 'function') {
143
+ await migration(tx, sbvrUtils);
144
+ } else if (typeof migration === 'string') {
145
+ await tx.executeSql(migration);
146
+ } else {
147
+ throw new MigrationError(`Invalid migration type: ${typeof migration}`);
148
+ }
149
+ };
150
+
151
+ export const config: Config = {
152
+ models: [
153
+ {
154
+ modelName: 'migrations',
155
+ apiRoot: 'migrations',
156
+ modelText,
157
+ migrations: {
158
+ '11.0.0-modified-at': `
159
+ ALTER TABLE "migration"
160
+ ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
161
+ `,
162
+ '11.0.1-modified-at': `
163
+ ALTER TABLE "migration lock"
164
+ ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
165
+ `,
166
+ },
167
+ },
168
+ ],
169
+ };
@@ -0,0 +1,154 @@
1
+ import type { Tx } from '../database-layer/db';
2
+ import type { Resolvable } from '../sbvr-api/common-types';
3
+
4
+ import { Engines } from '@balena/abstract-sql-compiler';
5
+ import * as _ from 'lodash';
6
+ import { TypedError } from 'typed-error';
7
+ import { migrator as migratorEnv } from '../config-loader/env';
8
+ export { migrator as migratorEnv } from '../config-loader/env';
9
+ import { delay } from '../sbvr-api/control-flow';
10
+
11
+ // tslint:disable-next-line:no-var-requires
12
+ export const modelText = require('./migrations.sbvr');
13
+
14
+ import * as sbvrUtils from '../sbvr-api/sbvr-utils';
15
+ export enum MigrationCategories {
16
+ 'sync' = 'sync',
17
+ }
18
+ export const defaultMigrationCategory = MigrationCategories.sync;
19
+ export type CategorizedMigrations = {
20
+ [key in MigrationCategories]: RunnableMigrations;
21
+ };
22
+
23
+ type SbvrUtils = typeof sbvrUtils;
24
+ export type MigrationTuple = [string, Migration];
25
+ export type MigrationFn = (tx: Tx, sbvrUtils: SbvrUtils) => Resolvable<void>;
26
+ export type Migration = string | MigrationFn;
27
+ export type RunnableMigrations = { [key: string]: Migration };
28
+ export type Migrations = CategorizedMigrations | RunnableMigrations;
29
+
30
+ export class MigrationError extends TypedError {}
31
+
32
+ // Tagged template to convert binds from `?` format to the necessary output format,
33
+ // eg `$1`/`$2`/etc for postgres
34
+ export const binds = (strings: TemplateStringsArray, ...bindNums: number[]) =>
35
+ strings
36
+ .map((str, i) => {
37
+ if (i === bindNums.length) {
38
+ return str;
39
+ }
40
+ if (i + 1 !== bindNums[i]) {
41
+ throw new SyntaxError('Migration sql binds must be sequential');
42
+ }
43
+ if (sbvrUtils.db.engine === Engines.postgres) {
44
+ return str + `$${bindNums[i]}`;
45
+ }
46
+ return str + `?`;
47
+ })
48
+ .join('');
49
+
50
+ export const lockMigrations = async <T>(
51
+ tx: Tx,
52
+ modelName: string,
53
+ fn: () => Promise<T>,
54
+ ): Promise<T | undefined> => {
55
+ if (!(await migrationTablesExist(tx))) {
56
+ return;
57
+ }
58
+
59
+ try {
60
+ await tx.executeSql(
61
+ binds`
62
+ DELETE FROM "migration lock"
63
+ WHERE "model name" = ${1}
64
+ AND "created at" < ${2}`,
65
+ [modelName, new Date(Date.now() - migratorEnv.lockTimeout)],
66
+ );
67
+ await tx.executeSql(
68
+ binds`
69
+ INSERT INTO "migration lock" ("model name")
70
+ VALUES (${1})`,
71
+ [modelName],
72
+ );
73
+ } catch (err: any) {
74
+ await delay(migratorEnv.lockFailDelay);
75
+ throw err;
76
+ }
77
+ try {
78
+ return await fn();
79
+ } finally {
80
+ try {
81
+ await tx.executeSql(
82
+ binds`
83
+ DELETE FROM "migration lock"
84
+ WHERE "model name" = ${1}`,
85
+ [modelName],
86
+ );
87
+ } catch {
88
+ // We ignore errors here as it's mostly likely caused by the migration failing and
89
+ // rolling back the transaction, and if we rethrow here we'll overwrite the real error
90
+ // making it much harder for users to see what went wrong and fix it
91
+ }
92
+ }
93
+ };
94
+
95
+ export const setExecutedMigrations = async (
96
+ tx: Tx,
97
+ modelName: string,
98
+ executedMigrations: string[],
99
+ ): Promise<void> => {
100
+ const stringifiedMigrations = JSON.stringify(executedMigrations);
101
+
102
+ if (!(await migrationTablesExist(tx))) {
103
+ return;
104
+ }
105
+
106
+ const { rowsAffected } = await tx.executeSql(
107
+ binds`
108
+ UPDATE "migration"
109
+ SET "model name" = ${1},
110
+ "executed migrations" = ${2}
111
+ WHERE "migration"."model name" = ${3}`,
112
+ [modelName, stringifiedMigrations, modelName],
113
+ );
114
+
115
+ if (rowsAffected === 0) {
116
+ await tx.executeSql(
117
+ binds`
118
+ INSERT INTO "migration" ("model name", "executed migrations")
119
+ VALUES (${1}, ${2})`,
120
+ [modelName, stringifiedMigrations],
121
+ );
122
+ }
123
+ };
124
+
125
+ export const getExecutedMigrations = async (
126
+ tx: Tx,
127
+ modelName: string,
128
+ ): Promise<string[]> => {
129
+ if (!(await migrationTablesExist(tx))) {
130
+ return [];
131
+ }
132
+
133
+ const { rows } = await tx.executeSql(
134
+ binds`
135
+ SELECT "migration"."executed migrations" AS "executed_migrations"
136
+ FROM "migration"
137
+ WHERE "migration"."model name" = ${1}`,
138
+ [modelName],
139
+ );
140
+
141
+ const data = rows[0];
142
+ if (data == null) {
143
+ return [];
144
+ }
145
+
146
+ return JSON.parse(data.executed_migrations) as string[];
147
+ };
148
+
149
+ export const migrationTablesExist = async (tx: Tx) => {
150
+ const tables = ['migration', 'migration lock'];
151
+ const where = tables.map((tableName) => `name = '${tableName}'`).join(' OR ');
152
+ const result = await tx.tableList(where);
153
+ return result.rows.length === tables.length;
154
+ };
@@ -21,17 +21,14 @@ const forEachUniqueTable = <T>(
21
21
  const usedTableNames: { [tableName: string]: true } = {};
22
22
 
23
23
  const result = [];
24
- for (const key in model) {
25
- if (model.hasOwnProperty(key)) {
26
- const table = model[key];
27
- if (
28
- typeof table !== 'string' &&
29
- !table.primitive &&
30
- !usedTableNames[table.name]
31
- ) {
32
- usedTableNames[table.name] = true;
33
- result.push(callback(key, table));
34
- }
24
+ for (const [key, table] of Object.entries(model)) {
25
+ if (
26
+ typeof table !== 'string' &&
27
+ !table.primitive &&
28
+ !usedTableNames[table.name]
29
+ ) {
30
+ usedTableNames[table.name] = true;
31
+ result.push(callback(key, table));
35
32
  }
36
33
  }
37
34
  return result;
@@ -65,8 +65,9 @@ const setup: ConfigLoader.SetupFunction = async (app: Express.Application) => {
65
65
  })(req, res, next);
66
66
 
67
67
  logout = (req, _res, next) => {
68
- req.logout();
69
- next();
68
+ req.logout((error) => {
69
+ error ? next(error) : next();
70
+ });
70
71
  };
71
72
  } else {
72
73
  let loggedIn = false;
@@ -47,7 +47,7 @@ export const compileRequest = (request: ODataRequest) => {
47
47
  );
48
48
  request.sqlQuery = sqlQuery;
49
49
  request.modifiedFields = modifiedFields;
50
- } catch (err) {
50
+ } catch (err: any) {
51
51
  sbvrUtils.api[request.vocabulary].logger.error(
52
52
  'Failed to compile abstract sql: ',
53
53
  request.abstractSqlQuery,
@@ -139,7 +139,7 @@ export const getAndCheckBindValues = async (
139
139
 
140
140
  try {
141
141
  return await AbstractSQLCompiler[engine].dataTypeValidate(value, field);
142
- } catch (err) {
142
+ } catch (err: any) {
143
143
  throw new BadRequestError(`"${fieldName}" ${err.message}`);
144
144
  }
145
145
  }),
@@ -173,7 +173,10 @@ const checkModifiedFields = (
173
173
  };
174
174
  export const isRuleAffected = (
175
175
  rule: AbstractSQLCompiler.SqlRule,
176
- request?: ODataRequest,
176
+ request?: Pick<
177
+ ODataRequest,
178
+ 'abstractSqlQuery' | 'modifiedFields' | 'method' | 'vocabulary'
179
+ >,
177
180
  ) => {
178
181
  // If there is no abstract sql query then nothing was modified
179
182
  if (request?.abstractSqlQuery == null) {
@@ -24,7 +24,7 @@ export const settleMapSeries: MappingFunction = async <T, U>(
24
24
  await mapSeries(a, async (p) => {
25
25
  try {
26
26
  return await fn(p);
27
- } catch (err) {
27
+ } catch (err: any) {
28
28
  return ensureError(err);
29
29
  }
30
30
  });
@@ -49,7 +49,7 @@ const mapTill: MappingFunction = async <T, U>(
49
49
  try {
50
50
  const result = await fn(p);
51
51
  results.push(result);
52
- } catch (err) {
52
+ } catch (err: any) {
53
53
  results.push(ensureError(err));
54
54
  break;
55
55
  }
@@ -1,6 +1,6 @@
1
1
  import type { OptionalField, Resolvable } from './common-types';
2
2
  import type { Tx } from '../database-layer/db';
3
- import type { ODataRequest } from './uri-parser';
3
+ import type { ODataRequest, ParsedODataRequest } from './uri-parser';
4
4
  import type { AnyObject } from 'pinejs-client-core';
5
5
  import type { TypedError } from 'typed-error';
6
6
  import type { SupportedMethod } from '@balena/odata-to-abstract-sql';
@@ -15,6 +15,7 @@ import {
15
15
  resolveSynonym,
16
16
  getAbstractSqlModel,
17
17
  api,
18
+ Response,
18
19
  } from './sbvr-utils';
19
20
 
20
21
  export interface HookReq {
@@ -46,6 +47,9 @@ export interface Hooks {
46
47
  options: HookArgs & {
47
48
  tx: Tx;
48
49
  result: any;
50
+ /** This can be mutated to modify the response sent to the client */
51
+ response: Response;
52
+ /** @deprecated Use the response object instead */
49
53
  data?: any;
50
54
  },
51
55
  ) => HookResponse;
@@ -127,11 +131,14 @@ export const rollbackRequestHooks = <T extends InstantiatedHooks>(
127
131
  if (hooks == null) {
128
132
  return;
129
133
  }
130
- settleMapSeries(_(hooks).flatMap().compact().value(), async (hook) => {
131
- if (hook instanceof SideEffectHook) {
132
- await hook.rollback();
133
- }
134
- });
134
+ settleMapSeries(
135
+ Object.values(hooks).flatMap((v): Array<Hook<HookFn>> => v),
136
+ async (hook) => {
137
+ if (hook instanceof SideEffectHook) {
138
+ await hook.rollback();
139
+ }
140
+ },
141
+ );
135
142
  };
136
143
 
137
144
  const instantiateHooks = (hooks: HookBlueprints): InstantiatedHooks =>
@@ -187,14 +194,17 @@ const getMethodHooks = memoize(
187
194
  );
188
195
  export const getHooks = (
189
196
  request: Pick<
190
- OptionalField<ODataRequest, 'resourceName'>,
197
+ OptionalField<ParsedODataRequest, 'resourceName'>,
191
198
  'resourceName' | 'method' | 'vocabulary'
192
199
  >,
193
200
  ): InstantiatedHooks => {
194
201
  let { resourceName } = request;
195
202
  if (resourceName != null) {
196
203
  resourceName = resolveSynonym(
197
- request as Pick<ODataRequest, 'resourceName' | 'method' | 'vocabulary'>,
204
+ request as Pick<
205
+ ParsedODataRequest,
206
+ 'resourceName' | 'method' | 'vocabulary'
207
+ >,
198
208
  );
199
209
  }
200
210
  return instantiateHooks(
@@ -148,16 +148,16 @@ export const process = async (
148
148
  );
149
149
 
150
150
  const odataIdField = sqlNameToODataName(table.idField);
151
- rows.forEach((row) => {
152
- processedFields.forEach((fieldName) => {
151
+ for (const row of rows) {
152
+ for (const fieldName of processedFields) {
153
153
  row[fieldName] = fetchProcessingFields[fieldName](row[fieldName]);
154
- });
154
+ }
155
155
  if (includeMetadata) {
156
156
  row.__metadata = {
157
157
  uri: resourceURI(vocab, resourceName, row[odataIdField]),
158
158
  };
159
159
  }
160
- });
160
+ }
161
161
 
162
162
  if (expandableFields.length > 0) {
163
163
  await Promise.all(