alepha 0.19.2 → 0.19.4

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 (241) hide show
  1. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  2. package/dist/api/audits/index.d.ts +8 -8
  3. package/dist/api/invitations/index.d.ts +790 -0
  4. package/dist/api/invitations/index.d.ts.map +1 -0
  5. package/dist/api/invitations/index.js +665 -0
  6. package/dist/api/invitations/index.js.map +1 -0
  7. package/dist/api/jobs/index.browser.js +8 -9
  8. package/dist/api/jobs/index.browser.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts +90 -34
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +267 -44
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/notifications/index.browser.js +0 -1
  14. package/dist/api/notifications/index.browser.js.map +1 -1
  15. package/dist/api/notifications/index.d.ts +3 -3
  16. package/dist/api/notifications/index.d.ts.map +1 -1
  17. package/dist/api/notifications/index.js +0 -1
  18. package/dist/api/notifications/index.js.map +1 -1
  19. package/dist/api/parameters/index.browser.js +112 -1
  20. package/dist/api/parameters/index.browser.js.map +1 -1
  21. package/dist/api/parameters/index.d.ts +90 -3
  22. package/dist/api/parameters/index.d.ts.map +1 -1
  23. package/dist/api/parameters/index.js +79 -12
  24. package/dist/api/parameters/index.js.map +1 -1
  25. package/dist/{billing → api/payments}/index.d.ts +67 -49
  26. package/dist/api/payments/index.d.ts.map +1 -0
  27. package/dist/{billing → api/payments}/index.js +108 -74
  28. package/dist/api/payments/index.js.map +1 -0
  29. package/dist/api/subscriptions/index.d.ts +1692 -0
  30. package/dist/api/subscriptions/index.d.ts.map +1 -0
  31. package/dist/api/subscriptions/index.js +1870 -0
  32. package/dist/api/subscriptions/index.js.map +1 -0
  33. package/dist/api/users/index.d.ts +27 -21
  34. package/dist/api/users/index.d.ts.map +1 -1
  35. package/dist/api/users/index.js +167 -34
  36. package/dist/api/users/index.js.map +1 -1
  37. package/dist/api/workflows/index.browser.js +246 -0
  38. package/dist/api/workflows/index.browser.js.map +1 -0
  39. package/dist/api/workflows/index.d.ts +1618 -0
  40. package/dist/api/workflows/index.d.ts.map +1 -0
  41. package/dist/api/workflows/index.js +1504 -0
  42. package/dist/api/workflows/index.js.map +1 -0
  43. package/dist/cli/config/index.d.ts +6 -28
  44. package/dist/cli/config/index.d.ts.map +1 -1
  45. package/dist/cli/config/index.js +5 -10
  46. package/dist/cli/config/index.js.map +1 -1
  47. package/dist/cli/core/index.d.ts +11669 -208
  48. package/dist/cli/core/index.d.ts.map +1 -1
  49. package/dist/cli/core/index.js +60 -69
  50. package/dist/cli/core/index.js.map +1 -1
  51. package/dist/cli/devtools/index.d.ts +5 -0
  52. package/dist/cli/devtools/index.d.ts.map +1 -1
  53. package/dist/cli/devtools/index.js +4 -0
  54. package/dist/cli/devtools/index.js.map +1 -1
  55. package/dist/cli/platform/index.d.ts +69 -64
  56. package/dist/cli/platform/index.d.ts.map +1 -1
  57. package/dist/cli/platform/index.js +6 -2
  58. package/dist/cli/platform/index.js.map +1 -1
  59. package/dist/cli/vendor/index.d.ts +38 -10
  60. package/dist/cli/vendor/index.d.ts.map +1 -1
  61. package/dist/cli/vendor/index.js +85 -26
  62. package/dist/cli/vendor/index.js.map +1 -1
  63. package/dist/core/index.browser.js +21 -2
  64. package/dist/core/index.browser.js.map +1 -1
  65. package/dist/core/index.d.ts +33 -2
  66. package/dist/core/index.d.ts.map +1 -1
  67. package/dist/core/index.js +25 -2
  68. package/dist/core/index.js.map +1 -1
  69. package/dist/core/index.native.js +25 -2
  70. package/dist/core/index.native.js.map +1 -1
  71. package/dist/core/index.workerd.js +25 -2
  72. package/dist/core/index.workerd.js.map +1 -1
  73. package/dist/email/smtp/index.js +24 -8
  74. package/dist/email/smtp/index.js.map +1 -1
  75. package/dist/logger/index.d.ts.map +1 -1
  76. package/dist/logger/index.js +1 -1
  77. package/dist/logger/index.js.map +1 -1
  78. package/dist/orm/core/index.browser.js +0 -18
  79. package/dist/orm/core/index.browser.js.map +1 -1
  80. package/dist/orm/core/index.bun.js +25 -73
  81. package/dist/orm/core/index.bun.js.map +1 -1
  82. package/dist/orm/core/index.d.ts +10 -32
  83. package/dist/orm/core/index.d.ts.map +1 -1
  84. package/dist/orm/core/index.js +25 -73
  85. package/dist/orm/core/index.js.map +1 -1
  86. package/dist/orm/postgres/index.bun.js +3 -3
  87. package/dist/orm/postgres/index.bun.js.map +1 -1
  88. package/dist/orm/postgres/index.d.ts +2 -1
  89. package/dist/orm/postgres/index.d.ts.map +1 -1
  90. package/dist/orm/postgres/index.js +3 -3
  91. package/dist/orm/postgres/index.js.map +1 -1
  92. package/dist/react/router/index.browser.js +25 -3
  93. package/dist/react/router/index.browser.js.map +1 -1
  94. package/dist/react/router/index.d.ts +16 -1
  95. package/dist/react/router/index.d.ts.map +1 -1
  96. package/dist/react/router/index.js +25 -3
  97. package/dist/react/router/index.js.map +1 -1
  98. package/dist/security/index.d.ts +28 -0
  99. package/dist/security/index.d.ts.map +1 -1
  100. package/dist/security/index.js +28 -0
  101. package/dist/security/index.js.map +1 -1
  102. package/package.json +37 -20
  103. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  104. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  105. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  106. package/src/api/invitations/entities/invitations.ts +33 -0
  107. package/src/api/invitations/index.ts +65 -0
  108. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  109. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  110. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  111. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  112. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  113. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  114. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  115. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  116. package/src/api/invitations/services/InvitationService.ts +556 -0
  117. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  118. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  119. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  120. package/src/api/jobs/index.ts +0 -3
  121. package/src/api/jobs/primitives/$job.ts +22 -11
  122. package/src/api/jobs/providers/JobProvider.ts +239 -25
  123. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  124. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  125. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  126. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  127. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  128. package/src/api/jobs/services/JobService.ts +51 -12
  129. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  130. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  131. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  132. package/src/api/parameters/index.browser.ts +12 -0
  133. package/src/api/parameters/primitives/$parameter.ts +20 -3
  134. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  135. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  136. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  137. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  138. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  139. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  140. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  141. package/src/{billing → api/payments}/index.ts +31 -25
  142. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  143. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  144. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  145. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  146. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  147. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  148. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  149. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  150. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  151. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  152. package/src/api/subscriptions/index.ts +144 -0
  153. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  154. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  155. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  156. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  157. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  158. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  159. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  160. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  161. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  162. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  163. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  164. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  165. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  166. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  167. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  168. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  169. package/src/api/subscriptions/services/BillingService.ts +437 -0
  170. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  171. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  172. package/src/api/subscriptions/services/UsageService.ts +118 -0
  173. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  174. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  175. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  176. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  177. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  178. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  179. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  180. package/src/api/users/controllers/UserController.ts +3 -8
  181. package/src/api/users/notifications/UserNotifications.ts +23 -0
  182. package/src/api/users/schemas/loginSchema.ts +1 -1
  183. package/src/api/users/services/CredentialService.ts +51 -4
  184. package/src/api/users/services/RegistrationService.ts +38 -9
  185. package/src/api/users/services/SessionService.ts +62 -9
  186. package/src/api/users/services/UserService.ts +21 -12
  187. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  188. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  189. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  190. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  191. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  192. package/src/api/workflows/index.browser.ts +22 -0
  193. package/src/api/workflows/index.ts +124 -0
  194. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  195. package/src/api/workflows/primitives/$workflow.ts +202 -0
  196. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  197. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  198. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  199. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  200. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  201. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  202. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  203. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  204. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  205. package/src/api/workflows/services/WorkflowService.ts +382 -0
  206. package/src/cli/config/defineConfig.ts +17 -46
  207. package/src/cli/core/providers/ViteDevServerProvider.ts +45 -3
  208. package/src/cli/core/services/PackageManagerUtils.ts +3 -1
  209. package/src/cli/core/services/ProjectScaffolder.ts +5 -5
  210. package/src/cli/core/templates/agentMd.ts +14 -5
  211. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  212. package/src/cli/devtools/index.ts +21 -1
  213. package/src/cli/platform/index.ts +23 -2
  214. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  215. package/src/cli/vendor/index.ts +20 -3
  216. package/src/cli/vendor/services/VendorService.ts +126 -27
  217. package/src/core/Alepha.ts +10 -0
  218. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  219. package/src/core/providers/SchemaValidator.ts +1 -1
  220. package/src/core/providers/TypeProvider.ts +46 -3
  221. package/src/logger/index.ts +6 -1
  222. package/src/orm/__tests__/enums.spec.ts +22 -29
  223. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  224. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  225. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  226. package/src/orm/core/providers/DrizzleKitProvider.ts +56 -105
  227. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  228. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  229. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  230. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  231. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  232. package/src/security/primitives/$secure.ts +28 -0
  233. package/tsconfig.base.json +0 -1
  234. package/dist/billing/index.d.ts.map +0 -1
  235. package/dist/billing/index.js.map +0 -1
  236. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  237. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  238. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  239. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  240. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  241. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "alepha",
3
3
  "description": "Easy-to-use modern TypeScript framework for building many kind of applications.",
4
4
  "author": "Feunard",
5
- "version": "0.19.2",
5
+ "version": "0.19.4",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=22.0.0"
@@ -24,38 +24,38 @@
24
24
  "@vitejs/plugin-react": "^6.0.1",
25
25
  "dayjs": "^1.11.20",
26
26
  "drizzle-orm": "^0.45.2",
27
- "postgres": "^3.4.8",
27
+ "postgres": "^3.4.9",
28
28
  "tsx": "^4.21.0",
29
- "typebox": "^1.1.7",
29
+ "typebox": "^1.1.23",
30
30
  "typescript": "^6.0.2",
31
- "vite": "^8.0.3",
32
- "vite-bundle-analyzer": "^1.3.6",
31
+ "vite-bundle-analyzer": "^1.3.7",
33
32
  "ws": "^8.20.0"
34
33
  },
35
34
  "devDependencies": {
36
- "@biomejs/biome": "^2.4.9",
37
- "@electric-sql/pglite": "^0.4.2",
35
+ "@biomejs/biome": "^2.4.11",
36
+ "@electric-sql/pglite": "^0.4.4",
38
37
  "@faker-js/faker": "^10.4.0",
39
38
  "@testing-library/dom": "^10.4.1",
40
39
  "@testing-library/react": "^16.3.2",
41
40
  "@types/bun": "^1.3.11",
42
- "@types/node": "^25.5.0",
43
- "@types/nodemailer": "^7.0.11",
41
+ "@types/node": "^25.6.0",
42
+ "@types/nodemailer": "^8.0.0",
44
43
  "@types/react": "^19.2.14",
45
44
  "@types/react-dom": "^19.2.3",
46
45
  "@types/ws": "^8.18.1",
47
46
  "cron-schedule": "^6.0.0",
48
47
  "drizzle-kit": "^0.31.10",
49
48
  "jose": "^6.2.2",
50
- "jsdom": "^29.0.1",
51
- "nodemailer": "^8.0.4",
49
+ "jsdom": "^29.0.2",
50
+ "nodemailer": "^8.0.5",
52
51
  "openid-client": "^6.8.2",
53
52
  "prom-client": "^15.1.3",
54
- "react": "^19.2.4",
55
- "react-dom": "^19.2.4",
56
- "swagger-ui-dist": "^5.32.1",
53
+ "react": "^19.2.5",
54
+ "react-dom": "^19.2.5",
55
+ "swagger-ui-dist": "^5.32.2",
57
56
  "tsdown": "^0.21.7",
58
- "vitest": "^4.1.2"
57
+ "vite": "^8.0.8",
58
+ "vitest": "^4.1.4"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "react": "^19",
@@ -108,6 +108,11 @@
108
108
  "import": "./dist/api/files/index.js",
109
109
  "default": "./dist/api/files/index.js"
110
110
  },
111
+ "./api/invitations": {
112
+ "types": "./dist/api/invitations/index.d.ts",
113
+ "import": "./dist/api/invitations/index.js",
114
+ "default": "./dist/api/invitations/index.js"
115
+ },
111
116
  "./api/jobs": {
112
117
  "types": "./dist/api/jobs/index.d.ts",
113
118
  "react-native": "./dist/api/jobs/index.browser.js",
@@ -141,6 +146,16 @@
141
146
  "import": "./dist/api/parameters/index.js",
142
147
  "default": "./dist/api/parameters/index.js"
143
148
  },
149
+ "./api/payments": {
150
+ "types": "./dist/api/payments/index.d.ts",
151
+ "import": "./dist/api/payments/index.js",
152
+ "default": "./dist/api/payments/index.js"
153
+ },
154
+ "./api/subscriptions": {
155
+ "types": "./dist/api/subscriptions/index.d.ts",
156
+ "import": "./dist/api/subscriptions/index.js",
157
+ "default": "./dist/api/subscriptions/index.js"
158
+ },
144
159
  "./api/users": {
145
160
  "types": "./dist/api/users/index.d.ts",
146
161
  "react-native": "./dist/api/users/index.browser.js",
@@ -155,16 +170,18 @@
155
170
  "import": "./dist/api/verifications/index.js",
156
171
  "default": "./dist/api/verifications/index.js"
157
172
  },
173
+ "./api/workflows": {
174
+ "types": "./dist/api/workflows/index.d.ts",
175
+ "react-native": "./dist/api/workflows/index.browser.js",
176
+ "browser": "./dist/api/workflows/index.browser.js",
177
+ "import": "./dist/api/workflows/index.js",
178
+ "default": "./dist/api/workflows/index.js"
179
+ },
158
180
  "./batch": {
159
181
  "types": "./dist/batch/index.d.ts",
160
182
  "import": "./dist/batch/index.js",
161
183
  "default": "./dist/batch/index.js"
162
184
  },
163
- "./billing": {
164
- "types": "./dist/billing/index.d.ts",
165
- "import": "./dist/billing/index.js",
166
- "default": "./dist/billing/index.js"
167
- },
168
185
  "./bin": {
169
186
  "types": "./dist/bin/index.d.ts",
170
187
  "import": "./dist/bin/index.js",
@@ -0,0 +1,439 @@
1
+ import { Alepha } from "alepha";
2
+ import { users } from "alepha/api/users";
3
+ import { $repository } from "alepha/orm";
4
+ import { AlephaOrmPostgres } from "alepha/orm/postgres";
5
+ import { BadRequestError, ForbiddenError } from "alepha/server";
6
+ import { describe, it } from "vitest";
7
+ import type { InvitationEntity } from "../entities/invitations.ts";
8
+ import { AlephaApiInvitations } from "../index.ts";
9
+ import { InvitationProvider } from "../providers/InvitationProvider.ts";
10
+ import { InvitationService } from "../services/InvitationService.ts";
11
+
12
+ class TestInvitationProvider extends InvitationProvider {
13
+ protected members = new Map<string, Set<string>>();
14
+
15
+ async validateResource(
16
+ _resourceType: string,
17
+ _resourceId: string,
18
+ _inviter: { id: string; email?: string },
19
+ ): Promise<void> {
20
+ // Always succeeds
21
+ }
22
+
23
+ async isMember(
24
+ resourceType: string,
25
+ resourceId: string,
26
+ _email: string,
27
+ userId?: string,
28
+ ): Promise<boolean> {
29
+ if (!userId) {
30
+ return false;
31
+ }
32
+
33
+ const key = `${resourceType}:${resourceId}`;
34
+ return this.members.get(key)?.has(userId) ?? false;
35
+ }
36
+
37
+ async onAccept(
38
+ invitation: InvitationEntity,
39
+ acceptedBy: { id: string; email?: string },
40
+ ): Promise<void> {
41
+ const key = `${invitation.resourceType}:${invitation.resourceId}`;
42
+
43
+ if (!this.members.has(key)) {
44
+ this.members.set(key, new Set());
45
+ }
46
+
47
+ this.members.get(key)!.add(acceptedBy.id);
48
+ }
49
+
50
+ async getResourceInfo(
51
+ _resourceType: string,
52
+ _resourceId: string,
53
+ ): Promise<{ name: string; description?: string; url?: string }> {
54
+ return { name: "Test Project" };
55
+ }
56
+
57
+ addMember(resourceType: string, resourceId: string, userId: string): void {
58
+ const key = `${resourceType}:${resourceId}`;
59
+
60
+ if (!this.members.has(key)) {
61
+ this.members.set(key, new Set());
62
+ }
63
+
64
+ this.members.get(key)!.add(userId);
65
+ }
66
+ }
67
+
68
+ class TestRepositories {
69
+ users = $repository(users);
70
+ }
71
+
72
+ const setup = async () => {
73
+ const alepha = Alepha.create()
74
+ .with(AlephaOrmPostgres)
75
+ .with({ provide: InvitationProvider, use: TestInvitationProvider })
76
+ .with(AlephaApiInvitations);
77
+
78
+ const service = alepha.inject(InvitationService);
79
+ const provider = alepha.inject(
80
+ TestInvitationProvider,
81
+ ) as TestInvitationProvider;
82
+ const repos = alepha.inject(TestRepositories);
83
+ await alepha.start();
84
+
85
+ const createUser = async (email: string) => {
86
+ return repos.users.create({ email, roles: [] });
87
+ };
88
+
89
+ return { alepha, service, provider, createUser };
90
+ };
91
+
92
+ describe("InvitationService", () => {
93
+ // -----------------------------------------------------------------------------------------------------------------
94
+
95
+ describe("create", () => {
96
+ it("should create a pending invitation", async ({ expect }) => {
97
+ const { service, createUser } = await setup();
98
+
99
+ const inviter = await createUser("inviter@example.com");
100
+
101
+ const invitation = await service.create(
102
+ {
103
+ email: "invitee@example.com",
104
+ resourceType: "project",
105
+ resourceId: "proj-1",
106
+ },
107
+ { id: inviter.id, email: inviter.email },
108
+ );
109
+
110
+ expect(invitation.status).toBe("pending");
111
+ expect(invitation.email).toBe("invitee@example.com");
112
+ expect(invitation.resourceType).toBe("project");
113
+ expect(invitation.resourceId).toBe("proj-1");
114
+ expect(invitation.invitedBy).toBe(inviter.id);
115
+ expect(invitation.token).toBeDefined();
116
+ expect(invitation.expiresAt).toBeDefined();
117
+ });
118
+
119
+ it("should reject self-invite", async ({ expect }) => {
120
+ const { service, createUser } = await setup();
121
+
122
+ const inviter = await createUser("self@example.com");
123
+
124
+ await expect(
125
+ service.create(
126
+ {
127
+ email: "self@example.com",
128
+ resourceType: "project",
129
+ resourceId: "proj-1",
130
+ },
131
+ { id: inviter.id, email: inviter.email },
132
+ ),
133
+ ).rejects.toThrow(BadRequestError);
134
+ });
135
+
136
+ it("should reject duplicate pending invitation", async ({ expect }) => {
137
+ const { service, createUser } = await setup();
138
+
139
+ const inviter = await createUser("inviter@example.com");
140
+
141
+ await service.create(
142
+ {
143
+ email: "dup@example.com",
144
+ resourceType: "project",
145
+ resourceId: "proj-1",
146
+ },
147
+ { id: inviter.id, email: inviter.email },
148
+ );
149
+
150
+ await expect(
151
+ service.create(
152
+ {
153
+ email: "dup@example.com",
154
+ resourceType: "project",
155
+ resourceId: "proj-1",
156
+ },
157
+ { id: inviter.id, email: inviter.email },
158
+ ),
159
+ ).rejects.toThrow(BadRequestError);
160
+ });
161
+
162
+ it("should reject when already a member", async ({ expect }) => {
163
+ const { service, provider, createUser } = await setup();
164
+
165
+ const inviter = await createUser("inviter@example.com");
166
+ const existingMember = await createUser("member@example.com");
167
+
168
+ provider.addMember("project", "proj-1", existingMember.id);
169
+
170
+ await expect(
171
+ service.create(
172
+ {
173
+ email: "member@example.com",
174
+ resourceType: "project",
175
+ resourceId: "proj-1",
176
+ },
177
+ { id: inviter.id, email: inviter.email },
178
+ ),
179
+ ).rejects.toThrow(BadRequestError);
180
+ });
181
+ });
182
+
183
+ // -----------------------------------------------------------------------------------------------------------------
184
+
185
+ describe("accept", () => {
186
+ it("should accept invitation and call onAccept", async ({ expect }) => {
187
+ const { service, provider, createUser } = await setup();
188
+
189
+ const inviter = await createUser("inviter@example.com");
190
+ const invitee = await createUser("invitee@example.com");
191
+
192
+ const invitation = await service.create(
193
+ {
194
+ email: "invitee@example.com",
195
+ resourceType: "project",
196
+ resourceId: "proj-1",
197
+ },
198
+ { id: inviter.id, email: inviter.email },
199
+ );
200
+
201
+ await service.accept(invitation.id, {
202
+ id: invitee.id,
203
+ email: invitee.email,
204
+ });
205
+
206
+ const updated = await service.getById(invitation.id);
207
+ expect(updated.status).toBe("accepted");
208
+ expect(updated.resolvedBy).toBe(invitee.id);
209
+ expect(updated.resolvedAt).toBeDefined();
210
+
211
+ const isMember = await provider.isMember(
212
+ "project",
213
+ "proj-1",
214
+ "invitee@example.com",
215
+ invitee.id,
216
+ );
217
+ expect(isMember).toBe(true);
218
+ });
219
+
220
+ it("should reject accept for wrong email", async ({ expect }) => {
221
+ const { service, createUser } = await setup();
222
+
223
+ const inviter = await createUser("inviter@example.com");
224
+ const wrongUser = await createUser("wrong@example.com");
225
+
226
+ const invitation = await service.create(
227
+ {
228
+ email: "invitee@example.com",
229
+ resourceType: "project",
230
+ resourceId: "proj-1",
231
+ },
232
+ { id: inviter.id, email: inviter.email },
233
+ );
234
+
235
+ await expect(
236
+ service.accept(invitation.id, {
237
+ id: wrongUser.id,
238
+ email: wrongUser.email,
239
+ }),
240
+ ).rejects.toThrow(ForbiddenError);
241
+ });
242
+
243
+ it("should reject accept for non-pending invitation", async ({
244
+ expect,
245
+ }) => {
246
+ const { service, createUser } = await setup();
247
+
248
+ const inviter = await createUser("inviter@example.com");
249
+ const invitee = await createUser("invitee@example.com");
250
+
251
+ const invitation = await service.create(
252
+ {
253
+ email: "invitee@example.com",
254
+ resourceType: "project",
255
+ resourceId: "proj-1",
256
+ },
257
+ { id: inviter.id, email: inviter.email },
258
+ );
259
+
260
+ await service.accept(invitation.id, {
261
+ id: invitee.id,
262
+ email: invitee.email,
263
+ });
264
+
265
+ await expect(
266
+ service.accept(invitation.id, {
267
+ id: invitee.id,
268
+ email: invitee.email,
269
+ }),
270
+ ).rejects.toThrow(BadRequestError);
271
+ });
272
+ });
273
+
274
+ // -----------------------------------------------------------------------------------------------------------------
275
+
276
+ describe("decline", () => {
277
+ it("should decline invitation", async ({ expect }) => {
278
+ const { service, createUser } = await setup();
279
+
280
+ const inviter = await createUser("inviter@example.com");
281
+ const invitee = await createUser("invitee@example.com");
282
+
283
+ const invitation = await service.create(
284
+ {
285
+ email: "invitee@example.com",
286
+ resourceType: "project",
287
+ resourceId: "proj-1",
288
+ },
289
+ { id: inviter.id, email: inviter.email },
290
+ );
291
+
292
+ await service.decline(invitation.id, {
293
+ id: invitee.id,
294
+ email: invitee.email,
295
+ });
296
+
297
+ const updated = await service.getById(invitation.id);
298
+ expect(updated.status).toBe("declined");
299
+ expect(updated.resolvedBy).toBe(invitee.id);
300
+ expect(updated.resolvedAt).toBeDefined();
301
+ });
302
+
303
+ it("should reject decline for wrong email", async ({ expect }) => {
304
+ const { service, createUser } = await setup();
305
+
306
+ const inviter = await createUser("inviter@example.com");
307
+ const wrongUser = await createUser("wrong@example.com");
308
+
309
+ const invitation = await service.create(
310
+ {
311
+ email: "invitee@example.com",
312
+ resourceType: "project",
313
+ resourceId: "proj-1",
314
+ },
315
+ { id: inviter.id, email: inviter.email },
316
+ );
317
+
318
+ await expect(
319
+ service.decline(invitation.id, {
320
+ id: wrongUser.id,
321
+ email: wrongUser.email,
322
+ }),
323
+ ).rejects.toThrow(ForbiddenError);
324
+ });
325
+ });
326
+
327
+ // -----------------------------------------------------------------------------------------------------------------
328
+
329
+ describe("revoke", () => {
330
+ it("should revoke pending invitation", async ({ expect }) => {
331
+ const { service, createUser } = await setup();
332
+
333
+ const inviter = await createUser("inviter@example.com");
334
+
335
+ const invitation = await service.create(
336
+ {
337
+ email: "invitee@example.com",
338
+ resourceType: "project",
339
+ resourceId: "proj-1",
340
+ },
341
+ { id: inviter.id, email: inviter.email },
342
+ );
343
+
344
+ await service.revoke(invitation.id, { id: inviter.id });
345
+
346
+ const updated = await service.getById(invitation.id);
347
+ expect(updated.status).toBe("revoked");
348
+ expect(updated.resolvedBy).toBe(inviter.id);
349
+ expect(updated.resolvedAt).toBeDefined();
350
+ });
351
+
352
+ it("should reject revoke for non-pending", async ({ expect }) => {
353
+ const { service, createUser } = await setup();
354
+
355
+ const inviter = await createUser("inviter@example.com");
356
+ const invitee = await createUser("invitee@example.com");
357
+
358
+ const invitation = await service.create(
359
+ {
360
+ email: "invitee@example.com",
361
+ resourceType: "project",
362
+ resourceId: "proj-1",
363
+ },
364
+ { id: inviter.id, email: inviter.email },
365
+ );
366
+
367
+ await service.accept(invitation.id, {
368
+ id: invitee.id,
369
+ email: invitee.email,
370
+ });
371
+
372
+ await expect(
373
+ service.revoke(invitation.id, { id: inviter.id }),
374
+ ).rejects.toThrow(BadRequestError);
375
+ });
376
+ });
377
+
378
+ // -----------------------------------------------------------------------------------------------------------------
379
+
380
+ describe("findByEmail", () => {
381
+ it("should return enriched invitations with resource info", async ({
382
+ expect,
383
+ }) => {
384
+ const { service, createUser } = await setup();
385
+
386
+ const inviter = await createUser("inviter@example.com");
387
+
388
+ await service.create(
389
+ {
390
+ email: "lookup@example.com",
391
+ resourceType: "project",
392
+ resourceId: "proj-1",
393
+ },
394
+ { id: inviter.id, email: inviter.email },
395
+ );
396
+
397
+ const results = await service.findByEmail("lookup@example.com");
398
+
399
+ expect(results).toHaveLength(1);
400
+ expect(results[0].email).toBe("lookup@example.com");
401
+ expect(results[0].resourceName).toBe("Test Project");
402
+ expect(results[0].invitedBy).toBe(inviter.id);
403
+ expect(results[0].inviterEmail).toBe("inviter@example.com");
404
+ });
405
+ });
406
+
407
+ // -----------------------------------------------------------------------------------------------------------------
408
+
409
+ describe("findInvitations", () => {
410
+ it("should return paginated results", async ({ expect }) => {
411
+ const { service, createUser } = await setup();
412
+
413
+ const inviter = await createUser("inviter@example.com");
414
+
415
+ await service.create(
416
+ {
417
+ email: "page1@example.com",
418
+ resourceType: "project",
419
+ resourceId: "proj-1",
420
+ },
421
+ { id: inviter.id, email: inviter.email },
422
+ );
423
+
424
+ await service.create(
425
+ {
426
+ email: "page2@example.com",
427
+ resourceType: "project",
428
+ resourceId: "proj-1",
429
+ },
430
+ { id: inviter.id, email: inviter.email },
431
+ );
432
+
433
+ const page = await service.findInvitations({ size: 10 });
434
+
435
+ expect(page.content.length).toBeGreaterThanOrEqual(2);
436
+ expect(page.page.totalElements).toBeGreaterThanOrEqual(2);
437
+ });
438
+ });
439
+ });
@@ -0,0 +1,86 @@
1
+ import { $inject, t } from "alepha";
2
+ import { $secure } from "alepha/security";
3
+ import { $action, okSchema } from "alepha/server";
4
+ import { invitationQuerySchema } from "../schemas/invitationQuerySchema.ts";
5
+ import { invitationResourceSchema } from "../schemas/invitationResourceSchema.ts";
6
+ import { InvitationService } from "../services/InvitationService.ts";
7
+
8
+ export class AdminInvitationController {
9
+ protected readonly url = "/invitations";
10
+ protected readonly group = "admin:invitations";
11
+ protected readonly invitationService = $inject(InvitationService);
12
+
13
+ /**
14
+ * Find invitations with pagination and filtering.
15
+ */
16
+ public readonly findInvitations = $action({
17
+ path: this.url,
18
+ group: this.group,
19
+ use: [$secure({ permissions: ["admin:invitation:read"] })],
20
+ description: "Find invitations with pagination and filtering",
21
+ schema: {
22
+ query: invitationQuerySchema,
23
+ response: t.page(invitationResourceSchema),
24
+ },
25
+ handler: ({ query }) => this.invitationService.findInvitations(query),
26
+ });
27
+
28
+ /**
29
+ * Get an invitation by ID.
30
+ */
31
+ public readonly getInvitation = $action({
32
+ path: `${this.url}/:id`,
33
+ group: this.group,
34
+ use: [$secure({ permissions: ["admin:invitation:read"] })],
35
+ description: "Get an invitation by ID",
36
+ schema: {
37
+ params: t.object({
38
+ id: t.uuid(),
39
+ }),
40
+ response: invitationResourceSchema,
41
+ },
42
+ handler: ({ params }) => this.invitationService.getById(params.id),
43
+ });
44
+
45
+ /**
46
+ * Revoke a pending invitation.
47
+ */
48
+ public readonly revokeInvitation = $action({
49
+ method: "POST",
50
+ path: `${this.url}/:id/revoke`,
51
+ group: this.group,
52
+ use: [$secure({ permissions: ["admin:invitation:delete"] })],
53
+ description: "Revoke a pending invitation",
54
+ schema: {
55
+ params: t.object({
56
+ id: t.uuid(),
57
+ }),
58
+ response: okSchema,
59
+ },
60
+ handler: async ({ params, user }) => {
61
+ await this.invitationService.revoke(params.id, { id: user.id });
62
+ return { ok: true };
63
+ },
64
+ });
65
+
66
+ /**
67
+ * Delete an invitation.
68
+ */
69
+ public readonly deleteInvitation = $action({
70
+ method: "DELETE",
71
+ path: `${this.url}/:id`,
72
+ group: this.group,
73
+ use: [$secure({ permissions: ["admin:invitation:delete"] })],
74
+ description: "Delete an invitation",
75
+ schema: {
76
+ params: t.object({
77
+ id: t.uuid(),
78
+ }),
79
+ response: okSchema,
80
+ },
81
+ handler: async ({ params }) => {
82
+ await this.invitationService.deleteInvitation(params.id);
83
+ return { ok: true, id: params.id };
84
+ },
85
+ });
86
+ }