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.
- package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
- package/dist/api/audits/index.d.ts +8 -8
- package/dist/api/invitations/index.d.ts +790 -0
- package/dist/api/invitations/index.d.ts.map +1 -0
- package/dist/api/invitations/index.js +665 -0
- package/dist/api/invitations/index.js.map +1 -0
- package/dist/api/jobs/index.browser.js +8 -9
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +99 -43
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +257 -40
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +5 -5
- package/dist/api/notifications/index.browser.js +0 -1
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.d.ts +3 -3
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +0 -1
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.browser.js +112 -1
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +90 -3
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +79 -12
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/{billing → api/payments}/index.d.ts +67 -49
- package/dist/api/payments/index.d.ts.map +1 -0
- package/dist/{billing → api/payments}/index.js +108 -74
- package/dist/api/payments/index.js.map +1 -0
- package/dist/api/subscriptions/index.d.ts +1692 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1870 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +18 -2
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +167 -34
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +13 -13
- package/dist/api/workflows/index.browser.js +246 -0
- package/dist/api/workflows/index.browser.js.map +1 -0
- package/dist/api/workflows/index.d.ts +1618 -0
- package/dist/api/workflows/index.d.ts.map +1 -0
- package/dist/api/workflows/index.js +1504 -0
- package/dist/api/workflows/index.js.map +1 -0
- package/dist/cli/core/index.d.ts +44 -28
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +16 -61
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +31 -8
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +79 -24
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/core/index.browser.js +21 -2
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +33 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +21 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +21 -2
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +21 -2
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.js +24 -8
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +0 -18
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +0 -17
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +1 -13
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +0 -17
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js +3 -3
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js +3 -3
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/react/router/index.browser.js +25 -3
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +16 -1
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +25 -3
- package/dist/react/router/index.js.map +1 -1
- package/dist/security/index.d.ts +28 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +28 -0
- package/dist/security/index.js.map +1 -1
- package/package.json +37 -20
- package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
- package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
- package/src/api/invitations/controllers/InvitationController.ts +84 -0
- package/src/api/invitations/entities/invitations.ts +33 -0
- package/src/api/invitations/index.ts +65 -0
- package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
- package/src/api/invitations/providers/InvitationProvider.ts +45 -0
- package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
- package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
- package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
- package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
- package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
- package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
- package/src/api/invitations/services/InvitationService.ts +556 -0
- package/src/api/jobs/__tests__/$job.spec.ts +876 -0
- package/src/api/jobs/controllers/AdminJobController.ts +44 -0
- package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
- package/src/api/jobs/index.ts +0 -3
- package/src/api/jobs/primitives/$job.ts +22 -11
- package/src/api/jobs/providers/JobProvider.ts +229 -19
- package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
- package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
- package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
- package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
- package/src/api/jobs/services/JobService.ts +51 -12
- package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
- package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
- package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
- package/src/api/parameters/index.browser.ts +12 -0
- package/src/api/parameters/primitives/$parameter.ts +20 -3
- package/src/api/parameters/services/ParameterProvider.ts +48 -7
- package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
- package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
- package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
- package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
- package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
- package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
- package/src/{billing → api/payments}/index.ts +31 -25
- package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
- package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
- package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
- package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +144 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -0
- package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
- package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
- package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
- package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
- package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
- package/src/api/users/__tests__/SessionService.spec.ts +142 -1
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
- package/src/api/users/controllers/UserController.ts +3 -8
- package/src/api/users/notifications/UserNotifications.ts +23 -0
- package/src/api/users/schemas/loginSchema.ts +1 -1
- package/src/api/users/services/CredentialService.ts +51 -4
- package/src/api/users/services/RegistrationService.ts +38 -9
- package/src/api/users/services/SessionService.ts +62 -9
- package/src/api/users/services/UserService.ts +21 -12
- package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
- package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
- package/src/api/workflows/entities/workflowExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
- package/src/api/workflows/index.browser.ts +22 -0
- package/src/api/workflows/index.ts +124 -0
- package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
- package/src/api/workflows/primitives/$workflow.ts +202 -0
- package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
- package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
- package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
- package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
- package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
- package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
- package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
- package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
- package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
- package/src/api/workflows/services/WorkflowService.ts +382 -0
- package/src/cli/core/templates/webAppRouterTs.ts +5 -58
- package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
- package/src/cli/vendor/services/VendorService.ts +126 -27
- package/src/core/__tests__/TypeProvider.spec.ts +4 -2
- package/src/core/providers/SchemaValidator.ts +1 -1
- package/src/core/providers/TypeProvider.ts +46 -3
- package/src/orm/__tests__/enums.spec.ts +22 -29
- package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
- package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
- package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
- package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
- package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
- package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
- package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
- package/src/security/primitives/$secure.ts +28 -0
- package/dist/billing/index.d.ts.map +0 -1
- package/dist/billing/index.js.map +0 -1
- package/src/billing/__tests__/BillingService.spec.ts +0 -136
- /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
- /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
- /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
- /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
- /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 (
|
|
389
|
-
const
|
|
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 = (
|