@jokio/rpc 0.7.2 → 0.7.3

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/dist/index.d.mts CHANGED
@@ -53,10 +53,10 @@ type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {
53
53
  [K in keyof T[M]]: T[M][K] extends RouteConfig | Omit<RouteConfig, "body"> ? (data: M extends "GET" ? HandlerData<Omit<InferRouteConfig<T[M][K]>, "body">, K> : HandlerData<InferRouteConfig<T[M][K]>, K>, ctx: TContext) => MaybePromise<InferRouteConfig<T[M][K]>["response"]> : never;
54
54
  } : never;
55
55
  };
56
- declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, routes: T, handlers: RouteHandlers<T, TContext> & {
56
+ declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, routes: T, config: {
57
57
  ctx?: (req: Request) => TContext;
58
58
  schemaFile?: string;
59
59
  validation?: boolean;
60
- }) => Router;
60
+ }, handlers: RouteHandlers<T, TContext>) => Router;
61
61
 
62
62
  export { type ExtractRouteParams, type InferRouteConfig, type RouteConfig, type RouteHandlers, type RouterClient, type RouterConfig, createClient, defineRoutes, registerExpressRoutes };
package/dist/index.d.ts CHANGED
@@ -53,10 +53,10 @@ type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {
53
53
  [K in keyof T[M]]: T[M][K] extends RouteConfig | Omit<RouteConfig, "body"> ? (data: M extends "GET" ? HandlerData<Omit<InferRouteConfig<T[M][K]>, "body">, K> : HandlerData<InferRouteConfig<T[M][K]>, K>, ctx: TContext) => MaybePromise<InferRouteConfig<T[M][K]>["response"]> : never;
54
54
  } : never;
55
55
  };
56
- declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, routes: T, handlers: RouteHandlers<T, TContext> & {
56
+ declare const registerExpressRoutes: <T extends Partial<RouterConfig>, TContext>(router: Router, routes: T, config: {
57
57
  ctx?: (req: Request) => TContext;
58
58
  schemaFile?: string;
59
59
  validation?: boolean;
60
- }) => Router;
60
+ }, handlers: RouteHandlers<T, TContext>) => Router;
61
61
 
62
62
  export { type ExtractRouteParams, type InferRouteConfig, type RouteConfig, type RouteHandlers, type RouterClient, type RouterConfig, createClient, defineRoutes, registerExpressRoutes };
package/dist/index.js CHANGED
@@ -91,14 +91,14 @@ var createClient = (routes, options) => {
91
91
  };
92
92
 
93
93
  // src/server.ts
94
- var createRouteHandler = (method, routes, handlers, route, validation) => {
94
+ var createRouteHandler = (method, routes, getCtx, handlers, route, validation) => {
95
95
  return async (req, res, next) => {
96
96
  try {
97
97
  if (method === "QUERY" && req.method !== "QUERY") {
98
98
  res.status(405).send("Method Not Allowed");
99
99
  return;
100
100
  }
101
- const ctx = handlers.ctx?.(req) ?? {};
101
+ const ctx = getCtx(req) ?? {};
102
102
  const routeConfig = routes[method][route];
103
103
  const data = {
104
104
  params: req.params,
@@ -114,8 +114,8 @@ var createRouteHandler = (method, routes, handlers, route, validation) => {
114
114
  }
115
115
  };
116
116
  };
117
- var registerExpressRoutes = (router, routes, handlers) => {
118
- const { schemaFile, validation = true } = handlers;
117
+ var registerExpressRoutes = (router, routes, config, handlers) => {
118
+ const { schemaFile, validation = true, ctx = () => null } = config;
119
119
  const expressMethodMap = {
120
120
  GET: "get",
121
121
  POST: "post",
@@ -131,7 +131,14 @@ var registerExpressRoutes = (router, routes, handlers) => {
131
131
  router = Object.keys(methodRoutes).reduce(
132
132
  (r, route) => r[routerMethod](
133
133
  route,
134
- createRouteHandler(methodKey, routes, handlers, route, validation)
134
+ createRouteHandler(
135
+ methodKey,
136
+ routes,
137
+ ctx,
138
+ handlers,
139
+ route,
140
+ validation
141
+ )
135
142
  ),
136
143
  router
137
144
  );
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options"],"mappings":"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","file":"index.js","sourcesContent":["import {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends Partial<RouterConfig>> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? M extends \"GET\"\n ? <K extends keyof T[M]>(\n path: K,\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : <K extends keyof T[M]>(\n path: K,\n body: InferRouteConfig<T[M][K]>[\"body\"],\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends Partial<RouterConfig>>(\n routes: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = true,\n } = options\n\n const buildUrl = (path: string, options?: any): string => {\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n return `${baseUrl}${finalPath}${queryString}`\n }\n\n const handleValidation = (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n if (!validate) return\n\n const routeConfig = (routes[method] as any)?.[path]\n if (body && routeConfig?.body) {\n routeConfig.body.parse(body)\n }\n if (options?.queryParams && routeConfig?.queryParams) {\n routeConfig.queryParams.parse(options.queryParams)\n }\n }\n\n const handleResponse = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n response: Response,\n options?: any\n ) => {\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options?.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n const routeConfig = (routes[method] as any)?.[path]\n if (routeConfig?.response?.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate && routeConfig?.response\n ? routeConfig.response.parse(json)\n : json\n }\n\n const makeRequest = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n handleValidation(method, path, body, options)\n\n const url = buildUrl(path, options)\n const fetchOptions: RequestInit = {\n method: method as string,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n }\n\n if (body !== undefined) {\n fetchOptions.body = JSON.stringify(body)\n }\n\n const response = await customFetch(url, fetchOptions)\n\n return handleResponse(method, path, response, options)\n }\n\n const client = {} as RouterClient<T>\n\n const methodHandlers = {\n GET: async (path: any, options?: any) =>\n makeRequest(\"GET\", path, undefined, options),\n QUERY: async (path: any, body: any, options?: any) =>\n makeRequest(\"QUERY\", path, body, options),\n POST: async (path: any, body: any, options?: any) =>\n makeRequest(\"POST\", path, body, options),\n PUT: async (path: any, body: any, options?: any) =>\n makeRequest(\"PUT\", path, body, options),\n PATCH: async (path: any, body: any, options?: any) =>\n makeRequest(\"PATCH\", path, body, options),\n DELETE: async (path: any, body: any, options?: any) =>\n makeRequest(\"DELETE\", path, body, options),\n }\n\n for (const method of Object.keys(routes) as Array<\n keyof T & keyof RouterConfig\n >) {\n if (method in methodHandlers) {\n ;(client as any)[method] = methodHandlers[method]\n }\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? {\n [K in keyof T[M]]: T[M][K] extends\n | RouteConfig\n | Omit<RouteConfig, \"body\">\n ? (\n data: M extends \"GET\"\n ? HandlerData<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n : HandlerData<InferRouteConfig<T[M][K]>, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n }\n : never\n}\n\nconst createRouteHandler = <\n T extends Partial<RouterConfig>,\n TContext,\n M extends keyof RouteHandlers<T, TContext>\n>(\n method: M,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n },\n route: string,\n validation: boolean\n) => {\n return async (req: Request, res: any, next: any) => {\n try {\n if (method === \"QUERY\" && req.method !== \"QUERY\") {\n res.status(405).send(\"Method Not Allowed\")\n return\n }\n\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n const routeConfig = (routes[method] as any)[route]\n\n const data = {\n params: req.params,\n ...(routeConfig?.body && { body: routeConfig.body.parse(req.body) }),\n ...(routeConfig?.queryParams && {\n queryParams: routeConfig.queryParams.parse(req.query),\n }),\n }\n\n const result = await handlers[method][route]?.(data as any, ctx)\n\n res.json(validation ? routeConfig?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }\n}\n\nexport const registerExpressRoutes = <\n T extends Partial<RouterConfig>,\n TContext\n>(\n router: Router,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFile?: string\n validation?: boolean\n }\n) => {\n const { schemaFile, validation = true } = handlers\n\n const expressMethodMap = {\n GET: \"get\",\n POST: \"post\",\n PUT: \"put\",\n PATCH: \"patch\",\n DELETE: \"delete\",\n QUERY: \"all\",\n } as const\n\n for (const [method, routerMethod] of Object.entries(expressMethodMap)) {\n const methodKey = method as keyof RouteHandlers<T, TContext>\n const methodRoutes = routes[methodKey]\n\n if (!methodRoutes) continue\n\n router = Object.keys(methodRoutes as object).reduce(\n (r, route) =>\n r[routerMethod](\n route,\n createRouteHandler(methodKey, routes, handlers, route, validation)\n ),\n router\n )\n }\n\n if (schemaFile) {\n router = router.get(\"/__routes\", async (_, res) =>\n res.contentType(\"text/plain\").send(schemaFile)\n )\n }\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n QUERY: Record<string, RouteConfig>\n POST: Record<string, RouteConfig>\n PUT: Record<string, RouteConfig>\n PATCH: Record<string, RouteConfig>\n DELETE: Record<string, RouteConfig>\n}\n\nexport type RouteConfig = {\n body: z.ZodType\n queryParams?: z.ZodType\n response: z.ZodType\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"body\">\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never\n}\n\nexport const defineRoutes = <T extends Partial<RouterConfig>>(\n routes: T\n): T => routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options"],"mappings":";;;AA2CO,IAAM,iBAAA,GAAoB,CAC/B,IAAA,EACA,MAAA,KACW;AACX,EAAA,MAAM,UAAA,uBAAiB,GAAA,EAAY;AACnC,EAAA,MAAM,YAAA,GAAe,WAAA;AACrB,EAAA,IAAI,KAAA;AAGJ,EAAA,OAAA,CAAQ,KAAA,GAAQ,YAAA,CAAa,IAAA,CAAK,IAAI,OAAO,IAAA,EAAM;AACjD,IAAA,UAAA,CAAW,GAAA,CAAI,KAAA,CAAM,CAAC,CAAC,CAAA;AAAA,EACzB;AAGA,EAAA,KAAA,MAAW,aAAa,UAAA,EAAY;AAClC,IAAA,IAAI,EAAE,aAAa,MAAA,CAAA,EAAS;AAC1B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,6BAAA,EAAgC,SAAS,CAAA,YAAA,EAAe,IAAI,CAAA,CAAA;AAAA,OAC9D;AAAA,IACF;AAAA,EACF;AAGA,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAa,CAAC,GAAG,SAAA,KAAc;AACjD,IAAA,OAAO,MAAA,CAAO,MAAA,CAAO,SAAS,CAAC,CAAA;AAAA,EACjC,CAAC,CAAA;AACH,CAAA;AAEO,IAAM,YAAA,GAAe,CAC1B,MAAA,EACA,OAAA,KACoB;AACpB,EAAA,MAAM;AAAA,IACJ,OAAA;AAAA,IACA,UAAA,GAAa,MAAM,OAAA,CAAQ,OAAA,CAAQ,EAAE,CAAA;AAAA,IACrC,OAAO,WAAA,GAAc,KAAA;AAAA,IACrB,QAAA,GAAW;AAAA,GACb,GAAI,OAAA;AAEJ,EAAA,MAAM,QAAA,GAAW,CAAC,IAAA,EAAcA,QAAAA,KAA0B;AACxD,IAAA,MAAM,WAAA,GAAcA,QAAAA,EAAS,WAAA,GACzB,GAAA,GAAM,IAAI,gBAAgBA,QAAAA,CAAQ,WAAW,CAAA,CAAE,QAAA,EAAS,GACxD,EAAA;AAEJ,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,GAC/B,iBAAA,CAAkB,IAAA,EAAMA,QAAAA,EAAS,MAAA,IAAU,EAAE,CAAA,GAC7C,IAAA;AAEJ,IAAA,OAAO,CAAA,EAAG,OAAO,CAAA,EAAG,SAAS,GAAG,WAAW,CAAA,CAAA;AAAA,EAC7C,CAAA;AAEA,EAAA,MAAM,gBAAA,GAAmB,CACvB,MAAA,EACA,IAAA,EACA,MACAA,QAAAA,KACG;AACH,IAAA,IAAI,CAAC,QAAA,EAAU;AAEf,IAAA,MAAM,WAAA,GAAe,MAAA,CAAO,MAAM,CAAA,GAAY,IAAI,CAAA;AAClD,IAAA,IAAI,IAAA,IAAQ,aAAa,IAAA,EAAM;AAC7B,MAAA,WAAA,CAAY,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,IAC7B;AACA,IAAA,IAAIA,QAAAA,EAAS,WAAA,IAAe,WAAA,EAAa,WAAA,EAAa;AACpD,MAAA,WAAA,CAAY,WAAA,CAAY,KAAA,CAAMA,QAAAA,CAAQ,WAAW,CAAA;AAAA,IACnD;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,cAAA,GAAiB,OACrB,MAAA,EACA,IAAA,EACA,UACAA,QAAAA,KACG;AACH,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,MAAM,KAAA,GAAa,MAAM,QAAA,CAAS,IAAA,EAAK;AAEvC,MAAA,IAAIA,UAAS,KAAA,EAAO;AAClB,QAAA,OAAA,CAAQ,MAAM,KAAK,CAAA;AAAA,MACrB;AAEA,MAAA,MAAM,IAAI,KAAA,CAAM,KAAA,CAAM,OAAO,CAAA;AAAA,IAC/B;AAEA,IAAA,MAAM,WAAA,GAAe,MAAA,CAAO,MAAM,CAAA,GAAY,IAAI,CAAA;AAClD,IAAA,IAAI,WAAA,EAAa,QAAA,EAAU,IAAA,KAAS,MAAA,EAAQ;AAC1C,MAAA,MAAM,SAAS,IAAA,EAAK;AACpB,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AAEjC,IAAA,OAAO,YAAY,WAAA,EAAa,QAAA,GAC5B,YAAY,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA,GAC/B,IAAA;AAAA,EACN,CAAA;AAEA,EAAA,MAAM,WAAA,GAAc,OAClB,MAAA,EACA,IAAA,EACA,MACAA,QAAAA,KACG;AACH,IAAA,gBAAA,CAAiB,MAAA,EAAQ,IAAA,EAAM,IAAA,EAAMA,QAAO,CAAA;AAE5C,IAAA,MAAM,GAAA,GAAM,QAAA,CAAS,IAAA,EAAMA,QAAO,CAAA;AAClC,IAAA,MAAM,YAAA,GAA4B;AAAA,MAChC,MAAA;AAAA,MACA,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,GAAI,MAAM,UAAA;AAAW;AACvB,KACF;AAEA,IAAA,IAAI,SAAS,MAAA,EAAW;AACtB,MAAA,YAAA,CAAa,IAAA,GAAO,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA;AAAA,IACzC;AAEA,IAAA,MAAM,QAAA,GAAW,MAAM,WAAA,CAAY,GAAA,EAAK,YAAY,CAAA;AAEpD,IAAA,OAAO,cAAA,CAAe,MAAA,EAAQ,IAAA,EAAM,QAAA,EAAUA,QAAO,CAAA;AAAA,EACvD,CAAA;AAEA,EAAA,MAAM,SAAS,EAAC;AAEhB,EAAA,MAAM,cAAA,GAAiB;AAAA,IACrB,GAAA,EAAK,OAAO,IAAA,EAAWA,QAAAA,KACrB,YAAY,KAAA,EAAO,IAAA,EAAM,QAAWA,QAAO,CAAA;AAAA,IAC7C,KAAA,EAAO,OAAO,IAAA,EAAW,IAAA,EAAWA,aAClC,WAAA,CAAY,OAAA,EAAS,IAAA,EAAM,IAAA,EAAMA,QAAO,CAAA;AAAA,IAC1C,IAAA,EAAM,OAAO,IAAA,EAAW,IAAA,EAAWA,aACjC,WAAA,CAAY,MAAA,EAAQ,IAAA,EAAM,IAAA,EAAMA,QAAO,CAAA;AAAA,IACzC,GAAA,EAAK,OAAO,IAAA,EAAW,IAAA,EAAWA,aAChC,WAAA,CAAY,KAAA,EAAO,IAAA,EAAM,IAAA,EAAMA,QAAO,CAAA;AAAA,IACxC,KAAA,EAAO,OAAO,IAAA,EAAW,IAAA,EAAWA,aAClC,WAAA,CAAY,OAAA,EAAS,IAAA,EAAM,IAAA,EAAMA,QAAO,CAAA;AAAA,IAC1C,MAAA,EAAQ,OAAO,IAAA,EAAW,IAAA,EAAWA,aACnC,WAAA,CAAY,QAAA,EAAU,IAAA,EAAM,IAAA,EAAMA,QAAO;AAAA,GAC7C;AAEA,EAAA,KAAA,MAAW,MAAA,IAAU,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA,EAEpC;AACD,IAAA,IAAI,UAAU,cAAA,EAAgB;AAC3B,MAAC,MAAA,CAAe,MAAM,CAAA,GAAI,cAAA,CAAe,MAAM,CAAA;AAAA,IAClD;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;;;AC/JA,IAAM,qBAAqB,CAKzB,MAAA,EACA,QACA,MAAA,EACA,QAAA,EACA,OACA,UAAA,KACG;AACH,EAAA,OAAO,OAAO,GAAA,EAAc,GAAA,EAAU,IAAA,KAAc;AAClD,IAAA,IAAI;AACF,MAAA,IAAI,MAAA,KAAW,OAAA,IAAW,GAAA,CAAI,MAAA,KAAW,OAAA,EAAS;AAChD,QAAA,GAAA,CAAI,MAAA,CAAO,GAAG,CAAA,CAAE,IAAA,CAAK,oBAAoB,CAAA;AACzC,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,GAAA,GAAO,MAAA,CAAO,GAAG,CAAA,IAAK,EAAC;AAC7B,MAAA,MAAM,WAAA,GAAe,MAAA,CAAO,MAAM,CAAA,CAAU,KAAK,CAAA;AAEjD,MAAA,MAAM,IAAA,GAAO;AAAA,QACX,QAAQ,GAAA,CAAI,MAAA;AAAA,QACZ,GAAI,WAAA,EAAa,IAAA,IAAQ,EAAE,IAAA,EAAM,YAAY,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,IAAI,CAAA,EAAE;AAAA,QAClE,GAAI,aAAa,WAAA,IAAe;AAAA,UAC9B,WAAA,EAAa,WAAA,CAAY,WAAA,CAAY,KAAA,CAAM,IAAI,KAAK;AAAA;AACtD,OACF;AAEA,MAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,MAAM,EAAE,KAAK,CAAA,GAAI,MAAa,GAAG,CAAA;AAE/D,MAAA,GAAA,CAAI,KAAK,UAAA,GAAa,WAAA,EAAa,SAAS,KAAA,CAAM,MAAM,IAAI,MAAM,CAAA;AAAA,IACpE,SAAS,GAAA,EAAK;AACZ,MAAA,IAAA,CAAK,GAAG,CAAA;AAAA,IACV;AAAA,EACF,CAAA;AACF,CAAA;AAEO,IAAM,qBAAA,GAAwB,CAInC,MAAA,EACA,MAAA,EACA,QAKA,QAAA,KACG;AACH,EAAA,MAAM,EAAE,UAAA,EAAY,UAAA,GAAa,MAAM,GAAA,GAAM,MAAM,MAAiB,GAAI,MAAA;AAExE,EAAA,MAAM,gBAAA,GAAmB;AAAA,IACvB,GAAA,EAAK,KAAA;AAAA,IACL,IAAA,EAAM,MAAA;AAAA,IACN,GAAA,EAAK,KAAA;AAAA,IACL,KAAA,EAAO,OAAA;AAAA,IACP,MAAA,EAAQ,QAAA;AAAA,IACR,KAAA,EAAO;AAAA,GACT;AAEA,EAAA,KAAA,MAAW,CAAC,MAAA,EAAQ,YAAY,KAAK,MAAA,CAAO,OAAA,CAAQ,gBAAgB,CAAA,EAAG;AACrE,IAAA,MAAM,SAAA,GAAY,MAAA;AAClB,IAAA,MAAM,YAAA,GAAe,OAAO,SAAS,CAAA;AAErC,IAAA,IAAI,CAAC,YAAA,EAAc;AAEnB,IAAA,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,YAAsB,CAAA,CAAE,MAAA;AAAA,MAC3C,CAAC,CAAA,EAAG,KAAA,KACF,CAAA,CAAE,YAAY,CAAA;AAAA,QACZ,KAAA;AAAA,QACA,kBAAA;AAAA,UACE,SAAA;AAAA,UACA,MAAA;AAAA,UACA,GAAA;AAAA,UACA,QAAA;AAAA,UACA,KAAA;AAAA,UACA;AAAA;AACF,OACF;AAAA,MACF;AAAA,KACF;AAAA,EACF;AAEA,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,MAAA,GAAS,MAAA,CAAO,GAAA;AAAA,MAAI,WAAA;AAAA,MAAa,OAAO,GAAG,GAAA,KACzC,GAAA,CAAI,YAAY,YAAY,CAAA,CAAE,KAAK,UAAU;AAAA,KAC/C;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;;;ACvGO,IAAM,YAAA,GAAe,CAC1B,MAAA,KACM","file":"index.js","sourcesContent":["import {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends Partial<RouterConfig>> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? M extends \"GET\"\n ? <K extends keyof T[M]>(\n path: K,\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : <K extends keyof T[M]>(\n path: K,\n body: InferRouteConfig<T[M][K]>[\"body\"],\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends Partial<RouterConfig>>(\n routes: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = true,\n } = options\n\n const buildUrl = (path: string, options?: any): string => {\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n return `${baseUrl}${finalPath}${queryString}`\n }\n\n const handleValidation = (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n if (!validate) return\n\n const routeConfig = (routes[method] as any)?.[path]\n if (body && routeConfig?.body) {\n routeConfig.body.parse(body)\n }\n if (options?.queryParams && routeConfig?.queryParams) {\n routeConfig.queryParams.parse(options.queryParams)\n }\n }\n\n const handleResponse = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n response: Response,\n options?: any\n ) => {\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options?.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n const routeConfig = (routes[method] as any)?.[path]\n if (routeConfig?.response?.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate && routeConfig?.response\n ? routeConfig.response.parse(json)\n : json\n }\n\n const makeRequest = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n handleValidation(method, path, body, options)\n\n const url = buildUrl(path, options)\n const fetchOptions: RequestInit = {\n method: method as string,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n }\n\n if (body !== undefined) {\n fetchOptions.body = JSON.stringify(body)\n }\n\n const response = await customFetch(url, fetchOptions)\n\n return handleResponse(method, path, response, options)\n }\n\n const client = {} as RouterClient<T>\n\n const methodHandlers = {\n GET: async (path: any, options?: any) =>\n makeRequest(\"GET\", path, undefined, options),\n QUERY: async (path: any, body: any, options?: any) =>\n makeRequest(\"QUERY\", path, body, options),\n POST: async (path: any, body: any, options?: any) =>\n makeRequest(\"POST\", path, body, options),\n PUT: async (path: any, body: any, options?: any) =>\n makeRequest(\"PUT\", path, body, options),\n PATCH: async (path: any, body: any, options?: any) =>\n makeRequest(\"PATCH\", path, body, options),\n DELETE: async (path: any, body: any, options?: any) =>\n makeRequest(\"DELETE\", path, body, options),\n }\n\n for (const method of Object.keys(routes) as Array<\n keyof T & keyof RouterConfig\n >) {\n if (method in methodHandlers) {\n ;(client as any)[method] = methodHandlers[method]\n }\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? {\n [K in keyof T[M]]: T[M][K] extends\n | RouteConfig\n | Omit<RouteConfig, \"body\">\n ? (\n data: M extends \"GET\"\n ? HandlerData<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n : HandlerData<InferRouteConfig<T[M][K]>, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n }\n : never\n}\n\nconst createRouteHandler = <\n T extends Partial<RouterConfig>,\n TContext,\n M extends keyof RouteHandlers<T, TContext>\n>(\n method: M,\n routes: T,\n getCtx: (req: Request) => TContext,\n handlers: RouteHandlers<T, TContext> & {},\n route: string,\n validation: boolean\n) => {\n return async (req: Request, res: any, next: any) => {\n try {\n if (method === \"QUERY\" && req.method !== \"QUERY\") {\n res.status(405).send(\"Method Not Allowed\")\n return\n }\n\n const ctx = (getCtx(req) ?? {}) as TContext\n const routeConfig = (routes[method] as any)[route]\n\n const data = {\n params: req.params,\n ...(routeConfig?.body && { body: routeConfig.body.parse(req.body) }),\n ...(routeConfig?.queryParams && {\n queryParams: routeConfig.queryParams.parse(req.query),\n }),\n }\n\n const result = await handlers[method][route]?.(data as any, ctx)\n\n res.json(validation ? routeConfig?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }\n}\n\nexport const registerExpressRoutes = <\n T extends Partial<RouterConfig>,\n TContext\n>(\n router: Router,\n routes: T,\n config: {\n ctx?: (req: Request) => TContext\n schemaFile?: string\n validation?: boolean\n },\n handlers: RouteHandlers<T, TContext>\n) => {\n const { schemaFile, validation = true, ctx = () => null as TContext } = config\n\n const expressMethodMap = {\n GET: \"get\",\n POST: \"post\",\n PUT: \"put\",\n PATCH: \"patch\",\n DELETE: \"delete\",\n QUERY: \"all\",\n } as const\n\n for (const [method, routerMethod] of Object.entries(expressMethodMap)) {\n const methodKey = method as keyof RouteHandlers<T, TContext>\n const methodRoutes = routes[methodKey]\n\n if (!methodRoutes) continue\n\n router = Object.keys(methodRoutes as object).reduce(\n (r, route) =>\n r[routerMethod](\n route,\n createRouteHandler(\n methodKey,\n routes,\n ctx,\n handlers,\n route,\n validation\n )\n ),\n router\n )\n }\n\n if (schemaFile) {\n router = router.get(\"/__routes\", async (_, res) =>\n res.contentType(\"text/plain\").send(schemaFile)\n )\n }\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n QUERY: Record<string, RouteConfig>\n POST: Record<string, RouteConfig>\n PUT: Record<string, RouteConfig>\n PATCH: Record<string, RouteConfig>\n DELETE: Record<string, RouteConfig>\n}\n\nexport type RouteConfig = {\n body: z.ZodType\n queryParams?: z.ZodType\n response: z.ZodType\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"body\">\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never\n}\n\nexport const defineRoutes = <T extends Partial<RouterConfig>>(\n routes: T\n): T => routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
package/dist/index.mjs CHANGED
@@ -89,14 +89,14 @@ var createClient = (routes, options) => {
89
89
  };
90
90
 
91
91
  // src/server.ts
92
- var createRouteHandler = (method, routes, handlers, route, validation) => {
92
+ var createRouteHandler = (method, routes, getCtx, handlers, route, validation) => {
93
93
  return async (req, res, next) => {
94
94
  try {
95
95
  if (method === "QUERY" && req.method !== "QUERY") {
96
96
  res.status(405).send("Method Not Allowed");
97
97
  return;
98
98
  }
99
- const ctx = handlers.ctx?.(req) ?? {};
99
+ const ctx = getCtx(req) ?? {};
100
100
  const routeConfig = routes[method][route];
101
101
  const data = {
102
102
  params: req.params,
@@ -112,8 +112,8 @@ var createRouteHandler = (method, routes, handlers, route, validation) => {
112
112
  }
113
113
  };
114
114
  };
115
- var registerExpressRoutes = (router, routes, handlers) => {
116
- const { schemaFile, validation = true } = handlers;
115
+ var registerExpressRoutes = (router, routes, config, handlers) => {
116
+ const { schemaFile, validation = true, ctx = () => null } = config;
117
117
  const expressMethodMap = {
118
118
  GET: "get",
119
119
  POST: "post",
@@ -129,7 +129,14 @@ var registerExpressRoutes = (router, routes, handlers) => {
129
129
  router = Object.keys(methodRoutes).reduce(
130
130
  (r, route) => r[routerMethod](
131
131
  route,
132
- createRouteHandler(methodKey, routes, handlers, route, validation)
132
+ createRouteHandler(
133
+ methodKey,
134
+ routes,
135
+ ctx,
136
+ handlers,
137
+ route,
138
+ validation
139
+ )
133
140
  ),
134
141
  router
135
142
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options"],"mappings":"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","file":"index.mjs","sourcesContent":["import {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends Partial<RouterConfig>> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? M extends \"GET\"\n ? <K extends keyof T[M]>(\n path: K,\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : <K extends keyof T[M]>(\n path: K,\n body: InferRouteConfig<T[M][K]>[\"body\"],\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends Partial<RouterConfig>>(\n routes: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = true,\n } = options\n\n const buildUrl = (path: string, options?: any): string => {\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n return `${baseUrl}${finalPath}${queryString}`\n }\n\n const handleValidation = (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n if (!validate) return\n\n const routeConfig = (routes[method] as any)?.[path]\n if (body && routeConfig?.body) {\n routeConfig.body.parse(body)\n }\n if (options?.queryParams && routeConfig?.queryParams) {\n routeConfig.queryParams.parse(options.queryParams)\n }\n }\n\n const handleResponse = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n response: Response,\n options?: any\n ) => {\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options?.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n const routeConfig = (routes[method] as any)?.[path]\n if (routeConfig?.response?.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate && routeConfig?.response\n ? routeConfig.response.parse(json)\n : json\n }\n\n const makeRequest = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n handleValidation(method, path, body, options)\n\n const url = buildUrl(path, options)\n const fetchOptions: RequestInit = {\n method: method as string,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n }\n\n if (body !== undefined) {\n fetchOptions.body = JSON.stringify(body)\n }\n\n const response = await customFetch(url, fetchOptions)\n\n return handleResponse(method, path, response, options)\n }\n\n const client = {} as RouterClient<T>\n\n const methodHandlers = {\n GET: async (path: any, options?: any) =>\n makeRequest(\"GET\", path, undefined, options),\n QUERY: async (path: any, body: any, options?: any) =>\n makeRequest(\"QUERY\", path, body, options),\n POST: async (path: any, body: any, options?: any) =>\n makeRequest(\"POST\", path, body, options),\n PUT: async (path: any, body: any, options?: any) =>\n makeRequest(\"PUT\", path, body, options),\n PATCH: async (path: any, body: any, options?: any) =>\n makeRequest(\"PATCH\", path, body, options),\n DELETE: async (path: any, body: any, options?: any) =>\n makeRequest(\"DELETE\", path, body, options),\n }\n\n for (const method of Object.keys(routes) as Array<\n keyof T & keyof RouterConfig\n >) {\n if (method in methodHandlers) {\n ;(client as any)[method] = methodHandlers[method]\n }\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? {\n [K in keyof T[M]]: T[M][K] extends\n | RouteConfig\n | Omit<RouteConfig, \"body\">\n ? (\n data: M extends \"GET\"\n ? HandlerData<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n : HandlerData<InferRouteConfig<T[M][K]>, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n }\n : never\n}\n\nconst createRouteHandler = <\n T extends Partial<RouterConfig>,\n TContext,\n M extends keyof RouteHandlers<T, TContext>\n>(\n method: M,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n },\n route: string,\n validation: boolean\n) => {\n return async (req: Request, res: any, next: any) => {\n try {\n if (method === \"QUERY\" && req.method !== \"QUERY\") {\n res.status(405).send(\"Method Not Allowed\")\n return\n }\n\n const ctx = (handlers.ctx?.(req) ?? {}) as TContext\n const routeConfig = (routes[method] as any)[route]\n\n const data = {\n params: req.params,\n ...(routeConfig?.body && { body: routeConfig.body.parse(req.body) }),\n ...(routeConfig?.queryParams && {\n queryParams: routeConfig.queryParams.parse(req.query),\n }),\n }\n\n const result = await handlers[method][route]?.(data as any, ctx)\n\n res.json(validation ? routeConfig?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }\n}\n\nexport const registerExpressRoutes = <\n T extends Partial<RouterConfig>,\n TContext\n>(\n router: Router,\n routes: T,\n handlers: RouteHandlers<T, TContext> & {\n ctx?: (req: Request) => TContext\n schemaFile?: string\n validation?: boolean\n }\n) => {\n const { schemaFile, validation = true } = handlers\n\n const expressMethodMap = {\n GET: \"get\",\n POST: \"post\",\n PUT: \"put\",\n PATCH: \"patch\",\n DELETE: \"delete\",\n QUERY: \"all\",\n } as const\n\n for (const [method, routerMethod] of Object.entries(expressMethodMap)) {\n const methodKey = method as keyof RouteHandlers<T, TContext>\n const methodRoutes = routes[methodKey]\n\n if (!methodRoutes) continue\n\n router = Object.keys(methodRoutes as object).reduce(\n (r, route) =>\n r[routerMethod](\n route,\n createRouteHandler(methodKey, routes, handlers, route, validation)\n ),\n router\n )\n }\n\n if (schemaFile) {\n router = router.get(\"/__routes\", async (_, res) =>\n res.contentType(\"text/plain\").send(schemaFile)\n )\n }\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n QUERY: Record<string, RouteConfig>\n POST: Record<string, RouteConfig>\n PUT: Record<string, RouteConfig>\n PATCH: Record<string, RouteConfig>\n DELETE: Record<string, RouteConfig>\n}\n\nexport type RouteConfig = {\n body: z.ZodType\n queryParams?: z.ZodType\n response: z.ZodType\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"body\">\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never\n}\n\nexport const defineRoutes = <T extends Partial<RouterConfig>>(\n routes: T\n): T => routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
1
+ {"version":3,"sources":["../src/client.ts","../src/server.ts","../src/types.ts"],"names":["options"],"mappings":"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","file":"index.mjs","sourcesContent":["import {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for client options with optional params\ntype ClientOptions<TConfig, K> = Omit<TConfig, \"response\"> & {\n params?: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouterClient<T extends Partial<RouterConfig>> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? M extends \"GET\"\n ? <K extends keyof T[M]>(\n path: K,\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : <K extends keyof T[M]>(\n path: K,\n body: InferRouteConfig<T[M][K]>[\"body\"],\n options?: ClientOptions<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n ) => Promise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n}\n\ntype FetchFunction = (url: string, options: RequestInit) => Promise<Response>\n\ntype CreateClientOptions = {\n baseUrl: string\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n fetch?: FetchFunction\n validate?: boolean\n debug?: boolean\n}\n\n/**\n * Replaces path parameters with their values.\n * @param path - The path template with parameters (e.g., \"/:id/test/:name/info\")\n * @param params - The parameter values (e.g., {id: \"123\", name: \"434\"})\n * @returns The resolved path (e.g., \"/123/test/434/info\")\n * @throws Error if a required parameter is missing\n */\nexport const replacePathParams = (\n path: string,\n params: Record<string, string | number>\n): string => {\n const paramNames = new Set<string>()\n const paramPattern = /:([^/]+)/g\n let match: RegExpExecArray | null\n\n // Extract all parameter names from the path\n while ((match = paramPattern.exec(path)) !== null) {\n paramNames.add(match[1])\n }\n\n // Check if all required parameters are provided\n for (const paramName of paramNames) {\n if (!(paramName in params)) {\n throw new Error(\n `Missing required parameter: \"${paramName}\" for path \"${path}\"`\n )\n }\n }\n\n // Replace all parameters with their values\n return path.replace(/:([^/]+)/g, (_, paramName) => {\n return String(params[paramName])\n })\n}\n\nexport const createClient = <T extends Partial<RouterConfig>>(\n routes: T,\n options: CreateClientOptions\n): RouterClient<T> => {\n const {\n baseUrl,\n getHeaders = () => Promise.resolve({}),\n fetch: customFetch = fetch,\n validate = true,\n } = options\n\n const buildUrl = (path: string, options?: any): string => {\n const queryString = options?.queryParams\n ? \"?\" + new URLSearchParams(options.queryParams).toString()\n : \"\"\n\n const finalPath = path.includes(\":\")\n ? replacePathParams(path, options?.params ?? {})\n : path\n\n return `${baseUrl}${finalPath}${queryString}`\n }\n\n const handleValidation = (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n if (!validate) return\n\n const routeConfig = (routes[method] as any)?.[path]\n if (body && routeConfig?.body) {\n routeConfig.body.parse(body)\n }\n if (options?.queryParams && routeConfig?.queryParams) {\n routeConfig.queryParams.parse(options.queryParams)\n }\n }\n\n const handleResponse = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n response: Response,\n options?: any\n ) => {\n if (!response.ok) {\n const error: any = await response.json()\n\n if (options?.debug) {\n console.debug(error)\n }\n\n throw new Error(error.message)\n }\n\n const routeConfig = (routes[method] as any)?.[path]\n if (routeConfig?.response?.type === \"void\") {\n await response.text()\n return\n }\n\n const json = await response.json()\n\n return validate && routeConfig?.response\n ? routeConfig.response.parse(json)\n : json\n }\n\n const makeRequest = async (\n method: keyof T & keyof RouterConfig,\n path: string,\n body?: any,\n options?: any\n ) => {\n handleValidation(method, path, body, options)\n\n const url = buildUrl(path, options)\n const fetchOptions: RequestInit = {\n method: method as string,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(await getHeaders()),\n },\n }\n\n if (body !== undefined) {\n fetchOptions.body = JSON.stringify(body)\n }\n\n const response = await customFetch(url, fetchOptions)\n\n return handleResponse(method, path, response, options)\n }\n\n const client = {} as RouterClient<T>\n\n const methodHandlers = {\n GET: async (path: any, options?: any) =>\n makeRequest(\"GET\", path, undefined, options),\n QUERY: async (path: any, body: any, options?: any) =>\n makeRequest(\"QUERY\", path, body, options),\n POST: async (path: any, body: any, options?: any) =>\n makeRequest(\"POST\", path, body, options),\n PUT: async (path: any, body: any, options?: any) =>\n makeRequest(\"PUT\", path, body, options),\n PATCH: async (path: any, body: any, options?: any) =>\n makeRequest(\"PATCH\", path, body, options),\n DELETE: async (path: any, body: any, options?: any) =>\n makeRequest(\"DELETE\", path, body, options),\n }\n\n for (const method of Object.keys(routes) as Array<\n keyof T & keyof RouterConfig\n >) {\n if (method in methodHandlers) {\n ;(client as any)[method] = methodHandlers[method]\n }\n }\n\n return client\n}\n","import type { Request, Router } from \"express\"\nimport {\n type ExtractRouteParams,\n type InferRouteConfig,\n type RouteConfig,\n type RouterConfig,\n} from \"./types\"\n\n// Reusable type for sync or async responses\ntype MaybePromise<T> = Promise<T> | T\n\n// Reusable type for handler data with params\ntype HandlerData<TConfig, K> = Omit<TConfig, \"response\"> & {\n params: K extends string ? ExtractRouteParams<K> : unknown\n}\n\nexport type RouteHandlers<T extends Partial<RouterConfig>, TContext> = {\n [M in keyof T & keyof RouterConfig]: T[M] extends Record<string, any>\n ? {\n [K in keyof T[M]]: T[M][K] extends\n | RouteConfig\n | Omit<RouteConfig, \"body\">\n ? (\n data: M extends \"GET\"\n ? HandlerData<Omit<InferRouteConfig<T[M][K]>, \"body\">, K>\n : HandlerData<InferRouteConfig<T[M][K]>, K>,\n ctx: TContext\n ) => MaybePromise<InferRouteConfig<T[M][K]>[\"response\"]>\n : never\n }\n : never\n}\n\nconst createRouteHandler = <\n T extends Partial<RouterConfig>,\n TContext,\n M extends keyof RouteHandlers<T, TContext>\n>(\n method: M,\n routes: T,\n getCtx: (req: Request) => TContext,\n handlers: RouteHandlers<T, TContext> & {},\n route: string,\n validation: boolean\n) => {\n return async (req: Request, res: any, next: any) => {\n try {\n if (method === \"QUERY\" && req.method !== \"QUERY\") {\n res.status(405).send(\"Method Not Allowed\")\n return\n }\n\n const ctx = (getCtx(req) ?? {}) as TContext\n const routeConfig = (routes[method] as any)[route]\n\n const data = {\n params: req.params,\n ...(routeConfig?.body && { body: routeConfig.body.parse(req.body) }),\n ...(routeConfig?.queryParams && {\n queryParams: routeConfig.queryParams.parse(req.query),\n }),\n }\n\n const result = await handlers[method][route]?.(data as any, ctx)\n\n res.json(validation ? routeConfig?.response.parse(result) : result)\n } catch (err) {\n next(err)\n }\n }\n}\n\nexport const registerExpressRoutes = <\n T extends Partial<RouterConfig>,\n TContext\n>(\n router: Router,\n routes: T,\n config: {\n ctx?: (req: Request) => TContext\n schemaFile?: string\n validation?: boolean\n },\n handlers: RouteHandlers<T, TContext>\n) => {\n const { schemaFile, validation = true, ctx = () => null as TContext } = config\n\n const expressMethodMap = {\n GET: \"get\",\n POST: \"post\",\n PUT: \"put\",\n PATCH: \"patch\",\n DELETE: \"delete\",\n QUERY: \"all\",\n } as const\n\n for (const [method, routerMethod] of Object.entries(expressMethodMap)) {\n const methodKey = method as keyof RouteHandlers<T, TContext>\n const methodRoutes = routes[methodKey]\n\n if (!methodRoutes) continue\n\n router = Object.keys(methodRoutes as object).reduce(\n (r, route) =>\n r[routerMethod](\n route,\n createRouteHandler(\n methodKey,\n routes,\n ctx,\n handlers,\n route,\n validation\n )\n ),\n router\n )\n }\n\n if (schemaFile) {\n router = router.get(\"/__routes\", async (_, res) =>\n res.contentType(\"text/plain\").send(schemaFile)\n )\n }\n\n return router\n}\n","import type z from \"zod\"\n\nexport type RouterConfig = {\n GET: Record<string, Omit<RouteConfig, \"body\">>\n QUERY: Record<string, RouteConfig>\n POST: Record<string, RouteConfig>\n PUT: Record<string, RouteConfig>\n PATCH: Record<string, RouteConfig>\n DELETE: Record<string, RouteConfig>\n}\n\nexport type RouteConfig = {\n body: z.ZodType\n queryParams?: z.ZodType\n response: z.ZodType\n}\n\nexport type InferRouteConfig<\n T extends RouteConfig | Omit<RouteConfig, \"body\">\n> = {\n [K in keyof T]: T[K] extends z.ZodType ? z.infer<T[K]> : never\n}\n\nexport const defineRoutes = <T extends Partial<RouterConfig>>(\n routes: T\n): T => routes\n\n// Extract path parameters from route string\n// e.g., \"/user/:id\" -> { id: string }, \"/user/:id/info\" -> { id: string }, \"/user/:id/post/:postId\" -> { id: string, postId: string }\nexport type ExtractRouteParams<T extends string> =\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? Rest extends `:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : Rest extends `${string}/:${string}`\n ? {\n [K in Param | keyof ExtractRouteParams<`/${Rest}`>]: string\n }\n : { [K in Param]: string }\n : T extends `${infer _Start}:${infer Param}`\n ? { [K in Param]: string }\n : Record<string, never>\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jokio/rpc",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Type-safe RPC framework with Zod validation for Express and TypeScript",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",