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.
- package/.claude/settings.local.json +30 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/LICENSE +21 -0
- package/README.md +1102 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- 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
|
+
}
|