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.
Files changed (216) hide show
  1. package/.claude/settings.local.json +29 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/README.md +1070 -1
  9. package/dist/cli/index.cjs +2026 -2168
  10. package/dist/cli/index.cjs.map +1 -1
  11. package/dist/cli/index.js +2026 -2168
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/index.cjs +595 -616
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +114 -52
  16. package/dist/index.d.ts +114 -52
  17. package/dist/index.js +595 -616
  18. package/dist/index.js.map +1 -1
  19. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  20. package/docs/DATABASE_MULTI_ORM.md +399 -0
  21. package/docs/PHASE1_BREAKDOWN.md +346 -0
  22. package/docs/PROGRESS.md +550 -0
  23. package/docs/modules/ANALYTICS.md +226 -0
  24. package/docs/modules/API-VERSIONING.md +252 -0
  25. package/docs/modules/AUDIT.md +192 -0
  26. package/docs/modules/AUTH.md +431 -0
  27. package/docs/modules/CACHE.md +346 -0
  28. package/docs/modules/EMAIL.md +254 -0
  29. package/docs/modules/FEATURE-FLAG.md +291 -0
  30. package/docs/modules/I18N.md +294 -0
  31. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  32. package/docs/modules/MFA.md +266 -0
  33. package/docs/modules/NOTIFICATION.md +311 -0
  34. package/docs/modules/OAUTH.md +237 -0
  35. package/docs/modules/PAYMENT.md +804 -0
  36. package/docs/modules/QUEUE.md +540 -0
  37. package/docs/modules/RATE-LIMIT.md +339 -0
  38. package/docs/modules/SEARCH.md +288 -0
  39. package/docs/modules/SECURITY.md +327 -0
  40. package/docs/modules/SESSION.md +382 -0
  41. package/docs/modules/SWAGGER.md +305 -0
  42. package/docs/modules/UPLOAD.md +296 -0
  43. package/docs/modules/USER.md +505 -0
  44. package/docs/modules/VALIDATION.md +294 -0
  45. package/docs/modules/WEBHOOK.md +270 -0
  46. package/docs/modules/WEBSOCKET.md +691 -0
  47. package/package.json +53 -38
  48. package/prisma/schema.prisma +395 -1
  49. package/src/cli/commands/add-module.ts +520 -87
  50. package/src/cli/commands/db.ts +3 -4
  51. package/src/cli/commands/docs.ts +256 -6
  52. package/src/cli/commands/generate.ts +12 -19
  53. package/src/cli/commands/init.ts +384 -214
  54. package/src/cli/index.ts +0 -4
  55. package/src/cli/templates/repository.ts +6 -1
  56. package/src/cli/templates/routes.ts +6 -21
  57. package/src/cli/utils/docs-generator.ts +6 -7
  58. package/src/cli/utils/env-manager.ts +717 -0
  59. package/src/cli/utils/field-parser.ts +16 -7
  60. package/src/cli/utils/interactive-prompt.ts +223 -0
  61. package/src/cli/utils/template-manager.ts +346 -0
  62. package/src/config/database.config.ts +183 -0
  63. package/src/config/env.ts +0 -10
  64. package/src/config/index.ts +0 -14
  65. package/src/core/server.ts +1 -1
  66. package/src/database/adapters/mongoose.adapter.ts +132 -0
  67. package/src/database/adapters/prisma.adapter.ts +118 -0
  68. package/src/database/connection.ts +190 -0
  69. package/src/database/interfaces/database.interface.ts +85 -0
  70. package/src/database/interfaces/index.ts +7 -0
  71. package/src/database/interfaces/repository.interface.ts +129 -0
  72. package/src/database/models/mongoose/index.ts +7 -0
  73. package/src/database/models/mongoose/payment.schema.ts +347 -0
  74. package/src/database/models/mongoose/user.schema.ts +154 -0
  75. package/src/database/prisma.ts +1 -4
  76. package/src/database/redis.ts +101 -0
  77. package/src/database/repositories/mongoose/index.ts +7 -0
  78. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  79. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  80. package/src/database/seed.ts +6 -1
  81. package/src/index.ts +9 -20
  82. package/src/middleware/security.ts +2 -6
  83. package/src/modules/analytics/analytics.routes.ts +80 -0
  84. package/src/modules/analytics/analytics.service.ts +364 -0
  85. package/src/modules/analytics/index.ts +18 -0
  86. package/src/modules/analytics/types.ts +180 -0
  87. package/src/modules/api-versioning/index.ts +15 -0
  88. package/src/modules/api-versioning/types.ts +86 -0
  89. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  90. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  91. package/src/modules/api-versioning/versioning.service.ts +189 -0
  92. package/src/modules/audit/audit.repository.ts +206 -0
  93. package/src/modules/audit/audit.service.ts +27 -59
  94. package/src/modules/auth/auth.controller.ts +2 -2
  95. package/src/modules/auth/auth.middleware.ts +3 -9
  96. package/src/modules/auth/auth.routes.ts +10 -107
  97. package/src/modules/auth/auth.service.ts +126 -23
  98. package/src/modules/auth/index.ts +3 -4
  99. package/src/modules/cache/cache.service.ts +367 -0
  100. package/src/modules/cache/index.ts +10 -0
  101. package/src/modules/cache/types.ts +44 -0
  102. package/src/modules/email/email.service.ts +3 -10
  103. package/src/modules/email/templates.ts +2 -8
  104. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  105. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  106. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  107. package/src/modules/feature-flag/index.ts +20 -0
  108. package/src/modules/feature-flag/types.ts +192 -0
  109. package/src/modules/i18n/i18n.middleware.ts +186 -0
  110. package/src/modules/i18n/i18n.routes.ts +191 -0
  111. package/src/modules/i18n/i18n.service.ts +456 -0
  112. package/src/modules/i18n/index.ts +18 -0
  113. package/src/modules/i18n/types.ts +118 -0
  114. package/src/modules/media-processing/index.ts +17 -0
  115. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  116. package/src/modules/media-processing/media-processing.service.ts +245 -0
  117. package/src/modules/media-processing/types.ts +156 -0
  118. package/src/modules/mfa/index.ts +20 -0
  119. package/src/modules/mfa/mfa.repository.ts +206 -0
  120. package/src/modules/mfa/mfa.routes.ts +595 -0
  121. package/src/modules/mfa/mfa.service.ts +572 -0
  122. package/src/modules/mfa/totp.ts +150 -0
  123. package/src/modules/mfa/types.ts +57 -0
  124. package/src/modules/notification/index.ts +20 -0
  125. package/src/modules/notification/notification.repository.ts +356 -0
  126. package/src/modules/notification/notification.service.ts +483 -0
  127. package/src/modules/notification/types.ts +119 -0
  128. package/src/modules/oauth/index.ts +20 -0
  129. package/src/modules/oauth/oauth.repository.ts +219 -0
  130. package/src/modules/oauth/oauth.routes.ts +446 -0
  131. package/src/modules/oauth/oauth.service.ts +293 -0
  132. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  133. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  134. package/src/modules/oauth/providers/github.provider.ts +248 -0
  135. package/src/modules/oauth/providers/google.provider.ts +189 -0
  136. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  137. package/src/modules/oauth/types.ts +94 -0
  138. package/src/modules/payment/index.ts +19 -0
  139. package/src/modules/payment/payment.repository.ts +733 -0
  140. package/src/modules/payment/payment.routes.ts +390 -0
  141. package/src/modules/payment/payment.service.ts +354 -0
  142. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  143. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  144. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  145. package/src/modules/payment/types.ts +140 -0
  146. package/src/modules/queue/cron.ts +438 -0
  147. package/src/modules/queue/index.ts +87 -0
  148. package/src/modules/queue/queue.routes.ts +600 -0
  149. package/src/modules/queue/queue.service.ts +842 -0
  150. package/src/modules/queue/types.ts +222 -0
  151. package/src/modules/queue/workers.ts +366 -0
  152. package/src/modules/rate-limit/index.ts +59 -0
  153. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  154. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  155. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  156. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  157. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  158. package/src/modules/rate-limit/types.ts +153 -0
  159. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  160. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  161. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  162. package/src/modules/search/index.ts +21 -0
  163. package/src/modules/search/search.service.ts +234 -0
  164. package/src/modules/search/types.ts +214 -0
  165. package/src/modules/security/index.ts +40 -0
  166. package/src/modules/security/sanitize.ts +223 -0
  167. package/src/modules/security/security-audit.service.ts +388 -0
  168. package/src/modules/security/security.middleware.ts +398 -0
  169. package/src/modules/session/index.ts +3 -0
  170. package/src/modules/session/session.repository.ts +159 -0
  171. package/src/modules/session/session.service.ts +340 -0
  172. package/src/modules/session/types.ts +38 -0
  173. package/src/modules/swagger/index.ts +7 -1
  174. package/src/modules/swagger/schema-builder.ts +16 -4
  175. package/src/modules/swagger/swagger.service.ts +9 -10
  176. package/src/modules/swagger/types.ts +0 -2
  177. package/src/modules/upload/index.ts +14 -0
  178. package/src/modules/upload/types.ts +83 -0
  179. package/src/modules/upload/upload.repository.ts +199 -0
  180. package/src/modules/upload/upload.routes.ts +311 -0
  181. package/src/modules/upload/upload.service.ts +448 -0
  182. package/src/modules/user/index.ts +3 -3
  183. package/src/modules/user/user.controller.ts +15 -9
  184. package/src/modules/user/user.repository.ts +237 -113
  185. package/src/modules/user/user.routes.ts +39 -164
  186. package/src/modules/user/user.service.ts +4 -3
  187. package/src/modules/validation/validator.ts +12 -17
  188. package/src/modules/webhook/index.ts +91 -0
  189. package/src/modules/webhook/retry.ts +196 -0
  190. package/src/modules/webhook/signature.ts +135 -0
  191. package/src/modules/webhook/types.ts +181 -0
  192. package/src/modules/webhook/webhook.repository.ts +358 -0
  193. package/src/modules/webhook/webhook.routes.ts +442 -0
  194. package/src/modules/webhook/webhook.service.ts +457 -0
  195. package/src/modules/websocket/features.ts +504 -0
  196. package/src/modules/websocket/index.ts +106 -0
  197. package/src/modules/websocket/middlewares.ts +298 -0
  198. package/src/modules/websocket/types.ts +181 -0
  199. package/src/modules/websocket/websocket.service.ts +692 -0
  200. package/src/utils/errors.ts +7 -0
  201. package/src/utils/pagination.ts +4 -1
  202. package/tests/helpers/db-check.ts +79 -0
  203. package/tests/integration/auth-redis.test.ts +94 -0
  204. package/tests/integration/cache-redis.test.ts +387 -0
  205. package/tests/integration/mongoose-repositories.test.ts +410 -0
  206. package/tests/integration/payment-prisma.test.ts +637 -0
  207. package/tests/integration/queue-bullmq.test.ts +417 -0
  208. package/tests/integration/user-prisma.test.ts +441 -0
  209. package/tests/integration/websocket-socketio.test.ts +552 -0
  210. package/tests/setup.ts +11 -9
  211. package/vitest.config.ts +3 -8
  212. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  213. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  216. 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
+ }