@balena/pinejs 17.1.0-build-model-based-typings-437bb06f44567532aec78e550f3d545732466411-1 → 17.1.0-build-joshbwlng-tasks-61ce10e444abec6afea3fec43e9a5c37c7cedea6-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.pinejs-cache.json +1 -1
  2. package/.versionbot/CHANGELOG.yml +13 -239
  3. package/CHANGELOG.md +5 -69
  4. package/out/config-loader/env.d.ts +4 -0
  5. package/out/config-loader/env.js +5 -1
  6. package/out/config-loader/env.js.map +1 -1
  7. package/out/data-server/sbvr-server.js +2 -3
  8. package/out/data-server/sbvr-server.js.map +1 -1
  9. package/out/database-layer/db.d.ts +3 -0
  10. package/out/database-layer/db.js +17 -0
  11. package/out/database-layer/db.js.map +1 -1
  12. package/out/migrator/sync.d.ts +0 -17
  13. package/out/migrator/sync.js +40 -39
  14. package/out/migrator/sync.js.map +1 -1
  15. package/out/sbvr-api/hooks.d.ts +33 -33
  16. package/out/sbvr-api/hooks.js.map +1 -1
  17. package/out/sbvr-api/odata-response.d.ts +2 -1
  18. package/out/sbvr-api/odata-response.js +4 -4
  19. package/out/sbvr-api/odata-response.js.map +1 -1
  20. package/out/sbvr-api/permissions.d.ts +2 -26
  21. package/out/sbvr-api/permissions.js +40 -39
  22. package/out/sbvr-api/permissions.js.map +1 -1
  23. package/out/sbvr-api/sbvr-utils.d.ts +6 -46
  24. package/out/sbvr-api/sbvr-utils.js +76 -73
  25. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  26. package/out/server-glue/module.d.ts +1 -0
  27. package/out/server-glue/module.js +4 -1
  28. package/out/server-glue/module.js.map +1 -1
  29. package/out/tasks/common.d.ts +4 -0
  30. package/out/tasks/common.js +13 -0
  31. package/out/tasks/common.js.map +1 -0
  32. package/out/tasks/index.d.ts +8 -0
  33. package/out/tasks/index.js +142 -0
  34. package/out/tasks/index.js.map +1 -0
  35. package/out/tasks/tasks.sbvr +60 -0
  36. package/out/tasks/types.d.ts +38 -0
  37. package/out/tasks/types.js +10 -0
  38. package/out/tasks/types.js.map +1 -0
  39. package/out/tasks/worker.d.ts +16 -0
  40. package/out/tasks/worker.js +228 -0
  41. package/out/tasks/worker.js.map +1 -0
  42. package/package.json +20 -19
  43. package/src/config-loader/env.ts +6 -1
  44. package/src/data-server/sbvr-server.js +2 -3
  45. package/src/database-layer/db.ts +25 -0
  46. package/src/migrator/sync.ts +41 -46
  47. package/src/sbvr-api/hooks.ts +20 -21
  48. package/src/sbvr-api/odata-response.ts +13 -3
  49. package/src/sbvr-api/permissions.ts +48 -54
  50. package/src/sbvr-api/sbvr-utils.ts +92 -133
  51. package/src/server-glue/module.ts +3 -0
  52. package/src/tasks/common.ts +14 -0
  53. package/src/tasks/index.ts +158 -0
  54. package/src/tasks/tasks.sbvr +60 -0
  55. package/src/tasks/types.ts +58 -0
  56. package/src/tasks/worker.ts +278 -0
  57. package/out/migrator/migrations.d.ts +0 -58
  58. package/out/migrator/migrations.js +0 -3
  59. package/out/migrator/migrations.js.map +0 -1
  60. package/out/sbvr-api/dev.d.ts +0 -22
  61. package/out/sbvr-api/dev.js +0 -3
  62. package/out/sbvr-api/dev.js.map +0 -1
  63. package/out/sbvr-api/user.d.ts +0 -236
  64. package/out/sbvr-api/user.js +0 -3
  65. package/out/sbvr-api/user.js.map +0 -1
  66. package/src/migrator/migrations.ts +0 -64
  67. package/src/sbvr-api/dev.ts +0 -26
  68. package/src/sbvr-api/user.ts +0 -216
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.Worker = void 0;
27
+ const promises_1 = require("node:timers/promises");
28
+ const env_1 = require("../config-loader/env");
29
+ const db_1 = require("../database-layer/db");
30
+ const permissions = __importStar(require("../sbvr-api/permissions"));
31
+ const sbvr_utils_1 = require("../sbvr-api/sbvr-utils");
32
+ const module_1 = require("../server-glue/module");
33
+ const common_1 = require("./common");
34
+ const selectColumns = Object.entries({
35
+ id: 'id',
36
+ 'is executed by-handler': 'is_executed_by__handler',
37
+ 'is executed with-parameter set': 'is_executed_with__parameter_set',
38
+ 'is scheduled with-cron expression': 'is_scheduled_with__cron_expression',
39
+ 'attempt count': 'attempt_count',
40
+ 'attempt limit': 'attempt_limit',
41
+ priority: 'priority',
42
+ 'is created by-actor': 'is_created_by__actor',
43
+ })
44
+ .map(([key, value]) => `t."${key}" AS "${value}"`)
45
+ .join(', ');
46
+ class Worker {
47
+ client;
48
+ handlers = {};
49
+ concurrency;
50
+ interval;
51
+ executing = 0;
52
+ constructor(client) {
53
+ this.client = client;
54
+ this.concurrency = env_1.tasks.queueConcurrency;
55
+ this.interval = env_1.tasks.queueIntervalMS;
56
+ }
57
+ canExecute() {
58
+ return (this.executing < this.concurrency && Object.keys(this.handlers).length > 0);
59
+ }
60
+ async execute(task, tx) {
61
+ this.executing++;
62
+ try {
63
+ const handler = this.handlers[task.is_executed_by__handler];
64
+ const startedOnTime = new Date();
65
+ if (handler == null) {
66
+ await this.finalize(tx, task, startedOnTime, 'failed', 'Matching task handler not found');
67
+ return;
68
+ }
69
+ if (handler.validate != null &&
70
+ !handler.validate(task.is_executed_with__parameter_set)) {
71
+ await this.finalize(tx, task, startedOnTime, 'failed', `Invalid parameter set: ${common_1.ajv.errorsText(handler.validate.errors)}`);
72
+ return;
73
+ }
74
+ let status = 'queued';
75
+ let error;
76
+ try {
77
+ await module_1.sbvrUtils.db.transaction(async (handlerTx) => {
78
+ const results = await handler.fn({
79
+ api: new sbvr_utils_1.PinejsClient({
80
+ passthrough: {
81
+ tx: handlerTx,
82
+ },
83
+ }),
84
+ params: task.is_executed_with__parameter_set ?? {},
85
+ tx: handlerTx,
86
+ });
87
+ status = results.status;
88
+ error = results.error;
89
+ if (results.status !== 'succeeded' && !handlerTx.isClosed()) {
90
+ await handlerTx.rollback();
91
+ }
92
+ });
93
+ }
94
+ catch (err) {
95
+ if (!(err instanceof db_1.TransactionClosedError)) {
96
+ throw err;
97
+ }
98
+ }
99
+ finally {
100
+ await this.finalize(tx, task, startedOnTime, status, error);
101
+ }
102
+ }
103
+ catch (err) {
104
+ console.error(`Failed to execute task ${task.id} with handler ${task.is_executed_by__handler}:`, err);
105
+ process.exit(1);
106
+ }
107
+ finally {
108
+ this.executing--;
109
+ }
110
+ }
111
+ async finalize(tx, task, startedOnTime, status, errorMessage) {
112
+ const attemptCount = task.attempt_count + 1;
113
+ const body = {
114
+ started_on__time: startedOnTime,
115
+ ended_on__time: new Date(),
116
+ status,
117
+ attempt_count: attemptCount,
118
+ ...(errorMessage != null && { error_message: errorMessage }),
119
+ };
120
+ if (status === 'failed' && attemptCount < task.attempt_limit) {
121
+ body.status = 'queued';
122
+ body.is_scheduled_to_execute_on__time =
123
+ this.getNextAttemptTime(attemptCount);
124
+ }
125
+ await this.client.patch({
126
+ resource: 'task',
127
+ passthrough: {
128
+ tx,
129
+ req: permissions.root,
130
+ },
131
+ id: task.id,
132
+ body,
133
+ });
134
+ if (['failed', 'succeeded'].includes(body.status) &&
135
+ task.is_scheduled_with__cron_expression != null) {
136
+ await this.client.post({
137
+ resource: 'task',
138
+ passthrough: {
139
+ tx,
140
+ req: permissions.root,
141
+ },
142
+ options: {
143
+ returnResource: false,
144
+ },
145
+ body: {
146
+ attempt_limit: task.attempt_limit,
147
+ is_created_by__actor: task.is_created_by__actor,
148
+ is_executed_by__handler: task.is_executed_by__handler,
149
+ is_executed_with__parameter_set: task.is_executed_with__parameter_set,
150
+ is_scheduled_with__cron_expression: task.is_scheduled_with__cron_expression,
151
+ priority: task.priority,
152
+ },
153
+ });
154
+ }
155
+ }
156
+ getNextAttemptTime(attempt) {
157
+ const delay = Math.ceil(Math.exp(Math.min(10, attempt)));
158
+ return new Date(Date.now() + delay);
159
+ }
160
+ poll() {
161
+ let executed = false;
162
+ void (async () => {
163
+ try {
164
+ const handlerNames = Object.keys(this.handlers);
165
+ const binds = handlerNames
166
+ .map((_, index) => `$${index + 1}`)
167
+ .join(', ');
168
+ if (!this.canExecute()) {
169
+ return;
170
+ }
171
+ await module_1.sbvrUtils.db.transaction(async (tx) => {
172
+ const result = await tx.executeSql(`SELECT ${selectColumns}
173
+ FROM task AS t
174
+ WHERE
175
+ t."is executed by-handler" IN (${binds}) AND
176
+ t."status" = 'queued' AND
177
+ t."attempt count" <= t."attempt limit" AND
178
+ (
179
+ t."is scheduled to execute on-time" IS NULL OR
180
+ t."is scheduled to execute on-time" <= CURRENT_TIMESTAMP + INTERVAL '${Math.ceil(this.interval / 1000)} second'
181
+ )
182
+ ORDER BY
183
+ t."is scheduled to execute on-time" ASC,
184
+ t."priority" DESC,
185
+ t."id" ASC
186
+ LIMIT ${Math.max(this.concurrency - this.executing, 0)}
187
+ FOR UPDATE SKIP LOCKED`, handlerNames);
188
+ if (result.rows.length === 0) {
189
+ return;
190
+ }
191
+ await Promise.all(result.rows.map(async (row) => {
192
+ await this.execute(row, tx);
193
+ }));
194
+ executed = true;
195
+ });
196
+ }
197
+ catch (err) {
198
+ console.error('Failed polling for tasks:', err);
199
+ }
200
+ finally {
201
+ if (!executed) {
202
+ await (0, promises_1.setTimeout)(this.interval);
203
+ }
204
+ this.poll();
205
+ }
206
+ })();
207
+ }
208
+ start() {
209
+ if (module_1.sbvrUtils.db.engine !== 'postgres' || module_1.sbvrUtils.db.on == null) {
210
+ throw new Error('Database does not support tasks, giving up on starting worker');
211
+ }
212
+ module_1.sbvrUtils.db.on('notification', async (msg) => {
213
+ if (this.canExecute()) {
214
+ await module_1.sbvrUtils.db.transaction(async (tx) => {
215
+ const result = await tx.executeSql(`SELECT ${selectColumns} FROM task AS t WHERE id = $1 FOR UPDATE SKIP LOCKED`, [msg.payload]);
216
+ if (result.rows.length > 0) {
217
+ await this.execute(result.rows[0], tx);
218
+ }
219
+ });
220
+ }
221
+ }, {
222
+ channel: common_1.channel,
223
+ });
224
+ this.poll();
225
+ }
226
+ }
227
+ exports.Worker = Worker;
228
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/tasks/worker.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@balena/pinejs",
3
- "version": "17.1.0-build-model-based-typings-437bb06f44567532aec78e550f3d545732466411-1",
3
+ "version": "17.1.0-build-joshbwlng-tasks-61ce10e444abec6afea3fec43e9a5c37c7cedea6-1",
4
4
  "main": "out/server-glue/module",
5
5
  "type": "commonjs",
6
6
  "repository": "git@github.com:balena-io/pinejs.git",
@@ -19,48 +19,48 @@
19
19
  "webpack-server": "grunt server",
20
20
  "webpack-build": "npm run webpack-browser && npm run webpack-module && npm run webpack-server",
21
21
  "lint": "balena-lint -t tsconfig.dev.json -e js -e ts src build typings Gruntfile.ts && npx tsc --project tsconfig.dev.json --noEmit",
22
- "test": "npm run lint && npm run build && npm run webpack-build && npm run test:compose && npm run test:generated-types",
23
- "test:compose": "trap 'docker compose -f docker-compose.npm-test.yml down ; echo Stopped ; exit 0' INT; docker compose -f docker-compose.npm-test.yml up -d && sleep 2 && DATABASE_URL=postgres://docker:docker@localhost:5431/postgres PINEJS_WEBRESOURCE_MAXFILESIZE=1000000000 S3_ENDPOINT=http://localhost:43680 S3_ACCESS_KEY=USERNAME S3_SECRET_KEY=PASSWORD S3_STORAGE_ADAPTER_BUCKET=balena-pine-web-resources S3_REGION=us-east-1 npm run mocha",
24
- "test:generated-types": "npm run generate-types && git diff --exit-code ./src/sbvr-api/user.ts ./src/migrator/migrations.ts ./src/sbvr-api/dev.ts",
22
+ "test": "npm run lint && npm run build && npm run webpack-build && npm run test:compose",
23
+ "test:compose": "trap 'docker compose -f docker-compose.npm-test.yml down ; echo Stopped ; exit 0' INT; docker compose -f docker-compose.npm-test.yml up -d && sleep 2 && DATABASE_URL=postgres://docker:docker@localhost:5431/postgres PINEJS_WEBRESOURCE_MAXFILESIZE=1000000000 S3_ENDPOINT=http://localhost:43680 S3_ACCESS_KEY=USERNAME S3_SECRET_KEY=PASSWORD S3_STORAGE_ADAPTER_BUCKET=balena-pine-web-resources S3_REGION=us-east-1 PINEJS_QUEUE_CONCURRENCY=1 npm run mocha",
25
24
  "mocha": "TS_NODE_FILES=true mocha",
26
- "prettify": "balena-lint -t tsconfig.dev.json -e js -e ts --fix src test build typings Gruntfile.ts",
27
- "generate-types": "node ./bin/sbvr-compiler.js generate-types ./src/sbvr-api/user.sbvr ./src/sbvr-api/user.ts && node ./bin/sbvr-compiler.js generate-types ./src/migrator/migrations.sbvr ./src/migrator/migrations.ts && node ./bin/sbvr-compiler.js generate-types ./src/sbvr-api/dev.sbvr ./src/sbvr-api/dev.ts && balena-lint -t tsconfig.dev.json --fix ./src/sbvr-api/user.ts ./src/migrator/migrations.ts ./src/sbvr-api/dev.ts"
25
+ "prettify": "balena-lint -t tsconfig.dev.json -e js -e ts --fix src test build typings Gruntfile.ts"
28
26
  },
29
27
  "dependencies": {
30
- "@balena/abstract-sql-compiler": "^9.2.0",
31
- "@balena/abstract-sql-to-typescript": "^3.2.1",
28
+ "@balena/abstract-sql-compiler": "^9.1.4",
29
+ "@balena/abstract-sql-to-typescript": "^3.1.1",
32
30
  "@balena/env-parsing": "^1.1.12",
33
31
  "@balena/lf-to-abstract-sql": "^5.0.2",
34
32
  "@balena/odata-parser": "^3.0.8",
35
33
  "@balena/odata-to-abstract-sql": "^6.2.7",
36
34
  "@balena/sbvr-parser": "^1.4.4",
37
- "@balena/sbvr-types": "^7.1.3",
35
+ "@balena/sbvr-types": "^7.1.1",
38
36
  "@types/body-parser": "^1.19.5",
39
37
  "@types/compression": "^1.7.5",
40
38
  "@types/cookie-parser": "^1.4.7",
41
39
  "@types/deep-freeze": "^0.1.5",
42
40
  "@types/express": "^4.17.21",
43
41
  "@types/express-session": "^1.18.0",
44
- "@types/lodash": "^4.17.5",
42
+ "@types/lodash": "^4.17.4",
45
43
  "@types/memoizee": "^0.4.11",
46
44
  "@types/method-override": "^0.0.35",
47
45
  "@types/multer": "^1.4.11",
48
46
  "@types/mysql": "^2.15.26",
49
- "@types/node": "^20.14.5",
47
+ "@types/node": "^20.14.2",
50
48
  "@types/passport": "^1.0.16",
51
49
  "@types/passport-local": "^1.0.38",
52
50
  "@types/passport-strategy": "^0.2.38",
53
51
  "@types/pg": "^8.11.6",
54
52
  "@types/randomstring": "^1.3.0",
55
53
  "@types/websql": "^0.0.30",
54
+ "ajv": "^8.12.0",
56
55
  "busboy": "^1.6.0",
57
56
  "commander": "^11.1.0",
57
+ "cron-parser": "^4.9.0",
58
58
  "deep-freeze": "^0.0.1",
59
59
  "eventemitter3": "^5.0.1",
60
60
  "express-session": "^1.18.0",
61
61
  "lodash": "^4.17.21",
62
62
  "memoizee": "^0.4.17",
63
- "pinejs-client-core": "^6.15.1",
63
+ "pinejs-client-core": "^6.14.6",
64
64
  "randomstring": "^1.3.0",
65
65
  "typed-error": "^3.2.2"
66
66
  },
@@ -91,11 +91,12 @@
91
91
  "grunt-ts": "^6.0.0-beta.22",
92
92
  "grunt-webpack": "^6.0.0",
93
93
  "husky": "^9.0.11",
94
- "lint-staged": "^15.2.7",
94
+ "json-schema-to-ts": "^3.1.0",
95
+ "lint-staged": "^15.2.5",
95
96
  "load-grunt-tasks": "^5.1.0",
96
97
  "mocha": "^10.4.0",
97
98
  "on-finished": "^2.4.1",
98
- "pinejs-client-supertest": "^2.0.4",
99
+ "pinejs-client-supertest": "^2.0.3",
99
100
  "raw-loader": "^4.0.2",
100
101
  "request": "^2.88.2",
101
102
  "require-npm4-to-publish": "^1.0.0",
@@ -104,13 +105,13 @@
104
105
  "ts-loader": "^9.5.1",
105
106
  "ts-node": "^10.9.2",
106
107
  "typescript": "^5.4.5",
107
- "webpack": "^5.92.0",
108
+ "webpack": "^5.91.0",
108
109
  "webpack-dev-server": "^4.15.2"
109
110
  },
110
111
  "optionalDependencies": {
111
- "@aws-sdk/client-s3": "^3.598.0",
112
- "@aws-sdk/lib-storage": "^3.598.0",
113
- "@aws-sdk/s3-request-presigner": "^3.598.0",
112
+ "@aws-sdk/client-s3": "^3.590.0",
113
+ "@aws-sdk/lib-storage": "^3.590.0",
114
+ "@aws-sdk/s3-request-presigner": "^3.590.0",
114
115
  "bcrypt": "^5.1.1",
115
116
  "body-parser": "^1.20.2",
116
117
  "compression": "^1.7.4",
@@ -146,6 +147,6 @@
146
147
  "recursive": true
147
148
  },
148
149
  "versionist": {
149
- "publishedAt": "2024-06-18T16:52:31.030Z"
150
+ "publishedAt": "2024-06-19T04:57:00.974Z"
150
151
  }
151
152
  }
@@ -49,7 +49,7 @@ export const cache = {
49
49
  apiKeyActorId: false as CacheOpts,
50
50
  };
51
51
 
52
- import { boolVar } from '@balena/env-parsing';
52
+ import { boolVar, intVar } from '@balena/env-parsing';
53
53
  import memoize from 'memoizee';
54
54
  import memoizeWeak = require('memoizee/weak');
55
55
  export const createCache = <T extends (...args: any[]) => any>(
@@ -146,3 +146,8 @@ export const migrator = {
146
146
  */
147
147
  asyncMigrationIsEnabled: boolVar('PINEJS_ASYNC_MIGRATION_ENABLED', true),
148
148
  };
149
+
150
+ export const tasks = {
151
+ queueConcurrency: intVar('PINEJS_QUEUE_CONCURRENCY', 0),
152
+ queueIntervalMS: intVar('PINEJS_QUEUE_INTERVAL_MS', 1000),
153
+ };
@@ -133,15 +133,14 @@ export async function setup(app, sbvrUtils, db) {
133
133
  },
134
134
  },
135
135
  })
136
- .then(async (result) => {
136
+ .then(async (/** @type { Array<{ [key: string]: any }> } */ result) => {
137
137
  if (result.length === 0) {
138
138
  throw new Error('No SE data model found');
139
139
  }
140
140
  const instance = result[0];
141
141
  await sbvrUtils.executeModel(tx, {
142
142
  apiRoot: instance.is_of__vocabulary,
143
- // prettier-ignore
144
- modelText: /** @type { string } */ (instance.model_value.value),
143
+ modelText: instance.model_value.value,
145
144
  });
146
145
  });
147
146
  await isServerOnAir(true);
@@ -98,6 +98,13 @@ export interface Database extends BaseDatabase {
98
98
  ) => Promise<Result>;
99
99
  transaction: TransactionFn;
100
100
  readTransaction: TransactionFn;
101
+ on?: (
102
+ name: 'notification',
103
+ fn: (...args: any[]) => Promise<void>,
104
+ options?: {
105
+ channel?: string;
106
+ },
107
+ ) => void;
101
108
  }
102
109
 
103
110
  interface EngineParams {
@@ -689,6 +696,24 @@ if (maybePg != null) {
689
696
  return {
690
697
  engine: Engines.postgres,
691
698
  executeSql: atomicExecuteSql,
699
+ on: async (name, fn, options) => {
700
+ if (name === 'notification' && options?.channel === undefined) {
701
+ throw new Error('Missing channel option for notification listener');
702
+ }
703
+
704
+ const client = await pool.connect();
705
+ client.on(name, async (msg) => {
706
+ try {
707
+ await fn(msg);
708
+ } catch (error) {
709
+ console.error('Error handling message:', error);
710
+ }
711
+ });
712
+
713
+ if (name === 'notification' && options?.channel !== undefined) {
714
+ await client.query(`LISTEN "${options.channel}";`);
715
+ }
716
+ },
692
717
  transaction: createTransaction(async (stackTraceErr) => {
693
718
  const client = await pool.connect();
694
719
  const tx = new PostgresTx(client, false, stackTraceErr);
@@ -1,4 +1,3 @@
1
- import type MigrationsModel from './migrations';
2
1
  import {
3
2
  type MigrationTuple,
4
3
  MigrationError,
@@ -17,7 +16,7 @@ import _ from 'lodash';
17
16
  import * as sbvrUtils from '../sbvr-api/sbvr-utils';
18
17
 
19
18
  // eslint-disable-next-line @typescript-eslint/no-var-requires
20
- const migrationsModel = require('./migrations.sbvr');
19
+ const modelText = require('./migrations.sbvr');
21
20
 
22
21
  type ApiRootModel = Model & { apiRoot: string };
23
22
 
@@ -137,49 +136,45 @@ const executeMigration = async (
137
136
  }
138
137
  };
139
138
 
140
- declare module '../sbvr-api/sbvr-utils' {
141
- export interface API {
142
- [migrationModelConfig.apiRoot]: PinejsClient<MigrationsModel>;
143
- }
144
- }
145
- const migrationModelConfig = {
146
- modelName: 'migrations',
147
- apiRoot: 'migrations',
148
- modelText: migrationsModel,
149
- migrations: {
150
- '11.0.0-modified-at': `
151
- ALTER TABLE "migration"
152
- ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
153
- `,
154
- '11.0.1-modified-at': `
155
- ALTER TABLE "migration lock"
156
- ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
157
- `,
158
- '15.0.0-data-types': async (tx, { db }) => {
159
- switch (db.engine) {
160
- case 'mysql':
161
- await tx.executeSql(`\
162
- ALTER TABLE "migration"
163
- MODIFY "executed migrations" JSON NOT NULL;`);
164
- await tx.executeSql(`\
165
- ALTER TABLE "migration status"
166
- MODIFY "is backing off" BOOLEAN NOT NULL;`);
167
- break;
168
- case 'postgres':
169
- await tx.executeSql(`\
170
- ALTER TABLE "migration"
171
- ALTER COLUMN "executed migrations" SET DATA TYPE JSONB USING "executed migrations"::JSONB;`);
172
- await tx.executeSql(`\
173
- ALTER TABLE "migration status"
174
- ALTER COLUMN "is backing off" DROP DEFAULT,
175
- ALTER COLUMN "is backing off" SET DATA TYPE BOOLEAN USING "is backing off"::BOOLEAN,
176
- ALTER COLUMN "is backing off" SET DEFAULT FALSE;`);
177
- break;
178
- // No need to migrate for websql
179
- }
180
- },
181
- },
182
- } as const satisfies sbvrUtils.ExecutableModel;
183
139
  export const config: Config = {
184
- models: [migrationModelConfig],
140
+ models: [
141
+ {
142
+ modelName: 'migrations',
143
+ apiRoot: 'migrations',
144
+ modelText,
145
+ migrations: {
146
+ '11.0.0-modified-at': `
147
+ ALTER TABLE "migration"
148
+ ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
149
+ `,
150
+ '11.0.1-modified-at': `
151
+ ALTER TABLE "migration lock"
152
+ ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
153
+ `,
154
+ '15.0.0-data-types': async (tx, { db }) => {
155
+ switch (db.engine) {
156
+ case 'mysql':
157
+ await tx.executeSql(`\
158
+ ALTER TABLE "migration"
159
+ MODIFY "executed migrations" JSON NOT NULL;`);
160
+ await tx.executeSql(`\
161
+ ALTER TABLE "migration status"
162
+ MODIFY "is backing off" BOOLEAN NOT NULL;`);
163
+ break;
164
+ case 'postgres':
165
+ await tx.executeSql(`\
166
+ ALTER TABLE "migration"
167
+ ALTER COLUMN "executed migrations" SET DATA TYPE JSONB USING "executed migrations"::JSONB;`);
168
+ await tx.executeSql(`\
169
+ ALTER TABLE "migration status"
170
+ ALTER COLUMN "is backing off" DROP DEFAULT,
171
+ ALTER COLUMN "is backing off" SET DATA TYPE BOOLEAN USING "is backing off"::BOOLEAN,
172
+ ALTER COLUMN "is backing off" SET DEFAULT FALSE;`);
173
+ break;
174
+ // No need to migrate for websql
175
+ }
176
+ },
177
+ },
178
+ },
179
+ ],
185
180
  };
@@ -1,5 +1,5 @@
1
1
  import type { OptionalField, Resolvable } from './common-types';
2
- import type { Tx } from '../database-layer/db';
2
+ import type { Result, Tx } from '../database-layer/db';
3
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';
@@ -9,6 +9,7 @@ import _ from 'lodash';
9
9
  import { settleMapSeries } from './control-flow';
10
10
  import memoize from 'memoizee';
11
11
  import {
12
+ type PinejsClient,
12
13
  type User,
13
14
  type ApiKey,
14
15
  resolveSynonym,
@@ -30,36 +31,34 @@ export interface HookReq {
30
31
  hooks?: InstantiatedHooks;
31
32
  is?: (type: string | string[]) => string | false | null;
32
33
  }
33
- export interface HookArgs<Vocab extends string = string> {
34
+ export interface HookArgs {
34
35
  req: HookReq;
35
36
  request: ODataRequest;
36
- api: (typeof api)[Vocab];
37
+ api: PinejsClient;
37
38
  tx?: Tx | undefined;
38
39
  }
39
40
  export type HookResponse = PromiseLike<any> | null | void;
40
41
 
41
- export interface Hooks<Vocab extends string = string> {
42
- PREPARSE?: (
43
- options: Omit<HookArgs<Vocab>, 'request' | 'api'>,
44
- ) => HookResponse;
45
- POSTPARSE?: (options: HookArgs<Vocab>) => HookResponse;
46
- PRERUN?: (options: HookArgs<Vocab> & { tx: Tx }) => HookResponse;
42
+ export interface Hooks {
43
+ PREPARSE?: (options: Omit<HookArgs, 'request' | 'api'>) => HookResponse;
44
+ POSTPARSE?: (options: HookArgs) => HookResponse;
45
+ PRERUN?: (options: HookArgs & { tx: Tx }) => HookResponse;
47
46
  /** These are run in reverse translation order from newest to oldest */
48
47
  POSTRUN?: (
49
- options: HookArgs<Vocab> & { tx: Tx; result: any },
48
+ options: HookArgs & { tx: Tx; result: Result | number | undefined },
50
49
  ) => HookResponse;
51
50
  /** These are run in reverse translation order from newest to oldest */
52
51
  PRERESPOND?: (
53
- options: HookArgs<Vocab> & {
52
+ options: HookArgs & {
54
53
  tx: Tx;
55
- result: any;
54
+ result?: Result | number | AnyObject;
56
55
  /** This can be mutated to modify the response sent to the client */
57
56
  response: Response;
58
57
  },
59
58
  ) => HookResponse;
60
59
  /** These are run in reverse translation order from newest to oldest */
61
60
  'POSTRUN-ERROR'?: (
62
- options: HookArgs<Vocab> & { tx: Tx; error: TypedError | any },
61
+ options: HookArgs & { tx: Tx; error: TypedError | any },
63
62
  ) => HookResponse;
64
63
  }
65
64
  export type HookBlueprints = {
@@ -265,9 +264,9 @@ const apiHooks = {
265
264
  // Share hooks between merge and patch since they are the same operation,
266
265
  // just MERGE was the OData intermediary until the HTTP spec added PATCH.
267
266
  apiHooks.MERGE = apiHooks.PATCH;
268
- export const addHook = <Vocab extends string>(
267
+ export const addHook = (
269
268
  method: keyof typeof apiHooks,
270
- vocabulary: Vocab,
269
+ vocabulary: string,
271
270
  resourceName: string,
272
271
  hooks:
273
272
  | { [key in keyof Hooks]: HookBlueprint<NonNullable<Hooks[key]>> }
@@ -346,11 +345,11 @@ export const addHook = <Vocab extends string>(
346
345
  getHooks.clear();
347
346
  };
348
347
 
349
- export const addSideEffectHook = <Vocab extends string>(
348
+ export const addSideEffectHook = (
350
349
  method: HookMethod,
351
- apiRoot: Vocab,
350
+ apiRoot: string,
352
351
  resourceName: string,
353
- hooks: Hooks<NoInfer<Vocab>>,
352
+ hooks: Hooks,
354
353
  ): void => {
355
354
  addHook(method, apiRoot, resourceName, {
356
355
  ...hooks,
@@ -359,11 +358,11 @@ export const addSideEffectHook = <Vocab extends string>(
359
358
  });
360
359
  };
361
360
 
362
- export const addPureHook = <Vocab extends string>(
361
+ export const addPureHook = (
363
362
  method: HookMethod,
364
- apiRoot: Vocab,
363
+ apiRoot: string,
365
364
  resourceName: string,
366
- hooks: Hooks<NoInfer<Vocab>>,
365
+ hooks: Hooks,
367
366
  ): void => {
368
367
  addHook(method, apiRoot, resourceName, {
369
368
  ...hooks,
@@ -76,11 +76,21 @@ const checkForExpansion = async (
76
76
  }
77
77
  };
78
78
 
79
- export const resourceURI = (
79
+ export function resourceURI(
80
80
  vocab: string,
81
81
  resourceName: string,
82
82
  id: string | number,
83
- ): string | undefined => {
83
+ ): string;
84
+ export function resourceURI(
85
+ vocab: string,
86
+ resourceName: string,
87
+ id: string | number | null | undefined,
88
+ ): string | undefined;
89
+ export function resourceURI(
90
+ vocab: string,
91
+ resourceName: string,
92
+ id: string | number | null | undefined,
93
+ ): string | undefined {
84
94
  if (id == null) {
85
95
  return;
86
96
  }
@@ -88,7 +98,7 @@ export const resourceURI = (
88
98
  id = "'" + encodeURIComponent(id) + "'";
89
99
  }
90
100
  return `/${vocab}/${resourceName}(@id)?@id=${id}`;
91
- };
101
+ }
92
102
 
93
103
  const getLocalFields = (table: AbstractSqlTable) => {
94
104
  if (table.localFields == null) {