alepha 0.19.3 → 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 (215) 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 +99 -43
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +257 -40
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/keys/index.d.ts +5 -5
  14. package/dist/api/notifications/index.browser.js +0 -1
  15. package/dist/api/notifications/index.browser.js.map +1 -1
  16. package/dist/api/notifications/index.d.ts +3 -3
  17. package/dist/api/notifications/index.d.ts.map +1 -1
  18. package/dist/api/notifications/index.js +0 -1
  19. package/dist/api/notifications/index.js.map +1 -1
  20. package/dist/api/parameters/index.browser.js +112 -1
  21. package/dist/api/parameters/index.browser.js.map +1 -1
  22. package/dist/api/parameters/index.d.ts +90 -3
  23. package/dist/api/parameters/index.d.ts.map +1 -1
  24. package/dist/api/parameters/index.js +79 -12
  25. package/dist/api/parameters/index.js.map +1 -1
  26. package/dist/{billing → api/payments}/index.d.ts +67 -49
  27. package/dist/api/payments/index.d.ts.map +1 -0
  28. package/dist/{billing → api/payments}/index.js +108 -74
  29. package/dist/api/payments/index.js.map +1 -0
  30. package/dist/api/subscriptions/index.d.ts +1692 -0
  31. package/dist/api/subscriptions/index.d.ts.map +1 -0
  32. package/dist/api/subscriptions/index.js +1870 -0
  33. package/dist/api/subscriptions/index.js.map +1 -0
  34. package/dist/api/users/index.d.ts +18 -2
  35. package/dist/api/users/index.d.ts.map +1 -1
  36. package/dist/api/users/index.js +167 -34
  37. package/dist/api/users/index.js.map +1 -1
  38. package/dist/api/verifications/index.d.ts +13 -13
  39. package/dist/api/workflows/index.browser.js +246 -0
  40. package/dist/api/workflows/index.browser.js.map +1 -0
  41. package/dist/api/workflows/index.d.ts +1618 -0
  42. package/dist/api/workflows/index.d.ts.map +1 -0
  43. package/dist/api/workflows/index.js +1504 -0
  44. package/dist/api/workflows/index.js.map +1 -0
  45. package/dist/cli/core/index.d.ts +44 -28
  46. package/dist/cli/core/index.d.ts.map +1 -1
  47. package/dist/cli/core/index.js +16 -61
  48. package/dist/cli/core/index.js.map +1 -1
  49. package/dist/cli/vendor/index.d.ts +31 -8
  50. package/dist/cli/vendor/index.d.ts.map +1 -1
  51. package/dist/cli/vendor/index.js +79 -24
  52. package/dist/cli/vendor/index.js.map +1 -1
  53. package/dist/core/index.browser.js +21 -2
  54. package/dist/core/index.browser.js.map +1 -1
  55. package/dist/core/index.d.ts +33 -2
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +21 -2
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/core/index.native.js +21 -2
  60. package/dist/core/index.native.js.map +1 -1
  61. package/dist/core/index.workerd.js +21 -2
  62. package/dist/core/index.workerd.js.map +1 -1
  63. package/dist/email/smtp/index.js +24 -8
  64. package/dist/email/smtp/index.js.map +1 -1
  65. package/dist/orm/core/index.browser.js +0 -18
  66. package/dist/orm/core/index.browser.js.map +1 -1
  67. package/dist/orm/core/index.bun.js +0 -17
  68. package/dist/orm/core/index.bun.js.map +1 -1
  69. package/dist/orm/core/index.d.ts +1 -13
  70. package/dist/orm/core/index.d.ts.map +1 -1
  71. package/dist/orm/core/index.js +0 -17
  72. package/dist/orm/core/index.js.map +1 -1
  73. package/dist/orm/postgres/index.bun.js +3 -3
  74. package/dist/orm/postgres/index.bun.js.map +1 -1
  75. package/dist/orm/postgres/index.d.ts.map +1 -1
  76. package/dist/orm/postgres/index.js +3 -3
  77. package/dist/orm/postgres/index.js.map +1 -1
  78. package/dist/react/router/index.browser.js +25 -3
  79. package/dist/react/router/index.browser.js.map +1 -1
  80. package/dist/react/router/index.d.ts +16 -1
  81. package/dist/react/router/index.d.ts.map +1 -1
  82. package/dist/react/router/index.js +25 -3
  83. package/dist/react/router/index.js.map +1 -1
  84. package/dist/security/index.d.ts +28 -0
  85. package/dist/security/index.d.ts.map +1 -1
  86. package/dist/security/index.js +28 -0
  87. package/dist/security/index.js.map +1 -1
  88. package/package.json +37 -20
  89. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  90. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  91. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  92. package/src/api/invitations/entities/invitations.ts +33 -0
  93. package/src/api/invitations/index.ts +65 -0
  94. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  95. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  96. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  97. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  98. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  99. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  100. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  101. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  102. package/src/api/invitations/services/InvitationService.ts +556 -0
  103. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  104. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  105. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  106. package/src/api/jobs/index.ts +0 -3
  107. package/src/api/jobs/primitives/$job.ts +22 -11
  108. package/src/api/jobs/providers/JobProvider.ts +229 -19
  109. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  110. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  111. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  112. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  113. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  114. package/src/api/jobs/services/JobService.ts +51 -12
  115. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  116. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  117. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  118. package/src/api/parameters/index.browser.ts +12 -0
  119. package/src/api/parameters/primitives/$parameter.ts +20 -3
  120. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  121. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  122. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  123. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  124. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  125. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  126. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  127. package/src/{billing → api/payments}/index.ts +31 -25
  128. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  129. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  130. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  131. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  132. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  133. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  134. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  135. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  136. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  137. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  138. package/src/api/subscriptions/index.ts +144 -0
  139. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  140. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  141. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  142. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  143. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  144. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  145. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  146. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  147. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  148. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  149. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  150. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  151. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  152. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  153. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  154. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  155. package/src/api/subscriptions/services/BillingService.ts +437 -0
  156. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  157. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  158. package/src/api/subscriptions/services/UsageService.ts +118 -0
  159. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  160. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  161. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  162. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  163. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  164. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  165. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  166. package/src/api/users/controllers/UserController.ts +3 -8
  167. package/src/api/users/notifications/UserNotifications.ts +23 -0
  168. package/src/api/users/schemas/loginSchema.ts +1 -1
  169. package/src/api/users/services/CredentialService.ts +51 -4
  170. package/src/api/users/services/RegistrationService.ts +38 -9
  171. package/src/api/users/services/SessionService.ts +62 -9
  172. package/src/api/users/services/UserService.ts +21 -12
  173. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  174. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  175. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  176. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  177. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  178. package/src/api/workflows/index.browser.ts +22 -0
  179. package/src/api/workflows/index.ts +124 -0
  180. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  181. package/src/api/workflows/primitives/$workflow.ts +202 -0
  182. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  183. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  184. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  185. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  186. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  187. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  188. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  189. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  190. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  191. package/src/api/workflows/services/WorkflowService.ts +382 -0
  192. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  193. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  194. package/src/cli/vendor/services/VendorService.ts +126 -27
  195. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  196. package/src/core/providers/SchemaValidator.ts +1 -1
  197. package/src/core/providers/TypeProvider.ts +46 -3
  198. package/src/orm/__tests__/enums.spec.ts +22 -29
  199. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  200. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  201. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  202. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  203. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  204. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  205. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  206. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  207. package/src/security/primitives/$secure.ts +28 -0
  208. package/dist/billing/index.d.ts.map +0 -1
  209. package/dist/billing/index.js.map +0 -1
  210. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  211. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  212. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  213. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  214. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  215. /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
@@ -0,0 +1,430 @@
1
+ import type { Alepha } from "alepha";
2
+ import { t } from "alepha";
3
+ import { expect } from "vitest";
4
+ import { $entity, $repository, db } from "../core/index.ts";
5
+
6
+ // ============================================================================
7
+ // Entity definitions — two entities with a foreign key relationship
8
+ // ============================================================================
9
+
10
+ const teams = $entity({
11
+ name: "teams",
12
+ schema: t.object({
13
+ id: db.primaryKey(),
14
+ name: t.text(),
15
+ country: t.text(),
16
+ }),
17
+ });
18
+
19
+ const players = $entity({
20
+ name: "players",
21
+ schema: t.object({
22
+ id: db.primaryKey(),
23
+ teamId: db.ref(t.optional(t.integer()), () => teams.cols.id),
24
+ name: t.text(),
25
+ position: t.text(),
26
+ goals: db.default(t.integer(), 0),
27
+ }),
28
+ });
29
+
30
+ class App {
31
+ teams = $repository(teams);
32
+ players = $repository(players);
33
+ }
34
+
35
+ // ============================================================================
36
+ // Shared setup
37
+ // ============================================================================
38
+
39
+ async function seed(app: App) {
40
+ const barca = await app.teams.create({
41
+ name: "FC Barcelona",
42
+ country: "Spain",
43
+ });
44
+ const psg = await app.teams.create({
45
+ name: "Paris Saint-Germain",
46
+ country: "France",
47
+ });
48
+ const real = await app.teams.create({
49
+ name: "Real Madrid",
50
+ country: "Spain",
51
+ });
52
+
53
+ const messi = await app.players.create({
54
+ teamId: barca.id,
55
+ name: "Messi",
56
+ position: "Forward",
57
+ goals: 672,
58
+ });
59
+ const pedri = await app.players.create({
60
+ teamId: barca.id,
61
+ name: "Pedri",
62
+ position: "Midfielder",
63
+ goals: 18,
64
+ });
65
+ const mbappe = await app.players.create({
66
+ teamId: psg.id,
67
+ name: "Mbappé",
68
+ position: "Forward",
69
+ goals: 256,
70
+ });
71
+ const modric = await app.players.create({
72
+ teamId: real.id,
73
+ name: "Modrić",
74
+ position: "Midfielder",
75
+ goals: 39,
76
+ });
77
+ const freeAgent = await app.players.create({
78
+ name: "Free Agent",
79
+ position: "Goalkeeper",
80
+ goals: 0,
81
+ });
82
+
83
+ return {
84
+ teams: { barca, psg, real },
85
+ players: { messi, pedri, mbappe, modric, freeAgent },
86
+ };
87
+ }
88
+
89
+ // ============================================================================
90
+ // 1. Basic left join — player with their team
91
+ // ============================================================================
92
+
93
+ export const testLeftJoinPlayerWithTeam = async (alepha: Alepha) => {
94
+ const app = alepha.inject(App);
95
+ await alepha.start();
96
+ const { players: p } = await seed(app);
97
+
98
+ const result = await app.players.getOne({
99
+ where: { id: { eq: p.messi.id } },
100
+ with: {
101
+ team: {
102
+ join: teams,
103
+ on: ["teamId", teams.cols.id],
104
+ },
105
+ },
106
+ });
107
+
108
+ expect(result.name).toBe("Messi");
109
+ expect(result.team).toBeDefined();
110
+ expect(result.team.name).toBe("FC Barcelona");
111
+ expect(result.team.country).toBe("Spain");
112
+ };
113
+
114
+ // ============================================================================
115
+ // 2. Left join returns undefined when FK is null
116
+ // ============================================================================
117
+
118
+ export const testLeftJoinReturnsUndefinedWhenFkNull = async (
119
+ alepha: Alepha,
120
+ ) => {
121
+ const app = alepha.inject(App);
122
+ await alepha.start();
123
+ const { players: p } = await seed(app);
124
+
125
+ const result = await app.players.getOne({
126
+ where: { id: { eq: p.freeAgent.id } },
127
+ with: {
128
+ team: {
129
+ join: teams,
130
+ on: ["teamId", teams.cols.id],
131
+ },
132
+ },
133
+ });
134
+
135
+ expect(result.name).toBe("Free Agent");
136
+ expect(result.team).toBeUndefined();
137
+ };
138
+
139
+ // ============================================================================
140
+ // 3. Inner join excludes rows without match
141
+ // ============================================================================
142
+
143
+ export const testInnerJoinExcludesNulls = async (alepha: Alepha) => {
144
+ const app = alepha.inject(App);
145
+ await alepha.start();
146
+ await seed(app);
147
+
148
+ const results = await app.players.findMany({
149
+ with: {
150
+ team: {
151
+ type: "inner",
152
+ join: teams,
153
+ on: ["teamId", teams.cols.id],
154
+ },
155
+ },
156
+ });
157
+
158
+ // Free Agent has no team → excluded by inner join
159
+ expect(results.length).toBe(4);
160
+ expect(results.every((r) => r.team !== null && r.team !== undefined)).toBe(
161
+ true,
162
+ );
163
+ expect(results.find((r) => r.name === "Free Agent")).toBeUndefined();
164
+ };
165
+
166
+ // ============================================================================
167
+ // 4. findMany with join + filter on joined table
168
+ // ============================================================================
169
+
170
+ export const testFilterOnJoinedTable = async (alepha: Alepha) => {
171
+ const app = alepha.inject(App);
172
+ await alepha.start();
173
+ await seed(app);
174
+
175
+ const spanishPlayers = await app.players.findMany({
176
+ with: {
177
+ team: {
178
+ join: teams,
179
+ on: ["teamId", teams.cols.id],
180
+ },
181
+ },
182
+ where: {
183
+ team: {
184
+ country: { eq: "Spain" },
185
+ },
186
+ },
187
+ });
188
+
189
+ expect(spanishPlayers.length).toBe(3); // Messi, Pedri (Barça), Modrić (Real)
190
+ expect(spanishPlayers.every((p) => p.team.country === "Spain")).toBe(true);
191
+ };
192
+
193
+ // ============================================================================
194
+ // 5. Combined filters — base table + joined table
195
+ // ============================================================================
196
+
197
+ export const testCombinedFilters = async (alepha: Alepha) => {
198
+ const app = alepha.inject(App);
199
+ await alepha.start();
200
+ await seed(app);
201
+
202
+ const results = await app.players.findMany({
203
+ with: {
204
+ team: {
205
+ join: teams,
206
+ on: ["teamId", teams.cols.id],
207
+ },
208
+ },
209
+ where: {
210
+ and: [
211
+ { position: { eq: "Forward" } },
212
+ { team: { country: { eq: "Spain" } } },
213
+ ],
214
+ },
215
+ });
216
+
217
+ expect(results.length).toBe(1);
218
+ expect(results[0].name).toBe("Messi");
219
+ };
220
+
221
+ // ============================================================================
222
+ // 6. findMany with join + orderBy + limit
223
+ // ============================================================================
224
+
225
+ export const testJoinWithOrderByAndLimit = async (alepha: Alepha) => {
226
+ const app = alepha.inject(App);
227
+ await alepha.start();
228
+ await seed(app);
229
+
230
+ const topScorers = await app.players.findMany({
231
+ with: {
232
+ team: {
233
+ join: teams,
234
+ on: ["teamId", teams.cols.id],
235
+ },
236
+ },
237
+ orderBy: { column: "goals", direction: "desc" },
238
+ limit: 2,
239
+ });
240
+
241
+ expect(topScorers.length).toBe(2);
242
+ expect(topScorers[0].name).toBe("Messi");
243
+ expect(topScorers[1].name).toBe("Mbappé");
244
+ expect(topScorers[0].team).toBeDefined();
245
+ expect(topScorers[1].team).toBeDefined();
246
+ };
247
+
248
+ // ============================================================================
249
+ // 7. Pagination with joins
250
+ // ============================================================================
251
+
252
+ export const testPaginationWithJoins = async (alepha: Alepha) => {
253
+ const app = alepha.inject(App);
254
+ await alepha.start();
255
+ await seed(app);
256
+
257
+ const page = await app.players.paginate(
258
+ { page: 0, size: 2 },
259
+ {
260
+ with: {
261
+ team: {
262
+ join: teams,
263
+ on: ["teamId", teams.cols.id],
264
+ },
265
+ },
266
+ orderBy: { column: "name", direction: "asc" },
267
+ },
268
+ );
269
+
270
+ expect(page.content.length).toBe(2);
271
+ expect(page.page.isFirst).toBe(true);
272
+ expect(page.page.isLast).toBe(false);
273
+ // Each result should include the joined team
274
+ for (const player of page.content) {
275
+ // Some players may have no team (left join)
276
+ if (player.teamId) {
277
+ expect(player.team).toBeDefined();
278
+ }
279
+ }
280
+ };
281
+
282
+ // ============================================================================
283
+ // 8. OR filter across base + joined table
284
+ // ============================================================================
285
+
286
+ export const testOrFilterAcrossTables = async (alepha: Alepha) => {
287
+ const app = alepha.inject(App);
288
+ await alepha.start();
289
+ await seed(app);
290
+
291
+ const results = await app.players.findMany({
292
+ with: {
293
+ team: {
294
+ join: teams,
295
+ on: ["teamId", teams.cols.id],
296
+ },
297
+ },
298
+ where: {
299
+ or: [{ goals: { gte: 200 } }, { team: { country: { eq: "France" } } }],
300
+ },
301
+ orderBy: { column: "goals", direction: "desc" },
302
+ });
303
+
304
+ // Messi (672), Mbappé (256 + France) — Mbappé matches both conditions
305
+ expect(results.length).toBe(2);
306
+ expect(results.map((r) => r.name)).toEqual(["Messi", "Mbappé"]);
307
+ };
308
+
309
+ // ============================================================================
310
+ // 9. getOne with join throws when not found
311
+ // ============================================================================
312
+
313
+ export const testGetOneWithJoinThrowsWhenNotFound = async (alepha: Alepha) => {
314
+ const app = alepha.inject(App);
315
+ await alepha.start();
316
+ await seed(app);
317
+
318
+ await expect(
319
+ app.players.getOne({
320
+ where: { name: { eq: "Neymar" } },
321
+ with: {
322
+ team: {
323
+ join: teams,
324
+ on: ["teamId", teams.cols.id],
325
+ },
326
+ },
327
+ }),
328
+ ).rejects.toThrow();
329
+ };
330
+
331
+ // ============================================================================
332
+ // 10. findOne with join — returns undefined instead of throwing
333
+ // ============================================================================
334
+
335
+ export const testFindOneWithJoinReturnsUndefined = async (alepha: Alepha) => {
336
+ const app = alepha.inject(App);
337
+ await alepha.start();
338
+ await seed(app);
339
+
340
+ const result = await app.players.findOne({
341
+ where: { name: { eq: "Neymar" } },
342
+ with: {
343
+ team: {
344
+ join: teams,
345
+ on: ["teamId", teams.cols.id],
346
+ },
347
+ },
348
+ });
349
+
350
+ expect(result).toBeUndefined();
351
+ };
352
+
353
+ // ============================================================================
354
+ // 11. Empty result set with joins
355
+ // ============================================================================
356
+
357
+ export const testEmptyResultWithJoins = async (alepha: Alepha) => {
358
+ const app = alepha.inject(App);
359
+ await alepha.start();
360
+ await seed(app);
361
+
362
+ const results = await app.players.findMany({
363
+ where: { position: { eq: "Defender" } },
364
+ with: {
365
+ team: {
366
+ join: teams,
367
+ on: ["teamId", teams.cols.id],
368
+ },
369
+ },
370
+ });
371
+
372
+ expect(results.length).toBe(0);
373
+ };
374
+
375
+ // ============================================================================
376
+ // 12. Multiple operators on joined table filter
377
+ // ============================================================================
378
+
379
+ export const testMultipleOperatorsOnJoinedFilter = async (alepha: Alepha) => {
380
+ const app = alepha.inject(App);
381
+ await alepha.start();
382
+ await seed(app);
383
+
384
+ const results = await app.players.findMany({
385
+ with: {
386
+ team: {
387
+ join: teams,
388
+ on: ["teamId", teams.cols.id],
389
+ },
390
+ },
391
+ where: {
392
+ team: {
393
+ name: { contains: "paris" }, // case-insensitive
394
+ },
395
+ },
396
+ });
397
+
398
+ expect(results.length).toBe(1);
399
+ expect(results[0].name).toBe("Mbappé");
400
+ expect(results[0].team.name).toBe("Paris Saint-Germain");
401
+ };
402
+
403
+ // ============================================================================
404
+ // 13. Join with offset (pagination without paginate)
405
+ // ============================================================================
406
+
407
+ export const testJoinWithOffset = async (alepha: Alepha) => {
408
+ const app = alepha.inject(App);
409
+ await alepha.start();
410
+ await seed(app);
411
+
412
+ const results = await app.players.findMany({
413
+ with: {
414
+ team: {
415
+ type: "inner",
416
+ join: teams,
417
+ on: ["teamId", teams.cols.id],
418
+ },
419
+ },
420
+ orderBy: { column: "name", direction: "asc" },
421
+ offset: 1,
422
+ limit: 2,
423
+ });
424
+
425
+ // Inner join excludes Free Agent → sorted: Mbappé, Messi, Modrić, Pedri
426
+ // offset 1 + limit 2 → Messi, Modrić
427
+ expect(results.length).toBe(2);
428
+ expect(results[0].name).toBe("Messi");
429
+ expect(results[1].name).toBe("Modrić");
430
+ };
@@ -0,0 +1,167 @@
1
+ import { Alepha } from "alepha";
2
+ import { describe, it } from "vitest";
3
+ import { AlephaOrmPostgres } from "../postgres/index.ts";
4
+ import {
5
+ testCombinedFilters,
6
+ testEmptyResultWithJoins,
7
+ testFilterOnJoinedTable,
8
+ testFindOneWithJoinReturnsUndefined,
9
+ testGetOneWithJoinThrowsWhenNotFound,
10
+ testInnerJoinExcludesNulls,
11
+ testJoinWithOffset,
12
+ testJoinWithOrderByAndLimit,
13
+ testLeftJoinPlayerWithTeam,
14
+ testLeftJoinReturnsUndefinedWhenFkNull,
15
+ testMultipleOperatorsOnJoinedFilter,
16
+ testOrFilterAcrossTables,
17
+ testPaginationWithJoins,
18
+ } from "./orm-showcase-tests.ts";
19
+
20
+ describe("orm showcase: entities + joins", () => {
21
+ describe("basic left join", () => {
22
+ it("should join player with team (sqlite)", async () => {
23
+ await testLeftJoinPlayerWithTeam(
24
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
25
+ );
26
+ });
27
+ it("should join player with team (postgres)", async () => {
28
+ await testLeftJoinPlayerWithTeam(Alepha.create().with(AlephaOrmPostgres));
29
+ });
30
+ });
31
+
32
+ describe("left join with null FK", () => {
33
+ it("should return undefined when FK is null (sqlite)", async () => {
34
+ await testLeftJoinReturnsUndefinedWhenFkNull(
35
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
36
+ );
37
+ });
38
+ it("should return undefined when FK is null (postgres)", async () => {
39
+ await testLeftJoinReturnsUndefinedWhenFkNull(
40
+ Alepha.create().with(AlephaOrmPostgres),
41
+ );
42
+ });
43
+ });
44
+
45
+ describe("inner join", () => {
46
+ it("should exclude rows without match (sqlite)", async () => {
47
+ await testInnerJoinExcludesNulls(
48
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
49
+ );
50
+ });
51
+ it("should exclude rows without match (postgres)", async () => {
52
+ await testInnerJoinExcludesNulls(Alepha.create().with(AlephaOrmPostgres));
53
+ });
54
+ });
55
+
56
+ describe("filter on joined table", () => {
57
+ it("should filter by joined table column (sqlite)", async () => {
58
+ await testFilterOnJoinedTable(
59
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
60
+ );
61
+ });
62
+ it("should filter by joined table column (postgres)", async () => {
63
+ await testFilterOnJoinedTable(Alepha.create().with(AlephaOrmPostgres));
64
+ });
65
+ });
66
+
67
+ describe("combined base + join filters", () => {
68
+ it("should combine base and join table filters (sqlite)", async () => {
69
+ await testCombinedFilters(
70
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
71
+ );
72
+ });
73
+ it("should combine base and join table filters (postgres)", async () => {
74
+ await testCombinedFilters(Alepha.create().with(AlephaOrmPostgres));
75
+ });
76
+ });
77
+
78
+ describe("orderBy + limit with joins", () => {
79
+ it("should order and limit with joins (sqlite)", async () => {
80
+ await testJoinWithOrderByAndLimit(
81
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
82
+ );
83
+ });
84
+ it("should order and limit with joins (postgres)", async () => {
85
+ await testJoinWithOrderByAndLimit(
86
+ Alepha.create().with(AlephaOrmPostgres),
87
+ );
88
+ });
89
+ });
90
+
91
+ describe("pagination with joins", () => {
92
+ it("should paginate with joins (sqlite)", async () => {
93
+ await testPaginationWithJoins(
94
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
95
+ );
96
+ });
97
+ it("should paginate with joins (postgres)", async () => {
98
+ await testPaginationWithJoins(Alepha.create().with(AlephaOrmPostgres));
99
+ });
100
+ });
101
+
102
+ describe("OR filter across tables", () => {
103
+ it("should OR across base and joined table (sqlite)", async () => {
104
+ await testOrFilterAcrossTables(
105
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
106
+ );
107
+ });
108
+ it("should OR across base and joined table (postgres)", async () => {
109
+ await testOrFilterAcrossTables(Alepha.create().with(AlephaOrmPostgres));
110
+ });
111
+ });
112
+
113
+ describe("getOne / findOne with joins", () => {
114
+ it("should throw when not found via getOne (sqlite)", async () => {
115
+ await testGetOneWithJoinThrowsWhenNotFound(
116
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
117
+ );
118
+ });
119
+ it("should throw when not found via getOne (postgres)", async () => {
120
+ await testGetOneWithJoinThrowsWhenNotFound(
121
+ Alepha.create().with(AlephaOrmPostgres),
122
+ );
123
+ });
124
+
125
+ it("should return undefined via findOne (sqlite)", async () => {
126
+ await testFindOneWithJoinReturnsUndefined(
127
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
128
+ );
129
+ });
130
+ it("should return undefined via findOne (postgres)", async () => {
131
+ await testFindOneWithJoinReturnsUndefined(
132
+ Alepha.create().with(AlephaOrmPostgres),
133
+ );
134
+ });
135
+ });
136
+
137
+ describe("edge cases", () => {
138
+ it("should return empty array when no matches (sqlite)", async () => {
139
+ await testEmptyResultWithJoins(
140
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
141
+ );
142
+ });
143
+ it("should return empty array when no matches (postgres)", async () => {
144
+ await testEmptyResultWithJoins(Alepha.create().with(AlephaOrmPostgres));
145
+ });
146
+
147
+ it("should filter with operators on joined table (sqlite)", async () => {
148
+ await testMultipleOperatorsOnJoinedFilter(
149
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
150
+ );
151
+ });
152
+ it("should filter with operators on joined table (postgres)", async () => {
153
+ await testMultipleOperatorsOnJoinedFilter(
154
+ Alepha.create().with(AlephaOrmPostgres),
155
+ );
156
+ });
157
+
158
+ it("should handle offset with inner join (sqlite)", async () => {
159
+ await testJoinWithOffset(
160
+ Alepha.create({ env: { DATABASE_URL: "sqlite://:memory:" } }),
161
+ );
162
+ });
163
+ it("should handle offset with inner join (postgres)", async () => {
164
+ await testJoinWithOffset(Alepha.create().with(AlephaOrmPostgres));
165
+ });
166
+ });
167
+ });
@@ -12,7 +12,6 @@ import {
12
12
  type TSchema,
13
13
  type TString,
14
14
  type TStringOptions,
15
- type TUnsafe,
16
15
  t,
17
16
  } from "alepha";
18
17
  import type { UpdateDeleteAction } from "drizzle-orm/pg-core/foreign-keys";
@@ -20,7 +19,6 @@ import {
20
19
  PG_CREATED_AT,
21
20
  PG_DEFAULT,
22
21
  PG_DELETED_AT,
23
- PG_ENUM,
24
22
  PG_IDENTITY,
25
23
  PG_ORGANIZATION,
26
24
  PG_PRIMARY_KEY,
@@ -28,7 +26,6 @@ import {
28
26
  PG_UPDATED_AT,
29
27
  PG_VERSION,
30
28
  type PgDefault,
31
- type PgEnumOptions,
32
29
  type PgIdentityOptions,
33
30
  type PgPrimaryKey,
34
31
  type PgRef,
@@ -198,32 +195,6 @@ export class DatabaseTypeProvider {
198
195
  public readonly organization = () =>
199
196
  pgAttr(t.optional(t.uuid()), PG_ORGANIZATION);
200
197
 
201
- /**
202
- * Creates a Postgres ENUM type.
203
- *
204
- * > By default, `t.enum()` is mapped to a TEXT column in Postgres.
205
- * > Using this method, you can create a real ENUM type in the database.
206
- *
207
- * @example
208
- * ```ts
209
- * const statusEnum = pg.enum(["pending", "active", "archived"], { name: "status_enum" });
210
- * ```
211
- */
212
- public readonly enum = <T extends string[]>(
213
- values: [...T],
214
- pgEnumOptions?: PgEnumOptions,
215
- typeOptions?: TStringOptions,
216
- ): PgAttr<TUnsafe<T[number]>, typeof PG_ENUM> => {
217
- return pgAttr(
218
- t.enum(values, {
219
- description: pgEnumOptions?.description,
220
- ...typeOptions,
221
- }),
222
- PG_ENUM,
223
- pgEnumOptions,
224
- );
225
- };
226
-
227
198
  /**
228
199
  * Creates a reference to another table or schema. Basically a foreign key.
229
200
  */
@@ -4,14 +4,12 @@ import {
4
4
  type FromSchema,
5
5
  ModelBuilder,
6
6
  PG_CREATED_AT,
7
- PG_ENUM,
8
7
  PG_GENERATED,
9
8
  PG_IDENTITY,
10
9
  PG_PRIMARY_KEY,
11
10
  PG_REF,
12
11
  PG_SERIAL,
13
12
  PG_UPDATED_AT,
14
- type PgEnumOptions,
15
13
  type PgGeneratedOptions,
16
14
  type PgIdentityOptions,
17
15
  type PgRefOptions,
@@ -384,10 +382,9 @@ export class PostgresModelBuilder extends ModelBuilder {
384
382
  );
385
383
  }
386
384
 
387
- // SQL Enum
388
- if (PG_ENUM in value && value[PG_ENUM]) {
389
- const options = value[PG_ENUM] as PgEnumOptions;
390
- const enumName = options.name ?? `${tableName}_${key}_enum`;
385
+ // SQL Enum (default for t.enum unless mode: "text")
386
+ if ((value as any).mode !== "text") {
387
+ const enumName = (value as any).enumName ?? `${tableName}_${key}_enum`;
391
388
 
392
389
  if (enums.has(enumName)) {
393
390
  const values = (