honertia 0.1.11 → 0.1.13
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 +171 -1
- package/dist/effect/binding.d.ts +95 -0
- package/dist/effect/binding.d.ts.map +1 -0
- package/dist/effect/binding.js +169 -0
- package/dist/effect/bridge.d.ts +12 -0
- package/dist/effect/bridge.d.ts.map +1 -1
- package/dist/effect/index.d.ts +2 -1
- package/dist/effect/index.d.ts.map +1 -1
- package/dist/effect/index.js +2 -0
- package/dist/effect/routing.d.ts +44 -26
- package/dist/effect/routing.d.ts.map +1 -1
- package/dist/effect/routing.js +111 -32
- package/dist/effect/services.d.ts +3 -1
- package/dist/effect/services.d.ts.map +1 -1
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -907,6 +907,174 @@ effectRoutes(app).group((route) => {
|
|
|
907
907
|
})
|
|
908
908
|
```
|
|
909
909
|
|
|
910
|
+
#### Route Parameter Validation
|
|
911
|
+
|
|
912
|
+
You can pass a `params` schema to validate route parameters before your handler runs. Invalid values automatically return a 404:
|
|
913
|
+
|
|
914
|
+
```typescript
|
|
915
|
+
import { Schema as S } from 'effect'
|
|
916
|
+
import { uuid } from 'honertia/effect'
|
|
917
|
+
|
|
918
|
+
effectRoutes(app).get(
|
|
919
|
+
'/projects/:id',
|
|
920
|
+
showProject,
|
|
921
|
+
{ params: S.Struct({ id: uuid }) }
|
|
922
|
+
)
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
This runs validation *before* the handler executes, so invalid UUIDs never hit your database. You can validate multiple params and use any Effect Schema:
|
|
926
|
+
|
|
927
|
+
```typescript
|
|
928
|
+
effectRoutes(app).get(
|
|
929
|
+
'/api/:version/projects/:id',
|
|
930
|
+
showProject,
|
|
931
|
+
{
|
|
932
|
+
params: S.Struct({
|
|
933
|
+
version: S.Literal('v1', 'v2'),
|
|
934
|
+
id: uuid,
|
|
935
|
+
}),
|
|
936
|
+
}
|
|
937
|
+
)
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
#### Laravel-Style Route Model Binding
|
|
941
|
+
|
|
942
|
+
Honertia supports Laravel-style route model binding with the `{param}` syntax. This automatically resolves route parameters to database models, returning 404 if the model isn't found.
|
|
943
|
+
|
|
944
|
+
**Setup:**
|
|
945
|
+
|
|
946
|
+
1. Add your Drizzle schema to the module augmentation:
|
|
947
|
+
|
|
948
|
+
```typescript
|
|
949
|
+
// src/types.d.ts
|
|
950
|
+
import type { Database } from '~/db/db'
|
|
951
|
+
import type { auth } from '~/lib/auth'
|
|
952
|
+
import * as schema from '~/db/schema'
|
|
953
|
+
|
|
954
|
+
declare module 'honertia/effect' {
|
|
955
|
+
interface HonertiaDatabaseType {
|
|
956
|
+
type: Database
|
|
957
|
+
schema: typeof schema // Add this for route model binding
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
interface HonertiaAuthType {
|
|
961
|
+
type: typeof auth
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
2. Pass your schema to `effectRoutes`:
|
|
967
|
+
|
|
968
|
+
```typescript
|
|
969
|
+
import * as schema from '~/db/schema'
|
|
970
|
+
|
|
971
|
+
effectRoutes(app, { schema })
|
|
972
|
+
.get('/projects/{project}', showProject)
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
**Basic Usage:**
|
|
976
|
+
|
|
977
|
+
```typescript
|
|
978
|
+
import { bound } from 'honertia/effect'
|
|
979
|
+
|
|
980
|
+
// Route: /projects/{project}
|
|
981
|
+
// Automatically queries: SELECT * FROM projects WHERE id = :project
|
|
982
|
+
|
|
983
|
+
effectRoutes(app, { schema }).get('/projects/{project}', showProject)
|
|
984
|
+
|
|
985
|
+
const showProject = Effect.gen(function* () {
|
|
986
|
+
const project = yield* bound('project') // Already fetched, guaranteed to exist
|
|
987
|
+
return yield* render('Projects/Show', { project })
|
|
988
|
+
})
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
**Custom Column Binding:**
|
|
992
|
+
|
|
993
|
+
By default, bindings query the `id` column. Use `{param:column}` syntax to bind by a different column:
|
|
994
|
+
|
|
995
|
+
```typescript
|
|
996
|
+
// Bind by slug instead of id
|
|
997
|
+
effectRoutes(app, { schema }).get('/projects/{project:slug}', showProject)
|
|
998
|
+
// Queries: SELECT * FROM projects WHERE slug = :project
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
**Nested Route Scoping:**
|
|
1002
|
+
|
|
1003
|
+
For nested routes, Honertia automatically scopes child models to their parents using Drizzle relations:
|
|
1004
|
+
|
|
1005
|
+
```typescript
|
|
1006
|
+
// Route: /users/{user}/posts/{post}
|
|
1007
|
+
effectRoutes(app, { schema }).get('/users/{user}/posts/{post}', showUserPost)
|
|
1008
|
+
|
|
1009
|
+
// Queries:
|
|
1010
|
+
// 1. SELECT * FROM users WHERE id = :user
|
|
1011
|
+
// 2. SELECT * FROM posts WHERE id = :post AND userId = :user.id
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
This uses your Drizzle relations to discover the foreign key:
|
|
1015
|
+
|
|
1016
|
+
```typescript
|
|
1017
|
+
// db/schema.ts
|
|
1018
|
+
export const postsRelations = relations(posts, ({ one }) => ({
|
|
1019
|
+
user: one(users, {
|
|
1020
|
+
fields: [posts.userId],
|
|
1021
|
+
references: [users.id],
|
|
1022
|
+
}),
|
|
1023
|
+
}))
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
If no relation is found, the child is resolved without scoping (useful for unrelated resources in the same route).
|
|
1027
|
+
|
|
1028
|
+
**How Binding Works:**
|
|
1029
|
+
|
|
1030
|
+
1. `{project}` is converted to `:project` for Hono's router
|
|
1031
|
+
2. At request time, the param value is extracted
|
|
1032
|
+
3. The table name is derived by pluralizing the param (`project` → `projects`)
|
|
1033
|
+
4. A database query is executed against that table
|
|
1034
|
+
5. If not found, a 404 is returned before your handler runs
|
|
1035
|
+
6. If found, the model is available via `bound('project')`
|
|
1036
|
+
|
|
1037
|
+
**Combining with Param Validation:**
|
|
1038
|
+
|
|
1039
|
+
Route model binding and param validation work together. Validation runs first:
|
|
1040
|
+
|
|
1041
|
+
```typescript
|
|
1042
|
+
effectRoutes(app, { schema }).get(
|
|
1043
|
+
'/projects/{project}',
|
|
1044
|
+
showProject,
|
|
1045
|
+
{ params: S.Struct({ project: uuid }) } // Validates UUID format first
|
|
1046
|
+
)
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
Order of execution:
|
|
1050
|
+
1. Param validation (returns 404 if schema fails)
|
|
1051
|
+
2. Model binding (returns 404 if not found in database)
|
|
1052
|
+
3. Your handler (model guaranteed to exist)
|
|
1053
|
+
|
|
1054
|
+
**Mixed Notation:**
|
|
1055
|
+
|
|
1056
|
+
You can mix Laravel-style `{binding}` with Hono-style `:param` in the same route. Only `{binding}` params are resolved from the database:
|
|
1057
|
+
|
|
1058
|
+
```typescript
|
|
1059
|
+
// :version is a regular Hono param (not bound)
|
|
1060
|
+
// {project} is resolved from the database
|
|
1061
|
+
effectRoutes(app, { schema }).get(
|
|
1062
|
+
'/api/:version/projects/{project}',
|
|
1063
|
+
showProject
|
|
1064
|
+
)
|
|
1065
|
+
|
|
1066
|
+
const showProject = Effect.gen(function* () {
|
|
1067
|
+
const request = yield* RequestService
|
|
1068
|
+
const version = request.param('version') // 'v1', 'v2', etc.
|
|
1069
|
+
const project = yield* bound('project') // Database model
|
|
1070
|
+
// ...
|
|
1071
|
+
})
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
**Performance:**
|
|
1075
|
+
|
|
1076
|
+
Routes without `{bindings}` have zero overhead—binding resolution only runs when Laravel-style params are detected. The binding check is a simple regex test at route registration time.
|
|
1077
|
+
|
|
910
1078
|
## Validation
|
|
911
1079
|
|
|
912
1080
|
Honertia uses Effect Schema with Laravel-inspired validators:
|
|
@@ -1354,10 +1522,12 @@ By default, `DatabaseService` and `AuthService` are typed as `unknown` since Hon
|
|
|
1354
1522
|
// src/types.d.ts (or any .d.ts file in your project)
|
|
1355
1523
|
import type { Database } from '~/db/db'
|
|
1356
1524
|
import type { auth } from '~/lib/auth'
|
|
1525
|
+
import * as schema from '~/db/schema'
|
|
1357
1526
|
|
|
1358
1527
|
declare module 'honertia/effect' {
|
|
1359
1528
|
interface HonertiaDatabaseType {
|
|
1360
|
-
type: Database
|
|
1529
|
+
type: Database // Your Drizzle/Prisma/Kysely type
|
|
1530
|
+
schema: typeof schema // Your Drizzle schema (for route model binding)
|
|
1361
1531
|
}
|
|
1362
1532
|
|
|
1363
1533
|
interface HonertiaAuthType {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route Model Binding
|
|
3
|
+
*
|
|
4
|
+
* Laravel-style route model binding for Effect routes.
|
|
5
|
+
* Automatically resolves route parameters to database models.
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Effect } from 'effect';
|
|
8
|
+
import type { Table } from 'drizzle-orm';
|
|
9
|
+
import type { HonertiaDatabaseType } from './services.js';
|
|
10
|
+
declare const BoundModelNotFound_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => import("effect/Cause").YieldableError & {
|
|
11
|
+
readonly _tag: "BoundModelNotFound";
|
|
12
|
+
} & Readonly<A>;
|
|
13
|
+
/**
|
|
14
|
+
* Error thrown when a bound model is not found in the BoundModels context.
|
|
15
|
+
* This indicates a programming error - the binding key doesn't match any resolved model.
|
|
16
|
+
*/
|
|
17
|
+
export declare class BoundModelNotFound extends BoundModelNotFound_base<{
|
|
18
|
+
readonly key: string;
|
|
19
|
+
}> {
|
|
20
|
+
get message(): string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parsed binding from route path.
|
|
24
|
+
*/
|
|
25
|
+
export interface ParsedBinding {
|
|
26
|
+
/** The parameter name (e.g., 'project' from '{project}') */
|
|
27
|
+
param: string;
|
|
28
|
+
/** The column to query (e.g., 'id' or 'slug' from '{project:slug}') */
|
|
29
|
+
column: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse Laravel-style bindings from a route path.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* parseBindings('/users/{user}/posts/{post:slug}')
|
|
36
|
+
* // => [{ param: 'user', column: 'id' }, { param: 'post', column: 'slug' }]
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseBindings(path: string): ParsedBinding[];
|
|
39
|
+
/**
|
|
40
|
+
* Convert Laravel-style route to Hono-style route.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* toHonoPath('/users/{user}/posts/{post:slug}')
|
|
44
|
+
* // => '/users/:user/posts/:post'
|
|
45
|
+
*/
|
|
46
|
+
export declare function toHonoPath(path: string): string;
|
|
47
|
+
declare const BoundModels_base: Context.TagClass<BoundModels, "honertia/BoundModels", ReadonlyMap<string, unknown>>;
|
|
48
|
+
/**
|
|
49
|
+
* Service tag for bound models.
|
|
50
|
+
* Provides access to resolved route models in handlers.
|
|
51
|
+
*/
|
|
52
|
+
export declare class BoundModels extends BoundModels_base {
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Type-safe accessor for bound models.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* const showProject = Effect.gen(function* () {
|
|
59
|
+
* const project = yield* bound('project')
|
|
60
|
+
* return inertia('Projects/Show', { project })
|
|
61
|
+
* })
|
|
62
|
+
*/
|
|
63
|
+
export declare const bound: <K extends string>(key: K) => Effect.Effect<K extends keyof HonertiaDatabaseType["schema"] ? HonertiaDatabaseType["schema"][K] extends Table ? HonertiaDatabaseType["schema"][K]["$inferSelect"] : unknown : unknown, BoundModelNotFound, BoundModels>;
|
|
64
|
+
/**
|
|
65
|
+
* Pluralize a singular word.
|
|
66
|
+
* Handles common English pluralization rules.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* pluralize('user') // 'users'
|
|
70
|
+
* pluralize('category') // 'categories'
|
|
71
|
+
* pluralize('box') // 'boxes'
|
|
72
|
+
* pluralize('class') // 'classes'
|
|
73
|
+
*/
|
|
74
|
+
export declare function pluralize(word: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* Information about a relation between tables.
|
|
77
|
+
*/
|
|
78
|
+
export interface RelationInfo {
|
|
79
|
+
/** Foreign key column on the child table (e.g., 'userId') */
|
|
80
|
+
foreignKey: string;
|
|
81
|
+
/** Referenced column on the parent table (e.g., 'id') */
|
|
82
|
+
references: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Find a relation from child table to parent table.
|
|
86
|
+
* Uses Drizzle's relations metadata to discover foreign keys.
|
|
87
|
+
*
|
|
88
|
+
* @param schema - The Drizzle schema object
|
|
89
|
+
* @param childTableName - Name of the child table (e.g., 'posts')
|
|
90
|
+
* @param parentTableName - Name of the parent table (e.g., 'users')
|
|
91
|
+
* @returns Relation info or null if no relation found
|
|
92
|
+
*/
|
|
93
|
+
export declare function findRelation(schema: Record<string, unknown>, childTableName: string, parentTableName: string): RelationInfo | null;
|
|
94
|
+
export {};
|
|
95
|
+
//# sourceMappingURL=binding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/effect/binding.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAQ,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC9C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA;;;;AAEzD;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC7E,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB,CAAC;IACA,IAAI,OAAO,WAEV;CACF;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAA;IACb,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,EAAE,CAa3D;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;;AAED;;;GAGG;AACH,qBAAa,WAAY,SAAQ,gBAG9B;CAAG;AAEN;;;;;;;;GAQG;AACH,eAAO,MAAM,KAAK,GAAI,CAAC,SAAS,MAAM,EACpC,KAAK,CAAC,KACL,MAAM,CAAC,MAAM,CACd,CAAC,SAAS,MAAM,oBAAoB,CAAC,QAAQ,CAAC,GAC1C,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,GAC7C,oBAAoB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,GACjD,OAAO,GACT,OAAO,EACX,kBAAkB,EAClB,WAAW,CAST,CAAA;AAEJ;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS9C;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAA;IAClB,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,GACtB,YAAY,GAAG,IAAI,CA2CrB"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route Model Binding
|
|
3
|
+
*
|
|
4
|
+
* Laravel-style route model binding for Effect routes.
|
|
5
|
+
* Automatically resolves route parameters to database models.
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Data, Effect } from 'effect';
|
|
8
|
+
/**
|
|
9
|
+
* Error thrown when a bound model is not found in the BoundModels context.
|
|
10
|
+
* This indicates a programming error - the binding key doesn't match any resolved model.
|
|
11
|
+
*/
|
|
12
|
+
export class BoundModelNotFound extends Data.TaggedError('BoundModelNotFound') {
|
|
13
|
+
get message() {
|
|
14
|
+
return `No bound model found for key: '${this.key}'. Ensure the route uses {${this.key}} binding syntax.`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse Laravel-style bindings from a route path.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* parseBindings('/users/{user}/posts/{post:slug}')
|
|
22
|
+
* // => [{ param: 'user', column: 'id' }, { param: 'post', column: 'slug' }]
|
|
23
|
+
*/
|
|
24
|
+
export function parseBindings(path) {
|
|
25
|
+
const regex = /\{(\w+)(?::(\w+))?\}/g;
|
|
26
|
+
const bindings = [];
|
|
27
|
+
let match;
|
|
28
|
+
while ((match = regex.exec(path)) !== null) {
|
|
29
|
+
bindings.push({
|
|
30
|
+
param: match[1],
|
|
31
|
+
column: match[2] ?? 'id',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return bindings;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Convert Laravel-style route to Hono-style route.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* toHonoPath('/users/{user}/posts/{post:slug}')
|
|
41
|
+
* // => '/users/:user/posts/:post'
|
|
42
|
+
*/
|
|
43
|
+
export function toHonoPath(path) {
|
|
44
|
+
return path.replace(/\{(\w+)(?::\w+)?\}/g, ':$1');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Service tag for bound models.
|
|
48
|
+
* Provides access to resolved route models in handlers.
|
|
49
|
+
*/
|
|
50
|
+
export class BoundModels extends Context.Tag('honertia/BoundModels')() {
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Type-safe accessor for bound models.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* const showProject = Effect.gen(function* () {
|
|
57
|
+
* const project = yield* bound('project')
|
|
58
|
+
* return inertia('Projects/Show', { project })
|
|
59
|
+
* })
|
|
60
|
+
*/
|
|
61
|
+
export const bound = (key) => Effect.gen(function* () {
|
|
62
|
+
const models = yield* BoundModels;
|
|
63
|
+
const model = models.get(key);
|
|
64
|
+
if (!model) {
|
|
65
|
+
return yield* Effect.fail(new BoundModelNotFound({ key }));
|
|
66
|
+
}
|
|
67
|
+
return model;
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* Pluralize a singular word.
|
|
71
|
+
* Handles common English pluralization rules.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* pluralize('user') // 'users'
|
|
75
|
+
* pluralize('category') // 'categories'
|
|
76
|
+
* pluralize('box') // 'boxes'
|
|
77
|
+
* pluralize('class') // 'classes'
|
|
78
|
+
*/
|
|
79
|
+
export function pluralize(word) {
|
|
80
|
+
// Words ending in vowel + y: just add 's' (day -> days)
|
|
81
|
+
if (/[aeiou]y$/i.test(word))
|
|
82
|
+
return word + 's';
|
|
83
|
+
// Words ending in consonant + y: replace y with ies (category -> categories)
|
|
84
|
+
if (/y$/i.test(word))
|
|
85
|
+
return word.slice(0, -1) + 'ies';
|
|
86
|
+
// Words ending in s, x, z, ch, sh: add 'es' (box -> boxes, class -> classes)
|
|
87
|
+
if (/(?:s|x|z|ch|sh)$/i.test(word))
|
|
88
|
+
return word + 'es';
|
|
89
|
+
// Default: add 's'
|
|
90
|
+
return word + 's';
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Find a relation from child table to parent table.
|
|
94
|
+
* Uses Drizzle's relations metadata to discover foreign keys.
|
|
95
|
+
*
|
|
96
|
+
* @param schema - The Drizzle schema object
|
|
97
|
+
* @param childTableName - Name of the child table (e.g., 'posts')
|
|
98
|
+
* @param parentTableName - Name of the parent table (e.g., 'users')
|
|
99
|
+
* @returns Relation info or null if no relation found
|
|
100
|
+
*/
|
|
101
|
+
export function findRelation(schema, childTableName, parentTableName) {
|
|
102
|
+
// Look for relations definition (e.g., postsRelations)
|
|
103
|
+
const relationsKey = `${childTableName}Relations`;
|
|
104
|
+
const relations = schema[relationsKey];
|
|
105
|
+
if (!relations || typeof relations !== 'object') {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// Drizzle stores relations config - we need to inspect it
|
|
109
|
+
// The relations object has a config property with the relation definitions
|
|
110
|
+
const config = relations.config;
|
|
111
|
+
if (!config || typeof config !== 'function') {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
// Try to extract relation info by calling the config
|
|
115
|
+
// This is a bit hacky but necessary to introspect Drizzle relations
|
|
116
|
+
try {
|
|
117
|
+
const relationDefs = config({
|
|
118
|
+
one: (table, opts) => ({ type: 'one', table, ...opts }),
|
|
119
|
+
many: (table, opts) => ({ type: 'many', table, ...opts }),
|
|
120
|
+
});
|
|
121
|
+
for (const [_name, rel] of Object.entries(relationDefs)) {
|
|
122
|
+
const relation = rel;
|
|
123
|
+
if (relation.type !== 'one')
|
|
124
|
+
continue;
|
|
125
|
+
// Check if this relation points to the parent table
|
|
126
|
+
const relatedTableName = getTableName(relation.table);
|
|
127
|
+
if (relatedTableName === parentTableName && relation.fields && relation.references) {
|
|
128
|
+
return {
|
|
129
|
+
foreignKey: getColumnName(relation.fields[0]),
|
|
130
|
+
references: getColumnName(relation.references[0]),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// If introspection fails, fall back to convention
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Get table name from a Drizzle table object.
|
|
142
|
+
*/
|
|
143
|
+
function getTableName(table) {
|
|
144
|
+
if (table && typeof table === 'object') {
|
|
145
|
+
// Drizzle tables have a Symbol for the table name
|
|
146
|
+
const symbols = Object.getOwnPropertySymbols(table);
|
|
147
|
+
for (const sym of symbols) {
|
|
148
|
+
if (sym.description === 'drizzle:Name') {
|
|
149
|
+
return table[sym];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Fallback: check for _ property
|
|
153
|
+
if ('_' in table && typeof table._ === 'object') {
|
|
154
|
+
return table._.name;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get column name from a Drizzle column object.
|
|
161
|
+
*/
|
|
162
|
+
function getColumnName(column) {
|
|
163
|
+
if (column && typeof column === 'object') {
|
|
164
|
+
if ('name' in column) {
|
|
165
|
+
return column.name;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return '';
|
|
169
|
+
}
|
package/dist/effect/bridge.d.ts
CHANGED
|
@@ -35,6 +35,18 @@ export interface EffectBridgeConfig<E extends Env, CustomServices = never> {
|
|
|
35
35
|
* Return a Layer that provides your custom services.
|
|
36
36
|
*/
|
|
37
37
|
services?: (c: HonoContext<E>) => Layer.Layer<CustomServices, never, never>;
|
|
38
|
+
/**
|
|
39
|
+
* Drizzle schema for route model binding.
|
|
40
|
+
* Required if using Laravel-style route model binding.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import * as schema from '~/db/schema'
|
|
45
|
+
*
|
|
46
|
+
* effectRoutes(app, { schema })
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
schema?: Record<string, unknown>;
|
|
38
50
|
}
|
|
39
51
|
/**
|
|
40
52
|
* Symbol for storing Effect runtime in Hono context.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/effect/bridge.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAC9C,OAAO,KAAK,EAAE,OAAO,IAAI,WAAW,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AAC1E,OAAO,EACL,eAAe,EACf,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,EAKvB,MAAM,eAAe,CAAA;AAEtB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,GAAG,KAAK;IACvE,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,OAAO,CAAA;IACzC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/effect/bridge.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAC9C,OAAO,KAAK,EAAE,OAAO,IAAI,WAAW,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AAC1E,OAAO,EACL,eAAe,EACf,WAAW,EACX,eAAe,EACf,eAAe,EACf,cAAc,EACd,sBAAsB,EAKvB,MAAM,eAAe,CAAA;AAEtB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,GAAG,KAAK;IACvE,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,OAAO,CAAA;IACzC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IAC3E;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACjC;AAED;;GAEG;AACH,QAAA,MAAM,cAAc,eAA0B,CAAA;AAE9C;;GAEG;AACH,OAAO,QAAQ,MAAM,CAAC;IACpB,UAAU,kBAAkB;QAC1B,CAAC,cAAc,CAAC,CAAC,EAAE,cAAc,CAAC,cAAc,CAC5C,eAAe,GACf,WAAW,GACX,eAAe,GACf,eAAe,GACf,cAAc,GACd,sBAAsB,EACxB,KAAK,CACN,CAAA;KACF;CACF;AAqDD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,GAAG,KAAK,EACrE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,EACjB,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,EAAE,cAAc,CAAC,GAC7C,KAAK,CAAC,KAAK,CACV,cAAc,GACd,sBAAsB,GACtB,eAAe,GACf,eAAe,GACf,WAAW,GACX,eAAe,GACf,cAAc,EAChB,KAAK,EACL,KAAK,CACN,CA0CA;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,GAAG,EAC5C,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAChB,cAAc,CAAC,cAAc,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,SAAS,CAEvD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,GAAG,EAAE,cAAc,GAAG,KAAK,EAChE,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,EAAE,cAAc,CAAC,GAC7C,iBAAiB,CAAC,CAAC,CAAC,CAetB"}
|
package/dist/effect/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { effectBridge, buildContextLayer, getEffectRuntime, type EffectBridgeCon
|
|
|
11
11
|
export { effectHandler, effect, handle, errorToResponse, } from './handler.js';
|
|
12
12
|
export { action, authorize, dbMutation, dbTransaction, type SafeTx, } from './action.js';
|
|
13
13
|
export { redirect, render, renderWithErrors, json, text, notFound, forbidden, httpError, prefersJson, jsonOrRender, share, } from './responses.js';
|
|
14
|
-
export { EffectRouteBuilder, effectRoutes, type EffectHandler, type BaseServices, } from './routing.js';
|
|
14
|
+
export { EffectRouteBuilder, effectRoutes, type EffectHandler, type BaseServices, type EffectRouteOptions, } from './routing.js';
|
|
15
|
+
export { BoundModels, BoundModelNotFound, bound, parseBindings, toHonoPath, type ParsedBinding, } from './binding.js';
|
|
15
16
|
export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, betterAuthFormAction, betterAuthLogoutAction, loadUser, type AuthRoutesConfig, type BetterAuthFormActionConfig, type BetterAuthLogoutConfig, type BetterAuthActionResult, } from './auth.js';
|
|
16
17
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -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,EACpB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,GACtB,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,EACf,WAAW,EACX,SAAS,EACT,KAAK,SAAS,EACd,KAAK,OAAO,GACb,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,UAAU,EACV,aAAa,EACb,KAAK,MAAM,GACZ,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,
|
|
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,EACpB,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,GACtB,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,EACf,WAAW,EACX,SAAS,EACT,KAAK,SAAS,EACd,KAAK,OAAO,GACb,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,UAAU,EACV,aAAa,EACb,KAAK,MAAM,GACZ,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,EACjB,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAA;AAGrB,OAAO,EACL,WAAW,EACX,kBAAkB,EAClB,KAAK,EACL,aAAa,EACb,UAAU,EACV,KAAK,aAAa,GACnB,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
|
@@ -21,5 +21,7 @@ export { action, authorize, dbMutation, dbTransaction, } from './action.js';
|
|
|
21
21
|
export { redirect, render, renderWithErrors, json, text, notFound, forbidden, httpError, prefersJson, jsonOrRender, share, } from './responses.js';
|
|
22
22
|
// Routing
|
|
23
23
|
export { EffectRouteBuilder, effectRoutes, } from './routing.js';
|
|
24
|
+
// Route Model Binding
|
|
25
|
+
export { BoundModels, BoundModelNotFound, bound, parseBindings, toHonoPath, } from './binding.js';
|
|
24
26
|
// Auth
|
|
25
27
|
export { RequireAuthLayer, RequireGuestLayer, isAuthenticated, currentUser, requireAuth, requireGuest, shareAuth, shareAuthMiddleware, effectAuthRoutes, betterAuthFormAction, betterAuthLogoutAction, loadUser, } from './auth.js';
|
package/dist/effect/routing.d.ts
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Laravel-style routing with Effect handlers.
|
|
5
5
|
*/
|
|
6
|
-
import { Effect, Layer } from 'effect';
|
|
6
|
+
import { Effect, Layer, Schema as S } from 'effect';
|
|
7
7
|
import type { Hono, Env } from 'hono';
|
|
8
8
|
import { type EffectBridgeConfig } from './bridge.js';
|
|
9
|
-
import type
|
|
10
|
-
import { Redirect } from './errors.js';
|
|
9
|
+
import { type AppError, Redirect } from './errors.js';
|
|
11
10
|
import { DatabaseService, AuthService, HonertiaService, RequestService, ResponseFactoryService } from './services.js';
|
|
11
|
+
import { BoundModels } from './binding.js';
|
|
12
12
|
/**
|
|
13
13
|
* Type for Effect-based route handlers.
|
|
14
14
|
* Error type includes Error for compatibility with Effect.tryPromise.
|
|
@@ -17,7 +17,26 @@ export type EffectHandler<R = never, E extends AppError | Error = AppError | Err
|
|
|
17
17
|
/**
|
|
18
18
|
* Base services available in every route.
|
|
19
19
|
*/
|
|
20
|
-
export type BaseServices = RequestService | ResponseFactoryService | HonertiaService | DatabaseService | AuthService;
|
|
20
|
+
export type BaseServices = RequestService | ResponseFactoryService | HonertiaService | DatabaseService | AuthService | BoundModels;
|
|
21
|
+
/**
|
|
22
|
+
* Route-level configuration options.
|
|
23
|
+
*/
|
|
24
|
+
export interface EffectRouteOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Validate route params with the provided schema.
|
|
27
|
+
* Invalid values will return a 404 before the handler runs.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* effectRoutes(app).get(
|
|
32
|
+
* '/projects/{project}',
|
|
33
|
+
* showProject,
|
|
34
|
+
* { params: S.Struct({ project: uuid }) }
|
|
35
|
+
* )
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
params?: S.Schema.Any;
|
|
39
|
+
}
|
|
21
40
|
/**
|
|
22
41
|
* Effect Route Builder with layer composition.
|
|
23
42
|
*/
|
|
@@ -45,33 +64,32 @@ export declare class EffectRouteBuilder<E extends Env, ProvidedServices = never,
|
|
|
45
64
|
*/
|
|
46
65
|
private resolvePath;
|
|
47
66
|
/**
|
|
48
|
-
*
|
|
49
|
-
*/
|
|
50
|
-
private createHandler;
|
|
51
|
-
/**
|
|
52
|
-
* Register a GET route.
|
|
67
|
+
* Validate route params against a schema.
|
|
53
68
|
*/
|
|
54
|
-
|
|
69
|
+
private ensureParams;
|
|
55
70
|
/**
|
|
56
|
-
*
|
|
71
|
+
* Resolve route model bindings from the database.
|
|
72
|
+
* Returns a Map of binding names to resolved models, or a 404 Response if any binding fails.
|
|
57
73
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* Register a PUT route.
|
|
61
|
-
*/
|
|
62
|
-
put<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
|
|
63
|
-
/**
|
|
64
|
-
* Register a PATCH route.
|
|
65
|
-
*/
|
|
66
|
-
patch<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
|
|
67
|
-
/**
|
|
68
|
-
* Register a DELETE route.
|
|
69
|
-
*/
|
|
70
|
-
delete<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>): void;
|
|
74
|
+
private resolveBindings;
|
|
75
|
+
private createHandler;
|
|
71
76
|
/**
|
|
72
|
-
* Register a route
|
|
77
|
+
* Register a route with the given HTTP method.
|
|
78
|
+
* Parses Laravel-style bindings and converts to Hono path format.
|
|
73
79
|
*/
|
|
74
|
-
|
|
80
|
+
private registerRoute;
|
|
81
|
+
/** Register a GET route. Supports Laravel-style route model binding: /projects/{project} */
|
|
82
|
+
get<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>, options?: EffectRouteOptions): void;
|
|
83
|
+
/** Register a POST route. Supports Laravel-style route model binding: /projects/{project} */
|
|
84
|
+
post<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>, options?: EffectRouteOptions): void;
|
|
85
|
+
/** Register a PUT route. Supports Laravel-style route model binding: /projects/{project} */
|
|
86
|
+
put<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>, options?: EffectRouteOptions): void;
|
|
87
|
+
/** Register a PATCH route. Supports Laravel-style route model binding: /projects/{project} */
|
|
88
|
+
patch<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>, options?: EffectRouteOptions): void;
|
|
89
|
+
/** Register a DELETE route. Supports Laravel-style route model binding: /projects/{project} */
|
|
90
|
+
delete<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>, options?: EffectRouteOptions): void;
|
|
91
|
+
/** Register a route for all HTTP methods. Supports Laravel-style route model binding: /projects/{project} */
|
|
92
|
+
all<R extends BaseServices | ProvidedServices | CustomServices>(path: string, effect: EffectHandler<R, AppError | Error>, options?: EffectRouteOptions): void;
|
|
75
93
|
}
|
|
76
94
|
/**
|
|
77
95
|
* Create an Effect route builder for an app.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/effect/routing.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../../src/effect/routing.ts"],"names":[],"mappings":"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"}
|
package/dist/effect/routing.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Laravel-style routing with Effect handlers.
|
|
5
5
|
*/
|
|
6
|
-
import { Effect, Layer } from 'effect';
|
|
6
|
+
import { Effect, Exit, Layer, Schema as S } from 'effect';
|
|
7
7
|
import { effectHandler } from './handler.js';
|
|
8
8
|
import { buildContextLayer } from './bridge.js';
|
|
9
|
+
import { parseBindings, toHonoPath, pluralize, findRelation, BoundModels, } from './binding.js';
|
|
9
10
|
/**
|
|
10
11
|
* Effect Route Builder with layer composition.
|
|
11
12
|
*/
|
|
@@ -50,16 +51,97 @@ export class EffectRouteBuilder {
|
|
|
50
51
|
return path;
|
|
51
52
|
}
|
|
52
53
|
/**
|
|
53
|
-
*
|
|
54
|
+
* Validate route params against a schema.
|
|
54
55
|
*/
|
|
55
|
-
|
|
56
|
+
async ensureParams(c, schema) {
|
|
57
|
+
if (!schema)
|
|
58
|
+
return null;
|
|
59
|
+
const rawParams = c.req.param();
|
|
60
|
+
const params = typeof rawParams === 'string' ? {} : rawParams;
|
|
61
|
+
const decode = S.decodeUnknown(schema);
|
|
62
|
+
const exit = await Effect.runPromiseExit(decode(params));
|
|
63
|
+
if (Exit.isFailure(exit)) {
|
|
64
|
+
return c.notFound();
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve route model bindings from the database.
|
|
70
|
+
* Returns a Map of binding names to resolved models, or a 404 Response if any binding fails.
|
|
71
|
+
*/
|
|
72
|
+
async resolveBindings(c, bindings, db, schema) {
|
|
73
|
+
if (bindings.length === 0) {
|
|
74
|
+
return new Map();
|
|
75
|
+
}
|
|
76
|
+
// Dynamic import to avoid requiring drizzle-orm for non-binding users
|
|
77
|
+
const { eq } = await import('drizzle-orm');
|
|
78
|
+
const models = new Map();
|
|
79
|
+
let parent = null;
|
|
80
|
+
for (const binding of bindings) {
|
|
81
|
+
const tableName = pluralize(binding.param);
|
|
82
|
+
const table = schema[tableName];
|
|
83
|
+
if (!table) {
|
|
84
|
+
return c.notFound();
|
|
85
|
+
}
|
|
86
|
+
const paramValue = c.req.param(binding.param);
|
|
87
|
+
if (!paramValue) {
|
|
88
|
+
return c.notFound();
|
|
89
|
+
}
|
|
90
|
+
// Build query with primary lookup
|
|
91
|
+
const column = table[binding.column];
|
|
92
|
+
if (!column) {
|
|
93
|
+
return c.notFound();
|
|
94
|
+
}
|
|
95
|
+
const dbClient = db;
|
|
96
|
+
let query = dbClient.select().from(table).where(eq(column, paramValue));
|
|
97
|
+
// If we have a parent, try to scope the query
|
|
98
|
+
if (parent) {
|
|
99
|
+
const relation = findRelation(schema, tableName, parent.tableName);
|
|
100
|
+
if (relation) {
|
|
101
|
+
const foreignKeyColumn = table[relation.foreignKey];
|
|
102
|
+
if (foreignKeyColumn && parent.model[relation.references]) {
|
|
103
|
+
query = query.where(eq(foreignKeyColumn, parent.model[relation.references]));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Execute the query
|
|
108
|
+
const result = await query.get();
|
|
109
|
+
if (!result) {
|
|
110
|
+
return c.notFound();
|
|
111
|
+
}
|
|
112
|
+
models.set(binding.param, result);
|
|
113
|
+
parent = { tableName, model: result };
|
|
114
|
+
}
|
|
115
|
+
return models;
|
|
116
|
+
}
|
|
117
|
+
createHandler(effect, bindings, options) {
|
|
56
118
|
const layers = this.layers;
|
|
57
119
|
const bridgeConfig = this.bridgeConfig;
|
|
58
120
|
return async (c) => {
|
|
121
|
+
const validation = await this.ensureParams(c, options?.params);
|
|
122
|
+
if (validation)
|
|
123
|
+
return validation;
|
|
59
124
|
// Build context layer from Hono context
|
|
60
125
|
const contextLayer = buildContextLayer(c, bridgeConfig);
|
|
126
|
+
// Resolve route model bindings if we have any and schema is configured
|
|
127
|
+
let boundModelsLayer;
|
|
128
|
+
if (bindings.length > 0 && bridgeConfig?.schema) {
|
|
129
|
+
const db = bridgeConfig.database ? bridgeConfig.database(c) : c.var?.db;
|
|
130
|
+
if (!db) {
|
|
131
|
+
return c.notFound();
|
|
132
|
+
}
|
|
133
|
+
const result = await this.resolveBindings(c, bindings, db, bridgeConfig.schema);
|
|
134
|
+
if (result instanceof Response) {
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
boundModelsLayer = Layer.succeed(BoundModels, result);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// Empty bound models for routes without bindings
|
|
141
|
+
boundModelsLayer = Layer.succeed(BoundModels, new Map());
|
|
142
|
+
}
|
|
61
143
|
// Combine with provided layers
|
|
62
|
-
let fullLayer = contextLayer;
|
|
144
|
+
let fullLayer = Layer.merge(contextLayer, boundModelsLayer);
|
|
63
145
|
for (const layer of layers) {
|
|
64
146
|
fullLayer = Layer.merge(fullLayer, layer);
|
|
65
147
|
}
|
|
@@ -70,40 +152,37 @@ export class EffectRouteBuilder {
|
|
|
70
152
|
};
|
|
71
153
|
}
|
|
72
154
|
/**
|
|
73
|
-
* Register a
|
|
155
|
+
* Register a route with the given HTTP method.
|
|
156
|
+
* Parses Laravel-style bindings and converts to Hono path format.
|
|
74
157
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
158
|
+
registerRoute(method, path, effect, options) {
|
|
159
|
+
const bindings = parseBindings(path);
|
|
160
|
+
const honoPath = toHonoPath(path);
|
|
161
|
+
this.app[method](this.resolvePath(honoPath), this.createHandler(effect, bindings, options));
|
|
77
162
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
post(path, effect) {
|
|
82
|
-
this.app.post(this.resolvePath(path), this.createHandler(effect));
|
|
163
|
+
/** Register a GET route. Supports Laravel-style route model binding: /projects/{project} */
|
|
164
|
+
get(path, effect, options) {
|
|
165
|
+
this.registerRoute('get', path, effect, options);
|
|
83
166
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
put(path, effect) {
|
|
88
|
-
this.app.put(this.resolvePath(path), this.createHandler(effect));
|
|
167
|
+
/** Register a POST route. Supports Laravel-style route model binding: /projects/{project} */
|
|
168
|
+
post(path, effect, options) {
|
|
169
|
+
this.registerRoute('post', path, effect, options);
|
|
89
170
|
}
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
patch(path, effect) {
|
|
94
|
-
this.app.patch(this.resolvePath(path), this.createHandler(effect));
|
|
171
|
+
/** Register a PUT route. Supports Laravel-style route model binding: /projects/{project} */
|
|
172
|
+
put(path, effect, options) {
|
|
173
|
+
this.registerRoute('put', path, effect, options);
|
|
95
174
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
delete(path, effect) {
|
|
100
|
-
this.app.delete(this.resolvePath(path), this.createHandler(effect));
|
|
175
|
+
/** Register a PATCH route. Supports Laravel-style route model binding: /projects/{project} */
|
|
176
|
+
patch(path, effect, options) {
|
|
177
|
+
this.registerRoute('patch', path, effect, options);
|
|
101
178
|
}
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
179
|
+
/** Register a DELETE route. Supports Laravel-style route model binding: /projects/{project} */
|
|
180
|
+
delete(path, effect, options) {
|
|
181
|
+
this.registerRoute('delete', path, effect, options);
|
|
182
|
+
}
|
|
183
|
+
/** Register a route for all HTTP methods. Supports Laravel-style route model binding: /projects/{project} */
|
|
184
|
+
all(path, effect, options) {
|
|
185
|
+
this.registerRoute('all', path, effect, options);
|
|
107
186
|
}
|
|
108
187
|
}
|
|
109
188
|
/**
|
|
@@ -14,6 +14,7 @@ import { Context } from 'effect';
|
|
|
14
14
|
* declare module 'honertia/effect' {
|
|
15
15
|
* interface HonertiaDatabaseType {
|
|
16
16
|
* type: Database // Your database type (Drizzle, Prisma, Kysely, etc.)
|
|
17
|
+
* schema: typeof schema // Your Drizzle schema for route model binding
|
|
17
18
|
* }
|
|
18
19
|
* }
|
|
19
20
|
* ```
|
|
@@ -21,7 +22,8 @@ import { Context } from 'effect';
|
|
|
21
22
|
* Then use the `DatabaseService` tag to get your typed database.
|
|
22
23
|
*/
|
|
23
24
|
export interface HonertiaDatabaseType {
|
|
24
|
-
|
|
25
|
+
type: unknown;
|
|
26
|
+
schema: Record<string, unknown>;
|
|
25
27
|
}
|
|
26
28
|
/**
|
|
27
29
|
* Augmentable interface for auth type.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/effect/services.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAEhC
|
|
1
|
+
{"version":3,"file":"services.d.ts","sourceRoot":"","sources":["../../src/effect/services.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "honertia",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Inertia.js-style server-driven SPA adapter for Hono",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,11 +60,21 @@
|
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"hono": ">=4.0.0",
|
|
63
|
-
"better-auth": ">=1.0.0"
|
|
63
|
+
"better-auth": ">=1.0.0",
|
|
64
|
+
"drizzle-orm": ">=0.30.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"better-auth": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"drizzle-orm": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
64
73
|
},
|
|
65
74
|
"devDependencies": {
|
|
66
75
|
"@types/bun": "^1.1.0",
|
|
67
76
|
"better-auth": "^1.0.0",
|
|
77
|
+
"drizzle-orm": "^0.38.0",
|
|
68
78
|
"hono": "^4.6.0",
|
|
69
79
|
"typescript": "^5.3.0"
|
|
70
80
|
}
|