ts-procedures 5.2.0 → 5.4.0
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 +150 -0
- package/agent_config/bin/postinstall.mjs +105 -0
- package/agent_config/bin/setup.mjs +286 -0
- package/agent_config/claude-code/.claude-plugin/plugin.json +5 -0
- package/agent_config/claude-code/agents/ts-procedures-architect.md +188 -0
- package/agent_config/claude-code/skills/guide/SKILL.md +142 -0
- package/agent_config/claude-code/skills/guide/anti-patterns.md +608 -0
- package/agent_config/claude-code/skills/guide/api-reference.md +696 -0
- package/agent_config/claude-code/skills/guide/patterns.md +727 -0
- package/agent_config/claude-code/skills/review/SKILL.md +53 -0
- package/agent_config/claude-code/skills/review/checklist.md +163 -0
- package/agent_config/claude-code/skills/scaffold/SKILL.md +56 -0
- package/agent_config/claude-code/skills/scaffold/templates/express-rpc.md +134 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-rpc.md +139 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-stream.md +134 -0
- package/agent_config/claude-code/skills/scaffold/templates/procedure.md +77 -0
- package/agent_config/claude-code/skills/scaffold/templates/stream-procedure.md +113 -0
- package/agent_config/copilot/copilot-instructions.md +290 -0
- package/agent_config/cursor/cursorrules +290 -0
- package/agent_config/lib/install-claude.mjs +109 -0
- package/build/implementations/http/hono-api/index.d.ts +102 -0
- package/build/implementations/http/hono-api/index.js +339 -0
- package/build/implementations/http/hono-api/index.js.map +1 -0
- package/build/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/implementations/http/hono-api/index.test.js +983 -0
- package/build/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/implementations/http/hono-api/types.d.ts +13 -0
- package/build/implementations/http/hono-api/types.js +2 -0
- package/build/implementations/http/hono-api/types.js.map +1 -0
- package/build/implementations/types.d.ts +44 -0
- package/build/index.d.ts +28 -6
- package/build/index.js +28 -0
- package/build/index.js.map +1 -1
- package/build/schema/compute-schema.d.ts +5 -0
- package/build/schema/compute-schema.js +8 -1
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +6 -5
- package/build/schema/parser.js +54 -0
- package/build/schema/parser.js.map +1 -1
- package/package.json +14 -4
- package/src/implementations/http/README.md +45 -2
- package/src/implementations/http/hono-api/index.test.ts +1328 -0
- package/src/implementations/http/hono-api/index.ts +461 -0
- package/src/implementations/http/hono-api/types.ts +16 -0
- package/src/implementations/types.ts +52 -0
- package/src/index.ts +87 -10
- package/src/schema/compute-schema.ts +23 -2
- package/src/schema/parser.ts +70 -3
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: review
|
|
3
|
+
description: "Review code for ts-procedures pattern adherence. Usage: /ts-procedures:review <path>"
|
|
4
|
+
invocable_by:
|
|
5
|
+
- user
|
|
6
|
+
- model
|
|
7
|
+
user_instructions: |
|
|
8
|
+
Usage: /ts-procedures:review <path>
|
|
9
|
+
|
|
10
|
+
Reviews files at the given path for ts-procedures pattern adherence.
|
|
11
|
+
Accepts a file path or directory.
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
/ts-procedures:review src/procedures/
|
|
15
|
+
/ts-procedures:review src/procedures/GetUser.procedure.ts
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Review ts-procedures Code
|
|
19
|
+
|
|
20
|
+
Parse `$ARGUMENTS` as a file or directory path. If a directory, review all `.ts` and `.tsx` files within it.
|
|
21
|
+
|
|
22
|
+
## Instructions
|
|
23
|
+
|
|
24
|
+
1. Read the target file(s).
|
|
25
|
+
2. Identify ts-procedures imports (`ts-procedures`, `ts-procedures/express-rpc`, `ts-procedures/hono-rpc`, `ts-procedures/hono-stream`, `ts-procedures/http`) to determine file types.
|
|
26
|
+
3. Check each file against the categorized checklist in `checklist.md`.
|
|
27
|
+
4. Output findings grouped by severity.
|
|
28
|
+
|
|
29
|
+
## Output Format
|
|
30
|
+
|
|
31
|
+
For each finding:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
[SEVERITY] file:line — Violation
|
|
35
|
+
Problem: What's wrong
|
|
36
|
+
Fix: Concrete before/after code
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Severity levels:
|
|
40
|
+
- **CRITICAL** — Will cause bugs, silent failures, resource leaks, or runtime errors. Must fix.
|
|
41
|
+
- **WARNING** — Anti-pattern that hurts maintainability or correctness. Should fix.
|
|
42
|
+
- **SUGGESTION** — Improvement for readability, type safety, or DX. Nice to have.
|
|
43
|
+
|
|
44
|
+
## Summary
|
|
45
|
+
|
|
46
|
+
After individual findings, provide:
|
|
47
|
+
- Total findings by severity
|
|
48
|
+
- Overall assessment (healthy / needs attention / significant issues)
|
|
49
|
+
- Top 3 priorities to address
|
|
50
|
+
|
|
51
|
+
## Reference
|
|
52
|
+
|
|
53
|
+
See `checklist.md` for the complete categorized checklist by file type.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# ts-procedures Review Checklist
|
|
2
|
+
|
|
3
|
+
## Procedure Definition Checks
|
|
4
|
+
|
|
5
|
+
### CRITICAL
|
|
6
|
+
- [ ] Uses `ctx.error()` for business logic errors, never throws raw `Error` instances
|
|
7
|
+
- [ ] Does not expect `schema.returnType` to be validated at runtime (it's documentation only)
|
|
8
|
+
- [ ] No duplicate procedure names within the same factory
|
|
9
|
+
- [ ] Schema uses TypeBox builders (`Type.Object(...)`, etc.), not plain JSON Schema objects
|
|
10
|
+
- [ ] Stream handlers check `ctx.signal.aborted` in loops to prevent infinite resource consumption
|
|
11
|
+
|
|
12
|
+
### WARNING
|
|
13
|
+
- [ ] Passes `ctx.signal` to all downstream async calls (fetch, database queries, etc.)
|
|
14
|
+
- [ ] Does not put manual validation logic in handler when `schema.params` handles it
|
|
15
|
+
- [ ] Does not swallow errors in try/catch without re-throwing (hides failures)
|
|
16
|
+
- [ ] Uses `validateYields: true` only when `schema.yieldType` is also defined
|
|
17
|
+
- [ ] Does not mix Create procedures and CreateStream procedures in confusing ways
|
|
18
|
+
|
|
19
|
+
### SUGGESTION
|
|
20
|
+
- [ ] Has `description` field set for documentation/introspection
|
|
21
|
+
- [ ] Uses meaningful procedure names (PascalCase, verb-noun: `GetUser`, `CreateOrder`)
|
|
22
|
+
- [ ] Destructures return value for clean exports: `const { GetUser, info } = Create(...)`
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Schema Checks
|
|
27
|
+
|
|
28
|
+
### CRITICAL
|
|
29
|
+
- [ ] Uses TypeBox (`Type.Object(...)`, `Type.String()`, etc.) — not plain objects
|
|
30
|
+
- [ ] Required fields use `Type.String()` directly; optional fields wrapped with `Type.Optional(...)`
|
|
31
|
+
- [ ] Does not define both `schema.params` and `schema.input` on the same procedure (mutually exclusive)
|
|
32
|
+
|
|
33
|
+
### WARNING
|
|
34
|
+
- [ ] Understands AJV `coerceTypes: true` — no manual type parsing for query string values
|
|
35
|
+
- [ ] Understands AJV `removeAdditional: true` — extra fields in params are silently stripped
|
|
36
|
+
- [ ] Does not rely on params fields not in the schema (they get removed)
|
|
37
|
+
|
|
38
|
+
### SUGGESTION
|
|
39
|
+
- [ ] `returnType` schema provided for documentation generation
|
|
40
|
+
- [ ] `yieldType` schema provided for streaming procedures
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Context Checks
|
|
45
|
+
|
|
46
|
+
### CRITICAL
|
|
47
|
+
- [ ] Factory has explicit `TContext` type parameter when using HTTP builders
|
|
48
|
+
- [ ] Context factory function handles errors gracefully (async failures don't crash requests)
|
|
49
|
+
|
|
50
|
+
### WARNING
|
|
51
|
+
- [ ] Context type includes `signal?: AbortSignal` if procedures need cancellation support
|
|
52
|
+
- [ ] Context factory resolves only what handlers need (avoid expensive unused computations)
|
|
53
|
+
|
|
54
|
+
### SUGGESTION
|
|
55
|
+
- [ ] Context type is documented or self-descriptive
|
|
56
|
+
- [ ] Separate factories for different access levels (public vs authenticated)
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## HTTP Builder Checks (Express/Hono)
|
|
61
|
+
|
|
62
|
+
### CRITICAL
|
|
63
|
+
- [ ] Standard `Create` procedures registered with `ExpressRPCAppBuilder` or `HonoRPCAppBuilder`, NOT `HonoStreamAppBuilder`
|
|
64
|
+
- [ ] Stream `CreateStream` procedures registered with `HonoStreamAppBuilder`, NOT the RPC builders
|
|
65
|
+
- [ ] `onError` callback handles `ProcedureValidationError` and `ProcedureError` differently
|
|
66
|
+
|
|
67
|
+
### WARNING
|
|
68
|
+
- [ ] `pathPrefix` set consistently across builders
|
|
69
|
+
- [ ] `scope` and `version` set on all procedures when using `RPCConfig`
|
|
70
|
+
- [ ] Uses `extendProcedureDoc` for documentation generation instead of manual doc building
|
|
71
|
+
- [ ] `onRequestEnd` used for cleanup/logging, not business logic
|
|
72
|
+
|
|
73
|
+
### SUGGESTION
|
|
74
|
+
- [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
|
|
75
|
+
- [ ] Lifecycle hooks used for observability (logging, metrics)
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Streaming Checks (HonoStreamAppBuilder)
|
|
80
|
+
|
|
81
|
+
### CRITICAL
|
|
82
|
+
- [ ] `onPreStreamError` handles validation errors before streaming starts (returns HTTP response)
|
|
83
|
+
- [ ] `onMidStreamError` handles runtime errors during streaming (yields error event)
|
|
84
|
+
- [ ] Stream handler does not assume `signal.reason` is always `'stream-completed'` — check for external abort
|
|
85
|
+
|
|
86
|
+
### WARNING
|
|
87
|
+
- [ ] Uses `sse()` helper for SSE metadata when custom event types or IDs are needed
|
|
88
|
+
- [ ] Correctly distinguishes stream modes: `'sse'` for EventSource clients, `'text'` for simple consumers
|
|
89
|
+
- [ ] Does not yield after `ctx.signal.aborted` is true
|
|
90
|
+
|
|
91
|
+
### SUGGESTION
|
|
92
|
+
- [ ] Stream mode documented in comments or config
|
|
93
|
+
- [ ] Error data shape matches `yieldType` schema for consistent client parsing
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## API Builder Checks (HonoAPIAppBuilder)
|
|
98
|
+
|
|
99
|
+
### CRITICAL
|
|
100
|
+
- [ ] `build()` is `await`ed — returns `Promise<Hono>`, not `Hono`
|
|
101
|
+
- [ ] Path param names in route template match `schema.input.pathParams` property names exactly
|
|
102
|
+
- [ ] Does not define both `schema.params` and `schema.input` on the same procedure
|
|
103
|
+
- [ ] `onError` callback handles `ProcedureValidationError` and `ProcedureError` differently
|
|
104
|
+
|
|
105
|
+
### WARNING
|
|
106
|
+
- [ ] Uses `schema.input` channels appropriate for the HTTP method (no `body` on GET/HEAD)
|
|
107
|
+
- [ ] `pathPrefix` set consistently
|
|
108
|
+
- [ ] `successStatus` overridden only when default (POST→201, DELETE→204) is wrong
|
|
109
|
+
- [ ] Uses `APIInput` type constraint (`satisfies APIInput`) to catch channel name typos
|
|
110
|
+
|
|
111
|
+
### SUGGESTION
|
|
112
|
+
- [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
|
|
113
|
+
- [ ] Custom `queryParser` provided if complex query string formats needed
|
|
114
|
+
- [ ] Lifecycle hooks used for observability (logging, metrics)
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Error Handling Checks
|
|
119
|
+
|
|
120
|
+
### CRITICAL
|
|
121
|
+
- [ ] Handler errors use `ctx.error(message, meta?)` — not `throw new Error()`
|
|
122
|
+
- [ ] Does not catch and swallow errors without re-throwing (hides failures from caller)
|
|
123
|
+
- [ ] HTTP builder has `onError` callback — default behavior may expose internal details
|
|
124
|
+
|
|
125
|
+
### WARNING
|
|
126
|
+
- [ ] `ProcedureValidationError` handled separately (400 status) from `ProcedureError` (4xx/5xx)
|
|
127
|
+
- [ ] Error `meta` object contains useful context (error codes, IDs) — not sensitive data
|
|
128
|
+
- [ ] Stack traces not exposed in production responses
|
|
129
|
+
|
|
130
|
+
### SUGGESTION
|
|
131
|
+
- [ ] Consistent error response shape across all procedures
|
|
132
|
+
- [ ] Error codes documented for API consumers
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Test File Checks
|
|
137
|
+
|
|
138
|
+
### CRITICAL
|
|
139
|
+
- [ ] Tests both valid and invalid param scenarios
|
|
140
|
+
- [ ] Tests `ProcedureValidationError` is thrown for schema violations
|
|
141
|
+
- [ ] Tests `ProcedureError` is thrown for business logic failures
|
|
142
|
+
|
|
143
|
+
### WARNING
|
|
144
|
+
- [ ] Tests pass context with required fields (doesn't rely on undefined context)
|
|
145
|
+
- [ ] HTTP builder tests verify response status codes and body shape
|
|
146
|
+
- [ ] Stream tests consume values with `for await...of` and break after expected count
|
|
147
|
+
|
|
148
|
+
### SUGGESTION
|
|
149
|
+
- [ ] Tests verify `info` metadata (name, schema, description)
|
|
150
|
+
- [ ] Tests verify `getProcedures()` returns expected registrations
|
|
151
|
+
- [ ] Tests verify error `meta` contains expected fields
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Extended Config Checks
|
|
156
|
+
|
|
157
|
+
### WARNING
|
|
158
|
+
- [ ] All procedures include required `TExtendedConfig` fields (e.g., `scope`, `version` for `RPCConfig`)
|
|
159
|
+
- [ ] Custom config fields used consistently across procedure group
|
|
160
|
+
|
|
161
|
+
### SUGGESTION
|
|
162
|
+
- [ ] Custom config fields documented
|
|
163
|
+
- [ ] `getProcedures()` used for config introspection (permissions audit, etc.)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scaffold
|
|
3
|
+
description: "Scaffold ts-procedures code with correct patterns. Usage: /ts-procedures:scaffold <type> <Name>"
|
|
4
|
+
invocable_by:
|
|
5
|
+
- user
|
|
6
|
+
- model
|
|
7
|
+
user_instructions: |
|
|
8
|
+
Usage: /ts-procedures:scaffold <type> <Name>
|
|
9
|
+
|
|
10
|
+
Types: procedure, stream-procedure, express-rpc, hono-rpc, hono-stream, hono-api
|
|
11
|
+
|
|
12
|
+
Examples:
|
|
13
|
+
/ts-procedures:scaffold procedure GetUser
|
|
14
|
+
/ts-procedures:scaffold stream-procedure StreamActivity
|
|
15
|
+
/ts-procedures:scaffold express-rpc UserApi
|
|
16
|
+
/ts-procedures:scaffold hono-rpc OrderApi
|
|
17
|
+
/ts-procedures:scaffold hono-stream LiveFeed
|
|
18
|
+
/ts-procedures:scaffold hono-api ProductApi
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Scaffold ts-procedures Code
|
|
22
|
+
|
|
23
|
+
Parse `$ARGUMENTS` as `<type> <Name>` (case-insensitive type, PascalCase Name).
|
|
24
|
+
|
|
25
|
+
## Instructions
|
|
26
|
+
|
|
27
|
+
1. Parse the arguments. If missing, ask the user for `<type>` and `<Name>`.
|
|
28
|
+
2. Derive placeholder variants from the provided PascalCase Name:
|
|
29
|
+
- `{{Name}}` — PascalCase as given (e.g., `UserProfile`)
|
|
30
|
+
- `{{name}}` — camelCase (e.g., `userProfile`)
|
|
31
|
+
- For URL scopes and file paths, use kebab-case (e.g., `user-profile`)
|
|
32
|
+
3. Read the template file from `templates/<type>.md` in this skill directory.
|
|
33
|
+
4. Replace all `{{Name}}` and `{{name}}` placeholders with the appropriate variants.
|
|
34
|
+
5. Generate the implementation file(s) following the template exactly.
|
|
35
|
+
6. Also generate a colocated test file following ts-procedures test conventions.
|
|
36
|
+
|
|
37
|
+
## Valid Types
|
|
38
|
+
|
|
39
|
+
| Type | Template | Files Generated |
|
|
40
|
+
|------|----------|----------------|
|
|
41
|
+
| `procedure` | `templates/procedure.md` | `{{Name}}.procedure.ts`, `{{Name}}.procedure.test.ts` |
|
|
42
|
+
| `stream-procedure` | `templates/stream-procedure.md` | `{{Name}}.stream.ts`, `{{Name}}.stream.test.ts` |
|
|
43
|
+
| `express-rpc` | `templates/express-rpc.md` | `{{Name}}.rpc.ts`, `{{Name}}.rpc.test.ts` |
|
|
44
|
+
| `hono-rpc` | `templates/hono-rpc.md` | `{{Name}}.rpc.ts`, `{{Name}}.rpc.test.ts` |
|
|
45
|
+
| `hono-stream` | `templates/hono-stream.md` | `{{Name}}.stream-rpc.ts`, `{{Name}}.stream-rpc.test.ts` |
|
|
46
|
+
| `hono-api` | `templates/hono-api.md` | `{{Name}}.api.ts`, `{{Name}}.api.test.ts` |
|
|
47
|
+
|
|
48
|
+
## Rules
|
|
49
|
+
|
|
50
|
+
- Use `ctx.error()` for business logic errors, never throw raw `Error`.
|
|
51
|
+
- `schema.params` is validated at runtime; `schema.returnType` is documentation only.
|
|
52
|
+
- Pass `ctx.signal` to all downstream async calls.
|
|
53
|
+
- Stream handlers always have `ctx.signal` (guaranteed `AbortSignal`).
|
|
54
|
+
- Use TypeBox for schema definitions (`import { Type } from 'typebox'`).
|
|
55
|
+
- AJV config: `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
|
|
56
|
+
- Test files use `describe`/`test` from vitest.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Express RPC Template: {{Name}}
|
|
2
|
+
|
|
3
|
+
## Implementation — `{{Name}}.rpc.ts`
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { Procedures, ProcedureError, ProcedureValidationError } from 'ts-procedures'
|
|
7
|
+
import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
|
|
8
|
+
import type { RPCConfig } from 'ts-procedures/http'
|
|
9
|
+
import { Type } from 'typebox'
|
|
10
|
+
|
|
11
|
+
// ─── Context ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
type {{Name}}Context = {
|
|
14
|
+
userId: string
|
|
15
|
+
requestId: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Procedures ───────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const RPC = Procedures<{{Name}}Context, RPCConfig>()
|
|
21
|
+
|
|
22
|
+
export const { GetItem } = RPC.Create(
|
|
23
|
+
'GetItem',
|
|
24
|
+
{
|
|
25
|
+
scope: '{{name}}', // TODO: set URL scope
|
|
26
|
+
version: 1,
|
|
27
|
+
description: 'Fetch item by ID',
|
|
28
|
+
schema: {
|
|
29
|
+
params: Type.Object({
|
|
30
|
+
id: Type.String(),
|
|
31
|
+
}),
|
|
32
|
+
returnType: Type.Object({
|
|
33
|
+
id: Type.String(),
|
|
34
|
+
name: Type.String(),
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
async (ctx, params) => {
|
|
39
|
+
// TODO: implement
|
|
40
|
+
return { id: params.id, name: 'Example' }
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
export const { ListItems } = RPC.Create(
|
|
45
|
+
'ListItems',
|
|
46
|
+
{
|
|
47
|
+
scope: '{{name}}',
|
|
48
|
+
version: 1,
|
|
49
|
+
description: 'List all items',
|
|
50
|
+
schema: {
|
|
51
|
+
params: Type.Object({
|
|
52
|
+
page: Type.Optional(Type.Number()),
|
|
53
|
+
limit: Type.Optional(Type.Number()),
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
async (ctx, params) => {
|
|
58
|
+
// TODO: implement
|
|
59
|
+
return { items: [], total: 0 }
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// ─── Express App Builder ──────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export const {{name}}App = new ExpressRPCAppBuilder({
|
|
66
|
+
pathPrefix: '/api',
|
|
67
|
+
onError: (procedure, req, res, error) => {
|
|
68
|
+
if (error instanceof ProcedureValidationError) {
|
|
69
|
+
res.status(400).json({
|
|
70
|
+
error: error.message,
|
|
71
|
+
details: error.errors,
|
|
72
|
+
procedure: error.procedureName,
|
|
73
|
+
})
|
|
74
|
+
} else if (error instanceof ProcedureError) {
|
|
75
|
+
res.status(422).json({
|
|
76
|
+
error: error.message,
|
|
77
|
+
meta: error.meta,
|
|
78
|
+
procedure: error.procedureName,
|
|
79
|
+
})
|
|
80
|
+
} else {
|
|
81
|
+
res.status(500).json({ error: 'Internal server error' })
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
.register(RPC, async (req) => ({
|
|
86
|
+
userId: req.headers['x-user-id'] as string || 'anonymous',
|
|
87
|
+
requestId: req.headers['x-request-id'] as string || crypto.randomUUID(),
|
|
88
|
+
}))
|
|
89
|
+
.build()
|
|
90
|
+
|
|
91
|
+
// Route map:
|
|
92
|
+
// POST /api/{{name}}/get-item/1
|
|
93
|
+
// POST /api/{{name}}/list-items/1
|
|
94
|
+
|
|
95
|
+
// Documentation: {{name}}App.docs
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Test — `{{Name}}.rpc.test.ts`
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { describe, test, expect } from 'vitest'
|
|
102
|
+
import supertest from 'supertest'
|
|
103
|
+
import { {{name}}App } from './{{Name}}.rpc'
|
|
104
|
+
|
|
105
|
+
describe('{{Name}} RPC', () => {
|
|
106
|
+
test('GET item returns expected result', async () => {
|
|
107
|
+
const res = await supertest({{name}}App)
|
|
108
|
+
.post('/api/{{name}}/get-item/1')
|
|
109
|
+
.send({ id: 'item-1' })
|
|
110
|
+
.expect(200)
|
|
111
|
+
|
|
112
|
+
expect(res.body).toEqual({ id: 'item-1', name: 'Example' })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('returns 400 for invalid params', async () => {
|
|
116
|
+
const res = await supertest({{name}}App)
|
|
117
|
+
.post('/api/{{name}}/get-item/1')
|
|
118
|
+
.send({})
|
|
119
|
+
.expect(400)
|
|
120
|
+
|
|
121
|
+
expect(res.body.error).toBeDefined()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('LIST items returns array', async () => {
|
|
125
|
+
const res = await supertest({{name}}App)
|
|
126
|
+
.post('/api/{{name}}/list-items/1')
|
|
127
|
+
.send({ page: 1, limit: 10 })
|
|
128
|
+
.expect(200)
|
|
129
|
+
|
|
130
|
+
expect(res.body).toHaveProperty('items')
|
|
131
|
+
expect(res.body).toHaveProperty('total')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
```
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Hono API Template: {{Name}}
|
|
2
|
+
|
|
3
|
+
## Implementation — `{{Name}}.api.ts`
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { Procedures, ProcedureError, ProcedureValidationError } from 'ts-procedures'
|
|
7
|
+
import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
|
|
8
|
+
import type { APIConfig, APIInput } from 'ts-procedures/http'
|
|
9
|
+
import { Type } from 'typebox'
|
|
10
|
+
|
|
11
|
+
// ─── Context ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
type {{Name}}Context = {
|
|
14
|
+
userId: string
|
|
15
|
+
requestId: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Procedures ───────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const API = Procedures<{{Name}}Context, APIConfig>()
|
|
21
|
+
|
|
22
|
+
export const { GetItem } = API.Create(
|
|
23
|
+
'GetItem',
|
|
24
|
+
{
|
|
25
|
+
path: '/{{name}}/:id', // TODO: set route path
|
|
26
|
+
method: 'get',
|
|
27
|
+
description: 'Fetch item by ID',
|
|
28
|
+
schema: {
|
|
29
|
+
input: {
|
|
30
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
31
|
+
} satisfies APIInput,
|
|
32
|
+
returnType: Type.Object({
|
|
33
|
+
id: Type.String(),
|
|
34
|
+
name: Type.String(),
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
async (ctx, { pathParams }) => {
|
|
39
|
+
// TODO: implement
|
|
40
|
+
return { id: pathParams.id, name: 'Example' }
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
export const { CreateItem } = API.Create(
|
|
45
|
+
'CreateItem',
|
|
46
|
+
{
|
|
47
|
+
path: '/{{name}}',
|
|
48
|
+
method: 'post',
|
|
49
|
+
description: 'Create a new item',
|
|
50
|
+
schema: {
|
|
51
|
+
input: {
|
|
52
|
+
body: Type.Object({
|
|
53
|
+
name: Type.String(),
|
|
54
|
+
}),
|
|
55
|
+
} satisfies APIInput,
|
|
56
|
+
returnType: Type.Object({
|
|
57
|
+
id: Type.String(),
|
|
58
|
+
name: Type.String(),
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
async (ctx, { body }) => {
|
|
63
|
+
// TODO: implement
|
|
64
|
+
return { id: 'new-id', name: body.name }
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
export const { DeleteItem } = API.Create(
|
|
69
|
+
'DeleteItem',
|
|
70
|
+
{
|
|
71
|
+
path: '/{{name}}/:id',
|
|
72
|
+
method: 'delete',
|
|
73
|
+
schema: {
|
|
74
|
+
input: {
|
|
75
|
+
pathParams: Type.Object({ id: Type.String() }),
|
|
76
|
+
} satisfies APIInput,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
async (ctx, { pathParams }) => {
|
|
80
|
+
// TODO: implement deletion
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// ─── Hono App Builder ─────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export const {{name}}App = await new HonoAPIAppBuilder({
|
|
87
|
+
pathPrefix: '/api',
|
|
88
|
+
onError: (procedure, c, error) => {
|
|
89
|
+
if (error instanceof ProcedureValidationError) {
|
|
90
|
+
return c.json({
|
|
91
|
+
error: error.message,
|
|
92
|
+
details: error.errors,
|
|
93
|
+
procedure: error.procedureName,
|
|
94
|
+
}, 400)
|
|
95
|
+
} else if (error instanceof ProcedureError) {
|
|
96
|
+
return c.json({
|
|
97
|
+
error: error.message,
|
|
98
|
+
meta: error.meta,
|
|
99
|
+
procedure: error.procedureName,
|
|
100
|
+
}, 422)
|
|
101
|
+
}
|
|
102
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
.register(API, (c) => ({
|
|
106
|
+
userId: c.req.header('x-user-id') || 'anonymous',
|
|
107
|
+
requestId: c.req.header('x-request-id') || crypto.randomUUID(),
|
|
108
|
+
}))
|
|
109
|
+
.build()
|
|
110
|
+
|
|
111
|
+
// Route map:
|
|
112
|
+
// GET /api/{{name}}/:id → 200
|
|
113
|
+
// POST /api/{{name}} → 201
|
|
114
|
+
// DELETE /api/{{name}}/:id → 204
|
|
115
|
+
|
|
116
|
+
// Documentation: builder.docs (access before .build() resolves)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Test — `{{Name}}.api.test.ts`
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { describe, test, expect } from 'vitest'
|
|
123
|
+
import { {{name}}App } from './{{Name}}.api'
|
|
124
|
+
|
|
125
|
+
describe('{{Name}} API', () => {
|
|
126
|
+
test('GET item returns expected result', async () => {
|
|
127
|
+
const app = await {{name}}App
|
|
128
|
+
const res = await app.request('/api/{{name}}/item-1')
|
|
129
|
+
|
|
130
|
+
expect(res.status).toBe(200)
|
|
131
|
+
const body = await res.json()
|
|
132
|
+
expect(body).toEqual({ id: 'item-1', name: 'Example' })
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('POST item returns 201', async () => {
|
|
136
|
+
const app = await {{name}}App
|
|
137
|
+
const res = await app.request('/api/{{name}}', {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({ name: 'New Item' }),
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
expect(res.status).toBe(201)
|
|
144
|
+
const body = await res.json()
|
|
145
|
+
expect(body).toHaveProperty('id')
|
|
146
|
+
expect(body.name).toBe('New Item')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('DELETE item returns 204', async () => {
|
|
150
|
+
const app = await {{name}}App
|
|
151
|
+
const res = await app.request('/api/{{name}}/item-1', {
|
|
152
|
+
method: 'DELETE',
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
expect(res.status).toBe(204)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('returns 400 for invalid POST body', async () => {
|
|
159
|
+
const app = await {{name}}App
|
|
160
|
+
const res = await app.request('/api/{{name}}', {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify({}),
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
expect(res.status).toBe(400)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
```
|