@wabot-dev/framework 0.9.1 → 0.9.5

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.
Files changed (84) hide show
  1. package/dist/src/addon/async/in-memory/InMemoryCronJobRepository.js +45 -0
  2. package/dist/src/addon/async/in-memory/InMemoryJobRepository.js +75 -0
  3. package/dist/src/addon/async/in-memory/index.js +2 -0
  4. package/dist/src/addon/async/pg/PgCronJobRepository.js +7 -5
  5. package/dist/src/addon/async/pg/PgJobRepository.js +8 -5
  6. package/dist/src/addon/async/pg/PgTransactionAdapter.js +25 -0
  7. package/dist/src/addon/async/pg/index.js +3 -0
  8. package/dist/src/addon/auth/api-key/ApiKey.js +4 -4
  9. package/dist/src/addon/auth/api-key/PgApiKeyRepository.js +9 -8
  10. package/dist/src/addon/auth/jwt/JwtRefreshToken.js +4 -4
  11. package/dist/src/addon/auth/jwt/PgJwtRefreshTokenRepository.js +6 -5
  12. package/dist/src/addon/chat-bot/{ram/RamChatMemory.js → in-memory/InMemoryChatMemory.js} +2 -2
  13. package/dist/src/addon/chat-bot/{ram/RamChatRepository.js → in-memory/InMemoryChatRepository.js} +6 -6
  14. package/dist/src/addon/chat-bot/in-memory/index.js +2 -0
  15. package/dist/src/addon/chat-bot/pg/PgChatMemory.js +8 -7
  16. package/dist/src/addon/chat-bot/pg/PgChatRepository.js +7 -6
  17. package/dist/src/addon/chat-bot/pg/index.js +2 -0
  18. package/dist/src/addon/chat-controller/cmd/@cmd.js +7 -2
  19. package/dist/src/addon/chat-controller/cmd/CmdChannel.js +85 -61
  20. package/dist/src/addon/chat-controller/cmd/CmdChannelConfig.js +8 -0
  21. package/dist/src/addon/chat-controller/cmd/CmdChannelServer.js +169 -0
  22. package/dist/src/addon/chat-controller/cmd/cmdChannelSocketPath.js +16 -0
  23. package/dist/src/addon/chat-controller/cmd/runCmdClient.js +226 -0
  24. package/dist/src/addon/chat-controller/socket/@socket.js +3 -1
  25. package/dist/src/addon/chat-controller/socket/SocketChannelConfig.js +4 -4
  26. package/dist/src/addon/chat-controller/whatsapp/WhatsAppSender.js +2 -72
  27. package/dist/src/addon/chat-controller/whatsapp/{WhatsAppReceiver.js → cloud-api/WhatsAppApiReceiver.js} +2 -2
  28. package/dist/src/addon/chat-controller/whatsapp/cloud-api/WhatsAppApiSender.js +67 -0
  29. package/dist/src/addon/chat-controller/whatsapp/cloud-api/WhatsAppReceiverByCloudApi.js +5 -7
  30. package/dist/src/addon/chat-controller/whatsapp/wasender/@wasender.js +23 -0
  31. package/dist/src/addon/chat-controller/{wasender/WhatsAppByWasenderChannel.js → whatsapp/wasender/WasenderChannel.js} +20 -20
  32. package/dist/src/addon/chat-controller/{wasender/WhatsAppByWasenderChannelConfig.js → whatsapp/wasender/WasenderChannelConfig.js} +3 -3
  33. package/dist/src/addon/chat-controller/whatsapp/wasender/WasenderChannelName.js +3 -0
  34. package/dist/src/addon/chat-controller/{wasender/WhatsAppReceiverByWasender.js → whatsapp/wasender/WasenderReceiver.js} +7 -7
  35. package/dist/src/addon/chat-controller/{wasender/WhatsAppSenderByWasender.js → whatsapp/wasender/WasenderSender.js} +8 -5
  36. package/dist/src/addon/chat-controller/{wasender → whatsapp/wasender}/WasenderWebhookController.js +4 -4
  37. package/dist/src/addon/lock/InMemoryLockKey.js +45 -0
  38. package/dist/src/addon/lock/InMemoryLocker.js +15 -0
  39. package/dist/src/addon/lock/index.js +2 -0
  40. package/dist/src/core/repository/CrudRepository.js +25 -0
  41. package/dist/src/feature/async/{@cron.js → @cronHandler.js} +2 -2
  42. package/dist/src/feature/async/@transaction.js +22 -0
  43. package/dist/src/feature/async/AsyncMetadataStore.js +6 -0
  44. package/dist/src/feature/async/TransactionMetadataStore.js +28 -0
  45. package/dist/src/feature/chat-controller/metadata/ControllerMetadataStore.js +3 -0
  46. package/dist/src/feature/pg/@pgExtension.js +33 -0
  47. package/dist/src/feature/pg/PgJsonRepositoryAdapter.js +50 -0
  48. package/dist/src/feature/pg/index.js +10 -0
  49. package/dist/src/feature/project-runner/ProjectRunner.js +276 -0
  50. package/dist/src/feature/repository/@memoryExtension.js +29 -0
  51. package/dist/src/feature/repository/@query.js +22 -0
  52. package/dist/src/feature/repository/@queryExtension.js +21 -0
  53. package/dist/src/feature/repository/@repository.js +170 -0
  54. package/dist/src/feature/repository/MemoryRepositoryAdapter.js +110 -0
  55. package/dist/src/feature/repository/RepositoryAdapterRegistry.js +27 -0
  56. package/dist/src/feature/repository/RepositoryMetadataStore.js +102 -0
  57. package/dist/src/feature/repository/evaluateQueryAst.js +134 -0
  58. package/dist/src/feature/rest-controller/metadata/RestControllerMetadataStore.js +3 -0
  59. package/dist/src/feature/socket-controller/metadata/SocketControllerMetadataStore.js +3 -0
  60. package/dist/src/index.d.ts +377 -298
  61. package/dist/src/index.js +40 -34
  62. package/package.json +9 -7
  63. package/dist/src/addon/chat-controller/wasender/@whatsAppByWasender.js +0 -20
  64. package/dist/src/addon/chat-controller/wasender/whatsAppByWasenderChannelName.js +0 -3
  65. package/dist/src/addon/chat-controller/whatsapp/@whatsApp.js +0 -20
  66. package/dist/src/addon/chat-controller/whatsapp/EnvWhatsAppRepository.js +0 -49
  67. package/dist/src/addon/chat-controller/whatsapp/PgWhatsAppRepository.js +0 -48
  68. package/dist/src/addon/chat-controller/whatsapp/WhatsApp.js +0 -30
  69. package/dist/src/addon/chat-controller/whatsapp/WhatsAppChannel.js +0 -58
  70. package/dist/src/addon/chat-controller/whatsapp/WhatsAppChannelConfig.js +0 -8
  71. package/dist/src/addon/chat-controller/whatsapp/WhatsAppRepository.js +0 -10
  72. package/dist/src/addon/chat-controller/whatsapp/cloud-api/WhatsAppSenderByCloudApi.js +0 -133
  73. package/dist/src/addon/chat-controller/whatsapp/proxy/WhatsAppProxyContracts.js +0 -5
  74. package/dist/src/addon/chat-controller/whatsapp/proxy/WhatsAppReceiverByWabotProxy.js +0 -67
  75. package/dist/src/addon/chat-controller/whatsapp/proxy/WhatsAppSenderByWabotProxy.js +0 -63
  76. package/dist/src/addon/chat-controller/whatsapp/proxy/WhatsAppWabotProxyConnection.js +0 -45
  77. package/dist/src/feature/pg/query/@pgJsonRepository.js +0 -73
  78. package/dist/src/feature/pg/query/@query.js +0 -14
  79. package/dist/src/feature/pg/query/PgJsonRepository.js +0 -23
  80. package/dist/src/feature/pg/query/PgRepositoryMetadataStore.js +0 -44
  81. /package/dist/src/addon/chat-controller/whatsapp/{whatsAppChannelName.js → cloud-api/whatsAppChannelName.js} +0 -0
  82. /package/dist/src/addon/chat-controller/{wasender → whatsapp/wasender}/extractNumberFromWasenderKey.js +0 -0
  83. /package/dist/src/feature/pg/{query/buildQuerySql.js → buildQuerySql.js} +0 -0
  84. /package/dist/src/feature/{pg/query → repository}/parseQueryMethodName.js +0 -0
@@ -0,0 +1,276 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { resolve, join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { container } from '../../core/injection/index.js';
5
+ import { Logger } from '../../core/logger/Logger.js';
6
+ import { Locker } from '../../core/lock/Locker.js';
7
+ import '../chat-bot/ChatAdapterRegistry.js';
8
+ import '../chat-bot/ChatBot.js';
9
+ import '../chat-bot/ChatOperator.js';
10
+ import { ChatRepository } from '../chat-bot/ChatRepository.js';
11
+ import '../chat-bot/UnionChatAdapter.js';
12
+ import '../chat-bot/metadata/ChatAdapterMetadataStore.js';
13
+ import 'uuid';
14
+ import '../chat-bot/metadata/ChatBotMetadataStore.js';
15
+ import { runChatAdapters } from '../chat-bot/runChatAdapters.js';
16
+ import '../../core/error/setupErrorHandlers.js';
17
+ import { ControllerMetadataStore } from '../chat-controller/metadata/ControllerMetadataStore.js';
18
+ import '../chat-controller/ChatResolver.js';
19
+ import { runChatControllers } from '../chat-controller/runChatControllers.js';
20
+ import { RestControllerMetadataStore } from '../rest-controller/metadata/RestControllerMetadataStore.js';
21
+ import { runRestControllers } from '../rest-controller/runRestControllers.js';
22
+ import { AsyncMetadataStore } from '../async/AsyncMetadataStore.js';
23
+ import { TransactionMetadataStore } from '../async/TransactionMetadataStore.js';
24
+ import '../async/Async.js';
25
+ import '../../_virtual/index.js';
26
+ import { CronJobRepository } from '../async/CronJobRepository.js';
27
+ import { JobRepository } from '../async/JobRepository.js';
28
+ import '../async/JobRunner.js';
29
+ import { runCommandHandlers } from '../async/runCommandHandlers.js';
30
+ import { runCronHandlers } from '../async/runCronHandlers.js';
31
+ import { SocketControllerMetadataStore } from '../socket-controller/metadata/SocketControllerMetadataStore.js';
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';
36
+
37
+ const logger = new Logger('wabot:project-runner');
38
+ const TEST_FILE_PATTERNS = /\.(test|spec|unit|integration|e2e|multiprocess)\.(ts|js)$/;
39
+ const DEFAULT_CHAT_ADAPTERS = [
40
+ ['../../addon/chat-bot/openia', 'OpenaiChatAdapter'],
41
+ ['../../addon/chat-bot/openrouter', 'OpenRouterChatAdapter'],
42
+ ['../../addon/chat-bot/anthropic', 'AnthropicChatAdapter'],
43
+ ['../../addon/chat-bot/google', 'GoogleChatAdapter'],
44
+ ];
45
+ class ProjectRunner {
46
+ directories;
47
+ chatAdapters;
48
+ connectionString;
49
+ isPg;
50
+ pool = null;
51
+ constructor(config = {}) {
52
+ this.directories = config.directories ?? ['src'];
53
+ this.chatAdapters = config.chatAdapters;
54
+ this.connectionString = this.resolveConnectionString(config.connectionString);
55
+ this.isPg = this.connectionString != null && isPostgresUrl(this.connectionString);
56
+ }
57
+ async run() {
58
+ const [, files] = await Promise.all([
59
+ this.isPg ? this.initPool() : Promise.resolve(),
60
+ this.scanDirectories(),
61
+ ]);
62
+ await this.importFiles(files);
63
+ const components = this.discoverComponents();
64
+ await this.registerAdapters(components);
65
+ await this.startComponents(components);
66
+ }
67
+ resolveConnectionString(configValue) {
68
+ const cs = configValue ?? process.env.DATABASE_URL ?? null;
69
+ if (cs && !isPostgresUrl(cs)) {
70
+ logger.warn(`connectionString "${cs}" does not match a known scheme (postgres://, postgresql://); falling back to in-memory adapters`);
71
+ }
72
+ return cs;
73
+ }
74
+ async initPool() {
75
+ const { Pool } = await import('pg');
76
+ this.pool = new Pool({ connectionString: this.connectionString });
77
+ container.registerInstance(Pool, this.pool);
78
+ }
79
+ async scanDirectories() {
80
+ const seen = new Set();
81
+ const roots = this.directories
82
+ .map((d) => resolve(d))
83
+ .filter((d) => {
84
+ if (seen.has(d))
85
+ return false;
86
+ seen.add(d);
87
+ return true;
88
+ });
89
+ const results = await Promise.all(roots.map((dir) => scanDir(dir).catch((err) => {
90
+ logger.warn(`Could not read directory ${dir}: ${err.message}`);
91
+ return [];
92
+ })));
93
+ return results.flat();
94
+ }
95
+ async importFiles(files) {
96
+ if (files.length === 0) {
97
+ logger.info('No files to import');
98
+ return;
99
+ }
100
+ const results = await Promise.allSettled(files.map((file) => import(pathToFileURL(file).href)));
101
+ let imported = 0;
102
+ let failed = 0;
103
+ const errorGroups = new Map();
104
+ for (let i = 0; i < results.length; i++) {
105
+ const result = results[i];
106
+ if (result.status === 'fulfilled') {
107
+ imported++;
108
+ }
109
+ else {
110
+ failed++;
111
+ const message = result.reason.message;
112
+ const group = errorGroups.get(message);
113
+ if (group) {
114
+ group.push(files[i]);
115
+ }
116
+ else {
117
+ errorGroups.set(message, [files[i]]);
118
+ }
119
+ }
120
+ }
121
+ for (const [message, affected] of errorGroups) {
122
+ const [first, ...rest] = affected;
123
+ const suffix = rest.length > 0 ? ` (also affects ${rest.length} more file(s))` : '';
124
+ logger.error(`Failed to import ${first}: ${message}${suffix}`);
125
+ }
126
+ if (failed > 0) {
127
+ logger.warn(`Imported ${imported}/${files.length} files (${failed} failed)`);
128
+ }
129
+ else {
130
+ logger.info(`Imported ${imported}/${files.length} files`);
131
+ }
132
+ }
133
+ discoverComponents() {
134
+ const asyncStore = container.resolve(AsyncMetadataStore);
135
+ return {
136
+ chatControllers: container
137
+ .resolve(ControllerMetadataStore)
138
+ .getAllChatControllerConstructors(),
139
+ restControllers: container
140
+ .resolve(RestControllerMetadataStore)
141
+ .getAllRestControllerConstructors(),
142
+ commandHandlers: asyncStore.getAllCommandHandlers(),
143
+ cronHandlers: asyncStore.getAllCronHandlers(),
144
+ socketControllers: container
145
+ .resolve(SocketControllerMetadataStore)
146
+ .getAllSocketControllerConstructors(),
147
+ };
148
+ }
149
+ registerAdapters(components) {
150
+ return this.isPg
151
+ ? this.registerPostgresAdapters(components)
152
+ : this.registerMemoryAdapters(components);
153
+ }
154
+ async registerMemoryAdapters(components) {
155
+ const needsJobs = components.commandHandlers.length > 0 || components.cronHandlers.length > 0;
156
+ const [chatBotMod, lockMod, asyncMod] = await Promise.all([
157
+ import('../../addon/chat-bot/in-memory/index.js'),
158
+ import('../../addon/lock/index.js'),
159
+ needsJobs ? import('../../addon/async/in-memory/index.js') : Promise.resolve(null),
160
+ ]);
161
+ container.register(ChatRepository, { useToken: chatBotMod.InMemoryChatRepository });
162
+ container.register(Locker, { useToken: lockMod.InMemoryLocker });
163
+ const memoryAdapter = new MemoryRepositoryAdapter();
164
+ container.resolve(RepositoryAdapterRegistry).setDefault(memoryAdapter);
165
+ container.resolve(RepositoryMetadataStore).validateExtensionsRegistered(memoryAdapter.id);
166
+ if (asyncMod) {
167
+ container.register(JobRepository, { useToken: asyncMod.InMemoryJobRepository });
168
+ if (components.cronHandlers.length > 0) {
169
+ container.register(CronJobRepository, {
170
+ useToken: asyncMod.InMemoryCronJobRepository,
171
+ });
172
+ }
173
+ }
174
+ logger.info('Configured with in-memory adapters');
175
+ }
176
+ async registerPostgresAdapters(components) {
177
+ if (!this.pool) {
178
+ throw new Error('Postgres pool was not initialized');
179
+ }
180
+ const [chatBotMod, pgMod, asyncMod] = await Promise.all([
181
+ import('../../addon/chat-bot/pg/index.js'),
182
+ import('../pg/index.js'),
183
+ import('../../addon/async/pg/index.js'),
184
+ ]);
185
+ container.register(ChatRepository, { useToken: chatBotMod.PgChatRepository });
186
+ container.register(Locker, { useToken: pgMod.PgLocker });
187
+ const pgAdapter = new pgMod.PgJsonRepositoryAdapter(this.pool);
188
+ container.resolve(RepositoryAdapterRegistry).setDefault(pgAdapter);
189
+ container.resolve(RepositoryMetadataStore).validateExtensionsRegistered(pgAdapter.id);
190
+ const transactionStore = container.resolve(TransactionMetadataStore);
191
+ transactionStore.registerAdapter('default', new asyncMod.PgTransactionAdapter(this.pool));
192
+ const hasCommandHandlers = components.commandHandlers.length > 0;
193
+ const hasCronHandlers = components.cronHandlers.length > 0;
194
+ if (hasCommandHandlers || hasCronHandlers) {
195
+ container.register(JobRepository, { useToken: asyncMod.PgJobRepository });
196
+ }
197
+ if (hasCronHandlers) {
198
+ container.register(CronJobRepository, { useToken: asyncMod.PgCronJobRepository });
199
+ }
200
+ logger.info('Configured with PostgreSQL adapters');
201
+ }
202
+ async startComponents(components) {
203
+ const chatAdapters = this.chatAdapters ?? (await this.resolveDefaultChatAdapters());
204
+ if (chatAdapters.length > 0) {
205
+ runChatAdapters(chatAdapters);
206
+ }
207
+ if (components.chatControllers.length > 0) {
208
+ logger.info(`Starting ${components.chatControllers.length} chat controller(s)`);
209
+ runChatControllers(components.chatControllers);
210
+ }
211
+ if (components.restControllers.length > 0) {
212
+ logger.info(`Starting ${components.restControllers.length} REST controller(s)`);
213
+ runRestControllers(components.restControllers);
214
+ }
215
+ if (components.commandHandlers.length > 0) {
216
+ logger.info(`Starting ${components.commandHandlers.length} command handler(s)`);
217
+ runCommandHandlers(components.commandHandlers);
218
+ }
219
+ if (components.cronHandlers.length > 0) {
220
+ logger.info(`Starting ${components.cronHandlers.length} cron handler(s)`);
221
+ runCronHandlers(components.cronHandlers);
222
+ }
223
+ if (components.socketControllers.length > 0) {
224
+ logger.info(`Starting ${components.socketControllers.length} socket controller(s)`);
225
+ runSocketControllers(components.socketControllers);
226
+ }
227
+ }
228
+ async resolveDefaultChatAdapters() {
229
+ const results = await Promise.all(DEFAULT_CHAT_ADAPTERS.map(async ([path, name]) => {
230
+ try {
231
+ const mod = await import(path);
232
+ const adapter = mod[name];
233
+ if (!adapter) {
234
+ logger.warn(`Skipping ${name}: module loaded but no '${name}' export found`);
235
+ return null;
236
+ }
237
+ return adapter;
238
+ }
239
+ catch {
240
+ logger.warn(`Skipping ${name}: missing peer dependency`);
241
+ return null;
242
+ }
243
+ }));
244
+ return results.filter((a) => a != null);
245
+ }
246
+ }
247
+ function run(config) {
248
+ return new ProjectRunner(config).run();
249
+ }
250
+ function isPostgresUrl(cs) {
251
+ return cs.startsWith('postgres://') || cs.startsWith('postgresql://');
252
+ }
253
+ async function scanDir(dir) {
254
+ const entries = await readdir(dir, { withFileTypes: true });
255
+ const subResults = await Promise.all(entries.map(async (entry) => {
256
+ const name = entry.name;
257
+ if (entry.isDirectory()) {
258
+ if (name.startsWith('__'))
259
+ return [];
260
+ return scanDir(join(dir, name));
261
+ }
262
+ if (!entry.isFile())
263
+ return [];
264
+ if (!(name.endsWith('.ts') || name.endsWith('.js')))
265
+ return [];
266
+ if (name.endsWith('.d.ts'))
267
+ return [];
268
+ const fullPath = join(dir, name);
269
+ if (TEST_FILE_PATTERNS.test(fullPath))
270
+ return [];
271
+ return [fullPath];
272
+ }));
273
+ return subResults.flat();
274
+ }
275
+
276
+ export { ProjectRunner, run };
@@ -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 };
@@ -0,0 +1,170 @@
1
+ import { container, singleton } from '../../core/injection/index.js';
2
+ import { parseQueryMethodName } from './parseQueryMethodName.js';
3
+ import { RepositoryAdapterRegistry } from './RepositoryAdapterRegistry.js';
4
+ import { RepositoryMetadataStore } from './RepositoryMetadataStore.js';
5
+
6
+ const RUNTIME_KEY = Symbol('wabot:repositoryRuntime');
7
+ const EXTENSION_KEY = Symbol('wabot:repositoryExtension');
8
+ const AST_CACHE_KEY = Symbol('wabot:repositoryAstCache');
9
+ function getConfig(self) {
10
+ const ctor = self.constructor;
11
+ const config = container.resolve(RepositoryMetadataStore).getRepositoryConfig(ctor);
12
+ if (!config) {
13
+ throw new Error(`${ctor.name} must be decorated with @repository`);
14
+ }
15
+ return config;
16
+ }
17
+ function getRuntime(self) {
18
+ let runtime = self[RUNTIME_KEY];
19
+ if (runtime)
20
+ return runtime;
21
+ const config = getConfig(self);
22
+ const adapter = container.resolve(RepositoryAdapterRegistry).getDefault();
23
+ runtime = adapter.build(config);
24
+ Object.defineProperty(self, RUNTIME_KEY, { value: runtime, enumerable: false });
25
+ return runtime;
26
+ }
27
+ function getExtension(self) {
28
+ const cached = self[EXTENSION_KEY];
29
+ if (cached !== undefined)
30
+ return cached;
31
+ const ctor = self.constructor;
32
+ const adapter = container.resolve(RepositoryAdapterRegistry).getDefault();
33
+ const store = container.resolve(RepositoryMetadataStore);
34
+ const ExtensionCtor = store.getExtension(ctor, adapter.id);
35
+ if (!ExtensionCtor) {
36
+ throw new Error(`${ctor.name}.extension is not available: no extension registered ` +
37
+ `for adapter "${adapter.id.description ?? 'unknown'}". ` +
38
+ `Import the extension class so its @<adapter>Extension decorator runs, ` +
39
+ `or check the active adapter.`);
40
+ }
41
+ if (typeof adapter.buildExtension !== 'function') {
42
+ throw new Error(`${ctor.name}.extension cannot be built: adapter ` +
43
+ `"${adapter.id.description ?? 'unknown'}" does not implement buildExtension().`);
44
+ }
45
+ const config = getConfig(self);
46
+ const ext = adapter.buildExtension(config, ExtensionCtor);
47
+ Object.defineProperty(self, EXTENSION_KEY, { value: ext, enumerable: false });
48
+ return ext;
49
+ }
50
+ function installExtensionAccessor(target) {
51
+ const proto = target.prototype;
52
+ if (Object.getOwnPropertyDescriptor(proto, 'extension'))
53
+ return;
54
+ Object.defineProperty(proto, 'extension', {
55
+ get() {
56
+ return getExtension(this);
57
+ },
58
+ configurable: true,
59
+ enumerable: false,
60
+ });
61
+ }
62
+ function getAst(self, methodName) {
63
+ let cache = self[AST_CACHE_KEY];
64
+ if (!cache) {
65
+ cache = new Map();
66
+ Object.defineProperty(self, AST_CACHE_KEY, { value: cache, enumerable: false });
67
+ }
68
+ let ast = cache.get(methodName);
69
+ if (!ast) {
70
+ ast = parseQueryMethodName(methodName);
71
+ cache.set(methodName, ast);
72
+ }
73
+ return ast;
74
+ }
75
+ function makeExtensionImpl(methodName) {
76
+ return function (...args) {
77
+ const ext = getExtension(this);
78
+ const fn = ext[methodName];
79
+ if (typeof fn !== 'function') {
80
+ throw new Error(`${this.constructor.name}.${methodName}: ` +
81
+ `the active adapter's extension does not implement this method.`);
82
+ }
83
+ return fn.apply(ext, args);
84
+ };
85
+ }
86
+ function makeQueryImpl(methodName) {
87
+ return async function (...args) {
88
+ const ast = getAst(this, methodName);
89
+ const runtime = getRuntime(this);
90
+ switch (ast.prefix) {
91
+ case 'find':
92
+ return runtime.runQuery(ast, args);
93
+ case 'findOne': {
94
+ const rows = await runtime.runQuery(ast, args);
95
+ return rows[0] ?? null;
96
+ }
97
+ case 'count':
98
+ return runtime.runCount(ast, args);
99
+ case 'exists':
100
+ return runtime.runExists(ast, args);
101
+ case 'delete':
102
+ return runtime.runDelete(ast, args);
103
+ }
104
+ };
105
+ }
106
+ const CRUD_METHODS = {
107
+ async find(id) {
108
+ return getRuntime(this).find(id);
109
+ },
110
+ async findOrThrow(id) {
111
+ return getRuntime(this).findOrThrow(id);
112
+ },
113
+ async findByIds(ids) {
114
+ return getRuntime(this).findByIds(ids);
115
+ },
116
+ async findAll() {
117
+ return getRuntime(this).findAll();
118
+ },
119
+ async create(item) {
120
+ return getRuntime(this).create(item);
121
+ },
122
+ async update(item) {
123
+ return getRuntime(this).update(item);
124
+ },
125
+ async delete(item) {
126
+ return getRuntime(this).delete(item);
127
+ },
128
+ };
129
+ function installCrudMethods(target) {
130
+ const proto = target.prototype;
131
+ for (const [name, fn] of Object.entries(CRUD_METHODS)) {
132
+ if (Object.prototype.hasOwnProperty.call(proto, name)) {
133
+ const existing = proto[name];
134
+ if (typeof existing === 'function')
135
+ continue;
136
+ }
137
+ Object.defineProperty(proto, name, { value: fn, writable: true, configurable: true });
138
+ }
139
+ }
140
+ function repository(config) {
141
+ return function (target) {
142
+ const store = container.resolve(RepositoryMetadataStore);
143
+ store.saveRepositoryConfig(target, config);
144
+ installCrudMethods(target);
145
+ installExtensionAccessor(target);
146
+ const queryMethods = store.getQueryMethods(target);
147
+ for (const meta of queryMethods) {
148
+ if (Object.prototype.hasOwnProperty.call(target.prototype, meta.functionName)) {
149
+ const existing = target.prototype[meta.functionName];
150
+ if (typeof existing === 'function' && existing.length > 0) {
151
+ continue;
152
+ }
153
+ }
154
+ target.prototype[meta.functionName] = makeQueryImpl(meta.functionName);
155
+ }
156
+ const extensionMethods = store.getExtensionMethods(target);
157
+ for (const meta of extensionMethods) {
158
+ if (Object.prototype.hasOwnProperty.call(target.prototype, meta.functionName)) {
159
+ const existing = target.prototype[meta.functionName];
160
+ if (typeof existing === 'function' && existing.length > 0) {
161
+ continue;
162
+ }
163
+ }
164
+ target.prototype[meta.functionName] = makeExtensionImpl(meta.functionName);
165
+ }
166
+ singleton()(target);
167
+ };
168
+ }
169
+
170
+ export { repository };
@@ -0,0 +1,110 @@
1
+ import { generate } from 'short-uuid';
2
+ import { CustomError } from '../../core/error/CustomError.js';
3
+ import '../../core/error/setupErrorHandlers.js';
4
+ import { evaluateQueryAst } from './evaluateQueryAst.js';
5
+
6
+ function cloneEntity(config, item) {
7
+ const data = JSON.parse(JSON.stringify(item['data']));
8
+ return new config.constructor(data);
9
+ }
10
+ class MemoryRepositoryExtension {
11
+ items;
12
+ config;
13
+ constructor(items, config) {
14
+ this.items = items;
15
+ this.config = config;
16
+ }
17
+ clone(item) {
18
+ return cloneEntity(this.config, item);
19
+ }
20
+ }
21
+ class MemoryRepositoryRuntime {
22
+ items;
23
+ config;
24
+ constructor(items, config) {
25
+ this.items = items;
26
+ this.config = config;
27
+ }
28
+ async find(id) {
29
+ const item = this.items.get(id);
30
+ return item ? cloneEntity(this.config, item) : null;
31
+ }
32
+ async findOrThrow(id) {
33
+ const item = await this.find(id);
34
+ if (!item) {
35
+ throw new CustomError({
36
+ message: `Not found ${this.config.constructor.name} with id = '${id}'`,
37
+ httpCode: 404,
38
+ });
39
+ }
40
+ return item;
41
+ }
42
+ async findByIds(ids) {
43
+ const out = [];
44
+ for (const id of ids) {
45
+ const item = this.items.get(id);
46
+ if (item)
47
+ out.push(cloneEntity(this.config, item));
48
+ }
49
+ return out;
50
+ }
51
+ async findAll() {
52
+ return [...this.items.values()].map((i) => cloneEntity(this.config, i));
53
+ }
54
+ async create(item) {
55
+ if (item.wasCreated()) {
56
+ throw new Error('Item already created');
57
+ }
58
+ item['data'].id = generate();
59
+ item['data'].createdAt = new Date().getTime();
60
+ item.validate();
61
+ this.items.set(item.id, cloneEntity(this.config, item));
62
+ }
63
+ async update(item) {
64
+ item.validate();
65
+ if (!this.items.has(item.id)) {
66
+ throw new Error(`Update failed: no affected rows`);
67
+ }
68
+ this.items.set(item.id, cloneEntity(this.config, item));
69
+ }
70
+ async delete(item) {
71
+ this.items.delete(item.id);
72
+ }
73
+ async runQuery(ast, args) {
74
+ const result = evaluateQueryAst(this.items.values(), ast, args);
75
+ return result.map((i) => cloneEntity(this.config, i));
76
+ }
77
+ async runCount(ast, args) {
78
+ return evaluateQueryAst(this.items.values(), ast, args).length;
79
+ }
80
+ async runExists(ast, args) {
81
+ return evaluateQueryAst(this.items.values(), ast, args).length > 0;
82
+ }
83
+ async runDelete(ast, args) {
84
+ const matched = evaluateQueryAst(this.items.values(), ast, args);
85
+ for (const item of matched) {
86
+ this.items.delete(item.id);
87
+ }
88
+ }
89
+ }
90
+ const MEMORY_ADAPTER_ID = Symbol('wabot:memory-adapter');
91
+ class MemoryRepositoryAdapter {
92
+ id = MEMORY_ADAPTER_ID;
93
+ stores = new Map();
94
+ getStore(config) {
95
+ let store = this.stores.get(config);
96
+ if (!store) {
97
+ store = new Map();
98
+ this.stores.set(config, store);
99
+ }
100
+ return store;
101
+ }
102
+ build(config) {
103
+ return new MemoryRepositoryRuntime(this.getStore(config), config);
104
+ }
105
+ buildExtension(config, ExtensionCtor) {
106
+ return new ExtensionCtor(this.getStore(config), config);
107
+ }
108
+ }
109
+
110
+ export { MEMORY_ADAPTER_ID, MemoryRepositoryAdapter, MemoryRepositoryExtension };
@@ -0,0 +1,27 @@
1
+ import { __decorate } from 'tslib';
2
+ import { singleton } from '../../core/injection/index.js';
3
+
4
+ let RepositoryAdapterRegistry = class RepositoryAdapterRegistry {
5
+ adapter = null;
6
+ setDefault(adapter) {
7
+ this.adapter = adapter;
8
+ }
9
+ getDefault() {
10
+ if (!this.adapter) {
11
+ throw new Error('No repository adapter registered. ' +
12
+ 'Register one with container.resolve(RepositoryAdapterRegistry).setDefault(adapter).');
13
+ }
14
+ return this.adapter;
15
+ }
16
+ hasDefault() {
17
+ return this.adapter !== null;
18
+ }
19
+ clear() {
20
+ this.adapter = null;
21
+ }
22
+ };
23
+ RepositoryAdapterRegistry = __decorate([
24
+ singleton()
25
+ ], RepositoryAdapterRegistry);
26
+
27
+ export { RepositoryAdapterRegistry };