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

Sign up to get free protection for your applications and to get access to all the features.
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) {