servcraft 0.1.0 → 0.1.3

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 (217) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/LICENSE +21 -0
  9. package/README.md +1102 -1
  10. package/dist/cli/index.cjs +2026 -2168
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +2026 -2168
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/index.cjs +595 -616
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +114 -52
  17. package/dist/index.d.ts +114 -52
  18. package/dist/index.js +595 -616
  19. package/dist/index.js.map +1 -1
  20. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  21. package/docs/DATABASE_MULTI_ORM.md +399 -0
  22. package/docs/PHASE1_BREAKDOWN.md +346 -0
  23. package/docs/PROGRESS.md +550 -0
  24. package/docs/modules/ANALYTICS.md +226 -0
  25. package/docs/modules/API-VERSIONING.md +252 -0
  26. package/docs/modules/AUDIT.md +192 -0
  27. package/docs/modules/AUTH.md +431 -0
  28. package/docs/modules/CACHE.md +346 -0
  29. package/docs/modules/EMAIL.md +254 -0
  30. package/docs/modules/FEATURE-FLAG.md +291 -0
  31. package/docs/modules/I18N.md +294 -0
  32. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  33. package/docs/modules/MFA.md +266 -0
  34. package/docs/modules/NOTIFICATION.md +311 -0
  35. package/docs/modules/OAUTH.md +237 -0
  36. package/docs/modules/PAYMENT.md +804 -0
  37. package/docs/modules/QUEUE.md +540 -0
  38. package/docs/modules/RATE-LIMIT.md +339 -0
  39. package/docs/modules/SEARCH.md +288 -0
  40. package/docs/modules/SECURITY.md +327 -0
  41. package/docs/modules/SESSION.md +382 -0
  42. package/docs/modules/SWAGGER.md +305 -0
  43. package/docs/modules/UPLOAD.md +296 -0
  44. package/docs/modules/USER.md +505 -0
  45. package/docs/modules/VALIDATION.md +294 -0
  46. package/docs/modules/WEBHOOK.md +270 -0
  47. package/docs/modules/WEBSOCKET.md +691 -0
  48. package/package.json +53 -38
  49. package/prisma/schema.prisma +395 -1
  50. package/src/cli/commands/add-module.ts +520 -87
  51. package/src/cli/commands/db.ts +3 -4
  52. package/src/cli/commands/docs.ts +256 -6
  53. package/src/cli/commands/generate.ts +12 -19
  54. package/src/cli/commands/init.ts +384 -214
  55. package/src/cli/index.ts +0 -4
  56. package/src/cli/templates/repository.ts +6 -1
  57. package/src/cli/templates/routes.ts +6 -21
  58. package/src/cli/utils/docs-generator.ts +6 -7
  59. package/src/cli/utils/env-manager.ts +717 -0
  60. package/src/cli/utils/field-parser.ts +16 -7
  61. package/src/cli/utils/interactive-prompt.ts +223 -0
  62. package/src/cli/utils/template-manager.ts +346 -0
  63. package/src/config/database.config.ts +183 -0
  64. package/src/config/env.ts +0 -10
  65. package/src/config/index.ts +0 -14
  66. package/src/core/server.ts +1 -1
  67. package/src/database/adapters/mongoose.adapter.ts +132 -0
  68. package/src/database/adapters/prisma.adapter.ts +118 -0
  69. package/src/database/connection.ts +190 -0
  70. package/src/database/interfaces/database.interface.ts +85 -0
  71. package/src/database/interfaces/index.ts +7 -0
  72. package/src/database/interfaces/repository.interface.ts +129 -0
  73. package/src/database/models/mongoose/index.ts +7 -0
  74. package/src/database/models/mongoose/payment.schema.ts +347 -0
  75. package/src/database/models/mongoose/user.schema.ts +154 -0
  76. package/src/database/prisma.ts +1 -4
  77. package/src/database/redis.ts +101 -0
  78. package/src/database/repositories/mongoose/index.ts +7 -0
  79. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  80. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  81. package/src/database/seed.ts +6 -1
  82. package/src/index.ts +9 -20
  83. package/src/middleware/security.ts +2 -6
  84. package/src/modules/analytics/analytics.routes.ts +80 -0
  85. package/src/modules/analytics/analytics.service.ts +364 -0
  86. package/src/modules/analytics/index.ts +18 -0
  87. package/src/modules/analytics/types.ts +180 -0
  88. package/src/modules/api-versioning/index.ts +15 -0
  89. package/src/modules/api-versioning/types.ts +86 -0
  90. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  91. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  92. package/src/modules/api-versioning/versioning.service.ts +189 -0
  93. package/src/modules/audit/audit.repository.ts +206 -0
  94. package/src/modules/audit/audit.service.ts +27 -59
  95. package/src/modules/auth/auth.controller.ts +2 -2
  96. package/src/modules/auth/auth.middleware.ts +3 -9
  97. package/src/modules/auth/auth.routes.ts +10 -107
  98. package/src/modules/auth/auth.service.ts +126 -23
  99. package/src/modules/auth/index.ts +3 -4
  100. package/src/modules/cache/cache.service.ts +367 -0
  101. package/src/modules/cache/index.ts +10 -0
  102. package/src/modules/cache/types.ts +44 -0
  103. package/src/modules/email/email.service.ts +3 -10
  104. package/src/modules/email/templates.ts +2 -8
  105. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  106. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  107. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  108. package/src/modules/feature-flag/index.ts +20 -0
  109. package/src/modules/feature-flag/types.ts +192 -0
  110. package/src/modules/i18n/i18n.middleware.ts +186 -0
  111. package/src/modules/i18n/i18n.routes.ts +191 -0
  112. package/src/modules/i18n/i18n.service.ts +456 -0
  113. package/src/modules/i18n/index.ts +18 -0
  114. package/src/modules/i18n/types.ts +118 -0
  115. package/src/modules/media-processing/index.ts +17 -0
  116. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  117. package/src/modules/media-processing/media-processing.service.ts +245 -0
  118. package/src/modules/media-processing/types.ts +156 -0
  119. package/src/modules/mfa/index.ts +20 -0
  120. package/src/modules/mfa/mfa.repository.ts +206 -0
  121. package/src/modules/mfa/mfa.routes.ts +595 -0
  122. package/src/modules/mfa/mfa.service.ts +572 -0
  123. package/src/modules/mfa/totp.ts +150 -0
  124. package/src/modules/mfa/types.ts +57 -0
  125. package/src/modules/notification/index.ts +20 -0
  126. package/src/modules/notification/notification.repository.ts +356 -0
  127. package/src/modules/notification/notification.service.ts +483 -0
  128. package/src/modules/notification/types.ts +119 -0
  129. package/src/modules/oauth/index.ts +20 -0
  130. package/src/modules/oauth/oauth.repository.ts +219 -0
  131. package/src/modules/oauth/oauth.routes.ts +446 -0
  132. package/src/modules/oauth/oauth.service.ts +293 -0
  133. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  134. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  135. package/src/modules/oauth/providers/github.provider.ts +248 -0
  136. package/src/modules/oauth/providers/google.provider.ts +189 -0
  137. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  138. package/src/modules/oauth/types.ts +94 -0
  139. package/src/modules/payment/index.ts +19 -0
  140. package/src/modules/payment/payment.repository.ts +733 -0
  141. package/src/modules/payment/payment.routes.ts +390 -0
  142. package/src/modules/payment/payment.service.ts +354 -0
  143. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  144. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  145. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  146. package/src/modules/payment/types.ts +140 -0
  147. package/src/modules/queue/cron.ts +438 -0
  148. package/src/modules/queue/index.ts +87 -0
  149. package/src/modules/queue/queue.routes.ts +600 -0
  150. package/src/modules/queue/queue.service.ts +842 -0
  151. package/src/modules/queue/types.ts +222 -0
  152. package/src/modules/queue/workers.ts +366 -0
  153. package/src/modules/rate-limit/index.ts +59 -0
  154. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  155. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  156. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  157. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  158. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  159. package/src/modules/rate-limit/types.ts +153 -0
  160. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  161. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  162. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  163. package/src/modules/search/index.ts +21 -0
  164. package/src/modules/search/search.service.ts +234 -0
  165. package/src/modules/search/types.ts +214 -0
  166. package/src/modules/security/index.ts +40 -0
  167. package/src/modules/security/sanitize.ts +223 -0
  168. package/src/modules/security/security-audit.service.ts +388 -0
  169. package/src/modules/security/security.middleware.ts +398 -0
  170. package/src/modules/session/index.ts +3 -0
  171. package/src/modules/session/session.repository.ts +159 -0
  172. package/src/modules/session/session.service.ts +340 -0
  173. package/src/modules/session/types.ts +38 -0
  174. package/src/modules/swagger/index.ts +7 -1
  175. package/src/modules/swagger/schema-builder.ts +16 -4
  176. package/src/modules/swagger/swagger.service.ts +9 -10
  177. package/src/modules/swagger/types.ts +0 -2
  178. package/src/modules/upload/index.ts +14 -0
  179. package/src/modules/upload/types.ts +83 -0
  180. package/src/modules/upload/upload.repository.ts +199 -0
  181. package/src/modules/upload/upload.routes.ts +311 -0
  182. package/src/modules/upload/upload.service.ts +448 -0
  183. package/src/modules/user/index.ts +3 -3
  184. package/src/modules/user/user.controller.ts +15 -9
  185. package/src/modules/user/user.repository.ts +237 -113
  186. package/src/modules/user/user.routes.ts +39 -164
  187. package/src/modules/user/user.service.ts +4 -3
  188. package/src/modules/validation/validator.ts +12 -17
  189. package/src/modules/webhook/index.ts +91 -0
  190. package/src/modules/webhook/retry.ts +196 -0
  191. package/src/modules/webhook/signature.ts +135 -0
  192. package/src/modules/webhook/types.ts +181 -0
  193. package/src/modules/webhook/webhook.repository.ts +358 -0
  194. package/src/modules/webhook/webhook.routes.ts +442 -0
  195. package/src/modules/webhook/webhook.service.ts +457 -0
  196. package/src/modules/websocket/features.ts +504 -0
  197. package/src/modules/websocket/index.ts +106 -0
  198. package/src/modules/websocket/middlewares.ts +298 -0
  199. package/src/modules/websocket/types.ts +181 -0
  200. package/src/modules/websocket/websocket.service.ts +692 -0
  201. package/src/utils/errors.ts +7 -0
  202. package/src/utils/pagination.ts +4 -1
  203. package/tests/helpers/db-check.ts +79 -0
  204. package/tests/integration/auth-redis.test.ts +94 -0
  205. package/tests/integration/cache-redis.test.ts +387 -0
  206. package/tests/integration/mongoose-repositories.test.ts +410 -0
  207. package/tests/integration/payment-prisma.test.ts +637 -0
  208. package/tests/integration/queue-bullmq.test.ts +417 -0
  209. package/tests/integration/user-prisma.test.ts +441 -0
  210. package/tests/integration/websocket-socketio.test.ts +552 -0
  211. package/tests/setup.ts +11 -9
  212. package/vitest.config.ts +3 -8
  213. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  216. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  217. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -0,0 +1,278 @@
1
+ import type {
2
+ SearchEngine,
3
+ SearchDocument,
4
+ SearchQuery,
5
+ SearchResult,
6
+ IndexSettings,
7
+ BulkIndexOperation,
8
+ BulkIndexResult,
9
+ IndexStats,
10
+ AutocompleteResult,
11
+ } from '../types.js';
12
+
13
+ interface IndexData {
14
+ documents: Map<string, SearchDocument>;
15
+ settings: IndexSettings;
16
+ createdAt: Date;
17
+ updatedAt: Date;
18
+ }
19
+
20
+ /**
21
+ * In-Memory Search Adapter
22
+ * For development and testing
23
+ */
24
+ export class MemorySearchAdapter implements SearchEngine {
25
+ private indexes = new Map<string, IndexData>();
26
+
27
+ async createIndex(indexName: string, settings?: IndexSettings): Promise<void> {
28
+ this.indexes.set(indexName, {
29
+ documents: new Map(),
30
+ settings: settings || {},
31
+ createdAt: new Date(),
32
+ updatedAt: new Date(),
33
+ });
34
+ }
35
+
36
+ async deleteIndex(indexName: string): Promise<void> {
37
+ this.indexes.delete(indexName);
38
+ }
39
+
40
+ async index(indexName: string, id: string, document: SearchDocument): Promise<void> {
41
+ if (!this.indexes.has(indexName)) {
42
+ await this.createIndex(indexName);
43
+ }
44
+
45
+ const index = this.indexes.get(indexName)!;
46
+ index.documents.set(id, { ...document, id });
47
+ index.updatedAt = new Date();
48
+ }
49
+
50
+ async bulkIndex(indexName: string, operations: BulkIndexOperation[]): Promise<BulkIndexResult> {
51
+ const startTime = Date.now();
52
+ let success = 0;
53
+ let failed = 0;
54
+ const errors: Array<{ id: string; error: string }> = [];
55
+
56
+ for (const op of operations) {
57
+ try {
58
+ if (op.operation === 'index' || op.operation === 'update') {
59
+ if (op.document) {
60
+ await this.index(indexName, op.id, op.document);
61
+ success++;
62
+ }
63
+ } else if (op.operation === 'delete') {
64
+ await this.delete(indexName, op.id);
65
+ success++;
66
+ }
67
+ } catch (error) {
68
+ failed++;
69
+ errors.push({
70
+ id: op.id,
71
+ error: error instanceof Error ? error.message : 'Unknown error',
72
+ });
73
+ }
74
+ }
75
+
76
+ return {
77
+ success,
78
+ failed,
79
+ errors: errors.length > 0 ? errors : undefined,
80
+ processingTime: Date.now() - startTime,
81
+ };
82
+ }
83
+
84
+ async search<T = SearchDocument>(
85
+ indexName: string,
86
+ query: SearchQuery
87
+ ): Promise<SearchResult<T>> {
88
+ const startTime = Date.now();
89
+ const index = this.indexes.get(indexName);
90
+
91
+ if (!index) {
92
+ return {
93
+ hits: [],
94
+ total: 0,
95
+ processingTime: Date.now() - startTime,
96
+ };
97
+ }
98
+
99
+ let documents = Array.from(index.documents.values());
100
+
101
+ // Simple text search
102
+ if (query.query) {
103
+ const searchTerms = query.query.toLowerCase().split(' ');
104
+ documents = documents.filter((doc) => {
105
+ const searchableFields = query.fields || index.settings.searchableAttributes || [];
106
+ const allFields = searchableFields.length > 0 ? searchableFields : Object.keys(doc);
107
+
108
+ return allFields.some((field) => {
109
+ const value = String(doc[field] || '').toLowerCase();
110
+ return searchTerms.some((term) => value.includes(term));
111
+ });
112
+ });
113
+ }
114
+
115
+ // Apply filters
116
+ if (query.filters) {
117
+ for (const filter of query.filters) {
118
+ documents = documents.filter((doc) => {
119
+ const value = doc[filter.field];
120
+
121
+ switch (filter.operator) {
122
+ case 'eq':
123
+ return value === filter.value;
124
+ case 'ne':
125
+ return value !== filter.value;
126
+ case 'gt':
127
+ return Number(value) > Number(filter.value);
128
+ case 'gte':
129
+ return Number(value) >= Number(filter.value);
130
+ case 'lt':
131
+ return Number(value) < Number(filter.value);
132
+ case 'lte':
133
+ return Number(value) <= Number(filter.value);
134
+ case 'in':
135
+ return Array.isArray(filter.value) && filter.value.includes(value);
136
+ case 'contains':
137
+ return String(value).toLowerCase().includes(String(filter.value).toLowerCase());
138
+ case 'exists':
139
+ return value !== undefined && value !== null;
140
+ default:
141
+ return true;
142
+ }
143
+ });
144
+ }
145
+ }
146
+
147
+ // Apply sorting
148
+ if (query.sort && query.sort.length > 0) {
149
+ documents.sort((a, b) => {
150
+ for (const sort of query.sort!) {
151
+ const aVal = a[sort.field];
152
+ const bVal = b[sort.field];
153
+
154
+ if (aVal === bVal) continue;
155
+
156
+ // Compare values handling different types
157
+ const aNum = typeof aVal === 'number' ? aVal : String(aVal ?? '');
158
+ const bNum = typeof bVal === 'number' ? bVal : String(bVal ?? '');
159
+ const comparison = aNum > bNum ? 1 : -1;
160
+ return sort.direction === 'asc' ? comparison : -comparison;
161
+ }
162
+ return 0;
163
+ });
164
+ }
165
+
166
+ const total = documents.length;
167
+
168
+ // Pagination
169
+ const offset = query.offset || 0;
170
+ const limit = query.limit || 20;
171
+ const paginatedDocs = documents.slice(offset, offset + limit);
172
+
173
+ // Build hits
174
+ const hits = paginatedDocs.map((doc) => ({
175
+ document: doc as T,
176
+ score: 1.0,
177
+ }));
178
+
179
+ return {
180
+ hits,
181
+ total,
182
+ processingTime: Date.now() - startTime,
183
+ page: Math.floor(offset / limit) + 1,
184
+ totalPages: Math.ceil(total / limit),
185
+ };
186
+ }
187
+
188
+ async delete(indexName: string, id: string): Promise<void> {
189
+ const index = this.indexes.get(indexName);
190
+ if (index) {
191
+ index.documents.delete(id);
192
+ index.updatedAt = new Date();
193
+ }
194
+ }
195
+
196
+ async update(indexName: string, id: string, document: Partial<SearchDocument>): Promise<void> {
197
+ const index = this.indexes.get(indexName);
198
+ if (!index) {
199
+ throw new Error('Index not found');
200
+ }
201
+
202
+ const existing = index.documents.get(id);
203
+ if (!existing) {
204
+ throw new Error('Document not found');
205
+ }
206
+
207
+ index.documents.set(id, { ...existing, ...document });
208
+ index.updatedAt = new Date();
209
+ }
210
+
211
+ async get(indexName: string, id: string): Promise<SearchDocument | null> {
212
+ const index = this.indexes.get(indexName);
213
+ return index?.documents.get(id) || null;
214
+ }
215
+
216
+ async updateSettings(indexName: string, settings: IndexSettings): Promise<void> {
217
+ const index = this.indexes.get(indexName);
218
+ if (!index) {
219
+ throw new Error('Index not found');
220
+ }
221
+
222
+ index.settings = { ...index.settings, ...settings };
223
+ index.updatedAt = new Date();
224
+ }
225
+
226
+ async getStats(indexName: string): Promise<IndexStats> {
227
+ const index = this.indexes.get(indexName);
228
+
229
+ if (!index) {
230
+ throw new Error('Index not found');
231
+ }
232
+
233
+ const docsSize = JSON.stringify(Array.from(index.documents.values())).length;
234
+
235
+ return {
236
+ name: indexName,
237
+ documentCount: index.documents.size,
238
+ size: docsSize,
239
+ isIndexing: false,
240
+ lastUpdate: index.updatedAt,
241
+ health: 'green',
242
+ };
243
+ }
244
+
245
+ async autocomplete(indexName: string, query: string, limit = 10): Promise<AutocompleteResult> {
246
+ const startTime = Date.now();
247
+ const index = this.indexes.get(indexName);
248
+
249
+ if (!index) {
250
+ return {
251
+ suggestions: [],
252
+ processingTime: Date.now() - startTime,
253
+ };
254
+ }
255
+
256
+ const queryLower = query.toLowerCase();
257
+ const suggestions = new Set<string>();
258
+
259
+ for (const doc of index.documents.values()) {
260
+ const searchableFields = index.settings.searchableAttributes || Object.keys(doc);
261
+
262
+ for (const field of searchableFields) {
263
+ const value = String(doc[field] || '');
264
+ if (value.toLowerCase().includes(queryLower)) {
265
+ suggestions.add(value);
266
+ if (suggestions.size >= limit) break;
267
+ }
268
+ }
269
+
270
+ if (suggestions.size >= limit) break;
271
+ }
272
+
273
+ return {
274
+ suggestions: Array.from(suggestions).slice(0, limit),
275
+ processingTime: Date.now() - startTime,
276
+ };
277
+ }
278
+ }
@@ -0,0 +1,21 @@
1
+ export { SearchService } from './search.service.js';
2
+ export { MemorySearchAdapter } from './adapters/memory.adapter.js';
3
+ export { ElasticsearchAdapter } from './adapters/elasticsearch.adapter.js';
4
+ export { MeilisearchAdapter } from './adapters/meilisearch.adapter.js';
5
+ export type {
6
+ SearchConfig,
7
+ SearchDocument,
8
+ SearchQuery,
9
+ SearchResult,
10
+ SearchHit,
11
+ SearchFilter,
12
+ SearchSort,
13
+ IndexSettings,
14
+ BulkIndexOperation,
15
+ BulkIndexResult,
16
+ IndexStats,
17
+ AutocompleteResult,
18
+ FacetResult,
19
+ SearchSuggestion,
20
+ SearchEngine,
21
+ } from './types.js';
@@ -0,0 +1,234 @@
1
+ import { logger } from '../../core/logger.js';
2
+ import { NotFoundError } from '../../utils/errors.js';
3
+ import type {
4
+ SearchConfig,
5
+ SearchDocument,
6
+ SearchQuery,
7
+ SearchResult,
8
+ IndexSettings,
9
+ BulkIndexOperation,
10
+ BulkIndexResult,
11
+ IndexStats,
12
+ AutocompleteResult,
13
+ SearchEngine,
14
+ } from './types.js';
15
+ import { MemorySearchAdapter } from './adapters/memory.adapter.js';
16
+
17
+ /**
18
+ * Search Service
19
+ * Unified interface for Elasticsearch, Meilisearch, or in-memory search
20
+ */
21
+ export class SearchService {
22
+ private engine: SearchEngine;
23
+ private config: SearchConfig;
24
+
25
+ constructor(config?: SearchConfig, engine?: SearchEngine) {
26
+ this.config = config || {};
27
+ this.engine = engine || new MemorySearchAdapter();
28
+
29
+ logger.info({ engine: this.config.engine || 'memory' }, 'Search service initialized');
30
+ }
31
+
32
+ /**
33
+ * Create a search index
34
+ */
35
+ async createIndex(indexName: string, settings?: IndexSettings): Promise<void> {
36
+ const mergedSettings = {
37
+ ...this.config.defaultSettings,
38
+ ...settings,
39
+ };
40
+
41
+ await this.engine.createIndex(indexName, mergedSettings);
42
+
43
+ logger.info({ indexName, settings: mergedSettings }, 'Search index created');
44
+ }
45
+
46
+ /**
47
+ * Delete a search index
48
+ */
49
+ async deleteIndex(indexName: string): Promise<void> {
50
+ await this.engine.deleteIndex(indexName);
51
+
52
+ logger.info({ indexName }, 'Search index deleted');
53
+ }
54
+
55
+ /**
56
+ * Index a single document
57
+ */
58
+ async indexDocument(indexName: string, id: string, document: SearchDocument): Promise<void> {
59
+ await this.engine.index(indexName, id, document);
60
+
61
+ logger.debug({ indexName, documentId: id }, 'Document indexed');
62
+ }
63
+
64
+ /**
65
+ * Index multiple documents
66
+ */
67
+ async indexDocuments(indexName: string, documents: SearchDocument[]): Promise<BulkIndexResult> {
68
+ const operations: BulkIndexOperation[] = documents.map((doc) => ({
69
+ operation: 'index',
70
+ id: doc.id,
71
+ document: doc,
72
+ }));
73
+
74
+ const result = await this.engine.bulkIndex(indexName, operations);
75
+
76
+ logger.info(
77
+ { indexName, success: result.success, failed: result.failed },
78
+ 'Bulk index completed'
79
+ );
80
+
81
+ return result;
82
+ }
83
+
84
+ /**
85
+ * Search documents
86
+ */
87
+ async search<T = SearchDocument>(
88
+ indexName: string,
89
+ query: SearchQuery
90
+ ): Promise<SearchResult<T>> {
91
+ const startTime = Date.now();
92
+
93
+ const result = await this.engine.search<T>(indexName, query);
94
+
95
+ const duration = Date.now() - startTime;
96
+
97
+ logger.debug(
98
+ {
99
+ indexName,
100
+ query: query.query,
101
+ hits: result.hits.length,
102
+ total: result.total,
103
+ duration,
104
+ },
105
+ 'Search completed'
106
+ );
107
+
108
+ return result;
109
+ }
110
+
111
+ /**
112
+ * Delete a document
113
+ */
114
+ async deleteDocument(indexName: string, id: string): Promise<void> {
115
+ await this.engine.delete(indexName, id);
116
+
117
+ logger.debug({ indexName, documentId: id }, 'Document deleted');
118
+ }
119
+
120
+ /**
121
+ * Update a document
122
+ */
123
+ async updateDocument(
124
+ indexName: string,
125
+ id: string,
126
+ document: Partial<SearchDocument>
127
+ ): Promise<void> {
128
+ await this.engine.update(indexName, id, document);
129
+
130
+ logger.debug({ indexName, documentId: id }, 'Document updated');
131
+ }
132
+
133
+ /**
134
+ * Get document by ID
135
+ */
136
+ async getDocument(indexName: string, id: string): Promise<SearchDocument> {
137
+ const doc = await this.engine.get(indexName, id);
138
+
139
+ if (!doc) {
140
+ throw new NotFoundError('Document not found');
141
+ }
142
+
143
+ return doc;
144
+ }
145
+
146
+ /**
147
+ * Update index settings
148
+ */
149
+ async updateSettings(indexName: string, settings: IndexSettings): Promise<void> {
150
+ await this.engine.updateSettings(indexName, settings);
151
+
152
+ logger.info({ indexName, settings }, 'Index settings updated');
153
+ }
154
+
155
+ /**
156
+ * Get index statistics
157
+ */
158
+ async getStats(indexName: string): Promise<IndexStats> {
159
+ return await this.engine.getStats(indexName);
160
+ }
161
+
162
+ /**
163
+ * Autocomplete suggestions
164
+ */
165
+ async autocomplete(indexName: string, query: string, limit = 10): Promise<AutocompleteResult> {
166
+ return await this.engine.autocomplete(indexName, query, limit);
167
+ }
168
+
169
+ /**
170
+ * Full-text search with facets
171
+ */
172
+ async searchWithFacets<T = SearchDocument>(
173
+ indexName: string,
174
+ query: string,
175
+ options?: {
176
+ filters?: SearchQuery['filters'];
177
+ facets?: string[];
178
+ limit?: number;
179
+ }
180
+ ): Promise<SearchResult<T>> {
181
+ return await this.search<T>(indexName, {
182
+ query,
183
+ filters: options?.filters,
184
+ facets: options?.facets,
185
+ limit: options?.limit || 20,
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Search similar documents
191
+ */
192
+ async searchSimilar<T = SearchDocument>(
193
+ indexName: string,
194
+ documentId: string,
195
+ limit = 10
196
+ ): Promise<SearchResult<T>> {
197
+ // Get the document
198
+ const doc = await this.getDocument(indexName, documentId);
199
+
200
+ // Use its content for similarity search
201
+ const searchTerms = Object.values(doc)
202
+ .filter((v) => typeof v === 'string')
203
+ .join(' ')
204
+ .substring(0, 200);
205
+
206
+ return await this.search<T>(indexName, {
207
+ query: searchTerms,
208
+ limit,
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Reindex documents from one index to another
214
+ */
215
+ async reindex(
216
+ sourceIndex: string,
217
+ targetIndex: string,
218
+ transform?: (doc: SearchDocument) => SearchDocument
219
+ ): Promise<BulkIndexResult> {
220
+ // Get all documents from source
221
+ const result = await this.search(sourceIndex, {
222
+ query: '*',
223
+ limit: 10000,
224
+ });
225
+
226
+ // Transform if needed
227
+ const documents = transform
228
+ ? result.hits.map((hit) => transform(hit.document as SearchDocument))
229
+ : result.hits.map((hit) => hit.document as SearchDocument);
230
+
231
+ // Index to target
232
+ return await this.indexDocuments(targetIndex, documents);
233
+ }
234
+ }