honertia 0.1.3 → 0.1.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/README.md +236 -51
- package/dist/effect/action.d.ts +43 -83
- package/dist/effect/action.d.ts.map +1 -1
- package/dist/effect/action.js +57 -116
- package/dist/effect/index.d.ts +1 -1
- package/dist/effect/index.d.ts.map +1 -1
- package/dist/effect/index.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -433,47 +433,55 @@ vite.hmrHead() // HMR preamble script tags for React Fast Refresh
|
|
|
433
433
|
|
|
434
434
|
### Effect-Based Handlers
|
|
435
435
|
|
|
436
|
-
Route handlers are Effect computations that return `Response | Redirect
|
|
436
|
+
Route handlers are Effect computations that return `Response | Redirect`. Actions are fully composable - you opt-in to features by yielding services:
|
|
437
437
|
|
|
438
438
|
```typescript
|
|
439
439
|
import { Effect } from 'effect'
|
|
440
440
|
import {
|
|
441
|
+
action,
|
|
442
|
+
authorize,
|
|
443
|
+
validateRequest,
|
|
441
444
|
DatabaseService,
|
|
442
|
-
AuthUserService,
|
|
443
445
|
render,
|
|
444
446
|
redirect,
|
|
445
|
-
} from 'honertia'
|
|
446
|
-
|
|
447
|
-
// Simple page render
|
|
448
|
-
export const showDashboard = Effect.gen(function* () {
|
|
449
|
-
const db = yield* DatabaseService
|
|
450
|
-
const user = yield* AuthUserService
|
|
447
|
+
} from 'honertia/effect'
|
|
451
448
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
)
|
|
449
|
+
// Simple page render with auth
|
|
450
|
+
export const showDashboard = action(
|
|
451
|
+
Effect.gen(function* () {
|
|
452
|
+
const auth = yield* authorize()
|
|
453
|
+
const db = yield* DatabaseService
|
|
458
454
|
|
|
459
|
-
|
|
460
|
-
|
|
455
|
+
const projects = yield* Effect.tryPromise(() =>
|
|
456
|
+
db.query.projects.findMany({
|
|
457
|
+
where: eq(schema.projects.userId, auth.user.id),
|
|
458
|
+
limit: 5,
|
|
459
|
+
})
|
|
460
|
+
)
|
|
461
461
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
const user = yield* AuthUserService
|
|
466
|
-
const input = yield* validateRequest(CreateProjectSchema)
|
|
462
|
+
return yield* render('Dashboard/Index', { projects })
|
|
463
|
+
})
|
|
464
|
+
)
|
|
467
465
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
466
|
+
// Form submission with permissions, validation, and redirect
|
|
467
|
+
export const createProject = action(
|
|
468
|
+
Effect.gen(function* () {
|
|
469
|
+
const auth = yield* authorize((a) => a.user.role === 'admin')
|
|
470
|
+
const input = yield* validateRequest(CreateProjectSchema, {
|
|
471
|
+
errorComponent: 'Projects/Create',
|
|
472
472
|
})
|
|
473
|
-
|
|
473
|
+
const db = yield* DatabaseService
|
|
474
474
|
|
|
475
|
-
|
|
476
|
-
|
|
475
|
+
yield* Effect.tryPromise(() =>
|
|
476
|
+
db.insert(schema.projects).values({
|
|
477
|
+
...input,
|
|
478
|
+
userId: auth.user.id,
|
|
479
|
+
})
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
return yield* redirect('/projects')
|
|
483
|
+
})
|
|
484
|
+
)
|
|
477
485
|
```
|
|
478
486
|
|
|
479
487
|
### Services
|
|
@@ -964,42 +972,219 @@ export const logoutUser = betterAuthLogoutAction({
|
|
|
964
972
|
2. **better-auth returns an error** → Calls your `errorMapper`, then re-renders `errorComponent` with those errors
|
|
965
973
|
3. **Success** → Sets session cookies from better-auth's response headers, then 303 redirects to `redirectTo`
|
|
966
974
|
|
|
967
|
-
## Action
|
|
975
|
+
## Anatomy of an Action
|
|
976
|
+
|
|
977
|
+
Actions in Honertia are fully composable Effect computations. Instead of using different action factories for different combinations of features, you opt-in to exactly what you need by yielding services and helpers inside your action.
|
|
968
978
|
|
|
969
|
-
|
|
979
|
+
This design is inspired by Laravel's [laravel-actions](https://laravelactions.com/) package, where you opt-in to capabilities by adding methods to your action class. In Honertia, you opt-in by yielding services - the order of your `yield*` statements determines the execution order.
|
|
980
|
+
|
|
981
|
+
### The `action` Wrapper
|
|
982
|
+
|
|
983
|
+
The `action` function is a semantic wrapper that marks an Effect as an action:
|
|
970
984
|
|
|
971
985
|
```typescript
|
|
972
|
-
import {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
export const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
986
|
+
import { Effect } from 'effect'
|
|
987
|
+
import { action } from 'honertia/effect'
|
|
988
|
+
|
|
989
|
+
export const myAction = action(
|
|
990
|
+
Effect.gen(function* () {
|
|
991
|
+
// Your action logic here
|
|
992
|
+
return new Response('OK')
|
|
993
|
+
})
|
|
994
|
+
)
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
It's intentionally minimal - all the power comes from what you yield inside.
|
|
998
|
+
|
|
999
|
+
### Composable Helpers
|
|
1000
|
+
|
|
1001
|
+
#### `authorize` - Authentication & Authorization
|
|
1002
|
+
|
|
1003
|
+
Opt-in to authentication and authorization checks. Returns the authenticated user, fails with `UnauthorizedError` if no user is present, and fails with `ForbiddenError` if the check returns `false`.
|
|
1004
|
+
|
|
1005
|
+
```typescript
|
|
1006
|
+
import { authorize } from 'honertia/effect'
|
|
1007
|
+
|
|
1008
|
+
// Just require authentication (any logged-in user)
|
|
1009
|
+
const auth = yield* authorize()
|
|
1010
|
+
|
|
1011
|
+
// Require a specific role
|
|
1012
|
+
const auth = yield* authorize((a) => a.user.role === 'admin')
|
|
1013
|
+
|
|
1014
|
+
// Require resource ownership
|
|
1015
|
+
const auth = yield* authorize((a) => a.user.id === project.userId)
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
If the check function returns `false`, the action fails immediately with a `ForbiddenError`.
|
|
1019
|
+
|
|
1020
|
+
#### `validateRequest` - Schema Validation
|
|
1021
|
+
|
|
1022
|
+
Opt-in to request validation using Effect Schema:
|
|
1023
|
+
|
|
1024
|
+
```typescript
|
|
1025
|
+
import { Schema as S } from 'effect'
|
|
1026
|
+
import { validateRequest, requiredString } from 'honertia/effect'
|
|
1027
|
+
|
|
1028
|
+
const input = yield* validateRequest(
|
|
1029
|
+
S.Struct({ name: requiredString, description: S.optional(S.String) }),
|
|
1030
|
+
{ errorComponent: 'Projects/Create' }
|
|
1031
|
+
)
|
|
1032
|
+
// input is fully typed: { name: string, description?: string }
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
On validation failure, re-renders `errorComponent` with field-level errors.
|
|
1036
|
+
|
|
1037
|
+
#### `DatabaseService` - Database Access
|
|
1038
|
+
|
|
1039
|
+
Opt-in to database access:
|
|
1040
|
+
|
|
1041
|
+
```typescript
|
|
1042
|
+
import { DatabaseService } from 'honertia/effect'
|
|
1043
|
+
|
|
1044
|
+
const db = yield* DatabaseService
|
|
1045
|
+
const projects = yield* Effect.tryPromise(() =>
|
|
1046
|
+
db.query.projects.findMany()
|
|
982
1047
|
)
|
|
1048
|
+
```
|
|
983
1049
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1050
|
+
#### `render` / `redirect` - Responses
|
|
1051
|
+
|
|
1052
|
+
Return responses from your action:
|
|
1053
|
+
|
|
1054
|
+
```typescript
|
|
1055
|
+
import { render, redirect } from 'honertia/effect'
|
|
1056
|
+
|
|
1057
|
+
// Render a page
|
|
1058
|
+
return yield* render('Projects/Index', { projects })
|
|
1059
|
+
|
|
1060
|
+
// Redirect after mutation
|
|
1061
|
+
return yield* redirect('/projects')
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
### Building an Action
|
|
1065
|
+
|
|
1066
|
+
Here's how these composables work together:
|
|
1067
|
+
|
|
1068
|
+
```typescript
|
|
1069
|
+
import { Effect, Schema as S } from 'effect'
|
|
1070
|
+
import {
|
|
1071
|
+
action,
|
|
1072
|
+
authorize,
|
|
1073
|
+
validateRequest,
|
|
1074
|
+
DatabaseService,
|
|
1075
|
+
redirect,
|
|
1076
|
+
requiredString,
|
|
1077
|
+
} from 'honertia/effect'
|
|
1078
|
+
|
|
1079
|
+
const CreateProjectSchema = S.Struct({
|
|
1080
|
+
name: requiredString,
|
|
1081
|
+
description: S.optional(S.String),
|
|
1082
|
+
})
|
|
1083
|
+
|
|
1084
|
+
export const createProject = action(
|
|
1085
|
+
Effect.gen(function* () {
|
|
1086
|
+
// 1. Authorization - fail fast if not allowed
|
|
1087
|
+
const auth = yield* authorize((a) => a.user.role === 'author')
|
|
1088
|
+
|
|
1089
|
+
// 2. Validation - parse and validate request body
|
|
1090
|
+
const input = yield* validateRequest(CreateProjectSchema, {
|
|
1091
|
+
errorComponent: 'Projects/Create',
|
|
1092
|
+
})
|
|
1093
|
+
|
|
1094
|
+
// 3. Database - perform the mutation
|
|
1095
|
+
const db = yield* DatabaseService
|
|
988
1096
|
yield* Effect.tryPromise(() =>
|
|
989
|
-
db.insert(projects).values({
|
|
1097
|
+
db.insert(projects).values({
|
|
1098
|
+
...input,
|
|
1099
|
+
userId: auth.user.id,
|
|
1100
|
+
})
|
|
990
1101
|
)
|
|
1102
|
+
|
|
1103
|
+
// 4. Response - redirect on success
|
|
991
1104
|
return yield* redirect('/projects')
|
|
992
|
-
})
|
|
993
|
-
{ errorComponent: 'Projects/Create' }
|
|
1105
|
+
})
|
|
994
1106
|
)
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
### Execution Order Matters
|
|
1110
|
+
|
|
1111
|
+
The order you yield services determines when they execute:
|
|
1112
|
+
|
|
1113
|
+
```typescript
|
|
1114
|
+
// Authorization BEFORE validation (recommended for mutations)
|
|
1115
|
+
// Don't waste cycles validating if user can't perform the action
|
|
1116
|
+
const auth = yield* authorize((a) => a.user.role === 'admin')
|
|
1117
|
+
const input = yield* validateRequest(schema)
|
|
1118
|
+
|
|
1119
|
+
// Validation BEFORE authorization (when you need input for auth check)
|
|
1120
|
+
const input = yield* validateRequest(schema)
|
|
1121
|
+
const auth = yield* authorize((a) => a.user.id === input.ownerId)
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### Type Safety
|
|
1125
|
+
|
|
1126
|
+
Effect tracks all service requirements at the type level. Your action's type signature shows exactly what it needs:
|
|
995
1127
|
|
|
996
|
-
|
|
997
|
-
|
|
1128
|
+
```typescript
|
|
1129
|
+
// This action requires: RequestService, DatabaseService
|
|
1130
|
+
export const createProject: Effect.Effect<
|
|
1131
|
+
Response | Redirect,
|
|
1132
|
+
ValidationError | UnauthorizedError | ForbiddenError | Error,
|
|
1133
|
+
RequestService | DatabaseService
|
|
1134
|
+
>
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
The compiler ensures all required services are provided when the action runs.
|
|
1138
|
+
Note: `authorize` uses an optional `AuthUserService`, so it won't appear in the required service list unless you `yield* AuthUserService` directly or provide `RequireAuthLayer` explicitly.
|
|
1139
|
+
|
|
1140
|
+
### Minimal Actions
|
|
1141
|
+
|
|
1142
|
+
Not every action needs all features. Use only what you need:
|
|
1143
|
+
|
|
1144
|
+
```typescript
|
|
1145
|
+
// Public page - no auth, no validation
|
|
1146
|
+
export const showAbout = action(
|
|
998
1147
|
Effect.gen(function* () {
|
|
999
|
-
|
|
1000
|
-
return yield* render('Dashboard', data)
|
|
1148
|
+
return yield* render('About', {})
|
|
1001
1149
|
})
|
|
1002
1150
|
)
|
|
1151
|
+
|
|
1152
|
+
// Read-only authenticated page
|
|
1153
|
+
export const showDashboard = action(
|
|
1154
|
+
Effect.gen(function* () {
|
|
1155
|
+
const auth = yield* authorize()
|
|
1156
|
+
const db = yield* DatabaseService
|
|
1157
|
+
const stats = yield* fetchStats(db, auth)
|
|
1158
|
+
return yield* render('Dashboard', { stats })
|
|
1159
|
+
})
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
// API endpoint with just validation
|
|
1163
|
+
export const searchProjects = action(
|
|
1164
|
+
Effect.gen(function* () {
|
|
1165
|
+
const { query } = yield* validateRequest(S.Struct({ query: S.String }))
|
|
1166
|
+
const db = yield* DatabaseService
|
|
1167
|
+
const results = yield* search(db, query)
|
|
1168
|
+
return yield* json({ results })
|
|
1169
|
+
})
|
|
1170
|
+
)
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
### Helper Utilities
|
|
1174
|
+
|
|
1175
|
+
#### `dbTransaction` - Database Transactions
|
|
1176
|
+
|
|
1177
|
+
Run multiple database operations in a transaction with automatic rollback on failure:
|
|
1178
|
+
|
|
1179
|
+
```typescript
|
|
1180
|
+
import { dbTransaction } from 'honertia/effect'
|
|
1181
|
+
|
|
1182
|
+
yield* dbTransaction(async (tx) => {
|
|
1183
|
+
await tx.insert(users).values({ name: 'Alice', email: 'alice@example.com' })
|
|
1184
|
+
await tx.update(accounts).set({ balance: 100 }).where(eq(accounts.userId, id))
|
|
1185
|
+
// If any operation fails, the entire transaction rolls back
|
|
1186
|
+
return { success: true }
|
|
1187
|
+
})
|
|
1003
1188
|
```
|
|
1004
1189
|
|
|
1005
1190
|
## React Integration
|
package/dist/effect/action.d.ts
CHANGED
|
@@ -1,107 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Effect Action
|
|
2
|
+
* Effect Action Composables
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Composable helpers for building Effect-based request handlers.
|
|
5
|
+
* Actions are fully opt-in - yield* only what you need.
|
|
5
6
|
*/
|
|
6
|
-
import { Effect
|
|
7
|
-
import { DatabaseService,
|
|
8
|
-
import {
|
|
9
|
-
import { Redirect } from './errors.js';
|
|
7
|
+
import { Effect } from 'effect';
|
|
8
|
+
import { DatabaseService, type AuthUser } from './services.js';
|
|
9
|
+
import { UnauthorizedError, ForbiddenError, Redirect } from './errors.js';
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Semantic wrapper for Effect actions.
|
|
12
|
+
*
|
|
13
|
+
* This is a minimal wrapper that marks an Effect as an action.
|
|
14
|
+
* All capabilities are opt-in via yield* inside your handler.
|
|
12
15
|
*
|
|
13
16
|
* @example
|
|
14
|
-
* const createProject =
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* const createProject = action(
|
|
18
|
+
* Effect.gen(function* () {
|
|
19
|
+
* // Opt-in to authorization
|
|
20
|
+
* const auth = yield* authorize()
|
|
21
|
+
*
|
|
22
|
+
* // Opt-in to validation
|
|
23
|
+
* const input = yield* validateRequest(S.Struct({ name: requiredString }))
|
|
24
|
+
*
|
|
25
|
+
* // Opt-in to database
|
|
17
26
|
* const db = yield* DatabaseService
|
|
18
|
-
* yield* Effect.tryPromise(() => db.insert(projects).values(input))
|
|
19
|
-
* return new Redirect({ url: '/projects', status: 303 })
|
|
20
|
-
* })
|
|
21
|
-
* )
|
|
22
|
-
*/
|
|
23
|
-
export declare function effectAction<A, I, R, E>(schema: S.Schema<A, I>, handler: (input: A) => Effect.Effect<Response | Redirect, E, R>, options?: {
|
|
24
|
-
errorComponent?: string;
|
|
25
|
-
messages?: Record<string, string>;
|
|
26
|
-
attributes?: Record<string, string>;
|
|
27
|
-
}): Effect.Effect<Response | Redirect, E | ValidationError, R | RequestService>;
|
|
28
|
-
/**
|
|
29
|
-
* Create an Effect action that requires authentication and database access.
|
|
30
27
|
*
|
|
31
|
-
* @example
|
|
32
|
-
* const createProject = dbAction(
|
|
33
|
-
* S.Struct({ name: requiredString }),
|
|
34
|
-
* (input, { db, user }) => Effect.gen(function* () {
|
|
35
28
|
* yield* Effect.tryPromise(() =>
|
|
36
|
-
* db.insert(projects).values({ ...input, userId:
|
|
29
|
+
* db.insert(projects).values({ ...input, userId: auth.user.id })
|
|
37
30
|
* )
|
|
38
31
|
* return new Redirect({ url: '/projects', status: 303 })
|
|
39
32
|
* })
|
|
40
33
|
* )
|
|
41
34
|
*/
|
|
42
|
-
export declare function
|
|
43
|
-
db: unknown;
|
|
44
|
-
user: AuthUser;
|
|
45
|
-
}) => Effect.Effect<Response | Redirect, E, never>, options?: {
|
|
46
|
-
errorComponent?: string;
|
|
47
|
-
messages?: Record<string, string>;
|
|
48
|
-
attributes?: Record<string, string>;
|
|
49
|
-
}): Effect.Effect<Response | Redirect, E | ValidationError | UnauthorizedError, RequestService | DatabaseService | AuthUserService>;
|
|
35
|
+
export declare function action<R, E>(handler: Effect.Effect<Response | Redirect, E, R>): Effect.Effect<Response | Redirect, E, R>;
|
|
50
36
|
/**
|
|
51
|
-
*
|
|
37
|
+
* Authorization helper - opt-in to auth check.
|
|
52
38
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* return yield* Effect.tryPromise(() => honertia.render('Dashboard', { user: user.user }))
|
|
58
|
-
* }))
|
|
59
|
-
*/
|
|
60
|
-
export declare function authAction<R, E>(handler: (user: AuthUser) => Effect.Effect<Response | Redirect, E, R>): Effect.Effect<Response | Redirect, E | UnauthorizedError, R | AuthUserService>;
|
|
61
|
-
/**
|
|
62
|
-
* Create a simple Effect action without validation.
|
|
39
|
+
* Returns the authenticated user if authorized.
|
|
40
|
+
* Fails with UnauthorizedError if no user is present.
|
|
41
|
+
* Fails with ForbiddenError if the check returns false.
|
|
42
|
+
* The check function is optional - if not provided, just requires authentication.
|
|
63
43
|
*
|
|
64
44
|
* @example
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
45
|
+
* // Just require authentication
|
|
46
|
+
* const auth = yield* authorize()
|
|
47
|
+
*
|
|
48
|
+
* // Require specific role (if your user type has a role field)
|
|
49
|
+
* const auth = yield* authorize((a) => a.user.role === 'admin')
|
|
50
|
+
*
|
|
51
|
+
* // Require resource ownership
|
|
52
|
+
* const auth = yield* authorize((a) => a.user.id === project.userId)
|
|
72
53
|
*/
|
|
73
|
-
export declare function
|
|
54
|
+
export declare function authorize(check?: (user: AuthUser) => boolean): Effect.Effect<AuthUser, UnauthorizedError | ForbiddenError, never>;
|
|
74
55
|
/**
|
|
75
|
-
*
|
|
56
|
+
* Run multiple database operations in a transaction.
|
|
57
|
+
* Automatically rolls back on any failure.
|
|
76
58
|
*
|
|
77
59
|
* @example
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* (
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* Effect.tryPromise(() => db.insert(projects).values({ name, userId }))
|
|
84
|
-
* )
|
|
85
|
-
* )
|
|
86
|
-
* )
|
|
87
|
-
*/
|
|
88
|
-
export declare function injectUser<T extends Record<string, unknown>>(input: T): Effect.Effect<T & {
|
|
89
|
-
userId: string;
|
|
90
|
-
}, UnauthorizedError, AuthUserService>;
|
|
91
|
-
/**
|
|
92
|
-
* Run a database operation wrapped in Effect.
|
|
93
|
-
*/
|
|
94
|
-
export declare function dbOperation<T>(operation: (db: unknown) => Promise<T>): Effect.Effect<T, Error, DatabaseService>;
|
|
95
|
-
/**
|
|
96
|
-
* Prepare validation data by transforming it before validation.
|
|
97
|
-
*/
|
|
98
|
-
export declare function prepareData<T extends Record<string, unknown>>(transform: (data: Record<string, unknown>) => T | Promise<T>): Effect.Effect<T, never, RequestService>;
|
|
99
|
-
/**
|
|
100
|
-
* Create an action with custom data preparation.
|
|
60
|
+
* yield* dbTransaction(async (tx) => {
|
|
61
|
+
* await tx.insert(users).values({ name: 'Alice' })
|
|
62
|
+
* await tx.update(accounts).set({ balance: 100 }).where(eq(accounts.userId, id))
|
|
63
|
+
* return { success: true }
|
|
64
|
+
* })
|
|
101
65
|
*/
|
|
102
|
-
export declare function
|
|
103
|
-
errorComponent?: string;
|
|
104
|
-
messages?: Record<string, string>;
|
|
105
|
-
attributes?: Record<string, string>;
|
|
106
|
-
}): Effect.Effect<Response | Redirect, E | ValidationError, R | RequestService>;
|
|
66
|
+
export declare function dbTransaction<T>(operations: (tx: unknown) => Promise<T>): Effect.Effect<T, Error, DatabaseService>;
|
|
107
67
|
//# sourceMappingURL=action.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../src/effect/action.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"action.d.ts","sourceRoot":"","sources":["../../src/effect/action.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAA;AACvC,OAAO,EACL,eAAe,EAEf,KAAK,QAAQ,EACd,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,CAAC,EACzB,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,GAAG,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,GAChD,MAAM,CAAC,MAAM,CAAC,QAAQ,GAAG,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,CAE1C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,CACvB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,GAClC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,iBAAiB,GAAG,cAAc,EAAE,KAAK,CAAC,CAoBpE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAC7B,UAAU,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GACtC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,CAQ1C"}
|
package/dist/effect/action.js
CHANGED
|
@@ -1,150 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Effect Action
|
|
2
|
+
* Effect Action Composables
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Composable helpers for building Effect-based request handlers.
|
|
5
|
+
* Actions are fully opt-in - yield* only what you need.
|
|
5
6
|
*/
|
|
6
|
-
import { Effect } from 'effect';
|
|
7
|
+
import { Effect, Option } from 'effect';
|
|
7
8
|
import { DatabaseService, AuthUserService, } from './services.js';
|
|
8
|
-
import {
|
|
9
|
+
import { UnauthorizedError, ForbiddenError } from './errors.js';
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
+
* Semantic wrapper for Effect actions.
|
|
12
|
+
*
|
|
13
|
+
* This is a minimal wrapper that marks an Effect as an action.
|
|
14
|
+
* All capabilities are opt-in via yield* inside your handler.
|
|
11
15
|
*
|
|
12
16
|
* @example
|
|
13
|
-
* const createProject =
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* const createProject = action(
|
|
18
|
+
* Effect.gen(function* () {
|
|
19
|
+
* // Opt-in to authorization
|
|
20
|
+
* const auth = yield* authorize()
|
|
21
|
+
*
|
|
22
|
+
* // Opt-in to validation
|
|
23
|
+
* const input = yield* validateRequest(S.Struct({ name: requiredString }))
|
|
24
|
+
*
|
|
25
|
+
* // Opt-in to database
|
|
16
26
|
* const db = yield* DatabaseService
|
|
17
|
-
* yield* Effect.tryPromise(() => db.insert(projects).values(input))
|
|
18
|
-
* return new Redirect({ url: '/projects', status: 303 })
|
|
19
|
-
* })
|
|
20
|
-
* )
|
|
21
|
-
*/
|
|
22
|
-
export function effectAction(schema, handler, options) {
|
|
23
|
-
return Effect.gen(function* () {
|
|
24
|
-
const input = yield* validateRequest(schema, {
|
|
25
|
-
errorComponent: options?.errorComponent,
|
|
26
|
-
messages: options?.messages,
|
|
27
|
-
attributes: options?.attributes,
|
|
28
|
-
});
|
|
29
|
-
return yield* handler(input);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Create an Effect action that requires authentication and database access.
|
|
34
27
|
*
|
|
35
|
-
* @example
|
|
36
|
-
* const createProject = dbAction(
|
|
37
|
-
* S.Struct({ name: requiredString }),
|
|
38
|
-
* (input, { db, user }) => Effect.gen(function* () {
|
|
39
28
|
* yield* Effect.tryPromise(() =>
|
|
40
|
-
* db.insert(projects).values({ ...input, userId:
|
|
29
|
+
* db.insert(projects).values({ ...input, userId: auth.user.id })
|
|
41
30
|
* )
|
|
42
31
|
* return new Redirect({ url: '/projects', status: 303 })
|
|
43
32
|
* })
|
|
44
33
|
* )
|
|
45
34
|
*/
|
|
46
|
-
export function
|
|
47
|
-
return
|
|
48
|
-
const db = yield* DatabaseService;
|
|
49
|
-
const user = yield* AuthUserService;
|
|
50
|
-
const input = yield* validateRequest(schema, {
|
|
51
|
-
errorComponent: options?.errorComponent,
|
|
52
|
-
messages: options?.messages,
|
|
53
|
-
attributes: options?.attributes,
|
|
54
|
-
});
|
|
55
|
-
return yield* handler(input, { db, user });
|
|
56
|
-
});
|
|
35
|
+
export function action(handler) {
|
|
36
|
+
return handler;
|
|
57
37
|
}
|
|
58
38
|
/**
|
|
59
|
-
*
|
|
39
|
+
* Authorization helper - opt-in to auth check.
|
|
60
40
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* return yield* Effect.tryPromise(() => honertia.render('Dashboard', { user: user.user }))
|
|
66
|
-
* }))
|
|
67
|
-
*/
|
|
68
|
-
export function authAction(handler) {
|
|
69
|
-
return Effect.gen(function* () {
|
|
70
|
-
const user = yield* AuthUserService;
|
|
71
|
-
return yield* handler(user);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Create a simple Effect action without validation.
|
|
41
|
+
* Returns the authenticated user if authorized.
|
|
42
|
+
* Fails with UnauthorizedError if no user is present.
|
|
43
|
+
* Fails with ForbiddenError if the check returns false.
|
|
44
|
+
* The check function is optional - if not provided, just requires authentication.
|
|
76
45
|
*
|
|
77
46
|
* @example
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* const user = yield* AuthUserService
|
|
81
|
-
* const projects = yield* Effect.tryPromise(() => db.query.projects.findMany())
|
|
82
|
-
* const honertia = yield* HonertiaService
|
|
83
|
-
* return yield* Effect.tryPromise(() => honertia.render('Projects', { projects }))
|
|
84
|
-
* }))
|
|
85
|
-
*/
|
|
86
|
-
export function simpleAction(handler) {
|
|
87
|
-
return handler();
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Inject additional data into the validated input.
|
|
47
|
+
* // Just require authentication
|
|
48
|
+
* const auth = yield* authorize()
|
|
91
49
|
*
|
|
92
|
-
*
|
|
93
|
-
* const
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* Effect.flatMap(({ name, userId }) =>
|
|
98
|
-
* Effect.tryPromise(() => db.insert(projects).values({ name, userId }))
|
|
99
|
-
* )
|
|
100
|
-
* )
|
|
101
|
-
* )
|
|
50
|
+
* // Require specific role (if your user type has a role field)
|
|
51
|
+
* const auth = yield* authorize((a) => a.user.role === 'admin')
|
|
52
|
+
*
|
|
53
|
+
* // Require resource ownership
|
|
54
|
+
* const auth = yield* authorize((a) => a.user.id === project.userId)
|
|
102
55
|
*/
|
|
103
|
-
export function
|
|
56
|
+
export function authorize(check) {
|
|
104
57
|
return Effect.gen(function* () {
|
|
105
|
-
const
|
|
106
|
-
|
|
58
|
+
const maybeUser = yield* Effect.serviceOption(AuthUserService);
|
|
59
|
+
if (Option.isNone(maybeUser)) {
|
|
60
|
+
return yield* Effect.fail(new UnauthorizedError({
|
|
61
|
+
message: 'Authentication required',
|
|
62
|
+
redirectTo: '/login',
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
const user = maybeUser.value;
|
|
66
|
+
if (check && !check(user)) {
|
|
67
|
+
return yield* Effect.fail(new ForbiddenError({ message: 'Not authorized' }));
|
|
68
|
+
}
|
|
69
|
+
return user;
|
|
107
70
|
});
|
|
108
71
|
}
|
|
109
72
|
/**
|
|
110
|
-
* Run
|
|
73
|
+
* Run multiple database operations in a transaction.
|
|
74
|
+
* Automatically rolls back on any failure.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* yield* dbTransaction(async (tx) => {
|
|
78
|
+
* await tx.insert(users).values({ name: 'Alice' })
|
|
79
|
+
* await tx.update(accounts).set({ balance: 100 }).where(eq(accounts.userId, id))
|
|
80
|
+
* return { success: true }
|
|
81
|
+
* })
|
|
111
82
|
*/
|
|
112
|
-
export function
|
|
83
|
+
export function dbTransaction(operations) {
|
|
113
84
|
return Effect.gen(function* () {
|
|
114
85
|
const db = yield* DatabaseService;
|
|
115
86
|
return yield* Effect.tryPromise({
|
|
116
|
-
try: () =>
|
|
87
|
+
try: () => db.transaction(operations),
|
|
117
88
|
catch: (error) => error instanceof Error ? error : new Error(String(error)),
|
|
118
89
|
});
|
|
119
90
|
});
|
|
120
91
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Prepare validation data by transforming it before validation.
|
|
123
|
-
*/
|
|
124
|
-
export function prepareData(transform) {
|
|
125
|
-
return Effect.gen(function* () {
|
|
126
|
-
const data = yield* getValidationData;
|
|
127
|
-
return yield* Effect.tryPromise({
|
|
128
|
-
try: () => Promise.resolve(transform(data)),
|
|
129
|
-
catch: () => new Error('Transform failed'),
|
|
130
|
-
}).pipe(Effect.catchAll(() => Effect.succeed(data)));
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Create an action with custom data preparation.
|
|
135
|
-
*/
|
|
136
|
-
export function preparedAction(schema, prepare, handler, options) {
|
|
137
|
-
return Effect.gen(function* () {
|
|
138
|
-
const rawData = yield* getValidationData;
|
|
139
|
-
const preparedData = yield* Effect.tryPromise({
|
|
140
|
-
try: () => Promise.resolve(prepare(rawData)),
|
|
141
|
-
catch: () => new Error('Prepare failed'),
|
|
142
|
-
}).pipe(Effect.catchAll(() => Effect.succeed(rawData)));
|
|
143
|
-
const input = yield* validate(schema, {
|
|
144
|
-
errorComponent: options?.errorComponent,
|
|
145
|
-
messages: options?.messages,
|
|
146
|
-
attributes: options?.attributes,
|
|
147
|
-
})(preparedData);
|
|
148
|
-
return yield* handler(input);
|
|
149
|
-
});
|
|
150
|
-
}
|
package/dist/effect/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export * from './schema.js';
|
|
|
9
9
|
export { getValidationData, formatSchemaErrors, validate, validateRequest, } from './validation.js';
|
|
10
10
|
export { effectBridge, buildContextLayer, getEffectRuntime, type EffectBridgeConfig, } from './bridge.js';
|
|
11
11
|
export { effectHandler, effect, handle, errorToResponse, } from './handler.js';
|
|
12
|
-
export {
|
|
12
|
+
export { action, authorize, dbTransaction, } from './action.js';
|
|
13
13
|
export { redirect, render, renderWithErrors, json, text, notFound, forbidden, httpError, prefersJson, jsonOrRender, share, } from './responses.js';
|
|
14
14
|
export { EffectRouteBuilder, effectRoutes, type EffectHandler, type BaseServices, } from './routing.js';
|
|
15
15
|
export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, betterAuthFormAction, betterAuthLogoutAction, loadUser, type AuthRoutesConfig, type BetterAuthFormActionConfig, type BetterAuthLogoutConfig, type BetterAuthActionResult, } from './auth.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/effect/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,eAAe,EACf,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAA;AAGtB,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,SAAS,EACT,QAAQ,EACR,KAAK,QAAQ,GACd,MAAM,aAAa,CAAA;AAGpB,cAAc,aAAa,CAAA;AAG3B,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,QAAQ,EACR,eAAe,GAChB,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAA;AAGpB,OAAO,EACL,aAAa,EACb,MAAM,EACN,MAAM,EACN,eAAe,GAChB,MAAM,cAAc,CAAA;AAGrB,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/effect/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,eAAe,EACf,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,KAAK,QAAQ,EACb,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,eAAe,CAAA;AAGtB,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,SAAS,EACT,QAAQ,EACR,KAAK,QAAQ,GACd,MAAM,aAAa,CAAA;AAGpB,cAAc,aAAa,CAAA;AAG3B,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,QAAQ,EACR,eAAe,GAChB,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,kBAAkB,GACxB,MAAM,aAAa,CAAA;AAGpB,OAAO,EACL,aAAa,EACb,MAAM,EACN,MAAM,EACN,eAAe,GAChB,MAAM,cAAc,CAAA;AAGrB,OAAO,EACL,MAAM,EACN,SAAS,EACT,aAAa,GACd,MAAM,aAAa,CAAA;AAGpB,OAAO,EACL,QAAQ,EACR,MAAM,EACN,gBAAgB,EAChB,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,SAAS,EACT,SAAS,EACT,WAAW,EACX,YAAY,EACZ,KAAK,GACN,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,cAAc,CAAA;AAGrB,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,WAAW,EACX,WAAW,EACX,YAAY,EACZ,SAAS,EACT,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,sBAAsB,EACtB,QAAQ,EACR,KAAK,gBAAgB,EACrB,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,GAC5B,MAAM,WAAW,CAAA"}
|
package/dist/effect/index.js
CHANGED
|
@@ -15,8 +15,8 @@ export { getValidationData, formatSchemaErrors, validate, validateRequest, } fro
|
|
|
15
15
|
export { effectBridge, buildContextLayer, getEffectRuntime, } from './bridge.js';
|
|
16
16
|
// Handler
|
|
17
17
|
export { effectHandler, effect, handle, errorToResponse, } from './handler.js';
|
|
18
|
-
// Action
|
|
19
|
-
export {
|
|
18
|
+
// Action Composables
|
|
19
|
+
export { action, authorize, dbTransaction, } from './action.js';
|
|
20
20
|
// Response Helpers
|
|
21
21
|
export { redirect, render, renderWithErrors, json, text, notFound, forbidden, httpError, prefersJson, jsonOrRender, share, } from './responses.js';
|
|
22
22
|
// Routing
|