@spfn/core 0.2.0-beta.69 → 0.2.0-beta.71

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 CHANGED
@@ -7,6 +7,12 @@ feature — an entity, a repository, a route, a router registration — so that
7
7
  nor your coding agent has to invent one per feature. Everything else in SPFN
8
8
  (`@spfn/auth`, `@spfn/mcp`, `@spfn/storage`, …) plugs into it.
9
9
 
10
+ Fixing the shape is an answer to [architecture drift](https://superfunction.xyz/architecture-drift)
11
+ — a codebase acquiring several structures because each feature was arranged freshly. The
12
+ usual answers detect it after the fact; this one leaves nothing to decide. It removes
13
+ drift that comes from structural choice, and nothing else: two services can still
14
+ implement the same rule differently inside a correct shape.
15
+
10
16
  📖 **[superfunction.xyz](https://superfunction.xyz)** — docs and the full-stack tutorial ·
11
17
  [repository README](https://github.com/fxylabs/spfn) for the whole framework.
12
18
 
@@ -40,7 +46,7 @@ pnpm add @spfn/core@beta drizzle-orm@1.0.0-rc.4 postgres pg
40
46
  # optional peer: next ^16.2.11 (only for the Next.js bridge)
41
47
  ```
42
48
 
43
- Node `>=18.18.0`. ESM only.
49
+ Node `>=20.0.0`. ESM only.
44
50
 
45
51
  > **Declare Drizzle and the Postgres drivers in your own app, not just in SPFN.**
46
52
  > `@spfn/core` takes `drizzle-orm` as a peer dependency, and Drizzle changes how it
@@ -373,7 +379,7 @@ There is no `.` entry in `exports` — by design. Subpaths keep server-only code
373
379
  client bundles, which a single barrel file cannot do.
374
380
 
375
381
  **Which Node version?**
376
- `>=18.18.0`. The package is ESM only.
382
+ `>=20.0.0`. The package is ESM only.
377
383
 
378
384
  **Where do my business rules go?**
379
385
  In services and repositories, not in route handlers. A handler validates, calls, and
@@ -281,4 +281,4 @@ declare function isBossRunning(): boolean;
281
281
  */
282
282
  declare function shouldClearOnStart(): boolean;
283
283
 
284
- export { type BossOptions as B, type CompensateHandler as C, type InferJobInput as I, type JobRouter as J, type JobOptions as a, type JobHandler as b, type JobDef as c, type JobRouterEntry as d, type JobSendOptions as e, type InferJobOutput as f, getBoss as g, isBossRunning as h, initBoss as i, shouldClearOnStart as j, type BossConfig as k, stopBoss as s };
284
+ export { type BossConfig as B, type CompensateHandler as C, type InferJobInput as I, type JobOptions as J, type JobHandler as a, type JobDef as b, type JobRouter as c, type JobRouterEntry as d, type BossOptions as e, type InferJobOutput as f, type JobSendOptions as g, getBoss as h, initBoss as i, isBossRunning as j, stopBoss as k, shouldClearOnStart as s };
@@ -1,6 +1,6 @@
1
- import { f as RouteAuthProfile, a as Router } from '../router-HDVa6gfc.js';
1
+ import { R as RouteAuthProfile, a as Router } from '../router-BgZUDHp5.js';
2
2
  import { HttpMethod } from '../route/types.js';
3
- import '../define-middleware-B9bFuXVU.js';
3
+ import '../define-middleware-DfDP39Nq.js';
4
4
  import 'hono';
5
5
  import '@sinclair/typebox';
6
6
  import 'hono/utils/http-status';
@@ -164,4 +164,4 @@ declare function defineMiddlewareFactory<TName extends string, TArgs extends unk
164
164
  */
165
165
  type ExtractMiddlewareNames<T extends readonly NamedMiddleware<string>[]> = T[number]['_name'];
166
166
 
167
- export { type ExtractMiddlewareNames as E, type NamedMiddlewareFactory as N, defineMiddlewareFactory as a, type NamedMiddleware as b, defineMiddleware as d };
167
+ export { type ExtractMiddlewareNames as E, type NamedMiddlewareFactory as N, type NamedMiddleware as a, defineMiddlewareFactory as b, defineMiddleware as d };
@@ -1,8 +1,8 @@
1
1
  import { TSchema, Static } from '@sinclair/typebox';
2
- import { a as EventDef } from '../token-manager-vZeqBbtA.js';
3
- export { e as EventHandler, E as EventRouterDef, I as InferEventNames, f as InferEventPayload, c as InferEventPayloads, b as InferRouterEventPayload, J as JobQueueSender, P as PubSubCache, d as defineEventRouter } from '../token-manager-vZeqBbtA.js';
2
+ import { E as EventDef } from '../token-manager-BT5EnUAR.js';
3
+ export { a as EventHandler, b as EventRouterDef, I as InferEventNames, c as InferEventPayload, d as InferEventPayloads, e as InferRouterEventPayload, J as JobQueueSender, P as PubSubCache, f as defineEventRouter } from '../token-manager-BT5EnUAR.js';
4
4
  export { defineWSRouter } from './ws/index.js';
5
- import '../types-CF-37KAG.js';
5
+ import '../types-2AbaW4Ie.js';
6
6
  import 'hono';
7
7
  import 'node:http';
8
8
 
@@ -1,5 +1,5 @@
1
- import { E as EventRouterDef, I as InferEventNames } from '../../token-manager-vZeqBbtA.js';
2
- import { g as SSESubscribeOptions, i as SSEUnsubscribe, h as SSEConnectionState, d as SSEClientConfig } from '../../types-D9uMxeQS.js';
1
+ import { b as EventRouterDef, I as InferEventNames } from '../../token-manager-BT5EnUAR.js';
2
+ import { S as SSEClientConfig, a as SSESubscribeOptions, b as SSEUnsubscribe, c as SSEConnectionState } from '../../types-ZQODsBft.js';
3
3
  import '@sinclair/typebox';
4
4
  import 'hono';
5
5
 
@@ -1,8 +1,8 @@
1
1
  import { Context } from 'hono';
2
- import { E as EventRouterDef, S as SSETokenManager } from '../../token-manager-vZeqBbtA.js';
3
- export { C as CacheTokenStore, g as SSEToken, i as SSETokenManagerConfig, h as SSETokenStore } from '../../token-manager-vZeqBbtA.js';
4
- import { S as SSEHandlerConfig } from '../../types-D9uMxeQS.js';
5
- export { a as SSEAuthConfig, d as SSEClientConfig, h as SSEConnectionState, e as SSEEventHandler, f as SSEEventHandlers, c as SSEHandlerAuthConfig, b as SSEMessage, g as SSESubscribeOptions, i as SSEUnsubscribe } from '../../types-D9uMxeQS.js';
2
+ import { b as EventRouterDef, S as SSETokenManager } from '../../token-manager-BT5EnUAR.js';
3
+ export { C as CacheTokenStore, g as SSEToken, h as SSETokenManagerConfig, i as SSETokenStore } from '../../token-manager-BT5EnUAR.js';
4
+ import { d as SSEHandlerConfig } from '../../types-ZQODsBft.js';
5
+ export { e as SSEAuthConfig, S as SSEClientConfig, c as SSEConnectionState, f as SSEEventHandler, g as SSEEventHandlers, h as SSEHandlerAuthConfig, i as SSEMessage, a as SSESubscribeOptions, b as SSEUnsubscribe } from '../../types-ZQODsBft.js';
6
6
  import '@sinclair/typebox';
7
7
 
8
8
  /**
@@ -1,6 +1,6 @@
1
- import { W as WSRouterDef, k as WSSubscribeOptions, l as WSUnsubscribe, i as WSConnectionState, h as WSClientConfig } from '../../types-CF-37KAG.js';
1
+ import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-2AbaW4Ie.js';
2
2
  import 'hono';
3
- import '../../token-manager-vZeqBbtA.js';
3
+ import '../../token-manager-BT5EnUAR.js';
4
4
  import '@sinclair/typebox';
5
5
 
6
6
  /**
@@ -1,6 +1,6 @@
1
- import { a as EventDef, S as SSETokenManager } from '../../token-manager-vZeqBbtA.js';
2
- import { b as WSMessageHandlers, W as WSRouterDef, a as WSHandlerConfig } from '../../types-CF-37KAG.js';
3
- export { c as WSAuthConfig, h as WSClientConfig, i as WSConnectionState, j as WSEventHandlers, d as WSHandlerAuthConfig, e as WSMessageContext, f as WSMessageHandlerFn, g as WSRawConnection, k as WSSubscribeOptions, l as WSUnsubscribe } from '../../types-CF-37KAG.js';
1
+ import { E as EventDef, S as SSETokenManager } from '../../token-manager-BT5EnUAR.js';
2
+ import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-2AbaW4Ie.js';
3
+ export { g as WSAuthConfig, d as WSClientConfig, c as WSConnectionState, h as WSEventHandlers, i as WSHandlerAuthConfig, j as WSMessageContext, k as WSMessageHandlerFn, l as WSRawConnection, a as WSSubscribeOptions, b as WSUnsubscribe } from '../../types-2AbaW4Ie.js';
4
4
  import { Server } from 'node:http';
5
5
  import '@sinclair/typebox';
6
6
  import 'hono';
@@ -1,5 +1,5 @@
1
- import { a as JobOptions, C as CompensateHandler, b as JobHandler, c as JobDef, d as JobRouterEntry, J as JobRouter } from '../boss-gXhgctn6.js';
2
- export { k as BossConfig, B as BossOptions, I as InferJobInput, f as InferJobOutput, e as JobSendOptions, g as getBoss, i as initBoss, h as isBossRunning, j as shouldClearOnStart, s as stopBoss } from '../boss-gXhgctn6.js';
1
+ import { J as JobOptions, C as CompensateHandler, a as JobHandler, b as JobDef, c as JobRouter, d as JobRouterEntry } from '../boss-D16fO2oG.js';
2
+ export { B as BossConfig, e as BossOptions, I as InferJobInput, f as InferJobOutput, g as JobSendOptions, h as getBoss, i as initBoss, j as isBossRunning, s as shouldClearOnStart, k as stopBoss } from '../boss-D16fO2oG.js';
3
3
  import * as _sinclair_typebox from '@sinclair/typebox';
4
4
  import { Static } from '@sinclair/typebox';
5
5
  import { EventDef, InferEventPayload } from '@spfn/core/event';
@@ -1,5 +1,5 @@
1
1
  import { Context, Next, MiddlewareHandler } from 'hono';
2
- import { N as NamedMiddlewareFactory, b as NamedMiddleware } from '../define-middleware-B9bFuXVU.js';
2
+ import { N as NamedMiddlewareFactory, a as NamedMiddleware } from '../define-middleware-DfDP39Nq.js';
3
3
  import { Redis, Cluster } from 'ioredis';
4
4
 
5
5
  /**
@@ -1,6 +1,6 @@
1
1
  import { Router, RouteDef } from '@spfn/core/route';
2
- import { R as RequestInterceptor, a as ResponseInterceptor, I as InferRouteInput, b as InferRouteOutput, A as ApiConfig } from '../types-7Mhoxnnt.js';
3
- export { C as CallOptions, e as CookieOptions, c as RouterInput, d as RouterOutput, f as SetCookie, S as StructuredInput } from '../types-7Mhoxnnt.js';
2
+ import { R as RequestInterceptor, a as ResponseInterceptor, I as InferRouteInput, b as InferRouteOutput, A as ApiConfig } from '../types-9oszaJqp.js';
3
+ export { C as CallOptions, c as CookieOptions, d as RouterInput, e as RouterOutput, S as SetCookie, f as StructuredInput } from '../types-9oszaJqp.js';
4
4
  import '@sinclair/typebox';
5
5
  import '@spfn/core/errors';
6
6
 
@@ -1,7 +1,7 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { HttpMethod } from '@spfn/core/route';
3
3
  import { InterceptorRule as InterceptorRule$1 } from '@spfn/core/nextjs/server';
4
- import { f as SetCookie } from '../types-7Mhoxnnt.js';
4
+ import { S as SetCookie } from '../types-9oszaJqp.js';
5
5
  import '@sinclair/typebox';
6
6
  import '@spfn/core/errors';
7
7
 
@@ -1,8 +1,8 @@
1
- import { R as RouteDef, a as Router } from '../router-HDVa6gfc.js';
2
- export { M as MergedInput, P as PaginatedResult, f as RouteAuthProfile, c as RouteBuilderContext, e as RouteContract, d as RouteHandlerFn, b as RouteInput, g as defineRouter, r as route } from '../router-HDVa6gfc.js';
1
+ import { b as RouteDef, a as Router } from '../router-BgZUDHp5.js';
2
+ export { M as MergedInput, P as PaginatedResult, R as RouteAuthProfile, c as RouteBuilderContext, d as RouteContract, e as RouteHandlerFn, f as RouteInput, g as defineRouter, r as route } from '../router-BgZUDHp5.js';
3
3
  import { Hono, MiddlewareHandler } from 'hono';
4
4
  import { HttpMethod } from './types.js';
5
- export { E as ExtractMiddlewareNames, b as NamedMiddleware, N as NamedMiddlewareFactory, d as defineMiddleware, a as defineMiddlewareFactory } from '../define-middleware-B9bFuXVU.js';
5
+ export { E as ExtractMiddlewareNames, a as NamedMiddleware, N as NamedMiddlewareFactory, d as defineMiddleware, b as defineMiddlewareFactory } from '../define-middleware-DfDP39Nq.js';
6
6
  import * as _sinclair_typebox from '@sinclair/typebox';
7
7
  import { TSchema, Kind } from '@sinclair/typebox';
8
8
  import 'hono/utils/http-status';
@@ -638,7 +638,7 @@ function registerRoutes(app, router, namedMiddlewares, collectedRoutes) {
638
638
  return routes;
639
639
  }
640
640
  function registerRoute(app, name, routeDef, namedMiddlewares) {
641
- const { method, path, input, middlewares = [], skipMiddlewares, handler } = routeDef;
641
+ const { method, path, input, interceptor, middlewares = [], skipMiddlewares, handler } = routeDef;
642
642
  if (!method || !path) {
643
643
  logger.warn(`Route "${name}" is missing method or path - skipping`, {
644
644
  method,
@@ -647,7 +647,7 @@ function registerRoute(app, name, routeDef, namedMiddlewares) {
647
647
  return null;
648
648
  }
649
649
  const wrappedHandler = async (c) => {
650
- const { context, responseMeta } = await createRouteBuilderContext(c, input || {});
650
+ const { context, responseMeta } = await createRouteBuilderContext(c, input || {}, interceptor);
651
651
  const result = await handler(context);
652
652
  if (result instanceof Response) {
653
653
  return result;
@@ -683,6 +683,8 @@ function registerRoute(app, name, routeDef, namedMiddlewares) {
683
683
  logger.debug(`\u23ED\uFE0F Skipping middleware '${middleware.name}' for route: ${method} ${path}`, { name });
684
684
  } else if (autoSkips.has(middleware.name)) {
685
685
  logger.debug(`\u23ED\uFE0F Auto-skipping middleware '${middleware.name}' for route: ${method} ${path}`, { name });
686
+ } else if (middleware.name && registeredNames.has(middleware.name)) {
687
+ logger.debug(`\u{1F504} Skipping duplicate middleware '${middleware.name}' for route: ${method} ${path}`, { name });
686
688
  } else {
687
689
  allMiddlewares.push(middleware.handler);
688
690
  registeredNames.add(middleware.name);
@@ -714,21 +716,27 @@ function registerRoute(app, name, routeDef, namedMiddlewares) {
714
716
  logger.debug(`Registered route: ${method} ${path}`, { name });
715
717
  return { method, path, name };
716
718
  }
717
- async function createRouteBuilderContext(c, input) {
719
+ function methodCarriesBody(method) {
720
+ return method !== "GET" && method !== "HEAD";
721
+ }
722
+ async function createRouteBuilderContext(c, input, interceptor) {
718
723
  const params = validateField(input.params, c.req.param(), "path parameters");
719
724
  const query = validateField(input.query, extractQueryParams(c), "query parameters");
720
725
  const headers = validateField(input.headers, extractHeaders(c), "headers");
721
726
  const cookies = validateField(input.cookies, extractCookies(c), "cookies");
722
727
  let body = {};
723
728
  let formData = {};
724
- if (input.body || input.formData) {
729
+ const injected = methodCarriesBody(c.req.method) ? interceptor : void 0;
730
+ const expectsBody = input.body ?? injected?.body;
731
+ const expectsFormData = input.formData ?? injected?.formData;
732
+ if (expectsBody || expectsFormData) {
725
733
  const contentType = c.req.header("content-type") || "";
726
- if (contentType.includes("multipart/form-data") && input.formData) {
734
+ if (contentType.includes("multipart/form-data") && expectsFormData) {
727
735
  const rawFormData = await parseFormData(c);
728
- formData = validateFormData(input.formData, rawFormData, "form data");
729
- } else if (input.body) {
736
+ formData = input.formData ? validateFormData(input.formData, rawFormData, "form data") : rawFormData;
737
+ } else if (expectsBody) {
730
738
  const rawBody = await parseJsonBody(c);
731
- body = validateField(input.body, rawBody, "request body");
739
+ body = input.body ? validateField(input.body, rawBody, "request body") : rawBody;
732
740
  }
733
741
  }
734
742
  let cachedData = null;