1- import " server-only" ;
1+ import ' server-only' ;
22/**
33 * The single (server-only) API client for the whole app, plus the SSR RPC bridge
44 * handler. Backed by https://dummyjson.com.
@@ -18,36 +18,33 @@ import "server-only";
1818 * npx @developerehsan/api-client generate \
1919 * --input ./lib/api/openapi.json --output ./lib/api/types/generated
2020 */
21- import { createTypedClient } from "@developerehsan/api-client" ;
22- import {
23- createRateLimiter ,
24- createRpcHandler ,
25- } from "@developerehsan/api-client/server" ;
26- import { generatedModules } from "./types/generated/api.modules" ;
27- import type { OperationsMap } from "./types/generated/api.types" ;
21+ import { createTypedClient } from '@developerehsan/api-client' ;
22+ import { createRateLimiter , createRpcHandler } from '@developerehsan/api-client/server' ;
23+ import { generatedModules } from './types/generated/api.modules' ;
24+ import type { OperationsMap } from './types/generated/api.types' ;
2825
2926export const api = createTypedClient < OperationsMap > ( ) (
3027 {
3128 // DummyJSON — the base URL, paths, and OpenAPI doc live ONLY here on the
3229 // server; the browser (via the RPC bridge) never sees them.
33- baseURL : " https://dummyjson.com" ,
30+ baseURL : ' https://dummyjson.com' ,
3431 dev : { logging : true } ,
35- openapi : { mode : " runtime" , validation : { enabled : true , mode : " loose" } } ,
32+ openapi : { mode : ' runtime' , validation : { enabled : true , mode : ' loose' } } ,
3633 http : {
37- adapter : " fetch" ,
34+ adapter : ' fetch' ,
3835 timeout : 12_000 ,
39- retry : { attempts : 3 , backoff : " exponential" , baseDelay : 400 } ,
36+ retry : { attempts : 3 , backoff : ' exponential' , baseDelay : 400 } ,
4037 queue : { concurrency : 6 } ,
4138 } ,
4239 hooks : {
4340 onCacheHit ( key ) {
44- console . log ( " CACHE HIT" , key ) ;
41+ console . log ( ' CACHE HIT' , key ) ;
4542 } ,
4643 onRetry ( attempt , error ) {
47- console . log ( " RETRYING" , error , { attempt } ) ;
44+ console . log ( ' RETRYING' , error , { attempt } ) ;
4845 } ,
4946 } ,
50- cache : { strategy : " stale-while-revalidate" , ttl : 30_000 } ,
47+ cache : { strategy : ' stale-while-revalidate' , ttl : 30_000 } ,
5148 modules : {
5249 auto : true ,
5350 products : {
@@ -56,18 +53,18 @@ export const api = createTypedClient<OperationsMap>()(
5653 getWithSiblings : async ( ctx , id : number ) => {
5754 const product = (
5855 await ctx . request ( {
59- method : " GET" ,
60- path : " /products/{id}" ,
56+ method : ' GET' ,
57+ path : ' /products/{id}' ,
6158 pathParams : { id } ,
6259 } )
63- ) . data as OperationsMap [ " getProductById" ] [ " response" ] ;
60+ ) . data as OperationsMap [ ' getProductById' ] [ ' response' ] ;
6461 const siblings = (
6562 await ctx . request ( {
66- method : " GET" ,
67- path : " /products/search" ,
68- query : { q : product . category ?? "" , limit : 5 } ,
63+ method : ' GET' ,
64+ path : ' /products/search' ,
65+ query : { q : product . category ?? '' , limit : 5 } ,
6966 } )
70- ) . data as OperationsMap [ " searchProducts" ] [ " response" ] ;
67+ ) . data as OperationsMap [ ' searchProducts' ] [ ' response' ] ;
7168 return { product, siblings : siblings . products } ;
7269 } ,
7370 } ,
@@ -88,11 +85,11 @@ export type Api = typeof api;
8885const limiter = createRateLimiter ( {
8986 windowMs : 10_000 ,
9087 max : 30 ,
91- keyFor : async ( ctx ) => ( await ctx . getCookie ?.( " demo_session" ) ) ?? " anon" ,
88+ keyFor : async ( ctx ) => ( await ctx . getCookie ?.( ' demo_session' ) ) ?? ' anon' ,
9289} ) ;
9390
9491/** Methods that mutate — gated behind an editor cookie by `authorize`. */
95- const WRITE_METHODS = new Set ( [ " addProduct" , " updateProduct" , " deleteProduct" ] ) ;
92+ const WRITE_METHODS = new Set ( [ ' addProduct' , ' updateProduct' , ' deleteProduct' ] ) ;
9693
9794/**
9895 * SSR RPC bridge — server side. `rpcHandler` is the single trust boundary: it
@@ -111,33 +108,23 @@ const WRITE_METHODS = new Set(["addProduct", "updateProduct", "deleteProduct"]);
111108 */
112109export const rpcHandler = createRpcHandler ( api , {
113110 expose : {
114- products : [
115- "getProductById" ,
116- "listProducts" ,
117- "searchProducts" ,
118- "addProduct" ,
119- "getWithSiblings" ,
120- ] ,
121- auth : [ "login" , "getCurrentUser" ] ,
122- users : [ "getUserById" ] ,
111+ products : [ 'getProductById' , 'listProducts' , 'searchProducts' , 'addProduct' , 'getWithSiblings' ] ,
112+ auth : [ 'login' , 'getCurrentUser' ] ,
113+ users : [ 'getUserById' ] ,
123114 } ,
124115 maxBatchSize : 10 ,
125116
126117 onRequest : limiter . onRequest ,
127118
128119 authorize : async ( ctx , call ) => {
129120 if ( ! WRITE_METHODS . has ( call . method ) ) return true ; // reads: always allowed
130- const editor = await ctx . getCookie ?.( " demo_editor" ) ;
131- return editor === "1" ; // deny → same error as "unknown method" (no enumeration)
121+ const editor = await ctx . getCookie ?.( ' demo_editor' ) ;
122+ return editor === '1' ; // deny → same error as "unknown method" (no enumeration)
132123 } ,
133124
134125 // Least-privilege: strip a product's bulky `images` array before it ships.
135126 transformResult : ( result , call ) => {
136- if (
137- call . method === "getProductById" &&
138- result &&
139- typeof result === "object"
140- ) {
127+ if ( call . method === 'getProductById' && result && typeof result === 'object' ) {
141128 const safe = { ...( result as Record < string , unknown > ) } ;
142129 delete safe . images ;
143130 return safe ;
0 commit comments