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,364 @@
|
|
|
1
|
+
import { logger } from '../../core/logger.js';
|
|
2
|
+
import type {
|
|
3
|
+
AnalyticsConfig,
|
|
4
|
+
Metric,
|
|
5
|
+
Counter,
|
|
6
|
+
Gauge,
|
|
7
|
+
Histogram,
|
|
8
|
+
AnalyticsEvent,
|
|
9
|
+
MetricQuery,
|
|
10
|
+
MetricResult,
|
|
11
|
+
} from './types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Analytics Service
|
|
15
|
+
* Metrics collection and monitoring
|
|
16
|
+
*/
|
|
17
|
+
export class AnalyticsService {
|
|
18
|
+
private config: AnalyticsConfig;
|
|
19
|
+
private counters = new Map<string, Counter>();
|
|
20
|
+
private gauges = new Map<string, Gauge>();
|
|
21
|
+
private histograms = new Map<string, Histogram>();
|
|
22
|
+
private events: AnalyticsEvent[] = [];
|
|
23
|
+
private metrics: Metric[] = [];
|
|
24
|
+
|
|
25
|
+
constructor(config: AnalyticsConfig = {}) {
|
|
26
|
+
this.config = {
|
|
27
|
+
enabled: true,
|
|
28
|
+
prefix: 'app',
|
|
29
|
+
defaultLabels: {},
|
|
30
|
+
prometheusEnabled: true,
|
|
31
|
+
flushInterval: 60000,
|
|
32
|
+
...config,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (this.config.flushInterval && this.config.flushInterval > 0) {
|
|
36
|
+
setInterval(() => this.flush(), this.config.flushInterval);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
logger.info('Analytics service initialized');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Increment counter
|
|
44
|
+
*/
|
|
45
|
+
incrementCounter(name: string, value = 1, labels: Record<string, string> = {}): void {
|
|
46
|
+
if (!this.config.enabled) return;
|
|
47
|
+
|
|
48
|
+
const fullName = this.getMetricName(name);
|
|
49
|
+
const key = this.getMetricKey(fullName, labels);
|
|
50
|
+
|
|
51
|
+
let counter = this.counters.get(key);
|
|
52
|
+
if (!counter) {
|
|
53
|
+
counter = {
|
|
54
|
+
name: fullName,
|
|
55
|
+
value: 0,
|
|
56
|
+
labels: { ...this.config.defaultLabels, ...labels },
|
|
57
|
+
};
|
|
58
|
+
this.counters.set(key, counter);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
counter.value += value;
|
|
62
|
+
|
|
63
|
+
this.recordMetric({
|
|
64
|
+
name: fullName,
|
|
65
|
+
type: 'counter',
|
|
66
|
+
value: counter.value,
|
|
67
|
+
labels: counter.labels,
|
|
68
|
+
timestamp: new Date(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set gauge value
|
|
74
|
+
*/
|
|
75
|
+
setGauge(name: string, value: number, labels: Record<string, string> = {}): void {
|
|
76
|
+
if (!this.config.enabled) return;
|
|
77
|
+
|
|
78
|
+
const fullName = this.getMetricName(name);
|
|
79
|
+
const key = this.getMetricKey(fullName, labels);
|
|
80
|
+
|
|
81
|
+
const gauge: Gauge = {
|
|
82
|
+
name: fullName,
|
|
83
|
+
value,
|
|
84
|
+
labels: { ...this.config.defaultLabels, ...labels },
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
this.gauges.set(key, gauge);
|
|
88
|
+
|
|
89
|
+
this.recordMetric({
|
|
90
|
+
name: fullName,
|
|
91
|
+
type: 'gauge',
|
|
92
|
+
value,
|
|
93
|
+
labels: gauge.labels,
|
|
94
|
+
timestamp: new Date(),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Observe value for histogram
|
|
100
|
+
*/
|
|
101
|
+
observeHistogram(
|
|
102
|
+
name: string,
|
|
103
|
+
value: number,
|
|
104
|
+
buckets: number[] = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
|
|
105
|
+
labels: Record<string, string> = {}
|
|
106
|
+
): void {
|
|
107
|
+
if (!this.config.enabled) return;
|
|
108
|
+
|
|
109
|
+
const fullName = this.getMetricName(name);
|
|
110
|
+
const key = this.getMetricKey(fullName, labels);
|
|
111
|
+
|
|
112
|
+
let histogram = this.histograms.get(key);
|
|
113
|
+
if (!histogram) {
|
|
114
|
+
histogram = {
|
|
115
|
+
name: fullName,
|
|
116
|
+
buckets: new Map(),
|
|
117
|
+
sum: 0,
|
|
118
|
+
count: 0,
|
|
119
|
+
labels: { ...this.config.defaultLabels, ...labels },
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Initialize buckets
|
|
123
|
+
buckets.forEach((bucket) => histogram!.buckets.set(bucket, 0));
|
|
124
|
+
this.histograms.set(key, histogram);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Update buckets
|
|
128
|
+
buckets.forEach((bucket) => {
|
|
129
|
+
if (value <= bucket) {
|
|
130
|
+
const current = histogram!.buckets.get(bucket) || 0;
|
|
131
|
+
histogram!.buckets.set(bucket, current + 1);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
histogram.sum += value;
|
|
136
|
+
histogram.count += 1;
|
|
137
|
+
|
|
138
|
+
this.recordMetric({
|
|
139
|
+
name: fullName,
|
|
140
|
+
type: 'histogram',
|
|
141
|
+
value,
|
|
142
|
+
labels: histogram.labels,
|
|
143
|
+
timestamp: new Date(),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Track event
|
|
149
|
+
*/
|
|
150
|
+
trackEvent(event: Omit<AnalyticsEvent, 'timestamp'>): void {
|
|
151
|
+
if (!this.config.enabled) return;
|
|
152
|
+
|
|
153
|
+
const fullEvent: AnalyticsEvent = {
|
|
154
|
+
...event,
|
|
155
|
+
timestamp: new Date(),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
this.events.push(fullEvent);
|
|
159
|
+
|
|
160
|
+
// Auto-increment counter for event
|
|
161
|
+
this.incrementCounter(`events_${event.name}`, 1, {
|
|
162
|
+
event: event.name,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
logger.debug({ event: event.name }, 'Event tracked');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Query metrics
|
|
170
|
+
*/
|
|
171
|
+
async queryMetrics(query: MetricQuery): Promise<MetricResult> {
|
|
172
|
+
let filtered = this.metrics.filter((m) => m.name === query.name);
|
|
173
|
+
|
|
174
|
+
// Filter by time range
|
|
175
|
+
if (query.startTime) {
|
|
176
|
+
filtered = filtered.filter((m) => m.timestamp >= query.startTime!);
|
|
177
|
+
}
|
|
178
|
+
if (query.endTime) {
|
|
179
|
+
filtered = filtered.filter((m) => m.timestamp <= query.endTime!);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Filter by labels
|
|
183
|
+
if (query.labels) {
|
|
184
|
+
filtered = filtered.filter((m) => {
|
|
185
|
+
if (!m.labels) return false;
|
|
186
|
+
return Object.entries(query.labels!).every(([key, value]) => m.labels![key] === value);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Group by labels
|
|
191
|
+
const grouped = new Map<string, Metric[]>();
|
|
192
|
+
if (query.groupBy && query.groupBy.length > 0) {
|
|
193
|
+
filtered.forEach((m) => {
|
|
194
|
+
const groupKey = query.groupBy!.map((label) => m.labels?.[label] || 'unknown').join(':');
|
|
195
|
+
if (!grouped.has(groupKey)) {
|
|
196
|
+
grouped.set(groupKey, []);
|
|
197
|
+
}
|
|
198
|
+
grouped.get(groupKey)!.push(m);
|
|
199
|
+
});
|
|
200
|
+
} else {
|
|
201
|
+
grouped.set('all', filtered);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Aggregate
|
|
205
|
+
const data = Array.from(grouped.entries()).map(([_key, metrics]) => {
|
|
206
|
+
const values = metrics.map((m) => m.value);
|
|
207
|
+
let aggregated: number;
|
|
208
|
+
|
|
209
|
+
switch (query.aggregation) {
|
|
210
|
+
case 'sum':
|
|
211
|
+
aggregated = values.reduce((a, b) => a + b, 0);
|
|
212
|
+
break;
|
|
213
|
+
case 'avg':
|
|
214
|
+
aggregated = values.reduce((a, b) => a + b, 0) / values.length;
|
|
215
|
+
break;
|
|
216
|
+
case 'min':
|
|
217
|
+
aggregated = Math.min(...values);
|
|
218
|
+
break;
|
|
219
|
+
case 'max':
|
|
220
|
+
aggregated = Math.max(...values);
|
|
221
|
+
break;
|
|
222
|
+
case 'count':
|
|
223
|
+
aggregated = values.length;
|
|
224
|
+
break;
|
|
225
|
+
default:
|
|
226
|
+
aggregated = values[values.length - 1] || 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const lastMetric = metrics[metrics.length - 1];
|
|
230
|
+
return {
|
|
231
|
+
timestamp: lastMetric?.timestamp ?? new Date(),
|
|
232
|
+
value: aggregated,
|
|
233
|
+
labels: lastMetric?.labels ?? {},
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const lastDataPoint = data[data.length - 1];
|
|
238
|
+
return {
|
|
239
|
+
name: query.name,
|
|
240
|
+
data,
|
|
241
|
+
aggregated: lastDataPoint?.value ?? 0,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get Prometheus metrics
|
|
247
|
+
*/
|
|
248
|
+
getPrometheusMetrics(): string {
|
|
249
|
+
const lines: string[] = [];
|
|
250
|
+
|
|
251
|
+
// Counters
|
|
252
|
+
this.counters.forEach((counter) => {
|
|
253
|
+
const labelsStr = this.formatLabels(counter.labels);
|
|
254
|
+
lines.push(`${counter.name}${labelsStr} ${counter.value}`);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Gauges
|
|
258
|
+
this.gauges.forEach((gauge) => {
|
|
259
|
+
const labelsStr = this.formatLabels(gauge.labels);
|
|
260
|
+
lines.push(`${gauge.name}${labelsStr} ${gauge.value}`);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Histograms
|
|
264
|
+
this.histograms.forEach((histogram) => {
|
|
265
|
+
const labelsStr = this.formatLabels(histogram.labels);
|
|
266
|
+
|
|
267
|
+
histogram.buckets.forEach((count, le) => {
|
|
268
|
+
lines.push(
|
|
269
|
+
`${histogram.name}_bucket${this.formatLabels({ ...histogram.labels, le: String(le) })} ${count}`
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
lines.push(`${histogram.name}_sum${labelsStr} ${histogram.sum}`);
|
|
274
|
+
lines.push(`${histogram.name}_count${labelsStr} ${histogram.count}`);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return lines.join('\n');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get all counters
|
|
282
|
+
*/
|
|
283
|
+
getCounters(): Counter[] {
|
|
284
|
+
return Array.from(this.counters.values());
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get all gauges
|
|
289
|
+
*/
|
|
290
|
+
getGauges(): Gauge[] {
|
|
291
|
+
return Array.from(this.gauges.values());
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get recent events
|
|
296
|
+
*/
|
|
297
|
+
getEvents(limit = 100): AnalyticsEvent[] {
|
|
298
|
+
return this.events.slice(-limit);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Clear all metrics
|
|
303
|
+
*/
|
|
304
|
+
clear(): void {
|
|
305
|
+
this.counters.clear();
|
|
306
|
+
this.gauges.clear();
|
|
307
|
+
this.histograms.clear();
|
|
308
|
+
this.events = [];
|
|
309
|
+
this.metrics = [];
|
|
310
|
+
logger.info('Metrics cleared');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Flush metrics
|
|
315
|
+
*/
|
|
316
|
+
private flush(): void {
|
|
317
|
+
// Keep only last 10000 metrics
|
|
318
|
+
if (this.metrics.length > 10000) {
|
|
319
|
+
this.metrics = this.metrics.slice(-10000);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Keep only last 1000 events
|
|
323
|
+
if (this.events.length > 1000) {
|
|
324
|
+
this.events = this.events.slice(-1000);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Record metric
|
|
330
|
+
*/
|
|
331
|
+
private recordMetric(metric: Metric): void {
|
|
332
|
+
this.metrics.push(metric);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get full metric name with prefix
|
|
337
|
+
*/
|
|
338
|
+
private getMetricName(name: string): string {
|
|
339
|
+
return this.config.prefix ? `${this.config.prefix}_${name}` : name;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get metric key for storage
|
|
344
|
+
*/
|
|
345
|
+
private getMetricKey(name: string, labels: Record<string, string>): string {
|
|
346
|
+
const labelsStr = Object.entries(labels)
|
|
347
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
348
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
349
|
+
.join(',');
|
|
350
|
+
return `${name}{${labelsStr}}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Format labels for Prometheus
|
|
355
|
+
*/
|
|
356
|
+
private formatLabels(labels: Record<string, string>): string {
|
|
357
|
+
const entries = Object.entries(labels);
|
|
358
|
+
if (entries.length === 0) return '';
|
|
359
|
+
|
|
360
|
+
const formatted = entries.map(([key, value]) => `${key}="${value}"`).join(',');
|
|
361
|
+
|
|
362
|
+
return `{${formatted}}`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { AnalyticsService } from './analytics.service.js';
|
|
2
|
+
export { createAnalyticsRoutes } from './analytics.routes.js';
|
|
3
|
+
export type {
|
|
4
|
+
AnalyticsConfig,
|
|
5
|
+
Metric,
|
|
6
|
+
MetricType,
|
|
7
|
+
Counter,
|
|
8
|
+
Gauge,
|
|
9
|
+
Histogram,
|
|
10
|
+
Summary,
|
|
11
|
+
AnalyticsEvent,
|
|
12
|
+
MetricQuery,
|
|
13
|
+
MetricResult,
|
|
14
|
+
MetricAggregation,
|
|
15
|
+
Dashboard,
|
|
16
|
+
Widget,
|
|
17
|
+
Alert,
|
|
18
|
+
} from './types.js';
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
export type MetricType = 'counter' | 'gauge' | 'histogram' | 'summary';
|
|
2
|
+
export type MetricAggregation = 'sum' | 'avg' | 'min' | 'max' | 'count';
|
|
3
|
+
|
|
4
|
+
export interface Metric {
|
|
5
|
+
/** Metric name */
|
|
6
|
+
name: string;
|
|
7
|
+
/** Metric type */
|
|
8
|
+
type: MetricType;
|
|
9
|
+
/** Metric value */
|
|
10
|
+
value: number;
|
|
11
|
+
/** Labels/tags */
|
|
12
|
+
labels?: Record<string, string>;
|
|
13
|
+
/** Timestamp */
|
|
14
|
+
timestamp: Date;
|
|
15
|
+
/** Unit */
|
|
16
|
+
unit?: string;
|
|
17
|
+
/** Help text */
|
|
18
|
+
help?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Counter {
|
|
22
|
+
/** Counter name */
|
|
23
|
+
name: string;
|
|
24
|
+
/** Current value */
|
|
25
|
+
value: number;
|
|
26
|
+
/** Labels */
|
|
27
|
+
labels: Record<string, string>;
|
|
28
|
+
/** Help text */
|
|
29
|
+
help?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Gauge {
|
|
33
|
+
/** Gauge name */
|
|
34
|
+
name: string;
|
|
35
|
+
/** Current value */
|
|
36
|
+
value: number;
|
|
37
|
+
/** Labels */
|
|
38
|
+
labels: Record<string, string>;
|
|
39
|
+
/** Help text */
|
|
40
|
+
help?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Histogram {
|
|
44
|
+
/** Histogram name */
|
|
45
|
+
name: string;
|
|
46
|
+
/** Buckets */
|
|
47
|
+
buckets: Map<number, number>;
|
|
48
|
+
/** Sum of all values */
|
|
49
|
+
sum: number;
|
|
50
|
+
/** Count of all values */
|
|
51
|
+
count: number;
|
|
52
|
+
/** Labels */
|
|
53
|
+
labels: Record<string, string>;
|
|
54
|
+
/** Help text */
|
|
55
|
+
help?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Summary {
|
|
59
|
+
/** Summary name */
|
|
60
|
+
name: string;
|
|
61
|
+
/** Quantiles (0.5, 0.9, 0.99, etc.) */
|
|
62
|
+
quantiles: Map<number, number>;
|
|
63
|
+
/** Sum of all values */
|
|
64
|
+
sum: number;
|
|
65
|
+
/** Count of all values */
|
|
66
|
+
count: number;
|
|
67
|
+
/** Labels */
|
|
68
|
+
labels: Record<string, string>;
|
|
69
|
+
/** Help text */
|
|
70
|
+
help?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface AnalyticsEvent {
|
|
74
|
+
/** Event name */
|
|
75
|
+
name: string;
|
|
76
|
+
/** User ID */
|
|
77
|
+
userId?: string;
|
|
78
|
+
/** Session ID */
|
|
79
|
+
sessionId?: string;
|
|
80
|
+
/** Event properties */
|
|
81
|
+
properties?: Record<string, unknown>;
|
|
82
|
+
/** Timestamp */
|
|
83
|
+
timestamp: Date;
|
|
84
|
+
/** Context */
|
|
85
|
+
context?: {
|
|
86
|
+
ip?: string;
|
|
87
|
+
userAgent?: string;
|
|
88
|
+
referrer?: string;
|
|
89
|
+
page?: string;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface AnalyticsConfig {
|
|
94
|
+
/** Enable metrics collection */
|
|
95
|
+
enabled?: boolean;
|
|
96
|
+
/** Metrics prefix */
|
|
97
|
+
prefix?: string;
|
|
98
|
+
/** Default labels */
|
|
99
|
+
defaultLabels?: Record<string, string>;
|
|
100
|
+
/** Prometheus endpoint */
|
|
101
|
+
prometheusEnabled?: boolean;
|
|
102
|
+
/** Flush interval (ms) */
|
|
103
|
+
flushInterval?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface MetricQuery {
|
|
107
|
+
/** Metric name */
|
|
108
|
+
name: string;
|
|
109
|
+
/** Start time */
|
|
110
|
+
startTime?: Date;
|
|
111
|
+
/** End time */
|
|
112
|
+
endTime?: Date;
|
|
113
|
+
/** Labels filter */
|
|
114
|
+
labels?: Record<string, string>;
|
|
115
|
+
/** Aggregation */
|
|
116
|
+
aggregation?: MetricAggregation;
|
|
117
|
+
/** Group by labels */
|
|
118
|
+
groupBy?: string[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface MetricResult {
|
|
122
|
+
/** Metric name */
|
|
123
|
+
name: string;
|
|
124
|
+
/** Data points */
|
|
125
|
+
data: Array<{
|
|
126
|
+
timestamp: Date;
|
|
127
|
+
value: number;
|
|
128
|
+
labels?: Record<string, string>;
|
|
129
|
+
}>;
|
|
130
|
+
/** Aggregated value */
|
|
131
|
+
aggregated?: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface Dashboard {
|
|
135
|
+
/** Dashboard ID */
|
|
136
|
+
id: string;
|
|
137
|
+
/** Dashboard name */
|
|
138
|
+
name: string;
|
|
139
|
+
/** Widgets */
|
|
140
|
+
widgets: Widget[];
|
|
141
|
+
/** Created at */
|
|
142
|
+
createdAt: Date;
|
|
143
|
+
/** Updated at */
|
|
144
|
+
updatedAt: Date;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface Widget {
|
|
148
|
+
/** Widget ID */
|
|
149
|
+
id: string;
|
|
150
|
+
/** Widget type */
|
|
151
|
+
type: 'chart' | 'counter' | 'gauge' | 'table';
|
|
152
|
+
/** Widget title */
|
|
153
|
+
title: string;
|
|
154
|
+
/** Metric query */
|
|
155
|
+
query: MetricQuery;
|
|
156
|
+
/** Chart type (for chart widgets) */
|
|
157
|
+
chartType?: 'line' | 'bar' | 'pie' | 'area';
|
|
158
|
+
/** Position */
|
|
159
|
+
position: { x: number; y: number; w: number; h: number };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface Alert {
|
|
163
|
+
/** Alert ID */
|
|
164
|
+
id: string;
|
|
165
|
+
/** Alert name */
|
|
166
|
+
name: string;
|
|
167
|
+
/** Metric to monitor */
|
|
168
|
+
metric: string;
|
|
169
|
+
/** Condition */
|
|
170
|
+
condition: {
|
|
171
|
+
operator: 'gt' | 'lt' | 'eq' | 'gte' | 'lte';
|
|
172
|
+
threshold: number;
|
|
173
|
+
};
|
|
174
|
+
/** Alert status */
|
|
175
|
+
status: 'active' | 'inactive' | 'triggered';
|
|
176
|
+
/** Notification channels */
|
|
177
|
+
channels: string[];
|
|
178
|
+
/** Created at */
|
|
179
|
+
createdAt: Date;
|
|
180
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { VersioningService } from './versioning.service.js';
|
|
2
|
+
export { createVersioningMiddleware, versionedRoute } from './versioning.middleware.js';
|
|
3
|
+
export { createVersioningRoutes } from './versioning.routes.js';
|
|
4
|
+
export type { VersionedRequest } from './versioning.middleware.js';
|
|
5
|
+
export type {
|
|
6
|
+
VersioningConfig,
|
|
7
|
+
ApiVersion,
|
|
8
|
+
VersionStrategy,
|
|
9
|
+
DeprecationStatus,
|
|
10
|
+
VersionedRoute,
|
|
11
|
+
RouteHandler,
|
|
12
|
+
VersionDetectionResult,
|
|
13
|
+
VersionMigration,
|
|
14
|
+
VersionMiddlewareOptions,
|
|
15
|
+
} from './types.js';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export type VersionStrategy = 'url' | 'header' | 'query' | 'accept-header';
|
|
2
|
+
export type DeprecationStatus = 'active' | 'deprecated' | 'sunset';
|
|
3
|
+
|
|
4
|
+
export interface ApiVersion {
|
|
5
|
+
/** Version identifier (e.g., 'v1', 'v2') */
|
|
6
|
+
version: string;
|
|
7
|
+
/** Version status */
|
|
8
|
+
status: DeprecationStatus;
|
|
9
|
+
/** Release date */
|
|
10
|
+
releasedAt: Date;
|
|
11
|
+
/** Deprecation date */
|
|
12
|
+
deprecatedAt?: Date;
|
|
13
|
+
/** Sunset date (when it will be removed) */
|
|
14
|
+
sunsetAt?: Date;
|
|
15
|
+
/** Supported until date */
|
|
16
|
+
supportedUntil?: Date;
|
|
17
|
+
/** Is this the default version */
|
|
18
|
+
isDefault: boolean;
|
|
19
|
+
/** Changelog */
|
|
20
|
+
changelog?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface VersioningConfig {
|
|
24
|
+
/** Versioning strategy */
|
|
25
|
+
strategy: VersionStrategy;
|
|
26
|
+
/** Default version */
|
|
27
|
+
defaultVersion: string;
|
|
28
|
+
/** Available versions */
|
|
29
|
+
versions: ApiVersion[];
|
|
30
|
+
/** Version header name */
|
|
31
|
+
headerName?: string;
|
|
32
|
+
/** Query parameter name */
|
|
33
|
+
queryParam?: string;
|
|
34
|
+
/** Strict mode (reject unknown versions) */
|
|
35
|
+
strict?: boolean;
|
|
36
|
+
/** Show deprecation warnings */
|
|
37
|
+
deprecationWarnings?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface VersionedRoute {
|
|
41
|
+
/** Route path */
|
|
42
|
+
path: string;
|
|
43
|
+
/** HTTP method */
|
|
44
|
+
method: string;
|
|
45
|
+
/** Versions where this route exists */
|
|
46
|
+
versions: string[];
|
|
47
|
+
/** Version-specific handlers */
|
|
48
|
+
handlers: Map<string, RouteHandler>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type RouteHandler = (req: unknown, res: unknown) => Promise<void> | void;
|
|
52
|
+
|
|
53
|
+
export interface VersionDetectionResult {
|
|
54
|
+
/** Detected version */
|
|
55
|
+
version: string;
|
|
56
|
+
/** Detection source */
|
|
57
|
+
source: VersionStrategy;
|
|
58
|
+
/** Is valid version */
|
|
59
|
+
isValid: boolean;
|
|
60
|
+
/** Warning message */
|
|
61
|
+
warning?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface VersionMigration {
|
|
65
|
+
/** From version */
|
|
66
|
+
from: string;
|
|
67
|
+
/** To version */
|
|
68
|
+
to: string;
|
|
69
|
+
/** Request transformer */
|
|
70
|
+
transformRequest?: (data: unknown) => unknown;
|
|
71
|
+
/** Response transformer */
|
|
72
|
+
transformResponse?: (data: unknown) => unknown;
|
|
73
|
+
/** Breaking changes description */
|
|
74
|
+
breakingChanges?: string[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface VersionMiddlewareOptions {
|
|
78
|
+
/** Required version */
|
|
79
|
+
requiredVersion?: string;
|
|
80
|
+
/** Minimum version */
|
|
81
|
+
minVersion?: string;
|
|
82
|
+
/** Maximum version */
|
|
83
|
+
maxVersion?: string;
|
|
84
|
+
/** Allow deprecated versions */
|
|
85
|
+
allowDeprecated?: boolean;
|
|
86
|
+
}
|