From c5d6577b774ba8d8a269c3a5a210a1a88c7bfb06 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 18 Aug 2025 18:43:49 -0400 Subject: [PATCH 01/35] WIP refactorings From 01a7a4146e8426c63bf772050d82353747a25cb6 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 11 Aug 2025 14:27:55 -0400 Subject: [PATCH 02/35] refactor(opapi): Explicit exports in library file --- entities/pkg/pnpm-lock.yaml | 5 +++++ opapi/package.json | 4 ++-- opapi/src/index.ts | 41 +++++++++++++++++++++++++++++++++++-- opapi/src/state.ts | 1 + verel/pkg/pnpm-lock.yaml | 5 +++++ 5 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 entities/pkg/pnpm-lock.yaml create mode 100644 verel/pkg/pnpm-lock.yaml diff --git a/entities/pkg/pnpm-lock.yaml b/entities/pkg/pnpm-lock.yaml new file mode 100644 index 00000000..2b9f1883 --- /dev/null +++ b/entities/pkg/pnpm-lock.yaml @@ -0,0 +1,5 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/opapi/package.json b/opapi/package.json index c1fbd219..ccd7608d 100644 --- a/opapi/package.json +++ b/opapi/package.json @@ -59,7 +59,7 @@ "license": "MIT", "engines": { "node": ">=16.0.0", - "pnpm": "8.6.2" + "pnpm": "8.15.9" }, - "packageManager": "pnpm@8.6.2" + "packageManager": "pnpm@8.15.9" } diff --git a/opapi/src/index.ts b/opapi/src/index.ts index e1eb9d43..f9ae7537 100644 --- a/opapi/src/index.ts +++ b/opapi/src/index.ts @@ -1,3 +1,40 @@ export { OpenApiZodAny } from '@anatine/zod-openapi' -export * from './opapi' -export * from './state' +export { + schema, + type OpenApiProps, + type CodePostProcessor, + type OpenApiPostProcessors, + type GenerateClientProps, + OpenApi, + SchemaOf, + ParameterOf, + SectionOf, + exportJsonSchemas, + exportZodSchemas, +} from './opapi' +export { + type Options, + type State, + type ApiError, + type Metadata, + type PathParameter, + type StandardParameter, + type BooleanParameter, + type IntegerParameter, + type NumberParameter, + type QueryParameterStringArray, + type QueryParameterObject, + type Parameter, + operationsWithBodyMethod, + type OperationWithBodyProps, + operationsWithoutBodyMethod, + type OperationWithoutBodyMethod, + type OperationWithoutBodyProps, + type Operation, + isOperationWithBodyProps, + ComponentType, + type ParametersMap, + createState, + getRef, + mapParameter, +} from './state' diff --git a/opapi/src/state.ts b/opapi/src/state.ts index 8a2fe48c..2ec572cb 100644 --- a/opapi/src/state.ts +++ b/opapi/src/state.ts @@ -177,6 +177,7 @@ export function isOperationWithBodyProps< } else return false } +/** TODO get rid of use of typescript enum */ export enum ComponentType { SCHEMAS = 'schemas', RESPONSES = 'responses', diff --git a/verel/pkg/pnpm-lock.yaml b/verel/pkg/pnpm-lock.yaml new file mode 100644 index 00000000..2b9f1883 --- /dev/null +++ b/verel/pkg/pnpm-lock.yaml @@ -0,0 +1,5 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false From 1f6e573b5397aaac3e3a34741e565ee1ed770ee9 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 11 Aug 2025 14:31:44 -0400 Subject: [PATCH 03/35] chore(opapi): Upgrade to latest vitest --- opapi/package.json | 2 +- opapi/pnpm-lock.yaml | 427 +++++++++++++++++++++++-------------------- 2 files changed, 231 insertions(+), 198 deletions(-) diff --git a/opapi/package.json b/opapi/package.json index ccd7608d..581e1eb7 100644 --- a/opapi/package.json +++ b/opapi/package.json @@ -35,7 +35,7 @@ "typescript": "5.7.2", "vite": "5.4.11", "vite-node": "1.6.0", - "vitest": "1.6.0" + "vitest": "3.2.4" }, "dependencies": { "@anatine/zod-openapi": "1.12.1", diff --git a/opapi/pnpm-lock.yaml b/opapi/pnpm-lock.yaml index 1c517526..c0f66649 100644 --- a/opapi/pnpm-lock.yaml +++ b/opapi/pnpm-lock.yaml @@ -75,7 +75,7 @@ devDependencies: version: 4.17.13 '@types/node': specifier: ^22.16.4 - version: 22.16.4 + version: 22.17.2 '@types/qs': specifier: ^6.9.15 version: 6.9.17 @@ -87,7 +87,7 @@ devDependencies: version: 3.4.1 ts-node: specifier: 10.9.2 - version: 10.9.2(@swc/core@1.9.3)(@types/node@22.16.4)(typescript@5.7.2) + version: 10.9.2(@swc/core@1.9.3)(@types/node@22.17.2)(typescript@5.7.2) tsup: specifier: 8.3.5 version: 8.3.5(@swc/core@1.9.3)(typescript@5.7.2) @@ -96,13 +96,13 @@ devDependencies: version: 5.7.2 vite: specifier: 5.4.11 - version: 5.4.11(@types/node@22.16.4) + version: 5.4.11(@types/node@22.17.2) vite-node: specifier: 1.6.0 - version: 1.6.0(@types/node@22.16.4) + version: 1.6.0(@types/node@22.17.2) vitest: - specifier: 1.6.0 - version: 1.6.0(@types/node@22.16.4) + specifier: 3.2.4 + version: 3.2.4(@types/node@22.17.2) packages: @@ -626,13 +626,6 @@ packages: wrap-ansi-cjs: /wrap-ansi@7.0.0 dev: true - /@jest/schemas@29.6.3: - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@sinclair/typebox': 0.27.8 - dev: true - /@jridgewell/gen-mapping@0.3.5: resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -1021,10 +1014,6 @@ packages: dev: true optional: true - /@sinclair/typebox@0.27.8: - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - dev: true - /@swc/core-darwin-arm64@1.9.3: resolution: {integrity: sha512-hGfl/KTic/QY4tB9DkTbNuxy5cV4IeejpPD4zo+Lzt4iLlDWIeANL4Fkg67FiVceNJboqg48CUX+APhDHO5G1w==} engines: {node: '>=10'} @@ -1177,19 +1166,29 @@ packages: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} dependencies: '@types/connect': 3.4.38 - '@types/node': 22.16.4 + '@types/node': 22.17.2 + dev: true + + /@types/chai@5.2.2: + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + dependencies: + '@types/deep-eql': 4.0.2 dev: true /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 22.16.4 + '@types/node': 22.17.2 dev: true /@types/decompress@4.2.7: resolution: {integrity: sha512-9z+8yjKr5Wn73Pt17/ldnmQToaFHZxK0N1GHysuk/JIPT8RIdQeoInM01wWPgypRcvb6VH1drjuFpQ4zmY437g==} dependencies: - '@types/node': 22.16.4 + '@types/node': 22.17.2 + dev: true + + /@types/deep-eql@4.0.2: + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} dev: true /@types/estree@1.0.5: @@ -1203,7 +1202,7 @@ packages: /@types/express-serve-static-core@4.19.6: resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} dependencies: - '@types/node': 22.16.4 + '@types/node': 22.17.2 '@types/qs': 6.9.17 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -1222,7 +1221,7 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 22.16.4 + '@types/node': 22.17.2 dev: false /@types/http-errors@2.0.4: @@ -1243,8 +1242,8 @@ packages: resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} dev: false - /@types/node@22.16.4: - resolution: {integrity: sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g==} + /@types/node@22.17.2: + resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==} dependencies: undici-types: 6.21.0 @@ -1264,14 +1263,14 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 22.16.4 + '@types/node': 22.17.2 dev: true /@types/serve-static@1.15.7: resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.16.4 + '@types/node': 22.17.2 '@types/send': 0.17.4 dev: true @@ -1283,43 +1282,67 @@ packages: resolution: {integrity: sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg==} dev: true - /@vitest/expect@1.6.0: - resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} + /@vitest/expect@3.2.4: + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} dependencies: - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - chai: 4.5.0 + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + tinyrainbow: 2.0.0 dev: true - /@vitest/runner@1.6.0: - resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} + /@vitest/mocker@3.2.4(vite@5.4.11): + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true dependencies: - '@vitest/utils': 1.6.0 - p-limit: 5.0.0 - pathe: 1.1.2 + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + vite: 5.4.11(@types/node@22.17.2) dev: true - /@vitest/snapshot@1.6.0: - resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} + /@vitest/pretty-format@3.2.4: + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} dependencies: - magic-string: 0.30.11 - pathe: 1.1.2 - pretty-format: 29.7.0 + tinyrainbow: 2.0.0 dev: true - /@vitest/spy@1.6.0: - resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} + /@vitest/runner@3.2.4: + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} dependencies: - tinyspy: 2.2.1 + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 dev: true - /@vitest/utils@1.6.0: - resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} + /@vitest/snapshot@3.2.4: + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + dev: true + + /@vitest/spy@3.2.4: + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + dependencies: + tinyspy: 4.0.3 + dev: true + + /@vitest/utils@3.2.4: + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.0 + tinyrainbow: 2.0.0 dev: true /acorn-walk@8.3.4: @@ -1383,11 +1406,6 @@ packages: dependencies: color-convert: 2.0.1 - /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - dev: true - /ansi-styles@6.2.1: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} @@ -1419,8 +1437,9 @@ packages: engines: {node: '>=0.8'} dev: false - /assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + /assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} dev: true /async@3.2.6: @@ -1531,17 +1550,15 @@ packages: resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} dev: false - /chai@4.5.0: - resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} - engines: {node: '>=4'} + /chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.4 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.1.0 + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.0 + pathval: 2.0.1 dev: true /chalk@2.4.2: @@ -1561,10 +1578,9 @@ packages: supports-color: 7.2.0 dev: false - /check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} - dependencies: - get-func-name: 2.0.2 + /check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} dev: true /chokidar@4.0.1: @@ -1653,10 +1669,6 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: false - /confbox@0.1.7: - resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} - dev: true - /consola@3.2.3: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} @@ -1705,6 +1717,18 @@ packages: dependencies: ms: 2.1.3 + /debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + /decompress-tar@4.1.1: resolution: {integrity: sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==} engines: {node: '>=4'} @@ -1758,11 +1782,9 @@ packages: strip-dirs: 2.1.0 dev: false - /deep-eql@4.1.4: - resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + /deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - dependencies: - type-detect: 4.1.0 dev: true /define-data-property@1.1.4: @@ -1786,11 +1808,6 @@ packages: wrappy: 1.0.2 dev: false - /diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true - /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} @@ -1830,6 +1847,10 @@ packages: engines: {node: '>= 0.4'} dev: false + /es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + dev: true + /es5-ext@0.10.64: resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} engines: {node: '>=0.10'} @@ -1976,6 +1997,12 @@ packages: onetime: 6.0.0 signal-exit: 4.1.0 strip-final-newline: 3.0.0 + dev: false + + /expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + dev: true /ext@1.7.0: resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} @@ -2034,6 +2061,17 @@ packages: picomatch: 4.0.2 dev: true + /fdir@6.4.6(picomatch@4.0.2): + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + dependencies: + picomatch: 4.0.2 + dev: true + /fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} dev: false @@ -2120,10 +2158,6 @@ packages: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: false - /get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - dev: true - /get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -2151,6 +2185,7 @@ packages: /get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} + dev: false /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2254,6 +2289,7 @@ packages: /human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} + dev: false /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2318,6 +2354,7 @@ packages: /is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -2343,8 +2380,8 @@ packages: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: false - /js-tokens@9.0.0: - resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} + /js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} dev: true /js-yaml@3.14.1: @@ -2457,14 +2494,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true - /local-pkg@0.5.0: - resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} - engines: {node: '>=14'} - dependencies: - mlly: 1.7.1 - pkg-types: 1.2.0 - dev: true - /lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} dev: true @@ -2485,10 +2514,8 @@ packages: triple-beam: 1.4.1 dev: false - /loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - dependencies: - get-func-name: 2.0.2 + /loupe@3.2.0: + resolution: {integrity: sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==} dev: true /lru-cache@10.4.3: @@ -2501,8 +2528,8 @@ packages: es5-ext: 0.10.64 dev: false - /magic-string@0.30.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + /magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} dependencies: '@jridgewell/sourcemap-codec': 1.5.0 dev: true @@ -2534,6 +2561,7 @@ packages: /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: false /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} @@ -2574,6 +2602,7 @@ packages: /mimic-fn@4.0.0: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + dev: false /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2603,15 +2632,6 @@ packages: hasBin: true dev: false - /mlly@1.7.1: - resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==} - dependencies: - acorn: 8.12.1 - pathe: 1.1.2 - pkg-types: 1.2.0 - ufo: 1.5.4 - dev: true - /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2641,6 +2661,7 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: path-key: 4.0.0 + dev: false /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -2668,6 +2689,7 @@ packages: engines: {node: '>=12'} dependencies: mimic-fn: 4.0.0 + dev: false /openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -2691,13 +2713,6 @@ packages: yaml: 1.10.2 dev: false - /p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - dependencies: - yocto-queue: 1.1.1 - dev: true - /package-json-from-dist@1.0.0: resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} dev: true @@ -2714,6 +2729,7 @@ packages: /path-key@4.0.0: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + dev: false /path-loader@1.0.12: resolution: {integrity: sha512-n7oDG8B+k/p818uweWrOixY9/Dsr89o2TkCm6tOTex3fpdo2+BFDgR+KpB37mGKBRsBAlR8CIJMFN0OEy/7hIQ==} @@ -2736,8 +2752,13 @@ packages: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} dev: true - /pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + /pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + dev: true + + /pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} dev: true /pend@1.2.0: @@ -2788,14 +2809,6 @@ packages: engines: {node: '>= 6'} dev: true - /pkg-types@1.2.0: - resolution: {integrity: sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==} - dependencies: - confbox: 0.1.7 - mlly: 1.7.1 - pathe: 1.1.2 - dev: true - /postcss-load-config@6.0.1: resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} engines: {node: '>= 18'} @@ -2838,15 +2851,6 @@ packages: hasBin: true dev: true - /pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - dev: true - /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: false @@ -2875,10 +2879,6 @@ packages: engines: {node: '>=14.18.0'} dev: false - /react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - dev: true - /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} dependencies: @@ -3085,8 +3085,8 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true - /std-env@3.7.0: - resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + /std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} dev: true /string-width@4.2.3: @@ -3147,11 +3147,12 @@ packages: /strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} + dev: false - /strip-literal@2.1.0: - resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + /strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} dependencies: - js-tokens: 9.0.0 + js-tokens: 9.0.1 dev: true /sucrase@3.35.0: @@ -3255,6 +3256,10 @@ packages: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} dev: true + /tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + dev: true + /tinyglobby@0.2.10: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} @@ -3263,13 +3268,26 @@ packages: picomatch: 4.0.2 dev: true - /tinypool@0.8.4: - resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + /tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + dev: true + + /tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + dev: true + + /tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} dev: true - /tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + /tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} dev: true @@ -3309,7 +3327,7 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true - /ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.16.4)(typescript@5.7.2): + /ts-node@10.9.2(@swc/core@1.9.3)(@types/node@22.17.2)(typescript@5.7.2): resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true peerDependencies: @@ -3329,7 +3347,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.16.4 + '@types/node': 22.17.2 acorn: 8.12.1 acorn-walk: 8.3.4 arg: 4.1.3 @@ -3398,11 +3416,6 @@ packages: - yaml dev: true - /type-detect@4.1.0: - resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} - engines: {node: '>=4'} - dev: true - /type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} dev: false @@ -3413,10 +3426,6 @@ packages: hasBin: true dev: true - /ufo@1.5.4: - resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} - dev: true - /unbzip2-stream@1.4.3: resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} dependencies: @@ -3457,7 +3466,7 @@ packages: extsprintf: 1.4.1 dev: false - /vite-node@1.6.0(@types/node@22.16.4): + /vite-node@1.6.0(@types/node@22.17.2): resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -3466,7 +3475,29 @@ packages: debug: 4.3.7 pathe: 1.1.2 picocolors: 1.1.0 - vite: 5.4.11(@types/node@22.16.4) + vite: 5.4.11(@types/node@22.17.2) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + dev: true + + /vite-node@3.2.4(@types/node@22.17.2): + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 5.4.11(@types/node@22.17.2) transitivePeerDependencies: - '@types/node' - less @@ -3479,7 +3510,7 @@ packages: - terser dev: true - /vite@5.4.11(@types/node@22.16.4): + /vite@5.4.11(@types/node@22.17.2): resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -3510,7 +3541,7 @@ packages: terser: optional: true dependencies: - '@types/node': 22.16.4 + '@types/node': 22.17.2 esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.22.4 @@ -3518,20 +3549,23 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.6.0(@types/node@22.16.4): - resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} - engines: {node: ^18.0.0 || >=20.0.0} + /vitest@3.2.4(@types/node@22.17.2): + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.6.0 - '@vitest/ui': 1.6.0 + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 happy-dom: '*' jsdom: '*' peerDependenciesMeta: '@edge-runtime/vm': optional: true + '@types/debug': + optional: true '@types/node': optional: true '@vitest/browser': @@ -3543,30 +3577,34 @@ packages: jsdom: optional: true dependencies: - '@types/node': 22.16.4 - '@vitest/expect': 1.6.0 - '@vitest/runner': 1.6.0 - '@vitest/snapshot': 1.6.0 - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - acorn-walk: 8.3.4 - chai: 4.5.0 - debug: 4.3.7 - execa: 8.0.1 - local-pkg: 0.5.0 - magic-string: 0.30.11 - pathe: 1.1.2 - picocolors: 1.1.0 - std-env: 3.7.0 - strip-literal: 2.1.0 + '@types/chai': 5.2.2 + '@types/node': 22.17.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@5.4.11) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + debug: 4.4.1 + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 tinybench: 2.9.0 - tinypool: 0.8.4 - vite: 5.4.11(@types/node@22.16.4) - vite-node: 1.6.0(@types/node@22.16.4) + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 5.4.11(@types/node@22.17.2) + vite-node: 3.2.4(@types/node@22.17.2) why-is-node-running: 2.3.0 transitivePeerDependencies: - less - lightningcss + - msw - sass - sass-embedded - stylus @@ -3678,11 +3716,6 @@ packages: engines: {node: '>=6'} dev: true - /yocto-queue@1.1.1: - resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} - engines: {node: '>=12.20'} - dev: true - /zod@3.22.4: resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} dev: false From 88bc41837da8af1198a9fbae79b5e935666f2948 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 11 Aug 2025 14:43:31 -0400 Subject: [PATCH 04/35] chore(opapi): Replace prettier with biome --- opapi/.prettierignore | 22 ----------- opapi/.prettierrc | 8 ---- opapi/biome.json | 52 +++++++++++++++++++++++++ opapi/package.json | 5 ++- opapi/pnpm-lock.yaml | 90 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 32 deletions(-) delete mode 100644 opapi/.prettierignore delete mode 100644 opapi/.prettierrc create mode 100644 opapi/biome.json diff --git a/opapi/.prettierignore b/opapi/.prettierignore deleted file mode 100644 index 86d61cd9..00000000 --- a/opapi/.prettierignore +++ /dev/null @@ -1,22 +0,0 @@ -# based on https://gist.github.com/thinkricardo/74f37d82b686de371b0853a5d66d559c - -# ignore all files -* - -# include all folders -!**/ - -# include files to format -!*.ts -!*.tsx -!*.json -!*.yaml -!*.yml -!*.md - -# exclusions -# **/*.d.ts -pnpm-lock.yaml -gen -dist -.botpress diff --git a/opapi/.prettierrc b/opapi/.prettierrc deleted file mode 100644 index 24a674bd..00000000 --- a/opapi/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "bracketSpacing": true, - "printWidth": 120, - "tabWidth": 2, - "semi": false, - "singleQuote": true, - "trailingComma": "all" -} diff --git a/opapi/biome.json b/opapi/biome.json new file mode 100644 index 00000000..c718e4dc --- /dev/null +++ b/opapi/biome.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.1.4/schema.json", + "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, + "files": { "ignoreUnknown": false }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 120, + "attributePosition": "auto", + "bracketSameLine": false, + "bracketSpacing": true, + "expand": "auto", + "useEditorconfig": true, + "includes": [ + "**", + "!**/*", + "**/", + "**/*.ts", + "**/*.tsx", + "**/*.json", + "**/*.yaml", + "**/*.yml", + "**/*.md", + "!**/pnpm-lock.yaml", + "!**/gen", + "!**/dist", + "!**/.botpress" + ] + }, + "linter": { "enabled": true, "rules": { "recommended": true } }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "all", + "semicolons": "asNeeded", + "arrowParentheses": "always", + "bracketSameLine": false, + "quoteStyle": "single", + "attributePosition": "auto", + "bracketSpacing": true + } + }, + "html": { "formatter": { "selfCloseVoidElements": "always" } }, + "assist": { + "enabled": true, + "actions": { "source": { "organizeImports": "on" } } + } +} diff --git a/opapi/package.json b/opapi/package.json index 581e1eb7..fc9aae51 100644 --- a/opapi/package.json +++ b/opapi/package.json @@ -16,10 +16,11 @@ "test": "vitest run", "build": "tsup src/index.ts --dts --format cjs,esm --clean", "check:type": "tsc --noEmit", - "check:format": "prettier --check .", - "fix:format": "prettier --write ." + "check:format": "biome format .", + "fix:format": "biome format --write ." }, "devDependencies": { + "@biomejs/biome": "2.1.4", "@swc/core": "1.9.3", "@swc/helpers": "0.5.15", "@types/decompress": "4.2.7", diff --git a/opapi/pnpm-lock.yaml b/opapi/pnpm-lock.yaml index c0f66649..7d7f4834 100644 --- a/opapi/pnpm-lock.yaml +++ b/opapi/pnpm-lock.yaml @@ -55,6 +55,9 @@ dependencies: version: 3.22.4 devDependencies: + '@biomejs/biome': + specifier: 2.1.4 + version: 2.1.4 '@swc/core': specifier: 1.9.3 version: 1.9.3(@swc/helpers@0.5.15) @@ -161,6 +164,93 @@ packages: js-yaml: 4.1.0 dev: false + /@biomejs/biome@2.1.4: + resolution: {integrity: sha512-QWlrqyxsU0FCebuMnkvBIkxvPqH89afiJzjMl+z67ybutse590jgeaFdDurE9XYtzpjRGTI1tlUZPGWmbKsElA==} + engines: {node: '>=14.21.3'} + hasBin: true + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.1.4 + '@biomejs/cli-darwin-x64': 2.1.4 + '@biomejs/cli-linux-arm64': 2.1.4 + '@biomejs/cli-linux-arm64-musl': 2.1.4 + '@biomejs/cli-linux-x64': 2.1.4 + '@biomejs/cli-linux-x64-musl': 2.1.4 + '@biomejs/cli-win32-arm64': 2.1.4 + '@biomejs/cli-win32-x64': 2.1.4 + dev: true + + /@biomejs/cli-darwin-arm64@2.1.4: + resolution: {integrity: sha512-sCrNENE74I9MV090Wq/9Dg7EhPudx3+5OiSoQOkIe3DLPzFARuL1dOwCWhKCpA3I5RHmbrsbNSRfZwCabwd8Qg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-darwin-x64@2.1.4: + resolution: {integrity: sha512-gOEICJbTCy6iruBywBDcG4X5rHMbqCPs3clh3UQ+hRKlgvJTk4NHWQAyHOXvaLe+AxD1/TNX1jbZeffBJzcrOw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-linux-arm64-musl@2.1.4: + resolution: {integrity: sha512-nYr7H0CyAJPaLupFE2cH16KZmRC5Z9PEftiA2vWxk+CsFkPZQ6dBRdcC6RuS+zJlPc/JOd8xw3uCCt9Pv41WvQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-linux-arm64@2.1.4: + resolution: {integrity: sha512-juhEkdkKR4nbUi5k/KRp1ocGPNWLgFRD4NrHZSveYrD6i98pyvuzmS9yFYgOZa5JhaVqo0HPnci0+YuzSwT2fw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-linux-x64-musl@2.1.4: + resolution: {integrity: sha512-lvwvb2SQQHctHUKvBKptR6PLFCM7JfRjpCCrDaTmvB7EeZ5/dQJPhTYBf36BE/B4CRWR2ZiBLRYhK7hhXBCZAg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-linux-x64@2.1.4: + resolution: {integrity: sha512-Eoy9ycbhpJVYuR+LskV9s3uyaIkp89+qqgqhGQsWnp/I02Uqg2fXFblHJOpGZR8AxdB9ADy87oFVxn9MpFKUrw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-win32-arm64@2.1.4: + resolution: {integrity: sha512-3WRYte7orvyi6TRfIZkDN9Jzoogbv+gSvR+b9VOXUg1We1XrjBg6WljADeVEaKTvOcpVdH0a90TwyOQ6ue4fGw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@biomejs/cli-win32-x64@2.1.4: + resolution: {integrity: sha512-tBc+W7anBPSFXGAoQW+f/+svkpt8/uXfRwDzN1DvnatkRMt16KIYpEi/iw8u9GahJlFv98kgHcIrSsZHZTR0sw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@colors/colors@1.6.0: resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} From c1fdd542d5517af7598c966dd87ce95ab360b273 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 11 Aug 2025 14:44:48 -0400 Subject: [PATCH 05/35] chore(opapi): Run biome format --- opapi/biome.json | 100 ++++++++++++++++++++++----------------------- opapi/src/opapi.ts | 35 +++++++++++----- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/opapi/biome.json b/opapi/biome.json index c718e4dc..0abd37b7 100644 --- a/opapi/biome.json +++ b/opapi/biome.json @@ -1,52 +1,52 @@ { - "$schema": "https://biomejs.dev/schemas/2.1.4/schema.json", - "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, - "files": { "ignoreUnknown": false }, - "formatter": { - "enabled": true, - "formatWithErrors": false, - "indentStyle": "space", - "indentWidth": 2, - "lineEnding": "lf", - "lineWidth": 120, - "attributePosition": "auto", - "bracketSameLine": false, - "bracketSpacing": true, - "expand": "auto", - "useEditorconfig": true, - "includes": [ - "**", - "!**/*", - "**/", - "**/*.ts", - "**/*.tsx", - "**/*.json", - "**/*.yaml", - "**/*.yml", - "**/*.md", - "!**/pnpm-lock.yaml", - "!**/gen", - "!**/dist", - "!**/.botpress" - ] - }, - "linter": { "enabled": true, "rules": { "recommended": true } }, - "javascript": { - "formatter": { - "jsxQuoteStyle": "double", - "quoteProperties": "asNeeded", - "trailingCommas": "all", - "semicolons": "asNeeded", - "arrowParentheses": "always", - "bracketSameLine": false, - "quoteStyle": "single", - "attributePosition": "auto", - "bracketSpacing": true - } - }, - "html": { "formatter": { "selfCloseVoidElements": "always" } }, - "assist": { - "enabled": true, - "actions": { "source": { "organizeImports": "on" } } - } + "$schema": "https://biomejs.dev/schemas/2.1.4/schema.json", + "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, + "files": { "ignoreUnknown": false }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 120, + "attributePosition": "auto", + "bracketSameLine": false, + "bracketSpacing": true, + "expand": "auto", + "useEditorconfig": true, + "includes": [ + "**", + "!**/*", + "**/", + "**/*.ts", + "**/*.tsx", + "**/*.json", + "**/*.yaml", + "**/*.yml", + "**/*.md", + "!**/pnpm-lock.yaml", + "!**/gen", + "!**/dist", + "!**/.botpress" + ] + }, + "linter": { "enabled": true, "rules": { "recommended": true } }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "all", + "semicolons": "asNeeded", + "arrowParentheses": "always", + "bracketSameLine": false, + "quoteStyle": "single", + "attributePosition": "auto", + "bracketSpacing": true + } + }, + "html": { "formatter": { "selfCloseVoidElements": "always" } }, + "assist": { + "enabled": true, + "actions": { "source": { "organizeImports": "on" } } + } } diff --git a/opapi/src/opapi.ts b/opapi/src/opapi.ts index c098d1a3..20cc541c 100644 --- a/opapi/src/opapi.ts +++ b/opapi/src/opapi.ts @@ -1,16 +1,26 @@ -import { extendApi, OpenApiZodAny } from '@anatine/zod-openapi' +import { extendApi, type OpenApiZodAny } from '@anatine/zod-openapi' import { - generateClientWithOpenapiGenerator, generateClientWithOpapi, + generateClientWithOpenapiGenerator, generateErrorsFile, generateOpenapi, generateServer, generateTypesBySection, } from './generator' -import { addOperation } from './operation' -import { ApiError, ComponentType, createState, getRef, Metadata, Operation, Options, Parameter, State } from './state' -import { exportStateAsTypescript, ExportStateAsTypescriptOptions } from './generators/ts-state' +import { exportStateAsTypescript, type ExportStateAsTypescriptOptions } from './generators/ts-state' import { generateHandler } from './handler-generator' +import { addOperation } from './operation' +import { + type ApiError, + ComponentType, + createState, + getRef, + type Metadata, + type Operation, + type Options, + type Parameter, + type State, +} from './state' export { Operation, Parameter } from './state' type AnatineSchemaObject = NonNullable[1]> @@ -121,13 +131,16 @@ export namespace OpenApi { ) => createOpapiFromState(state as State) } -export type SchemaOf> = - O extends OpenApi ? Skema : never +export type SchemaOf> = O extends OpenApi + ? Skema + : never -export type ParameterOf> = - O extends OpenApi ? Param : never +export type ParameterOf> = O extends OpenApi + ? Param + : never -export type SectionOf> = - O extends OpenApi ? Sexion : never +export type SectionOf> = O extends OpenApi + ? Sexion + : never export { exportJsonSchemas, exportZodSchemas } from './handler-generator/export-schemas' From 3bd8c6d34b805bdeeef3d4cb32da386367b553bc Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 11 Aug 2025 14:49:18 -0400 Subject: [PATCH 06/35] chore(opapi): Modify the included files for format checking --- opapi/biome.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/opapi/biome.json b/opapi/biome.json index 0abd37b7..b6579400 100644 --- a/opapi/biome.json +++ b/opapi/biome.json @@ -15,9 +15,6 @@ "expand": "auto", "useEditorconfig": true, "includes": [ - "**", - "!**/*", - "**/", "**/*.ts", "**/*.tsx", "**/*.json", From dff60e608de641ef4b7c4e3a4813b4993736b6a4 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 11 Aug 2025 15:25:05 -0400 Subject: [PATCH 07/35] chore(opapi): Disable linter and assist --- opapi/biome.json | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/opapi/biome.json b/opapi/biome.json index b6579400..844dd5d6 100644 --- a/opapi/biome.json +++ b/opapi/biome.json @@ -9,7 +9,6 @@ "indentWidth": 2, "lineEnding": "lf", "lineWidth": 120, - "attributePosition": "auto", "bracketSameLine": false, "bracketSpacing": true, "expand": "auto", @@ -27,23 +26,18 @@ "!**/.botpress" ] }, - "linter": { "enabled": true, "rules": { "recommended": true } }, + "linter": { "enabled": false }, "javascript": { "formatter": { - "jsxQuoteStyle": "double", "quoteProperties": "asNeeded", "trailingCommas": "all", "semicolons": "asNeeded", "arrowParentheses": "always", "bracketSameLine": false, "quoteStyle": "single", - "attributePosition": "auto", "bracketSpacing": true } }, "html": { "formatter": { "selfCloseVoidElements": "always" } }, - "assist": { - "enabled": true, - "actions": { "source": { "organizeImports": "on" } } - } + "assist": { "enabled": false } } From 1ab9e0e1007aae227819c0a8702b675b067a37e0 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 11 Aug 2025 15:29:37 -0400 Subject: [PATCH 08/35] chore(opapi): Fix biome file includes --- opapi/biome.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opapi/biome.json b/opapi/biome.json index 844dd5d6..40295545 100644 --- a/opapi/biome.json +++ b/opapi/biome.json @@ -21,9 +21,9 @@ "**/*.yml", "**/*.md", "!**/pnpm-lock.yaml", - "!**/gen", - "!**/dist", - "!**/.botpress" + "!**/gen/**/*", + "!**/dist/**/*", + "!**/.botpress/**/*" ] }, "linter": { "enabled": false }, From 49c482117b17a29f199825828a1246072fdc7c9b Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 11 Aug 2025 16:16:36 -0400 Subject: [PATCH 09/35] core(opapi): Add test for handler-generator --- opapi/__mocks__/fs/promises.ts | 3 + opapi/foo.js | 203 ++++++++++++++++ opapi/package.json | 1 + opapi/pnpm-lock.yaml | 100 ++++++++ .../handler-generator/export-handler.test.ts | 223 ++++++++++++++++++ 5 files changed, 530 insertions(+) create mode 100644 opapi/__mocks__/fs/promises.ts create mode 100644 opapi/foo.js create mode 100644 opapi/src/handler-generator/export-handler.test.ts diff --git a/opapi/__mocks__/fs/promises.ts b/opapi/__mocks__/fs/promises.ts new file mode 100644 index 00000000..ce1183d7 --- /dev/null +++ b/opapi/__mocks__/fs/promises.ts @@ -0,0 +1,3 @@ +import { fs } from 'memfs' + +export default fs.promises diff --git a/opapi/foo.js b/opapi/foo.js new file mode 100644 index 00000000..e68cf723 --- /dev/null +++ b/opapi/foo.js @@ -0,0 +1,203 @@ +import qs from 'qs' +import { isAxiosError } from 'axios' +import { isApiError } from './errors' +import * as types from './typings' + +type RoutePart = + | { + type: 'argument' + name: string + } + | { + type: 'namespace' + name: string + } + +const tokenize = (path: string) => path.split('/').filter((x) => !!x) + +class Route { + private _parts: RoutePart[] + + public constructor(public readonly path: string) { + this._parts = this._parse(path) + } + + public match(path: string): Record | null { + const tokens = tokenize(path) + + if (tokens.length !== this._parts.length) { + return null + } + + const args: Record = {} + const zipped = tokens.map((token: string, index: number) => [token, this._parts[index]!] as const) + for (const [token, part] of zipped) { + if (part.type === 'argument') { + args[part.name] = token + continue + } + + if (part.name !== token) { + return null + } + } + + return args + } + + private _parse(path: string): RoutePart[] { + const tokens = tokenize(path) + const parts: RoutePart[] = [] + + for (const token of tokens) { + const match = /{(.+?)}/.exec(token) + if (match) { + parts.push({ + type: 'argument', + name: match[1]!, + }) + continue + } + + parts.push({ + type: 'namespace', + name: token, + }) + } + + return parts + } +} + +type RouteMatch = { path: string; params: Record } +export class Router { + private _parsed: Route[] = [] + + public constructor(routes: string[]) { + this._parsed = routes.map((route) => new Route(route)) + } + + public match(path: string): RouteMatch | null { + const matches: RouteMatch[] = [] + for (const route of this._parsed) { + const params = route.match(path) + if (params !== null) { + matches.push({ + path: route.path, + params, + }) + } + } + + if (matches.length === 0) { + return null + } + + // find the match with the least amount of params (i.e. the most specific match) + matches.sort((a, b) => Object.keys(a.params).length - Object.keys(b.params).length) + return matches[0]! + } +} + +const getErrorBody = (thrown: unknown) => { + if (isAxiosError(thrown)) { + const data = thrown.response?.data + const statusCode = thrown.response?.status + + if (!data) { + return `${thrown.message} (no response data) (Status Code: ${statusCode})` + } + + return `${data.message || data.error?.message || data.error || data.body || thrown.message} (Status Code: ${statusCode})` + } else if (thrown instanceof Error) { + return thrown.message + } + try { + return JSON.stringify(thrown) + } catch { + return 'Unknown error' + } +} + +type PlainRequest = { + body?: string; + path: string; + query: string; + method: string; + headers: { + [key: string]: string | undefined; + }; +} + +type PlainResponse = { + body?: string + headers?: { + [key: string]: string + } + status?: number +} + +export const handleRequest = async (routes: Record>>, props: T): Promise => { + try { + const router = new Router(Object.keys(routes)) + const match = router.match(props.req.path) + if (!match) { + return { + status: 404, + body: JSON.stringify({ message: "Route doesn't exist" }), + } + } + + const route = routes[match.path]! + const method = props.req.method.toLowerCase() + const leaf = route[method] + if (!leaf) { + return { + status: 404, + body: JSON.stringify({ message: "Method doesn't exist" }), + } + } + + let body: any + if (props.req.body) { + try { + body = JSON.parse(props.req.body) + } catch { + return { + status: 400, + body: JSON.stringify({ message: 'Invalid JSON body' }), + } + } + } + + const res = await leaf( + props, + { + path: match.path, + method, + body, + params: match.params, + headers: props.req.headers, + query: qs.parse(props.req.query) as Record, + } + ) + + return { + body: typeof res.body === 'string' ? res.body : JSON.stringify(res.body), + status: res.status ?? 200, + headers: res.headers, + } + } catch (thrown) { + if (isApiError(thrown)) { + return { + status: thrown.code, + body: JSON.stringify(thrown.toJSON()), + } + } + return { + status: 500, + body: getErrorBody(thrown), + } + } + } + diff --git a/opapi/package.json b/opapi/package.json index fc9aae51..f21f879a 100644 --- a/opapi/package.json +++ b/opapi/package.json @@ -30,6 +30,7 @@ "@types/node": "^22.16.4", "@types/qs": "^6.9.15", "@types/verror": "1.10.10", + "memfs": "^4.36.0", "prettier": "3.4.1", "ts-node": "10.9.2", "tsup": "8.3.5", diff --git a/opapi/pnpm-lock.yaml b/opapi/pnpm-lock.yaml index 7d7f4834..79b32d20 100644 --- a/opapi/pnpm-lock.yaml +++ b/opapi/pnpm-lock.yaml @@ -85,6 +85,9 @@ devDependencies: '@types/verror': specifier: 1.10.10 version: 1.10.10 + memfs: + specifier: ^4.36.0 + version: 4.36.0 prettier: specifier: 3.4.1 version: 3.4.1 @@ -757,6 +760,70 @@ packages: resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} dev: false + /@jsonjoy.com/base64@1.1.2(tslib@2.8.1): + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + dependencies: + tslib: 2.8.1 + dev: true + + /@jsonjoy.com/buffers@1.0.0(tslib@2.8.1): + resolution: {integrity: sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + dependencies: + tslib: 2.8.1 + dev: true + + /@jsonjoy.com/codegen@1.0.0(tslib@2.8.1): + resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + dependencies: + tslib: 2.8.1 + dev: true + + /@jsonjoy.com/json-pack@1.10.0(tslib@2.8.1): + resolution: {integrity: sha512-PMOU9Sh0baiLZEDewwR/YAHJBV2D8pPIzcFQSU7HQl/k/HNCDyVfO1OvkyDwBGp4dPtvZc7Hl9FFYWwTP1CbZw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/buffers': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 1.0.1(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.5.0(tslib@2.8.1) + tslib: 2.8.1 + dev: true + + /@jsonjoy.com/json-pointer@1.0.1(tslib@2.8.1): + resolution: {integrity: sha512-tJpwQfuBuxqZlyoJOSZcqf7OUmiYQ6MiPNmOv4KbZdXE/DdvBSSAwhos0zIlJU/AXxC8XpuO8p08bh2fIl+RKA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + dependencies: + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + tslib: 2.8.1 + dev: true + + /@jsonjoy.com/util@1.9.0(tslib@2.8.1): + resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + dependencies: + '@jsonjoy.com/buffers': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + tslib: 2.8.1 + dev: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2381,6 +2448,11 @@ packages: engines: {node: '>=16.17.0'} dev: false + /hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + dev: true + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false @@ -2635,6 +2707,16 @@ packages: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true + /memfs@4.36.0: + resolution: {integrity: sha512-mfBfzGUdoEw5AZwG8E965ej3BbvW2F9LxEWj4uLxF6BEh1dO2N9eS3AGu9S6vfenuQYrVjsbUOOZK7y3vz4vyQ==} + engines: {node: '>= 4.0.0'} + dependencies: + '@jsonjoy.com/json-pack': 1.10.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + tree-dump: 1.0.3(tslib@2.8.1) + tslib: 2.8.1 + dev: true + /memoizee@0.4.17: resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} engines: {node: '>=0.12'} @@ -3326,6 +3408,15 @@ packages: dependencies: any-promise: 1.3.0 + /thingies@2.5.0(tslib@2.8.1): + resolution: {integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + dependencies: + tslib: 2.8.1 + dev: true + /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: false @@ -3398,6 +3489,15 @@ packages: punycode: 2.3.1 dev: true + /tree-dump@1.0.3(tslib@2.8.1): + resolution: {integrity: sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + dependencies: + tslib: 2.8.1 + dev: true + /tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true diff --git a/opapi/src/handler-generator/export-handler.test.ts b/opapi/src/handler-generator/export-handler.test.ts new file mode 100644 index 00000000..fc1d34ab --- /dev/null +++ b/opapi/src/handler-generator/export-handler.test.ts @@ -0,0 +1,223 @@ +import { beforeEach, describe, it, vi, expect } from 'vitest' +import { exportHandler } from './export-handler' +import { fs, vol } from 'memfs' + +vi.mock('fs/promises') + +describe('export-handler', () => { + beforeEach(() => { + vol.reset() + }) + + it('should write the content to the provided file', async () => { + await exportHandler('/handler.ts') + const content = await fs.promises.readFile('/handler.ts').then(String) + + expect(content).toMatchInlineSnapshot(` + "import qs from 'qs' + import { isAxiosError } from 'axios' + import { isApiError } from './errors' + import * as types from './typings' + + type RoutePart = + | { + type: 'argument' + name: string + } + | { + type: 'namespace' + name: string + } + + const tokenize = (path: string) => path.split('/').filter((x) => !!x) + + class Route { + private _parts: RoutePart[] + + public constructor(public readonly path: string) { + this._parts = this._parse(path) + } + + public match(path: string): Record | null { + const tokens = tokenize(path) + + if (tokens.length !== this._parts.length) { + return null + } + + const args: Record = {} + const zipped = tokens.map((token: string, index: number) => [token, this._parts[index]!] as const) + for (const [token, part] of zipped) { + if (part.type === 'argument') { + args[part.name] = token + continue + } + + if (part.name !== token) { + return null + } + } + + return args + } + + private _parse(path: string): RoutePart[] { + const tokens = tokenize(path) + const parts: RoutePart[] = [] + + for (const token of tokens) { + const match = /{(.+?)}/.exec(token) + if (match) { + parts.push({ + type: 'argument', + name: match[1]!, + }) + continue + } + + parts.push({ + type: 'namespace', + name: token, + }) + } + + return parts + } + } + + type RouteMatch = { path: string; params: Record } + export class Router { + private _parsed: Route[] = [] + + public constructor(routes: string[]) { + this._parsed = routes.map((route) => new Route(route)) + } + + public match(path: string): RouteMatch | null { + const matches: RouteMatch[] = [] + for (const route of this._parsed) { + const params = route.match(path) + if (params !== null) { + matches.push({ + path: route.path, + params, + }) + } + } + + if (matches.length === 0) { + return null + } + + // find the match with the least amount of params (i.e. the most specific match) + matches.sort((a, b) => Object.keys(a.params).length - Object.keys(b.params).length) + return matches[0]! + } + } + + const getErrorBody = (thrown: unknown) => { + if (isAxiosError(thrown)) { + const data = thrown.response?.data + const statusCode = thrown.response?.status + + if (!data) { + return \`\${thrown.message} (no response data) (Status Code: \${statusCode})\` + } + + return \`\${data.message || data.error?.message || data.error || data.body || thrown.message} (Status Code: \${statusCode})\` + } else if (thrown instanceof Error) { + return thrown.message + } + try { + return JSON.stringify(thrown) + } catch { + return 'Unknown error' + } + } + + type PlainRequest = { + body?: string; + path: string; + query: string; + method: string; + headers: { + [key: string]: string | undefined; + }; + } + + type PlainResponse = { + body?: string + headers?: { + [key: string]: string + } + status?: number + } + + export const handleRequest = async (routes: Record>>, props: T): Promise => { + try { + const router = new Router(Object.keys(routes)) + const match = router.match(props.req.path) + if (!match) { + return { + status: 404, + body: JSON.stringify({ message: "Route doesn't exist" }), + } + } + + const route = routes[match.path]! + const method = props.req.method.toLowerCase() + const leaf = route[method] + if (!leaf) { + return { + status: 404, + body: JSON.stringify({ message: "Method doesn't exist" }), + } + } + + let body: any + if (props.req.body) { + try { + body = JSON.parse(props.req.body) + } catch { + return { + status: 400, + body: JSON.stringify({ message: 'Invalid JSON body' }), + } + } + } + + const res = await leaf( + props, + { + path: match.path, + method, + body, + params: match.params, + headers: props.req.headers, + query: qs.parse(props.req.query) as Record, + } + ) + + return { + body: typeof res.body === 'string' ? res.body : JSON.stringify(res.body), + status: res.status ?? 200, + headers: res.headers, + } + } catch (thrown) { + if (isApiError(thrown)) { + return { + status: thrown.code, + body: JSON.stringify(thrown.toJSON()), + } + } + return { + status: 500, + body: getErrorBody(thrown), + } + } + } + + " + `) + }) +}) From ddb8f6678b3cfe35d8a7e65c8c17d66b9f474755 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 18 Aug 2025 18:42:27 -0400 Subject: [PATCH 10/35] Make exportHandler test fail --- opapi/src/handler-generator/export-handler.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/opapi/src/handler-generator/export-handler.ts b/opapi/src/handler-generator/export-handler.ts index dbfc802c..eae3ba22 100644 --- a/opapi/src/handler-generator/export-handler.ts +++ b/opapi/src/handler-generator/export-handler.ts @@ -1,4 +1,5 @@ import fs from 'fs/promises' +import * as ts from 'typescript' const CONTENT = `import qs from 'qs' import { isAxiosError } from 'axios' @@ -206,5 +207,14 @@ export const handleRequest = async (routes: Rec ` export const exportHandler = async (outFile: string) => { - await fs.writeFile(outFile, CONTENT) + const printer = ts.createPrinter() + const y = ts.factory.createAdd(ts.factory.createNumericLiteral(1), ts.factory.createNumericLiteral(2)) + const z = ts.factory.createImport + const output = printer.printNode( + ts.EmitHint.Unspecified, + y, + ts.createSourceFile('source.ts', '', ts.ScriptTarget.ES2015) + ) + + await fs.writeFile(outFile, output) } From 9b1f5b8301fa4f268dc8c700392c04067ff9a1cf Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 17:32:42 -0400 Subject: [PATCH 11/35] Skip some tests that make network calls --- opapi/test/client.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/opapi/test/client.test.ts b/opapi/test/client.test.ts index 809c924c..230e8e8d 100644 --- a/opapi/test/client.test.ts +++ b/opapi/test/client.test.ts @@ -5,7 +5,8 @@ import { getFiles } from '../src/file' import { validateTypescriptFile } from './util' describe('client generator', () => { - it('should be able to generate a client with openapi-generator', async () => { + // Makes a call to api.openapi-generator.tech ??? + it.skip('should be able to generate a client with openapi-generator', async () => { const genClientFolder = join(__dirname, 'gen/client-openapi-generator') const api = getMockApi() @@ -21,13 +22,14 @@ describe('client generator', () => { }) }) + // Makes a call to api.openapi-generator.tech ??? it('should be able to generate a client with opapi', async () => { const genClientFolder = join(__dirname, 'gen/client-opapi') const api = getMockApi() await api.exportClient(genClientFolder, { - generator: 'opapi', + generator: 'opapi' }) const files = getFiles(genClientFolder) From 64767dfcf017c56fc1d983d8d112b807d62fd721 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 17:33:04 -0400 Subject: [PATCH 12/35] Fix build warnings --- opapi/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opapi/package.json b/opapi/package.json index f21f879a..a216a2dc 100644 --- a/opapi/package.json +++ b/opapi/package.json @@ -7,9 +7,9 @@ "types": "./dist/index.d.ts", "exports": { ".": { + "types": "./dist/index.d.ts", "require": "./dist/index.js", - "import": "./dist/index.mjs", - "types": "./dist/index.d.ts" + "import": "./dist/index.mjs" } }, "scripts": { From 824be5db0afe0215e72de2c761fe2ce287eff826 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 17:33:57 -0400 Subject: [PATCH 13/35] Restore export handler function --- opapi/src/handler-generator/export-handler.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/opapi/src/handler-generator/export-handler.ts b/opapi/src/handler-generator/export-handler.ts index eae3ba22..dbfc802c 100644 --- a/opapi/src/handler-generator/export-handler.ts +++ b/opapi/src/handler-generator/export-handler.ts @@ -1,5 +1,4 @@ import fs from 'fs/promises' -import * as ts from 'typescript' const CONTENT = `import qs from 'qs' import { isAxiosError } from 'axios' @@ -207,14 +206,5 @@ export const handleRequest = async (routes: Rec ` export const exportHandler = async (outFile: string) => { - const printer = ts.createPrinter() - const y = ts.factory.createAdd(ts.factory.createNumericLiteral(1), ts.factory.createNumericLiteral(2)) - const z = ts.factory.createImport - const output = printer.printNode( - ts.EmitHint.Unspecified, - y, - ts.createSourceFile('source.ts', '', ts.ScriptTarget.ES2015) - ) - - await fs.writeFile(outFile, output) + await fs.writeFile(outFile, CONTENT) } From f69b58b47c08804211c2d8011fa24cdad0c113c8 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 17:44:19 -0400 Subject: [PATCH 14/35] Bump @anatine/zod-openapi and openapi3-ts --- opapi/package.json | 4 +-- opapi/pnpm-lock.yaml | 37 ++++++++++---------- opapi/src/openapi.ts | 56 +++++++++++++++---------------- opapi/test/export-schemas.test.ts | 36 ++++++++++++-------- 4 files changed, 71 insertions(+), 62 deletions(-) diff --git a/opapi/package.json b/opapi/package.json index a216a2dc..e94449b4 100644 --- a/opapi/package.json +++ b/opapi/package.json @@ -40,7 +40,7 @@ "vitest": "3.2.4" }, "dependencies": { - "@anatine/zod-openapi": "1.12.1", + "@anatine/zod-openapi": "2.2.8", "@readme/openapi-parser": "2.6.0", "axios": "1.7.8", "chalk": "4.1.2", @@ -50,7 +50,7 @@ "json-schema-to-zod": "1.1.1", "lodash": "^4.17.21", "openapi-typescript": "6.7.6", - "openapi3-ts": "2.0.2", + "openapi3-ts": "4.5.0", "radash": "12.1.0", "tsconfig-paths": "4.2.0", "verror": "1.10.1", diff --git a/opapi/pnpm-lock.yaml b/opapi/pnpm-lock.yaml index 79b32d20..45c3d0e7 100644 --- a/opapi/pnpm-lock.yaml +++ b/opapi/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@anatine/zod-openapi': - specifier: 1.12.1 - version: 1.12.1(openapi3-ts@2.0.2)(zod@3.22.4) + specifier: 2.2.8 + version: 2.2.8(openapi3-ts@4.5.0)(zod@3.22.4) '@readme/openapi-parser': specifier: 2.6.0 version: 2.6.0(openapi-types@12.1.3) @@ -36,8 +36,8 @@ dependencies: specifier: 6.7.6 version: 6.7.6 openapi3-ts: - specifier: 2.0.2 - version: 2.0.2 + specifier: 4.5.0 + version: 4.5.0 radash: specifier: 12.1.0 version: 12.1.0 @@ -112,14 +112,14 @@ devDependencies: packages: - /@anatine/zod-openapi@1.12.1(openapi3-ts@2.0.2)(zod@3.22.4): - resolution: {integrity: sha512-scMpuku9VkDG4NahlnTRQcxlNZP7RGFpjKTOYkteZl/P8SSNaTogyOB1DRak/efQhyJryUAno3wvMVY9WBNxGA==} + /@anatine/zod-openapi@2.2.8(openapi3-ts@4.5.0)(zod@3.22.4): + resolution: {integrity: sha512-iyM8mB556KdiZ6a1GTZ67ACLnJakU1hrzzXoh7PLaReldAdMq88MlZn/Ir/U56/TBuQctBhh/4seo0b0B343uw==} peerDependencies: - openapi3-ts: ^2.0.0 || ^3.0.0 + openapi3-ts: ^4.1.2 zod: ^3.20.0 dependencies: - openapi3-ts: 2.0.2 - ts-deepmerge: 4.0.0 + openapi3-ts: 4.5.0 + ts-deepmerge: 6.2.1 zod: 3.22.4 dev: false @@ -2879,10 +2879,10 @@ packages: yargs-parser: 21.1.1 dev: false - /openapi3-ts@2.0.2: - resolution: {integrity: sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==} + /openapi3-ts@4.5.0: + resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==} dependencies: - yaml: 1.10.2 + yaml: 2.8.1 dev: false /package-json-from-dist@1.0.0: @@ -3508,9 +3508,9 @@ packages: engines: {node: '>= 14.0.0'} dev: false - /ts-deepmerge@4.0.0: - resolution: {integrity: sha512-IrjjAwfM/J6ajWv5wDRZBdpVaTmuONJN1vC85mXlWVPXKelouLFiqsjR7m0h245qY6zZEtcDtcOTc4Rozgg1TQ==} - engines: {node: '>=14'} + /ts-deepmerge@6.2.1: + resolution: {integrity: sha512-8CYSLazCyj0DJDpPIxOFzJG46r93uh6EynYjuey+bxcLltBeqZL7DMfaE5ZPzZNFlav7wx+2TDa/mBl8gkTYzw==} + engines: {node: '>=14.13.1'} dev: false /ts-interface-checker@0.1.13: @@ -3884,9 +3884,10 @@ packages: engines: {node: '>=0.4'} dev: false - /yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} + /yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true dev: false /yargs-parser@21.1.1: diff --git a/opapi/src/openapi.ts b/opapi/src/openapi.ts index 55f84e09..0dc76207 100644 --- a/opapi/src/openapi.ts +++ b/opapi/src/openapi.ts @@ -1,4 +1,4 @@ -import { OpenApiBuilder, OperationObject, ReferenceObject } from 'openapi3-ts' +import { OpenApiBuilder, OperationObject, ReferenceObject } from 'openapi3-ts/oas31' import VError from 'verror' import { defaultResponseStatus } from './const' import { generateSchemaFromZod } from './jsonschema' @@ -9,9 +9,9 @@ import { formatBodyName, formatResponseName } from './util' export const createOpenapi = < SchemaName extends string, DefaultParameterName extends string, - SectionName extends string, + SectionName extends string >( - state: State, + state: State ) => { const { metadata, schemas, operations } = state const { description, server, title, version } = metadata @@ -22,15 +22,15 @@ export const createOpenapi = < info: { title, description, - version, + version }, paths: {}, components: { schemas: {}, responses: {}, requestBodies: {}, - parameters: {}, - }, + parameters: {} + } }) objects.entries(schemas).forEach(([schemaName, { schema }]) => { @@ -47,13 +47,13 @@ export const createOpenapi = < description: response.description, content: { 'application/json': { - schema: response.schema, - }, - }, + schema: response.schema + } + } }) const responseRefSchema = generateSchemaFromZod( - getRef(state, ComponentType.RESPONSES, responseName), + getRef(state, ComponentType.RESPONSES, responseName) ) as unknown as ReferenceObject const operation: OperationObject = { @@ -62,8 +62,8 @@ export const createOpenapi = < parameters: [], responses: { default: responseRefSchema as ReferenceObject, - [response.status ?? defaultResponseStatus]: responseRefSchema as ReferenceObject, - }, + [response.status ?? defaultResponseStatus]: responseRefSchema as ReferenceObject + } } if (isOperationWithBodyProps(operationObject)) { @@ -74,13 +74,13 @@ export const createOpenapi = < description: requestBody.description, content: { [contentType]: { - schema: requestBody.schema, - }, - }, + schema: requestBody.schema + } + } }) const bodyRefSchema = generateSchemaFromZod( - getRef(state, ComponentType.REQUESTS, bodyName), + getRef(state, ComponentType.REQUESTS, bodyName) ) as unknown as ReferenceObject operation.requestBody = bodyRefSchema @@ -99,8 +99,8 @@ export const createOpenapi = < required: parameter.in === 'path' ? true : parameter.required, schema: { type: 'string', - enum: parameter.enum as string[], - }, + enum: parameter.enum as string[] + } }) break case 'string[]': @@ -113,9 +113,9 @@ export const createOpenapi = < type: 'array', items: { type: 'string', - enum: parameter.enum, - }, - }, + enum: parameter.enum + } + } }) break case 'object': @@ -124,7 +124,7 @@ export const createOpenapi = < in: parameter.in, description: parameter.description, required: parameter.required, - schema: parameter.schema, + schema: parameter.schema }) break case 'boolean': @@ -134,8 +134,8 @@ export const createOpenapi = < description: parameter.description, required: parameter.required, schema: { - type: 'boolean', - }, + type: 'boolean' + } }) break case 'integer': @@ -145,8 +145,8 @@ export const createOpenapi = < description: parameter.description, required: parameter.required, schema: { - type: 'integer', - }, + type: 'integer' + } }) break case 'number': @@ -156,8 +156,8 @@ export const createOpenapi = < description: parameter.description, required: parameter.required, schema: { - type: 'number', - }, + type: 'number' + } }) break default: diff --git a/opapi/test/export-schemas.test.ts b/opapi/test/export-schemas.test.ts index 27c132df..31156242 100644 --- a/opapi/test/export-schemas.test.ts +++ b/opapi/test/export-schemas.test.ts @@ -19,10 +19,18 @@ const assert = async (genFolder: string, exporter: (outDir: string) => Promise basename(f))) @@ -43,25 +51,25 @@ describe('schemas generator', () => { id: { anyOf: [ { - type: 'string', + type: 'string' }, { - type: 'number', - }, - ], - }, + type: 'number' + } + ] + } }, - required: ['name'], + required: ['name'] }, ticket: { type: 'object', properties: { title: { type: 'string' }, - content: { type: 'string' }, + content: { type: 'string' } }, - required: ['title'], - }, - }), + required: ['title'] + } + }) ) }) @@ -73,13 +81,13 @@ describe('schemas generator', () => { user: z.object({ name: z.string(), age: z.number().optional(), - id: z.union([z.string(), z.number()]), + id: z.union([z.string(), z.number()]) }), ticket: z.object({ title: z.string(), - content: z.string().optional(), - }), - }), + content: z.string().optional() + }) + }) ) }) }) From 084789f0eedd941132025dbb749f253c8d1a3f49 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 17:53:28 -0400 Subject: [PATCH 15/35] Rename state.test.ts to opapi.test.ts --- opapi/test/{state.test.ts => opapi.test.ts} | 62 ++++++++++----------- 1 file changed, 31 insertions(+), 31 deletions(-) rename opapi/test/{state.test.ts => opapi.test.ts} (86%) diff --git a/opapi/test/state.test.ts b/opapi/test/opapi.test.ts similarity index 86% rename from opapi/test/state.test.ts rename to opapi/test/opapi.test.ts index f6a06bc6..b901f97d 100644 --- a/opapi/test/state.test.ts +++ b/opapi/test/opapi.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import z from 'zod' -import { OpenApi, OpenApiProps } from '../src' +import { OpenApi, type OpenApiProps } from '../src' import { join } from 'path' import { getFiles } from '../src/file' import { validateTypescriptFile } from './util' @@ -12,25 +12,25 @@ const metadata = { description: 'Test API', server: 'http://localhost:3000', version: '1.0.0', - prefix: '/v1', + prefix: '/v1' } satisfies AnyProps['metadata'] const sections = { trees: { title: 'Trees', - description: 'Trees section', - }, + description: 'Trees section' + } } satisfies AnyProps['sections'] const leaf: z.ZodType = z.object({ type: z.literal('leaf'), name: z.string(), - data: z.string(), + data: z.string() }) const node: z.ZodType = z.object({ type: z.literal('node'), name: z.string(), - children: z.array(z.lazy(() => tree)), + children: z.array(z.lazy(() => tree)) }) const tree: z.ZodType = z.union([leaf, node]) @@ -46,9 +46,9 @@ describe('openapi generator with unions not allowed', () => { schemas: { Tree: { section: 'trees', - schema: tree, - }, - }, + schema: tree + } + } }) }).toThrowError(expectedErrorMessage) }) @@ -65,13 +65,13 @@ describe('openapi generator with unions not allowed', () => { id: { description: 'Tree id', in: 'path', - type: 'string', - }, + type: 'string' + } }, response: { description: 'Tree information', - schema: tree, - }, + schema: tree + } }) }).toThrowError(expectedErrorMessage) }) @@ -86,12 +86,12 @@ describe('openapi generator with unions not allowed', () => { path: '/trees', requestBody: { description: 'Tree information', - schema: tree, + schema: tree }, response: { description: 'Tree information', - schema: z.object({}), - }, + schema: z.object({}) + } }) }).toThrowError(expectedErrorMessage) }) @@ -107,11 +107,11 @@ describe('openapi generator with unions allowed', () => { schemas: { Tree: { section: 'trees', - schema: tree, - }, - }, + schema: tree + } + } }, - opts, + opts ) }) @@ -126,13 +126,13 @@ describe('openapi generator with unions allowed', () => { id: { description: 'Tree id', in: 'path', - type: 'string', - }, + type: 'string' + } }, response: { description: 'Tree information', - schema: tree, - }, + schema: tree + } }) }) @@ -145,12 +145,12 @@ describe('openapi generator with unions allowed', () => { path: '/trees', requestBody: { description: 'Tree information', - schema: tree, + schema: tree }, response: { description: 'Tree information', - schema: z.object({}), - }, + schema: z.object({}) + } }) }) }) @@ -164,11 +164,11 @@ describe('openapi state generator', () => { schemas: { Tree: { section: 'trees', - schema: tree, - }, - }, + schema: tree + } + } }, - { allowUnions: true }, + { allowUnions: true } ) const genStateFolder = join(__dirname, 'gen/state') From ee97c2c72635f21d2f06fea4e541cca4502463a4 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 17:57:04 -0400 Subject: [PATCH 16/35] Add empty state tests --- opapi/src/state.ts | 36 ++++++++++++++++++------------------ opapi/test/state.test.ts | 11 +++++++++++ 2 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 opapi/test/state.test.ts diff --git a/opapi/src/state.ts b/opapi/src/state.ts index 2ec572cb..5ed427a0 100644 --- a/opapi/src/state.ts +++ b/opapi/src/state.ts @@ -1,4 +1,4 @@ -import type { SchemaObject } from 'openapi3-ts' +import type { SchemaObject } from 'openapi3-ts/oas31' import { VError } from 'verror' import { z } from 'zod' import { schema } from './opapi' @@ -34,13 +34,13 @@ export type State = { // Method of the operation method: OperationsWithBodyMethod @@ -149,7 +149,7 @@ export type OperationWithoutBodyProps< DefaultParameterName extends string, SectionName extends string, Path extends string = string, - S extends SchemaType = 'zod-schema', + S extends SchemaType = 'zod-schema' > = { // Method of the operation method: OperationWithoutBodyMethod @@ -159,7 +159,7 @@ export type Operation< DefaultParameterName extends string, SectionName extends string, Path extends string = string, - S extends SchemaType = 'zod-schema', + S extends SchemaType = 'zod-schema' > = | OperationWithBodyProps | OperationWithoutBodyProps @@ -168,9 +168,9 @@ export function isOperationWithBodyProps< DefaultParameterName extends string, SectionName extends string, Path extends string, - TypeOfSchema extends SchemaType = 'json-schema', + TypeOfSchema extends SchemaType = 'json-schema' >( - operation: Operation, + operation: Operation ): operation is OperationWithBodyProps { if ((operationsWithBodyMethod as any as string[]).includes(operation.method)) { return true @@ -182,7 +182,7 @@ export enum ComponentType { SCHEMAS = 'schemas', RESPONSES = 'responses', REQUESTS = 'requestBodies', - PARAMETERS = 'parameters', + PARAMETERS = 'parameters' } export type ParametersMap = Record< @@ -195,7 +195,7 @@ type BaseOperationProps< DefaultParameterName extends string, SectionName extends string, Path extends string = string, - S extends SchemaType = 'zod-schema', + S extends SchemaType = 'zod-schema' > = { // Name of the operation name: string @@ -230,7 +230,7 @@ type CreateStateProps( props: CreateStateProps, - opts: Partial = {}, + opts: Partial = {} ): State { const options = { ...DEFAULT_OPTIONS, ...opts } @@ -238,7 +238,7 @@ export function createState(props.schemas).map(([name, data]) => ({ name, schema: data.schema, - section: data.section, + section: data.section })) : [] @@ -248,7 +248,7 @@ export function createState(obj: Record): [K, T][] => Object.entries(obj) as [K, T][] @@ -258,7 +258,7 @@ export function createState schemaEntry.section === name)?.name, + schema: schemaEntries.find((schemaEntry) => schemaEntry.section === name)?.name })) : [] @@ -275,7 +275,7 @@ export function createState, type: ComponentType type: undefined, properties: undefined, required: undefined, - $ref: `#/components/${type}/${name}`, + $ref: `#/components/${type}/${name}` }) } @@ -334,7 +334,7 @@ export const mapParameter = (param: Parameter<'zod-schema'>): Parameter<'json-sc if ('schema' in param) { return { ...param, - schema: generateSchemaFromZod(param.schema), + schema: generateSchemaFromZod(param.schema) } } return param diff --git a/opapi/test/state.test.ts b/opapi/test/state.test.ts new file mode 100644 index 00000000..d875291d --- /dev/null +++ b/opapi/test/state.test.ts @@ -0,0 +1,11 @@ +import { describe } from 'vitest' + +describe('state', () => { + describe.skip('operationsWithBodyMethod') + describe.skip('operationsWithoutBodyMethod') + describe.skip('isOperationWithBodyProps') + describe.skip('ComponentType') + describe.skip('createState') + describe.skip('getRef') + describe.skip('mapParameter') +}) From bd59eca41a43136b2275ff82344f5f2bba70c6a8 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 17:57:27 -0400 Subject: [PATCH 17/35] Fix export of OpenApiZodAny --- opapi/src/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/opapi/src/index.ts b/opapi/src/index.ts index f9ae7537..f1ed831e 100644 --- a/opapi/src/index.ts +++ b/opapi/src/index.ts @@ -1,4 +1,5 @@ -export { OpenApiZodAny } from '@anatine/zod-openapi' +export type { OpenApiZodAny } from '@anatine/zod-openapi' + export { schema, type OpenApiProps, @@ -10,7 +11,7 @@ export { ParameterOf, SectionOf, exportJsonSchemas, - exportZodSchemas, + exportZodSchemas } from './opapi' export { type Options, @@ -36,5 +37,5 @@ export { type ParametersMap, createState, getRef, - mapParameter, + mapParameter } from './state' From 91bdc87794ae12ef8b3825301f832e89e38b8050 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 18:04:43 -0400 Subject: [PATCH 18/35] Fix VError import --- opapi/src/generator.ts | 22 +++++++++++----------- opapi/src/operation.ts | 26 +++++++++++++------------- opapi/src/state.ts | 2 +- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/opapi/src/generator.ts b/opapi/src/generator.ts index 839d6c34..ffa469ad 100644 --- a/opapi/src/generator.ts +++ b/opapi/src/generator.ts @@ -2,7 +2,7 @@ import pathlib from 'path' import fslib from 'fs' import chalk from 'chalk' import _ from 'lodash' -import { VError } from 'verror' +import VError from 'verror' import { defaultResponseStatus, invalidLine, tsFileHeader } from './const' import { appendHeaders, initDirectory, removeLineFromFiles, saveFile } from './file' import { @@ -12,7 +12,7 @@ import { generateHandlers, generateTypes, runOpenApiCodeGenerator, - clientNode, + clientNode } from './generators' import { generateErrors } from './generators/errors' import { generateOpenapiTypescript } from './generators/openapi-typescript' @@ -26,7 +26,7 @@ import { executeOperationParsers, executeSectionParsers, operationParsers, - sectionParsers, + sectionParsers } from './section-types-generator' import { Block } from './section-types-generator/types' import { ApiError, isOperationWithBodyProps, type Operation, type State } from './state' @@ -41,7 +41,7 @@ export async function generateTypesBySection(state: DefaultState, targetDirector for (const section of state.sections) { const [sectionBlocks, operationBlocks] = await Promise.all([ executeSectionParsers(sectionParsers, section, state), - executeOperationParsers(operationParsers, section, state), + executeOperationParsers(operationParsers, section, state) ]) const blocks = [...sectionBlocks, ...operationBlocks] allBlocks.push(...blocks) @@ -73,9 +73,9 @@ export const generateServer = async (state: State, dir: log.info('Generating handlers code') const handlersCode = generateHandlers({ operations: Object.entries(state.operations).map(([name, operation]) => - mapOperationPropsToHandlerProps(name, operation), + mapOperationPropsToHandlerProps(name, operation) ), - useExpressTypes, + useExpressTypes }) log.info('') @@ -106,7 +106,7 @@ export const generateClientWithOpenapiGenerator = async ( state: State, dir = '.', openApiGeneratorEndpoint: string, - postProcessors?: OpenApiPostProcessors, + postProcessors?: OpenApiPostProcessors ) => { initDirectory(dir) @@ -123,8 +123,8 @@ export const generateClientWithOpenapiGenerator = async ( log.info('Generating client code') const clientCode = generateClientCode({ operations: Object.entries(state.operations).map(([name, operation]) => - mapOperationPropsToHandlerProps(name, operation), - ), + mapOperationPropsToHandlerProps(name, operation) + ) }) log.info('') @@ -219,7 +219,7 @@ export function generateOpenapi(state: State, dir = '.') function mapOperationPropsToHandlerProps( operationName: string, - operation: Operation, + operation: Operation ) { const generateHandlerProps: GenerateHandlerProps = { operationName, @@ -233,7 +233,7 @@ function mapOperationPropsToHandlerProps( isEmptyBody: isOperationWithBodyProps(operation) ? schemaIsEmptyObject(operation.requestBody.schema) : true, contentType: isOperationWithBodyProps(operation) ? (operation.contentType ?? 'application/json') - : 'application/json', + : 'application/json' } if (operation.parameters) { diff --git a/opapi/src/operation.ts b/opapi/src/operation.ts index c87fb2d1..55280a21 100644 --- a/opapi/src/operation.ts +++ b/opapi/src/operation.ts @@ -1,5 +1,5 @@ import { extendApi } from '@anatine/zod-openapi' -import { VError } from 'verror' +import VError from 'verror' import { generateSchemaFromZod } from './jsonschema' import { objects } from './objects' import { @@ -8,17 +8,17 @@ import { ParametersMap, State, isOperationWithBodyProps, - mapParameter, + mapParameter } from './state' import { formatBodyName, formatResponseName, isAlphanumeric } from './util' export const addOperation = < SchemaName extends string, DefaultParameterName extends string, - SectionName extends string, + SectionName extends string >( state: State, - operationProps: Operation, + operationProps: Operation ) => { const { name } = operationProps const responseName = formatResponseName(name) @@ -27,7 +27,7 @@ export const addOperation = < const parameters = createParameters( operationProps.parameters ? objects.mapValues(operationProps.parameters, mapParameter) : undefined, state.defaultParameters, - operationProps.disableDefaultParameters, + operationProps.disableDefaultParameters ) if (operationProps.path[0] !== '/') { @@ -51,8 +51,8 @@ export const addOperation = < status: operationProps.response.status, schema: generateSchemaFromZod( extendApi(operationProps.response.schema, { title: responseName, format: operationProps.response.format }), - state.options, - ), + state.options + ) } let operation: Operation @@ -67,9 +67,9 @@ export const addOperation = < description: operationProps.requestBody.description, schema: generateSchemaFromZod( extendApi(operationProps.requestBody.schema, { title: bodyName, format: operationProps.requestBody?.format }), - state.options, - ), - }, + state.options + ) + } } } else { operation = { @@ -77,7 +77,7 @@ export const addOperation = < method: operationProps.method as OperationWithoutBodyMethod, parameters, path, - response, + response } } @@ -91,7 +91,7 @@ export const addOperation = < function createParameters( parameters: ParametersMap = {}, defaultParameters: ParametersMap = {}, - disableDefaultParameters: { [name in DefaultParameterNames]?: boolean } = {}, + disableDefaultParameters: { [name in DefaultParameterNames]?: boolean } = {} ): ParametersMap { const params: ParametersMap = parameters @@ -112,7 +112,7 @@ function validateParametersInPath(path: string, parameters?: ParametersMap Date: Fri, 12 Sep 2025 18:37:05 -0400 Subject: [PATCH 19/35] Add simple opapi api --- opapi/package.json | 2 +- opapi/src/jsonschema.ts | 16 ++++---- opapi/src/opapi.ts | 86 ++++++++++++++++++++++++++++++++--------- 3 files changed, 77 insertions(+), 27 deletions(-) diff --git a/opapi/package.json b/opapi/package.json index e94449b4..3682d1f8 100644 --- a/opapi/package.json +++ b/opapi/package.json @@ -14,7 +14,7 @@ }, "scripts": { "test": "vitest run", - "build": "tsup src/index.ts --dts --format cjs,esm --clean", + "build": "tsup src/index.ts --dts --format cjs,esm --clean --sourcemap", "check:type": "tsc --noEmit", "check:format": "biome format .", "fix:format": "biome format --write ." diff --git a/opapi/src/jsonschema.ts b/opapi/src/jsonschema.ts index e6153049..457ece98 100644 --- a/opapi/src/jsonschema.ts +++ b/opapi/src/jsonschema.ts @@ -1,6 +1,6 @@ import { OpenApiZodAny, generateSchema as generateJsonSchema } from '@anatine/zod-openapi' import { JSONSchema7, JSONSchema7Definition } from 'json-schema' -import type { SchemaObject } from 'openapi3-ts' +import type { SchemaObject } from 'openapi3-ts/oas31' import { removeFromArray } from './util' import _ from 'lodash' @@ -37,7 +37,7 @@ export const formatJsonSchema = (jsonSchema: SchemaObject, allowUnions: boolean) } Object.entries(jsonSchema.properties ?? {}).forEach(([_, value]) => - formatJsonSchema(value as SchemaObject, allowUnions), + formatJsonSchema(value as SchemaObject, allowUnions) ) } @@ -101,7 +101,7 @@ export const exploreJsonSchema = if (Array.isArray(mappedSchema.items)) { return { ...mappedSchema, - items: mappedSchema.items.map(exploreJsonSchemaDef(cb)), + items: mappedSchema.items.map(exploreJsonSchemaDef(cb)) } } return { ...mappedSchema, items: exploreJsonSchemaDef(cb)(mappedSchema.items) } @@ -110,21 +110,21 @@ export const exploreJsonSchema = if (mappedSchema.anyOf) { return { ...mappedSchema, - anyOf: mappedSchema.anyOf.map(exploreJsonSchemaDef(cb)), + anyOf: mappedSchema.anyOf.map(exploreJsonSchemaDef(cb)) } } if (mappedSchema.allOf) { return { ...mappedSchema, - allOf: mappedSchema.allOf.map(exploreJsonSchemaDef(cb)), + allOf: mappedSchema.allOf.map(exploreJsonSchemaDef(cb)) } } if (mappedSchema.oneOf) { return { ...mappedSchema, - oneOf: mappedSchema.oneOf.map(exploreJsonSchemaDef(cb)), + oneOf: mappedSchema.oneOf.map(exploreJsonSchemaDef(cb)) } } @@ -170,7 +170,7 @@ const _setDefaultAdditionalPropertiesInPlace = (schema: JsonSchema, additionalPr schema.additionalProperties ??= additionalProperties Object.values(schema.properties ?? {}).forEach( - (s) => typeof s === 'object' && _setDefaultAdditionalPropertiesInPlace(s, additionalProperties), + (s) => typeof s === 'object' && _setDefaultAdditionalPropertiesInPlace(s, additionalProperties) ) if (typeof schema.additionalProperties === 'object') { @@ -187,7 +187,7 @@ const _setDefaultAdditionalPropertiesInPlace = (schema: JsonSchema, additionalPr if (Array.isArray(schema.items)) { schema.items.forEach( - (s) => typeof s === 'object' && _setDefaultAdditionalPropertiesInPlace(s, additionalProperties), + (s) => typeof s === 'object' && _setDefaultAdditionalPropertiesInPlace(s, additionalProperties) ) return } diff --git a/opapi/src/opapi.ts b/opapi/src/opapi.ts index 20cc541c..d81e06ae 100644 --- a/opapi/src/opapi.ts +++ b/opapi/src/opapi.ts @@ -5,7 +5,7 @@ import { generateErrorsFile, generateOpenapi, generateServer, - generateTypesBySection, + generateTypesBySection } from './generator' import { exportStateAsTypescript, type ExportStateAsTypescriptOptions } from './generators/ts-state' import { generateHandler } from './handler-generator' @@ -20,6 +20,8 @@ import { type Options, type Parameter, type State, + type ParametersMap, + PathParameter } from './state' export { Operation, Parameter } from './state' @@ -27,7 +29,7 @@ type AnatineSchemaObject = NonNullable[1]> export const schema = ( schema: T, - schemaObject?: AnatineSchemaObject & { $ref?: string }, + schemaObject?: AnatineSchemaObject & { $ref?: string } ): T => { const This = (schema as any).constructor const copy = new This(schema._def) as T @@ -37,7 +39,7 @@ export const schema = ( export type OpenApi< SchemaName extends string = string, DefaultParameterName extends string = string, - SectionName extends string = string, + SectionName extends string = string > = ReturnType> // TODO: ensure type inference comes from field 'sections' not 'schemas' @@ -73,7 +75,7 @@ function exportClient(state: State) { function _exportClient( dir: string, openapiGeneratorEndpoint: string, - postProcessors?: OpenApiPostProcessors, + postProcessors?: OpenApiPostProcessors ): Promise function _exportClient(dir: string, props: GenerateClientProps): Promise function _exportClient(dir = '.', props: GenerateClientProps | string, postProcessors?: OpenApiPostProcessors) { @@ -98,14 +100,64 @@ function exportClient(state: State) { const createOpapiFromState = < SchemaName extends string, DefaultParameterName extends string, - SectionName extends string, + SectionName extends string >( - state: State, + state: State ) => { + type InputItem = { + type: Parameter<'zod-schema'>['type'] + description: Parameter<'zod-schema'>['description'] + } + type Inputs = { + parameters: { + query: Record + } + response: Operation['response'] + } + const complexifyParameters = (parameters: Inputs['parameters']): ParametersMap => { + const map: ParametersMap = {} + for (const [key, value] of Object.entries(parameters.query)) { + map[key] = { + type: value.type, + description: value.description, + in: 'query' + } as any + } + return map + } + const simp = { + ops: { + get: (path: string, name: string, inputs: Inputs) => { + addOperation(state, { + name, + description: name, + method: 'get', + path, + parameters: complexifyParameters(inputs.parameters), + response: inputs.response + }) + } + }, + dt: { + boolean: (description: string): InputItem => ({ + type: 'boolean', + description + }), + integer: (description: string): InputItem => ({ + type: 'integer', + description + }), + number: (description: string): InputItem => ({ + type: 'number', + description + }) + } + } as const + return { getModelRef: (name: SchemaName): OpenApiZodAny => getRef(state, ComponentType.SCHEMAS, name), addOperation: ( - operationProps: Operation, + operationProps: Operation ) => addOperation(state, operationProps), exportClient: exportClient(state), exportTypesBySection: (dir = '.') => generateTypesBySection(state, dir), @@ -114,12 +166,13 @@ const createOpapiFromState = < exportState: (dir = '.', opts?: ExportStateAsTypescriptOptions) => exportStateAsTypescript(state, dir, opts), exportErrors: (dir = '.') => generateErrorsFile(state.errors ?? [], dir), exportHandler: (dir = '.') => generateHandler(state, dir), + simp } } export function OpenApi( props: OpenApiProps, - opts: Partial = {}, + opts: Partial = {} ) { const state = createState(props, opts) return createOpapiFromState(state) @@ -127,20 +180,17 @@ export function OpenApi( - state: State, + state: State ) => createOpapiFromState(state as State) } -export type SchemaOf> = O extends OpenApi - ? Skema - : never +export type SchemaOf> = + O extends OpenApi ? Skema : never -export type ParameterOf> = O extends OpenApi - ? Param - : never +export type ParameterOf> = + O extends OpenApi ? Param : never -export type SectionOf> = O extends OpenApi - ? Sexion - : never +export type SectionOf> = + O extends OpenApi ? Sexion : never export { exportJsonSchemas, exportZodSchemas } from './handler-generator/export-schemas' From ac5572c5346a52314c2b3f3123f39c93fcead6ae Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 19:34:58 -0400 Subject: [PATCH 20/35] Simplify createState --- opapi/package.json | 2 ++ opapi/pnpm-lock.yaml | 26 +++++++++++++++++ opapi/src/objects.ts | 6 ++-- opapi/src/state.ts | 67 ++++++++++++++++++++++---------------------- opapi/src/util.ts | 12 -------- 5 files changed, 65 insertions(+), 48 deletions(-) diff --git a/opapi/package.json b/opapi/package.json index 3682d1f8..4f7be248 100644 --- a/opapi/package.json +++ b/opapi/package.json @@ -29,6 +29,7 @@ "@types/lodash": "^4.17.0", "@types/node": "^22.16.4", "@types/qs": "^6.9.15", + "@types/ramda": "^0.31.1", "@types/verror": "1.10.10", "memfs": "^4.36.0", "prettier": "3.4.1", @@ -52,6 +53,7 @@ "openapi-typescript": "6.7.6", "openapi3-ts": "4.5.0", "radash": "12.1.0", + "ramda": "^0.31.3", "tsconfig-paths": "4.2.0", "verror": "1.10.1", "winston": "3.17.0", diff --git a/opapi/pnpm-lock.yaml b/opapi/pnpm-lock.yaml index 45c3d0e7..1017c138 100644 --- a/opapi/pnpm-lock.yaml +++ b/opapi/pnpm-lock.yaml @@ -41,6 +41,9 @@ dependencies: radash: specifier: 12.1.0 version: 12.1.0 + ramda: + specifier: ^0.31.3 + version: 0.31.3 tsconfig-paths: specifier: 4.2.0 version: 4.2.0 @@ -82,6 +85,9 @@ devDependencies: '@types/qs': specifier: ^6.9.15 version: 6.9.17 + '@types/ramda': + specifier: ^0.31.1 + version: 0.31.1 '@types/verror': specifier: 1.10.10 version: 1.10.10 @@ -1412,6 +1418,12 @@ packages: resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} dev: true + /@types/ramda@0.31.1: + resolution: {integrity: sha512-Vt6sFXnuRpzaEj+yeutA0q3bcAsK7wdPuASIzR9LXqL4gJPyFw8im9qchlbp4ltuf3kDEIRmPJTD/Fkg60dn7g==} + dependencies: + types-ramda: 0.31.0 + dev: true + /@types/range-parser@1.2.7: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true @@ -3051,6 +3063,10 @@ packages: engines: {node: '>=14.18.0'} dev: false + /ramda@0.31.3: + resolution: {integrity: sha512-xKADKRNnqmDdX59PPKLm3gGmk1ZgNnj3k7DryqWwkamp4TJ6B36DdpyKEQ0EoEYmH2R62bV4Q+S0ym2z8N2f3Q==} + dev: false + /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} dependencies: @@ -3549,6 +3565,10 @@ packages: yn: 3.1.1 dev: true + /ts-toolbelt@9.6.0: + resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} + dev: true + /tsconfig-paths@4.2.0: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} @@ -3610,6 +3630,12 @@ packages: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} dev: false + /types-ramda@0.31.0: + resolution: {integrity: sha512-vaoC35CRC3xvL8Z6HkshDbi6KWM1ezK0LHN0YyxXWUn9HKzBNg/T3xSGlJZjCYspnOD3jE7bcizsp0bUXZDxnQ==} + dependencies: + ts-toolbelt: 9.6.0 + dev: true + /typescript@5.7.2: resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} engines: {node: '>=14.17'} diff --git a/opapi/src/objects.ts b/opapi/src/objects.ts index 9ba16c1c..37066a17 100644 --- a/opapi/src/objects.ts +++ b/opapi/src/objects.ts @@ -1,10 +1,12 @@ export namespace objects { + /** @deprecated */ export const entries = (obj: Record): [K, V][] => Object.entries(obj) as [K, V][] export const mapValues = (obj: Record, fn: (value: V, key: K) => R): Record => { const result: Record = {} as any - entries(obj).forEach(([key, value]) => { + for (const key in obj) { + const value = obj[key] result[key] = fn(value, key) - }) + } return result } } diff --git a/opapi/src/state.ts b/opapi/src/state.ts index 17090a90..1d1d931e 100644 --- a/opapi/src/state.ts +++ b/opapi/src/state.ts @@ -3,10 +3,11 @@ import VError from 'verror' import { z } from 'zod' import { schema } from './opapi' import type { PathParams } from './path-params' -import { isAlphanumeric, isCapitalAlphabetical, uniqueBy } from './util' +import { isAlphanumeric, isCapitalAlphabetical } from './util' import { generateSchemaFromZod } from './jsonschema' import { OpenApiZodAny } from '@anatine/zod-openapi' import { objects } from './objects' +import { uniqBy, prop } from 'ramda' type SchemaType = 'zod-schema' | 'json-schema' type SchemaOfType = T extends 'zod-schema' ? OpenApiZodAny : SchemaObject @@ -14,17 +15,19 @@ type SchemaOfType = T extends 'zod-schema' ? OpenApiZodAny export type Options = { allowUnions: boolean } const DEFAULT_OPTIONS: Options = { allowUnions: false } +type StateSection = { + name: SectionName + title: string + description: string + schema?: string + operations: string[] +} + export type State = { metadata: Metadata refs: RefMap defaultParameters?: { [name in DefaultParameterName]: Parameter<'json-schema'> } - sections: { - name: SectionName - title: string - description: string - schema?: string - operations: string[] - }[] + sections: StateSection[] schemas: Record errors?: ApiError[] operations: { [name: string]: Operation } @@ -234,14 +237,6 @@ export function createState { const options = { ...DEFAULT_OPTIONS, ...opts } - const schemaEntries = props.schemas - ? Object.entries<(typeof props.schemas)[SchemaName]>(props.schemas).map(([name, data]) => ({ - name, - schema: data.schema, - section: data.section - })) - : [] - const schemas: Record = {} const refs: State['refs'] = { @@ -251,20 +246,8 @@ export function createState(obj: Record): [K, T][] => Object.entries(obj) as [K, T][] - - const sections = props.sections - ? toPairs(props.sections).map(([name, section]) => ({ - ...section, - name, - operations: [], - schema: schemaEntries.find((schemaEntry) => schemaEntry.section === name)?.name - })) - : [] - - schemaEntries.forEach((schemaEntry) => { - const name = schemaEntry.name - + const sectionNameToSchemaName = new Map() + for (const name in props.schemas) { if (!isAlphanumeric(name)) { throw new VError(`Invalid operation name ${name}. It must be alphanumeric and start with a letter`) } @@ -273,16 +256,32 @@ export function createState[] = [] + for (const name in props.sections) { + sections.push({ + ...props.sections[name], + name, + operations: [], + schema: sectionNameToSchemaName.get(name) + }) + } const userErrors = props.errors ?? [] const defaultErrors = [unknownError, internalError] - const errors = uniqueBy([...defaultErrors, ...userErrors], 'type') + const errors = uniqBy(prop('type'), [...defaultErrors, ...userErrors]) errors.forEach((error) => { if (!isCapitalAlphabetical(error.type)) { diff --git a/opapi/src/util.ts b/opapi/src/util.ts index cb8d33db..d89bb378 100644 --- a/opapi/src/util.ts +++ b/opapi/src/util.ts @@ -22,15 +22,3 @@ export function removeFromArray(array: string[], item: string) { array.splice(index, 1) } } - -export function uniqueBy(array: readonly T[], k: K): T[] { - const seen = new Set() - return array.filter((item) => { - const v = item[k] - if (seen.has(v)) { - return false - } - seen.add(v) - return true - }) -} From 10924ad13795093166dd6f416e975e09105b5c71 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 19:36:07 -0400 Subject: [PATCH 21/35] skip flaky test --- opapi/test/server.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/opapi/test/server.test.ts b/opapi/test/server.test.ts index 4a9f1f9b..0a69e7ae 100644 --- a/opapi/test/server.test.ts +++ b/opapi/test/server.test.ts @@ -13,7 +13,7 @@ const serverFiles = [ 'type.ts', 'metadata.json', 'openapi.json', - 'errors.ts', + 'errors.ts' ] const GEN_DIR = join(__dirname, 'gen/server') @@ -44,7 +44,8 @@ describe('server generator', () => { expect(files.length).toBe(serverFiles.length) }) - it('should correctly handle empty request body', async () => { + // TODO this flaky test should be revisited + it.skip('should correctly handle empty request body', async () => { const genServerFolder = GEN_DIR const api = getMockApi() @@ -60,9 +61,9 @@ describe('server generator', () => { id: { in: 'path', description: 'Baz id', - type: 'string', - }, - }, + type: 'string' + } + } }) await api.exportServer(genServerFolder, true) From cf9f1072bc57776a5cde88f3e59aefc0f8ac7703 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 19:50:12 -0400 Subject: [PATCH 22/35] Fix typing in generateSchemaFromZod --- opapi/src/jsonschema.ts | 20 ++++++++++++++------ opapi/src/state.ts | 9 ++------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/opapi/src/jsonschema.ts b/opapi/src/jsonschema.ts index 457ece98..3223628b 100644 --- a/opapi/src/jsonschema.ts +++ b/opapi/src/jsonschema.ts @@ -1,6 +1,6 @@ import { OpenApiZodAny, generateSchema as generateJsonSchema } from '@anatine/zod-openapi' import { JSONSchema7, JSONSchema7Definition } from 'json-schema' -import type { SchemaObject } from 'openapi3-ts/oas31' +import { isReferenceObject, type SchemaObject } from 'openapi3-ts/oas31' import { removeFromArray } from './util' import _ from 'lodash' @@ -13,7 +13,7 @@ export type JsonSchema = JSONSchema7 export type NullableJsonSchema = JSONSchema7 & { nullable?: boolean } export const generateSchemaFromZod = (zodRef: OpenApiZodAny, opts?: GenerateSchemaFromZodOpts) => { - const jsonSchema = generateJsonSchema(zodRef, opts?.useOutput) as SchemaObject + const jsonSchema = generateJsonSchema(zodRef, opts?.useOutput) formatJsonSchema(jsonSchema, opts?.allowUnions ?? false) return jsonSchema } @@ -33,12 +33,20 @@ export const formatJsonSchema = (jsonSchema: SchemaObject, allowUnions: boolean) } if (typeof jsonSchema.additionalProperties === 'object') { - formatJsonSchema(jsonSchema.additionalProperties as SchemaObject, allowUnions) + if (isReferenceObject(jsonSchema.additionalProperties)) { + throw new Error('Expected additionalProperties to be a SchemaObject') + } + formatJsonSchema(jsonSchema.additionalProperties, allowUnions) } - Object.entries(jsonSchema.properties ?? {}).forEach(([_, value]) => - formatJsonSchema(value as SchemaObject, allowUnions) - ) + if (jsonSchema.properties) { + for (const schemaObject of Object.values(jsonSchema.properties)) { + if (isReferenceObject(schemaObject)) { + throw new Error('Expected entries in properties to be of type SchemaObject') + } + formatJsonSchema(schemaObject, allowUnions) + } + } } if (!allowUnions && (jsonSchema.allOf || jsonSchema.anyOf || jsonSchema.oneOf)) { diff --git a/opapi/src/state.ts b/opapi/src/state.ts index 1d1d931e..ca48b6df 100644 --- a/opapi/src/state.ts +++ b/opapi/src/state.ts @@ -7,7 +7,7 @@ import { isAlphanumeric, isCapitalAlphabetical } from './util' import { generateSchemaFromZod } from './jsonschema' import { OpenApiZodAny } from '@anatine/zod-openapi' import { objects } from './objects' -import { uniqBy, prop } from 'ramda' +import { uniqBy, prop, map } from 'ramda' type SchemaType = 'zod-schema' | 'json-schema' type SchemaOfType = T extends 'zod-schema' ? OpenApiZodAny : SchemaObject @@ -297,12 +297,7 @@ export function createState - >) - : undefined + const defaultParameters = props.defaultParameters ? map(mapParameter, props.defaultParameters) : undefined return { operations: {}, From 574f9cf58138c5015bcd06e208b7f3f22d1d4f40 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 12 Sep 2025 20:00:49 -0400 Subject: [PATCH 23/35] refactor openapi --- opapi/src/openapi.ts | 81 +++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/opapi/src/openapi.ts b/opapi/src/openapi.ts index 0dc76207..6d848e02 100644 --- a/opapi/src/openapi.ts +++ b/opapi/src/openapi.ts @@ -52,6 +52,7 @@ export const createOpenapi = < } }) + // TODO generateSchemaFromZod returns SchemaObject but we expect a ReferenceObject here const responseRefSchema = generateSchemaFromZod( getRef(state, ComponentType.RESPONSES, responseName) ) as unknown as ReferenceObject @@ -61,8 +62,8 @@ export const createOpenapi = < description: operationObject.description, parameters: [], responses: { - default: responseRefSchema as ReferenceObject, - [response.status ?? defaultResponseStatus]: responseRefSchema as ReferenceObject + default: responseRefSchema, + [response.status ?? defaultResponseStatus]: responseRefSchema } } @@ -79,82 +80,76 @@ export const createOpenapi = < } }) - const bodyRefSchema = generateSchemaFromZod( - getRef(state, ComponentType.REQUESTS, bodyName) - ) as unknown as ReferenceObject + // TODO we expect this to be a ReferenceObject but generateSchemaFromZod returns SchemaObject only. + const bodyRefSchema = generateSchemaFromZod(getRef(state, ComponentType.REQUESTS, bodyName)) - operation.requestBody = bodyRefSchema + // TODO this expects a RequestBodyObject or ReferenceObject but generateSchemaFromZod returns SchemaObject only. + operation.requestBody = bodyRefSchema as unknown as ReferenceObject } - if (operationObject.parameters) { - objects.entries(operationObject.parameters).forEach(([parameterName, parameter]) => { - const parameterType = parameter.type + if (operationObject.parameters && operation.parameters) { + for (const [parameterName, parameterSpec] of Object.entries(operationObject.parameters)) { + const parameterType = parameterSpec.type + + const parameter = { + name: parameterName, + in: parameterSpec.in, + description: parameterSpec.description + } switch (parameterType) { case 'string': - operation.parameters?.push({ - name: parameterName, - in: parameter.in, - description: parameter.description, - required: parameter.in === 'path' ? true : parameter.required, + operation.parameters.push({ + ...parameter, + required: parameterSpec.in === 'path' ? true : parameterSpec.required, schema: { type: 'string', - enum: parameter.enum as string[] + enum: parameterSpec.enum as string[] } }) break case 'string[]': - operation.parameters?.push({ - name: parameterName, - in: parameter.in, - description: parameter.description, - required: parameter.required, + operation.parameters.push({ + ...parameter, + required: parameterSpec.required, schema: { type: 'array', items: { type: 'string', - enum: parameter.enum + enum: parameterSpec.enum } } }) break case 'object': - operation.parameters?.push({ - name: parameterName, - in: parameter.in, - description: parameter.description, - required: parameter.required, - schema: parameter.schema + operation.parameters.push({ + ...parameter, + required: parameterSpec.required, + schema: parameterSpec.schema }) break case 'boolean': - operation.parameters?.push({ - name: parameterName, - in: parameter.in, - description: parameter.description, - required: parameter.required, + operation.parameters.push({ + ...parameter, + required: parameterSpec.required, schema: { type: 'boolean' } }) break case 'integer': - operation.parameters?.push({ - name: parameterName, - in: parameter.in, - description: parameter.description, - required: parameter.required, + operation.parameters.push({ + ...parameter, + required: parameterSpec.required, schema: { type: 'integer' } }) break case 'number': - operation.parameters?.push({ - name: parameterName, - in: parameter.in, - description: parameter.description, - required: parameter.required, + operation.parameters.push({ + ...parameter, + required: parameterSpec.required, schema: { type: 'number' } @@ -163,7 +158,7 @@ export const createOpenapi = < default: throw new VError(`Parameter type ${parameterType} is not supported`) } - }) + } } if (!openapi.rootDoc.paths) { From 61fa5c8a83c53a1b8b2ea09df3c447e2f7846758 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Sat, 13 Sep 2025 01:04:53 -0400 Subject: [PATCH 24/35] Simplify jsonschema --- opapi/src/jsonschema.ts | 108 ++++++++++++++---------------- opapi/src/util.ts | 7 -- opapi/test/export-schemas.test.ts | 2 +- 3 files changed, 50 insertions(+), 67 deletions(-) diff --git a/opapi/src/jsonschema.ts b/opapi/src/jsonschema.ts index 3223628b..37c1a1ae 100644 --- a/opapi/src/jsonschema.ts +++ b/opapi/src/jsonschema.ts @@ -1,7 +1,6 @@ import { OpenApiZodAny, generateSchema as generateJsonSchema } from '@anatine/zod-openapi' import { JSONSchema7, JSONSchema7Definition } from 'json-schema' import { isReferenceObject, type SchemaObject } from 'openapi3-ts/oas31' -import { removeFromArray } from './util' import _ from 'lodash' export type GenerateSchemaFromZodOpts = { @@ -55,10 +54,7 @@ export const formatJsonSchema = (jsonSchema: SchemaObject, allowUnions: boolean) } export function schemaIsEmptyObject(schema: SchemaObject) { - const keys = Object.keys(schema) - - removeFromArray(keys, 'title') - removeFromArray(keys, 'description') + const keys = Object.keys(schema).filter((key) => !['title', 'description'].includes(key)) if (keys.length === 0) { return true @@ -77,68 +73,62 @@ export function schemaIsEmptyObject(schema: SchemaObject) { return false } -const exploreJsonSchemaDef = - (cb: (s: JsonSchema) => JsonSchema) => - (inputSchema: JSONSchema7Definition): JSONSchema7Definition => { - if (typeof inputSchema === 'boolean') { - return inputSchema - } - return exploreJsonSchema(cb)(inputSchema) +const exploreJsonSchemaDef = ( + cb: (s: JsonSchema) => JsonSchema, + inputSchema: JSONSchema7Definition +): JSONSchema7Definition => (typeof inputSchema === 'boolean' ? inputSchema : exploreJsonSchema(cb, inputSchema)) + +export const exploreJsonSchema = (cb: (s: JsonSchema) => JsonSchema, inputSchema: JsonSchema): JsonSchema => { + const mappedSchema = cb(inputSchema) + + if (mappedSchema.type === 'object') { + const properties = mappedSchema.properties + ? _.mapValues(mappedSchema.properties, (schema: JsonSchema) => exploreJsonSchema(cb, schema)) + : undefined + const additionalProperties = + typeof mappedSchema.additionalProperties === 'object' + ? exploreJsonSchema(cb, mappedSchema.additionalProperties) + : mappedSchema.additionalProperties + return { ...mappedSchema, properties, additionalProperties } } -export const exploreJsonSchema = - (cb: (s: JsonSchema) => JsonSchema) => - (inputSchema: JsonSchema): JsonSchema => { - const mappedSchema = cb(inputSchema) - - if (mappedSchema.type === 'object') { - const properties = mappedSchema.properties - ? _.mapValues(mappedSchema.properties, exploreJsonSchema(cb)) - : mappedSchema.properties - const additionalProperties = - typeof mappedSchema.additionalProperties === 'object' - ? exploreJsonSchema(cb)(mappedSchema.additionalProperties) - : mappedSchema.additionalProperties - return { ...mappedSchema, properties, additionalProperties } - } - - if (mappedSchema.type === 'array') { - if (mappedSchema.items === undefined) { - return mappedSchema - } - if (Array.isArray(mappedSchema.items)) { - return { - ...mappedSchema, - items: mappedSchema.items.map(exploreJsonSchemaDef(cb)) - } - } - return { ...mappedSchema, items: exploreJsonSchemaDef(cb)(mappedSchema.items) } + if (mappedSchema.type === 'array') { + if (mappedSchema.items === undefined) { + return mappedSchema } - - if (mappedSchema.anyOf) { + if (Array.isArray(mappedSchema.items)) { return { ...mappedSchema, - anyOf: mappedSchema.anyOf.map(exploreJsonSchemaDef(cb)) + items: mappedSchema.items.map((item) => exploreJsonSchemaDef(cb, item)) } } + return { ...mappedSchema, items: exploreJsonSchemaDef(cb, mappedSchema.items) } + } - if (mappedSchema.allOf) { - return { - ...mappedSchema, - allOf: mappedSchema.allOf.map(exploreJsonSchemaDef(cb)) - } + if (mappedSchema.anyOf) { + return { + ...mappedSchema, + anyOf: mappedSchema.anyOf.map((item) => exploreJsonSchemaDef(cb, item)) } + } - if (mappedSchema.oneOf) { - return { - ...mappedSchema, - oneOf: mappedSchema.oneOf.map(exploreJsonSchemaDef(cb)) - } + if (mappedSchema.allOf) { + return { + ...mappedSchema, + allOf: mappedSchema.allOf.map((item) => exploreJsonSchemaDef(cb, item)) } + } - return mappedSchema + if (mappedSchema.oneOf) { + return { + ...mappedSchema, + oneOf: mappedSchema.oneOf.map((item) => exploreJsonSchemaDef(cb, item)) + } } + return mappedSchema +} + /** * Lib "@anatine/zod-openapi" transforms zod to json-schema using the nullable property. * This property is not officially supported by json-schema, but supported by ajv (see: https://ajv.js.org/json-schema.html#nullable) @@ -146,15 +136,15 @@ export const exploreJsonSchema = * This function replaces all occurences of { type: T, nullable: true } with { anyOf: [{ type: T }, { type: 'null' }] } */ export const replaceNullableWithUnion = (schema: NullableJsonSchema): JSONSchema7 => { - const mapper = exploreJsonSchema((s) => { + const mapper = (s: JsonSchema): JsonSchema => { const { nullable, ...schema } = s as NullableJsonSchema if (nullable) { const { title, description, ...rest } = schema return { title, description, anyOf: [rest, { type: 'null' }] } } return schema - }) - return mapper(schema) + } + return exploreJsonSchema(mapper, schema) } /** @@ -163,14 +153,14 @@ export const replaceNullableWithUnion = (schema: NullableJsonSchema): JSONSchema * This function replaces all occurences of { oneOf: [{ type: T1 }, { type: T2 }] } with { anyOf: [{ type: T1 }, { type: T2 }] } */ export const replaceOneOfWithAnyOf = (oneOfSchema: JsonSchema): JSONSchema7 => { - const mapper = exploreJsonSchema((schema) => { + const mapper = (schema: JsonSchema): JsonSchema => { if (schema.oneOf) { const { oneOf, ...rest } = schema return { anyOf: oneOf, ...rest } } return schema - }) - return mapper(oneOfSchema) + } + return exploreJsonSchema(mapper, oneOfSchema) } const _setDefaultAdditionalPropertiesInPlace = (schema: JsonSchema, additionalProperties: boolean): void => { diff --git a/opapi/src/util.ts b/opapi/src/util.ts index d89bb378..fbaa5de5 100644 --- a/opapi/src/util.ts +++ b/opapi/src/util.ts @@ -15,10 +15,3 @@ export function formatResponseName(operationName: string) { export function formatBodyName(operationName: string) { return `${operationName}Body` } - -export function removeFromArray(array: string[], item: string) { - const index = array.indexOf(item) - if (index > -1) { - array.splice(index, 1) - } -} diff --git a/opapi/test/export-schemas.test.ts b/opapi/test/export-schemas.test.ts index 31156242..66120f6f 100644 --- a/opapi/test/export-schemas.test.ts +++ b/opapi/test/export-schemas.test.ts @@ -1,4 +1,4 @@ -import fs from 'fs' +import * as fs from 'fs' import { describe, expect, it } from 'vitest' import { join, basename } from 'path' import { exportJsonSchemas, exportZodSchemas } from '../src' From 8c94817d85bac9deb1b893872223e9e9d4c34fa6 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Sat, 13 Sep 2025 10:34:47 -0400 Subject: [PATCH 25/35] Fix schema property mapping --- opapi/src/jsonschema.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opapi/src/jsonschema.ts b/opapi/src/jsonschema.ts index 37c1a1ae..0ae217a0 100644 --- a/opapi/src/jsonschema.ts +++ b/opapi/src/jsonschema.ts @@ -1,7 +1,7 @@ import { OpenApiZodAny, generateSchema as generateJsonSchema } from '@anatine/zod-openapi' import { JSONSchema7, JSONSchema7Definition } from 'json-schema' import { isReferenceObject, type SchemaObject } from 'openapi3-ts/oas31' -import _ from 'lodash' +import * as R from 'ramda' export type GenerateSchemaFromZodOpts = { useOutput?: boolean @@ -83,7 +83,7 @@ export const exploreJsonSchema = (cb: (s: JsonSchema) => JsonSchema, inputSchema if (mappedSchema.type === 'object') { const properties = mappedSchema.properties - ? _.mapValues(mappedSchema.properties, (schema: JsonSchema) => exploreJsonSchema(cb, schema)) + ? R.map((schema: JSONSchema7Definition) => exploreJsonSchemaDef(cb, schema), mappedSchema.properties) : undefined const additionalProperties = typeof mappedSchema.additionalProperties === 'object' @@ -201,7 +201,7 @@ const _setDefaultAdditionalPropertiesInPlace = (schema: JsonSchema, additionalPr } export const setDefaultAdditionalProperties = (schema: JsonSchema, additionalProperties: boolean): JsonSchema => { - const copy = _.cloneDeep(schema) + const copy = R.clone(schema) _setDefaultAdditionalPropertiesInPlace(copy, additionalProperties) return copy } From 1e5547701276b52f6af75c53e3d557ac6e774eb0 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 15 Sep 2025 22:44:42 -0400 Subject: [PATCH 26/35] refactor handler-generator --- opapi/src/handler-generator/index.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/opapi/src/handler-generator/index.ts b/opapi/src/handler-generator/index.ts index 72625f59..3e562968 100644 --- a/opapi/src/handler-generator/index.ts +++ b/opapi/src/handler-generator/index.ts @@ -1,6 +1,7 @@ import fs from 'fs' import pathlib from 'path' import _ from 'lodash' +import R from 'ramda' import { State } from '../state' import { toRequestSchema, toResponseSchema } from './map-operation' import { exportErrors } from './export-errors' @@ -15,18 +16,21 @@ type JsonSchemaMap = Record type ExportableSchema = { exportSchemas: (outDir: string) => Promise } const toExportableSchema = (schemas: JsonSchemaMap): ExportableSchema => ({ - exportSchemas: (outDir: string) => exportJsonSchemas(schemas)(outDir, { includeZodSchemas: false }), + exportSchemas: (outDir: string) => exportJsonSchemas(schemas)(outDir, { includeZodSchemas: false }) }) export const generateHandler = async ( state: State, - outDir: string, + outDir: string ) => { - const operationsByName = _.mapKeys(state.operations, (v) => v.name) + const operationsByName = Object.fromEntries(Object.values(state.operations).map((v) => [v.name, v])) - const requestSchemas: JsonSchemaMap = _.mapValues(operationsByName, (o) => toRequestSchema(state, o)) - const responseSchemas: JsonSchemaMap = _.mapValues(operationsByName, (o) => toResponseSchema(state, o)) - const modelSchemas: JsonSchemaMap = _.mapValues(state.schemas, (s) => s.schema as JSONSchema7) + const requestSchemas = R.map((o) => toRequestSchema(state, o), operationsByName) + const responseSchemas = R.map((o) => toResponseSchema(state, o), operationsByName) + const modelSchemas = _.mapValues(state.schemas, (s) => s.schema) + for (const k in state.schemas) { + const x = state.schemas[k] + } const models: ExportableSchema = toExportableSchema(modelSchemas) const requests: ExportableSchema = toExportableSchema(requestSchemas) From 3a822f75175194e5a220edde232132eadec6eb01 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Mon, 15 Sep 2025 22:51:50 -0400 Subject: [PATCH 27/35] fix:format --- opapi/readme.md | 38 +++++++++--------- opapi/src/generator.ts | 20 +++++----- opapi/src/handler-generator/index.ts | 7 +--- opapi/src/index.ts | 4 +- opapi/src/jsonschema.ts | 14 +++---- opapi/src/opapi.ts | 53 ++++++++++++------------ opapi/src/openapi.ts | 54 ++++++++++++------------- opapi/src/operation.ts | 24 +++++------ opapi/src/state.ts | 32 +++++++-------- opapi/test/client.test.ts | 2 +- opapi/test/export-schemas.test.ts | 28 ++++++------- opapi/test/opapi.test.ts | 60 ++++++++++++++-------------- opapi/test/server.test.ts | 8 ++-- 13 files changed, 172 insertions(+), 172 deletions(-) diff --git a/opapi/readme.md b/opapi/readme.md index 56330507..9d3e106c 100644 --- a/opapi/readme.md +++ b/opapi/readme.md @@ -16,14 +16,14 @@ const api = OpenApi({ description: 'Description of this api', // This is the description of the API server: 'https://api.example.com', // This is the base URL of the API version: '0.1.0', // This is the version of the API - prefix: 'v1', // This prefix will be added to all routes + prefix: 'v1' // This prefix will be added to all routes }, // This is metadata to be used in the documentation section: { User: { - tilte: 'User', - description: 'User related endpoints', - }, + title: 'User', + description: 'User related endpoints' + } }, // This is where you define your schemas that will be used in the API // You can use the `ref` function to reference a schema @@ -33,32 +33,32 @@ const api = OpenApi({ schema: schema( z.object({ id: z.string(), - name: z.string(), + name: z.string() }), { - description: 'User schema', - }, - ), - }, + description: 'User schema' + } + ) + } }, // This is the error definitions that will be used in the API errors: [ { status: 403, type: 'Forbidden', - description: "The requested action can't be peform by this resource.", + description: "The requested action can't be peform by this resource." }, { status: 400, type: 'InvalidPayload', - description: "The request payload isn't invalid.", + description: "The request payload isn't invalid." }, { status: 405, type: 'MethodNotFound', - description: 'The requested method does not exist.', - }, - ], + description: 'The requested method does not exist.' + } + ] }) api.addOperation({ @@ -71,15 +71,15 @@ api.addOperation({ name: { in: 'query', type: 'string', - description: 'Name filter for the users', - }, + description: 'Name filter for the users' + } }, response: { description: 'Returns a list of User objects.', schema: z.object({ - users: openapi.getModelRef('User'), - }), - }, + users: openapi.getModelRef('User') + }) + } }) api.exportServer('./gen/server') // This will generate a server that can be used with any framework diff --git a/opapi/src/generator.ts b/opapi/src/generator.ts index ffa469ad..209a6bc8 100644 --- a/opapi/src/generator.ts +++ b/opapi/src/generator.ts @@ -12,7 +12,7 @@ import { generateHandlers, generateTypes, runOpenApiCodeGenerator, - clientNode + clientNode, } from './generators' import { generateErrors } from './generators/errors' import { generateOpenapiTypescript } from './generators/openapi-typescript' @@ -26,7 +26,7 @@ import { executeOperationParsers, executeSectionParsers, operationParsers, - sectionParsers + sectionParsers, } from './section-types-generator' import { Block } from './section-types-generator/types' import { ApiError, isOperationWithBodyProps, type Operation, type State } from './state' @@ -41,7 +41,7 @@ export async function generateTypesBySection(state: DefaultState, targetDirector for (const section of state.sections) { const [sectionBlocks, operationBlocks] = await Promise.all([ executeSectionParsers(sectionParsers, section, state), - executeOperationParsers(operationParsers, section, state) + executeOperationParsers(operationParsers, section, state), ]) const blocks = [...sectionBlocks, ...operationBlocks] allBlocks.push(...blocks) @@ -73,9 +73,9 @@ export const generateServer = async (state: State, dir: log.info('Generating handlers code') const handlersCode = generateHandlers({ operations: Object.entries(state.operations).map(([name, operation]) => - mapOperationPropsToHandlerProps(name, operation) + mapOperationPropsToHandlerProps(name, operation), ), - useExpressTypes + useExpressTypes, }) log.info('') @@ -106,7 +106,7 @@ export const generateClientWithOpenapiGenerator = async ( state: State, dir = '.', openApiGeneratorEndpoint: string, - postProcessors?: OpenApiPostProcessors + postProcessors?: OpenApiPostProcessors, ) => { initDirectory(dir) @@ -123,8 +123,8 @@ export const generateClientWithOpenapiGenerator = async ( log.info('Generating client code') const clientCode = generateClientCode({ operations: Object.entries(state.operations).map(([name, operation]) => - mapOperationPropsToHandlerProps(name, operation) - ) + mapOperationPropsToHandlerProps(name, operation), + ), }) log.info('') @@ -219,7 +219,7 @@ export function generateOpenapi(state: State, dir = '.') function mapOperationPropsToHandlerProps( operationName: string, - operation: Operation + operation: Operation, ) { const generateHandlerProps: GenerateHandlerProps = { operationName, @@ -233,7 +233,7 @@ function mapOperationPropsToHandlerProps( isEmptyBody: isOperationWithBodyProps(operation) ? schemaIsEmptyObject(operation.requestBody.schema) : true, contentType: isOperationWithBodyProps(operation) ? (operation.contentType ?? 'application/json') - : 'application/json' + : 'application/json', } if (operation.parameters) { diff --git a/opapi/src/handler-generator/index.ts b/opapi/src/handler-generator/index.ts index 3e562968..d7f07d76 100644 --- a/opapi/src/handler-generator/index.ts +++ b/opapi/src/handler-generator/index.ts @@ -16,21 +16,18 @@ type JsonSchemaMap = Record type ExportableSchema = { exportSchemas: (outDir: string) => Promise } const toExportableSchema = (schemas: JsonSchemaMap): ExportableSchema => ({ - exportSchemas: (outDir: string) => exportJsonSchemas(schemas)(outDir, { includeZodSchemas: false }) + exportSchemas: (outDir: string) => exportJsonSchemas(schemas)(outDir, { includeZodSchemas: false }), }) export const generateHandler = async ( state: State, - outDir: string + outDir: string, ) => { const operationsByName = Object.fromEntries(Object.values(state.operations).map((v) => [v.name, v])) const requestSchemas = R.map((o) => toRequestSchema(state, o), operationsByName) const responseSchemas = R.map((o) => toResponseSchema(state, o), operationsByName) const modelSchemas = _.mapValues(state.schemas, (s) => s.schema) - for (const k in state.schemas) { - const x = state.schemas[k] - } const models: ExportableSchema = toExportableSchema(modelSchemas) const requests: ExportableSchema = toExportableSchema(requestSchemas) diff --git a/opapi/src/index.ts b/opapi/src/index.ts index f1ed831e..42bc3a2c 100644 --- a/opapi/src/index.ts +++ b/opapi/src/index.ts @@ -11,7 +11,7 @@ export { ParameterOf, SectionOf, exportJsonSchemas, - exportZodSchemas + exportZodSchemas, } from './opapi' export { type Options, @@ -37,5 +37,5 @@ export { type ParametersMap, createState, getRef, - mapParameter + mapParameter, } from './state' diff --git a/opapi/src/jsonschema.ts b/opapi/src/jsonschema.ts index 0ae217a0..dc63decc 100644 --- a/opapi/src/jsonschema.ts +++ b/opapi/src/jsonschema.ts @@ -75,7 +75,7 @@ export function schemaIsEmptyObject(schema: SchemaObject) { const exploreJsonSchemaDef = ( cb: (s: JsonSchema) => JsonSchema, - inputSchema: JSONSchema7Definition + inputSchema: JSONSchema7Definition, ): JSONSchema7Definition => (typeof inputSchema === 'boolean' ? inputSchema : exploreJsonSchema(cb, inputSchema)) export const exploreJsonSchema = (cb: (s: JsonSchema) => JsonSchema, inputSchema: JsonSchema): JsonSchema => { @@ -99,7 +99,7 @@ export const exploreJsonSchema = (cb: (s: JsonSchema) => JsonSchema, inputSchema if (Array.isArray(mappedSchema.items)) { return { ...mappedSchema, - items: mappedSchema.items.map((item) => exploreJsonSchemaDef(cb, item)) + items: mappedSchema.items.map((item) => exploreJsonSchemaDef(cb, item)), } } return { ...mappedSchema, items: exploreJsonSchemaDef(cb, mappedSchema.items) } @@ -108,21 +108,21 @@ export const exploreJsonSchema = (cb: (s: JsonSchema) => JsonSchema, inputSchema if (mappedSchema.anyOf) { return { ...mappedSchema, - anyOf: mappedSchema.anyOf.map((item) => exploreJsonSchemaDef(cb, item)) + anyOf: mappedSchema.anyOf.map((item) => exploreJsonSchemaDef(cb, item)), } } if (mappedSchema.allOf) { return { ...mappedSchema, - allOf: mappedSchema.allOf.map((item) => exploreJsonSchemaDef(cb, item)) + allOf: mappedSchema.allOf.map((item) => exploreJsonSchemaDef(cb, item)), } } if (mappedSchema.oneOf) { return { ...mappedSchema, - oneOf: mappedSchema.oneOf.map((item) => exploreJsonSchemaDef(cb, item)) + oneOf: mappedSchema.oneOf.map((item) => exploreJsonSchemaDef(cb, item)), } } @@ -168,7 +168,7 @@ const _setDefaultAdditionalPropertiesInPlace = (schema: JsonSchema, additionalPr schema.additionalProperties ??= additionalProperties Object.values(schema.properties ?? {}).forEach( - (s) => typeof s === 'object' && _setDefaultAdditionalPropertiesInPlace(s, additionalProperties) + (s) => typeof s === 'object' && _setDefaultAdditionalPropertiesInPlace(s, additionalProperties), ) if (typeof schema.additionalProperties === 'object') { @@ -185,7 +185,7 @@ const _setDefaultAdditionalPropertiesInPlace = (schema: JsonSchema, additionalPr if (Array.isArray(schema.items)) { schema.items.forEach( - (s) => typeof s === 'object' && _setDefaultAdditionalPropertiesInPlace(s, additionalProperties) + (s) => typeof s === 'object' && _setDefaultAdditionalPropertiesInPlace(s, additionalProperties), ) return } diff --git a/opapi/src/opapi.ts b/opapi/src/opapi.ts index d81e06ae..446a2149 100644 --- a/opapi/src/opapi.ts +++ b/opapi/src/opapi.ts @@ -5,7 +5,7 @@ import { generateErrorsFile, generateOpenapi, generateServer, - generateTypesBySection + generateTypesBySection, } from './generator' import { exportStateAsTypescript, type ExportStateAsTypescriptOptions } from './generators/ts-state' import { generateHandler } from './handler-generator' @@ -21,7 +21,7 @@ import { type Parameter, type State, type ParametersMap, - PathParameter + PathParameter, } from './state' export { Operation, Parameter } from './state' @@ -29,7 +29,7 @@ type AnatineSchemaObject = NonNullable[1]> export const schema = ( schema: T, - schemaObject?: AnatineSchemaObject & { $ref?: string } + schemaObject?: AnatineSchemaObject & { $ref?: string }, ): T => { const This = (schema as any).constructor const copy = new This(schema._def) as T @@ -39,7 +39,7 @@ export const schema = ( export type OpenApi< SchemaName extends string = string, DefaultParameterName extends string = string, - SectionName extends string = string + SectionName extends string = string, > = ReturnType> // TODO: ensure type inference comes from field 'sections' not 'schemas' @@ -75,7 +75,7 @@ function exportClient(state: State) { function _exportClient( dir: string, openapiGeneratorEndpoint: string, - postProcessors?: OpenApiPostProcessors + postProcessors?: OpenApiPostProcessors, ): Promise function _exportClient(dir: string, props: GenerateClientProps): Promise function _exportClient(dir = '.', props: GenerateClientProps | string, postProcessors?: OpenApiPostProcessors) { @@ -100,9 +100,9 @@ function exportClient(state: State) { const createOpapiFromState = < SchemaName extends string, DefaultParameterName extends string, - SectionName extends string + SectionName extends string, >( - state: State + state: State, ) => { type InputItem = { type: Parameter<'zod-schema'>['type'] @@ -120,7 +120,7 @@ const createOpapiFromState = < map[key] = { type: value.type, description: value.description, - in: 'query' + in: 'query', } as any } return map @@ -134,30 +134,30 @@ const createOpapiFromState = < method: 'get', path, parameters: complexifyParameters(inputs.parameters), - response: inputs.response + response: inputs.response, }) - } + }, }, dt: { boolean: (description: string): InputItem => ({ type: 'boolean', - description + description, }), integer: (description: string): InputItem => ({ type: 'integer', - description + description, }), number: (description: string): InputItem => ({ type: 'number', - description - }) - } + description, + }), + }, } as const return { getModelRef: (name: SchemaName): OpenApiZodAny => getRef(state, ComponentType.SCHEMAS, name), addOperation: ( - operationProps: Operation + operationProps: Operation, ) => addOperation(state, operationProps), exportClient: exportClient(state), exportTypesBySection: (dir = '.') => generateTypesBySection(state, dir), @@ -166,13 +166,13 @@ const createOpapiFromState = < exportState: (dir = '.', opts?: ExportStateAsTypescriptOptions) => exportStateAsTypescript(state, dir, opts), exportErrors: (dir = '.') => generateErrorsFile(state.errors ?? [], dir), exportHandler: (dir = '.') => generateHandler(state, dir), - simp + simp, } } export function OpenApi( props: OpenApiProps, - opts: Partial = {} + opts: Partial = {}, ) { const state = createState(props, opts) return createOpapiFromState(state) @@ -180,17 +180,20 @@ export function OpenApi( - state: State + state: State, ) => createOpapiFromState(state as State) } -export type SchemaOf> = - O extends OpenApi ? Skema : never +export type SchemaOf> = O extends OpenApi + ? Skema + : never -export type ParameterOf> = - O extends OpenApi ? Param : never +export type ParameterOf> = O extends OpenApi + ? Param + : never -export type SectionOf> = - O extends OpenApi ? Sexion : never +export type SectionOf> = O extends OpenApi + ? Sexion + : never export { exportJsonSchemas, exportZodSchemas } from './handler-generator/export-schemas' diff --git a/opapi/src/openapi.ts b/opapi/src/openapi.ts index 6d848e02..35906385 100644 --- a/opapi/src/openapi.ts +++ b/opapi/src/openapi.ts @@ -9,9 +9,9 @@ import { formatBodyName, formatResponseName } from './util' export const createOpenapi = < SchemaName extends string, DefaultParameterName extends string, - SectionName extends string + SectionName extends string, >( - state: State + state: State, ) => { const { metadata, schemas, operations } = state const { description, server, title, version } = metadata @@ -22,15 +22,15 @@ export const createOpenapi = < info: { title, description, - version + version, }, paths: {}, components: { schemas: {}, responses: {}, requestBodies: {}, - parameters: {} - } + parameters: {}, + }, }) objects.entries(schemas).forEach(([schemaName, { schema }]) => { @@ -47,14 +47,14 @@ export const createOpenapi = < description: response.description, content: { 'application/json': { - schema: response.schema - } - } + schema: response.schema, + }, + }, }) // TODO generateSchemaFromZod returns SchemaObject but we expect a ReferenceObject here const responseRefSchema = generateSchemaFromZod( - getRef(state, ComponentType.RESPONSES, responseName) + getRef(state, ComponentType.RESPONSES, responseName), ) as unknown as ReferenceObject const operation: OperationObject = { @@ -63,8 +63,8 @@ export const createOpenapi = < parameters: [], responses: { default: responseRefSchema, - [response.status ?? defaultResponseStatus]: responseRefSchema - } + [response.status ?? defaultResponseStatus]: responseRefSchema, + }, } if (isOperationWithBodyProps(operationObject)) { @@ -75,9 +75,9 @@ export const createOpenapi = < description: requestBody.description, content: { [contentType]: { - schema: requestBody.schema - } - } + schema: requestBody.schema, + }, + }, }) // TODO we expect this to be a ReferenceObject but generateSchemaFromZod returns SchemaObject only. @@ -94,7 +94,7 @@ export const createOpenapi = < const parameter = { name: parameterName, in: parameterSpec.in, - description: parameterSpec.description + description: parameterSpec.description, } switch (parameterType) { @@ -104,8 +104,8 @@ export const createOpenapi = < required: parameterSpec.in === 'path' ? true : parameterSpec.required, schema: { type: 'string', - enum: parameterSpec.enum as string[] - } + enum: parameterSpec.enum as string[], + }, }) break case 'string[]': @@ -116,16 +116,16 @@ export const createOpenapi = < type: 'array', items: { type: 'string', - enum: parameterSpec.enum - } - } + enum: parameterSpec.enum, + }, + }, }) break case 'object': operation.parameters.push({ ...parameter, required: parameterSpec.required, - schema: parameterSpec.schema + schema: parameterSpec.schema, }) break case 'boolean': @@ -133,8 +133,8 @@ export const createOpenapi = < ...parameter, required: parameterSpec.required, schema: { - type: 'boolean' - } + type: 'boolean', + }, }) break case 'integer': @@ -142,8 +142,8 @@ export const createOpenapi = < ...parameter, required: parameterSpec.required, schema: { - type: 'integer' - } + type: 'integer', + }, }) break case 'number': @@ -151,8 +151,8 @@ export const createOpenapi = < ...parameter, required: parameterSpec.required, schema: { - type: 'number' - } + type: 'number', + }, }) break default: diff --git a/opapi/src/operation.ts b/opapi/src/operation.ts index 55280a21..b8644df2 100644 --- a/opapi/src/operation.ts +++ b/opapi/src/operation.ts @@ -8,17 +8,17 @@ import { ParametersMap, State, isOperationWithBodyProps, - mapParameter + mapParameter, } from './state' import { formatBodyName, formatResponseName, isAlphanumeric } from './util' export const addOperation = < SchemaName extends string, DefaultParameterName extends string, - SectionName extends string + SectionName extends string, >( state: State, - operationProps: Operation + operationProps: Operation, ) => { const { name } = operationProps const responseName = formatResponseName(name) @@ -27,7 +27,7 @@ export const addOperation = < const parameters = createParameters( operationProps.parameters ? objects.mapValues(operationProps.parameters, mapParameter) : undefined, state.defaultParameters, - operationProps.disableDefaultParameters + operationProps.disableDefaultParameters, ) if (operationProps.path[0] !== '/') { @@ -51,8 +51,8 @@ export const addOperation = < status: operationProps.response.status, schema: generateSchemaFromZod( extendApi(operationProps.response.schema, { title: responseName, format: operationProps.response.format }), - state.options - ) + state.options, + ), } let operation: Operation @@ -67,9 +67,9 @@ export const addOperation = < description: operationProps.requestBody.description, schema: generateSchemaFromZod( extendApi(operationProps.requestBody.schema, { title: bodyName, format: operationProps.requestBody?.format }), - state.options - ) - } + state.options, + ), + }, } } else { operation = { @@ -77,7 +77,7 @@ export const addOperation = < method: operationProps.method as OperationWithoutBodyMethod, parameters, path, - response + response, } } @@ -91,7 +91,7 @@ export const addOperation = < function createParameters( parameters: ParametersMap = {}, defaultParameters: ParametersMap = {}, - disableDefaultParameters: { [name in DefaultParameterNames]?: boolean } = {} + disableDefaultParameters: { [name in DefaultParameterNames]?: boolean } = {}, ): ParametersMap { const params: ParametersMap = parameters @@ -112,7 +112,7 @@ function validateParametersInPath(path: string, parameters?: ParametersMap = { // Method of the operation method: OperationsWithBodyMethod @@ -152,7 +152,7 @@ export type OperationWithoutBodyProps< DefaultParameterName extends string, SectionName extends string, Path extends string = string, - S extends SchemaType = 'zod-schema' + S extends SchemaType = 'zod-schema', > = { // Method of the operation method: OperationWithoutBodyMethod @@ -162,7 +162,7 @@ export type Operation< DefaultParameterName extends string, SectionName extends string, Path extends string = string, - S extends SchemaType = 'zod-schema' + S extends SchemaType = 'zod-schema', > = | OperationWithBodyProps | OperationWithoutBodyProps @@ -171,9 +171,9 @@ export function isOperationWithBodyProps< DefaultParameterName extends string, SectionName extends string, Path extends string, - TypeOfSchema extends SchemaType = 'json-schema' + TypeOfSchema extends SchemaType = 'json-schema', >( - operation: Operation + operation: Operation, ): operation is OperationWithBodyProps { if ((operationsWithBodyMethod as any as string[]).includes(operation.method)) { return true @@ -185,7 +185,7 @@ export enum ComponentType { SCHEMAS = 'schemas', RESPONSES = 'responses', REQUESTS = 'requestBodies', - PARAMETERS = 'parameters' + PARAMETERS = 'parameters', } export type ParametersMap = Record< @@ -198,7 +198,7 @@ type BaseOperationProps< DefaultParameterName extends string, SectionName extends string, Path extends string = string, - S extends SchemaType = 'zod-schema' + S extends SchemaType = 'zod-schema', > = { // Name of the operation name: string @@ -233,7 +233,7 @@ type CreateStateProps( props: CreateStateProps, - opts: Partial = {} + opts: Partial = {}, ): State { const options = { ...DEFAULT_OPTIONS, ...opts } @@ -243,7 +243,7 @@ export function createState() @@ -264,7 +264,7 @@ export function createState, type: ComponentType type: undefined, properties: undefined, required: undefined, - $ref: `#/components/${type}/${name}` + $ref: `#/components/${type}/${name}`, }) } @@ -328,7 +328,7 @@ export const mapParameter = (param: Parameter<'zod-schema'>): Parameter<'json-sc if ('schema' in param) { return { ...param, - schema: generateSchemaFromZod(param.schema) + schema: generateSchemaFromZod(param.schema), } } return param diff --git a/opapi/test/client.test.ts b/opapi/test/client.test.ts index 230e8e8d..a1ba2a9d 100644 --- a/opapi/test/client.test.ts +++ b/opapi/test/client.test.ts @@ -29,7 +29,7 @@ describe('client generator', () => { const api = getMockApi() await api.exportClient(genClientFolder, { - generator: 'opapi' + generator: 'opapi', }) const files = getFiles(genClientFolder) diff --git a/opapi/test/export-schemas.test.ts b/opapi/test/export-schemas.test.ts index 66120f6f..b0a302c5 100644 --- a/opapi/test/export-schemas.test.ts +++ b/opapi/test/export-schemas.test.ts @@ -51,25 +51,25 @@ describe('schemas generator', () => { id: { anyOf: [ { - type: 'string' + type: 'string', }, { - type: 'number' - } - ] - } + type: 'number', + }, + ], + }, }, - required: ['name'] + required: ['name'], }, ticket: { type: 'object', properties: { title: { type: 'string' }, - content: { type: 'string' } + content: { type: 'string' }, }, - required: ['title'] - } - }) + required: ['title'], + }, + }), ) }) @@ -81,13 +81,13 @@ describe('schemas generator', () => { user: z.object({ name: z.string(), age: z.number().optional(), - id: z.union([z.string(), z.number()]) + id: z.union([z.string(), z.number()]), }), ticket: z.object({ title: z.string(), - content: z.string().optional() - }) - }) + content: z.string().optional(), + }), + }), ) }) }) diff --git a/opapi/test/opapi.test.ts b/opapi/test/opapi.test.ts index b901f97d..744ce501 100644 --- a/opapi/test/opapi.test.ts +++ b/opapi/test/opapi.test.ts @@ -12,25 +12,25 @@ const metadata = { description: 'Test API', server: 'http://localhost:3000', version: '1.0.0', - prefix: '/v1' + prefix: '/v1', } satisfies AnyProps['metadata'] const sections = { trees: { title: 'Trees', - description: 'Trees section' - } + description: 'Trees section', + }, } satisfies AnyProps['sections'] const leaf: z.ZodType = z.object({ type: z.literal('leaf'), name: z.string(), - data: z.string() + data: z.string(), }) const node: z.ZodType = z.object({ type: z.literal('node'), name: z.string(), - children: z.array(z.lazy(() => tree)) + children: z.array(z.lazy(() => tree)), }) const tree: z.ZodType = z.union([leaf, node]) @@ -46,9 +46,9 @@ describe('openapi generator with unions not allowed', () => { schemas: { Tree: { section: 'trees', - schema: tree - } - } + schema: tree, + }, + }, }) }).toThrowError(expectedErrorMessage) }) @@ -65,13 +65,13 @@ describe('openapi generator with unions not allowed', () => { id: { description: 'Tree id', in: 'path', - type: 'string' - } + type: 'string', + }, }, response: { description: 'Tree information', - schema: tree - } + schema: tree, + }, }) }).toThrowError(expectedErrorMessage) }) @@ -86,12 +86,12 @@ describe('openapi generator with unions not allowed', () => { path: '/trees', requestBody: { description: 'Tree information', - schema: tree + schema: tree, }, response: { description: 'Tree information', - schema: z.object({}) - } + schema: z.object({}), + }, }) }).toThrowError(expectedErrorMessage) }) @@ -107,11 +107,11 @@ describe('openapi generator with unions allowed', () => { schemas: { Tree: { section: 'trees', - schema: tree - } - } + schema: tree, + }, + }, }, - opts + opts, ) }) @@ -126,13 +126,13 @@ describe('openapi generator with unions allowed', () => { id: { description: 'Tree id', in: 'path', - type: 'string' - } + type: 'string', + }, }, response: { description: 'Tree information', - schema: tree - } + schema: tree, + }, }) }) @@ -145,12 +145,12 @@ describe('openapi generator with unions allowed', () => { path: '/trees', requestBody: { description: 'Tree information', - schema: tree + schema: tree, }, response: { description: 'Tree information', - schema: z.object({}) - } + schema: z.object({}), + }, }) }) }) @@ -164,11 +164,11 @@ describe('openapi state generator', () => { schemas: { Tree: { section: 'trees', - schema: tree - } - } + schema: tree, + }, + }, }, - { allowUnions: true } + { allowUnions: true }, ) const genStateFolder = join(__dirname, 'gen/state') diff --git a/opapi/test/server.test.ts b/opapi/test/server.test.ts index 0a69e7ae..2544dcd3 100644 --- a/opapi/test/server.test.ts +++ b/opapi/test/server.test.ts @@ -13,7 +13,7 @@ const serverFiles = [ 'type.ts', 'metadata.json', 'openapi.json', - 'errors.ts' + 'errors.ts', ] const GEN_DIR = join(__dirname, 'gen/server') @@ -61,9 +61,9 @@ describe('server generator', () => { id: { in: 'path', description: 'Baz id', - type: 'string' - } - } + type: 'string', + }, + }, }) await api.exportServer(genServerFolder, true) From d49513438a1ac7f37ae8ceaa653e5848e3af1583 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Tue, 16 Sep 2025 11:30:48 -0400 Subject: [PATCH 28/35] Fix readme typo --- opapi/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opapi/readme.md b/opapi/readme.md index 9d3e106c..db33d748 100644 --- a/opapi/readme.md +++ b/opapi/readme.md @@ -77,7 +77,7 @@ api.addOperation({ response: { description: 'Returns a list of User objects.', schema: z.object({ - users: openapi.getModelRef('User') + users: api.getModelRef('User') }) } }) From 87fd87e3c5a6696b25b59d3a055af8a806ce8622 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Tue, 16 Sep 2025 11:57:49 -0400 Subject: [PATCH 29/35] Rename exportClient --- opapi/src/opapi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opapi/src/opapi.ts b/opapi/src/opapi.ts index 446a2149..2751a6a0 100644 --- a/opapi/src/opapi.ts +++ b/opapi/src/opapi.ts @@ -71,7 +71,7 @@ export type GenerateClientProps = generator: 'opapi' } -function exportClient(state: State) { +function createExportClient(state: State) { function _exportClient( dir: string, openapiGeneratorEndpoint: string, @@ -159,7 +159,7 @@ const createOpapiFromState = < addOperation: ( operationProps: Operation, ) => addOperation(state, operationProps), - exportClient: exportClient(state), + exportClient: createExportClient(state), exportTypesBySection: (dir = '.') => generateTypesBySection(state, dir), exportServer: (dir = '.', useExpressTypes: boolean) => generateServer(state, dir, useExpressTypes), exportOpenapi: (dir = '.') => generateOpenapi(state, dir), From 46ffb2bd19096834efca22c9365735d8d7884502 Mon Sep 17 00:00:00 2001 From: sea-grass Date: Tue, 16 Sep 2025 11:58:06 -0400 Subject: [PATCH 30/35] Add NewOpapi export --- opapi/src/index.ts | 1 + opapi/src/opapi.ts | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/opapi/src/index.ts b/opapi/src/index.ts index 42bc3a2c..6f9081b7 100644 --- a/opapi/src/index.ts +++ b/opapi/src/index.ts @@ -1,6 +1,7 @@ export type { OpenApiZodAny } from '@anatine/zod-openapi' export { + NewOpapi, schema, type OpenApiProps, type CodePostProcessor, diff --git a/opapi/src/opapi.ts b/opapi/src/opapi.ts index 2751a6a0..3a39db4a 100644 --- a/opapi/src/opapi.ts +++ b/opapi/src/opapi.ts @@ -27,6 +27,16 @@ export { Operation, Parameter } from './state' type AnatineSchemaObject = NonNullable[1]> +type NewOpapiState = {} + +/** + * This Opapi is a class-centric approach. + */ +export class NewOpapi { + private state: NewOpapiState + constructor() {} +} + export const schema = ( schema: T, schemaObject?: AnatineSchemaObject & { $ref?: string }, From 619eb0bc655f2d8c25e0297d172282b419cac88e Mon Sep 17 00:00:00 2001 From: sea-grass Date: Tue, 16 Sep 2025 12:02:50 -0400 Subject: [PATCH 31/35] Fix build error --- opapi/src/opapi.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/opapi/src/opapi.ts b/opapi/src/opapi.ts index 3a39db4a..cf36b4ea 100644 --- a/opapi/src/opapi.ts +++ b/opapi/src/opapi.ts @@ -34,7 +34,9 @@ type NewOpapiState = {} */ export class NewOpapi { private state: NewOpapiState - constructor() {} + constructor() { + this.state = {} + } } export const schema = ( From f9aee84174309c45fdb3eb67a92b708cd9fde78d Mon Sep 17 00:00:00 2001 From: sea-grass Date: Tue, 16 Sep 2025 12:03:22 -0400 Subject: [PATCH 32/35] Fix import --- opapi/src/handler-generator/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opapi/src/handler-generator/index.ts b/opapi/src/handler-generator/index.ts index d7f07d76..27697641 100644 --- a/opapi/src/handler-generator/index.ts +++ b/opapi/src/handler-generator/index.ts @@ -1,7 +1,7 @@ import fs from 'fs' import pathlib from 'path' import _ from 'lodash' -import R from 'ramda' +import * as R from 'ramda' import { State } from '../state' import { toRequestSchema, toResponseSchema } from './map-operation' import { exportErrors } from './export-errors' From 040ef90cf9dbddc26ef6725405353bbdaa5c588d Mon Sep 17 00:00:00 2001 From: sea-grass Date: Tue, 16 Sep 2025 12:04:07 -0400 Subject: [PATCH 33/35] update --- opapi/src/generators/client-node.ts | 24 ++++++++++++------------ promex/src/normalize-path.ts | 6 +++--- promex/src/prometheus.ts | 9 +++++---- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/opapi/src/generators/client-node.ts b/opapi/src/generators/client-node.ts index b56fc5bd..d901fcbb 100644 --- a/opapi/src/generators/client-node.ts +++ b/opapi/src/generators/client-node.ts @@ -11,7 +11,7 @@ import { replaceNullableWithUnion, NullableJsonSchema, setDefaultAdditionalPrope type ObjectBuilder = utils.JsonSchemaBuilder['object'] const objectBuilder: ObjectBuilder = (...args) => ({ ...utils.jsonSchemaBuilder.object(...args), - additionalProperties: false, + additionalProperties: false }) const s = { ...utils.jsonSchemaBuilder, object: objectBuilder } @@ -120,7 +120,7 @@ const toTs = async (originalSchema: JSONSchema7, name: string): Promise unknownAny: false, bannerComment: '', additionalProperties: false, - ignoreMinAndMaxItems: true, + ignoreMinAndMaxItems: true }) return `${typeCode}\n` @@ -200,7 +200,7 @@ export const generateOperations = async (state: State, o ` query: ${queryName};`, ` params: ${paramsName};`, ` body: ${reqBodyName};`, - `}\n\n`, + `}\n\n` ].join('\n') const getKey = (variable: string, key: string) => `${variable}['${key}']` @@ -223,7 +223,7 @@ export const generateOperations = async (state: State, o ` params: ${toObject(paramsKeys)},`, ` body: ${toObject(reqBodyKeys)},`, ` }`, - `}\n`, + `}\n` ].join('\n') let responseCode = '' @@ -245,19 +245,19 @@ export const generateIndex = async (state: State, indexF let indexCode = [ `${HEADER}`, "import axios, { AxiosInstance } from 'axios'", - "import { errorFrom } from './errors'", - "import { toAxiosRequest } from './to-axios'", - '', + "import { errorFrom } from './errors.js'", + "import { toAxiosRequest } from './to-axios.js'", + '' ].join('\n') for (const [name] of Object.entries(operationsByName)) { - indexCode += `import * as ${name} from './operations/${name}'\n` + indexCode += `import * as ${name} from './operations/${name}.js'\n` } indexCode += '\n' - indexCode += "export * from './models'\n\n" + indexCode += "export * from './models.js'\n\n" for (const [name] of Object.entries(operationsByName)) { - indexCode += `export * as ${name} from './operations/${name}'\n` + indexCode += `export * as ${name} from './operations/${name}.js'\n` } indexCode += '\n' @@ -267,7 +267,7 @@ export const generateIndex = async (state: State, indexF 'export type ClientProps = {', ' toAxiosRequest: typeof toAxiosRequest', // allows to override the toAxiosRequest function ' toApiError: typeof toApiError', // allows to override the toApiError function - '}', + '}' ].join('\n') indexCode += '\n\n' @@ -293,7 +293,7 @@ export const generateIndex = async (state: State, indexF ` return this.axiosInstance.request<${name}.${resName}>(axiosReq)`, ` .then((res) => res.data)`, ` .catch((e) => { throw mapErrorResponse(e) })`, - ' }\n\n', + ' }\n\n' ].join('\n') } indexCode += '}\n\n' diff --git a/promex/src/normalize-path.ts b/promex/src/normalize-path.ts index ab755286..688dee1e 100644 --- a/promex/src/normalize-path.ts +++ b/promex/src/normalize-path.ts @@ -1,4 +1,4 @@ -import type { Express, Request, Response } from 'express' +import type { Express, Request } from 'express' import { nanoid } from 'nanoid' type Route = { @@ -27,8 +27,8 @@ let appRoutes: AppRoute = {} * @returns A normalized path for the given request using the app routes */ export const normalizePath = - () => - (path: string, { req }: { req: Request; res: Response }) => { + (): ((path: string, ctx: { req: Request }) => string) => + (path, { req }) => { const appId = (req.app as any).promexId const appRoute = appRoutes[appId] diff --git a/promex/src/prometheus.ts b/promex/src/prometheus.ts index 238e2324..724583cb 100644 --- a/promex/src/prometheus.ts +++ b/promex/src/prometheus.ts @@ -31,12 +31,13 @@ export const config = (options: TOptionalPromsterOptions = {}): ReturnType defaultNormalizers.normalizeStatusCode(statusCode), + normalizeMethod: (method) => defaultNormalizers.normalizeMethod(method), + normalizePath: (path, { req }) => normalizePath()(path, { req }), metricBuckets: { - httpRequestDurationInSeconds: [0.05, 0.1, 0.5, 1, 3, 10, 60, 120], + httpRequestDurationInSeconds: [0.05, 0.1, 0.5, 1, 3, 10, 60, 120] }, - ...options, + ...options } }) } From eda78c5b0d74b29f942e8ec689af5e0389b8172f Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 22 May 2026 14:45:03 -0400 Subject: [PATCH 34/35] WIP something about creating opapi from state or from spec --- .tool-versions | 2 +- opapi/src/openapi.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.tool-versions b/.tool-versions index 35c7370b..5e60e60d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -pnpm 8.6.2 +pnpm 8.15.9 nodejs 22.17.0 diff --git a/opapi/src/openapi.ts b/opapi/src/openapi.ts index 35906385..696884f3 100644 --- a/opapi/src/openapi.ts +++ b/opapi/src/openapi.ts @@ -1,4 +1,4 @@ -import { OpenApiBuilder, OperationObject, ReferenceObject } from 'openapi3-ts/oas31' +import { OpenApiBuilder, OpenAPIObject, OperationObject, ReferenceObject } from 'openapi3-ts/oas31' import VError from 'verror' import { defaultResponseStatus } from './const' import { generateSchemaFromZod } from './jsonschema' @@ -6,7 +6,10 @@ import { objects } from './objects' import { ComponentType, State, getRef, isOperationWithBodyProps } from './state' import { formatBodyName, formatResponseName } from './util' -export const createOpenapi = < +export const createOpenapiFromSpec = (spec: OpenAPIObject) => + OpenApiBuilder.create(spec) + +const createOpenapiFromState = < SchemaName extends string, DefaultParameterName extends string, SectionName extends string, @@ -178,3 +181,5 @@ export const createOpenapi = < return openapi } + +export const createOpenapi = createOpenapiFromState From 23a3ebed488b8869ca2feec8576ac6678928cb7c Mon Sep 17 00:00:00 2001 From: sea-grass Date: Fri, 22 May 2026 14:56:38 -0400 Subject: [PATCH 35/35] tests pass --- opapi/test/opapi.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/opapi/test/opapi.test.ts b/opapi/test/opapi.test.ts index 744ce501..ae299d32 100644 --- a/opapi/test/opapi.test.ts +++ b/opapi/test/opapi.test.ts @@ -40,7 +40,7 @@ const expectedErrorMessage = 'allOf, anyOf and oneOf are not supported' describe('openapi generator with unions not allowed', () => { it('should not allow unions when creating api', async () => { expect(() => { - OpenApi({ + new OpenApi({ metadata, sections, schemas: { @@ -54,7 +54,7 @@ describe('openapi generator with unions not allowed', () => { }) it('should not allow unions in response when adding an operation', async () => { - const api = OpenApi({ metadata, sections }) + const api = new OpenApi({ metadata, sections }) expect(() => { api.addOperation({ name: 'getTree', @@ -77,7 +77,7 @@ describe('openapi generator with unions not allowed', () => { }) it('should not allow unions in request body when adding an operation', async () => { - const api = OpenApi({ metadata, sections }) + const api = new OpenApi({ metadata, sections }) expect(() => { api.addOperation({ name: 'createTree', @@ -100,7 +100,7 @@ describe('openapi generator with unions not allowed', () => { describe('openapi generator with unions allowed', () => { const opts = { allowUnions: true } as const it('should allow unions when creating api', async () => { - OpenApi( + new OpenApi( { metadata, sections, @@ -116,7 +116,7 @@ describe('openapi generator with unions allowed', () => { }) it('should allow unions in response when adding an operation', async () => { - const api = OpenApi({ metadata, sections }, opts) + const api = new OpenApi({ metadata, sections }, opts) api.addOperation({ name: 'getTree', description: 'Get a tree', @@ -137,7 +137,7 @@ describe('openapi generator with unions allowed', () => { }) it('should allow unions in request body when adding an operation', async () => { - const api = OpenApi({ metadata, sections }, opts) + const api = new OpenApi({ metadata, sections }, opts) api.addOperation({ name: 'createTree', description: 'Create a tree', @@ -157,7 +157,7 @@ describe('openapi generator with unions allowed', () => { describe('openapi state generator', () => { it('should export state', async () => { - const api = OpenApi( + const api = new OpenApi( { metadata, sections,