@wabot-dev/framework 0.2.6 → 0.4.0-beta.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.
- package/README.md +9 -12
- package/dist/src/addon/async/pg/PgJobRepository.js +50 -2
- package/dist/src/addon/auth/api-key/PgApiKeyRepository.js +5 -0
- package/dist/src/addon/auth/jwt/PgJwtRefreshTokenRepository.js +7 -2
- package/dist/src/addon/chat-bot/openia/OpenaiChatAdapter.js +15 -3
- package/dist/src/addon/chat-bot/pg/PgChatMemory.js +5 -0
- package/dist/src/addon/chat-bot/pg/PgChatRepository.js +5 -0
- package/dist/src/addon/chat-bot/wabot/WabotChatAdapter.js +1 -1
- package/dist/src/addon/chat-controller/whatsapp/PgWhatsAppRepository.js +5 -0
- package/dist/src/addon/chat-controller/whatsapp/proxy/WhatsAppReceiverByWabotProxy.js +1 -0
- package/dist/src/core/lock/Locker.js +7 -0
- package/dist/src/feature/async/@command.js +3 -3
- package/dist/src/feature/async/@commandHandler.js +3 -3
- package/dist/src/feature/async/Async.js +21 -14
- package/dist/src/feature/async/{CommandMetadataStore.js → AsyncMetadataStore.js} +27 -5
- package/dist/src/feature/async/Job.js +92 -2
- package/dist/src/feature/async/JobExecutor.js +62 -0
- package/dist/src/feature/async/JobRepository.js +9 -0
- package/dist/src/feature/async/JobRunner.js +17 -6
- package/dist/src/feature/async/JobScheduler.js +80 -0
- package/dist/src/feature/async/JobWatchdog.js +74 -0
- package/dist/src/feature/async/runCommandHandlers.js +19 -25
- package/dist/src/feature/chat-bot/Chat.js +3 -0
- package/dist/src/feature/chat-controller/runChatControllers.js +1 -1
- package/dist/src/feature/pg/PgCrudRepository.js +7 -1
- package/dist/src/feature/pg/PgLock.js +20 -0
- package/dist/src/feature/pg/PgLockKey.js +68 -0
- package/dist/src/feature/pg/PgRepositoryBase.js +40 -27
- package/dist/src/feature/pg/pgStorage.js +16 -0
- package/dist/src/feature/pg/withPgClient.js +45 -0
- package/dist/src/feature/pg/withPgTransaction.js +45 -0
- package/dist/src/index.d.ts +252 -96
- package/dist/src/index.js +8 -4
- package/package.json +8 -6
- package/dist/src/feature/async/Command.js +0 -9
- package/dist/src/feature/async/JobsEventsHub.js +0 -36
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { __decorate, __metadata } from 'tslib';
|
|
2
|
+
import { singleton } from '../../core/injection/index.js';
|
|
3
|
+
import { JobRepository } from './JobRepository.js';
|
|
4
|
+
import { Env } from '../../core/env/Env.js';
|
|
5
|
+
import { Logger } from '../../core/logger/Logger.js';
|
|
6
|
+
import { JobExecutor } from './JobExecutor.js';
|
|
7
|
+
import { Locker } from '../../core/lock/Locker.js';
|
|
8
|
+
|
|
9
|
+
let JobScheduler = class JobScheduler {
|
|
10
|
+
locker;
|
|
11
|
+
repo;
|
|
12
|
+
executor;
|
|
13
|
+
env;
|
|
14
|
+
timeout;
|
|
15
|
+
logger = new Logger('wabot:job-scheduler');
|
|
16
|
+
commands = new Set();
|
|
17
|
+
running = false;
|
|
18
|
+
constructor(locker, repo, executor, env) {
|
|
19
|
+
this.locker = locker;
|
|
20
|
+
this.repo = repo;
|
|
21
|
+
this.executor = executor;
|
|
22
|
+
this.env = env;
|
|
23
|
+
}
|
|
24
|
+
start(commands) {
|
|
25
|
+
this.logger.info(`starting handlers for commands ${commands}`);
|
|
26
|
+
commands.forEach((x) => this.commands.add(x));
|
|
27
|
+
this.logger.info(`starting job handlers for commands ${commands.join(', ')}`);
|
|
28
|
+
if (this.commands.size > 0 && !this.running) {
|
|
29
|
+
this.running = true;
|
|
30
|
+
this.tick();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
stop(commands) {
|
|
34
|
+
this.logger.info(`stoping handlers for commands ${commands}`);
|
|
35
|
+
commands.forEach((x) => this.commands.delete(x));
|
|
36
|
+
if (this.commands.size === 0) {
|
|
37
|
+
this.running = false;
|
|
38
|
+
if (this.timeout) {
|
|
39
|
+
clearTimeout(this.timeout);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
tryExecuteNow(job) {
|
|
44
|
+
if (this.commands.has(job.commandName))
|
|
45
|
+
this.executor.execute(job).catch((e) => this.logger.error(e));
|
|
46
|
+
}
|
|
47
|
+
async tick() {
|
|
48
|
+
if (!this.running)
|
|
49
|
+
return;
|
|
50
|
+
const interval = this.env.requireNumber('WABOT_JOB_SCHEDULER_INTERVAL_SECONDS', {
|
|
51
|
+
default: 10,
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
const remainingSlots = this.executor.remainingSlots();
|
|
55
|
+
if (remainingSlots === 0)
|
|
56
|
+
return;
|
|
57
|
+
await this.locker.withKey('wabot-job-scheduler-loop').run(async () => {
|
|
58
|
+
const jobs = await this.repo.findPendingForRunFrom(new Date(), this.executor.remainingSlots());
|
|
59
|
+
const readyToRunJobs = jobs.filter((job) => this.commands.has(job.commandName) && job.isScheduleReady());
|
|
60
|
+
readyToRunJobs.forEach((j) => this.executor.execute(j).catch((e) => this.logger.error(e)));
|
|
61
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
this.logger.error(e);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
this.timeout = setTimeout(() => this.tick(), interval * 1000);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
JobScheduler = __decorate([
|
|
73
|
+
singleton(),
|
|
74
|
+
__metadata("design:paramtypes", [Locker,
|
|
75
|
+
JobRepository,
|
|
76
|
+
JobExecutor,
|
|
77
|
+
Env])
|
|
78
|
+
], JobScheduler);
|
|
79
|
+
|
|
80
|
+
export { JobScheduler };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { __decorate, __metadata } from 'tslib';
|
|
2
|
+
import { singleton } from '../../core/injection/index.js';
|
|
3
|
+
import { JobRepository } from './JobRepository.js';
|
|
4
|
+
import { Env } from '../../core/env/Env.js';
|
|
5
|
+
import { Logger } from '../../core/logger/Logger.js';
|
|
6
|
+
import { Locker } from '../../core/lock/Locker.js';
|
|
7
|
+
|
|
8
|
+
let JobWatchdog = class JobWatchdog {
|
|
9
|
+
locker;
|
|
10
|
+
repo;
|
|
11
|
+
env;
|
|
12
|
+
timeout;
|
|
13
|
+
logger = new Logger('wabot:job-watchdog');
|
|
14
|
+
commands = new Set();
|
|
15
|
+
running = false;
|
|
16
|
+
constructor(locker, repo, env) {
|
|
17
|
+
this.locker = locker;
|
|
18
|
+
this.repo = repo;
|
|
19
|
+
this.env = env;
|
|
20
|
+
}
|
|
21
|
+
start(commands) {
|
|
22
|
+
commands.forEach((x) => this.commands.add(x));
|
|
23
|
+
if (this.commands.size > 0 && !this.running) {
|
|
24
|
+
this.running = true;
|
|
25
|
+
this.tick();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
stop(commands) {
|
|
29
|
+
commands.forEach((x) => this.commands.delete(x));
|
|
30
|
+
if (this.commands.size === 0) {
|
|
31
|
+
this.running = false;
|
|
32
|
+
if (this.timeout)
|
|
33
|
+
clearTimeout(this.timeout);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async tick() {
|
|
37
|
+
if (!this.running)
|
|
38
|
+
return;
|
|
39
|
+
const interval = this.env.requireNumber('WABOT_JOB_WATCHDOG_INTERVAL_SECONDS', {
|
|
40
|
+
default: 300,
|
|
41
|
+
});
|
|
42
|
+
try {
|
|
43
|
+
await this.locker.withKey('wabot-job-watchdog-loop').run(async () => {
|
|
44
|
+
const jobs = await this.repo.findRunningJobs();
|
|
45
|
+
for (const job of jobs) {
|
|
46
|
+
if (!job.isStuck() || !this.commands.has(job.commandName))
|
|
47
|
+
continue;
|
|
48
|
+
try {
|
|
49
|
+
this.logger.warn(`Recovering stuck job ${job.id}`);
|
|
50
|
+
job.recover();
|
|
51
|
+
await this.repo.update(job);
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
this.logger.error(`Failed to recover job ${job.id}`, e);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
this.logger.error('Recovery process failed', e);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
this.timeout = setTimeout(() => this.tick(), interval * 1000);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
JobWatchdog = __decorate([
|
|
68
|
+
singleton(),
|
|
69
|
+
__metadata("design:paramtypes", [Locker,
|
|
70
|
+
JobRepository,
|
|
71
|
+
Env])
|
|
72
|
+
], JobWatchdog);
|
|
73
|
+
|
|
74
|
+
export { JobWatchdog };
|
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { JobRunner } from './JobRunner.js';
|
|
3
|
-
import { CommandMetadataStore } from './CommandMetadataStore.js';
|
|
4
|
-
import { JobRepository } from './JobRepository.js';
|
|
1
|
+
import { AsyncMetadataStore } from './AsyncMetadataStore.js';
|
|
5
2
|
import { container } from '../../core/injection/index.js';
|
|
3
|
+
import { JobScheduler } from './JobScheduler.js';
|
|
4
|
+
import { JobWatchdog } from './JobWatchdog.js';
|
|
6
5
|
|
|
7
|
-
function
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
catch (e) {
|
|
24
|
-
console.error(e);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
6
|
+
function runCommandHandlers(handlers) {
|
|
7
|
+
const jobScheduler = container.resolve(JobScheduler);
|
|
8
|
+
const jobWatchdog = container.resolve(JobWatchdog);
|
|
9
|
+
const metadataStore = container.resolve(AsyncMetadataStore);
|
|
10
|
+
const commands = handlers.map((x) => metadataStore.requireCommandNameForHandler(x));
|
|
11
|
+
jobScheduler.start(commands);
|
|
12
|
+
jobWatchdog.start(commands);
|
|
13
|
+
}
|
|
14
|
+
function stopCommandHandlers(handlers) {
|
|
15
|
+
const jobScheduler = container.resolve(JobScheduler);
|
|
16
|
+
const jobWatchdog = container.resolve(JobWatchdog);
|
|
17
|
+
const metadataStore = container.resolve(AsyncMetadataStore);
|
|
18
|
+
const commands = handlers.map((x) => metadataStore.requireCommandNameForHandler(x));
|
|
19
|
+
jobScheduler.stop(commands);
|
|
20
|
+
jobWatchdog.stop(commands);
|
|
27
21
|
}
|
|
28
22
|
|
|
29
|
-
export {
|
|
23
|
+
export { runCommandHandlers, stopCommandHandlers };
|
|
@@ -10,6 +10,9 @@ class Chat extends Entity {
|
|
|
10
10
|
isGroup() {
|
|
11
11
|
return this.data.type === 'GROUP';
|
|
12
12
|
}
|
|
13
|
+
get connections() {
|
|
14
|
+
return this.data.connections;
|
|
15
|
+
}
|
|
13
16
|
hasConnection(connection) {
|
|
14
17
|
for (const con of this.data.connections) {
|
|
15
18
|
if (con.channelName === connection.channelName && con.id === connection.id) {
|
|
@@ -20,7 +20,7 @@ async function prepareChatContainer(container, messageContext, mindsetCtor) {
|
|
|
20
20
|
const chatRepository = container.resolve(ChatRepository);
|
|
21
21
|
const chatMemory = await chatRepository.findMemory(messageContext.chat.id);
|
|
22
22
|
if (!chatMemory) {
|
|
23
|
-
throw new Error('Not found
|
|
23
|
+
throw new Error('Not found Chaqt Memory for Chat with Id=' + messageContext.chat.id);
|
|
24
24
|
}
|
|
25
25
|
chatContainer.registerInstance(ChatMemory, chatMemory);
|
|
26
26
|
if (messageContext.authInfo) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as shortUUID from 'short-uuid';
|
|
2
2
|
import { PgRepositoryBase } from './PgRepositoryBase.js';
|
|
3
3
|
import { CustomError } from '../../core/error/CustomError.js';
|
|
4
|
+
import { withPgClient } from './withPgClient.js';
|
|
4
5
|
|
|
5
6
|
class PgCrudRepository extends PgRepositoryBase {
|
|
6
7
|
config;
|
|
@@ -69,7 +70,12 @@ class PgCrudRepository extends PgRepositoryBase {
|
|
|
69
70
|
SET ${this.updates}
|
|
70
71
|
WHERE id = $${this.columnsList.length + 1}
|
|
71
72
|
`;
|
|
72
|
-
await this.
|
|
73
|
+
const result = await withPgClient(this.pool, async (client) => {
|
|
74
|
+
return await client.query(sql, [...this.values(item), item.id]);
|
|
75
|
+
});
|
|
76
|
+
if (result.rowCount === 0) {
|
|
77
|
+
throw new Error(`Update failed: no affected rows'`);
|
|
78
|
+
}
|
|
73
79
|
}
|
|
74
80
|
async delete(item) {
|
|
75
81
|
const sql = `
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { __decorate, __metadata } from 'tslib';
|
|
2
|
+
import { singleton } from '../../core/injection/index.js';
|
|
3
|
+
import { Pool } from 'pg';
|
|
4
|
+
import { PgLockKey } from './PgLockKey.js';
|
|
5
|
+
|
|
6
|
+
let PgLocker = class PgLocker {
|
|
7
|
+
pool;
|
|
8
|
+
constructor(pool) {
|
|
9
|
+
this.pool = pool;
|
|
10
|
+
}
|
|
11
|
+
withKey(key) {
|
|
12
|
+
return new PgLockKey(key, this.pool);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
PgLocker = __decorate([
|
|
16
|
+
singleton(),
|
|
17
|
+
__metadata("design:paramtypes", [Pool])
|
|
18
|
+
], PgLocker);
|
|
19
|
+
|
|
20
|
+
export { PgLocker };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Logger } from '../../core/logger/Logger.js';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { withPgClient } from './withPgClient.js';
|
|
4
|
+
|
|
5
|
+
class PgLockKey {
|
|
6
|
+
key;
|
|
7
|
+
pool;
|
|
8
|
+
value;
|
|
9
|
+
logger = new Logger('wabot:pg-lock-key');
|
|
10
|
+
constructor(key, pool) {
|
|
11
|
+
this.key = key;
|
|
12
|
+
this.pool = pool;
|
|
13
|
+
this.value = typeof key === 'number' ? BigInt(key) : PgLockKey.hashString(key);
|
|
14
|
+
}
|
|
15
|
+
async run(fn) {
|
|
16
|
+
return withPgClient(this.pool, async (client) => {
|
|
17
|
+
let locked = false;
|
|
18
|
+
try {
|
|
19
|
+
this.logger.debug(`try to adquire ${this.key}`);
|
|
20
|
+
await client.query('SELECT pg_advisory_lock($1)', [this.value]);
|
|
21
|
+
this.logger.debug(`adquired ${this.key}`);
|
|
22
|
+
locked = true;
|
|
23
|
+
return await fn();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
if (locked) {
|
|
27
|
+
this.logger.debug(`try to release ${this.key}`);
|
|
28
|
+
const res = await client.query('SELECT pg_advisory_unlock($1) AS unlocked', [this.value]);
|
|
29
|
+
if (!res.rows[0]?.unlocked) {
|
|
30
|
+
this.logger.error('error - no unlock');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
this.logger.debug(`released ${this.key}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async tryRun(fn) {
|
|
40
|
+
return withPgClient(this.pool, async (client) => {
|
|
41
|
+
let locked = false;
|
|
42
|
+
try {
|
|
43
|
+
const result = await client.query('SELECT pg_try_advisory_lock($1) AS locked', [this.value]);
|
|
44
|
+
locked = result.rows[0]?.locked === true;
|
|
45
|
+
if (!locked)
|
|
46
|
+
return undefined;
|
|
47
|
+
return await fn();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (locked) {
|
|
51
|
+
const res = await client.query('SELECT pg_advisory_unlock($1) AS unlocked', [this.value]);
|
|
52
|
+
if (!res.rows[0]?.unlocked) {
|
|
53
|
+
this.logger.error('error - no unlock');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
toString() {
|
|
60
|
+
return `PgLockKey(${String(this.key)})`;
|
|
61
|
+
}
|
|
62
|
+
static hashString(key) {
|
|
63
|
+
const hash = createHash('sha256').update(key).digest('hex').slice(0, 15);
|
|
64
|
+
return BigInt('0x' + hash);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { PgLockKey };
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { withPgClient } from './withPgClient.js';
|
|
2
|
+
import { Logger } from '../../core/logger/Logger.js';
|
|
3
|
+
import { PgLocker } from './PgLock.js';
|
|
4
|
+
|
|
1
5
|
class PgRepositoryBase {
|
|
2
6
|
pool;
|
|
3
7
|
config;
|
|
4
|
-
|
|
8
|
+
tableIsReady = false;
|
|
5
9
|
schema;
|
|
6
10
|
table;
|
|
7
11
|
columnsList;
|
|
@@ -9,6 +13,7 @@ class PgRepositoryBase {
|
|
|
9
13
|
columns;
|
|
10
14
|
vars;
|
|
11
15
|
updates;
|
|
16
|
+
logger = new Logger('wabot:pg-repository-base');
|
|
12
17
|
addColumns;
|
|
13
18
|
constructor(pool, config) {
|
|
14
19
|
this.pool = pool;
|
|
@@ -25,7 +30,7 @@ class PgRepositoryBase {
|
|
|
25
30
|
'"created_at" TIMESTAMP',
|
|
26
31
|
'"data" JSONB',
|
|
27
32
|
...this.columnsList.slice(3).map((x) => `${x} ${this.addColumns[x].type}`),
|
|
28
|
-
]
|
|
33
|
+
];
|
|
29
34
|
this.columns = this.columnsList.map((x) => `"${x}"`).join(', ');
|
|
30
35
|
this.vars = this.columnsList.map((_, i) => `$${i + 1}`).join(', ');
|
|
31
36
|
this.updates = this.columnsList.map((x, i) => `"${x}" = $${i + 1}`).join(', ');
|
|
@@ -39,36 +44,44 @@ class PgRepositoryBase {
|
|
|
39
44
|
];
|
|
40
45
|
}
|
|
41
46
|
async exec(sql, values) {
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
return withPgClient(this.pool, async (client) => {
|
|
48
|
+
await this.ensureTable(client);
|
|
49
|
+
await client.query(sql, values);
|
|
50
|
+
});
|
|
44
51
|
}
|
|
45
52
|
async query(sql, values) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
return withPgClient(this.pool, async (client) => {
|
|
54
|
+
await this.ensureTable(client);
|
|
55
|
+
const { rows } = await client.query(sql, values);
|
|
56
|
+
return rows.map((row) => new this.config.constructor(row.data));
|
|
57
|
+
});
|
|
49
58
|
}
|
|
50
|
-
async
|
|
51
|
-
|
|
52
|
-
return this.pool;
|
|
53
|
-
}
|
|
54
|
-
async ensureTable() {
|
|
55
|
-
if (this.tableIsCreated) {
|
|
59
|
+
async ensureTable(client) {
|
|
60
|
+
if (this.tableIsReady) {
|
|
56
61
|
return;
|
|
57
62
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
const locker = new PgLocker(this.pool);
|
|
64
|
+
await locker.withKey(`wabot-pgrepo-ensure-table-${this.table}`).run(async () => {
|
|
65
|
+
if (this.tableIsReady) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.logger.debug(`running schema sync for ${this.table}`);
|
|
69
|
+
const schemaQuery = `
|
|
70
|
+
CREATE SCHEMA IF NOT EXISTS ${this.schema}
|
|
71
|
+
`;
|
|
72
|
+
const tableQuery = `
|
|
73
|
+
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
74
|
+
${this.columnsAndTypes.join(', ')}
|
|
75
|
+
)
|
|
76
|
+
`;
|
|
77
|
+
await client.query(schemaQuery);
|
|
78
|
+
await client.query(tableQuery);
|
|
79
|
+
await this.ensureColumns(client);
|
|
80
|
+
this.tableIsReady = true;
|
|
81
|
+
});
|
|
69
82
|
}
|
|
70
|
-
async ensureColumns() {
|
|
71
|
-
const { rows: existing } = await
|
|
83
|
+
async ensureColumns(client) {
|
|
84
|
+
const { rows: existing } = await client.query(`
|
|
72
85
|
SELECT column_name
|
|
73
86
|
FROM information_schema.columns
|
|
74
87
|
WHERE table_name = $1 AND table_schema = $2
|
|
@@ -80,7 +93,7 @@ class PgRepositoryBase {
|
|
|
80
93
|
if (!existingColumns.has(col)) {
|
|
81
94
|
const alterSql = `ALTER TABLE ${this.table} ADD COLUMN ${columnAndType}`;
|
|
82
95
|
console.log(`[INFO] Adding column: ${alterSql}`);
|
|
83
|
-
await
|
|
96
|
+
await client.query(alterSql);
|
|
84
97
|
}
|
|
85
98
|
}
|
|
86
99
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
const pgStorage = new AsyncLocalStorage();
|
|
4
|
+
/**
|
|
5
|
+
* Get or initialize the client map for the current async context
|
|
6
|
+
*/
|
|
7
|
+
function getClientMap() {
|
|
8
|
+
let map = pgStorage.getStore();
|
|
9
|
+
if (!map) {
|
|
10
|
+
map = new Map();
|
|
11
|
+
pgStorage.enterWith(map);
|
|
12
|
+
}
|
|
13
|
+
return map;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { getClientMap, pgStorage };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
|
|
3
|
+
// Async-local storage for clients per pool
|
|
4
|
+
const pgStorage = new AsyncLocalStorage();
|
|
5
|
+
function getClientMap() {
|
|
6
|
+
let map = pgStorage.getStore();
|
|
7
|
+
if (!map) {
|
|
8
|
+
map = new Map();
|
|
9
|
+
pgStorage.enterWith(map);
|
|
10
|
+
}
|
|
11
|
+
return map;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Runs a function with a shared Postgres client for the given pool.
|
|
15
|
+
* Reuses the client if already present in the async context.
|
|
16
|
+
*/
|
|
17
|
+
async function withPgClient(pool, fn) {
|
|
18
|
+
const clients = getClientMap();
|
|
19
|
+
if (clients.has(pool)) {
|
|
20
|
+
// Already a shared client in async context, reuse it
|
|
21
|
+
return fn(clients.get(pool));
|
|
22
|
+
}
|
|
23
|
+
// No client yet, acquire a new one
|
|
24
|
+
const client = await pool.connect();
|
|
25
|
+
try {
|
|
26
|
+
clients.set(pool, client);
|
|
27
|
+
return await fn(client);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
clients.delete(pool);
|
|
31
|
+
client.release();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get the current shared client for a pool.
|
|
36
|
+
* Falls back to the pool itself if no shared client exists.
|
|
37
|
+
*/
|
|
38
|
+
function getPgClient(pool) {
|
|
39
|
+
const clients = pgStorage.getStore();
|
|
40
|
+
if (clients?.has(pool))
|
|
41
|
+
return clients.get(pool);
|
|
42
|
+
return pool;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { getPgClient, withPgClient };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getPgClient } from './withPgClient.js';
|
|
2
|
+
|
|
3
|
+
let savepointCounter = 0;
|
|
4
|
+
async function withPgTransaction(pool, fn) {
|
|
5
|
+
// Always get the shared client
|
|
6
|
+
const client = getPgClient(pool);
|
|
7
|
+
if (!('__transactionStack' in client)) {
|
|
8
|
+
client.__transactionStack = [];
|
|
9
|
+
}
|
|
10
|
+
const stack = client.__transactionStack;
|
|
11
|
+
const isOuterTransaction = stack.length === 0;
|
|
12
|
+
const savepointName = isOuterTransaction ? null : `sp_${++savepointCounter}`;
|
|
13
|
+
if (savepointName)
|
|
14
|
+
await client.query(`SAVEPOINT ${savepointName}`);
|
|
15
|
+
if (isOuterTransaction)
|
|
16
|
+
await client.query('BEGIN');
|
|
17
|
+
stack.push(savepointName || 'outer');
|
|
18
|
+
try {
|
|
19
|
+
const result = await fn(client);
|
|
20
|
+
if (savepointName) {
|
|
21
|
+
await client.query(`RELEASE SAVEPOINT ${savepointName}`);
|
|
22
|
+
}
|
|
23
|
+
else if (isOuterTransaction) {
|
|
24
|
+
await client.query('COMMIT');
|
|
25
|
+
}
|
|
26
|
+
stack.pop();
|
|
27
|
+
if (isOuterTransaction)
|
|
28
|
+
delete client.__transactionStack;
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (savepointName) {
|
|
33
|
+
await client.query(`ROLLBACK TO SAVEPOINT ${savepointName}`);
|
|
34
|
+
}
|
|
35
|
+
else if (isOuterTransaction) {
|
|
36
|
+
await client.query('ROLLBACK');
|
|
37
|
+
}
|
|
38
|
+
stack.pop();
|
|
39
|
+
if (isOuterTransaction)
|
|
40
|
+
delete client.__transactionStack;
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { withPgTransaction };
|