@emartech/program-executor 3.11.0 → 3.12.0
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.
- package/.env.example +9 -1
- package/.github/CODEOWNERS +1 -1
- package/.github/dependabot.yml +0 -7
- package/.github/workflows/main.yaml +17 -5
- package/docker-compose.yml +11 -0
- package/eslint.config.js +1 -5
- package/jest.config.js +13 -0
- package/package.json +13 -16
- package/src/create-test-databases.js +84 -0
- package/src/execution-time-exceeded-error/{index.spec.js → index.test.js} +3 -3
- package/src/graphql/{schema.spec.js → schema.test.js} +69 -24
- package/src/ignorable-error/{index.spec.js → index.test.js} +5 -5
- package/src/index-pubsub-e2e.test.js +94 -0
- package/src/index-pubsub.js +78 -0
- package/src/index-pubsub.test.js +154 -0
- package/src/{index.spec.js → index.test.js} +42 -38
- package/src/job-data-handler/{index.spec.js → index.test.js} +15 -21
- package/src/program-executor-processor/{index.spec.js → index.test.js} +62 -49
- package/src/program-handler/index.js +7 -1
- package/src/program-handler/{index.spec.js → index.test.js} +58 -34
- package/src/queue-manager/{index.spec.js → index.test.js} +16 -19
- package/src/queue-manager-pubsub/index.js +33 -0
- package/src/queue-manager-pubsub/index.test.js +44 -0
- package/src/repositories/{programs.spec.js → programs.test.js} +54 -37
- package/src/retryable-error/{index.spec.js → index.test.js} +5 -5
- package/src/runid-generator/{index.spec.js → index.test.js} +2 -2
- package/src/test-helper/get-test-db-config.js +29 -0
- package/src/testSetup.js +23 -0
- package/src/testTeardown.js +13 -0
- package/src/setup.spec.js +0 -50
package/.env.example
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
DEBUG=program-executor*
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
POSTGRES_USER=developer
|
|
4
|
+
POSTGRES_HOST=localhost
|
|
5
|
+
POSTGRES_PASSWORD=development_secret
|
|
6
|
+
POSTGRES_PORT=5435
|
|
7
|
+
POSTGRES_DATABASE=programexecutor
|
|
8
|
+
|
|
9
|
+
PUBSUB_EMULATOR_HOST=0.0.0.0:8085
|
|
10
|
+
PUBSUB_PROJECT_ID=some-project-id
|
|
11
|
+
PUBSUB_TIMEOUT=1000
|
package/.github/CODEOWNERS
CHANGED
package/.github/dependabot.yml
CHANGED
|
@@ -19,13 +19,6 @@ updates:
|
|
|
19
19
|
interval: "weekly"
|
|
20
20
|
day: "monday"
|
|
21
21
|
time: "09:00"
|
|
22
|
-
# Add assignees
|
|
23
|
-
reviewers:
|
|
24
|
-
- "IvanFroehlich"
|
|
25
|
-
- "MauroGreco"
|
|
26
|
-
- "oliverweisenburger"
|
|
27
|
-
- "dimirovn"
|
|
28
|
-
- "ianhelmrich"
|
|
29
22
|
commit-message:
|
|
30
23
|
# Prefix all commit messages with "npm: "
|
|
31
24
|
prefix: "[dependabot]npm"
|
|
@@ -11,8 +11,6 @@ env:
|
|
|
11
11
|
jobs:
|
|
12
12
|
test:
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
|
-
container: node:18-alpine3.16
|
|
15
|
-
|
|
16
14
|
services:
|
|
17
15
|
postgres:
|
|
18
16
|
image: postgres:14-alpine
|
|
@@ -29,12 +27,19 @@ jobs:
|
|
|
29
27
|
- 5432:5432
|
|
30
28
|
|
|
31
29
|
steps:
|
|
30
|
+
- name: 'Install Cloud SDK'
|
|
31
|
+
uses: google-github-actions/setup-gcloud@v2.1.4
|
|
32
|
+
with:
|
|
33
|
+
install_components: 'beta,pubsub-emulator'
|
|
34
|
+
- name: 'start pubsub emulator'
|
|
35
|
+
run: |
|
|
36
|
+
gcloud beta emulators pubsub start --project=some-project-id --host-port=0.0.0.0:8085 &
|
|
32
37
|
- name: Check out repository
|
|
33
38
|
uses: actions/checkout@v4
|
|
34
|
-
- name: Use
|
|
39
|
+
- name: Use Node 20.18.0 version
|
|
35
40
|
uses: actions/setup-node@v3
|
|
36
41
|
with:
|
|
37
|
-
node-version: "
|
|
42
|
+
node-version: "20.18.0"
|
|
38
43
|
- name: Set NPM token
|
|
39
44
|
run: npm config set '//registry.npmjs.org/:_authToken' "${{ env.NPM_TOKEN }}"
|
|
40
45
|
- name: npm dependencies
|
|
@@ -42,7 +47,14 @@ jobs:
|
|
|
42
47
|
- name: npm test
|
|
43
48
|
run: npm test
|
|
44
49
|
env:
|
|
45
|
-
|
|
50
|
+
POSTGRES_USER: developer
|
|
51
|
+
POSTGRES_HOST: localhost
|
|
52
|
+
POSTGRES_PASSWORD: development_secret
|
|
53
|
+
POSTGRES_PORT: 5432
|
|
54
|
+
POSTGRES_DATABASE: programexecutor
|
|
55
|
+
PUBSUB_EMULATOR_HOST: 0.0.0.0:8085
|
|
56
|
+
PUBSUB_PROJECT_ID: some-project-id
|
|
57
|
+
PUBSUB_TIMEOUT: 2000
|
|
46
58
|
|
|
47
59
|
deploy:
|
|
48
60
|
name: deploy
|
package/docker-compose.yml
CHANGED
|
@@ -7,3 +7,14 @@ services:
|
|
|
7
7
|
- POSTGRES_DB=programexecutor
|
|
8
8
|
ports:
|
|
9
9
|
- 5435:5432
|
|
10
|
+
pub-sub-emulator:
|
|
11
|
+
image: google/cloud-sdk:526.0.0-emulators
|
|
12
|
+
command: ["gcloud", "beta", "emulators", "pubsub", "start", "--host-port=0.0.0.0:8085", "--project=some-project-id"]
|
|
13
|
+
ports:
|
|
14
|
+
- "8085:8085"
|
|
15
|
+
healthcheck:
|
|
16
|
+
test: ["CMD", "curl", "-f", "http://localhost:8085/v1/projects/test/schemas"]
|
|
17
|
+
interval: 1m30s
|
|
18
|
+
timeout: 10s
|
|
19
|
+
retries: 3
|
|
20
|
+
start_period: 40s
|
package/eslint.config.js
CHANGED
|
@@ -14,14 +14,10 @@ const compat = new FlatCompat({
|
|
|
14
14
|
module.exports = [
|
|
15
15
|
...compat.extends('emarsys'),
|
|
16
16
|
{
|
|
17
|
-
plugins: {
|
|
18
|
-
mocha
|
|
19
|
-
},
|
|
20
|
-
|
|
21
17
|
languageOptions: {
|
|
22
18
|
globals: {
|
|
23
19
|
...globals.node,
|
|
24
|
-
...globals.
|
|
20
|
+
...globals.jest,
|
|
25
21
|
inject: true,
|
|
26
22
|
onmessage: true,
|
|
27
23
|
expect: true
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
rootDir: './',
|
|
6
|
+
testPathIgnorePatterns: ['/node_modules/'],
|
|
7
|
+
testRegex: '\\.(test|e2e)\\.js$',
|
|
8
|
+
clearMocks: true,
|
|
9
|
+
collectCoverage: false,
|
|
10
|
+
coverageReporters: ['json', 'html', 'text', 'lcov'],
|
|
11
|
+
globalSetup: "./src/testSetup.js",
|
|
12
|
+
globalTeardown: './src/testTeardown.js',
|
|
13
|
+
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"description": "",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"test": "
|
|
6
|
+
"test": "npm run create-test-dbs && jest",
|
|
7
|
+
"jest-watch": "jest --watch",
|
|
8
|
+
"create-test-dbs": "NODE_ENV=test node ./src/create-test-databases.js",
|
|
7
9
|
"code-style": "eslint '**/*.js' --ignore-pattern node_modules/",
|
|
8
10
|
"semantic-release": "semantic-release --branches main"
|
|
9
11
|
},
|
|
@@ -17,30 +19,25 @@
|
|
|
17
19
|
"author": "team-shopify@emarsys.com",
|
|
18
20
|
"license": "ISC",
|
|
19
21
|
"dependencies": {
|
|
20
|
-
"@emartech/
|
|
22
|
+
"@emartech/json-logger": "3.4.0",
|
|
23
|
+
"@emartech/pubsub-client-js": "^1.2.5",
|
|
24
|
+
"@emartech/rabbitmq-client": "5.9.1",
|
|
21
25
|
"camelcase-keys": "^6.2.2",
|
|
22
|
-
"graphql": "^
|
|
26
|
+
"graphql": "^16.11.0"
|
|
23
27
|
},
|
|
24
28
|
"engines": {
|
|
25
29
|
"node": "20.18.0"
|
|
26
30
|
},
|
|
27
31
|
"devDependencies": {
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"chai-string": "1.5.0",
|
|
31
|
-
"chai-subset": "1.6.0",
|
|
32
|
-
"dotenv": "^16.4.7",
|
|
33
|
-
"eslint": "^9.17.0",
|
|
32
|
+
"dotenv": "^17.2.1",
|
|
33
|
+
"eslint": "^9.34.0",
|
|
34
34
|
"eslint-config-emarsys": "5.1.0",
|
|
35
|
-
"eslint-plugin-mocha": "^10.5.0",
|
|
36
35
|
"eslint-plugin-no-only-tests": "^3.3.0",
|
|
37
36
|
"eslint-plugin-security": "^3.0.1",
|
|
37
|
+
"jest": "29.0.0",
|
|
38
38
|
"knex": "^3.1.0",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"semantic-release": "^24.2.0",
|
|
42
|
-
"sinon": "19.0.2",
|
|
43
|
-
"sinon-chai": "^3.7.0"
|
|
39
|
+
"pg": "^8.16.3",
|
|
40
|
+
"semantic-release": "^24.2.0"
|
|
44
41
|
},
|
|
45
|
-
"version": "3.
|
|
42
|
+
"version": "3.12.0"
|
|
46
43
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('dotenv').config({ silent: true });
|
|
4
|
+
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const { db } = require('./test-helper/get-test-db-config');
|
|
8
|
+
|
|
9
|
+
const { Client } = require('pg');
|
|
10
|
+
|
|
11
|
+
const cpuCount = os.cpus() ? os.cpus().length : 1;
|
|
12
|
+
|
|
13
|
+
const defaultDbName = db.connection.database;
|
|
14
|
+
|
|
15
|
+
const getConnection = function (databaseName) {
|
|
16
|
+
return {
|
|
17
|
+
...db.connection,
|
|
18
|
+
database: databaseName
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const recreateTestDbs = async () => {
|
|
23
|
+
const client = new Client(getConnection('template1'));
|
|
24
|
+
|
|
25
|
+
await client.connect();
|
|
26
|
+
|
|
27
|
+
for (let index = 0; index <= cpuCount; index++) {
|
|
28
|
+
console.log(`Creating test database ${index}`);
|
|
29
|
+
try {
|
|
30
|
+
const pgUser = process.env.POSTGRES_USER || 'developer';
|
|
31
|
+
await client.query(`DROP DATABASE IF EXISTS ${defaultDbName}_${index}`);
|
|
32
|
+
await client.query(`CREATE DATABASE ${defaultDbName}_${index} TEMPLATE ${defaultDbName}`);
|
|
33
|
+
await client.query(`ALTER DATABASE ${defaultDbName}_${index} OWNER TO ${pgUser}`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.log(error.message);
|
|
36
|
+
await client.end();
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
await client.end();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getLatestMigrationId = async (databaseName) => {
|
|
44
|
+
const client = new Client(getConnection(databaseName));
|
|
45
|
+
await client.connect();
|
|
46
|
+
|
|
47
|
+
let result;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
result = await client.query('SELECT MAX(id) FROM knex_migrations');
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.log(`Migrations not found in ${databaseName ? databaseName : defaultDbName}...`);
|
|
53
|
+
await client.end();
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await client.end();
|
|
58
|
+
return result.rows[0].max;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const isRecreationNeeded = async () => {
|
|
62
|
+
try {
|
|
63
|
+
const [mainMigrationId, secondaryMigrationId] = await Promise.all([
|
|
64
|
+
getLatestMigrationId(defaultDbName),
|
|
65
|
+
getLatestMigrationId(`${defaultDbName}_${cpuCount}`)
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
return mainMigrationId !== secondaryMigrationId;
|
|
69
|
+
// eslint-disable-next-line no-unused-vars
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
(async () => {
|
|
76
|
+
const recreate = await isRecreationNeeded();
|
|
77
|
+
|
|
78
|
+
if (recreate) {
|
|
79
|
+
await recreateTestDbs();
|
|
80
|
+
} else {
|
|
81
|
+
console.log('Everything is fine, using previously generated dbs...');
|
|
82
|
+
}
|
|
83
|
+
process.exit(0);
|
|
84
|
+
})();
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const ExecutionTimeExceededError = require('
|
|
3
|
+
const ExecutionTimeExceededError = require('.');
|
|
4
4
|
|
|
5
5
|
describe('ExecutionTimeExceededError', () => {
|
|
6
6
|
it('should have a executionTimeExceeded property set to true', () => {
|
|
7
7
|
try {
|
|
8
8
|
throw new ExecutionTimeExceededError();
|
|
9
9
|
} catch (error) {
|
|
10
|
-
expect(error.executionTimeExceeded).
|
|
10
|
+
expect(error.executionTimeExceeded).toBe(true);
|
|
11
11
|
}
|
|
12
12
|
});
|
|
13
13
|
|
|
@@ -15,7 +15,7 @@ describe('ExecutionTimeExceededError', () => {
|
|
|
15
15
|
try {
|
|
16
16
|
throw new ExecutionTimeExceededError('Something bad happened!');
|
|
17
17
|
} catch (error) {
|
|
18
|
-
expect(error.message).
|
|
18
|
+
expect(error.message).toEqual('Something bad happened!');
|
|
19
19
|
}
|
|
20
20
|
});
|
|
21
21
|
});
|
|
@@ -3,8 +3,28 @@
|
|
|
3
3
|
const { graphql } = require('graphql');
|
|
4
4
|
const ProgramsRepository = require('../repositories/programs');
|
|
5
5
|
const schema = require('./schema');
|
|
6
|
+
const knex = require('knex');
|
|
7
|
+
const DbCleaner = require('../test-helper/db-cleaner');
|
|
8
|
+
const { db } = require('../test-helper/get-test-db-config');
|
|
6
9
|
|
|
7
10
|
describe('schema', function () {
|
|
11
|
+
|
|
12
|
+
let dbConnection;
|
|
13
|
+
|
|
14
|
+
beforeAll(async function () {
|
|
15
|
+
dbConnection = knex({
|
|
16
|
+
client: 'pg',
|
|
17
|
+
connection: db.connection.connString
|
|
18
|
+
});
|
|
19
|
+
await DbCleaner.create(dbConnection).tearDown();
|
|
20
|
+
ProgramsRepository.create(dbConnection, 'programs');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterAll(async function () {
|
|
24
|
+
await DbCleaner.create(dbConnection).tearDown();
|
|
25
|
+
dbConnection.destroy();
|
|
26
|
+
});
|
|
27
|
+
|
|
8
28
|
describe('with empty database', () => {
|
|
9
29
|
it('returns with empty array when table does not exist (error code 42P01)', async function () {
|
|
10
30
|
const query = `
|
|
@@ -14,8 +34,9 @@ describe('schema', function () {
|
|
|
14
34
|
}
|
|
15
35
|
}`;
|
|
16
36
|
|
|
17
|
-
const { data } = await graphql(schema, query,
|
|
18
|
-
|
|
37
|
+
const { data } = await graphql({ schema, source: query, contextValue: { knex: dbConnection,
|
|
38
|
+
tableName: 'programs' } });
|
|
39
|
+
expect(data).toEqual({ programs: [] });
|
|
19
40
|
});
|
|
20
41
|
});
|
|
21
42
|
|
|
@@ -48,8 +69,11 @@ describe('schema', function () {
|
|
|
48
69
|
}
|
|
49
70
|
];
|
|
50
71
|
|
|
72
|
+
|
|
73
|
+
|
|
51
74
|
beforeEach(async function () {
|
|
52
|
-
|
|
75
|
+
await DbCleaner.create(dbConnection).tearDown();
|
|
76
|
+
const programsRepository = ProgramsRepository.create(dbConnection, 'programs');
|
|
53
77
|
for (const program of programs) {
|
|
54
78
|
await programsRepository.save(program);
|
|
55
79
|
}
|
|
@@ -73,33 +97,34 @@ describe('schema', function () {
|
|
|
73
97
|
}
|
|
74
98
|
}`;
|
|
75
99
|
|
|
76
|
-
const { data } = await graphql(schema, query,
|
|
100
|
+
const { data } = await graphql({ schema, source: query, contextValue: { knex: dbConnection,
|
|
101
|
+
tableName: 'programs' } });
|
|
77
102
|
|
|
78
|
-
expect(data.programs[0]).
|
|
103
|
+
expect(data.programs[0]).toEqual(expect.objectContaining({
|
|
79
104
|
...programs[0],
|
|
80
105
|
jobData: JSON.stringify({}),
|
|
81
106
|
programData: JSON.stringify(programs[0].programData)
|
|
82
|
-
});
|
|
83
|
-
expect(data.programs[0].createdAt).not.
|
|
84
|
-
expect(data.programs[0].updatedAt).not.
|
|
107
|
+
}));
|
|
108
|
+
expect(data.programs[0].createdAt).not.toBe(undefined);
|
|
109
|
+
expect(data.programs[0].updatedAt).not.toBe(undefined);
|
|
85
110
|
|
|
86
|
-
expect(data.programs[1]).
|
|
111
|
+
expect(data.programs[1]).toEqual(expect.objectContaining({
|
|
87
112
|
...programs[1],
|
|
88
113
|
jobData: JSON.stringify(programs[1].jobData),
|
|
89
114
|
programData: JSON.stringify(programs[1].programData),
|
|
90
115
|
erroredAt: programs[1].erroredAt.getTime().toString()
|
|
91
|
-
});
|
|
92
|
-
expect(data.programs[1].createdAt).not.
|
|
93
|
-
expect(data.programs[1].updatedAt).not.
|
|
116
|
+
}));
|
|
117
|
+
expect(data.programs[1].createdAt).not.toBe(undefined);
|
|
118
|
+
expect(data.programs[1].updatedAt).not.toBe(undefined);
|
|
94
119
|
|
|
95
|
-
expect(data.programs[2]).
|
|
120
|
+
expect(data.programs[2]).toEqual(expect.objectContaining({
|
|
96
121
|
...programs[2],
|
|
97
122
|
jobData: JSON.stringify(programs[2].jobData),
|
|
98
123
|
programData: JSON.stringify(programs[2].programData),
|
|
99
124
|
finishedAt: programs[2].finishedAt.getTime().toString()
|
|
100
|
-
});
|
|
101
|
-
expect(data.programs[2].createdAt).not.
|
|
102
|
-
expect(data.programs[2].updatedAt).not.
|
|
125
|
+
}));
|
|
126
|
+
expect(data.programs[2].createdAt).not.toBe(undefined);
|
|
127
|
+
expect(data.programs[2].updatedAt).not.toBe(undefined);
|
|
103
128
|
});
|
|
104
129
|
|
|
105
130
|
describe('accepts orderBy input for `id` field', () => {
|
|
@@ -110,10 +135,15 @@ describe('schema', function () {
|
|
|
110
135
|
}
|
|
111
136
|
}`;
|
|
112
137
|
|
|
113
|
-
const { data } = await graphql(
|
|
138
|
+
const { data } = await graphql({
|
|
139
|
+
schema,
|
|
140
|
+
source: query,
|
|
141
|
+
contextValue: { knex: dbConnection,
|
|
142
|
+
tableName: 'programs' }
|
|
143
|
+
});
|
|
114
144
|
const ids = data.programs.map((program) => program.id);
|
|
115
145
|
|
|
116
|
-
expect(ids).
|
|
146
|
+
expect(ids).toEqual(['1', '2', '3']);
|
|
117
147
|
});
|
|
118
148
|
|
|
119
149
|
it('supports ordering by id DESC', async function () {
|
|
@@ -123,10 +153,15 @@ describe('schema', function () {
|
|
|
123
153
|
}
|
|
124
154
|
}`;
|
|
125
155
|
|
|
126
|
-
const { data } = await graphql(
|
|
156
|
+
const { data } = await graphql({
|
|
157
|
+
schema,
|
|
158
|
+
source: query,
|
|
159
|
+
contextValue: { knex: dbConnection,
|
|
160
|
+
tableName: 'programs' }
|
|
161
|
+
});
|
|
127
162
|
const ids = data.programs.map((program) => program.id);
|
|
128
163
|
|
|
129
|
-
expect(ids).
|
|
164
|
+
expect(ids).toEqual(['3', '2', '1']);
|
|
130
165
|
});
|
|
131
166
|
});
|
|
132
167
|
|
|
@@ -138,10 +173,15 @@ describe('schema', function () {
|
|
|
138
173
|
}
|
|
139
174
|
}`;
|
|
140
175
|
|
|
141
|
-
const { data } = await graphql(
|
|
176
|
+
const { data } = await graphql({
|
|
177
|
+
schema,
|
|
178
|
+
source: query,
|
|
179
|
+
contextValue: { knex: dbConnection,
|
|
180
|
+
tableName: 'programs' }
|
|
181
|
+
});
|
|
142
182
|
const ids = data.programs.map((program) => program.id);
|
|
143
183
|
|
|
144
|
-
expect(ids).
|
|
184
|
+
expect(ids).toEqual(['1']);
|
|
145
185
|
});
|
|
146
186
|
|
|
147
187
|
it('supports filtering for programs with step retry count greater than a given input', async function () {
|
|
@@ -151,10 +191,15 @@ describe('schema', function () {
|
|
|
151
191
|
}
|
|
152
192
|
}`;
|
|
153
193
|
|
|
154
|
-
const { data } = await graphql(
|
|
194
|
+
const { data } = await graphql({
|
|
195
|
+
schema,
|
|
196
|
+
source: query,
|
|
197
|
+
contextValue: { knex: dbConnection,
|
|
198
|
+
tableName: 'programs' }
|
|
199
|
+
});
|
|
155
200
|
const ids = data.programs.map((program) => program.id);
|
|
156
201
|
|
|
157
|
-
expect(ids).
|
|
202
|
+
expect(ids).toEqual(['3']);
|
|
158
203
|
});
|
|
159
204
|
});
|
|
160
205
|
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const IgnorableError = require('
|
|
3
|
+
const IgnorableError = require('.');
|
|
4
4
|
|
|
5
5
|
describe('IgnorableError', () => {
|
|
6
6
|
it('should have a retryable property set to true', () => {
|
|
7
7
|
try {
|
|
8
8
|
throw new IgnorableError();
|
|
9
9
|
} catch (error) {
|
|
10
|
-
expect(error.ignorable).
|
|
10
|
+
expect(error.ignorable).toBe(true);
|
|
11
11
|
}
|
|
12
12
|
});
|
|
13
13
|
|
|
@@ -15,8 +15,8 @@ describe('IgnorableError', () => {
|
|
|
15
15
|
try {
|
|
16
16
|
throw new IgnorableError('Something bad happened!', 200);
|
|
17
17
|
} catch (error) {
|
|
18
|
-
expect(error.message).
|
|
19
|
-
expect(error.code).
|
|
18
|
+
expect(error.message).toEqual('Something bad happened!');
|
|
19
|
+
expect(error.code).toEqual(200);
|
|
20
20
|
}
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@ describe('IgnorableError', () => {
|
|
|
24
24
|
try {
|
|
25
25
|
throw new IgnorableError('Something bad happened!');
|
|
26
26
|
} catch (error) {
|
|
27
|
-
expect(IgnorableError.isIgnorable(error)).
|
|
27
|
+
expect(IgnorableError.isIgnorable(error)).toEqual(true);
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ProgramExecutor = require('./index-pubsub');
|
|
4
|
+
const ProgramsRepository = require('./repositories/programs');
|
|
5
|
+
const e2eTestReuse = require('@emartech/pubsub-client-js/e2e/e2e-test-resuse.js');
|
|
6
|
+
|
|
7
|
+
const knex = require('knex');
|
|
8
|
+
const DbCleaner = require('./test-helper/db-cleaner');
|
|
9
|
+
const { db } = require('./test-helper/get-test-db-config');
|
|
10
|
+
|
|
11
|
+
const customerId = 1234;
|
|
12
|
+
const topicName = 'program-executor';
|
|
13
|
+
|
|
14
|
+
const testJobLibrary = {
|
|
15
|
+
firstJob: {},
|
|
16
|
+
secondJob: {}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const waitForMessages = (timeout) => {
|
|
20
|
+
let timeOutLength = timeout;
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, timeOutLength));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('ProgramExecutor', function () {
|
|
25
|
+
let config;
|
|
26
|
+
let dbConnection;
|
|
27
|
+
let mockConstructorSpy;
|
|
28
|
+
let publisher;
|
|
29
|
+
|
|
30
|
+
beforeAll(async function () {
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
dbConnection = knex({
|
|
33
|
+
client: 'pg',
|
|
34
|
+
connection: db.connection.connString
|
|
35
|
+
});
|
|
36
|
+
await DbCleaner.create(dbConnection).tearDown();
|
|
37
|
+
ProgramsRepository.create(dbConnection, 'programs');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterAll(async function () {
|
|
41
|
+
await DbCleaner.create(dbConnection).tearDown();
|
|
42
|
+
dbConnection.destroy();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
afterEach(async () => {
|
|
47
|
+
await e2eTestReuse.afterEach(mockConstructorSpy, publisher);
|
|
48
|
+
|
|
49
|
+
publisher = undefined;
|
|
50
|
+
jest.clearAllMocks();
|
|
51
|
+
jest.resetModules();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
beforeEach(async function () {
|
|
56
|
+
|
|
57
|
+
config = {
|
|
58
|
+
knex: dbConnection,
|
|
59
|
+
tableName: 'programs',
|
|
60
|
+
topicName: topicName,
|
|
61
|
+
projectId: 'some-project-id'
|
|
62
|
+
};
|
|
63
|
+
const result = await e2eTestReuse.beforeEach(topicName);
|
|
64
|
+
mockConstructorSpy = result.mockConstructorSpy;
|
|
65
|
+
publisher = result.publisher;
|
|
66
|
+
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('#Program Executor', function () {
|
|
70
|
+
it('should execute program and maintain status in DB', async function () {
|
|
71
|
+
const runId = await ProgramExecutor.create(config).createProgram(
|
|
72
|
+
{
|
|
73
|
+
programData: {
|
|
74
|
+
customerId: customerId
|
|
75
|
+
},
|
|
76
|
+
jobs: ['current_program', 'next_program']
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
80
|
+
|
|
81
|
+
await waitForMessages(1000);
|
|
82
|
+
|
|
83
|
+
const result = await dbConnection('programs').where({ run_id: runId }).first();
|
|
84
|
+
|
|
85
|
+
expect(result.run_id).toBe(runId);
|
|
86
|
+
expect(result.jobs).toEqual(['current_program', 'next_program']);
|
|
87
|
+
expect(result.step).toEqual(1);
|
|
88
|
+
expect(result.errored_at).toEqual(null);
|
|
89
|
+
expect(result.finished_at).not.toEqual(null);
|
|
90
|
+
expect(result.program_data).toEqual({ customerId: 1234 });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const consumer = require('@emartech/pubsub-client-js').Consumer;
|
|
4
|
+
const EventEmitter = require('events');
|
|
5
|
+
|
|
6
|
+
const ProgramHandler = require('./program-handler');
|
|
7
|
+
const ProgramsRepository = require('./repositories/programs');
|
|
8
|
+
const QueueManager = require('./queue-manager-pubsub');
|
|
9
|
+
|
|
10
|
+
class ProgramExecutor extends EventEmitter {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} config
|
|
13
|
+
* @param {object} config.knex - Connected Knex instance
|
|
14
|
+
* @param {string} config.projectId - GCP Project ID
|
|
15
|
+
* @param {string} config.tableName - Table name for bookkeeping
|
|
16
|
+
* @param {string} config.topicName - Topic name to publish to
|
|
17
|
+
*/
|
|
18
|
+
constructor(config) {
|
|
19
|
+
super();
|
|
20
|
+
this._config = config;
|
|
21
|
+
this._programsRepository = ProgramsRepository.create(config.knex, config.tableName);
|
|
22
|
+
this._queueManager = QueueManager.create(config.topicName, config.projectId);
|
|
23
|
+
this._programHandler = ProgramHandler.create(this._programsRepository, this._queueManager);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {object} data
|
|
28
|
+
* @param {object} data.programData
|
|
29
|
+
* @param {array} data.jobs
|
|
30
|
+
* @param {object} data.jobsData
|
|
31
|
+
*/
|
|
32
|
+
createProgram(data) {
|
|
33
|
+
return this._programHandler.createProgram(data);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
processPrograms(jobLibrary) {
|
|
37
|
+
const eventEmitter = this; // eslint-disable-line consistent-this
|
|
38
|
+
|
|
39
|
+
const programExecutorProcessor = require('./program-executor-processor').create(
|
|
40
|
+
this._programHandler,
|
|
41
|
+
this._queueManager,
|
|
42
|
+
jobLibrary
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
consumer
|
|
46
|
+
.create(this._config.topicName,
|
|
47
|
+
{
|
|
48
|
+
logger: `${this._config.topicName}-consumer`,
|
|
49
|
+
MaxStreams: 1,
|
|
50
|
+
MaxMessages: 1,
|
|
51
|
+
onMessage: async (message) => {
|
|
52
|
+
try {
|
|
53
|
+
await programExecutorProcessor.process(message);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
eventEmitter.emit('programError', { message, error });
|
|
56
|
+
error.message = error.message.substring(0, 255);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{ projectId: this._config.projectId }
|
|
62
|
+
)
|
|
63
|
+
.process();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {object} config
|
|
68
|
+
* @param {object} config.knex - Connected Knex instance
|
|
69
|
+
* @param {string} config.projectId - GCP Project ID
|
|
70
|
+
* @param {string} config.tableName - Table name for bookkeeping
|
|
71
|
+
* @param {string} config.topicName - Topic name to publish to
|
|
72
|
+
*/
|
|
73
|
+
static create(config) {
|
|
74
|
+
return new ProgramExecutor(config);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = ProgramExecutor;
|