@spfn/core 0.3.0-beta.12 → 0.3.0-beta.14
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 +7 -5
- package/dist/codegen/index.d.ts +9 -0
- package/dist/codegen/index.js +143 -37
- package/dist/codegen/index.js.map +1 -1
- package/dist/contract/index.d.ts +1 -1
- package/dist/event/index.d.ts +1 -2
- package/dist/event/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +13 -13
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +11 -6
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +1 -1
- package/dist/ops/index.js.map +1 -1
- package/dist/route/index.d.ts +2 -2
- package/dist/route/index.js.map +1 -1
- package/dist/{router-9L3El7nO.d.ts → router-BqyEARoY.d.ts} +25 -23
- package/dist/server/index.js.map +1 -1
- package/dist/{types-yv6z_mK7.d.ts → types-QoyjaJWw.d.ts} +6 -0
- package/package.json +1 -1
- package/src/codegen/README.md +29 -15
- package/src/nextjs/README.md +17 -10
- package/src/route/README.md +14 -9
|
@@ -252,6 +252,12 @@ interface SetCookie {
|
|
|
252
252
|
type RequestInterceptor = (url: string, init: RequestInit) => Promise<RequestInit> | RequestInit;
|
|
253
253
|
/**
|
|
254
254
|
* Response interceptor - called after fetch
|
|
255
|
+
*
|
|
256
|
+
* It runs outside the client's transport error handling, so throwing from it is a
|
|
257
|
+
* supported way to end the call: the error reaches the caller unchanged, which is what
|
|
258
|
+
* lets `redirect()` and `notFound()` navigate from one (issue #104). Returning
|
|
259
|
+
* `{ response, body }` replaces both for everything after it, error-status handling
|
|
260
|
+
* included.
|
|
255
261
|
*/
|
|
256
262
|
type ResponseInterceptor = (response: Response, body: any) => Promise<{
|
|
257
263
|
response: Response;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfn/core",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.14",
|
|
4
4
|
"description": "Full-stack TypeScript backend for Next.js: file-based typed routes, Drizzle entities and repositories, PostgreSQL transactions and a generated end-to-end client, in one fixed vertical slice per feature",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/codegen/README.md
CHANGED
|
@@ -153,9 +153,13 @@ way `registerRoutes` walks it. How the router is *written* does not matter: a on
|
|
|
153
153
|
server registers them.
|
|
154
154
|
- `method` and `path` come from the `RouteDef`, not from a pattern over the source. A
|
|
155
155
|
route not registered in the router is absent, because there is nothing registering it.
|
|
156
|
-
- **`.packages()` routers are
|
|
157
|
-
route map
|
|
158
|
-
|
|
156
|
+
- **`.packages()` routers are carried**, at every depth, provided the router publishes a
|
|
157
|
+
route map (`defineRouter`, not `defineUnmappedRouter`). A package ships a client that
|
|
158
|
+
names its own routes — `authApi.login`, `monitorApi.getStats` — and the app's
|
|
159
|
+
`createRpcProxy` resolves a name in the one map it was constructed with, so the app
|
|
160
|
+
merges nothing by hand: `{ ...routeMap, ...authRouteMap }` is a no-op today. They are
|
|
161
|
+
written after the app's own routes, under a comment line naming where they came from,
|
|
162
|
+
and the header says how many there are and which mount each came through.
|
|
159
163
|
- A name that is not a JS identifier (`'get-user'`) is emitted quoted, and `__proto__` is
|
|
160
164
|
emitted as a computed key, so the generated file parses and the map keeps every route as
|
|
161
165
|
an own property.
|
|
@@ -202,16 +206,20 @@ Unlike the old source parser, which dropped what it could not read, the generato
|
|
|
202
206
|
line in the diff rather than a route that vanishes at build.
|
|
203
207
|
- Two routes reach the same name (the message names both places).
|
|
204
208
|
- An **app route and a package route** reach the same name, where that package publishes a
|
|
205
|
-
route map. Both register at runtime, and the
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
209
|
+
route map. Both register at runtime, and both want the single entry that name has in this
|
|
210
|
+
map — so one of the two paths becomes unreachable while the generated types still
|
|
211
|
+
describe the app's route. **Two package routers** reaching one name is refused too, and
|
|
212
|
+
names both mounts: the generator writes the order now, so the ambiguity is its to refuse.
|
|
213
|
+
The same route reached through two mounts is not two routes — a package exporting one set
|
|
214
|
+
of routes under a router and a sub-router an app mounts beside it hands both mounts the
|
|
215
|
+
same route object, and it is collected once. No first-party package ships that shape today;
|
|
216
|
+
the rule is keyed on object identity so that an app mounting both still builds when one
|
|
217
|
+
does, and the header line of the second mount says which mount got there first.
|
|
218
|
+
A collision with a router that publishes **no** map is not refused — one declared with
|
|
210
219
|
`defineUnmappedRouter`, whose routes are reached by the URL something was handed rather
|
|
211
|
-
than by name: the ops surface (`createOpsRouter`)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
build.
|
|
220
|
+
than by name: the ops surface (`createOpsRouter`) and the tracking routes of
|
|
221
|
+
`@spfn/notification`. Nothing of theirs is written here, so an app route named after one
|
|
222
|
+
of their routes keeps its own path and must not fail the build.
|
|
215
223
|
- A route's `method` is not an `HttpMethod`. `registerRoutes` lowercases the method before
|
|
216
224
|
registering, so `method: 'get'` serves fine and would emit a map that fails `tsc`.
|
|
217
225
|
|
|
@@ -222,8 +230,9 @@ registering a route before `.handler()` was called), and a router entry that is
|
|
|
222
230
|
route nor a router. `registerRoutes` warns and skips both, so a map without them still names
|
|
223
231
|
every route the server answers — which is all a refusal would have been protecting.
|
|
224
232
|
|
|
225
|
-
|
|
226
|
-
is
|
|
233
|
+
A package route with no method or path is skipped the same way, and for the same reason.
|
|
234
|
+
What is refused is only what would write a file that does not compile or that answers to
|
|
235
|
+
the wrong path: a method that is not an `HttpMethod`, and the two collisions above.
|
|
227
236
|
|
|
228
237
|
### Generated output
|
|
229
238
|
|
|
@@ -240,6 +249,8 @@ export interface RouteInfo
|
|
|
240
249
|
export const routeMap: Record<string, RouteInfo> = {
|
|
241
250
|
getUser: { method: 'GET', path: '/users/:id' },
|
|
242
251
|
createUser: { method: 'POST', path: '/users' },
|
|
252
|
+
// From the package routers this app mounts with .packages(), not its own:
|
|
253
|
+
login: { method: 'POST', path: '/_auth/login' },
|
|
243
254
|
};
|
|
244
255
|
|
|
245
256
|
export type RouteMap = typeof routeMap;
|
|
@@ -484,7 +495,10 @@ regeneration when it's absent (build/manual passes don't set it).
|
|
|
484
495
|
output the RPC proxy imports. If it's missing or stale, `api.<route>.call()` resolves the
|
|
485
496
|
wrong (or no) `method`/`path`. `spfn build` runs codegen first; if you build by other
|
|
486
497
|
means, run `spfn codegen run` yourself. Treat the file as generated (it's marked
|
|
487
|
-
`DO NOT EDIT`).
|
|
498
|
+
`DO NOT EDIT`). **Upgrading a mounted package goes stale the same way and is quieter about
|
|
499
|
+
it**: the map carries that package's routes, but the upgrade changes no app source, so no
|
|
500
|
+
watcher fires and nothing else says the committed map is a version behind — re-run codegen
|
|
501
|
+
after an upgrade as you would after adding a route.
|
|
488
502
|
- **A route must be in `defineRouter({...})` to be emitted.** Defining `export const foo =
|
|
489
503
|
route.get(...)` but not registering `foo` in the router drops it from the map — the
|
|
490
504
|
generator walks the router, so an unregistered route does not exist as far as it is
|
package/src/nextjs/README.md
CHANGED
|
@@ -292,24 +292,26 @@ type RouteMap = Record<string, RouteMapEntry>;
|
|
|
292
292
|
|
|
293
293
|
### `routeMap` — the proxy's source of truth
|
|
294
294
|
|
|
295
|
-
`routeMap` comes from codegen (`@/generated/route-map`)
|
|
296
|
-
|
|
295
|
+
`routeMap` comes from codegen (`@/generated/route-map`) and already holds the routes of every
|
|
296
|
+
package router the app router mounts with `.packages()` — `@spfn/auth`'s included, so
|
|
297
|
+
`{ ...routeMap, ...authRouteMap }` is a no-op and no longer needed. `eventRouteMap` is still
|
|
298
|
+
merged by hand: it is a hand-written constant, not a mounted router.
|
|
297
299
|
|
|
298
300
|
```typescript
|
|
299
301
|
// app/api/rpc/[routeName]/route.ts
|
|
300
302
|
import '@spfn/auth/nextjs/api'; // side-effect: registers auth interceptors
|
|
301
303
|
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
302
|
-
import { authRouteMap } from '@spfn/auth';
|
|
303
304
|
import { eventRouteMap } from '@spfn/core/event';
|
|
304
305
|
import { routeMap } from '@/generated/route-map';
|
|
305
306
|
|
|
306
307
|
export const { GET, POST } = createRpcProxy({
|
|
307
|
-
routeMap: { ...routeMap, ...
|
|
308
|
+
routeMap: { ...routeMap, ...eventRouteMap },
|
|
308
309
|
});
|
|
309
310
|
```
|
|
310
311
|
|
|
311
|
-
A `routeName` absent from
|
|
312
|
-
|
|
312
|
+
A `routeName` absent from `routeMap` returns **404** from the proxy (not from the backend).
|
|
313
|
+
After adding routes — or after upgrading a mounted package — re-run codegen and clear `.spfn`
|
|
314
|
+
cache if stale.
|
|
313
315
|
|
|
314
316
|
### Header forwarding & client IP
|
|
315
317
|
|
|
@@ -341,7 +343,7 @@ to your LB/nginx depth) rather than widening the header allowlist — forwarding
|
|
|
341
343
|
|
|
342
344
|
```typescript
|
|
343
345
|
export const { GET, POST } = createRpcProxy({
|
|
344
|
-
routeMap: { ...routeMap, ...
|
|
346
|
+
routeMap: { ...routeMap, ...eventRouteMap },
|
|
345
347
|
apiUrl: process.env.SPFN_API_URL,
|
|
346
348
|
timeout: 60000,
|
|
347
349
|
debug: true,
|
|
@@ -595,9 +597,10 @@ export async function createUser(formData: FormData) {
|
|
|
595
597
|
- **The proxy resolves the real HTTP method from `routeMap`, not from the client call.** The
|
|
596
598
|
client only ever sends GET (no body) or POST (body/formData) to `/api/rpc/...`. A PUT /
|
|
597
599
|
PATCH / DELETE route still works — the backend method comes from `routeMap[routeName]`.
|
|
598
|
-
- **A missing `routeName` in `routeMap` is a proxy 404, not a backend 404.**
|
|
599
|
-
|
|
600
|
-
delete `.spfn` if the map
|
|
600
|
+
- **A missing `routeName` in `routeMap` is a proxy 404, not a backend 404.** Re-run codegen
|
|
601
|
+
after adding routes or upgrading a mounted package — the generated map carries the mounted
|
|
602
|
+
packages' routes — merge `eventRouteMap` if you use SSE, and delete `.spfn` if the map
|
|
603
|
+
looks stale.
|
|
601
604
|
- **Interceptor `pathPattern` matches the resolved backend path** (e.g. `/users/123`,
|
|
602
605
|
`/_auth/login`) — *not* the `/api/rpc/{routeName}` URL the client hit.
|
|
603
606
|
- **Two same-named interceptor type pairs.** `RequestInterceptor`/`ResponseInterceptor` from
|
|
@@ -619,6 +622,10 @@ export async function createUser(formData: FormData) {
|
|
|
619
622
|
`x-forwarded-for` — adding it back makes the IP spoofable. See *Header forwarding & client IP*.
|
|
620
623
|
- **Each `.headers()/.cookies()/.fetchOptions()/.onRequest()/.onResponse()` returns a new
|
|
621
624
|
builder.** Chain them in one expression; a dangling builder without `.call()` does nothing.
|
|
625
|
+
- **A client `onResponse` may throw, and the throw is the caller's.** It runs outside the
|
|
626
|
+
transport error handling, so `redirect('/login')` on a 401 navigates and a domain error
|
|
627
|
+
raised there is raised as itself — neither is relabelled an `ApiError` of type `'network'`.
|
|
628
|
+
Only a failed fetch (`0` / `'network'`) and an aborted one (`408` / `'timeout'`) are.
|
|
622
629
|
|
|
623
630
|
---
|
|
624
631
|
|
package/src/route/README.md
CHANGED
|
@@ -458,19 +458,24 @@ export const appRouter = defineRouter({ getRoot, getStatus })
|
|
|
458
458
|
|
|
459
459
|
`.packages()` also flattens any nested package routers the given routers themselves declared.
|
|
460
460
|
|
|
461
|
+
A mounted package's routes are written into the app's generated route map, so the app merges
|
|
462
|
+
nothing by hand — `createRpcProxy({ routeMap })` resolves `authApi.login` as it resolves the
|
|
463
|
+
app's own names.
|
|
464
|
+
|
|
461
465
|
**A name a package route already uses is not available to an app route.** Both register, at
|
|
462
|
-
their own paths,
|
|
463
|
-
|
|
464
|
-
|
|
466
|
+
their own paths, but the map holds one entry per name: one of the two paths becomes
|
|
467
|
+
unreachable while the generated types still describe the app's route. `spfn codegen run`
|
|
468
|
+
refuses rather than generating that, naming both sides. Two package routers reaching one
|
|
469
|
+
name is refused the same way.
|
|
465
470
|
|
|
466
471
|
A package that publishes **no** route map is the exception, and declares itself one with
|
|
467
472
|
`defineUnmappedRouter`: its routes are reached by the URL something was handed rather than by
|
|
468
|
-
name, so nothing
|
|
469
|
-
freely. `createOpsRouter` builds the ops surface that way — `spfn ops`
|
|
470
|
-
its URL — as do
|
|
471
|
-
`defineRouter` is the default because the obligation runs the other way: a package
|
|
472
|
-
|
|
473
|
-
|
|
473
|
+
name, so nothing of theirs is written into the app's map and an app route may share a name
|
|
474
|
+
with one of them freely. `createOpsRouter` builds the ops surface that way — `spfn ops`
|
|
475
|
+
invokes a command over its URL — as do the tracking routes of `@spfn/notification`.
|
|
476
|
+
`defineRouter` is the default because the obligation runs the other way: a package whose
|
|
477
|
+
client names its own routes (`@spfn/auth`, `@spfn/monitor`, `@spfn/cms`) must not declare
|
|
478
|
+
itself unmapped, or those names land in nobody's map and resolve nowhere.
|
|
474
479
|
|
|
475
480
|
### `.use([...])` — router-level global middleware
|
|
476
481
|
|