servcraft 0.1.0 → 0.1.1
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 +29 -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/README.md +1070 -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,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature Flag Repository
|
|
3
|
+
* Prisma-based persistence for feature flags and overrides
|
|
4
|
+
*/
|
|
5
|
+
import { Prisma } from '@prisma/client';
|
|
6
|
+
import type {
|
|
7
|
+
FeatureFlag as PrismaFeatureFlag,
|
|
8
|
+
FlagOverride as PrismaFlagOverride,
|
|
9
|
+
FeatureFlagStatus as PrismaFlagStatus,
|
|
10
|
+
FlagStrategy as PrismaFlagStrategy,
|
|
11
|
+
PrismaClient,
|
|
12
|
+
} from '@prisma/client';
|
|
13
|
+
import type {
|
|
14
|
+
FeatureFlag,
|
|
15
|
+
FlagOverride,
|
|
16
|
+
FlagStatus,
|
|
17
|
+
FlagStrategy,
|
|
18
|
+
FlagListFilters,
|
|
19
|
+
FlagConfig,
|
|
20
|
+
} from './types.js';
|
|
21
|
+
|
|
22
|
+
// Enum mappings
|
|
23
|
+
const statusToPrisma: Record<FlagStatus | 'archived', PrismaFlagStatus> = {
|
|
24
|
+
enabled: 'ENABLED',
|
|
25
|
+
disabled: 'DISABLED',
|
|
26
|
+
archived: 'ARCHIVED',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const statusFromPrisma: Record<PrismaFlagStatus, FlagStatus> = {
|
|
30
|
+
ENABLED: 'enabled',
|
|
31
|
+
DISABLED: 'disabled',
|
|
32
|
+
ARCHIVED: 'disabled', // Map archived to disabled for app layer
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const strategyToPrisma: Record<FlagStrategy, PrismaFlagStrategy> = {
|
|
36
|
+
boolean: 'BOOLEAN',
|
|
37
|
+
percentage: 'PERCENTAGE',
|
|
38
|
+
'user-list': 'USER_LIST',
|
|
39
|
+
'user-attribute': 'USER_ATTRIBUTE',
|
|
40
|
+
'date-range': 'DATE_RANGE',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const strategyFromPrisma: Record<PrismaFlagStrategy, FlagStrategy> = {
|
|
44
|
+
BOOLEAN: 'boolean',
|
|
45
|
+
PERCENTAGE: 'percentage',
|
|
46
|
+
USER_LIST: 'user-list',
|
|
47
|
+
USER_ATTRIBUTE: 'user-attribute',
|
|
48
|
+
DATE_RANGE: 'date-range',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export class FeatureFlagRepository {
|
|
52
|
+
constructor(private prisma: PrismaClient) {}
|
|
53
|
+
|
|
54
|
+
// ==========================================
|
|
55
|
+
// FLAG METHODS
|
|
56
|
+
// ==========================================
|
|
57
|
+
|
|
58
|
+
async create(data: Omit<FeatureFlag, 'createdAt' | 'updatedAt'>): Promise<FeatureFlag> {
|
|
59
|
+
const flag = await this.prisma.featureFlag.create({
|
|
60
|
+
data: {
|
|
61
|
+
key: data.key,
|
|
62
|
+
name: data.name,
|
|
63
|
+
description: data.description,
|
|
64
|
+
status: statusToPrisma[data.status],
|
|
65
|
+
strategy: strategyToPrisma[data.strategy],
|
|
66
|
+
config: data.config as Prisma.InputJsonValue,
|
|
67
|
+
environment: data.environment,
|
|
68
|
+
tags: data.tags || [],
|
|
69
|
+
createdBy: data.createdBy,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return this.mapFlagFromPrisma(flag);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getByKey(key: string): Promise<FeatureFlag | null> {
|
|
77
|
+
const flag = await this.prisma.featureFlag.findUnique({
|
|
78
|
+
where: { key },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return flag ? this.mapFlagFromPrisma(flag) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async getById(id: string): Promise<FeatureFlag | null> {
|
|
85
|
+
const flag = await this.prisma.featureFlag.findUnique({
|
|
86
|
+
where: { id },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return flag ? this.mapFlagFromPrisma(flag) : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async list(filters?: FlagListFilters): Promise<FeatureFlag[]> {
|
|
93
|
+
const where: Prisma.FeatureFlagWhereInput = {};
|
|
94
|
+
|
|
95
|
+
if (filters?.status) {
|
|
96
|
+
where.status = statusToPrisma[filters.status];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (filters?.environment) {
|
|
100
|
+
where.environment = filters.environment;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (filters?.tags && filters.tags.length > 0) {
|
|
104
|
+
where.tags = { hasSome: filters.tags };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (filters?.search) {
|
|
108
|
+
where.OR = [
|
|
109
|
+
{ name: { contains: filters.search, mode: 'insensitive' } },
|
|
110
|
+
{ description: { contains: filters.search, mode: 'insensitive' } },
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const flags = await this.prisma.featureFlag.findMany({
|
|
115
|
+
where,
|
|
116
|
+
orderBy: { createdAt: 'desc' },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return flags.map((f) => this.mapFlagFromPrisma(f));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async update(
|
|
123
|
+
key: string,
|
|
124
|
+
data: Partial<Omit<FeatureFlag, 'key' | 'createdAt' | 'updatedAt'>>
|
|
125
|
+
): Promise<FeatureFlag | null> {
|
|
126
|
+
try {
|
|
127
|
+
const updateData: Prisma.FeatureFlagUpdateInput = {};
|
|
128
|
+
|
|
129
|
+
if (data.name !== undefined) updateData.name = data.name;
|
|
130
|
+
if (data.description !== undefined) updateData.description = data.description;
|
|
131
|
+
if (data.status !== undefined) updateData.status = statusToPrisma[data.status];
|
|
132
|
+
if (data.strategy !== undefined) updateData.strategy = strategyToPrisma[data.strategy];
|
|
133
|
+
if (data.config !== undefined) updateData.config = data.config as Prisma.InputJsonValue;
|
|
134
|
+
if (data.environment !== undefined) updateData.environment = data.environment;
|
|
135
|
+
if (data.tags !== undefined) updateData.tags = data.tags;
|
|
136
|
+
if (data.createdBy !== undefined) updateData.createdBy = data.createdBy;
|
|
137
|
+
|
|
138
|
+
const flag = await this.prisma.featureFlag.update({
|
|
139
|
+
where: { key },
|
|
140
|
+
data: updateData,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return this.mapFlagFromPrisma(flag);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async delete(key: string): Promise<boolean> {
|
|
153
|
+
try {
|
|
154
|
+
await this.prisma.featureFlag.delete({ where: { key } });
|
|
155
|
+
return true;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ==========================================
|
|
165
|
+
// OVERRIDE METHODS
|
|
166
|
+
// ==========================================
|
|
167
|
+
|
|
168
|
+
async createOverride(data: {
|
|
169
|
+
flagKey: string;
|
|
170
|
+
targetId: string;
|
|
171
|
+
targetType: 'user' | 'session';
|
|
172
|
+
enabled: boolean;
|
|
173
|
+
expiresAt?: Date;
|
|
174
|
+
}): Promise<FlagOverride> {
|
|
175
|
+
const flag = await this.prisma.featureFlag.findUnique({
|
|
176
|
+
where: { key: data.flagKey },
|
|
177
|
+
select: { id: true },
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (!flag) {
|
|
181
|
+
throw new Error(`Flag "${data.flagKey}" not found`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const override = await this.prisma.flagOverride.upsert({
|
|
185
|
+
where: {
|
|
186
|
+
flagId_targetId: {
|
|
187
|
+
flagId: flag.id,
|
|
188
|
+
targetId: data.targetId,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
create: {
|
|
192
|
+
flagId: flag.id,
|
|
193
|
+
targetId: data.targetId,
|
|
194
|
+
targetType: data.targetType,
|
|
195
|
+
enabled: data.enabled,
|
|
196
|
+
expiresAt: data.expiresAt,
|
|
197
|
+
},
|
|
198
|
+
update: {
|
|
199
|
+
enabled: data.enabled,
|
|
200
|
+
expiresAt: data.expiresAt,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return this.mapOverrideFromPrisma(override);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async getOverride(flagKey: string, targetId: string): Promise<FlagOverride | null> {
|
|
208
|
+
const flag = await this.prisma.featureFlag.findUnique({
|
|
209
|
+
where: { key: flagKey },
|
|
210
|
+
select: { id: true },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (!flag) return null;
|
|
214
|
+
|
|
215
|
+
const override = await this.prisma.flagOverride.findUnique({
|
|
216
|
+
where: {
|
|
217
|
+
flagId_targetId: {
|
|
218
|
+
flagId: flag.id,
|
|
219
|
+
targetId,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return override ? this.mapOverrideFromPrisma(override) : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async getOverridesForFlag(flagKey: string): Promise<FlagOverride[]> {
|
|
228
|
+
const flag = await this.prisma.featureFlag.findUnique({
|
|
229
|
+
where: { key: flagKey },
|
|
230
|
+
include: { overrides: true },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (!flag) return [];
|
|
234
|
+
|
|
235
|
+
return flag.overrides.map((o) => this.mapOverrideFromPrisma(o));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async deleteOverride(flagKey: string, targetId: string): Promise<boolean> {
|
|
239
|
+
const flag = await this.prisma.featureFlag.findUnique({
|
|
240
|
+
where: { key: flagKey },
|
|
241
|
+
select: { id: true },
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (!flag) return false;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
await this.prisma.flagOverride.delete({
|
|
248
|
+
where: {
|
|
249
|
+
flagId_targetId: {
|
|
250
|
+
flagId: flag.id,
|
|
251
|
+
targetId,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
return true;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async deleteExpiredOverrides(): Promise<number> {
|
|
265
|
+
const result = await this.prisma.flagOverride.deleteMany({
|
|
266
|
+
where: {
|
|
267
|
+
expiresAt: { lt: new Date() },
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return result.count;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ==========================================
|
|
275
|
+
// MAPPING HELPERS
|
|
276
|
+
// ==========================================
|
|
277
|
+
|
|
278
|
+
private mapFlagFromPrisma(prismaFlag: PrismaFeatureFlag): FeatureFlag {
|
|
279
|
+
return {
|
|
280
|
+
key: prismaFlag.key,
|
|
281
|
+
name: prismaFlag.name,
|
|
282
|
+
description: prismaFlag.description || undefined,
|
|
283
|
+
status: statusFromPrisma[prismaFlag.status],
|
|
284
|
+
strategy: strategyFromPrisma[prismaFlag.strategy],
|
|
285
|
+
config: prismaFlag.config as FlagConfig,
|
|
286
|
+
environment: prismaFlag.environment as FeatureFlag['environment'],
|
|
287
|
+
tags: prismaFlag.tags,
|
|
288
|
+
createdBy: prismaFlag.createdBy || undefined,
|
|
289
|
+
createdAt: prismaFlag.createdAt,
|
|
290
|
+
updatedAt: prismaFlag.updatedAt,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private mapOverrideFromPrisma(prismaOverride: PrismaFlagOverride): FlagOverride {
|
|
295
|
+
return {
|
|
296
|
+
targetId: prismaOverride.targetId,
|
|
297
|
+
targetType: prismaOverride.targetType as 'user' | 'session',
|
|
298
|
+
enabled: prismaOverride.enabled,
|
|
299
|
+
expiresAt: prismaOverride.expiresAt || undefined,
|
|
300
|
+
createdAt: prismaOverride.createdAt,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import type { Request, Response } from 'express';
|
|
3
|
+
import type { FeatureFlagService } from './feature-flag.service.js';
|
|
4
|
+
import type { FeatureFlag, FlagEvaluationContext, FlagListFilters } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create feature flag routes
|
|
8
|
+
*/
|
|
9
|
+
export function createFeatureFlagRoutes(flagService: FeatureFlagService): Router {
|
|
10
|
+
const router = Router();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a feature flag
|
|
14
|
+
* POST /flags
|
|
15
|
+
*/
|
|
16
|
+
router.post('/', async (req: Request, res: Response) => {
|
|
17
|
+
try {
|
|
18
|
+
const flag = await flagService.createFlag(
|
|
19
|
+
req.body as Omit<FeatureFlag, 'createdAt' | 'updatedAt'>
|
|
20
|
+
);
|
|
21
|
+
res.status(201).json(flag);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
res
|
|
24
|
+
.status(400)
|
|
25
|
+
.json({ error: error instanceof Error ? error.message : 'Failed to create flag' });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* List feature flags
|
|
31
|
+
* GET /flags?status=enabled&environment=production&tags=premium&search=dark
|
|
32
|
+
*/
|
|
33
|
+
router.get('/', async (req: Request, res: Response) => {
|
|
34
|
+
const filters: FlagListFilters = {
|
|
35
|
+
status: req.query.status as FlagListFilters['status'],
|
|
36
|
+
environment: req.query.environment as FlagListFilters['environment'],
|
|
37
|
+
tags: req.query.tags ? String(req.query.tags).split(',') : undefined,
|
|
38
|
+
search: req.query.search as string,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const flags = await flagService.listFlags(filters);
|
|
42
|
+
res.json({ flags, count: flags.length });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get a feature flag
|
|
47
|
+
* GET /flags/:key
|
|
48
|
+
*/
|
|
49
|
+
router.get('/:key', async (req: Request, res: Response) => {
|
|
50
|
+
try {
|
|
51
|
+
const key = req.params.key;
|
|
52
|
+
if (!key) {
|
|
53
|
+
res.status(400).json({ error: 'Key parameter required' });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const flag = await flagService.getFlag(key);
|
|
57
|
+
res.json(flag);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
res.status(404).json({ error: error instanceof Error ? error.message : 'Flag not found' });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Update a feature flag
|
|
65
|
+
* PATCH /flags/:key
|
|
66
|
+
*/
|
|
67
|
+
router.patch('/:key', async (req: Request, res: Response) => {
|
|
68
|
+
try {
|
|
69
|
+
const key = req.params.key;
|
|
70
|
+
if (!key) {
|
|
71
|
+
res.status(400).json({ error: 'Key parameter required' });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const flag = await flagService.updateFlag(key, req.body);
|
|
75
|
+
res.json(flag);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
res
|
|
78
|
+
.status(400)
|
|
79
|
+
.json({ error: error instanceof Error ? error.message : 'Failed to update flag' });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Delete a feature flag
|
|
85
|
+
* DELETE /flags/:key
|
|
86
|
+
*/
|
|
87
|
+
router.delete('/:key', async (req: Request, res: Response) => {
|
|
88
|
+
try {
|
|
89
|
+
const key = req.params.key;
|
|
90
|
+
if (!key) {
|
|
91
|
+
res.status(400).json({ error: 'Key parameter required' });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
await flagService.deleteFlag(key);
|
|
95
|
+
res.json({ success: true, message: 'Flag deleted' });
|
|
96
|
+
} catch (error) {
|
|
97
|
+
res.status(404).json({ error: error instanceof Error ? error.message : 'Flag not found' });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Evaluate a feature flag
|
|
103
|
+
* POST /flags/:key/evaluate
|
|
104
|
+
* Body: { userId, userAttributes, environment, sessionId }
|
|
105
|
+
*/
|
|
106
|
+
router.post('/:key/evaluate', async (req: Request, res: Response) => {
|
|
107
|
+
try {
|
|
108
|
+
const key = req.params.key;
|
|
109
|
+
if (!key) {
|
|
110
|
+
res.status(400).json({ error: 'Key parameter required' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const context: FlagEvaluationContext = req.body;
|
|
114
|
+
const result = await flagService.evaluateFlag(key, context);
|
|
115
|
+
res.json(result);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
res.status(404).json({ error: error instanceof Error ? error.message : 'Flag not found' });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Evaluate multiple flags
|
|
123
|
+
* POST /flags/evaluate
|
|
124
|
+
* Body: { keys: ['flag1', 'flag2'], context: { userId, ... } }
|
|
125
|
+
*/
|
|
126
|
+
router.post('/evaluate', async (req: Request, res: Response) => {
|
|
127
|
+
const { keys, context } = req.body as { keys: string[]; context: FlagEvaluationContext };
|
|
128
|
+
|
|
129
|
+
if (!keys || !Array.isArray(keys)) {
|
|
130
|
+
res.status(400).json({ error: 'keys array is required' });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const results = await flagService.evaluateFlags(keys, context || {});
|
|
135
|
+
res.json(results);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if flag is enabled (simplified)
|
|
140
|
+
* POST /flags/:key/enabled
|
|
141
|
+
* Body: { userId, userAttributes }
|
|
142
|
+
*/
|
|
143
|
+
router.post('/:key/enabled', async (req: Request, res: Response) => {
|
|
144
|
+
try {
|
|
145
|
+
const key = req.params.key;
|
|
146
|
+
if (!key) {
|
|
147
|
+
res.status(400).json({ error: 'Key parameter required' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const context: FlagEvaluationContext = req.body;
|
|
151
|
+
const enabled = await flagService.isEnabled(key, context);
|
|
152
|
+
res.json({ enabled });
|
|
153
|
+
} catch (error) {
|
|
154
|
+
res.status(404).json({ error: error instanceof Error ? error.message : 'Flag not found' });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Set override for user/session
|
|
160
|
+
* POST /flags/:key/override
|
|
161
|
+
* Body: { targetId, targetType, enabled, expiresAt }
|
|
162
|
+
*/
|
|
163
|
+
router.post('/:key/override', async (req: Request, res: Response) => {
|
|
164
|
+
try {
|
|
165
|
+
const key = req.params.key;
|
|
166
|
+
if (!key) {
|
|
167
|
+
res.status(400).json({ error: 'Key parameter required' });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const { targetId, targetType, enabled, expiresAt } = req.body as {
|
|
171
|
+
targetId: string;
|
|
172
|
+
targetType: 'user' | 'session';
|
|
173
|
+
enabled: boolean;
|
|
174
|
+
expiresAt?: string;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (!targetId || !targetType) {
|
|
178
|
+
res.status(400).json({ error: 'targetId and targetType are required' });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await flagService.setOverride(
|
|
183
|
+
key,
|
|
184
|
+
targetId,
|
|
185
|
+
targetType,
|
|
186
|
+
enabled,
|
|
187
|
+
expiresAt ? new Date(expiresAt) : undefined
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
res.json({ success: true, message: 'Override set' });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
res
|
|
193
|
+
.status(400)
|
|
194
|
+
.json({ error: error instanceof Error ? error.message : 'Failed to set override' });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Remove override
|
|
200
|
+
* DELETE /flags/:key/override/:targetId
|
|
201
|
+
*/
|
|
202
|
+
router.delete('/:key/override/:targetId', async (req: Request, res: Response) => {
|
|
203
|
+
const key = req.params.key;
|
|
204
|
+
const targetId = req.params.targetId;
|
|
205
|
+
if (!key || !targetId) {
|
|
206
|
+
res.status(400).json({ error: 'Key and targetId parameters required' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
await flagService.removeOverride(key, targetId);
|
|
210
|
+
res.json({ success: true, message: 'Override removed' });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get flag statistics
|
|
215
|
+
* GET /flags/:key/stats
|
|
216
|
+
*/
|
|
217
|
+
router.get('/:key/stats', async (req: Request, res: Response) => {
|
|
218
|
+
try {
|
|
219
|
+
const key = req.params.key;
|
|
220
|
+
if (!key) {
|
|
221
|
+
res.status(400).json({ error: 'Key parameter required' });
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const stats = await flagService.getStats(key);
|
|
225
|
+
res.json(stats);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
res.status(404).json({ error: error instanceof Error ? error.message : 'Stats not found' });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get flag events
|
|
233
|
+
* GET /flags/:key/events?limit=100
|
|
234
|
+
*/
|
|
235
|
+
router.get('/:key/events', async (req: Request, res: Response) => {
|
|
236
|
+
const key = req.params.key;
|
|
237
|
+
if (!key) {
|
|
238
|
+
res.status(400).json({ error: 'Key parameter required' });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 100;
|
|
242
|
+
const events = await flagService.getEvents(key, limit);
|
|
243
|
+
res.json({ events, count: events.length });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return router;
|
|
247
|
+
}
|