appos 0.3.2-0 → 0.3.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.
Files changed (158) hide show
  1. package/dist/bin/auth-schema-CcqAJY9P.mjs +2 -0
  2. package/dist/bin/better-sqlite3-CuQ3hsWl.mjs +2 -0
  3. package/dist/bin/bun-sql-DGeo-s_M.mjs +2 -0
  4. package/dist/bin/cache-3oO07miM.mjs +2 -0
  5. package/dist/bin/chunk-l9p7A9gZ.mjs +2 -0
  6. package/dist/bin/cockroach-BaICwY7N.mjs +2 -0
  7. package/dist/bin/database-CaysWPpa.mjs +2 -0
  8. package/dist/bin/esm-BvsccvmM.mjs +2 -0
  9. package/dist/bin/esm-CGKzJ7Am.mjs +3 -0
  10. package/dist/bin/event-DnSe3eh0.mjs +8 -0
  11. package/dist/bin/extract-blob-metadata-iqwTl2ft.mjs +170 -0
  12. package/dist/bin/generate-image-variant-Lyx0vhM6.mjs +2 -0
  13. package/dist/bin/generate-preview-0MrKxslA.mjs +2 -0
  14. package/dist/bin/libsql-DQJrZsU9.mjs +2 -0
  15. package/dist/bin/logger-BAGZLUzj.mjs +2 -0
  16. package/dist/bin/main.mjs +1201 -190
  17. package/dist/bin/migrator-B7iNKM8N.mjs +2 -0
  18. package/dist/bin/migrator-BKE1cSQQ.mjs +2 -0
  19. package/dist/bin/migrator-BXcbc9zs.mjs +2 -0
  20. package/dist/bin/migrator-B_XhRWZC.mjs +8 -0
  21. package/dist/bin/migrator-Bz52Gtr8.mjs +2 -0
  22. package/dist/bin/migrator-C7W-cZHB.mjs +2 -0
  23. package/dist/bin/migrator-CEnKyGSW.mjs +2 -0
  24. package/dist/bin/migrator-CHzIIl5X.mjs +2 -0
  25. package/dist/bin/migrator-CR-rjZdM.mjs +2 -0
  26. package/dist/bin/migrator-CjIr1ZCx.mjs +8 -0
  27. package/dist/bin/migrator-Cuubh2dg.mjs +2 -0
  28. package/dist/bin/migrator-D8m-ORbr.mjs +8 -0
  29. package/dist/bin/migrator-DBFwrhZH.mjs +2 -0
  30. package/dist/bin/migrator-DLmhW9u_.mjs +2 -0
  31. package/dist/bin/migrator-DLoHx807.mjs +4 -0
  32. package/dist/bin/migrator-DtN_iS87.mjs +2 -0
  33. package/dist/bin/migrator-Yc57lb3w.mjs +2 -0
  34. package/dist/bin/migrator-cEVXH3xC.mjs +2 -0
  35. package/dist/bin/migrator-hWi-sYIq.mjs +2 -0
  36. package/dist/bin/mysql2-DufFWkj4.mjs +2 -0
  37. package/dist/bin/neon-serverless-5a4h2VFz.mjs +2 -0
  38. package/dist/bin/node-CiOp4xrR.mjs +22 -0
  39. package/dist/bin/node-mssql-DvZGaUkB.mjs +322 -0
  40. package/dist/bin/node-postgres-BqbJVBQY.mjs +2 -0
  41. package/dist/bin/node-postgres-DnhRTTO8.mjs +2 -0
  42. package/dist/bin/open-0ksnL0S8.mjs +2 -0
  43. package/dist/bin/pdf-sUYeFPr4.mjs +14 -0
  44. package/dist/bin/pg-CaH8ptj-.mjs +2 -0
  45. package/dist/bin/pg-core-BLTZt9AH.mjs +8 -0
  46. package/dist/bin/pg-core-CGzidKaA.mjs +2 -0
  47. package/dist/bin/pglite-BJB9z7Ju.mjs +2 -0
  48. package/dist/bin/planetscale-serverless-H3RfLlMK.mjs +13 -0
  49. package/dist/bin/postgres-js-DuOf1eWm.mjs +2 -0
  50. package/dist/bin/purge-attachment-DQXpTtTx.mjs +2 -0
  51. package/dist/bin/purge-audit-logs-BEt2J2gD.mjs +2 -0
  52. package/dist/bin/{purge-unattached-blobs-Duvv8Izd.mjs → purge-unattached-blobs-DOmk4ddJ.mjs} +1 -1
  53. package/dist/bin/query-builder-DSRrR6X_.mjs +8 -0
  54. package/dist/bin/query-builder-V8-LDhvA.mjs +3 -0
  55. package/dist/bin/session-CdB1A-LB.mjs +14 -0
  56. package/dist/bin/session-Cl2e-_i8.mjs +8 -0
  57. package/dist/bin/singlestore-COft6TlR.mjs +8 -0
  58. package/dist/bin/sql-D-eKV1Dn.mjs +2 -0
  59. package/dist/bin/sqlite-cloud-Co9jOn5G.mjs +2 -0
  60. package/dist/bin/sqlite-proxy-Cpu78gJF.mjs +2 -0
  61. package/dist/bin/src-C-oXmCzx.mjs +6 -0
  62. package/dist/bin/table-3zUpWkMg.mjs +2 -0
  63. package/dist/bin/track-db-changes-DWyY5jXm.mjs +2 -0
  64. package/dist/bin/utils-CyoeCJlf.mjs +2 -0
  65. package/dist/bin/utils-EoqYQKy1.mjs +2 -0
  66. package/dist/bin/utils-bsypyqPl.mjs +2 -0
  67. package/dist/bin/vercel-postgres-HWL6xtqi.mjs +2 -0
  68. package/dist/bin/workflow-zxHDyfLq.mjs +2 -0
  69. package/dist/bin/youch-handler-DrYdbUhe.mjs +2 -0
  70. package/dist/bin/zod-MJjkEkRY.mjs +24 -0
  71. package/dist/exports/api/_virtual/rolldown_runtime.mjs +36 -1
  72. package/dist/exports/api/app-context.mjs +24 -1
  73. package/dist/exports/api/auth-schema.mjs +373 -1
  74. package/dist/exports/api/auth.d.mts +4 -0
  75. package/dist/exports/api/auth.mjs +188 -1
  76. package/dist/exports/api/cache.d.mts +2 -2
  77. package/dist/exports/api/cache.mjs +28 -1
  78. package/dist/exports/api/config.mjs +72 -1
  79. package/dist/exports/api/constants.mjs +92 -1
  80. package/dist/exports/api/container.mjs +49 -1
  81. package/dist/exports/api/database.mjs +218 -1
  82. package/dist/exports/api/event.mjs +236 -1
  83. package/dist/exports/api/i18n.mjs +45 -1
  84. package/dist/exports/api/index.mjs +20 -1
  85. package/dist/exports/api/instrumentation.mjs +40 -1
  86. package/dist/exports/api/logger.mjs +26 -1
  87. package/dist/exports/api/mailer.mjs +37 -1
  88. package/dist/exports/api/middleware.mjs +73 -1
  89. package/dist/exports/api/openapi.mjs +507 -1
  90. package/dist/exports/api/orm.mjs +43 -1
  91. package/dist/exports/api/otel.mjs +56 -1
  92. package/dist/exports/api/redis.mjs +41 -1
  93. package/dist/exports/api/storage-schema.mjs +72 -1
  94. package/dist/exports/api/storage.mjs +833 -1
  95. package/dist/exports/api/web/auth.mjs +17 -1
  96. package/dist/exports/api/workflow.mjs +196 -1
  97. package/dist/exports/api/workflows/_virtual/rolldown_runtime.mjs +36 -1
  98. package/dist/exports/api/workflows/api/auth-schema.mjs +373 -1
  99. package/dist/exports/api/workflows/api/auth.d.mts +4 -0
  100. package/dist/exports/api/workflows/api/cache.d.mts +2 -2
  101. package/dist/exports/api/workflows/api/event.mjs +126 -1
  102. package/dist/exports/api/workflows/api/redis.mjs +3 -1
  103. package/dist/exports/api/workflows/api/workflow.mjs +135 -1
  104. package/dist/exports/api/workflows/constants.mjs +23 -1
  105. package/dist/exports/api/workflows/extract-blob-metadata.mjs +132 -1
  106. package/dist/exports/api/workflows/generate-image-variant.d.mts +2 -2
  107. package/dist/exports/api/workflows/generate-image-variant.mjs +118 -1
  108. package/dist/exports/api/workflows/generate-preview.mjs +160 -1
  109. package/dist/exports/api/workflows/index.mjs +3 -1
  110. package/dist/exports/api/workflows/purge-attachment.mjs +34 -1
  111. package/dist/exports/api/workflows/purge-audit-logs.mjs +47 -1
  112. package/dist/exports/api/workflows/purge-unattached-blobs.mjs +46 -1
  113. package/dist/exports/api/workflows/track-db-changes.mjs +110 -1
  114. package/dist/exports/cli/_virtual/rolldown_runtime.mjs +36 -1
  115. package/dist/exports/cli/api/auth-schema.mjs +373 -1
  116. package/dist/exports/cli/api/auth.d.mts +4 -0
  117. package/dist/exports/cli/api/cache.d.mts +2 -2
  118. package/dist/exports/cli/api/event.mjs +126 -1
  119. package/dist/exports/cli/api/redis.mjs +3 -1
  120. package/dist/exports/cli/api/workflow.mjs +135 -1
  121. package/dist/exports/cli/api/workflows/extract-blob-metadata.mjs +132 -1
  122. package/dist/exports/cli/api/workflows/generate-image-variant.mjs +118 -1
  123. package/dist/exports/cli/api/workflows/generate-preview.mjs +160 -1
  124. package/dist/exports/cli/api/workflows/purge-attachment.mjs +34 -1
  125. package/dist/exports/cli/api/workflows/purge-audit-logs.mjs +47 -1
  126. package/dist/exports/cli/api/workflows/purge-unattached-blobs.mjs +46 -1
  127. package/dist/exports/cli/api/workflows/track-db-changes.mjs +110 -1
  128. package/dist/exports/cli/command.d.mts +2 -0
  129. package/dist/exports/cli/command.mjs +43 -1
  130. package/dist/exports/cli/constants.mjs +23 -1
  131. package/dist/exports/cli/index.mjs +3 -1
  132. package/dist/exports/devtools/index.js +4 -1
  133. package/dist/exports/tests/api/auth.d.mts +4 -0
  134. package/dist/exports/tests/api/cache.d.mts +2 -2
  135. package/dist/exports/tests/api/middleware/i18n.mjs +1 -1
  136. package/dist/exports/tests/api/middleware/youch-handler.mjs +1 -1
  137. package/dist/exports/tests/api/openapi.mjs +1 -1
  138. package/dist/exports/tests/api/server.mjs +1 -1
  139. package/dist/exports/tests/api/storage.d.mts +4 -4
  140. package/dist/exports/tests/constants.mjs +1 -1
  141. package/dist/exports/vendors/date.js +1 -1
  142. package/dist/exports/vendors/toolkit.js +1 -1
  143. package/dist/exports/vendors/zod.js +1 -1
  144. package/dist/exports/vitest/globals.mjs +1 -1
  145. package/dist/exports/web/auth.js +75 -1
  146. package/dist/exports/web/i18n.js +45 -1
  147. package/dist/exports/web/index.js +8 -1
  148. package/package.json +19 -18
  149. package/dist/bin/auth-schema-Va0CYicu.mjs +0 -2
  150. package/dist/bin/event-8JibGFH_.mjs +0 -2
  151. package/dist/bin/extract-blob-metadata-DjPfHtQ2.mjs +0 -2
  152. package/dist/bin/generate-image-variant-D5VDFyWj.mjs +0 -2
  153. package/dist/bin/generate-preview-Dssw7w5U.mjs +0 -2
  154. package/dist/bin/purge-attachment-BBPzIxwt.mjs +0 -2
  155. package/dist/bin/purge-audit-logs-BeZy3IFM.mjs +0 -2
  156. package/dist/bin/track-db-changes-CFykw_YO.mjs +0 -2
  157. package/dist/bin/workflow-BNUZrj4F.mjs +0 -2
  158. package/dist/bin/youch-handler-BadUgHb0.mjs +0 -2
@@ -1 +1,507 @@
1
- import{defineAppContext as e}from"./app-context.mjs";import{APPOS_DIR as t,FILE_EXT as n,PUBLIC_DIR as r,ROUTES_DIR as i}from"./constants.mjs";import{z as a}from"zod";import{join as o,resolve as s}from"node:path";import{access as c,mkdir as l,writeFile as u}from"node:fs/promises";import{remixRoutesOptionAdapter as d}from"@react-router/remix-routes-option-adapter";import{isEmpty as f}from"es-toolkit/compat";import{flatRoutes as p}from"remix-flat-routes";import{createDocument as m}from"zod-openapi";const h=s(r,`openapi`);function g(e){let t={};for(let[n,r]of Object.entries(e)){let e=Number(n),i=_(e);i&&(t[i]=t=>{let n=r.schema;if(n instanceof a.ZodNull||t==null)return{__response:!0,status:e,data:t??null};try{let r=n;if(n instanceof a.ZodObject)r=n.strict();else if(n instanceof a.ZodArray){let e=n.element;e instanceof a.ZodObject&&(r=a.array(e.strict()))}return{__response:!0,status:e,data:r.parse(t)}}catch(n){throw n instanceof a.ZodError?Error(`Response validation failed for status ${e}: ${JSON.stringify(n.issues,null,2)}\n\nData provided: ${JSON.stringify(t,null,2)}`):n}})}return t.status=(t,n)=>{let r=Number(t),i=e[t];if(!i)return{__response:!0,status:r,data:n??null};let o=i.schema;if(!o||o instanceof a.ZodNull||n==null)return{__response:!0,status:r,data:n??null};try{let e=o;if(o instanceof a.ZodObject)e=o.strict();else if(o instanceof a.ZodArray){let t=o.element;t instanceof a.ZodObject&&(e=a.array(t.strict()))}return{__response:!0,status:r,data:e.parse(n)}}catch(e){throw e instanceof a.ZodError?Error(`Response validation failed for status ${r}: ${JSON.stringify(e.issues,null,2)}\n\nData provided: ${JSON.stringify(n,null,2)}`):e}},t}function _(e){return{200:`ok`,201:`created`,202:`accepted`,204:`noContent`,400:`badRequest`,401:`unauthorized`,403:`forbidden`,404:`notFound`,409:`conflict`,422:`unprocessableEntity`,500:`internalServerError`}[e]||null}function v(e){return e}function y(e,t){return e?e.parse(t):t}function b(e,t){let n={};for(let[e,r]of t.searchParams.entries()){let t=n[e];t===void 0?n[e]=r:Array.isArray(t)?t.push(r):n[e]=[t,r]}return e?e.parse(n):n}function x(e,t){let n=t.body;return e?e.parse(n):n}function S(e,t){let n=t.headers;return e?e.parse(n):n}function C(e){switch(e){case`params`:case`query`:case`headers`:return 400;case`body`:return 422;default:return 400}}function w(e,t){let n=C(t);return{__response:!0,status:n,data:{error:`validation_error`,status:n,detail:`Request validation failed`,errors:e.issues.map(e=>({field:e.path.join(`.`),message:e.message,received:`received`in e?e.received:e.input}))}}}function T(t,n){return async(r,i,o)=>{try{let n=new URL(r.url||`/`,`http://${r.headers.host}`),a=y(t.params,r.params),o=b(t.query,n),s=x(t.requestBody,r),c=S(t.headers,r),l=g(t.responses),u=await e({apiKey:r.apiKey,container:r.app.locals.container,request:r,fetchSession:!1}),d=await t.handler({ctx:u,body:s,headers:c,params:a,query:o,request:r,response:l});if(d&&typeof d==`object`&&`__response`in d){let e=d;e.data===null||e.data===void 0?i.status(e.status).end():i.status(e.status).json(e.data)}else i.status(200).json(d)}catch(e){if(e instanceof a.ZodError){let t=w(e,n===`GET`||n===`HEAD`?`query`:`body`);i.status(t.status).json(t.data)}else o(e)}}}function E(e){let t=[];for(let n of[`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]){let r=e[n];if(!r)continue;let i={};r.params&&(i.params=r.params),r.query&&(i.query=r.query),r.requestBody&&(i.body=r.requestBody),r.headers&&(i.headers=r.headers);let a={};for(let[e,t]of Object.entries(r.responses))a[Number(e)]={description:t.description,content:{"application/json":{schema:t.schema}}};t.push({method:n.toLowerCase(),summary:r.summary,description:r.description,request:Object.keys(i).length>0?i:void 0,responses:a})}return t}function D(e){let t={};for(let n of[`GET`,`HEAD`,`POST`,`PUT`,`PATCH`,`DELETE`]){let r=e[n];r&&(t[n]=T(r,n))}return{handlers:t,openAPISpec:E(e)}}function O(e){return t=>{let n=e(t);return{info:n.info,servers:n.servers,openapi:`3.1.0`}}}function k(e){return e.match(/^\/(v\d+)/)?.[1]}function A(e){return e.replace(/:(\w+)/g,`{$1}`)}function j(e,t){let n=A(e),r={summary:t.summary,description:t.description};(t.request?.params||t.request?.query||t.request?.headers)&&(r.requestParams={},t.request.params&&(r.requestParams.path=t.request.params),t.request.query&&(r.requestParams.query=t.request.query),t.request.headers&&(r.requestParams.header=t.request.headers)),t.request?.body&&(r.requestBody={content:{"application/json":{schema:t.request.body}}}),r.responses={};for(let[e,n]of Object.entries(t.responses))r.responses[e]={description:n.description,content:{"application/json":{schema:n.content[`application/json`].schema}}};return{path:n,config:r}}function M(e,t,n){let r={};for(let n of e){if(n.version!==t)continue;let e=n.path.replace(`/${t}`,``)||`/`;for(let t of n.openAPISpec){let{path:n,config:i}=j(e,t);r[n]||(r[n]={}),r[n][t.method]=i}}return m({openapi:`3.1.0`,info:n?.info||{title:`API ${t.toUpperCase()}`,version:t.replace(`v`,``),description:`OpenAPI specification for ${t} API`},servers:n?.servers||[{url:`http://localhost:8000/${t}`,description:process.env.NODE_ENV===`production`?`Production server`:`Development server`}],paths:r})}async function N(e){let n=s(t,i);try{await c(n)}catch(t){return e.logger.error({error:t},`OpenAPI routes directory not found`),[]}let r=await d(e=>p(i,e,{appDir:t,ignoredRouteFiles:[`**/.*`,`**/*.{spec,test}.{ts,tsx}`,`**/*-????????.{js,ts}`]})),a=[];for(let[n,i]of Object.entries(r)){let n=s(t,i.file).replace(/.ts$/,process.env.NODE_ENV===`production`?`.js`:`.ts`);try{let r=(await import(n)).default;if(!r||f(r)||/\/openapi.(j|t)s$/.test(n))continue;if(!r?.handlers&&!r?.openAPISpec){e.logger.warn(`Missing default export with 'defineOpenAPI' for '${t}/${i.file}'`);continue}let o=`/${i.path}`;a.push({path:o,filePath:n,handlers:r.handlers,openAPISpec:r.openAPISpec,version:k(o)})}catch(t){e.logger.error(t,`Error loading route ${i.file}:`)}}return a}function P(e,t){for(let n of t)for(let[t,r]of Object.entries(n.handlers))e[t.toLowerCase()](n.path,r)}async function F(e,r){let a=s(t,i,`${r}+/openapi.${n}`);try{let t=(await import(a)).default;return t?t(e):void 0}catch{return}}async function I(e,t){let n=[...new Set(t.map(e=>e.version).filter(Boolean))];n.length>0&&await l(h,{recursive:!0});for(let r of n){let n=M(t,r,await F(e,r));await u(o(h,`${r}.json`),JSON.stringify(n,null,2),`utf-8`)}}async function L(e){let t=await N(e.locals.container);P(e,t),process.env.NODE_ENV!==`production`&&await I(e.locals.container,t)}export{D as defineOpenAPI,O as defineOpenAPIConfig,v as defineOpenAPIEndpoint,g as defineTypedResponses,M as generateOpenAPIDocument,L as loadAndRegisterAPIRoutes,P as registerRoutes,N as scanAPIRoutes,I as writeOpenAPISpecs};
1
+ import { defineAppContext } from "./app-context.mjs";
2
+ import { APPOS_DIR, FILE_EXT, PUBLIC_DIR, ROUTES_DIR } from "./constants.mjs";
3
+ import { z } from "zod";
4
+ import { join, resolve } from "node:path";
5
+ import { access, mkdir, writeFile } from "node:fs/promises";
6
+ import { remixRoutesOptionAdapter } from "@react-router/remix-routes-option-adapter";
7
+ import { isEmpty } from "es-toolkit/compat";
8
+ import { flatRoutes } from "remix-flat-routes";
9
+ import { createDocument } from "zod-openapi";
10
+
11
+ //#region src/api/openapi.ts
12
+ const outputDir = resolve(PUBLIC_DIR, "openapi");
13
+ /**
14
+ * Define typed response builders based on defined response schemas.
15
+ */
16
+ function defineTypedResponses(responses) {
17
+ const builder = {};
18
+ for (const [statusCode, responseSpec] of Object.entries(responses)) {
19
+ const status = Number(statusCode);
20
+ const methodName = getMethodNameForStatus(status);
21
+ const schema = responseSpec.schema;
22
+ let strictSchema = schema;
23
+ if (schema instanceof z.ZodObject) strictSchema = schema.strict();
24
+ else if (schema instanceof z.ZodArray) {
25
+ const elementSchema = schema.element;
26
+ if (elementSchema instanceof z.ZodObject) strictSchema = z.array(elementSchema.strict());
27
+ }
28
+ if (methodName) builder[methodName] = (data) => {
29
+ if (schema instanceof z.ZodNull || data === null || data === void 0) return {
30
+ __response: true,
31
+ status,
32
+ data: data ?? null
33
+ };
34
+ try {
35
+ return {
36
+ __response: true,
37
+ status,
38
+ data: strictSchema.parse(data)
39
+ };
40
+ } catch (error) {
41
+ if (error instanceof z.ZodError) throw new Error(`Response validation failed for status ${status}: ${JSON.stringify(error.issues, null, 2)}\n\nData provided: ${JSON.stringify(data, null, 2)}`);
42
+ throw error;
43
+ }
44
+ };
45
+ }
46
+ const strictSchemaCache = /* @__PURE__ */ new Map();
47
+ for (const [statusCode, responseSpec] of Object.entries(responses)) {
48
+ const status = Number(statusCode);
49
+ const schema = responseSpec.schema;
50
+ let strictSchema = schema;
51
+ if (schema instanceof z.ZodObject) strictSchema = schema.strict();
52
+ else if (schema instanceof z.ZodArray) {
53
+ const elementSchema = schema.element;
54
+ if (elementSchema instanceof z.ZodObject) strictSchema = z.array(elementSchema.strict());
55
+ }
56
+ strictSchemaCache.set(status, strictSchema);
57
+ }
58
+ builder.status = (statusCode, data) => {
59
+ const status = Number(statusCode);
60
+ const responseSpec = responses[statusCode];
61
+ if (!responseSpec) return {
62
+ __response: true,
63
+ status,
64
+ data: data ?? null
65
+ };
66
+ const schema = responseSpec.schema;
67
+ if (!schema) return {
68
+ __response: true,
69
+ status,
70
+ data: data ?? null
71
+ };
72
+ if (schema instanceof z.ZodNull || data === null || data === void 0) return {
73
+ __response: true,
74
+ status,
75
+ data: data ?? null
76
+ };
77
+ const strictSchema = strictSchemaCache.get(status) || schema;
78
+ try {
79
+ return {
80
+ __response: true,
81
+ status,
82
+ data: strictSchema.parse(data)
83
+ };
84
+ } catch (error) {
85
+ if (error instanceof z.ZodError) throw new Error(`Response validation failed for status ${status}: ${JSON.stringify(error.issues, null, 2)}\n\nData provided: ${JSON.stringify(data, null, 2)}`);
86
+ throw error;
87
+ }
88
+ };
89
+ return builder;
90
+ }
91
+ function getMethodNameForStatus(status) {
92
+ return {
93
+ 200: "ok",
94
+ 201: "created",
95
+ 202: "accepted",
96
+ 204: "noContent",
97
+ 400: "badRequest",
98
+ 401: "unauthorized",
99
+ 403: "forbidden",
100
+ 404: "notFound",
101
+ 409: "conflict",
102
+ 422: "unprocessableEntity",
103
+ 500: "internalServerError"
104
+ }[status] || null;
105
+ }
106
+ /**
107
+ * Helper function to define an OpenAPI endpoint with full type safety.
108
+ */
109
+ function defineOpenAPIEndpoint(spec) {
110
+ return spec;
111
+ }
112
+ /**
113
+ * Parse and validate request parameters with Zod schema
114
+ */
115
+ function parseParams(schema, rawParams) {
116
+ if (!schema) return rawParams;
117
+ return schema.parse(rawParams);
118
+ }
119
+ /**
120
+ * Parse and validate query parameters with Zod schema
121
+ */
122
+ function parseQuery(schema, url) {
123
+ const queryObj = {};
124
+ for (const [key, value] of url.searchParams.entries()) {
125
+ const existing = queryObj[key];
126
+ if (existing === void 0) queryObj[key] = value;
127
+ else if (Array.isArray(existing)) existing.push(value);
128
+ else queryObj[key] = [existing, value];
129
+ }
130
+ if (!schema) return queryObj;
131
+ return schema.parse(queryObj);
132
+ }
133
+ /**
134
+ * Parse and validate request body with Zod schema
135
+ * Note: Express body is already parsed by express.json() middleware
136
+ */
137
+ function parseBody(schema, req) {
138
+ const bodyData = req.body;
139
+ if (!schema) return bodyData;
140
+ return schema.parse(bodyData);
141
+ }
142
+ /**
143
+ * Parse and validate request headers with Zod schema
144
+ * Note: Express req.headers is already a plain object
145
+ */
146
+ function parseHeaders(schema, req) {
147
+ const headersObj = req.headers;
148
+ if (!schema) return headersObj;
149
+ return schema.parse(headersObj);
150
+ }
151
+ /**
152
+ * Get appropriate HTTP status code for validation error type
153
+ */
154
+ function getValidationStatusCode(errorType) {
155
+ switch (errorType) {
156
+ case "params":
157
+ case "query":
158
+ case "headers": return 400;
159
+ case "body": return 422;
160
+ default: return 400;
161
+ }
162
+ }
163
+ /**
164
+ * Create RFC 9457 compliant validation error response
165
+ */
166
+ function createValidationErrorResponse(error, errorType) {
167
+ const status = getValidationStatusCode(errorType);
168
+ return {
169
+ __response: true,
170
+ status,
171
+ data: {
172
+ error: "validation_error",
173
+ status,
174
+ detail: "Request validation failed",
175
+ errors: error.issues.map((issue) => ({
176
+ field: issue.path.join("."),
177
+ message: issue.message,
178
+ received: "received" in issue ? issue.received : issue.input
179
+ }))
180
+ }
181
+ };
182
+ }
183
+ /**
184
+ * Define Express handler for a single HTTP method.
185
+ *
186
+ * Builds a unified AppContext from the Express request, including:
187
+ * - container and workflows from app.locals
188
+ * - request metadata (id, ipAddress, userAgent, headers)
189
+ * - auth state (user, session) from Better Auth
190
+ */
191
+ function defineExpressHandler(spec, method) {
192
+ const responseBuilder = defineTypedResponses(spec.responses);
193
+ return async (req, res, next) => {
194
+ try {
195
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
196
+ const parsedParams = parseParams(spec.params, req.params);
197
+ const parsedQuery = parseQuery(spec.query, url);
198
+ const parsedBody = parseBody(spec.requestBody, req);
199
+ const parsedHeaders = parseHeaders(spec.headers, req);
200
+ const ctx = await defineAppContext({
201
+ apiKey: req.apiKey,
202
+ container: req.app.locals.container,
203
+ request: req,
204
+ fetchSession: false
205
+ });
206
+ const result = await spec.handler({
207
+ ctx,
208
+ body: parsedBody,
209
+ headers: parsedHeaders,
210
+ params: parsedParams,
211
+ query: parsedQuery,
212
+ request: req,
213
+ response: responseBuilder
214
+ });
215
+ if (result && typeof result === "object" && "__response" in result) {
216
+ const marker = result;
217
+ if (marker.data === null || marker.data === void 0) res.status(marker.status).end();
218
+ else res.status(marker.status).json(marker.data);
219
+ } else res.status(200).json(result);
220
+ } catch (error) {
221
+ if (error instanceof z.ZodError) {
222
+ const errorResponse = createValidationErrorResponse(error, method === "GET" || method === "HEAD" ? "query" : "body");
223
+ res.status(errorResponse.status).json(errorResponse.data);
224
+ } else next(error);
225
+ }
226
+ };
227
+ }
228
+ /**
229
+ * Generate OpenAPI specification array for CLI registration.
230
+ */
231
+ function generateOpenAPISpec(config) {
232
+ const specs = [];
233
+ for (const method of [
234
+ "GET",
235
+ "HEAD",
236
+ "POST",
237
+ "PUT",
238
+ "PATCH",
239
+ "DELETE"
240
+ ]) {
241
+ const spec = config[method];
242
+ if (!spec) continue;
243
+ const request = {};
244
+ if (spec.params) request.params = spec.params;
245
+ if (spec.query) request.query = spec.query;
246
+ if (spec.requestBody) request.body = spec.requestBody;
247
+ if (spec.headers) request.headers = spec.headers;
248
+ const responses = {};
249
+ for (const [statusCode, responseSpec] of Object.entries(spec.responses)) responses[Number(statusCode)] = {
250
+ description: responseSpec.description,
251
+ content: { "application/json": { schema: responseSpec.schema } }
252
+ };
253
+ specs.push({
254
+ method: method.toLowerCase(),
255
+ summary: spec.summary,
256
+ description: spec.description,
257
+ request: Object.keys(request).length > 0 ? request : void 0,
258
+ responses
259
+ });
260
+ }
261
+ return specs;
262
+ }
263
+ /**
264
+ * Define OpenAPI-based route handlers with full type safety.
265
+ *
266
+ * @param config Configuration object with HTTP method specifications
267
+ * @returns Object containing Express handlers and openAPISpec
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * // Basic usage
272
+ * export default defineOpenAPI({
273
+ * GET: {
274
+ * summary: "Get user by ID",
275
+ * params: z.object({ id: z.uuid() }),
276
+ * responses: {
277
+ * 200: { description: "User data", schema: UserSchema }
278
+ * },
279
+ * handler: async ({ params, response }) => {
280
+ * const user = await getUser(params.id);
281
+ * return response.ok(user); // Fully typed!
282
+ * }
283
+ * },
284
+ * POST: {
285
+ * summary: "Create user",
286
+ * requestBody: CreateUserSchema,
287
+ * responses: {
288
+ * 201: { description: "User created", schema: UserSchema }
289
+ * },
290
+ * handler: async ({ body, response }) => {
291
+ * const user = await createUser(body);
292
+ * return response.created(user); // Fully typed!
293
+ * }
294
+ * }
295
+ * });
296
+ * ```
297
+ */
298
+ function defineOpenAPI(config) {
299
+ const handlers = {};
300
+ for (const method of [
301
+ "GET",
302
+ "HEAD",
303
+ "POST",
304
+ "PUT",
305
+ "PATCH",
306
+ "DELETE"
307
+ ]) {
308
+ const spec = config[method];
309
+ if (spec) handlers[method] = defineExpressHandler(spec, method);
310
+ }
311
+ return {
312
+ handlers,
313
+ openAPISpec: generateOpenAPISpec(config)
314
+ };
315
+ }
316
+ /**
317
+ * Define OpenAPI document configuration with improved DX.
318
+ *
319
+ * Only allows specifying info and servers objects, ensuring a consistent
320
+ * OpenAPI v3.1.0 structure while preventing configuration of other fields.
321
+ *
322
+ * @param config Configuration object with info and optional servers
323
+ * @returns Complete OpenAPI configuration object
324
+ *
325
+ * @example
326
+ * ```typescript
327
+ * export const openAPIConfig = defineOpenAPIConfig({
328
+ * info: {
329
+ * title: "My API",
330
+ * version: "1.0.0",
331
+ * description: "API v1 - Authentication and core features",
332
+ * contact: {
333
+ * name: "API Support",
334
+ * email: "support@example.com",
335
+ * },
336
+ * },
337
+ * servers: [
338
+ * {
339
+ * url: process.env.API_URL || "http://localhost:8000",
340
+ * description: "API Server",
341
+ * },
342
+ * ],
343
+ * });
344
+ * ```
345
+ */
346
+ function defineOpenAPIConfig(config) {
347
+ return (container) => {
348
+ const input = config(container);
349
+ return {
350
+ info: input.info,
351
+ servers: input.servers,
352
+ openapi: "3.1.0"
353
+ };
354
+ };
355
+ }
356
+ /**
357
+ * Extract API version from route path.
358
+ */
359
+ function extractVersion(path) {
360
+ return path.match(/^\/(v\d+)/)?.[1];
361
+ }
362
+ /**
363
+ * Convert React Router path params (:param) to OpenAPI format ({param}).
364
+ */
365
+ function convertToOpenAPIPath(reactRouterPath) {
366
+ return reactRouterPath.replace(/:(\w+)/g, "{$1}");
367
+ }
368
+ /**
369
+ * Build an OpenAPI path object from a route specification.
370
+ */
371
+ function buildPathObject(path, spec) {
372
+ const openApiPath = convertToOpenAPIPath(path);
373
+ const methodConfig = {
374
+ summary: spec.summary,
375
+ description: spec.description
376
+ };
377
+ if (spec.request?.params || spec.request?.query || spec.request?.headers) {
378
+ methodConfig.requestParams = {};
379
+ if (spec.request.params) methodConfig.requestParams.path = spec.request.params;
380
+ if (spec.request.query) methodConfig.requestParams.query = spec.request.query;
381
+ if (spec.request.headers) methodConfig.requestParams.header = spec.request.headers;
382
+ }
383
+ if (spec.request?.body) methodConfig.requestBody = { content: { "application/json": { schema: spec.request.body } } };
384
+ methodConfig.responses = {};
385
+ for (const [statusCode, response] of Object.entries(spec.responses)) methodConfig.responses[statusCode] = {
386
+ description: response.description,
387
+ content: { "application/json": { schema: response.content["application/json"].schema } }
388
+ };
389
+ return {
390
+ path: openApiPath,
391
+ config: methodConfig
392
+ };
393
+ }
394
+ /**
395
+ * Generate complete OpenAPI 3.1.0 document from routes using zod-openapi
396
+ */
397
+ function generateOpenAPIDocument(routes, version, config) {
398
+ const paths = {};
399
+ for (const route of routes) {
400
+ if (route.version !== version) continue;
401
+ const apiPath = route.path.replace(`/${version}`, "") || "/";
402
+ for (const spec of route.openAPISpec) {
403
+ const { path: openApiPath, config: methodConfig } = buildPathObject(apiPath, spec);
404
+ if (!paths[openApiPath]) paths[openApiPath] = {};
405
+ paths[openApiPath][spec.method] = methodConfig;
406
+ }
407
+ }
408
+ return createDocument({
409
+ openapi: "3.1.0",
410
+ info: config?.info || {
411
+ title: `API ${version.toUpperCase()}`,
412
+ version: version.replace("v", ""),
413
+ description: `OpenAPI specification for ${version} API`
414
+ },
415
+ servers: config?.servers || [{
416
+ url: `http://localhost:8000/${version}`,
417
+ description: process.env.NODE_ENV === "production" ? "Production server" : "Development server"
418
+ }],
419
+ paths
420
+ });
421
+ }
422
+ /**
423
+ * Scan routes directory and convert to Express route paths using remix-flat-routes.
424
+ */
425
+ async function scanAPIRoutes(container) {
426
+ const fullPath = resolve(APPOS_DIR, ROUTES_DIR);
427
+ try {
428
+ await access(fullPath);
429
+ } catch (error) {
430
+ container.logger.error({ error }, "OpenAPI routes directory not found");
431
+ return [];
432
+ }
433
+ const routeManifest = await remixRoutesOptionAdapter((defineRoutes) => {
434
+ return flatRoutes(ROUTES_DIR, defineRoutes, {
435
+ appDir: APPOS_DIR,
436
+ ignoredRouteFiles: [
437
+ "**/.*",
438
+ "**/*.{spec,test}.{ts,tsx}",
439
+ "**/*-????????.{js,ts}"
440
+ ]
441
+ });
442
+ });
443
+ const routes = [];
444
+ for (const [routeId, route] of Object.entries(routeManifest)) {
445
+ const absolutePath = resolve(APPOS_DIR, route.file).replace(/\.ts$/, `.${FILE_EXT}`);
446
+ try {
447
+ const exported = (await import(absolutePath)).default;
448
+ if (!exported || isEmpty(exported) || /\/openapi.(j|t)s$/.test(absolutePath)) continue;
449
+ if (!exported?.handlers && !exported?.openAPISpec) {
450
+ container.logger.warn(`Missing default export with 'defineOpenAPI' for '${APPOS_DIR}/${route.file}'`);
451
+ continue;
452
+ }
453
+ const routePath = `/${route.path}`;
454
+ routes.push({
455
+ path: routePath,
456
+ filePath: absolutePath,
457
+ handlers: exported.handlers,
458
+ openAPISpec: exported.openAPISpec,
459
+ version: extractVersion(routePath)
460
+ });
461
+ } catch (error) {
462
+ container.logger.error(error, `Error loading route ${route.file}:`);
463
+ }
464
+ }
465
+ return routes;
466
+ }
467
+ /**
468
+ * Register scanned routes with Express app.
469
+ */
470
+ function registerRoutes(app, routes) {
471
+ for (const route of routes) for (const [method, handler] of Object.entries(route.handlers)) app[method.toLowerCase()](route.path, handler);
472
+ }
473
+ /**
474
+ * Load version-specific OpenAPI config from openapi.ts file.
475
+ */
476
+ async function loadVersionConfig(container, version) {
477
+ const configPath = resolve(APPOS_DIR, ROUTES_DIR, `${version}+/openapi.${FILE_EXT}`);
478
+ try {
479
+ const configFn = (await import(configPath)).default;
480
+ return configFn ? configFn(container) : void 0;
481
+ } catch {
482
+ return;
483
+ }
484
+ }
485
+ /**
486
+ * Write OpenAPI specs to api/public/openapi directory.
487
+ */
488
+ async function writeOpenAPISpecs(container, routes) {
489
+ const versions = [...new Set(routes.map((r) => r.version).filter(Boolean))];
490
+ if (versions.length > 0) await mkdir(outputDir, { recursive: true });
491
+ for (const version of versions) {
492
+ const doc = generateOpenAPIDocument(routes, version, await loadVersionConfig(container, version));
493
+ await writeFile(join(outputDir, `${version}.json`), JSON.stringify(doc, null, 2), "utf-8");
494
+ }
495
+ }
496
+ /**
497
+ * Load and register API routes with Express (convenience function).
498
+ * Also auto-generates OpenAPI specs in development mode.
499
+ */
500
+ async function loadAndRegisterAPIRoutes(app) {
501
+ const routes = await scanAPIRoutes(app.locals.container);
502
+ registerRoutes(app, routes);
503
+ if (process.env.NODE_ENV !== "production") await writeOpenAPISpecs(app.locals.container, routes);
504
+ }
505
+
506
+ //#endregion
507
+ export { defineOpenAPI, defineOpenAPIConfig, defineOpenAPIEndpoint, defineTypedResponses, generateOpenAPIDocument, loadAndRegisterAPIRoutes, registerRoutes, scanAPIRoutes, writeOpenAPISpecs };
@@ -1 +1,43 @@
1
- import{__export as e,__reExport as t}from"./_virtual/rolldown_runtime.mjs";import{index as n,isPgEnum as r,isPgMaterializedView as i,isPgSchema as a,isPgSequence as o,isPgView as s,numeric as c,parsePgArray as l,parsePgNestedArray as u,pgEnum as d,pgMaterializedView as f,pgPolicy as p,pgRole as m,pgSchema as h,pgSequence as g,pgTable as _,pgTableCreator as v,pgView as y,serial as b,smallint as x,smallserial as S,sparsevec as C,unique as w,uniqueIndex as T,uniqueKeyName as E,withReplicas as D}from"drizzle-orm/pg-core";export*from"drizzle-orm";export*from"drizzle-seed";var O=e({index:()=>n,isPgEnum:()=>r,isPgMaterializedView:()=>i,isPgSchema:()=>a,isPgSequence:()=>o,isPgView:()=>s,numeric:()=>c,parsePgArray:()=>l,parsePgNestedArray:()=>u,pgEnum:()=>d,pgMaterializedView:()=>f,pgPolicy:()=>p,pgRole:()=>m,pgSchema:()=>h,pgSequence:()=>g,pgTable:()=>_,pgTableCreator:()=>v,pgView:()=>y,serial:()=>b,smallint:()=>x,smallserial:()=>S,sparsevec:()=>C,unique:()=>w,uniqueIndex:()=>T,uniqueKeyName:()=>E,withReplicas:()=>D});import*as k from"drizzle-orm";t(O,k);import*as A from"drizzle-seed";t(O,A);export{n as index,r as isPgEnum,i as isPgMaterializedView,a as isPgSchema,o as isPgSequence,s as isPgView,c as numeric,O as orm_exports,l as parsePgArray,u as parsePgNestedArray,d as pgEnum,f as pgMaterializedView,p as pgPolicy,m as pgRole,h as pgSchema,g as pgSequence,_ as pgTable,v as pgTableCreator,y as pgView,b as serial,x as smallint,S as smallserial,C as sparsevec,w as unique,T as uniqueIndex,E as uniqueKeyName,D as withReplicas};
1
+ import { __export, __reExport } from "./_virtual/rolldown_runtime.mjs";
2
+ import { index, isPgEnum, isPgMaterializedView, isPgSchema, isPgSequence, isPgView, numeric, parsePgArray, parsePgNestedArray, pgEnum, pgMaterializedView, pgPolicy, pgRole, pgSchema, pgSequence, pgTable, pgTableCreator, pgView, serial, smallint, smallserial, sparsevec, unique, uniqueIndex, uniqueKeyName, withReplicas } from "drizzle-orm/pg-core";
3
+
4
+ export * from "drizzle-orm"
5
+
6
+ export * from "drizzle-seed"
7
+
8
+ //#region src/api/orm.ts
9
+ var orm_exports = /* @__PURE__ */ __export({
10
+ index: () => index,
11
+ isPgEnum: () => isPgEnum,
12
+ isPgMaterializedView: () => isPgMaterializedView,
13
+ isPgSchema: () => isPgSchema,
14
+ isPgSequence: () => isPgSequence,
15
+ isPgView: () => isPgView,
16
+ numeric: () => numeric,
17
+ parsePgArray: () => parsePgArray,
18
+ parsePgNestedArray: () => parsePgNestedArray,
19
+ pgEnum: () => pgEnum,
20
+ pgMaterializedView: () => pgMaterializedView,
21
+ pgPolicy: () => pgPolicy,
22
+ pgRole: () => pgRole,
23
+ pgSchema: () => pgSchema,
24
+ pgSequence: () => pgSequence,
25
+ pgTable: () => pgTable,
26
+ pgTableCreator: () => pgTableCreator,
27
+ pgView: () => pgView,
28
+ serial: () => serial,
29
+ smallint: () => smallint,
30
+ smallserial: () => smallserial,
31
+ sparsevec: () => sparsevec,
32
+ unique: () => unique,
33
+ uniqueIndex: () => uniqueIndex,
34
+ uniqueKeyName: () => uniqueKeyName,
35
+ withReplicas: () => withReplicas
36
+ });
37
+ import * as import_drizzle_orm from "drizzle-orm";
38
+ __reExport(orm_exports, import_drizzle_orm);
39
+ import * as import_drizzle_seed from "drizzle-seed";
40
+ __reExport(orm_exports, import_drizzle_seed);
41
+
42
+ //#endregion
43
+ export { index, isPgEnum, isPgMaterializedView, isPgSchema, isPgSequence, isPgView, numeric, orm_exports, parsePgArray, parsePgNestedArray, pgEnum, pgMaterializedView, pgPolicy, pgRole, pgSchema, pgSequence, pgTable, pgTableCreator, pgView, serial, smallint, smallserial, sparsevec, unique, uniqueIndex, uniqueKeyName, withReplicas };
@@ -1 +1,56 @@
1
- import{instrumentation_exports as e}from"./instrumentation.mjs";import{getCallSites as t}from"node:util";async function n(n,r){let i=t()[1],a=i.scriptName.replace(process.cwd(),``).replace(`file:///`,``),o=i.functionName,s=e.trace.getTracer(a.replace(`build/`,``)).startSpan(o,{attributes:r}),c=e.trace.setSpan(e.context.active(),s);try{let t=await e.context.with(c,()=>n(s));return s.end(),t}catch(e){throw s.recordException(e),s.end(),e}}export{n as withOtelSpan};
1
+ import { instrumentation_exports } from "./instrumentation.mjs";
2
+ import { getCallSites } from "node:util";
3
+
4
+ //#region src/api/otel.ts
5
+ /**
6
+ * Wraps an async function with an OpenTelemetry span for distributed tracing.
7
+ *
8
+ * Automatically handles span lifecycle (start/end), error recording, and context propagation.
9
+ * The span will appear as a child of the currently active span (if any).
10
+ *
11
+ * @param tracerName - Fully qualified module path (e.g., "api/routes/payouts", "packages/primitives/mailer")
12
+ * @param spanName - Operation name following {verb} {object} pattern (e.g., "fetch payouts", "send email")
13
+ * @param fn - The async function to execute within the span context. Receives span for custom attributes.
14
+ * @param attributes - Optional span attributes for high-cardinality data (IDs, counts, etc.).
15
+ * Use semantic conventions: https://opentelemetry.io/docs/specs/semconv/
16
+ * @returns Promise resolving to the function's return value.
17
+ *
18
+ * @example
19
+ * // Basic usage - simple business operation
20
+ * await withOtelSpan("api/routes/payouts", "fetch payouts", async () => {
21
+ * return await db.query.payouts.findMany();
22
+ * });
23
+ *
24
+ * @example
25
+ * // With attributes and custom span data
26
+ * const payouts = await withOtelSpan(
27
+ * "api/routes/payouts",
28
+ * "fetch payouts",
29
+ * async (span) => {
30
+ * const results = await db.query.payouts.findMany();
31
+ * span.setAttribute("payouts.count", results.length);
32
+ * span.addEvent("cache.miss");
33
+ * return results;
34
+ * },
35
+ * { "payouts.limit": 100, "payouts.offset": 0 }
36
+ * );
37
+ */
38
+ async function withOtelSpan(fn, attributes) {
39
+ const callSite = getCallSites()[1];
40
+ const tracerName = callSite.scriptName.replace(process.cwd(), "").replace("file:///", "");
41
+ const spanName = callSite.functionName;
42
+ const span = instrumentation_exports.trace.getTracer(tracerName.replace("build/", "")).startSpan(spanName, { attributes });
43
+ const ctx = instrumentation_exports.trace.setSpan(instrumentation_exports.context.active(), span);
44
+ try {
45
+ const result = await instrumentation_exports.context.with(ctx, () => fn(span));
46
+ span.end();
47
+ return result;
48
+ } catch (error) {
49
+ span.recordException(error);
50
+ span.end();
51
+ throw error;
52
+ }
53
+ }
54
+
55
+ //#endregion
56
+ export { withOtelSpan };
@@ -1 +1,41 @@
1
- import{createClient as e,createCluster as t}from"redis";function n(n){let r=n.url.split(`,`).map(e=>e.trim()).filter(Boolean),i;i=r.length===1?e({url:r[0]}):t({rootNodes:r.map(e=>({url:e}))}),i.on(`error`,e=>n.logger.error({err:e},`Redis error`));let a=null,o=async()=>{i.isOpen||(a??=i.connect(),await a)};return new Proxy(i,{get(e,t,n){let r=Reflect.get(e,t,n);return typeof r==`function`&&t!==`on`&&t!==`once`?async(...t)=>(await o(),r.apply(e,t)):r}})}export{n as defineRedisClient};
1
+ import { createClient, createCluster } from "redis";
2
+
3
+ //#region src/api/redis.ts
4
+ /**
5
+ * Defines Redis client based on URL format with lazy connection.
6
+ * Single URL → createClient(), comma-separated URLs → createCluster()
7
+ *
8
+ * Algorithm:
9
+ * 1. Split URL by comma to detect cluster vs single
10
+ * 2. Create appropriate client type
11
+ * 3. Attach error handler with label for debugging
12
+ * 4. Wrap in proxy that auto-connects on first use
13
+ *
14
+ * @param opts Options for defining the Redis client.
15
+ * @returns Redis client that auto-connects on first operation.
16
+ */
17
+ function defineRedisClient(opts) {
18
+ const urls = opts.url.split(",").map((u) => u.trim()).filter(Boolean);
19
+ let client;
20
+ if (urls.length === 1) client = createClient({ url: urls[0] });
21
+ else client = createCluster({ rootNodes: urls.map((u) => ({ url: u })) });
22
+ client.on("error", (err) => opts.logger.error({ err }, "Redis error"));
23
+ let connectPromise = null;
24
+ const ensureConnected = async () => {
25
+ if (!client.isOpen) {
26
+ connectPromise ??= client.connect();
27
+ await connectPromise;
28
+ }
29
+ };
30
+ return new Proxy(client, { get(target, prop, receiver) {
31
+ const value = Reflect.get(target, prop, receiver);
32
+ if (typeof value === "function" && prop !== "on" && prop !== "once") return async (...args) => {
33
+ await ensureConnected();
34
+ return value.apply(target, args);
35
+ };
36
+ return value;
37
+ } });
38
+ }
39
+
40
+ //#endregion
41
+ export { defineRedisClient };