@balena/pinejs 17.1.0-build-joshbwlng-tasks-e3dcb0e73ea9c960af553c67cbf7121650f8d1ea-1 → 17.1.0-build-model-based-typings-c276ef4fb8482a246c25940d617d76b76847eff8-1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. package/.pinejs-cache.json +1 -1
  2. package/.versionbot/CHANGELOG.yml +227 -13
  3. package/CHANGELOG.md +70 -4
  4. package/out/config-loader/env.d.ts +0 -4
  5. package/out/config-loader/env.js +1 -5
  6. package/out/config-loader/env.js.map +1 -1
  7. package/out/data-server/sbvr-server.js +3 -2
  8. package/out/data-server/sbvr-server.js.map +1 -1
  9. package/out/database-layer/db.d.ts +0 -3
  10. package/out/database-layer/db.js +0 -17
  11. package/out/database-layer/db.js.map +1 -1
  12. package/out/migrator/migrations.d.ts +58 -0
  13. package/out/migrator/migrations.js +3 -0
  14. package/out/migrator/migrations.js.map +1 -0
  15. package/out/migrator/sync.d.ts +17 -0
  16. package/out/migrator/sync.js +39 -40
  17. package/out/migrator/sync.js.map +1 -1
  18. package/out/sbvr-api/dev.d.ts +22 -0
  19. package/out/sbvr-api/dev.js +3 -0
  20. package/out/sbvr-api/dev.js.map +1 -0
  21. package/out/sbvr-api/hooks.d.ts +28 -28
  22. package/out/sbvr-api/hooks.js.map +1 -1
  23. package/out/sbvr-api/permissions.d.ts +26 -2
  24. package/out/sbvr-api/permissions.js +39 -40
  25. package/out/sbvr-api/permissions.js.map +1 -1
  26. package/out/sbvr-api/sbvr-utils.d.ts +46 -6
  27. package/out/sbvr-api/sbvr-utils.js +44 -44
  28. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  29. package/out/sbvr-api/user.d.ts +236 -0
  30. package/out/sbvr-api/user.js +3 -0
  31. package/out/sbvr-api/user.js.map +1 -0
  32. package/out/server-glue/module.d.ts +0 -1
  33. package/out/server-glue/module.js +1 -4
  34. package/out/server-glue/module.js.map +1 -1
  35. package/package.json +20 -21
  36. package/src/config-loader/env.ts +1 -6
  37. package/src/data-server/sbvr-server.js +3 -2
  38. package/src/database-layer/db.ts +0 -25
  39. package/src/migrator/migrations.ts +64 -0
  40. package/src/migrator/sync.ts +46 -41
  41. package/src/sbvr-api/dev.ts +26 -0
  42. package/src/sbvr-api/hooks.ts +19 -18
  43. package/src/sbvr-api/permissions.ts +54 -48
  44. package/src/sbvr-api/sbvr-utils.ts +93 -53
  45. package/src/sbvr-api/user.ts +216 -0
  46. package/src/server-glue/module.ts +0 -3
  47. package/out/tasks/common.d.ts +0 -4
  48. package/out/tasks/common.js +0 -11
  49. package/out/tasks/common.js.map +0 -1
  50. package/out/tasks/index.d.ts +0 -8
  51. package/out/tasks/index.js +0 -140
  52. package/out/tasks/index.js.map +0 -1
  53. package/out/tasks/tasks.sbvr +0 -55
  54. package/out/tasks/types.d.ts +0 -37
  55. package/out/tasks/types.js +0 -10
  56. package/out/tasks/types.js.map +0 -1
  57. package/out/tasks/worker.d.ts +0 -16
  58. package/out/tasks/worker.js +0 -226
  59. package/out/tasks/worker.js.map +0 -1
  60. package/src/tasks/common.ts +0 -9
  61. package/src/tasks/index.ts +0 -155
  62. package/src/tasks/tasks.sbvr +0 -55
  63. package/src/tasks/types.ts +0 -56
  64. package/src/tasks/worker.ts +0 -276
@@ -1,55 +0,0 @@
1
- Vocabulary: tasks
2
-
3
- Term: id
4
- Concept Type: Big Serial (Type)
5
- Term: actor
6
- Concept Type: Integer (Type)
7
- Term: attempt count
8
- Concept Type: Integer (Type)
9
- Term: attempt limit
10
- Concept Type: Integer (Type)
11
- Term: cron expression
12
- Concept Type: Short Text (Type)
13
- Term: error message
14
- Concept Type: Short Text (Type)
15
- Term: handler
16
- Concept Type: Short Text (Type)
17
- Term: key
18
- Concept Type: Short Text (Type)
19
- Term: parameter set
20
- Concept Type: JSON (Type)
21
- Term: status
22
- Concept Type: Short Text (Type)
23
- Term: time
24
- Concept Type: Date Time (Type)
25
-
26
- Term: task
27
- Fact type: task has id
28
- Necessity: each task has exactly one id
29
- Fact type: task has key
30
- Necessity: each task has at most one key
31
- Fact type: task is created by actor
32
- Necessity: each task is created by exactly one actor
33
- Fact type: task is executed by handler
34
- Necessity: each task is executed by exactly one handler
35
- Fact type: task is executed with parameter set
36
- Necessity: each task is executed with at most one parameter set
37
- Fact type: task is scheduled with cron expression
38
- Necessity: each task is scheduled with at most one cron expression
39
- Fact type: task is scheduled to execute on time
40
- Necessity: each task is scheduled to execute on at most one time
41
- Fact type: task has status
42
- Necessity: each task has exactly one status
43
- Definition: "queued" or "cancelled" or "succeeded" or "failed"
44
- Fact type: task started on time
45
- Necessity: each task started on at most one time
46
- Fact type: task ended on time
47
- Necessity: each task ended on at most one time
48
- Fact type: task has error message
49
- Necessity: each task has at most one error message
50
- Fact type: task has attempt count
51
- Necessity: each task has exactly one attempt count
52
- Fact type: task has attempt limit
53
- Necessity: each task has exactly one attempt limit
54
- Necessity: each task has an attempt limit that is greater than or equal to 1
55
-
@@ -1,37 +0,0 @@
1
- import type { ValidateFunction } from 'ajv';
2
- import type { AnyObject } from 'pinejs-client-core';
3
- import type * as Db from '../database-layer/db';
4
- import type { PinejsClient } from '../sbvr-api/sbvr-utils';
5
- export declare const taskStatuses: readonly ["queued", "cancelled", "succeeded", "failed"];
6
- export type TaskStatus = (typeof taskStatuses)[number];
7
- export interface Task {
8
- id: number;
9
- created_at: Date;
10
- modified_at: Date;
11
- is_created_by__actor: number;
12
- is_executed_by__handler: string;
13
- is_executed_with__parameter_set: object | null;
14
- is_scheduled_with__cron_expression: string | null;
15
- is_scheduled_to_execute_on__time: Date | null;
16
- status: TaskStatus;
17
- started_on__time: Date | null;
18
- ended_on__time: Date | null;
19
- error_message: string | null;
20
- attempt_count: number;
21
- attempt_limit: number;
22
- }
23
- export type PartialTask = Pick<Task, 'id' | 'is_created_by__actor' | 'is_executed_by__handler' | 'is_executed_with__parameter_set' | 'is_scheduled_with__cron_expression' | 'attempt_count' | 'attempt_limit'>;
24
- export interface TaskArgs {
25
- api: PinejsClient;
26
- params: AnyObject;
27
- tx: Db.Tx;
28
- }
29
- export type TaskResponse = Promise<{
30
- status: TaskStatus;
31
- error?: string;
32
- }>;
33
- export interface TaskHandler {
34
- name: string;
35
- fn: (options: TaskArgs) => TaskResponse;
36
- validate?: ValidateFunction;
37
- }
@@ -1,10 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.taskStatuses = void 0;
4
- exports.taskStatuses = [
5
- 'queued',
6
- 'cancelled',
7
- 'succeeded',
8
- 'failed',
9
- ];
10
- //# sourceMappingURL=types.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/tasks/types.ts"],"names":[],"mappings":";;;AAKa,QAAA,YAAY,GAAG;IAC3B,QAAQ;IACR,WAAW;IACX,WAAW;IACX,QAAQ;CACC,CAAC"}
@@ -1,16 +0,0 @@
1
- import { PinejsClient } from '../sbvr-api/sbvr-utils';
2
- import type { TaskHandler } from './types';
3
- export declare class Worker {
4
- private readonly client;
5
- handlers: Record<string, TaskHandler>;
6
- private readonly concurrency;
7
- private readonly interval;
8
- private executing;
9
- constructor(client: PinejsClient);
10
- private canExecute;
11
- private execute;
12
- private finalize;
13
- private getNextAttemptTime;
14
- private poll;
15
- start(): void;
16
- }
@@ -1,226 +0,0 @@
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
- 'is created by-actor': 'is_created_by__actor',
42
- })
43
- .map(([key, value]) => `t."${key}" AS "${value}"`)
44
- .join(', ');
45
- class Worker {
46
- client;
47
- handlers = {};
48
- concurrency;
49
- interval;
50
- executing = 0;
51
- constructor(client) {
52
- this.client = client;
53
- this.concurrency = env_1.tasks.queueConcurrency;
54
- this.interval = env_1.tasks.queueIntervalMS;
55
- }
56
- canExecute() {
57
- return (this.executing < this.concurrency && Object.keys(this.handlers).length > 0);
58
- }
59
- async execute(task, tx) {
60
- this.executing++;
61
- try {
62
- const handler = this.handlers[task.is_executed_by__handler];
63
- const startedOnTime = new Date();
64
- if (handler == null) {
65
- await this.finalize(tx, task, startedOnTime, 'failed', 'Matching task handler not found');
66
- return;
67
- }
68
- if (handler.validate != null &&
69
- !handler.validate(task.is_executed_with__parameter_set)) {
70
- await this.finalize(tx, task, startedOnTime, 'failed', `Invalid parameter set: ${common_1.ajv.errorsText(handler.validate.errors)}`);
71
- return;
72
- }
73
- let status = 'queued';
74
- let error;
75
- try {
76
- await module_1.sbvrUtils.db.transaction(async (handlerTx) => {
77
- const results = await handler.fn({
78
- api: new sbvr_utils_1.PinejsClient({
79
- passthrough: {
80
- tx: handlerTx,
81
- },
82
- }),
83
- params: task.is_executed_with__parameter_set ?? {},
84
- tx: handlerTx,
85
- });
86
- status = results.status;
87
- error = results.error;
88
- if (results.status !== 'succeeded' && !handlerTx.isClosed()) {
89
- await handlerTx.rollback();
90
- }
91
- });
92
- }
93
- catch (err) {
94
- if (!(err instanceof db_1.TransactionClosedError)) {
95
- throw err;
96
- }
97
- }
98
- finally {
99
- await this.finalize(tx, task, startedOnTime, status, error);
100
- }
101
- }
102
- catch (err) {
103
- console.error(`Failed to execute task ${task.id} with handler ${task.is_executed_by__handler}:`, err);
104
- process.exit(1);
105
- }
106
- finally {
107
- this.executing--;
108
- }
109
- }
110
- async finalize(tx, task, startedOnTime, status, errorMessage) {
111
- const attemptCount = task.attempt_count + 1;
112
- const body = {
113
- started_on__time: startedOnTime,
114
- ended_on__time: new Date(),
115
- status,
116
- attempt_count: attemptCount,
117
- ...(errorMessage != null && { error_message: errorMessage }),
118
- };
119
- if (status === 'failed' && attemptCount < task.attempt_limit) {
120
- body.status = 'queued';
121
- body.is_scheduled_to_execute_on__time =
122
- this.getNextAttemptTime(attemptCount);
123
- }
124
- await this.client.patch({
125
- resource: 'task',
126
- passthrough: {
127
- tx,
128
- req: permissions.root,
129
- },
130
- id: task.id,
131
- body,
132
- });
133
- if (['failed', 'succeeded'].includes(body.status) &&
134
- task.is_scheduled_with__cron_expression != null) {
135
- await this.client.post({
136
- resource: 'task',
137
- passthrough: {
138
- tx,
139
- req: permissions.root,
140
- },
141
- options: {
142
- returnResource: false,
143
- },
144
- body: {
145
- attempt_limit: task.attempt_limit,
146
- is_created_by__actor: task.is_created_by__actor,
147
- is_executed_by__handler: task.is_executed_by__handler,
148
- is_executed_with__parameter_set: task.is_executed_with__parameter_set,
149
- is_scheduled_with__cron_expression: task.is_scheduled_with__cron_expression,
150
- },
151
- });
152
- }
153
- }
154
- getNextAttemptTime(attempt) {
155
- const delay = Math.ceil(Math.exp(Math.min(10, attempt)));
156
- return new Date(Date.now() + delay);
157
- }
158
- poll() {
159
- let executed = false;
160
- void (async () => {
161
- try {
162
- if (!this.canExecute()) {
163
- return;
164
- }
165
- const handlerNames = Object.keys(this.handlers);
166
- await module_1.sbvrUtils.db.transaction(async (tx) => {
167
- const result = await tx.executeSql(`SELECT ${selectColumns}
168
- FROM task AS t
169
- WHERE
170
- t."is executed by-handler" IN (${handlerNames.map((_, index) => `$${index + 1}`).join(', ')}) AND
171
- t."status" = 'queued' AND
172
- t."attempt count" <= t."attempt limit" AND
173
- (
174
- t."is scheduled to execute on-time" IS NULL OR
175
- t."is scheduled to execute on-time" <= CURRENT_TIMESTAMP + $${handlerNames.length + 1} * INTERVAL '1 SECOND'
176
- )
177
- ORDER BY
178
- t."is scheduled to execute on-time" ASC,
179
- t."id" ASC
180
- LIMIT $${handlerNames.length + 2}
181
- FOR UPDATE SKIP LOCKED`, [
182
- ...handlerNames,
183
- Math.ceil(this.interval / 1000),
184
- Math.max(this.concurrency - this.executing, 0),
185
- ]);
186
- if (result.rows.length === 0) {
187
- return;
188
- }
189
- await Promise.all(result.rows.map(async (row) => {
190
- await this.execute(row, tx);
191
- }));
192
- executed = true;
193
- });
194
- }
195
- catch (err) {
196
- console.error('Failed polling for tasks:', err);
197
- }
198
- finally {
199
- if (!executed) {
200
- await (0, promises_1.setTimeout)(this.interval);
201
- }
202
- this.poll();
203
- }
204
- })();
205
- }
206
- start() {
207
- if (module_1.sbvrUtils.db.engine !== 'postgres' || module_1.sbvrUtils.db.on == null) {
208
- throw new Error('Database does not support tasks, giving up on starting worker');
209
- }
210
- module_1.sbvrUtils.db.on('notification', async (msg) => {
211
- if (this.canExecute()) {
212
- await module_1.sbvrUtils.db.transaction(async (tx) => {
213
- const result = await tx.executeSql(`SELECT ${selectColumns} FROM task AS t WHERE id = $1 FOR UPDATE SKIP LOCKED`, [msg.payload]);
214
- if (result.rows.length > 0) {
215
- await this.execute(result.rows[0], tx);
216
- }
217
- });
218
- }
219
- }, {
220
- channel: common_1.channel,
221
- });
222
- this.poll();
223
- }
224
- }
225
- exports.Worker = Worker;
226
- //# sourceMappingURL=worker.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../src/tasks/worker.ts"],"names":[],"mappings":"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"}
@@ -1,9 +0,0 @@
1
- import Ajv from 'ajv';
2
-
3
- // Root path for the tasks API
4
- export const apiRoot = 'tasks';
5
-
6
- // Channel name for task insert notifications
7
- export const channel = 'pinejs$task_insert';
8
-
9
- export const ajv = new Ajv();
@@ -1,155 +0,0 @@
1
- import type { Schema } from 'ajv';
2
- import * as cronParser from 'cron-parser';
3
- import { tasks as tasksEnv } from '../config-loader/env';
4
- import type * as Db from '../database-layer/db';
5
- import { BadRequestError } from '../sbvr-api/errors';
6
- import { addPureHook } from '../sbvr-api/hooks';
7
- import * as sbvrUtils from '../sbvr-api/sbvr-utils';
8
- import type { ConfigLoader } from '../server-glue/module';
9
- import { ajv, apiRoot, channel } from './common';
10
- import type { TaskHandler } from './types';
11
- import { Worker } from './worker';
12
-
13
- export * from './types';
14
-
15
- // eslint-disable-next-line @typescript-eslint/no-var-requires
16
- const modelText: string = require('./tasks.sbvr');
17
-
18
- // Create trigger for handling new tasks
19
- // Create index for polling tasks table
20
- const initSql = `
21
- CREATE OR REPLACE FUNCTION notify_task_insert()
22
- RETURNS TRIGGER AS $$
23
- BEGIN
24
- PERFORM pg_notify('${channel}', NEW.id::text);
25
- RETURN NEW;
26
- END;
27
- $$ LANGUAGE plpgsql;
28
-
29
- CREATE OR REPLACE TRIGGER task_insert_trigger
30
- AFTER INSERT ON task
31
- FOR EACH ROW WHEN (NEW.status = 'queued' AND NEW."is scheduled to execute on-time" IS NULL)
32
- EXECUTE FUNCTION notify_task_insert();
33
-
34
- CREATE INDEX IF NOT EXISTS idx_task_poll ON task USING btree (
35
- "is executed by-handler",
36
- "is scheduled to execute on-time" ASC,
37
- "id" ASC
38
- ) WHERE status = 'queued';
39
- `;
40
-
41
- export const config: ConfigLoader.Config = {
42
- models: [
43
- {
44
- modelName: apiRoot,
45
- apiRoot,
46
- modelText,
47
- customServerCode: exports,
48
- initSql,
49
- },
50
- ],
51
- };
52
-
53
- let worker: Worker | null = null;
54
- export async function setup(db: Db.Database): Promise<void> {
55
- // Async task functionality is only supported on Postgres
56
- if (db.engine !== 'postgres') {
57
- console.warn('Skipping task setup as database not supported');
58
- return;
59
- }
60
-
61
- const client = sbvrUtils.api[apiRoot];
62
- worker = new Worker(client);
63
-
64
- // Add resource hooks
65
- addPureHook('POST', apiRoot, 'task', {
66
- POSTPARSE: async ({ req, request }) => {
67
- // Set the actor
68
- request.values.is_created_by__actor =
69
- req.user?.actor ?? req.apiKey?.actor;
70
- if (request.values.is_created_by__actor == null) {
71
- throw new BadRequestError(
72
- 'Creating tasks with missing actor on req is not allowed',
73
- );
74
- }
75
-
76
- // Set defaults
77
- request.values.status = 'queued';
78
- request.values.attempt_count = 0;
79
- request.values.attempt_limit ??= 1;
80
-
81
- // Set scheduled start time using cron expression if provided
82
- if (
83
- request.values.is_scheduled_with__cron_expression != null &&
84
- request.values.is_scheduled_to_execute_on__time == null
85
- ) {
86
- try {
87
- request.values.is_scheduled_to_execute_on__time = cronParser
88
- .parseExpression(request.values.is_scheduled_with__cron_expression)
89
- .next()
90
- .toDate()
91
- .toISOString();
92
- } catch {
93
- throw new BadRequestError(
94
- `Invalid cron expression: ${request.values.is_scheduled_with__cron_expression}`,
95
- );
96
- }
97
- }
98
-
99
- // Assert that the provided start time is far enough in the future
100
- if (request.values.is_scheduled_to_execute_on__time != null) {
101
- const now = new Date(Date.now() + tasksEnv.queueIntervalMS);
102
- const startTime = new Date(
103
- request.values.is_scheduled_to_execute_on__time,
104
- );
105
- if (startTime < now) {
106
- throw new BadRequestError(
107
- `Task scheduled start time must be greater than ${tasksEnv.queueIntervalMS} milliseconds in the future`,
108
- );
109
- }
110
- }
111
-
112
- // Assert that the requested handler exists
113
- const handlerName = request.values.is_executed_by__handler;
114
- if (handlerName == null) {
115
- throw new BadRequestError(`Must specify a task handler to execute`);
116
- }
117
- const handler = worker?.handlers[handlerName];
118
- if (handler == null) {
119
- throw new BadRequestError(
120
- `No task handler with name '${handlerName}' registered`,
121
- );
122
- }
123
-
124
- // Assert that the provided parameter set is valid
125
- if (handler.validate != null) {
126
- if (!handler.validate(request.values.is_executed_with__parameter_set)) {
127
- throw new BadRequestError(
128
- `Invalid parameter set: ${ajv.errorsText(handler.validate.errors)}`,
129
- );
130
- }
131
- }
132
- },
133
- });
134
- worker.start();
135
- }
136
-
137
- // Register a task handler
138
- export function addTaskHandler(
139
- name: string,
140
- fn: TaskHandler['fn'],
141
- schema?: Schema,
142
- ): void {
143
- if (worker == null) {
144
- throw new Error('Database does not support tasks');
145
- }
146
-
147
- if (worker.handlers[name] != null) {
148
- throw new Error(`Task handler with name '${name}' already registered`);
149
- }
150
- worker.handlers[name] = {
151
- name,
152
- fn,
153
- validate: schema != null ? ajv.compile(schema) : undefined,
154
- };
155
- }
@@ -1,55 +0,0 @@
1
- Vocabulary: tasks
2
-
3
- Term: id
4
- Concept Type: Big Serial (Type)
5
- Term: actor
6
- Concept Type: Integer (Type)
7
- Term: attempt count
8
- Concept Type: Integer (Type)
9
- Term: attempt limit
10
- Concept Type: Integer (Type)
11
- Term: cron expression
12
- Concept Type: Short Text (Type)
13
- Term: error message
14
- Concept Type: Short Text (Type)
15
- Term: handler
16
- Concept Type: Short Text (Type)
17
- Term: key
18
- Concept Type: Short Text (Type)
19
- Term: parameter set
20
- Concept Type: JSON (Type)
21
- Term: status
22
- Concept Type: Short Text (Type)
23
- Term: time
24
- Concept Type: Date Time (Type)
25
-
26
- Term: task
27
- Fact type: task has id
28
- Necessity: each task has exactly one id
29
- Fact type: task has key
30
- Necessity: each task has at most one key
31
- Fact type: task is created by actor
32
- Necessity: each task is created by exactly one actor
33
- Fact type: task is executed by handler
34
- Necessity: each task is executed by exactly one handler
35
- Fact type: task is executed with parameter set
36
- Necessity: each task is executed with at most one parameter set
37
- Fact type: task is scheduled with cron expression
38
- Necessity: each task is scheduled with at most one cron expression
39
- Fact type: task is scheduled to execute on time
40
- Necessity: each task is scheduled to execute on at most one time
41
- Fact type: task has status
42
- Necessity: each task has exactly one status
43
- Definition: "queued" or "cancelled" or "succeeded" or "failed"
44
- Fact type: task started on time
45
- Necessity: each task started on at most one time
46
- Fact type: task ended on time
47
- Necessity: each task ended on at most one time
48
- Fact type: task has error message
49
- Necessity: each task has at most one error message
50
- Fact type: task has attempt count
51
- Necessity: each task has exactly one attempt count
52
- Fact type: task has attempt limit
53
- Necessity: each task has exactly one attempt limit
54
- Necessity: each task has an attempt limit that is greater than or equal to 1
55
-
@@ -1,56 +0,0 @@
1
- import type { ValidateFunction } from 'ajv';
2
- import type { AnyObject } from 'pinejs-client-core';
3
- import type * as Db from '../database-layer/db';
4
- import type { PinejsClient } from '../sbvr-api/sbvr-utils';
5
-
6
- export const taskStatuses = [
7
- 'queued',
8
- 'cancelled',
9
- 'succeeded',
10
- 'failed',
11
- ] as const;
12
- export type TaskStatus = (typeof taskStatuses)[number];
13
- export interface Task {
14
- id: number;
15
- created_at: Date;
16
- modified_at: Date;
17
- is_created_by__actor: number;
18
- is_executed_by__handler: string;
19
- is_executed_with__parameter_set: object | null;
20
- is_scheduled_with__cron_expression: string | null;
21
- is_scheduled_to_execute_on__time: Date | null;
22
- status: TaskStatus;
23
- started_on__time: Date | null;
24
- ended_on__time: Date | null;
25
- error_message: string | null;
26
- attempt_count: number;
27
- attempt_limit: number;
28
- }
29
-
30
- export type PartialTask = Pick<
31
- Task,
32
- | 'id'
33
- | 'is_created_by__actor'
34
- | 'is_executed_by__handler'
35
- | 'is_executed_with__parameter_set'
36
- | 'is_scheduled_with__cron_expression'
37
- | 'attempt_count'
38
- | 'attempt_limit'
39
- >;
40
-
41
- export interface TaskArgs {
42
- api: PinejsClient;
43
- params: AnyObject;
44
- tx: Db.Tx;
45
- }
46
-
47
- export type TaskResponse = Promise<{
48
- status: TaskStatus;
49
- error?: string;
50
- }>;
51
-
52
- export interface TaskHandler {
53
- name: string;
54
- fn: (options: TaskArgs) => TaskResponse;
55
- validate?: ValidateFunction;
56
- }