@wabot-dev/framework 0.9.2 → 0.9.6
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/dist/src/addon/async/pg/PgCronJobRepository.js +7 -5
- package/dist/src/addon/async/pg/PgJobRepository.js +7 -5
- package/dist/src/addon/async/pg/PgTransactionAdapter.js +4 -4
- package/dist/src/addon/auth/api-key/ApiKey.js +4 -4
- package/dist/src/addon/auth/api-key/PgApiKeyRepository.js +9 -8
- package/dist/src/addon/auth/jwt/JwtRefreshToken.js +4 -4
- package/dist/src/addon/auth/jwt/PgJwtRefreshTokenRepository.js +6 -5
- package/dist/src/addon/chat-bot/pg/PgChatMemory.js +8 -7
- package/dist/src/addon/chat-bot/pg/PgChatRepository.js +7 -6
- package/dist/src/addon/chat-controller/cmd/@cmd.js +7 -2
- package/dist/src/addon/chat-controller/cmd/CmdChannel.js +85 -61
- package/dist/src/addon/chat-controller/cmd/CmdChannelConfig.js +8 -0
- package/dist/src/addon/chat-controller/cmd/CmdChannelServer.js +169 -0
- package/dist/src/addon/chat-controller/cmd/cmdChannelSocketPath.js +16 -0
- package/dist/src/addon/chat-controller/cmd/runCmdClient.js +226 -0
- package/dist/src/core/repository/CrudRepository.js +25 -0
- package/dist/src/feature/pg/@pgExtension.js +33 -0
- package/dist/src/feature/pg/PgJsonRepositoryAdapter.js +50 -0
- package/dist/src/feature/pg/index.js +4 -7
- package/dist/src/feature/project-runner/ProjectRunner.js +67 -17
- package/dist/src/feature/repository/@memoryExtension.js +29 -0
- package/dist/src/feature/repository/@query.js +22 -0
- package/dist/src/feature/repository/@queryExtension.js +21 -0
- package/dist/src/feature/repository/@repository.js +170 -0
- package/dist/src/feature/repository/MemoryRepositoryAdapter.js +110 -0
- package/dist/src/feature/repository/RepositoryAdapterRegistry.js +27 -0
- package/dist/src/feature/repository/RepositoryMetadataStore.js +102 -0
- package/dist/src/feature/repository/evaluateQueryAst.js +134 -0
- package/dist/src/index.d.ts +197 -47
- package/dist/src/index.js +18 -7
- package/package.json +4 -2
- package/dist/src/feature/pg/query/@pgJsonRepository.js +0 -73
- package/dist/src/feature/pg/query/@query.js +0 -14
- package/dist/src/feature/pg/query/PgJsonRepository.js +0 -23
- package/dist/src/feature/pg/query/PgRepositoryMetadataStore.js +0 -44
- /package/dist/src/feature/pg/{query/buildQuerySql.js → buildQuerySql.js} +0 -0
- /package/dist/src/feature/{pg/query → repository}/parseQueryMethodName.js +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import * as net from 'node:net';
|
|
2
|
+
import * as readline from 'node:readline';
|
|
3
|
+
import { cmdChannelSocketPath } from './cmdChannelSocketPath.js';
|
|
4
|
+
|
|
5
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== 'dumb';
|
|
6
|
+
const ansi = (code) => (text) => useColor ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
7
|
+
const bold = ansi('1');
|
|
8
|
+
const dim = ansi('2');
|
|
9
|
+
const cyan = ansi('1;36');
|
|
10
|
+
const green = ansi('1;32');
|
|
11
|
+
const greenText = ansi('32');
|
|
12
|
+
const red = ansi('1;31');
|
|
13
|
+
const yellow = ansi('33');
|
|
14
|
+
const COMMANDS = ['/channels', '/help', '/exit'];
|
|
15
|
+
const HELP_LINES = [
|
|
16
|
+
'Commands:',
|
|
17
|
+
' /channels list channels and switch',
|
|
18
|
+
' /help show this help',
|
|
19
|
+
' /exit quit',
|
|
20
|
+
];
|
|
21
|
+
const RECONNECT_DELAY_MS = 1000;
|
|
22
|
+
function runCmdClient() {
|
|
23
|
+
const socketPath = cmdChannelSocketPath();
|
|
24
|
+
let socket = null;
|
|
25
|
+
let buffer = '';
|
|
26
|
+
let routes = [];
|
|
27
|
+
let state = 'disconnected';
|
|
28
|
+
let selected = null;
|
|
29
|
+
let reconnectTimer = null;
|
|
30
|
+
let waitingNoticeShown = false;
|
|
31
|
+
let exiting = false;
|
|
32
|
+
let restoring = false;
|
|
33
|
+
const completer = (line) => {
|
|
34
|
+
if (line.startsWith('/')) {
|
|
35
|
+
const hits = COMMANDS.filter((c) => c.startsWith(line));
|
|
36
|
+
return [hits.length > 0 ? hits : [...COMMANDS], line];
|
|
37
|
+
}
|
|
38
|
+
if (state === 'choosing') {
|
|
39
|
+
const hits = routes
|
|
40
|
+
.map((r) => r.route)
|
|
41
|
+
.filter((r) => r.toLowerCase().startsWith(line.toLowerCase()));
|
|
42
|
+
return [hits, line];
|
|
43
|
+
}
|
|
44
|
+
return [[], line];
|
|
45
|
+
};
|
|
46
|
+
const rl = readline.createInterface({
|
|
47
|
+
input: process.stdin,
|
|
48
|
+
output: process.stdout,
|
|
49
|
+
completer,
|
|
50
|
+
});
|
|
51
|
+
const send = (msg) => {
|
|
52
|
+
if (!socket) {
|
|
53
|
+
process.stderr.write(red('not connected to framework — waiting for server...') + '\n');
|
|
54
|
+
rl.prompt();
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
socket.write(JSON.stringify(msg) + '\n');
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
const printChannels = (list) => {
|
|
61
|
+
routes = list;
|
|
62
|
+
if (list.length === 0) {
|
|
63
|
+
process.stdout.write(yellow('No cmd channels are registered on the server yet.') + '\n');
|
|
64
|
+
rl.prompt();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (list.length === 1) {
|
|
68
|
+
send({ type: 'select', route: list[0].route });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
process.stdout.write(bold('Available cmd channels:') + '\n');
|
|
72
|
+
list.forEach((entry, i) => {
|
|
73
|
+
const marker = entry.route === selected ? ' ' + yellow('(active)') : '';
|
|
74
|
+
process.stdout.write(` ${dim(`${i + 1}.`)} ${cyan(entry.route)}${marker}\n`);
|
|
75
|
+
});
|
|
76
|
+
state = 'choosing';
|
|
77
|
+
rl.setPrompt(bold('select # ') + dim('> '));
|
|
78
|
+
rl.prompt();
|
|
79
|
+
};
|
|
80
|
+
const handleServerMessage = (msg) => {
|
|
81
|
+
switch (msg.type) {
|
|
82
|
+
case 'channels':
|
|
83
|
+
printChannels(msg.list);
|
|
84
|
+
return;
|
|
85
|
+
case 'selected':
|
|
86
|
+
selected = msg.route;
|
|
87
|
+
state = 'chatting';
|
|
88
|
+
if (!restoring) {
|
|
89
|
+
process.stdout.write(green(`[connected to ${msg.route}]`) + '\n');
|
|
90
|
+
}
|
|
91
|
+
restoring = false;
|
|
92
|
+
rl.setPrompt(cyan(msg.route) + dim(' > '));
|
|
93
|
+
rl.prompt();
|
|
94
|
+
return;
|
|
95
|
+
case 'reply':
|
|
96
|
+
process.stdout.write(`\n${green(`[${msg.senderName ?? 'bot'}]:`)} ${greenText(msg.text)}\n\n`);
|
|
97
|
+
rl.prompt();
|
|
98
|
+
return;
|
|
99
|
+
case 'error':
|
|
100
|
+
process.stderr.write(red('error: ') + red(msg.message) + '\n');
|
|
101
|
+
rl.prompt();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const scheduleReconnect = () => {
|
|
106
|
+
if (exiting || reconnectTimer)
|
|
107
|
+
return;
|
|
108
|
+
reconnectTimer = setTimeout(() => {
|
|
109
|
+
reconnectTimer = null;
|
|
110
|
+
connect();
|
|
111
|
+
}, RECONNECT_DELAY_MS);
|
|
112
|
+
};
|
|
113
|
+
const connect = () => {
|
|
114
|
+
if (exiting)
|
|
115
|
+
return;
|
|
116
|
+
buffer = '';
|
|
117
|
+
socket = net.connect(socketPath);
|
|
118
|
+
socket.on('connect', () => {
|
|
119
|
+
waitingNoticeShown = false;
|
|
120
|
+
// Silent reconnect when we're restoring a prior selection;
|
|
121
|
+
// otherwise announce the connection and ask for channels.
|
|
122
|
+
if (selected) {
|
|
123
|
+
restoring = true;
|
|
124
|
+
socket.write(JSON.stringify({ type: 'select', route: selected }) + '\n');
|
|
125
|
+
state = 'chatting';
|
|
126
|
+
rl.setPrompt(cyan(selected) + dim(' > '));
|
|
127
|
+
rl.prompt();
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
process.stdout.write(green('[connected to framework]') + '\n');
|
|
131
|
+
socket.write(JSON.stringify({ type: 'hello' }) + '\n');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
socket.on('data', (chunk) => {
|
|
135
|
+
buffer += chunk.toString();
|
|
136
|
+
let idx;
|
|
137
|
+
while ((idx = buffer.indexOf('\n')) !== -1) {
|
|
138
|
+
const line = buffer.slice(0, idx);
|
|
139
|
+
buffer = buffer.slice(idx + 1);
|
|
140
|
+
if (!line)
|
|
141
|
+
continue;
|
|
142
|
+
try {
|
|
143
|
+
handleServerMessage(JSON.parse(line));
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
process.stderr.write(red(`invalid server message: ${err.message}`) + '\n');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
socket.on('error', (err) => {
|
|
151
|
+
const transient = err.code === 'ENOENT' ||
|
|
152
|
+
err.code === 'ECONNREFUSED' ||
|
|
153
|
+
err.code === 'ECONNRESET' ||
|
|
154
|
+
err.code === 'EPIPE';
|
|
155
|
+
if (!transient) {
|
|
156
|
+
process.stderr.write(red(`socket error: ${err.message}`) + '\n');
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
socket.on('close', () => {
|
|
160
|
+
const wasConnected = state !== 'disconnected';
|
|
161
|
+
state = 'disconnected';
|
|
162
|
+
socket = null;
|
|
163
|
+
if (exiting)
|
|
164
|
+
return;
|
|
165
|
+
if (wasConnected) {
|
|
166
|
+
process.stdout.write(yellow('\n[disconnected — waiting for framework to come back...]') + '\n');
|
|
167
|
+
}
|
|
168
|
+
else if (!waitingNoticeShown && !selected) {
|
|
169
|
+
waitingNoticeShown = true;
|
|
170
|
+
process.stdout.write(dim('Waiting for framework...') + '\n');
|
|
171
|
+
}
|
|
172
|
+
scheduleReconnect();
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
const cleanup = (code) => {
|
|
176
|
+
exiting = true;
|
|
177
|
+
if (reconnectTimer) {
|
|
178
|
+
clearTimeout(reconnectTimer);
|
|
179
|
+
reconnectTimer = null;
|
|
180
|
+
}
|
|
181
|
+
rl.close();
|
|
182
|
+
if (socket)
|
|
183
|
+
socket.end();
|
|
184
|
+
process.exit(code);
|
|
185
|
+
};
|
|
186
|
+
rl.on('line', (input) => {
|
|
187
|
+
const trimmed = input.trim();
|
|
188
|
+
if (!trimmed) {
|
|
189
|
+
rl.prompt();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (trimmed === '/exit' || trimmed.toLowerCase() === 'exit') {
|
|
193
|
+
cleanup(0);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (trimmed === '/help') {
|
|
197
|
+
process.stdout.write(HELP_LINES.join('\n') + '\n');
|
|
198
|
+
rl.prompt();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (trimmed === '/channels') {
|
|
202
|
+
send({ type: 'hello' });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (state === 'disconnected') {
|
|
206
|
+
process.stderr.write(red('not connected to framework — waiting for server...') + '\n');
|
|
207
|
+
rl.prompt();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (state === 'choosing') {
|
|
211
|
+
const num = Number.parseInt(trimmed, 10);
|
|
212
|
+
if (!Number.isInteger(num) || num < 1 || num > routes.length) {
|
|
213
|
+
process.stderr.write(red(`Invalid selection. Enter a number between 1 and ${routes.length}.`) + '\n');
|
|
214
|
+
rl.prompt();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
send({ type: 'select', route: routes[num - 1].route });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
send({ type: 'message', text: trimmed });
|
|
221
|
+
});
|
|
222
|
+
rl.on('close', () => cleanup(0));
|
|
223
|
+
connect();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export { runCmdClient };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class CrudRepository {
|
|
2
|
+
find(id) {
|
|
3
|
+
throw new Error("Method not implemented.");
|
|
4
|
+
}
|
|
5
|
+
findOrThrow(id) {
|
|
6
|
+
throw new Error("Method not implemented.");
|
|
7
|
+
}
|
|
8
|
+
findByIds(ids) {
|
|
9
|
+
throw new Error("Method not implemented.");
|
|
10
|
+
}
|
|
11
|
+
findAll(id) {
|
|
12
|
+
throw new Error("Method not implemented.");
|
|
13
|
+
}
|
|
14
|
+
create(item) {
|
|
15
|
+
throw new Error("Method not implemented.");
|
|
16
|
+
}
|
|
17
|
+
update(item) {
|
|
18
|
+
throw new Error("Method not implemented.");
|
|
19
|
+
}
|
|
20
|
+
delete(item) {
|
|
21
|
+
throw new Error("Method not implemented.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { CrudRepository };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { container } from '../../core/injection/index.js';
|
|
2
|
+
import 'short-uuid';
|
|
3
|
+
import '../../core/error/setupErrorHandlers.js';
|
|
4
|
+
import { RepositoryMetadataStore } from '../repository/RepositoryMetadataStore.js';
|
|
5
|
+
import '../repository/RepositoryAdapterRegistry.js';
|
|
6
|
+
import { PgRepositoryBase } from './PgRepositoryBase.js';
|
|
7
|
+
|
|
8
|
+
const PG_ADAPTER_ID = Symbol('wabot:pg-adapter');
|
|
9
|
+
function inheritsFrom(ctor, base) {
|
|
10
|
+
let proto = ctor.prototype;
|
|
11
|
+
while (proto) {
|
|
12
|
+
if (proto === base.prototype)
|
|
13
|
+
return true;
|
|
14
|
+
proto = Object.getPrototypeOf(proto);
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
function pgExtension(repositoryClass) {
|
|
19
|
+
if (typeof repositoryClass !== 'function') {
|
|
20
|
+
throw new Error(`@pgExtension: repository argument must be a class, ` +
|
|
21
|
+
`got ${typeof repositoryClass}`);
|
|
22
|
+
}
|
|
23
|
+
return function (target) {
|
|
24
|
+
if (!inheritsFrom(target, PgRepositoryBase)) {
|
|
25
|
+
throw new Error(`@pgExtension on ${target.name}: extension class must extend ` +
|
|
26
|
+
`PgRepositoryExtension.`);
|
|
27
|
+
}
|
|
28
|
+
const store = container.resolve(RepositoryMetadataStore);
|
|
29
|
+
store.saveExtension(repositoryClass, PG_ADAPTER_ID, target);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { PG_ADAPTER_ID, pgExtension };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { PgCrudRepository } from './PgCrudRepository.js';
|
|
2
|
+
import { PG_ADAPTER_ID } from './@pgExtension.js';
|
|
3
|
+
import { buildQuerySql } from './buildQuerySql.js';
|
|
4
|
+
import { withPgClient } from './withPgClient.js';
|
|
5
|
+
|
|
6
|
+
class PgJsonRepositoryRuntime extends PgCrudRepository {
|
|
7
|
+
async runQuery(ast, args) {
|
|
8
|
+
const built = buildQuerySql(ast, this.table, this.columns);
|
|
9
|
+
const params = built.buildParams(args);
|
|
10
|
+
return this.query(built.sql, params);
|
|
11
|
+
}
|
|
12
|
+
async runCount(ast, args) {
|
|
13
|
+
const built = buildQuerySql(ast, this.table, this.columns);
|
|
14
|
+
const params = built.buildParams(args);
|
|
15
|
+
return withPgClient(this.pool, async (client) => {
|
|
16
|
+
await this.ensureTable(client);
|
|
17
|
+
const result = await client.query(built.sql, params);
|
|
18
|
+
return result.rows[0]?.count ?? 0;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async runExists(ast, args) {
|
|
22
|
+
const built = buildQuerySql(ast, this.table, this.columns);
|
|
23
|
+
const params = built.buildParams(args);
|
|
24
|
+
return withPgClient(this.pool, async (client) => {
|
|
25
|
+
await this.ensureTable(client);
|
|
26
|
+
const result = await client.query(built.sql, params);
|
|
27
|
+
return Boolean(result.rows[0]?.exists);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async runDelete(ast, args) {
|
|
31
|
+
const built = buildQuerySql(ast, this.table, this.columns);
|
|
32
|
+
const params = built.buildParams(args);
|
|
33
|
+
await this.exec(built.sql, params);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
class PgJsonRepositoryAdapter {
|
|
37
|
+
pool;
|
|
38
|
+
id = PG_ADAPTER_ID;
|
|
39
|
+
constructor(pool) {
|
|
40
|
+
this.pool = pool;
|
|
41
|
+
}
|
|
42
|
+
build(config) {
|
|
43
|
+
return new PgJsonRepositoryRuntime(this.pool, config);
|
|
44
|
+
}
|
|
45
|
+
buildExtension(config, ExtensionCtor) {
|
|
46
|
+
return new ExtensionCtor(this.pool, config);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { PgJsonRepositoryAdapter };
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
+
export { PG_ADAPTER_ID, pgExtension } from './@pgExtension.js';
|
|
1
2
|
export { PgCrudRepository } from './PgCrudRepository.js';
|
|
3
|
+
export { PgJsonRepositoryAdapter } from './PgJsonRepositoryAdapter.js';
|
|
2
4
|
export { PgLocker } from './PgLocker.js';
|
|
3
5
|
export { PgLockKey } from './PgLockKey.js';
|
|
4
|
-
export { PgRepositoryBase } from './PgRepositoryBase.js';
|
|
6
|
+
export { PgRepositoryBase, PgRepositoryBase as PgRepositoryExtension } from './PgRepositoryBase.js';
|
|
5
7
|
export { getClientMap, pgStorage } from './pgStorage.js';
|
|
6
|
-
export {
|
|
7
|
-
export { query } from './query/@query.js';
|
|
8
|
-
export { PgJsonRepository } from './query/PgJsonRepository.js';
|
|
9
|
-
export { PgRepositoryMetadataStore } from './query/PgRepositoryMetadataStore.js';
|
|
10
|
-
export { buildQuerySql } from './query/buildQuerySql.js';
|
|
11
|
-
export { parseQueryMethodName } from './query/parseQueryMethodName.js';
|
|
8
|
+
export { buildQuerySql } from './buildQuerySql.js';
|
|
12
9
|
export { getPgClient, withPgClient } from './withPgClient.js';
|
|
13
10
|
export { withPgTransaction } from './withPgTransaction.js';
|
|
@@ -30,9 +30,13 @@ import { runCommandHandlers } from '../async/runCommandHandlers.js';
|
|
|
30
30
|
import { runCronHandlers } from '../async/runCronHandlers.js';
|
|
31
31
|
import { SocketControllerMetadataStore } from '../socket-controller/metadata/SocketControllerMetadataStore.js';
|
|
32
32
|
import { runSocketControllers } from '../socket-controller/runSocketControllers.js';
|
|
33
|
+
import { MemoryRepositoryAdapter } from '../repository/MemoryRepositoryAdapter.js';
|
|
34
|
+
import { RepositoryMetadataStore } from '../repository/RepositoryMetadataStore.js';
|
|
35
|
+
import { RepositoryAdapterRegistry } from '../repository/RepositoryAdapterRegistry.js';
|
|
33
36
|
|
|
34
37
|
const logger = new Logger('wabot:project-runner');
|
|
35
38
|
const TEST_FILE_PATTERNS = /\.(test|spec|unit|integration|e2e|multiprocess)\.(ts|js)$/;
|
|
39
|
+
const DEFAULT_EXCLUDE = ['run.ts', 'cmd.ts'];
|
|
36
40
|
const DEFAULT_CHAT_ADAPTERS = [
|
|
37
41
|
['../../addon/chat-bot/openia', 'OpenaiChatAdapter'],
|
|
38
42
|
['../../addon/chat-bot/openrouter', 'OpenRouterChatAdapter'],
|
|
@@ -41,12 +45,14 @@ const DEFAULT_CHAT_ADAPTERS = [
|
|
|
41
45
|
];
|
|
42
46
|
class ProjectRunner {
|
|
43
47
|
directories;
|
|
48
|
+
exclude;
|
|
44
49
|
chatAdapters;
|
|
45
50
|
connectionString;
|
|
46
51
|
isPg;
|
|
47
52
|
pool = null;
|
|
48
53
|
constructor(config = {}) {
|
|
49
54
|
this.directories = config.directories ?? ['src'];
|
|
55
|
+
this.exclude = [...DEFAULT_EXCLUDE, ...(config.exclude ?? [])];
|
|
50
56
|
this.chatAdapters = config.chatAdapters;
|
|
51
57
|
this.connectionString = this.resolveConnectionString(config.connectionString);
|
|
52
58
|
this.isPg = this.connectionString != null && isPostgresUrl(this.connectionString);
|
|
@@ -83,10 +89,30 @@ class ProjectRunner {
|
|
|
83
89
|
seen.add(d);
|
|
84
90
|
return true;
|
|
85
91
|
});
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
const excludedNames = new Set();
|
|
93
|
+
const excludedPathsByRoot = new Map();
|
|
94
|
+
for (const entry of this.exclude) {
|
|
95
|
+
if (entry.includes('/') || entry.includes('\\')) {
|
|
96
|
+
for (const root of roots) {
|
|
97
|
+
let paths = excludedPathsByRoot.get(root);
|
|
98
|
+
if (!paths) {
|
|
99
|
+
paths = new Set();
|
|
100
|
+
excludedPathsByRoot.set(root, paths);
|
|
101
|
+
}
|
|
102
|
+
paths.add(resolve(root, entry));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
excludedNames.add(entry);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const results = await Promise.all(roots.map((dir) => {
|
|
110
|
+
const excludedPaths = excludedPathsByRoot.get(dir) ?? new Set();
|
|
111
|
+
return scanDir(dir, excludedNames, excludedPaths).catch((err) => {
|
|
112
|
+
logger.warn(`Could not read directory ${dir}: ${err.message}`);
|
|
113
|
+
return [];
|
|
114
|
+
});
|
|
115
|
+
}));
|
|
90
116
|
return results.flat();
|
|
91
117
|
}
|
|
92
118
|
async importFiles(files) {
|
|
@@ -97,6 +123,7 @@ class ProjectRunner {
|
|
|
97
123
|
const results = await Promise.allSettled(files.map((file) => import(pathToFileURL(file).href)));
|
|
98
124
|
let imported = 0;
|
|
99
125
|
let failed = 0;
|
|
126
|
+
const errorGroups = new Map();
|
|
100
127
|
for (let i = 0; i < results.length; i++) {
|
|
101
128
|
const result = results[i];
|
|
102
129
|
if (result.status === 'fulfilled') {
|
|
@@ -104,10 +131,21 @@ class ProjectRunner {
|
|
|
104
131
|
}
|
|
105
132
|
else {
|
|
106
133
|
failed++;
|
|
107
|
-
const
|
|
108
|
-
|
|
134
|
+
const message = result.reason.message;
|
|
135
|
+
const group = errorGroups.get(message);
|
|
136
|
+
if (group) {
|
|
137
|
+
group.push(files[i]);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
errorGroups.set(message, [files[i]]);
|
|
141
|
+
}
|
|
109
142
|
}
|
|
110
143
|
}
|
|
144
|
+
for (const [message, affected] of errorGroups) {
|
|
145
|
+
const [first, ...rest] = affected;
|
|
146
|
+
const suffix = rest.length > 0 ? ` (also affects ${rest.length} more file(s))` : '';
|
|
147
|
+
logger.error(`Failed to import ${first}: ${message}${suffix}`);
|
|
148
|
+
}
|
|
111
149
|
if (failed > 0) {
|
|
112
150
|
logger.warn(`Imported ${imported}/${files.length} files (${failed} failed)`);
|
|
113
151
|
}
|
|
@@ -143,12 +181,17 @@ class ProjectRunner {
|
|
|
143
181
|
import('../../addon/lock/index.js'),
|
|
144
182
|
needsJobs ? import('../../addon/async/in-memory/index.js') : Promise.resolve(null),
|
|
145
183
|
]);
|
|
146
|
-
container.
|
|
147
|
-
container.
|
|
184
|
+
container.register(ChatRepository, { useToken: chatBotMod.InMemoryChatRepository });
|
|
185
|
+
container.register(Locker, { useToken: lockMod.InMemoryLocker });
|
|
186
|
+
const memoryAdapter = new MemoryRepositoryAdapter();
|
|
187
|
+
container.resolve(RepositoryAdapterRegistry).setDefault(memoryAdapter);
|
|
188
|
+
container.resolve(RepositoryMetadataStore).validateExtensionsRegistered(memoryAdapter.id);
|
|
148
189
|
if (asyncMod) {
|
|
149
|
-
container.
|
|
190
|
+
container.register(JobRepository, { useToken: asyncMod.InMemoryJobRepository });
|
|
150
191
|
if (components.cronHandlers.length > 0) {
|
|
151
|
-
container.
|
|
192
|
+
container.register(CronJobRepository, {
|
|
193
|
+
useToken: asyncMod.InMemoryCronJobRepository,
|
|
194
|
+
});
|
|
152
195
|
}
|
|
153
196
|
}
|
|
154
197
|
logger.info('Configured with in-memory adapters');
|
|
@@ -162,17 +205,20 @@ class ProjectRunner {
|
|
|
162
205
|
import('../pg/index.js'),
|
|
163
206
|
import('../../addon/async/pg/index.js'),
|
|
164
207
|
]);
|
|
165
|
-
container.
|
|
166
|
-
container.
|
|
208
|
+
container.register(ChatRepository, { useToken: chatBotMod.PgChatRepository });
|
|
209
|
+
container.register(Locker, { useToken: pgMod.PgLocker });
|
|
210
|
+
const pgAdapter = new pgMod.PgJsonRepositoryAdapter(this.pool);
|
|
211
|
+
container.resolve(RepositoryAdapterRegistry).setDefault(pgAdapter);
|
|
212
|
+
container.resolve(RepositoryMetadataStore).validateExtensionsRegistered(pgAdapter.id);
|
|
167
213
|
const transactionStore = container.resolve(TransactionMetadataStore);
|
|
168
214
|
transactionStore.registerAdapter('default', new asyncMod.PgTransactionAdapter(this.pool));
|
|
169
215
|
const hasCommandHandlers = components.commandHandlers.length > 0;
|
|
170
216
|
const hasCronHandlers = components.cronHandlers.length > 0;
|
|
171
217
|
if (hasCommandHandlers || hasCronHandlers) {
|
|
172
|
-
container.
|
|
218
|
+
container.register(JobRepository, { useToken: asyncMod.PgJobRepository });
|
|
173
219
|
}
|
|
174
220
|
if (hasCronHandlers) {
|
|
175
|
-
container.
|
|
221
|
+
container.register(CronJobRepository, { useToken: asyncMod.PgCronJobRepository });
|
|
176
222
|
}
|
|
177
223
|
logger.info('Configured with PostgreSQL adapters');
|
|
178
224
|
}
|
|
@@ -227,14 +273,19 @@ function run(config) {
|
|
|
227
273
|
function isPostgresUrl(cs) {
|
|
228
274
|
return cs.startsWith('postgres://') || cs.startsWith('postgresql://');
|
|
229
275
|
}
|
|
230
|
-
async function scanDir(dir) {
|
|
276
|
+
async function scanDir(dir, excludedNames, excludedPaths) {
|
|
231
277
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
232
278
|
const subResults = await Promise.all(entries.map(async (entry) => {
|
|
233
279
|
const name = entry.name;
|
|
280
|
+
const fullPath = join(dir, name);
|
|
281
|
+
if (excludedNames.has(name))
|
|
282
|
+
return [];
|
|
283
|
+
if (excludedPaths.has(fullPath))
|
|
284
|
+
return [];
|
|
234
285
|
if (entry.isDirectory()) {
|
|
235
286
|
if (name.startsWith('__'))
|
|
236
287
|
return [];
|
|
237
|
-
return scanDir(
|
|
288
|
+
return scanDir(fullPath, excludedNames, excludedPaths);
|
|
238
289
|
}
|
|
239
290
|
if (!entry.isFile())
|
|
240
291
|
return [];
|
|
@@ -242,7 +293,6 @@ async function scanDir(dir) {
|
|
|
242
293
|
return [];
|
|
243
294
|
if (name.endsWith('.d.ts'))
|
|
244
295
|
return [];
|
|
245
|
-
const fullPath = join(dir, name);
|
|
246
296
|
if (TEST_FILE_PATTERNS.test(fullPath))
|
|
247
297
|
return [];
|
|
248
298
|
return [fullPath];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { container } from '../../core/injection/index.js';
|
|
2
|
+
import { MEMORY_ADAPTER_ID, MemoryRepositoryExtension } from './MemoryRepositoryAdapter.js';
|
|
3
|
+
import { RepositoryMetadataStore } from './RepositoryMetadataStore.js';
|
|
4
|
+
|
|
5
|
+
function inheritsFrom(ctor, base) {
|
|
6
|
+
let proto = ctor.prototype;
|
|
7
|
+
while (proto) {
|
|
8
|
+
if (proto === base.prototype)
|
|
9
|
+
return true;
|
|
10
|
+
proto = Object.getPrototypeOf(proto);
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
function memoryExtension(repositoryClass) {
|
|
15
|
+
if (typeof repositoryClass !== 'function') {
|
|
16
|
+
throw new Error(`@memoryExtension: repository argument must be a class, ` +
|
|
17
|
+
`got ${typeof repositoryClass}`);
|
|
18
|
+
}
|
|
19
|
+
return function (target) {
|
|
20
|
+
if (!inheritsFrom(target, MemoryRepositoryExtension)) {
|
|
21
|
+
throw new Error(`@memoryExtension on ${target.name}: extension class must extend ` +
|
|
22
|
+
`MemoryRepositoryExtension.`);
|
|
23
|
+
}
|
|
24
|
+
const store = container.resolve(RepositoryMetadataStore);
|
|
25
|
+
store.saveExtension(repositoryClass, MEMORY_ADAPTER_ID, target);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { memoryExtension };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { container } from '../../core/injection/index.js';
|
|
2
|
+
import { parseQueryMethodName } from './parseQueryMethodName.js';
|
|
3
|
+
import { RepositoryMetadataStore } from './RepositoryMetadataStore.js';
|
|
4
|
+
|
|
5
|
+
function query() {
|
|
6
|
+
return function (target, propertyKey, descriptor) {
|
|
7
|
+
const functionName = propertyKey.toString();
|
|
8
|
+
const ctor = target.constructor;
|
|
9
|
+
parseQueryMethodName(functionName);
|
|
10
|
+
const value = descriptor ? descriptor.value : target[propertyKey];
|
|
11
|
+
if (value !== undefined && typeof value !== 'function') {
|
|
12
|
+
throw new Error(`@query() on ${ctor.name}.${functionName}: decorated property must be a function`);
|
|
13
|
+
}
|
|
14
|
+
const store = container.resolve(RepositoryMetadataStore);
|
|
15
|
+
store.saveQueryMethodMetadata({
|
|
16
|
+
repositoryConstructor: ctor,
|
|
17
|
+
functionName,
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { query };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { container } from '../../core/injection/index.js';
|
|
2
|
+
import { RepositoryMetadataStore } from './RepositoryMetadataStore.js';
|
|
3
|
+
|
|
4
|
+
function queryExtension() {
|
|
5
|
+
return function (target, propertyKey, descriptor) {
|
|
6
|
+
const functionName = propertyKey.toString();
|
|
7
|
+
const ctor = target.constructor;
|
|
8
|
+
const value = descriptor ? descriptor.value : target[propertyKey];
|
|
9
|
+
if (value !== undefined && typeof value !== 'function') {
|
|
10
|
+
throw new Error(`@queryExtension() on ${ctor.name}.${functionName}: ` +
|
|
11
|
+
`decorated property must be a function (typically a declare).`);
|
|
12
|
+
}
|
|
13
|
+
const store = container.resolve(RepositoryMetadataStore);
|
|
14
|
+
store.saveExtensionMethodMetadata({
|
|
15
|
+
repositoryConstructor: ctor,
|
|
16
|
+
functionName,
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { queryExtension };
|