From 09624c318b5929fd5dff76b46fc6eddb4aa34995 Mon Sep 17 00:00:00 2001 From: Martin Paucot Date: Sun, 10 Nov 2024 13:36:07 +0100 Subject: [PATCH] openapi-metadata (#1992) --- .changeset/strange-nails-jam.md | 5 + .gitignore | 1 + docs/.vitepress/en.ts | 36 +- docs/data/contributors.json | 2 +- docs/openapi-metadata/about.md | 23 + docs/openapi-metadata/decorators.md | 25 + docs/openapi-metadata/examples.md | 49 ++ docs/openapi-metadata/index.md | 213 ++++++++ docs/openapi-metadata/metadata.md | 87 +++ docs/openapi-metadata/type-loader.md | 56 ++ docs/openapi-metadata/ui.md | 31 ++ docs/scripts/update-contributors.js | 18 +- package.json | 1 + packages/openapi-metadata/.npmignore | 4 + packages/openapi-metadata/CONTRIBUTING.md | 91 ++++ packages/openapi-metadata/LICENSE | 21 + packages/openapi-metadata/README.md | 19 + packages/openapi-metadata/biome.json | 20 + packages/openapi-metadata/package.json | 81 +++ packages/openapi-metadata/src/context.ts | 13 + .../src/decorators/api-body.ts | 17 + .../src/decorators/api-cookie.ts | 9 + .../src/decorators/api-exclude.ts | 13 + .../src/decorators/api-extra-models.ts | 8 + .../src/decorators/api-header.ts | 9 + .../src/decorators/api-operation.ts | 9 + .../src/decorators/api-param.ts | 9 + .../src/decorators/api-property.ts | 49 ++ .../src/decorators/api-query.ts | 9 + .../src/decorators/api-response.ts | 21 + .../src/decorators/api-security.ts | 29 + .../src/decorators/api-tags.ts | 7 + .../openapi-metadata/src/decorators/index.ts | 12 + .../src/errors/invalid-configuration.ts | 7 + .../src/errors/no-explicit-type.ts | 15 + .../src/errors/reflect-metadata-missing.ts | 9 + .../src/errors/symbol-keys-not-supported.ts | 6 + .../src/generators/document.ts | 53 ++ .../src/generators/operation-body.ts | 19 + .../src/generators/operation-parameters.ts | 16 + .../src/generators/operation-response.ts | 21 + .../src/generators/operation.ts | 49 ++ .../openapi-metadata/src/generators/paths.ts | 43 ++ packages/openapi-metadata/src/index.ts | 5 + packages/openapi-metadata/src/loaders/type.ts | 107 ++++ .../openapi-metadata/src/metadata/exclude.ts | 5 + .../src/metadata/extra-models.ts | 9 + .../openapi-metadata/src/metadata/factory.ts | 38 ++ .../openapi-metadata/src/metadata/index.ts | 9 + .../src/metadata/operation-body.ts | 9 + .../src/metadata/operation-header.ts | 12 + .../src/metadata/operation-parameter.ts | 15 + .../src/metadata/operation-response.ts | 16 + .../src/metadata/operation-security.ts | 11 + .../src/metadata/operation.ts | 12 + .../openapi-metadata/src/metadata/property.ts | 11 + packages/openapi-metadata/src/types.ts | 26 + packages/openapi-metadata/src/ui/index.ts | 3 + packages/openapi-metadata/src/ui/rapidoc.ts | 39 ++ packages/openapi-metadata/src/ui/scalar.ts | 21 + packages/openapi-metadata/src/ui/swagger.ts | 40 ++ packages/openapi-metadata/src/utils/enum.ts | 31 ++ .../openapi-metadata/src/utils/metadata.ts | 48 ++ packages/openapi-metadata/src/utils/schema.ts | 4 + .../openapi-metadata/test/decorators.test.ts | 191 +++++++ .../test/utils/metadata.test.ts | 20 + packages/openapi-metadata/tsconfig.build.json | 4 + packages/openapi-metadata/tsconfig.json | 21 + packages/openapi-metadata/tsup.config.ts | 8 + packages/openapi-metadata/vitest.config.ts | 12 + pnpm-lock.yaml | 499 +++++++++++++++++- 71 files changed, 2447 insertions(+), 14 deletions(-) create mode 100644 .changeset/strange-nails-jam.md create mode 100644 docs/openapi-metadata/about.md create mode 100644 docs/openapi-metadata/decorators.md create mode 100644 docs/openapi-metadata/examples.md create mode 100644 docs/openapi-metadata/index.md create mode 100644 docs/openapi-metadata/metadata.md create mode 100644 docs/openapi-metadata/type-loader.md create mode 100644 docs/openapi-metadata/ui.md create mode 100644 packages/openapi-metadata/.npmignore create mode 100644 packages/openapi-metadata/CONTRIBUTING.md create mode 100644 packages/openapi-metadata/LICENSE create mode 100644 packages/openapi-metadata/README.md create mode 100644 packages/openapi-metadata/biome.json create mode 100644 packages/openapi-metadata/package.json create mode 100644 packages/openapi-metadata/src/context.ts create mode 100644 packages/openapi-metadata/src/decorators/api-body.ts create mode 100644 packages/openapi-metadata/src/decorators/api-cookie.ts create mode 100644 packages/openapi-metadata/src/decorators/api-exclude.ts create mode 100644 packages/openapi-metadata/src/decorators/api-extra-models.ts create mode 100644 packages/openapi-metadata/src/decorators/api-header.ts create mode 100644 packages/openapi-metadata/src/decorators/api-operation.ts create mode 100644 packages/openapi-metadata/src/decorators/api-param.ts create mode 100644 packages/openapi-metadata/src/decorators/api-property.ts create mode 100644 packages/openapi-metadata/src/decorators/api-query.ts create mode 100644 packages/openapi-metadata/src/decorators/api-response.ts create mode 100644 packages/openapi-metadata/src/decorators/api-security.ts create mode 100644 packages/openapi-metadata/src/decorators/api-tags.ts create mode 100644 packages/openapi-metadata/src/decorators/index.ts create mode 100644 packages/openapi-metadata/src/errors/invalid-configuration.ts create mode 100644 packages/openapi-metadata/src/errors/no-explicit-type.ts create mode 100644 packages/openapi-metadata/src/errors/reflect-metadata-missing.ts create mode 100644 packages/openapi-metadata/src/errors/symbol-keys-not-supported.ts create mode 100644 packages/openapi-metadata/src/generators/document.ts create mode 100644 packages/openapi-metadata/src/generators/operation-body.ts create mode 100644 packages/openapi-metadata/src/generators/operation-parameters.ts create mode 100644 packages/openapi-metadata/src/generators/operation-response.ts create mode 100644 packages/openapi-metadata/src/generators/operation.ts create mode 100644 packages/openapi-metadata/src/generators/paths.ts create mode 100644 packages/openapi-metadata/src/index.ts create mode 100644 packages/openapi-metadata/src/loaders/type.ts create mode 100644 packages/openapi-metadata/src/metadata/exclude.ts create mode 100644 packages/openapi-metadata/src/metadata/extra-models.ts create mode 100644 packages/openapi-metadata/src/metadata/factory.ts create mode 100644 packages/openapi-metadata/src/metadata/index.ts create mode 100644 packages/openapi-metadata/src/metadata/operation-body.ts create mode 100644 packages/openapi-metadata/src/metadata/operation-header.ts create mode 100644 packages/openapi-metadata/src/metadata/operation-parameter.ts create mode 100644 packages/openapi-metadata/src/metadata/operation-response.ts create mode 100644 packages/openapi-metadata/src/metadata/operation-security.ts create mode 100644 packages/openapi-metadata/src/metadata/operation.ts create mode 100644 packages/openapi-metadata/src/metadata/property.ts create mode 100644 packages/openapi-metadata/src/types.ts create mode 100644 packages/openapi-metadata/src/ui/index.ts create mode 100644 packages/openapi-metadata/src/ui/rapidoc.ts create mode 100644 packages/openapi-metadata/src/ui/scalar.ts create mode 100644 packages/openapi-metadata/src/ui/swagger.ts create mode 100644 packages/openapi-metadata/src/utils/enum.ts create mode 100644 packages/openapi-metadata/src/utils/metadata.ts create mode 100644 packages/openapi-metadata/src/utils/schema.ts create mode 100644 packages/openapi-metadata/test/decorators.test.ts create mode 100644 packages/openapi-metadata/test/utils/metadata.test.ts create mode 100644 packages/openapi-metadata/tsconfig.build.json create mode 100644 packages/openapi-metadata/tsconfig.json create mode 100644 packages/openapi-metadata/tsup.config.ts create mode 100644 packages/openapi-metadata/vitest.config.ts diff --git a/.changeset/strange-nails-jam.md b/.changeset/strange-nails-jam.md new file mode 100644 index 000000000..cb7fd582d --- /dev/null +++ b/.changeset/strange-nails-jam.md @@ -0,0 +1,5 @@ +--- +"openapi-metadata": patch +--- + +Inititial release diff --git a/.gitignore b/.gitignore index 07389397b..777a4ce87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store dist node_modules +coverage packages/openapi-typescript/test/fixtures/cli-outputs/out diff --git a/docs/.vitepress/en.ts b/docs/.vitepress/en.ts index 6eb9c0d71..31a6ccaec 100644 --- a/docs/.vitepress/en.ts +++ b/docs/.vitepress/en.ts @@ -71,12 +71,19 @@ export default defineConfig({ }, { text: "openapi-react-query", + base: "/openapi-react-query", items: [ - { text: "Getting Started", link: "/openapi-react-query/" }, - { text: "useQuery", link: "/openapi-react-query/use-query" }, - { text: "useMutation", link: "/openapi-react-query/use-mutation" }, - { text: "useSuspenseQuery", link: "/openapi-react-query/use-suspense-query" }, - { text: "queryOptions", link: "/openapi-react-query/query-options" }, + { text: "Getting Started", link: "/" }, + { text: "useQuery", link: "/use-query" }, + { text: "useMutation", link: "/use-mutation" }, + { + text: "useSuspenseQuery", + link: "/use-suspense-query", + }, + { + text: "queryOptions", + link: "/query-options", + }, { text: "About", link: "/openapi-react-query/about" }, ], }, @@ -93,6 +100,19 @@ export default defineConfig({ { text: "About", link: "/about" }, ], }, + { + text: "openapi-metadata", + base: "/openapi-metadata", + items: [ + { text: "Getting Started", link: "/" }, + { text: "Decorators", link: "/decorators" }, + { text: "Metadata", link: "/metadata" }, + { text: "Type loader", link: "/type-loader" }, + { text: "UI Integrations", link: "/ui" }, + { text: "Examples", link: "/examples" }, + { text: "About", link: "/about" }, + ], + }, ], }, search: { @@ -103,6 +123,12 @@ export default defineConfig({ indexName: "openapi-ts", }, }, + socialLinks: [ + { + icon: "github", + link: "https://github.com/openapi-ts/openapi-typescript", + }, + ], footer: { message: 'Released under the MIT License.', diff --git a/docs/data/contributors.json b/docs/data/contributors.json index a3f96de37..85ec43a36 100644 --- a/docs/data/contributors.json +++ b/docs/data/contributors.json @@ -1 +1 @@ -{"openapi-typescript":[{"username":"drwpow","name":"Drew Powers","avatar":"https://avatars.githubusercontent.com/u/1369770?v=4","links":[{"icon":"github","link":"https://github.com/drwpow"}],"lastFetch":1730904775879},{"username":"psmyrdek","name":"Przemek Smyrdek","avatar":"https://avatars.githubusercontent.com/u/6187417?v=4","links":[{"icon":"github","link":"https://github.com/psmyrdek"}],"lastFetch":1730904776065},{"username":"enmand","name":"Dan Enman","avatar":"https://avatars.githubusercontent.com/u/432487?v=4","links":[{"icon":"github","link":"https://github.com/enmand"}],"lastFetch":1730904776255},{"username":"atlefren","name":"Atle Frenvik Sveen","avatar":"https://avatars.githubusercontent.com/u/1829927?v=4","links":[{"icon":"github","link":"https://github.com/atlefren"}],"lastFetch":1730904776450},{"username":"tpdewolf","name":"Tim de Wolf","avatar":"https://avatars.githubusercontent.com/u/4455209?v=4","links":[{"icon":"github","link":"https://github.com/tpdewolf"}],"lastFetch":1730904776638},{"username":"tombarton","name":"Tom Barton","avatar":"https://avatars.githubusercontent.com/u/6222711?v=4","links":[{"icon":"github","link":"https://github.com/tombarton"}],"lastFetch":1730904776827},{"username":"svnv","name":"Sven Nicolai Viig","avatar":"https://avatars.githubusercontent.com/u/1080888?v=4","links":[{"icon":"github","link":"https://github.com/svnv"}],"lastFetch":1730904777020},{"username":"sorin-davidoi","name":"Sorin Davidoi","avatar":"https://avatars.githubusercontent.com/u/2109702?v=4","links":[{"icon":"github","link":"https://github.com/sorin-davidoi"}],"lastFetch":1730904777214},{"username":"scvnathan","name":"Nathan Schneirov","avatar":"https://avatars.githubusercontent.com/u/73474?v=4","links":[{"icon":"github","link":"https://github.com/scvnathan"}],"lastFetch":1730904777408},{"username":"lbenie","name":"Lucien Bénié","avatar":"https://avatars.githubusercontent.com/u/7316046?v=4","links":[{"icon":"github","link":"https://github.com/lbenie"}],"lastFetch":1730904777600},{"username":"bokub","name":"Boris K","avatar":"https://avatars.githubusercontent.com/u/17952318?v=4","links":[{"icon":"github","link":"https://github.com/bokub"}],"lastFetch":1730904777782},{"username":"antonk52","name":"Anton Kastritskii","avatar":"https://avatars.githubusercontent.com/u/5817809?v=4","links":[{"icon":"github","link":"https://github.com/antonk52"}],"lastFetch":1730904777971},{"username":"tshelburne","name":"Tim Shelburne","avatar":"https://avatars.githubusercontent.com/u/1202267?v=4","links":[{"icon":"github","link":"https://github.com/tshelburne"}],"lastFetch":1730904778150},{"username":"typeofweb","name":"Michał Miszczyszyn","avatar":"https://avatars.githubusercontent.com/u/1338731?v=4","links":[{"icon":"github","link":"https://github.com/typeofweb"}],"lastFetch":1730904778356},{"username":"skh-","name":"Sam K Hall","avatar":"https://avatars.githubusercontent.com/u/1292598?v=4","links":[{"icon":"github","link":"https://github.com/skh-"}],"lastFetch":1730904778622},{"username":"BlooJeans","name":"Matt Jeanes","avatar":"https://avatars.githubusercontent.com/u/1751182?v=4","links":[{"icon":"github","link":"https://github.com/BlooJeans"}],"lastFetch":1730904778839},{"username":"selbekk","name":"Kristofer Giltvedt Selbekk","avatar":"https://avatars.githubusercontent.com/u/1307267?v=4","links":[{"icon":"github","link":"https://github.com/selbekk"}],"lastFetch":1730904779063},{"username":"Mause","name":"Elliana May","avatar":"https://avatars.githubusercontent.com/u/1405026?v=4","links":[{"icon":"github","link":"https://github.com/Mause"}],"lastFetch":1730904779274},{"username":"henhal","name":"Henrik Hall","avatar":"https://avatars.githubusercontent.com/u/9608258?v=4","links":[{"icon":"github","link":"https://github.com/henhal"}],"lastFetch":1730904779469},{"username":"gr2m","name":"Gregor Martynus","avatar":"https://avatars.githubusercontent.com/u/39992?v=4","links":[{"icon":"github","link":"https://github.com/gr2m"}],"lastFetch":1730904779659},{"username":"samdbmg","name":"Sam Mesterton-Gibbons","avatar":"https://avatars.githubusercontent.com/u/408983?v=4","links":[{"icon":"github","link":"https://github.com/samdbmg"}],"lastFetch":1730904779851},{"username":"rendall","name":"Rendall","avatar":"https://avatars.githubusercontent.com/u/293263?v=4","links":[{"icon":"github","link":"https://github.com/rendall"}],"lastFetch":1730904780092},{"username":"robertmassaioli","name":"Robert Massaioli","avatar":"https://avatars.githubusercontent.com/u/149178?v=4","links":[{"icon":"github","link":"https://github.com/robertmassaioli"}],"lastFetch":1730904780280},{"username":"jankuca","name":"Jan Kuča","avatar":"https://avatars.githubusercontent.com/u/367262?v=4","links":[{"icon":"github","link":"https://github.com/jankuca"}],"lastFetch":1730904780459},{"username":"th-m","name":"Thomas Valadez","avatar":"https://avatars.githubusercontent.com/u/13792029?v=4","links":[{"icon":"github","link":"https://github.com/th-m"}],"lastFetch":1730904780665},{"username":"asithade","name":"Asitha de Silva","avatar":"https://avatars.githubusercontent.com/u/3814354?v=4","links":[{"icon":"github","link":"https://github.com/asithade"}],"lastFetch":1730904780855},{"username":"misha-erm","name":"Misha","avatar":"https://avatars.githubusercontent.com/u/8783498?v=4","links":[{"icon":"github","link":"https://github.com/misha-erm"}],"lastFetch":1730904781054},{"username":"radist2s","name":"Alex Batalov","avatar":"https://avatars.githubusercontent.com/u/725645?v=4","links":[{"icon":"github","link":"https://github.com/radist2s"}],"lastFetch":1730904781234},{"username":"FedeBev","name":"Federico Bevione","avatar":"https://avatars.githubusercontent.com/u/22151395?v=4","links":[{"icon":"github","link":"https://github.com/FedeBev"}],"lastFetch":1730904781434},{"username":"yamacent","name":"Daisuke Yamamoto","avatar":"https://avatars.githubusercontent.com/u/8544439?v=4","links":[{"icon":"github","link":"https://github.com/yamacent"}],"lastFetch":1730904781617},{"username":"dnalborczyk","name":null,"avatar":"https://avatars.githubusercontent.com/u/2903325?v=4","links":[{"icon":"github","link":"https://github.com/dnalborczyk"}],"lastFetch":1730904781848},{"username":"FabioWanner","name":null,"avatar":"https://avatars.githubusercontent.com/u/46821078?v=4","links":[{"icon":"github","link":"https://github.com/FabioWanner"}],"lastFetch":1730904782036},{"username":"ashsmith","name":"Ash Smith","avatar":"https://avatars.githubusercontent.com/u/1086841?v=4","links":[{"icon":"github","link":"https://github.com/ashsmith"}],"lastFetch":1730904782216},{"username":"mehalter","name":"Micah Halter","avatar":"https://avatars.githubusercontent.com/u/1591837?v=4","links":[{"icon":"github","link":"https://github.com/mehalter"}],"lastFetch":1730904782405},{"username":"Chrg1001","name":"chrg1001","avatar":"https://avatars.githubusercontent.com/u/40189653?v=4","links":[{"icon":"github","link":"https://github.com/Chrg1001"}],"lastFetch":1730904782605},{"username":"sharmarajdaksh","name":"Dakshraj Sharma","avatar":"https://avatars.githubusercontent.com/u/33689528?v=4","links":[{"icon":"github","link":"https://github.com/sharmarajdaksh"}],"lastFetch":1730904782804},{"username":"shuluster","name":"Shaosu Liu","avatar":"https://avatars.githubusercontent.com/u/1707910?v=4","links":[{"icon":"github","link":"https://github.com/shuluster"}],"lastFetch":1730904782994},{"username":"FDiskas","name":"Vytenis","avatar":"https://avatars.githubusercontent.com/u/468006?v=4","links":[{"icon":"github","link":"https://github.com/FDiskas"}],"lastFetch":1730904783188},{"username":"ericzorn93","name":"Eric Zorn","avatar":"https://avatars.githubusercontent.com/u/22532542?v=4","links":[{"icon":"github","link":"https://github.com/ericzorn93"}],"lastFetch":1730904783381},{"username":"mbelsky","name":"Max Belsky","avatar":"https://avatars.githubusercontent.com/u/3923527?v=4","links":[{"icon":"github","link":"https://github.com/mbelsky"}],"lastFetch":1730904783564},{"username":"techbech","name":"Peter Bech","avatar":"https://avatars.githubusercontent.com/u/1520592?v=4","links":[{"icon":"github","link":"https://github.com/techbech"}],"lastFetch":1730904783756},{"username":"rustyconover","name":"Rusty Conover","avatar":"https://avatars.githubusercontent.com/u/731941?v=4","links":[{"icon":"github","link":"https://github.com/rustyconover"}],"lastFetch":1730904783935},{"username":"bunkscene","name":"Dave Carlson","avatar":"https://avatars.githubusercontent.com/u/2693678?v=4","links":[{"icon":"github","link":"https://github.com/bunkscene"}],"lastFetch":1730904784123},{"username":"ottomated","name":null,"avatar":"https://avatars.githubusercontent.com/u/31470743?v=4","links":[{"icon":"github","link":"https://github.com/ottomated"}],"lastFetch":1730904784313},{"username":"sadfsdfdsa","name":"Artem Shuvaev","avatar":"https://avatars.githubusercontent.com/u/28733669?v=4","links":[{"icon":"github","link":"https://github.com/sadfsdfdsa"}],"lastFetch":1730904784500},{"username":"ajaishankar","name":null,"avatar":"https://avatars.githubusercontent.com/u/328008?v=4","links":[{"icon":"github","link":"https://github.com/ajaishankar"}],"lastFetch":1730904784702},{"username":"dominikdosoudil","name":"Dominik Dosoudil","avatar":"https://avatars.githubusercontent.com/u/15929942?v=4","links":[{"icon":"github","link":"https://github.com/dominikdosoudil"}],"lastFetch":1730904784895},{"username":"kgtkr","name":"kgtkr","avatar":"https://avatars.githubusercontent.com/u/17868838?v=4","links":[{"icon":"github","link":"https://github.com/kgtkr"}],"lastFetch":1730904785091},{"username":"berzi","name":null,"avatar":"https://avatars.githubusercontent.com/u/32619123?v=4","links":[{"icon":"github","link":"https://github.com/berzi"}],"lastFetch":1730904785279},{"username":"PhilipTrauner","name":"Philip Trauner","avatar":"https://avatars.githubusercontent.com/u/9287847?v=4","links":[{"icon":"github","link":"https://github.com/PhilipTrauner"}],"lastFetch":1730904785465},{"username":"Powell-v2","name":"Pavel Yermolin","avatar":"https://avatars.githubusercontent.com/u/25308326?v=4","links":[{"icon":"github","link":"https://github.com/Powell-v2"}],"lastFetch":1730904785651},{"username":"duncanbeevers","name":"Duncan Beevers","avatar":"https://avatars.githubusercontent.com/u/7367?v=4","links":[{"icon":"github","link":"https://github.com/duncanbeevers"}],"lastFetch":1730904785833},{"username":"tkukushkin","name":"Timofei Kukushkin","avatar":"https://avatars.githubusercontent.com/u/1482516?v=4","links":[{"icon":"github","link":"https://github.com/tkukushkin"}],"lastFetch":1730904786028},{"username":"Semigradsky","name":"Dmitry Semigradsky","avatar":"https://avatars.githubusercontent.com/u/1198848?v=4","links":[{"icon":"github","link":"https://github.com/Semigradsky"}],"lastFetch":1730904786239},{"username":"MrLeebo","name":"Jeremy Liberman","avatar":"https://avatars.githubusercontent.com/u/2754163?v=4","links":[{"icon":"github","link":"https://github.com/MrLeebo"}],"lastFetch":1730904786427},{"username":"axelhzf","name":"Axel Hernández Ferrera","avatar":"https://avatars.githubusercontent.com/u/175627?v=4","links":[{"icon":"github","link":"https://github.com/axelhzf"}],"lastFetch":1730904786621},{"username":"imagoiq","name":"Loïc Fürhoff","avatar":"https://avatars.githubusercontent.com/u/12294151?v=4","links":[{"icon":"github","link":"https://github.com/imagoiq"}],"lastFetch":1730904786815},{"username":"BTMPL","name":"Bartosz Szczeciński","avatar":"https://avatars.githubusercontent.com/u/247153?v=4","links":[{"icon":"github","link":"https://github.com/BTMPL"}],"lastFetch":1730904787028},{"username":"HiiiiD","name":"Marco Salomone","avatar":"https://avatars.githubusercontent.com/u/61231210?v=4","links":[{"icon":"github","link":"https://github.com/HiiiiD"}],"lastFetch":1730904787218},{"username":"yacinehmito","name":"Yacine Hmito","avatar":"https://avatars.githubusercontent.com/u/6893840?v=4","links":[{"icon":"github","link":"https://github.com/yacinehmito"}],"lastFetch":1730904787414},{"username":"sajadtorkamani","name":"Sajad Torkamani","avatar":"https://avatars.githubusercontent.com/u/9380313?v=4","links":[{"icon":"github","link":"https://github.com/sajadtorkamani"}],"lastFetch":1730904787613},{"username":"mvdbeek","name":"Marius van den Beek","avatar":"https://avatars.githubusercontent.com/u/6804901?v=4","links":[{"icon":"github","link":"https://github.com/mvdbeek"}],"lastFetch":1730904787822},{"username":"sgrimm","name":"Steven Grimm","avatar":"https://avatars.githubusercontent.com/u/1248649?v=4","links":[{"icon":"github","link":"https://github.com/sgrimm"}],"lastFetch":1730904788040},{"username":"Swiftwork","name":"Erik Hughes","avatar":"https://avatars.githubusercontent.com/u/455178?v=4","links":[{"icon":"github","link":"https://github.com/Swiftwork"}],"lastFetch":1730904788237},{"username":"mtth","name":"Matthieu Monsch","avatar":"https://avatars.githubusercontent.com/u/1216372?v=4","links":[{"icon":"github","link":"https://github.com/mtth"}],"lastFetch":1730904788440},{"username":"mitchell-merry","name":"Mitchell Merry","avatar":"https://avatars.githubusercontent.com/u/8567231?v=4","links":[{"icon":"github","link":"https://github.com/mitchell-merry"}],"lastFetch":1730904788658},{"username":"qnp","name":"François Risoud","avatar":"https://avatars.githubusercontent.com/u/6012554?v=4","links":[{"icon":"github","link":"https://github.com/qnp"}],"lastFetch":1730904788873},{"username":"shoffmeister","name":null,"avatar":"https://avatars.githubusercontent.com/u/3868036?v=4","links":[{"icon":"github","link":"https://github.com/shoffmeister"}],"lastFetch":1730904789090},{"username":"liangskyli","name":"liangsky","avatar":"https://avatars.githubusercontent.com/u/31531283?v=4","links":[{"icon":"github","link":"https://github.com/liangskyli"}],"lastFetch":1730904789278},{"username":"happycollision","name":"Don Denton","avatar":"https://avatars.githubusercontent.com/u/3663628?v=4","links":[{"icon":"github","link":"https://github.com/happycollision"}],"lastFetch":1730904789514},{"username":"ysmood","name":"Yad Smood","avatar":"https://avatars.githubusercontent.com/u/1415488?v=4","links":[{"icon":"github","link":"https://github.com/ysmood"}],"lastFetch":1730904789695},{"username":"barakalon","name":"barak","avatar":"https://avatars.githubusercontent.com/u/12398927?v=4","links":[{"icon":"github","link":"https://github.com/barakalon"}],"lastFetch":1730904789894},{"username":"horaklukas","name":"Lukáš Horák","avatar":"https://avatars.githubusercontent.com/u/996088?v=4","links":[{"icon":"github","link":"https://github.com/horaklukas"}],"lastFetch":1730904790097},{"username":"pvanagtmaal","name":null,"avatar":"https://avatars.githubusercontent.com/u/5946464?v=4","links":[{"icon":"github","link":"https://github.com/pvanagtmaal"}],"lastFetch":1730904790307},{"username":"toomuchdesign","name":"Andrea Carraro","avatar":"https://avatars.githubusercontent.com/u/4573549?v=4","links":[{"icon":"github","link":"https://github.com/toomuchdesign"}],"lastFetch":1730904790491},{"username":"psychedelicious","name":"psychedelicious","avatar":"https://avatars.githubusercontent.com/u/4822129?v=4","links":[{"icon":"github","link":"https://github.com/psychedelicious"}],"lastFetch":1730904790681},{"username":"tkrotoff","name":"Tanguy Krotoff","avatar":"https://avatars.githubusercontent.com/u/643434?v=4","links":[{"icon":"github","link":"https://github.com/tkrotoff"}],"lastFetch":1730904790877},{"username":"pimveldhuisen","name":"Pim Veldhuisen","avatar":"https://avatars.githubusercontent.com/u/3043834?v=4","links":[{"icon":"github","link":"https://github.com/pimveldhuisen"}],"lastFetch":1730904791082},{"username":"asvishnyakov","name":"Aleksandr Vishniakov","avatar":"https://avatars.githubusercontent.com/u/6369252?v=4","links":[{"icon":"github","link":"https://github.com/asvishnyakov"}],"lastFetch":1730904791273},{"username":"SchabaJo","name":null,"avatar":"https://avatars.githubusercontent.com/u/138689813?v=4","links":[{"icon":"github","link":"https://github.com/SchabaJo"}],"lastFetch":1730904791463},{"username":"AhsanFazal","name":"Ahsan Fazal","avatar":"https://avatars.githubusercontent.com/u/7458046?v=4","links":[{"icon":"github","link":"https://github.com/AhsanFazal"}],"lastFetch":1730904791703},{"username":"ElForastero","name":"Eugene Dzhumak","avatar":"https://avatars.githubusercontent.com/u/5102818?v=4","links":[{"icon":"github","link":"https://github.com/ElForastero"}],"lastFetch":1730904791901},{"username":"msgadi","name":"Mohammed Gadi","avatar":"https://avatars.githubusercontent.com/u/9037086?v=4","links":[{"icon":"github","link":"https://github.com/msgadi"}],"lastFetch":1730904792087},{"username":"muttonchop","name":"Adam K","avatar":"https://avatars.githubusercontent.com/u/1037657?v=4","links":[{"icon":"github","link":"https://github.com/muttonchop"}],"lastFetch":1730904792299},{"username":"christoph-fricke","name":"Christoph Fricke","avatar":"https://avatars.githubusercontent.com/u/23103835?v=4","links":[{"icon":"github","link":"https://github.com/christoph-fricke"}],"lastFetch":1730904792483},{"username":"JorrinKievit","name":"Jorrin","avatar":"https://avatars.githubusercontent.com/u/43169049?v=4","links":[{"icon":"github","link":"https://github.com/JorrinKievit"}],"lastFetch":1730904792667},{"username":"WickyNilliams","name":"Nick Williams","avatar":"https://avatars.githubusercontent.com/u/1091390?v=4","links":[{"icon":"github","link":"https://github.com/WickyNilliams"}],"lastFetch":1730904792858},{"username":"hrsh7th","name":"hrsh7th","avatar":"https://avatars.githubusercontent.com/u/629908?v=4","links":[{"icon":"github","link":"https://github.com/hrsh7th"}],"lastFetch":1730904793044},{"username":"davidleger95","name":"David Leger","avatar":"https://avatars.githubusercontent.com/u/10498708?v=4","links":[{"icon":"github","link":"https://github.com/davidleger95"}],"lastFetch":1729913934448},{"username":"phk422","name":"Hongkun","avatar":"https://avatars.githubusercontent.com/u/59734322?v=4","links":[{"icon":"github","link":"https://github.com/phk422"}],"lastFetch":1730904869285},{"username":"mzronek","name":"Matthias Zronek","avatar":"https://avatars.githubusercontent.com/u/3847700?v=4","links":[{"icon":"github","link":"https://github.com/mzronek"}],"lastFetch":1730904869474},{"username":"raurfang","name":"Łukasz Wiśniewski","avatar":"https://avatars.githubusercontent.com/u/867241?v=4","links":[{"icon":"github","link":"https://github.com/raurfang"}],"lastFetch":1730904869679},{"username":"JeanRemiDelteil","name":"Jean-Rémi Delteil","avatar":"https://avatars.githubusercontent.com/u/9743907?v=4","links":[{"icon":"github","link":"https://github.com/JeanRemiDelteil"}],"lastFetch":1730904869872},{"username":"TzviPM","name":"Tzvi Melamed","avatar":"https://avatars.githubusercontent.com/u/1950680?v=4","links":[{"icon":"github","link":"https://github.com/TzviPM"}],"lastFetch":1730904870076},{"username":"LucaSchwan","name":"ehrenschwan","avatar":"https://avatars.githubusercontent.com/u/25820532?v=4","links":[{"icon":"github","link":"https://github.com/LucaSchwan"}],"lastFetch":1730904870278},{"username":"nzapponi","name":"Niccolo Zapponi","avatar":"https://avatars.githubusercontent.com/u/20582065?v=4","links":[{"icon":"github","link":"https://github.com/nzapponi"}],"lastFetch":1730904870468},{"username":"luchsamapparat","name":"Marvin Luchs","avatar":"https://avatars.githubusercontent.com/u/875017?v=4","links":[{"icon":"github","link":"https://github.com/luchsamapparat"}],"lastFetch":1730904870652},{"username":"nmacmunn","name":"Neil MacMunn","avatar":"https://avatars.githubusercontent.com/u/849964?v=4","links":[{"icon":"github","link":"https://github.com/nmacmunn"}],"lastFetch":1730904870844}],"openapi-fetch":[{"username":"drwpow","name":"Drew Powers","avatar":"https://avatars.githubusercontent.com/u/1369770?v=4","links":[{"icon":"github","link":"https://github.com/drwpow"}],"lastFetch":1730904775872},{"username":"fergusean","name":null,"avatar":"https://avatars.githubusercontent.com/u/1029297?v=4","links":[{"icon":"github","link":"https://github.com/fergusean"}],"lastFetch":1730904776068},{"username":"shinzui","name":"Nadeem Bitar","avatar":"https://avatars.githubusercontent.com/u/519?v=4","links":[{"icon":"github","link":"https://github.com/shinzui"}],"lastFetch":1730904776251},{"username":"ezpuzz","name":"Emory Petermann","avatar":"https://avatars.githubusercontent.com/u/672182?v=4","links":[{"icon":"github","link":"https://github.com/ezpuzz"}],"lastFetch":1730904776445},{"username":"KotoriK","name":null,"avatar":"https://avatars.githubusercontent.com/u/52659125?v=4","links":[{"icon":"github","link":"https://github.com/KotoriK"}],"lastFetch":1730904776662},{"username":"fletchertyler914","name":"Tyler Fletcher","avatar":"https://avatars.githubusercontent.com/u/3344498?v=4","links":[{"icon":"github","link":"https://github.com/fletchertyler914"}],"lastFetch":1730904776885},{"username":"nholik","name":"Nicklos Holik","avatar":"https://avatars.githubusercontent.com/u/2022214?v=4","links":[{"icon":"github","link":"https://github.com/nholik"}],"lastFetch":1730904777069},{"username":"roj1512","name":null,"avatar":"https://avatars.githubusercontent.com/u/175297870?v=4","links":[{"icon":"github","link":"https://github.com/roj1512"}],"lastFetch":1730904777261},{"username":"nickcaballero","name":"Nick Caballero","avatar":"https://avatars.githubusercontent.com/u/355976?v=4","links":[{"icon":"github","link":"https://github.com/nickcaballero"}],"lastFetch":1730904777452},{"username":"hd-o","name":"Hadrian de Oliveira","avatar":"https://avatars.githubusercontent.com/u/58871222?v=4","links":[{"icon":"github","link":"https://github.com/hd-o"}],"lastFetch":1730904777662},{"username":"kecrily","name":"Percy Ma","avatar":"https://avatars.githubusercontent.com/u/45708948?v=4","links":[{"icon":"github","link":"https://github.com/kecrily"}],"lastFetch":1730904777856},{"username":"psychedelicious","name":"psychedelicious","avatar":"https://avatars.githubusercontent.com/u/4822129?v=4","links":[{"icon":"github","link":"https://github.com/psychedelicious"}],"lastFetch":1730904778045},{"username":"muttonchop","name":"Adam K","avatar":"https://avatars.githubusercontent.com/u/1037657?v=4","links":[{"icon":"github","link":"https://github.com/muttonchop"}],"lastFetch":1730904778226},{"username":"marcomuser","name":"Marco Muser","avatar":"https://avatars.githubusercontent.com/u/64737396?v=4","links":[{"icon":"github","link":"https://github.com/marcomuser"}],"lastFetch":1730904778456},{"username":"HugeLetters","name":"Evgenii Perminov","avatar":"https://avatars.githubusercontent.com/u/119697239?v=4","links":[{"icon":"github","link":"https://github.com/HugeLetters"}],"lastFetch":1730904778645},{"username":"Fumaz","name":"alex","avatar":"https://avatars.githubusercontent.com/u/45318608?v=4","links":[{"icon":"github","link":"https://github.com/Fumaz"}],"lastFetch":1730904778836},{"username":"darwish","name":"Mike Darwish","avatar":"https://avatars.githubusercontent.com/u/292570?v=4","links":[{"icon":"github","link":"https://github.com/darwish"}],"lastFetch":1730904779040},{"username":"kaechele","name":"Felix Kaechele","avatar":"https://avatars.githubusercontent.com/u/454490?v=4","links":[{"icon":"github","link":"https://github.com/kaechele"}],"lastFetch":1730904779235},{"username":"phk422","name":"Hongkun","avatar":"https://avatars.githubusercontent.com/u/59734322?v=4","links":[{"icon":"github","link":"https://github.com/phk422"}],"lastFetch":1730904779414},{"username":"mikestopcontinues","name":"Mike Stop Continues","avatar":"https://avatars.githubusercontent.com/u/150434?v=4","links":[{"icon":"github","link":"https://github.com/mikestopcontinues"}],"lastFetch":1730904779611},{"username":"JE-Lee","name":"maurice","avatar":"https://avatars.githubusercontent.com/u/19794813?v=4","links":[{"icon":"github","link":"https://github.com/JE-Lee"}],"lastFetch":1730904779792},{"username":"vipentti","name":"Ville Penttinen","avatar":"https://avatars.githubusercontent.com/u/4726680?v=4","links":[{"icon":"github","link":"https://github.com/vipentti"}],"lastFetch":1730904780019},{"username":"armandabric","name":"Armand Abric","avatar":"https://avatars.githubusercontent.com/u/95120?v=4","links":[{"icon":"github","link":"https://github.com/armandabric"}],"lastFetch":1730904780220},{"username":"illright","name":"Lev Chelyadinov","avatar":"https://avatars.githubusercontent.com/u/15035286?v=4","links":[{"icon":"github","link":"https://github.com/illright"}],"lastFetch":1730904780416}],"openapi-react-query":[{"username":"drwpow","name":"Drew Powers","avatar":"https://avatars.githubusercontent.com/u/1369770?v=4","links":[{"icon":"github","link":"https://github.com/drwpow"}],"lastFetch":1730904775876},{"username":"kerwanp","name":"Martin Paucot","avatar":"https://avatars.githubusercontent.com/u/36955373?v=4","links":[{"icon":"github","link":"https://github.com/kerwanp"}],"lastFetch":1730904776056},{"username":"yoshi2no","name":"yoshi2no","avatar":"https://avatars.githubusercontent.com/u/57059705?v=4","links":[{"icon":"github","link":"https://github.com/yoshi2no"}],"lastFetch":1730904776248},{"username":"elaygelbart","name":"Elay Gelbart","avatar":"https://avatars.githubusercontent.com/u/88675154?v=4","links":[{"icon":"github","link":"https://github.com/elaygelbart"}],"lastFetch":1730904973287}],"swr-openapi":[{"username":"htunnicliff","name":"Hunter Tunnicliff","avatar":"https://avatars.githubusercontent.com/u/7614039?v=4","links":[{"icon":"github","link":"https://github.com/htunnicliff"}],"lastFetch":1730904775878}]} \ No newline at end of file +{"openapi-typescript":[{"username":"drwpow","name":"Drew Powers","avatar":"https://avatars.githubusercontent.com/u/1369770?v=4","links":[{"icon":"github","link":"https://github.com/drwpow"}],"lastFetch":1731241407703},{"username":"psmyrdek","name":"Przemek Smyrdek","avatar":"https://avatars.githubusercontent.com/u/6187417?v=4","links":[{"icon":"github","link":"https://github.com/psmyrdek"}],"lastFetch":1731241407860},{"username":"enmand","name":"Dan Enman","avatar":"https://avatars.githubusercontent.com/u/432487?v=4","links":[{"icon":"github","link":"https://github.com/enmand"}],"lastFetch":1731241408023},{"username":"atlefren","name":"Atle Frenvik Sveen","avatar":"https://avatars.githubusercontent.com/u/1829927?v=4","links":[{"icon":"github","link":"https://github.com/atlefren"}],"lastFetch":1731241408165},{"username":"tpdewolf","name":"Tim de Wolf","avatar":"https://avatars.githubusercontent.com/u/4455209?v=4","links":[{"icon":"github","link":"https://github.com/tpdewolf"}],"lastFetch":1731241408320},{"username":"tombarton","name":"Tom Barton","avatar":"https://avatars.githubusercontent.com/u/6222711?v=4","links":[{"icon":"github","link":"https://github.com/tombarton"}],"lastFetch":1731241408470},{"username":"svnv","name":"Sven Nicolai Viig","avatar":"https://avatars.githubusercontent.com/u/1080888?v=4","links":[{"icon":"github","link":"https://github.com/svnv"}],"lastFetch":1731241408622},{"username":"sorin-davidoi","name":"Sorin Davidoi","avatar":"https://avatars.githubusercontent.com/u/2109702?v=4","links":[{"icon":"github","link":"https://github.com/sorin-davidoi"}],"lastFetch":1731241408783},{"username":"scvnathan","name":"Nathan Schneirov","avatar":"https://avatars.githubusercontent.com/u/73474?v=4","links":[{"icon":"github","link":"https://github.com/scvnathan"}],"lastFetch":1731241408941},{"username":"lbenie","name":"Lucien Bénié","avatar":"https://avatars.githubusercontent.com/u/7316046?v=4","links":[{"icon":"github","link":"https://github.com/lbenie"}],"lastFetch":1731241409091},{"username":"bokub","name":"Boris K","avatar":"https://avatars.githubusercontent.com/u/17952318?v=4","links":[{"icon":"github","link":"https://github.com/bokub"}],"lastFetch":1731241409245},{"username":"antonk52","name":"Anton Kastritskii","avatar":"https://avatars.githubusercontent.com/u/5817809?v=4","links":[{"icon":"github","link":"https://github.com/antonk52"}],"lastFetch":1731241409395},{"username":"tshelburne","name":"Tim Shelburne","avatar":"https://avatars.githubusercontent.com/u/1202267?v=4","links":[{"icon":"github","link":"https://github.com/tshelburne"}],"lastFetch":1731241409552},{"username":"typeofweb","name":"Michał Miszczyszyn","avatar":"https://avatars.githubusercontent.com/u/1338731?v=4","links":[{"icon":"github","link":"https://github.com/typeofweb"}],"lastFetch":1731241409690},{"username":"skh-","name":"Sam K Hall","avatar":"https://avatars.githubusercontent.com/u/1292598?v=4","links":[{"icon":"github","link":"https://github.com/skh-"}],"lastFetch":1731241409830},{"username":"BlooJeans","name":"Matt Jeanes","avatar":"https://avatars.githubusercontent.com/u/1751182?v=4","links":[{"icon":"github","link":"https://github.com/BlooJeans"}],"lastFetch":1731241409987},{"username":"selbekk","name":"Kristofer Giltvedt Selbekk","avatar":"https://avatars.githubusercontent.com/u/1307267?v=4","links":[{"icon":"github","link":"https://github.com/selbekk"}],"lastFetch":1731241410149},{"username":"Mause","name":"Elliana May","avatar":"https://avatars.githubusercontent.com/u/1405026?v=4","links":[{"icon":"github","link":"https://github.com/Mause"}],"lastFetch":1731241410307},{"username":"henhal","name":"Henrik Hall","avatar":"https://avatars.githubusercontent.com/u/9608258?v=4","links":[{"icon":"github","link":"https://github.com/henhal"}],"lastFetch":1731241410448},{"username":"gr2m","name":"Gregor Martynus","avatar":"https://avatars.githubusercontent.com/u/39992?v=4","links":[{"icon":"github","link":"https://github.com/gr2m"}],"lastFetch":1731241410626},{"username":"samdbmg","name":"Sam Mesterton-Gibbons","avatar":"https://avatars.githubusercontent.com/u/408983?v=4","links":[{"icon":"github","link":"https://github.com/samdbmg"}],"lastFetch":1731241410770},{"username":"rendall","name":"Rendall","avatar":"https://avatars.githubusercontent.com/u/293263?v=4","links":[{"icon":"github","link":"https://github.com/rendall"}],"lastFetch":1731241410928},{"username":"robertmassaioli","name":"Robert Massaioli","avatar":"https://avatars.githubusercontent.com/u/149178?v=4","links":[{"icon":"github","link":"https://github.com/robertmassaioli"}],"lastFetch":1731241411079},{"username":"jankuca","name":"Jan Kuča","avatar":"https://avatars.githubusercontent.com/u/367262?v=4","links":[{"icon":"github","link":"https://github.com/jankuca"}],"lastFetch":1731241411236},{"username":"th-m","name":"Thomas Valadez","avatar":"https://avatars.githubusercontent.com/u/13792029?v=4","links":[{"icon":"github","link":"https://github.com/th-m"}],"lastFetch":1731241411392},{"username":"asithade","name":"Asitha de Silva","avatar":"https://avatars.githubusercontent.com/u/3814354?v=4","links":[{"icon":"github","link":"https://github.com/asithade"}],"lastFetch":1731241411536},{"username":"misha-erm","name":"Misha","avatar":"https://avatars.githubusercontent.com/u/8783498?v=4","links":[{"icon":"github","link":"https://github.com/misha-erm"}],"lastFetch":1731241411683},{"username":"radist2s","name":"Alex Batalov","avatar":"https://avatars.githubusercontent.com/u/725645?v=4","links":[{"icon":"github","link":"https://github.com/radist2s"}],"lastFetch":1731241411839},{"username":"FedeBev","name":"Federico Bevione","avatar":"https://avatars.githubusercontent.com/u/22151395?v=4","links":[{"icon":"github","link":"https://github.com/FedeBev"}],"lastFetch":1731241411995},{"username":"yamacent","name":"Daisuke Yamamoto","avatar":"https://avatars.githubusercontent.com/u/8544439?v=4","links":[{"icon":"github","link":"https://github.com/yamacent"}],"lastFetch":1731241412150},{"username":"dnalborczyk","name":null,"avatar":"https://avatars.githubusercontent.com/u/2903325?v=4","links":[{"icon":"github","link":"https://github.com/dnalborczyk"}],"lastFetch":1731241412294},{"username":"FabioWanner","name":null,"avatar":"https://avatars.githubusercontent.com/u/46821078?v=4","links":[{"icon":"github","link":"https://github.com/FabioWanner"}],"lastFetch":1731241412446},{"username":"ashsmith","name":"Ash Smith","avatar":"https://avatars.githubusercontent.com/u/1086841?v=4","links":[{"icon":"github","link":"https://github.com/ashsmith"}],"lastFetch":1731241412603},{"username":"mehalter","name":"Micah Halter","avatar":"https://avatars.githubusercontent.com/u/1591837?v=4","links":[{"icon":"github","link":"https://github.com/mehalter"}],"lastFetch":1731241412791},{"username":"Chrg1001","name":"chrg1001","avatar":"https://avatars.githubusercontent.com/u/40189653?v=4","links":[{"icon":"github","link":"https://github.com/Chrg1001"}],"lastFetch":1731241412971},{"username":"sharmarajdaksh","name":"Dakshraj Sharma","avatar":"https://avatars.githubusercontent.com/u/33689528?v=4","links":[{"icon":"github","link":"https://github.com/sharmarajdaksh"}],"lastFetch":1731241413109},{"username":"shuluster","name":"Shaosu Liu","avatar":"https://avatars.githubusercontent.com/u/1707910?v=4","links":[{"icon":"github","link":"https://github.com/shuluster"}],"lastFetch":1731241413312},{"username":"FDiskas","name":"Vytenis","avatar":"https://avatars.githubusercontent.com/u/468006?v=4","links":[{"icon":"github","link":"https://github.com/FDiskas"}],"lastFetch":1731241413466},{"username":"ericzorn93","name":"Eric Zorn","avatar":"https://avatars.githubusercontent.com/u/22532542?v=4","links":[{"icon":"github","link":"https://github.com/ericzorn93"}],"lastFetch":1731241413646},{"username":"mbelsky","name":"Max Belsky","avatar":"https://avatars.githubusercontent.com/u/3923527?v=4","links":[{"icon":"github","link":"https://github.com/mbelsky"}],"lastFetch":1731241413798},{"username":"techbech","name":"Peter Bech","avatar":"https://avatars.githubusercontent.com/u/1520592?v=4","links":[{"icon":"github","link":"https://github.com/techbech"}],"lastFetch":1731241413948},{"username":"rustyconover","name":"Rusty Conover","avatar":"https://avatars.githubusercontent.com/u/731941?v=4","links":[{"icon":"github","link":"https://github.com/rustyconover"}],"lastFetch":1731241414084},{"username":"bunkscene","name":"Dave Carlson","avatar":"https://avatars.githubusercontent.com/u/2693678?v=4","links":[{"icon":"github","link":"https://github.com/bunkscene"}],"lastFetch":1731241414238},{"username":"ottomated","name":null,"avatar":"https://avatars.githubusercontent.com/u/31470743?v=4","links":[{"icon":"github","link":"https://github.com/ottomated"}],"lastFetch":1731241414402},{"username":"sadfsdfdsa","name":"Artem Shuvaev","avatar":"https://avatars.githubusercontent.com/u/28733669?v=4","links":[{"icon":"github","link":"https://github.com/sadfsdfdsa"}],"lastFetch":1731241414560},{"username":"ajaishankar","name":null,"avatar":"https://avatars.githubusercontent.com/u/328008?v=4","links":[{"icon":"github","link":"https://github.com/ajaishankar"}],"lastFetch":1731241414728},{"username":"dominikdosoudil","name":"Dominik Dosoudil","avatar":"https://avatars.githubusercontent.com/u/15929942?v=4","links":[{"icon":"github","link":"https://github.com/dominikdosoudil"}],"lastFetch":1731241414885},{"username":"kgtkr","name":"kgtkr","avatar":"https://avatars.githubusercontent.com/u/17868838?v=4","links":[{"icon":"github","link":"https://github.com/kgtkr"}],"lastFetch":1731241415047},{"username":"berzi","name":null,"avatar":"https://avatars.githubusercontent.com/u/32619123?v=4","links":[{"icon":"github","link":"https://github.com/berzi"}],"lastFetch":1731241415206},{"username":"PhilipTrauner","name":"Philip Trauner","avatar":"https://avatars.githubusercontent.com/u/9287847?v=4","links":[{"icon":"github","link":"https://github.com/PhilipTrauner"}],"lastFetch":1731241415359},{"username":"Powell-v2","name":"Pavel Yermolin","avatar":"https://avatars.githubusercontent.com/u/25308326?v=4","links":[{"icon":"github","link":"https://github.com/Powell-v2"}],"lastFetch":1731241415514},{"username":"duncanbeevers","name":"Duncan Beevers","avatar":"https://avatars.githubusercontent.com/u/7367?v=4","links":[{"icon":"github","link":"https://github.com/duncanbeevers"}],"lastFetch":1731241415670},{"username":"tkukushkin","name":"Timofei Kukushkin","avatar":"https://avatars.githubusercontent.com/u/1482516?v=4","links":[{"icon":"github","link":"https://github.com/tkukushkin"}],"lastFetch":1731241415820},{"username":"Semigradsky","name":"Dmitry Semigradsky","avatar":"https://avatars.githubusercontent.com/u/1198848?v=4","links":[{"icon":"github","link":"https://github.com/Semigradsky"}],"lastFetch":1731241415970},{"username":"MrLeebo","name":"Jeremy Liberman","avatar":"https://avatars.githubusercontent.com/u/2754163?v=4","links":[{"icon":"github","link":"https://github.com/MrLeebo"}],"lastFetch":1731241416125},{"username":"axelhzf","name":"Axel Hernández Ferrera","avatar":"https://avatars.githubusercontent.com/u/175627?v=4","links":[{"icon":"github","link":"https://github.com/axelhzf"}],"lastFetch":1731241416271},{"username":"imagoiq","name":"Loïc Fürhoff","avatar":"https://avatars.githubusercontent.com/u/12294151?v=4","links":[{"icon":"github","link":"https://github.com/imagoiq"}],"lastFetch":1731241416420},{"username":"BTMPL","name":"Bartosz Szczeciński","avatar":"https://avatars.githubusercontent.com/u/247153?v=4","links":[{"icon":"github","link":"https://github.com/BTMPL"}],"lastFetch":1731241416558},{"username":"HiiiiD","name":"Marco Salomone","avatar":"https://avatars.githubusercontent.com/u/61231210?v=4","links":[{"icon":"github","link":"https://github.com/HiiiiD"}],"lastFetch":1731241416698},{"username":"yacinehmito","name":"Yacine Hmito","avatar":"https://avatars.githubusercontent.com/u/6893840?v=4","links":[{"icon":"github","link":"https://github.com/yacinehmito"}],"lastFetch":1731241416857},{"username":"sajadtorkamani","name":"Sajad Torkamani","avatar":"https://avatars.githubusercontent.com/u/9380313?v=4","links":[{"icon":"github","link":"https://github.com/sajadtorkamani"}],"lastFetch":1731241417002},{"username":"mvdbeek","name":"Marius van den Beek","avatar":"https://avatars.githubusercontent.com/u/6804901?v=4","links":[{"icon":"github","link":"https://github.com/mvdbeek"}],"lastFetch":1731241417165},{"username":"sgrimm","name":"Steven Grimm","avatar":"https://avatars.githubusercontent.com/u/1248649?v=4","links":[{"icon":"github","link":"https://github.com/sgrimm"}],"lastFetch":1731241417302},{"username":"Swiftwork","name":"Erik Hughes","avatar":"https://avatars.githubusercontent.com/u/455178?v=4","links":[{"icon":"github","link":"https://github.com/Swiftwork"}],"lastFetch":1731241417454},{"username":"mtth","name":"Matthieu Monsch","avatar":"https://avatars.githubusercontent.com/u/1216372?v=4","links":[{"icon":"github","link":"https://github.com/mtth"}],"lastFetch":1731241417603},{"username":"mitchell-merry","name":"Mitchell Merry","avatar":"https://avatars.githubusercontent.com/u/8567231?v=4","links":[{"icon":"github","link":"https://github.com/mitchell-merry"}],"lastFetch":1731241417764},{"username":"qnp","name":"François Risoud","avatar":"https://avatars.githubusercontent.com/u/6012554?v=4","links":[{"icon":"github","link":"https://github.com/qnp"}],"lastFetch":1731241417916},{"username":"shoffmeister","name":null,"avatar":"https://avatars.githubusercontent.com/u/3868036?v=4","links":[{"icon":"github","link":"https://github.com/shoffmeister"}],"lastFetch":1731241418088},{"username":"liangskyli","name":"liangsky","avatar":"https://avatars.githubusercontent.com/u/31531283?v=4","links":[{"icon":"github","link":"https://github.com/liangskyli"}],"lastFetch":1731241418251},{"username":"happycollision","name":"Don Denton","avatar":"https://avatars.githubusercontent.com/u/3663628?v=4","links":[{"icon":"github","link":"https://github.com/happycollision"}],"lastFetch":1731241418399},{"username":"ysmood","name":"Yad Smood","avatar":"https://avatars.githubusercontent.com/u/1415488?v=4","links":[{"icon":"github","link":"https://github.com/ysmood"}],"lastFetch":1731241418574},{"username":"barakalon","name":"barak","avatar":"https://avatars.githubusercontent.com/u/12398927?v=4","links":[{"icon":"github","link":"https://github.com/barakalon"}],"lastFetch":1731241418716},{"username":"horaklukas","name":"Lukáš Horák","avatar":"https://avatars.githubusercontent.com/u/996088?v=4","links":[{"icon":"github","link":"https://github.com/horaklukas"}],"lastFetch":1731241418866},{"username":"pvanagtmaal","name":null,"avatar":"https://avatars.githubusercontent.com/u/5946464?v=4","links":[{"icon":"github","link":"https://github.com/pvanagtmaal"}],"lastFetch":1731241419011},{"username":"toomuchdesign","name":"Andrea Carraro","avatar":"https://avatars.githubusercontent.com/u/4573549?v=4","links":[{"icon":"github","link":"https://github.com/toomuchdesign"}],"lastFetch":1731241419169},{"username":"psychedelicious","name":"psychedelicious","avatar":"https://avatars.githubusercontent.com/u/4822129?v=4","links":[{"icon":"github","link":"https://github.com/psychedelicious"}],"lastFetch":1731241419314},{"username":"tkrotoff","name":"Tanguy Krotoff","avatar":"https://avatars.githubusercontent.com/u/643434?v=4","links":[{"icon":"github","link":"https://github.com/tkrotoff"}],"lastFetch":1731241419476},{"username":"pimveldhuisen","name":"Pim Veldhuisen","avatar":"https://avatars.githubusercontent.com/u/3043834?v=4","links":[{"icon":"github","link":"https://github.com/pimveldhuisen"}],"lastFetch":1731241419630},{"username":"asvishnyakov","name":"Aleksandr Vishniakov","avatar":"https://avatars.githubusercontent.com/u/6369252?v=4","links":[{"icon":"github","link":"https://github.com/asvishnyakov"}],"lastFetch":1731241419785},{"username":"SchabaJo","name":null,"avatar":"https://avatars.githubusercontent.com/u/138689813?v=4","links":[{"icon":"github","link":"https://github.com/SchabaJo"}],"lastFetch":1731241419928},{"username":"AhsanFazal","name":"Ahsan Fazal","avatar":"https://avatars.githubusercontent.com/u/7458046?v=4","links":[{"icon":"github","link":"https://github.com/AhsanFazal"}],"lastFetch":1731241420090},{"username":"ElForastero","name":"Eugene Dzhumak","avatar":"https://avatars.githubusercontent.com/u/5102818?v=4","links":[{"icon":"github","link":"https://github.com/ElForastero"}],"lastFetch":1731241420243},{"username":"msgadi","name":"Mohammed Gadi","avatar":"https://avatars.githubusercontent.com/u/9037086?v=4","links":[{"icon":"github","link":"https://github.com/msgadi"}],"lastFetch":1731241420398},{"username":"muttonchop","name":"Adam K","avatar":"https://avatars.githubusercontent.com/u/1037657?v=4","links":[{"icon":"github","link":"https://github.com/muttonchop"}],"lastFetch":1731241420548},{"username":"christoph-fricke","name":"Christoph Fricke","avatar":"https://avatars.githubusercontent.com/u/23103835?v=4","links":[{"icon":"github","link":"https://github.com/christoph-fricke"}],"lastFetch":1731241420689},{"username":"JorrinKievit","name":"Jorrin","avatar":"https://avatars.githubusercontent.com/u/43169049?v=4","links":[{"icon":"github","link":"https://github.com/JorrinKievit"}],"lastFetch":1731241420844},{"username":"WickyNilliams","name":"Nick Williams","avatar":"https://avatars.githubusercontent.com/u/1091390?v=4","links":[{"icon":"github","link":"https://github.com/WickyNilliams"}],"lastFetch":1731241420998},{"username":"hrsh7th","name":"hrsh7th","avatar":"https://avatars.githubusercontent.com/u/629908?v=4","links":[{"icon":"github","link":"https://github.com/hrsh7th"}],"lastFetch":1731241421193},{"username":"davidleger95","name":"David Leger","avatar":"https://avatars.githubusercontent.com/u/10498708?v=4","links":[{"icon":"github","link":"https://github.com/davidleger95"}],"lastFetch":1729913934448},{"username":"phk422","name":"Hongkun","avatar":"https://avatars.githubusercontent.com/u/59734322?v=4","links":[{"icon":"github","link":"https://github.com/phk422"}],"lastFetch":1731241421343},{"username":"mzronek","name":"Matthias Zronek","avatar":"https://avatars.githubusercontent.com/u/3847700?v=4","links":[{"icon":"github","link":"https://github.com/mzronek"}],"lastFetch":1731241421491},{"username":"raurfang","name":"Łukasz Wiśniewski","avatar":"https://avatars.githubusercontent.com/u/867241?v=4","links":[{"icon":"github","link":"https://github.com/raurfang"}],"lastFetch":1731241421658},{"username":"JeanRemiDelteil","name":"Jean-Rémi Delteil","avatar":"https://avatars.githubusercontent.com/u/9743907?v=4","links":[{"icon":"github","link":"https://github.com/JeanRemiDelteil"}],"lastFetch":1731241421818},{"username":"TzviPM","name":"Tzvi Melamed","avatar":"https://avatars.githubusercontent.com/u/1950680?v=4","links":[{"icon":"github","link":"https://github.com/TzviPM"}],"lastFetch":1731241421959},{"username":"LucaSchwan","name":"ehrenschwan","avatar":"https://avatars.githubusercontent.com/u/25820532?v=4","links":[{"icon":"github","link":"https://github.com/LucaSchwan"}],"lastFetch":1731241422111},{"username":"nzapponi","name":"Niccolo Zapponi","avatar":"https://avatars.githubusercontent.com/u/20582065?v=4","links":[{"icon":"github","link":"https://github.com/nzapponi"}],"lastFetch":1731241422283},{"username":"luchsamapparat","name":"Marvin Luchs","avatar":"https://avatars.githubusercontent.com/u/875017?v=4","links":[{"icon":"github","link":"https://github.com/luchsamapparat"}],"lastFetch":1731241422433},{"username":"nmacmunn","name":"Neil MacMunn","avatar":"https://avatars.githubusercontent.com/u/849964?v=4","links":[{"icon":"github","link":"https://github.com/nmacmunn"}],"lastFetch":1731241422569}],"openapi-fetch":[{"username":"drwpow","name":"Drew Powers","avatar":"https://avatars.githubusercontent.com/u/1369770?v=4","links":[{"icon":"github","link":"https://github.com/drwpow"}],"lastFetch":1731241407720},{"username":"fergusean","name":null,"avatar":"https://avatars.githubusercontent.com/u/1029297?v=4","links":[{"icon":"github","link":"https://github.com/fergusean"}],"lastFetch":1731241407870},{"username":"shinzui","name":"Nadeem Bitar","avatar":"https://avatars.githubusercontent.com/u/519?v=4","links":[{"icon":"github","link":"https://github.com/shinzui"}],"lastFetch":1731241408021},{"username":"ezpuzz","name":"Emory Petermann","avatar":"https://avatars.githubusercontent.com/u/672182?v=4","links":[{"icon":"github","link":"https://github.com/ezpuzz"}],"lastFetch":1731241408171},{"username":"KotoriK","name":null,"avatar":"https://avatars.githubusercontent.com/u/52659125?v=4","links":[{"icon":"github","link":"https://github.com/KotoriK"}],"lastFetch":1731241408324},{"username":"fletchertyler914","name":"Tyler Fletcher","avatar":"https://avatars.githubusercontent.com/u/3344498?v=4","links":[{"icon":"github","link":"https://github.com/fletchertyler914"}],"lastFetch":1731241408481},{"username":"nholik","name":"Nicklos Holik","avatar":"https://avatars.githubusercontent.com/u/2022214?v=4","links":[{"icon":"github","link":"https://github.com/nholik"}],"lastFetch":1731241408620},{"username":"roj1512","name":null,"avatar":"https://avatars.githubusercontent.com/u/175297870?v=4","links":[{"icon":"github","link":"https://github.com/roj1512"}],"lastFetch":1731241408786},{"username":"nickcaballero","name":"Nick Caballero","avatar":"https://avatars.githubusercontent.com/u/355976?v=4","links":[{"icon":"github","link":"https://github.com/nickcaballero"}],"lastFetch":1731241408946},{"username":"hd-o","name":"Hadrian de Oliveira","avatar":"https://avatars.githubusercontent.com/u/58871222?v=4","links":[{"icon":"github","link":"https://github.com/hd-o"}],"lastFetch":1731241409096},{"username":"kecrily","name":"Percy Ma","avatar":"https://avatars.githubusercontent.com/u/45708948?v=4","links":[{"icon":"github","link":"https://github.com/kecrily"}],"lastFetch":1731241409244},{"username":"psychedelicious","name":"psychedelicious","avatar":"https://avatars.githubusercontent.com/u/4822129?v=4","links":[{"icon":"github","link":"https://github.com/psychedelicious"}],"lastFetch":1731241409403},{"username":"muttonchop","name":"Adam K","avatar":"https://avatars.githubusercontent.com/u/1037657?v=4","links":[{"icon":"github","link":"https://github.com/muttonchop"}],"lastFetch":1731241409577},{"username":"marcomuser","name":"Marco Muser","avatar":"https://avatars.githubusercontent.com/u/64737396?v=4","links":[{"icon":"github","link":"https://github.com/marcomuser"}],"lastFetch":1731241409724},{"username":"HugeLetters","name":"Evgenii Perminov","avatar":"https://avatars.githubusercontent.com/u/119697239?v=4","links":[{"icon":"github","link":"https://github.com/HugeLetters"}],"lastFetch":1731241409891},{"username":"Fumaz","name":"alex","avatar":"https://avatars.githubusercontent.com/u/45318608?v=4","links":[{"icon":"github","link":"https://github.com/Fumaz"}],"lastFetch":1731241410068},{"username":"darwish","name":"Mike Darwish","avatar":"https://avatars.githubusercontent.com/u/292570?v=4","links":[{"icon":"github","link":"https://github.com/darwish"}],"lastFetch":1731241410224},{"username":"kaechele","name":"Felix Kaechele","avatar":"https://avatars.githubusercontent.com/u/454490?v=4","links":[{"icon":"github","link":"https://github.com/kaechele"}],"lastFetch":1731241410405},{"username":"phk422","name":"Hongkun","avatar":"https://avatars.githubusercontent.com/u/59734322?v=4","links":[{"icon":"github","link":"https://github.com/phk422"}],"lastFetch":1731241410564},{"username":"mikestopcontinues","name":"Mike Stop Continues","avatar":"https://avatars.githubusercontent.com/u/150434?v=4","links":[{"icon":"github","link":"https://github.com/mikestopcontinues"}],"lastFetch":1731241410708},{"username":"JE-Lee","name":"maurice","avatar":"https://avatars.githubusercontent.com/u/19794813?v=4","links":[{"icon":"github","link":"https://github.com/JE-Lee"}],"lastFetch":1731241410856},{"username":"vipentti","name":"Ville Penttinen","avatar":"https://avatars.githubusercontent.com/u/4726680?v=4","links":[{"icon":"github","link":"https://github.com/vipentti"}],"lastFetch":1731241411028},{"username":"armandabric","name":"Armand Abric","avatar":"https://avatars.githubusercontent.com/u/95120?v=4","links":[{"icon":"github","link":"https://github.com/armandabric"}],"lastFetch":1731241411171},{"username":"illright","name":"Lev Chelyadinov","avatar":"https://avatars.githubusercontent.com/u/15035286?v=4","links":[{"icon":"github","link":"https://github.com/illright"}],"lastFetch":1731241411329}],"openapi-react-query":[{"username":"drwpow","name":"Drew Powers","avatar":"https://avatars.githubusercontent.com/u/1369770?v=4","links":[{"icon":"github","link":"https://github.com/drwpow"}],"lastFetch":1731241407714},{"username":"kerwanp","name":"Martin Paucot","avatar":"https://avatars.githubusercontent.com/u/36955373?v=4","links":[{"icon":"github","link":"https://github.com/kerwanp"}],"lastFetch":1731241407862},{"username":"yoshi2no","name":"yoshi2no","avatar":"https://avatars.githubusercontent.com/u/57059705?v=4","links":[{"icon":"github","link":"https://github.com/yoshi2no"}],"lastFetch":1731241408013},{"username":"elaygelbart","name":"Elay Gelbart","avatar":"https://avatars.githubusercontent.com/u/88675154?v=4","links":[{"icon":"github","link":"https://github.com/elaygelbart"}],"lastFetch":1730904973287}],"swr-openapi":[{"username":"htunnicliff","name":"Hunter Tunnicliff","avatar":"https://avatars.githubusercontent.com/u/7614039?v=4","links":[{"icon":"github","link":"https://github.com/htunnicliff"}],"lastFetch":1731241407730}],"openapi-metadata":[{"username":"kerwanp","name":"Martin Paucot","avatar":"https://avatars.githubusercontent.com/u/36955373?v=4","links":[{"icon":"github","link":"https://github.com/kerwanp"}],"lastFetch":1731241407754},{"username":"drwpow","name":"Drew Powers","avatar":"https://avatars.githubusercontent.com/u/1369770?v=4","links":[{"icon":"github","link":"https://github.com/drwpow"}],"lastFetch":1731241407895}]} \ No newline at end of file diff --git a/docs/openapi-metadata/about.md b/docs/openapi-metadata/about.md new file mode 100644 index 000000000..512c3749b --- /dev/null +++ b/docs/openapi-metadata/about.md @@ -0,0 +1,23 @@ +--- +title: About openapi-metadata +description: openapi-metadata Project Goals, comparisons, and more +--- + + + +# About + +## Project Goals + +1. Must respect the OpenAPI V3 specification +2. Be extensible and easily integrated inside backend frameworks +3. Be focused around developer experience + +## Contributors + +This library wouldn’t be possible without all these amazing contributors: + + diff --git a/docs/openapi-metadata/decorators.md b/docs/openapi-metadata/decorators.md new file mode 100644 index 000000000..ad5a28729 --- /dev/null +++ b/docs/openapi-metadata/decorators.md @@ -0,0 +1,25 @@ +--- +title: Decorators +--- + +# Decorators + +Decorators are used to enrich your OpenAPI specifications. They can be applied on a Controller, a Method or a Model. They are all prefixed with `Api`. + +> For more information about the decorators, you can directly refer to the [source code](https://github.com/openapi-ts/openapi-typescript/packages/openapi-metadata/src/decorators). + +| Decorator | Usage | Description | +| ----------------------- | ------------------- | ------------------------------------------------------------------------ | +| `@ApiBody` | Method | Sets the requestBody of the operation. | +| `@ApiCookie` | Controller / Method | Adds a cookie parameter to the operation(s). | +| `@ApiExcludeController` | Method | Excludes the operations of this controller from the document generation. | +| `@ApiExcludeOperation` | Method | Excludes this operation from the document generation. | +| `@ApiExtraModels` | Controller | Adds extra models to be loaded in the schema. | +| `@ApiHeader` | Controller / Method | Adds a header parameter to the operation(s). | +| `@ApiOperation` | Method | Configures an operation. | +| `@ApiParam` | Controller / Method | Adds a path parameter to the operation(s). | +| `@ApiProperty` | Model | Configures a schema property property. | +| `@ApiQuery` | Controller / Method | Adds a query parameter to the operation(s). | +| `@ApiResponse` | Controller / Method | Adds a response to the operation(s). | +| `@ApiSecurity` | Controller / Method | Sets the security scheme to the operation(s). | +| `@ApiTags` | Controller / Method | Adds tags to the operation(s). | diff --git a/docs/openapi-metadata/examples.md b/docs/openapi-metadata/examples.md new file mode 100644 index 000000000..1f9091c9b --- /dev/null +++ b/docs/openapi-metadata/examples.md @@ -0,0 +1,49 @@ +--- +title: Examples +--- + +# Examples + +This library is made to be used through an integration with your favorite framework but you can as well use it directly as a document generator. + +## Express + +```ts +import express from "express"; +import { generateDocument } from "openapi-metadata"; +import { generateScalarUI } from "openapi-metadata/ui"; + +const app = express(); + +const document = await generateDocument(yourConfiguration); + +app.get("/api", async (req, res) => { + res.send(JSON.stringify(document)); +}); + +app.get("/api/docs", (req, res) => { + const ui = generateScalarUI("/api"); + res.send(ui); +}); +``` + +## Fastify + +```ts +import fastify from "fastify"; +import { generateDocument } from "openapi-metadata"; +import { generateScalarUI } from "openapi-metadata/ui"; + +const app = Fastify(); + +const document = await generateDocument(yourConfiguration); + +app.get("/api", async () => { + return document; +}); + +app.get("/api/docs", () => { + const ui = generateScalarUI("/api"); + return ui; +}); +``` diff --git a/docs/openapi-metadata/index.md b/docs/openapi-metadata/index.md new file mode 100644 index 000000000..24b35bc64 --- /dev/null +++ b/docs/openapi-metadata/index.md @@ -0,0 +1,213 @@ +--- +title: "Getting started" +--- + +# Introduction + +`openapi-metadata` is a framework agnostic library to automatically generate OpenAPI schemas and documentation by using Typescript decorators and metadata. + +::: code-group + +```ts [users_controller.ts] +import { ApiOperation, ApiResponse } from "openapi-metadata/decorators"; +import User from "./user"; + +class UsersController { + @ApiOperation({ + method: "get", + pattern: "/users", + summary: "List users" + }) + @ApiResponse({ type: [User] }) + async list() { + ... + } +} +``` + +```ts [user.ts] +import { ApiProperty } from "openapi-metadata/decorators"; + +class User { + @ApiProperty() + declare id: number; + + @ApiProperty() + declare name: string; + + @ApiProperty({ required: false }) + declare mobile?: string; +} +``` + +```ts [index.ts] +import "reflect-metadata"; +import { generateDocument } from "openapi-metadata"; +import UsersController from "./users_controller"; + +const document = await generateDocument({ + controllers: [UsersController], + document: { + info: { + name: "My Api", + version: "1.0.0", + }, + }, +}); + +console.log(document); // <- Your generated OpenAPI specifications +``` + +::: + +- ✅ Fully compliant [OpenAPI V3](https://swagger.io/specification/) +- ✅ Automatic type inference +- ✅ Supports [Scalar](https://scalar.com/), [Swagger UI](https://swagger.io/tools/swagger-ui/) and [Rapidoc](https://rapidocweb.com/) +- ✅ Extensible with custom type loaders +- ✅ Ready to be integrated with your favorite framework + +## Getting started + +### Setup + +Install `openapi-metadata` and `reflect-metadata` using your favorite package manager. + +```bash +npm install openapi-metadata reflect-metadata +``` + +Import `reflect-metadata` in your main file. + +::: code-group + +```ts [index.ts] +import "reflect-metadata"; + +// Rest of your app +``` + +::: + +Enable `experimentalDecorators` and `experimentalDecorators`. + +::: code-group + +```json [tsconfig.json] +{ + "compilerOptions": { + "emitDecoratorMetadata": true, + "experimentalDecorators": true + } +} +``` + +::: + +### Create your OpenAPI document + +To get started, you can use the `generateDocument` function to create an (almost) empty documentation. You can define a base document that will be merged with the generated one. + +::: code-group + +```ts [index.ts] +import "reflect-metadata"; +import { generateDocument } from "openapi-metadata"; + +const builder = await generateDocument({ + controllers: [], + document: { + info: { + name: "My API", + version: "1.0.0", + }, + }, +}); + +console.log(document.build()); // <- Your generated OpenAPI specifications +``` + +::: + +### Create your first controller + +A controller is a simple class where each methods could be an Operation. +In the following example we have a `UsersController` which declares an operation `GET /users` that returns a list of `Users`. + +::: code-group + +```ts [controllers/users_controller.ts] +import { ApiOperation, ApiResponse } from "openapi-metadata/decorators"; +import User from "../schemas/user"; + +export default class UsersController { + @ApiOperation({ + method: "get", + pattern: "/users", + summary: "List users", + }) + @ApiResponse({ type: [User] }) + async list() { + // ...your logic + } +} +``` + +::: + +### Create your first schema + +In our controller we define the response of your operation to be `[User]` (a list of users). We now need to create this model. + +By using the `@ApiProperty` decorator on class we can define the properties of our schema. + +> Unlike other libraries like `@nestjs/swagger`, every element of your OpenAPI schema is lazy-loaded. Your models will only be part of your documentation if it is used. + +::: code-group + +```ts [schemas/user.ts] +import { ApiProperty } from "openapi-metadata/decorators"; + +export default class User { + @ApiProperty() + declare id: string; + + @ApiProperty({ example: "John Doe" }) + declare name: string; + + @ApiProperty() + declare email: string; + + @ApiProperty({ required: false }) + declare mobile?: string; +} +``` + +::: + +### Add the controller to the generated document + +Now that we have our controller ready, we can use it to generate our document. + +::: code-group + +```ts [index.ts] +import "reflect-metadata"; +import { generateDocument } from "openapi-metadata"; +import UsersController from "./controllers/users_controller.ts"; + +const builder = await generateDocument({ + controllers: [UsersController], + document: { + info: { + name: "My API", + version: "1.0.0", + }, + }, +}); + +console.log(document.build()); // <- Your generated OpenAPI specifications +``` + +::: + +### Going further diff --git a/docs/openapi-metadata/metadata.md b/docs/openapi-metadata/metadata.md new file mode 100644 index 000000000..295ec0c4a --- /dev/null +++ b/docs/openapi-metadata/metadata.md @@ -0,0 +1,87 @@ +--- +title: Metadata +--- + +# Metadata + +[Decorators](./decorators.md) does not contain business logic, their purpose is to store metadata to be used when generating the document. +This makes it easy to integrate this library into a framework and create custom decorators. + +Here is an example of a custom decorator that define an operation summary: + +```ts +import { OperationMetadataStorage } from "openapi-metadata/metadata"; + +export function ApiSummary(summary: string): MethodDecorator { + return (target, propertyKey) => { + OperationMetadataStorage.mergeMetadata(target, { summary }, propertyKey); + }; +} +``` + +## MetadataStorage + +A MetadataStorage is a utility for managing metadata: + +### `defineMetadata` + +This method sets the metadata. It overwrites existing metadata. + +```ts +import { OperationMetadataStorage } from "openapi-metadata/metadata"; + +// Without propertyKey +OperationMetadataStorage.defineMetadata(target, { summary: "Hello world" }); + +// With propertyKey +OperationMetadataStorage.defineMetadata( + target, + { summary: "Hello world" }, + propertyKey, +); +``` + +### `mergeMetadata` + +Similar to `defineMetadata`, this method sets the metadata but [deepmerge](https://www.npmjs.com/package/deepmerge) its content with exising metadata. + +```ts +import { OperationMetadataStorage } from "openapi-metadata/metadata"; + +// Without propertyKey +OperationMetadataStorage.mergeMetadata(target, { summary: "Hello world" }); + +// With propertyKey +OperationMetadataStorage.mergeMetadata( + target, + { summary: "Hello world" }, + propertyKey, +); +``` + +### `getMetadata` + +This method retrieve the stored metadata. When used with a `propertyKey` you can also define `withParent` to [deepmerge](https://www.npmjs.com/package/deepmerge) the metadata with the one defined on the class. + +```ts +import { OperationMetadataStorage } from "openapi-metadata/metadata"; + +OperationMetadataStorage.getMetadata(target); +OperationMetadataStorage.getMetadata(target, propertyKey); +OperationMetadataStorage.getMetadata(target, propertyKey, true); +``` + +## Custom MetadataStorage + +You can create a custom metadata storage by using the `createMetadataStorage` function. + +```ts +import { createMetadataStorage } from "openapi-metadata/metadata"; + +type CustomMetadata = { foo: "bar" }; + +const CustomMetadataKey = Symbol("Custom"); + +const CustomMetadataStorage = + createMetadataStorage(CustomMetadataKey); +``` diff --git a/docs/openapi-metadata/type-loader.md b/docs/openapi-metadata/type-loader.md new file mode 100644 index 000000000..dd001d254 --- /dev/null +++ b/docs/openapi-metadata/type-loader.md @@ -0,0 +1,56 @@ +--- +title: Type Loader +--- + +# Type Loader + +A Type Loader transforms a `Constructor` into a `SchemaObject`. It gives the ability to extends the inference capabilities when generating a document. + +For example `String` will be transformed into `{ type: 'string' }`. + +## Custom Type Loader + +In this example we will see how to create a custom loader for the [Luxon](https://moment.github.io/luxon/#/?id=luxon) `DateTime`. + +In the following schema we have a property that cannot be loaded as is is unknown by the Document generator. + +```ts +import { DateTime } from "luxon"; + +export default class User { + @ApiProperty() + createdAt: DateTime; +} +``` + +When our API returns a Luxon DateTime, it uses the `toString()` meaning that our client will receive a string. +The user could explicitly define the type `@ApiProperty({ type: 'string' })` but as this library goal is to provide a great DX it is possible to create a custom type loader. + +```ts +import { TypeLoaderFn, generateDocument } from "openapi-metadata"; +import { DateTime } from "luxon"; + +const LuxonDateTimeLoader = (_context, value) => { + if (value === DateTime) { + return { type: "string" }; + } +}; + +await generateDocument({ + loaders: [LuxonDateTimeLoader], +}); +``` + +If you have more complex schemas to generate, you can store it in the components and return a reference instead: + +```ts +const CustomLoader: TypeLoaderFn = (context, value) => { + if (isCustom(value)) { + const [name, schema] = generateCustomSchema(value); + + context.schemas[name] = schema; + + return { $ref: `#/components/schemas/${name}` }; + } +}; +``` diff --git a/docs/openapi-metadata/ui.md b/docs/openapi-metadata/ui.md new file mode 100644 index 000000000..2fc67c720 --- /dev/null +++ b/docs/openapi-metadata/ui.md @@ -0,0 +1,31 @@ +--- +title: UI integrations +--- + +# UI integrations + +`openapi-metadata` bring some utilities to easily display your API documentation. + +## [Scalar](https://scalar.com) + +```ts +import { generateScalarUI } from "openapi-metadata/ui"; + +generateScalarUI("http://localhost:3000/api"); +``` + +## [Swagger UI](https://swagger.io/tools/swagger-ui/) + +```ts +import { generateSwaggerUI } from "openapi-metadata/ui"; + +generateSwaggerUI("http://localhost:3000/api"); +``` + +## [Rapidoc](https://rapidocweb.com/) + +```ts +import { generateRapidocUI } from "openapi-metadata/ui"; + +generateRapidocUI("http://localhost:3000/api"); +``` diff --git a/docs/scripts/update-contributors.js b/docs/scripts/update-contributors.js index 7f95c6183..b627ef755 100644 --- a/docs/scripts/update-contributors.js +++ b/docs/scripts/update-contributors.js @@ -146,7 +146,6 @@ const CONTRIBUTORS = { "JorrinKievit", "WickyNilliams", "hrsh7th", - "davidleger95", "phk422", "mzronek", "raurfang", @@ -183,20 +182,26 @@ const CONTRIBUTORS = { "armandabric", "illright", ]), - "openapi-react-query": new Set(["drwpow", "kerwanp", "yoshi2no", "elaygelbart"]), + "openapi-react-query": new Set(["drwpow", "kerwanp", "yoshi2no"]), "swr-openapi": new Set(["htunnicliff"]), + "openapi-metadata": new Set(["kerwanp", "drwpow"]), }; async function main() { let i = 0; - const total = Object.values(CONTRIBUTORS).reduce((total, next) => total + next.size, 0); + const total = Object.values(CONTRIBUTORS).reduce( + (total, next) => total + next.size, + 0, + ); await Promise.all( Object.entries(CONTRIBUTORS).map(async ([repo, contributors]) => { data[repo] ??= []; for (const username of [...contributors]) { i++; // skip profiles that have been updated within the past week - const { lastFetch } = data[repo].find((u) => u.username === username) ?? { lastFetch: 0 }; + const { lastFetch } = data[repo].find( + (u) => u.username === username, + ) ?? { lastFetch: 0 }; if (Date.now() - lastFetch < ONE_WEEK) { // biome-ignore lint/suspicious/noConsoleLog: this is a script console.log(`[${i}/${total}] (Skipped ${username})`); @@ -214,7 +219,10 @@ async function main() { upsert(data[repo], userData); // biome-ignore lint/suspicious/noConsoleLog: this is a script console.log(`[${i}/${total}] Updated for ${username}`); - fs.writeFileSync(new URL("../data/contributors.json", import.meta.url), JSON.stringify(data)); // update file while fetching (sync happens safely in between fetches) + fs.writeFileSync( + new URL("../data/contributors.json", import.meta.url), + JSON.stringify(data), + ); // update file while fetching (sync happens safely in between fetches) } catch (err) { if (err instanceof UserFetchError && err.notFound) { console.warn(`[${i}/${total}] (Skipped ${username}, not found)`); diff --git a/package.json b/package.json index 0662645e7..cca2ef4af 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "scripts": { "build": "pnpm run -r --filter \"!*docs\" --aggregate-output build", "lint": "pnpm run -r --parallel --aggregate-output lint", + "dev": "pnpm run -r --parallel --filter \"{packages/*}\" --aggregate-output dev", "format": "pnpm run -r --parallel --aggregate-output format", "test": "pnpm run -r --parallel --aggregate-output test", "test-e2e": "pnpm run -r --parallel --aggregate-output test-e2e", diff --git a/packages/openapi-metadata/.npmignore b/packages/openapi-metadata/.npmignore new file mode 100644 index 000000000..215f90603 --- /dev/null +++ b/packages/openapi-metadata/.npmignore @@ -0,0 +1,4 @@ +test +tsconfig*.json +vitest.config.ts +biome.json diff --git a/packages/openapi-metadata/CONTRIBUTING.md b/packages/openapi-metadata/CONTRIBUTING.md new file mode 100644 index 000000000..e64717c75 --- /dev/null +++ b/packages/openapi-metadata/CONTRIBUTING.md @@ -0,0 +1,91 @@ +# Contributing + +Thanks for being willing to contribute! 🙏 + +**Working on your first Pull Request (PR)?** You can learn how from this free series [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). + +## Open issues + +Please check out the [the open issues](https://github.com/openapi-ts/openapi-typescript/issues). Issues labelled [**Good First Issue**](https://github.com/openapi-ts/openapi-typescript/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) are especially good to start with. + +Contributing doesn’t have to be in code. Simply answering questions in open issues or providing workarounds is as important as making pull requests. + +## Writing code + +### Setup + +1. Install [pnpm](https://pnpm.io/) +2. [Fork this repo](https://docs.github.com/en/get-started/quickstart/fork-a-repo) and clone your copy locally +3. Run `pnpm i` to install dependencies + +### Testing + +This library uses [Vitest](https://vitest.dev/) for testing. There’s a great [VS Code extension](https://marketplace.visualstudio.com/items?itemName=ZixuanChen.vitest-explorer) you can optionally use if you’d like in-editor debugging tools. + +To run the entire test suite, run: + +```bash +pnpm test +``` + +To run an individual test: + +```bash +pnpm test -- [partial filename] +``` + +To start the entire test suite in watch mode: + +```bash +npx vitest +``` + +#### TypeScript tests + +**Don’t neglect writing TS tests!** In the test suite, you’ll see `// @ts-expect-error` comments. These are critical tests in and of themselves—they are asserting that TypeScript throws an error when it should be throwing an error (the test suite will actually fail in places if a TS error is _not_ raised). + +As this is just a minimal fetch wrapper meant to provide deep type inference for API schemas, **testing TS types** is arguably more important than testing the runtime. So please make liberal use of `// @ts-expect-error`, and as a general rule of thumb, write more **unwanted** output tests than _wanted_ output tests. + +### Running linting + +Linting is handled via [Biome](https://biomejs.dev), a faster ESLint replacement. It was installed with `pnpm i` and can be run with: + +```bash +pnpm run lint +``` + +### Changelogs + +The changelog is generated via [changesets](https://github.com/changesets/changesets), and is separate from Git commit messages and pull request titles. To write a human-readable changelog for your changes, run: + +``` +npx changeset +``` + +This will ask if it’s a `patch`, `minor`, or `major` change ([semver](https://semver.org/)), along with a plain description of what you did. Commit this new file along with the rest of your PR, and during the next release this will go into the official changelog! + +## Opening a Pull Request + +Pull requests are **welcome** for this repo! + +Bugfixes will always be accepted, though in some cases some small changes may be requested. + +However, if adding a feature or breaking change, please **open an issue first to discuss.** This ensures no time or work is wasted writing code that won’t be accepted to the project (see [Project Goals](https://openapi-ts.dev/openapi-fetch/about/#project-goals)). Undiscussed feature work may be rejected at the discretion of the maintainers. + +### Writing the commit + +Create a new branch for your PR with `git checkout -b your-branch-name`. Add the relevant code as well as docs and tests. When you push everything up (`git push`), navigate back to your repo in GitHub and you should see a prompt to open a new PR. + +While best practices for commit messages are encouraged (e.g. start with an imperative verb, keep it short, use the body if needed), this repo doesn’t follow any specific guidelines. Clarity is favored over strict rules. Changelogs are generated separately from git (see [the Changelogs section](#changelogs)). + +### Writing the PR notes + +**Please fill out the template!** It’s a very lightweight template 🙂. + +### Adding docs + +If you added a feature, or changed how something worked, please [update the docs](../../docs/)! + +### Passing CI + +All PRs must fix lint errors, and all tests must pass. PRs will not be merged until all CI checks are “green” (✅). diff --git a/packages/openapi-metadata/LICENSE b/packages/openapi-metadata/LICENSE new file mode 100644 index 000000000..03c72fbc2 --- /dev/null +++ b/packages/openapi-metadata/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Martin Paucot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/openapi-metadata/README.md b/packages/openapi-metadata/README.md new file mode 100644 index 000000000..7ae42aafb --- /dev/null +++ b/packages/openapi-metadata/README.md @@ -0,0 +1,19 @@ +# openapi-metadata + +`openapi-metadata` is a framework agnostic library to automatically generate OpenAPI schemas and documentation by using Typescript decorators and metadata. + +- ✅ Fully compliant [OpenAPI V3](https://swagger.io/specification/) +- ✅ Automatic type inference +- ✅ Supports [Scalar](https://scalar.com/), [Swagger UI](https://swagger.io/tools/swagger-ui/) and [Rapidoc](https://rapidocweb.com/) +- ✅ Extensible with custom type loaders +- ✅ Ready to be integrated with your favorite framework + +## Installation + +```bash +npm i openapi-metadata reflect-metadata +``` + +## 📓 Docs + +[View Docs](https://openapi-ts.dev/openapi-metadata/) diff --git a/packages/openapi-metadata/biome.json b/packages/openapi-metadata/biome.json new file mode 100644 index 000000000..3ef072e57 --- /dev/null +++ b/packages/openapi-metadata/biome.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.1/schema.json", + "extends": ["../../biome.json"], + "files": { + "ignore": ["./example/", "./coverage/"] + }, + "linter": { + "rules": { + "performance": { + "noAccumulatingSpread": "off" + }, + "complexity": { + "noBannedTypes": "off" + }, + "suspicious": { + "noConfusingVoidType": "off" + } + } + } +} diff --git a/packages/openapi-metadata/package.json b/packages/openapi-metadata/package.json new file mode 100644 index 000000000..e5e6db35d --- /dev/null +++ b/packages/openapi-metadata/package.json @@ -0,0 +1,81 @@ +{ + "name": "openapi-metadata", + "description": "Auto-Generate OpenAPI specifications from Typescript decorators", + "version": "0.0.0-rc-4", + "author": { + "name": "Martin PAUCOT", + "email": "contact@martin-paucot.Fr" + }, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./decorators": { + "types": "./dist/decorators/index.d.ts", + "import": "./dist/decorators/index.js" + }, + "./metadata": { + "types": "./dist/metadata/index.d.ts", + "import": "./dist/metadata/index.js" + }, + "./errors": { + "types": "./dist/errors/index.d.ts", + "import": "./dist/errors/index.js" + }, + "./ui": { + "types": "./dist/ui/index.d.ts", + "import": "./dist/ui/index.js" + } + }, + "homepage": "https://openapi-ts.dev", + "repository": { + "type": "git", + "url": "https://github.com/openapi-ts/openapi-typescript", + "directory": "packages/openapi-metadata" + }, + "bugs": { + "url": "https://github.com/openapi-ts/openapi-typescript/issues" + }, + "keywords": [ + "openapi", + "swagger", + "rest", + "api", + "oapi_3", + "oapi_3_1", + "typescript" + ], + "scripts": { + "build": "pnpm run build:clean && pnpm run build:esm", + "build:clean": "del-cli dist", + "build:esm": "tsc -p tsconfig.build.json", + "dev": "tsc -p tsconfig.build.json --watch", + "format": "biome format . --write", + "lint": "biome check .", + "test": "vitest run", + "coverage": "vitest run --coverage", + "version": "pnpm run prepare && pnpm run build" + }, + "dependencies": { + "deepmerge": "^4.3.1", + "openapi-types": "^12.1.3" + }, + "peerDependencies": { + "reflect-metadata": "^0.2.2" + }, + "devDependencies": { + "@types/lodash": "^4.17.7", + "@types/node": "^22.1.0", + "@vitest/coverage-v8": "^2.0.5", + "del-cli": "^5.1.0", + "esbuild": "^0.20.2", + "execa": "^8.0.1", + "reflect-metadata": "^0.2.2", + "tsup": "^8.2.4", + "type-fest": "^4.26.1", + "typescript": "^5.4.5", + "unplugin-swc": "^1.5.1" + } +} diff --git a/packages/openapi-metadata/src/context.ts b/packages/openapi-metadata/src/context.ts new file mode 100644 index 000000000..453b8fe74 --- /dev/null +++ b/packages/openapi-metadata/src/context.ts @@ -0,0 +1,13 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { Logger, TypeLoaderFn } from "./types.js"; + +export class Context { + schemas: Record = {}; + typeLoaders: TypeLoaderFn[]; + logger: Logger; + + constructor(logger?: Logger, typeLoaders?: TypeLoaderFn[]) { + this.logger = logger ?? console; + this.typeLoaders = typeLoaders ?? []; + } +} diff --git a/packages/openapi-metadata/src/decorators/api-body.ts b/packages/openapi-metadata/src/decorators/api-body.ts new file mode 100644 index 000000000..b3f72d21d --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-body.ts @@ -0,0 +1,17 @@ +import type { SetOptional } from "type-fest"; +import { type OperationBodyMetadata, OperationBodyMetadataStorage } from "../metadata/operation-body.js"; + +export type ApiBodyOptions = SetOptional; + +export function ApiBody(options: ApiBodyOptions): MethodDecorator { + return (target, propertyKey) => { + OperationBodyMetadataStorage.defineMetadata( + target, + { + mediaType: "application/json", + ...options, + }, + propertyKey, + ); + }; +} diff --git a/packages/openapi-metadata/src/decorators/api-cookie.ts b/packages/openapi-metadata/src/decorators/api-cookie.ts new file mode 100644 index 000000000..057dfc04e --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-cookie.ts @@ -0,0 +1,9 @@ +import { type OperationParameterMetadata, OperationParameterMetadataStorage } from "../metadata/operation-parameter.js"; + +export type ApiCookieOptions = Omit; + +export function ApiCookie(options: ApiCookieOptions) { + return (target: Object, propertyKey?: string | symbol) => { + OperationParameterMetadataStorage.mergeMetadata(target, [{ in: "cookie", ...options }], propertyKey); + }; +} diff --git a/packages/openapi-metadata/src/decorators/api-exclude.ts b/packages/openapi-metadata/src/decorators/api-exclude.ts new file mode 100644 index 000000000..0837ede04 --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-exclude.ts @@ -0,0 +1,13 @@ +import { ExcludeMetadataStorage } from "../metadata/exclude.js"; + +export function ApiExcludeController(): ClassDecorator { + return (target) => { + ExcludeMetadataStorage.defineMetadata(target, true); + }; +} + +export function ApiExcludeOperation(): MethodDecorator { + return (target, propertyKey) => { + ExcludeMetadataStorage.defineMetadata(target, true, propertyKey); + }; +} diff --git a/packages/openapi-metadata/src/decorators/api-extra-models.ts b/packages/openapi-metadata/src/decorators/api-extra-models.ts new file mode 100644 index 000000000..3e13fc61f --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-extra-models.ts @@ -0,0 +1,8 @@ +import { ExtraModelsMetadataStorage } from "../metadata/extra-models.js"; +import type { Thunk, TypeValue } from "../types.js"; + +export function ApiExtraModels(...models: (TypeValue | Thunk)[]) { + return (target: Object) => { + ExtraModelsMetadataStorage.mergeMetadata(target, models); + }; +} diff --git a/packages/openapi-metadata/src/decorators/api-header.ts b/packages/openapi-metadata/src/decorators/api-header.ts new file mode 100644 index 000000000..b3509503e --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-header.ts @@ -0,0 +1,9 @@ +import { type OperationParameterMetadata, OperationParameterMetadataStorage } from "../metadata/operation-parameter.js"; + +export type ApiHeaderOptions = Omit; + +export function ApiHeader(options: ApiHeaderOptions) { + return (target: Object, propertyKey?: string | symbol) => { + OperationParameterMetadataStorage.mergeMetadata(target, [{ in: "header", ...options }], propertyKey); + }; +} diff --git a/packages/openapi-metadata/src/decorators/api-operation.ts b/packages/openapi-metadata/src/decorators/api-operation.ts new file mode 100644 index 000000000..1f32590a2 --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-operation.ts @@ -0,0 +1,9 @@ +import { type OperationMetadata, OperationMetadataStorage } from "../metadata/operation.js"; + +export type ApiOperationOptions = OperationMetadata; + +export function ApiOperation(options: ApiOperationOptions): MethodDecorator { + return (target, propertyKey) => { + OperationMetadataStorage.defineMetadata(target, options, propertyKey); + }; +} diff --git a/packages/openapi-metadata/src/decorators/api-param.ts b/packages/openapi-metadata/src/decorators/api-param.ts new file mode 100644 index 000000000..0ae4c4ea5 --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-param.ts @@ -0,0 +1,9 @@ +import { type OperationParameterMetadata, OperationParameterMetadataStorage } from "../metadata/operation-parameter.js"; + +export type ApiParamOptions = Omit; + +export function ApiParam(options: ApiParamOptions) { + return function (target: Object, propertyKey?: string | symbol) { + OperationParameterMetadataStorage.mergeMetadata(target, [{ in: "path", ...options }], propertyKey); + }; +} diff --git a/packages/openapi-metadata/src/decorators/api-property.ts b/packages/openapi-metadata/src/decorators/api-property.ts new file mode 100644 index 000000000..ca10ee4b0 --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-property.ts @@ -0,0 +1,49 @@ +import type { Context } from "../context.js"; +import { SymbolKeysNotSupportedError } from "../errors/symbol-keys-not-supported.js"; +import { type PropertyMetadata, PropertyMetadataStorage } from "../metadata/property.js"; +import { findType } from "../utils/metadata.js"; + +export type ApiPropertyOptions = Partial; + +export function ApiProperty(options?: ApiPropertyOptions): PropertyDecorator; +export function ApiProperty(options?: ApiPropertyOptions): MethodDecorator; +export function ApiProperty(options?: ApiPropertyOptions): PropertyDecorator | MethodDecorator { + return (prototype, propertyKey, descriptor) => { + const isMethod = Boolean(descriptor?.value); + + if (typeof propertyKey === "symbol") { + throw new SymbolKeysNotSupportedError(); + } + + const metadata = { + name: options?.name ?? propertyKey, + required: true, + ...options, + } as PropertyMetadata; + + if (!metadata.type && !metadata.schema && !metadata.enum) { + (metadata as any).type = (context: Context) => + findType({ + context, + metadataKey: isMethod ? "design:returntype" : "design:type", + prototype, + propertyKey, + }); + } + + PropertyMetadataStorage.mergeMetadata(prototype, { + [metadata.name]: metadata as PropertyMetadata, + }); + }; +} + +export function ApiPropertyOptional(options?: Omit): PropertyDecorator; +export function ApiPropertyOptional(options?: Omit): MethodDecorator; +export function ApiPropertyOptional( + options?: Omit, +): PropertyDecorator | MethodDecorator { + return ApiProperty({ + ...options, + required: false, + }); +} diff --git a/packages/openapi-metadata/src/decorators/api-query.ts b/packages/openapi-metadata/src/decorators/api-query.ts new file mode 100644 index 000000000..caba1bb44 --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-query.ts @@ -0,0 +1,9 @@ +import { type OperationParameterMetadata, OperationParameterMetadataStorage } from "../metadata/operation-parameter.js"; + +export type ApiQueryOptions = Omit; + +export function ApiQuery(options: ApiQueryOptions) { + return function (target: Object, propertyKey?: string | symbol) { + OperationParameterMetadataStorage.mergeMetadata(target, [{ in: "query", ...options }], propertyKey); + }; +} diff --git a/packages/openapi-metadata/src/decorators/api-response.ts b/packages/openapi-metadata/src/decorators/api-response.ts new file mode 100644 index 000000000..80135f203 --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-response.ts @@ -0,0 +1,21 @@ +import type { SetOptional } from "type-fest"; +import { type OperationResponseMetadata, OperationResponseMetadataStorage } from "../metadata/operation-response.js"; + +export type ApiResponseOptions = SetOptional; + +export function ApiResponse(options: ApiResponseOptions) { + return function (target: Object, propertyKey?: string | symbol) { + const metadata = { + status: "default" as const, + mediaType: "application/json", + ...options, + }; + OperationResponseMetadataStorage.mergeMetadata( + target, + { + [metadata.status.toString()]: metadata, + }, + propertyKey, + ); + }; +} diff --git a/packages/openapi-metadata/src/decorators/api-security.ts b/packages/openapi-metadata/src/decorators/api-security.ts new file mode 100644 index 000000000..6067d2426 --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-security.ts @@ -0,0 +1,29 @@ +import { OperationSecurityMetadataStorage } from "../metadata/operation-security.js"; + +export function ApiSecurity(name: string, ...scopes: string[]) { + return (target: Object, propertyKey?: string | symbol) => { + OperationSecurityMetadataStorage.mergeMetadata( + target, + { + [name]: scopes, + }, + propertyKey, + ); + }; +} + +export function ApiBasicAuth() { + return ApiSecurity("basic"); +} + +export function ApiBearerAuth() { + return ApiSecurity("bearer"); +} + +export function ApiCookieAuth() { + return ApiSecurity("cookie"); +} + +export function ApiOauth2(...scopes: string[]) { + return ApiSecurity("oauth2", ...scopes); +} diff --git a/packages/openapi-metadata/src/decorators/api-tags.ts b/packages/openapi-metadata/src/decorators/api-tags.ts new file mode 100644 index 000000000..e0db2e12f --- /dev/null +++ b/packages/openapi-metadata/src/decorators/api-tags.ts @@ -0,0 +1,7 @@ +import { OperationMetadataStorage } from "../metadata/operation.js"; + +export function ApiTags(...tags: string[]) { + return (target: Object, propertyKey?: string | symbol) => { + OperationMetadataStorage.mergeMetadata(target, { tags }, propertyKey); + }; +} diff --git a/packages/openapi-metadata/src/decorators/index.ts b/packages/openapi-metadata/src/decorators/index.ts new file mode 100644 index 000000000..0594158dd --- /dev/null +++ b/packages/openapi-metadata/src/decorators/index.ts @@ -0,0 +1,12 @@ +export { ApiBody } from "./api-body.js"; +export { ApiCookie } from "./api-cookie.js"; +export { ApiHeader } from "./api-header.js"; +export { ApiOperation } from "./api-operation.js"; +export { ApiParam } from "./api-param.js"; +export { ApiProperty, ApiPropertyOptional } from "./api-property.js"; +export { ApiQuery } from "./api-query.js"; +export { ApiResponse } from "./api-response.js"; +export { ApiSecurity } from "./api-security.js"; +export { ApiTags } from "./api-tags.js"; +export { ApiExcludeController, ApiExcludeOperation } from "./api-exclude.js"; +export { ApiExtraModels } from "./api-extra-models.js"; diff --git a/packages/openapi-metadata/src/errors/invalid-configuration.ts b/packages/openapi-metadata/src/errors/invalid-configuration.ts new file mode 100644 index 000000000..c4c466bfa --- /dev/null +++ b/packages/openapi-metadata/src/errors/invalid-configuration.ts @@ -0,0 +1,7 @@ +export class InvalidConfigurationError extends Error { + constructor(message: string) { + super(`Invalid configuration: ${message}`); + + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/packages/openapi-metadata/src/errors/no-explicit-type.ts b/packages/openapi-metadata/src/errors/no-explicit-type.ts new file mode 100644 index 000000000..914cd4315 --- /dev/null +++ b/packages/openapi-metadata/src/errors/no-explicit-type.ts @@ -0,0 +1,15 @@ +export class NoExplicitTypeError extends Error { + constructor(typeName: string, propertyKey: string, parameterIndex?: number, argName?: string) { + let errorMessage = + "Unable to infer OpenAPI type from TypeScript reflection system. " + "You need to provide explicit type for "; + if (argName) { + errorMessage += `argument named '${argName}' of `; + } else if (parameterIndex !== undefined) { + errorMessage += `parameter #${parameterIndex} of `; + } + errorMessage += `'${propertyKey}' of '${typeName}' class.`; + super(errorMessage); + + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/packages/openapi-metadata/src/errors/reflect-metadata-missing.ts b/packages/openapi-metadata/src/errors/reflect-metadata-missing.ts new file mode 100644 index 000000000..9d88bb3ea --- /dev/null +++ b/packages/openapi-metadata/src/errors/reflect-metadata-missing.ts @@ -0,0 +1,9 @@ +export class ReflectMetadataMissingError extends Error { + constructor() { + super( + "Looks like you've forgot to provide experimental metadata API polyfill. " + + "Please read the installation instruction for more details.", + ); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/packages/openapi-metadata/src/errors/symbol-keys-not-supported.ts b/packages/openapi-metadata/src/errors/symbol-keys-not-supported.ts new file mode 100644 index 000000000..6d3232671 --- /dev/null +++ b/packages/openapi-metadata/src/errors/symbol-keys-not-supported.ts @@ -0,0 +1,6 @@ +export class SymbolKeysNotSupportedError extends Error { + constructor() { + super("Symbol keys are not supported yet!"); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/packages/openapi-metadata/src/generators/document.ts b/packages/openapi-metadata/src/generators/document.ts new file mode 100644 index 000000000..d54a601ee --- /dev/null +++ b/packages/openapi-metadata/src/generators/document.ts @@ -0,0 +1,53 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { OpenAPIDocument, TypeLoaderFn } from "../index.js"; +import type { Logger } from "../types.js"; +import type { SetOptional } from "type-fest"; +import { Context } from "../context.js"; +import { generatePaths } from "./paths.js"; +import deepmerge from "deepmerge"; + +export type GenerateDocumentOptions = { + /** + * List of Controller constructors that will be loaded into the document. + * + * @example [UsersController, PostsController] + */ + controllers: Function[]; + + /** + * Base document that will be deep merged with the result. + * + * @example { info: { name: "My Api", version: "1.0.0" } } + */ + document: SetOptional; + + /** + * Custom logger. + * + * @example { warn: (message) => myLogger.warn(mesage) } + * @default console + */ + customLogger?: Logger; + + /** + * Additional type loaders. + * + * @example [VineTypeLoader, LuxonTypeLoader] + */ + loaders?: TypeLoaderFn[]; +}; + +/** + * Generates a compliant OpenAPIV3 schema. + */ +export async function generateDocument(options: GenerateDocumentOptions): Promise { + const context = new Context(options.customLogger, options.loaders); + + return deepmerge(options.document, { + openapi: "3.0.0", + paths: await generatePaths(context, options.controllers), + components: { + schemas: context.schemas, + }, + }); +} diff --git a/packages/openapi-metadata/src/generators/operation-body.ts b/packages/openapi-metadata/src/generators/operation-body.ts new file mode 100644 index 000000000..6ca471a66 --- /dev/null +++ b/packages/openapi-metadata/src/generators/operation-body.ts @@ -0,0 +1,19 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { Context } from "../context.js"; +import type { OperationBodyMetadata } from "../metadata/operation-body.js"; +import { loadType } from "../loaders/type.js"; + +export async function generateOperationBody( + context: Context, + metadata: OperationBodyMetadata, +): Promise { + const schema = await loadType(context, metadata); + + return { + content: { + [metadata.mediaType]: { + schema: schema, + }, + }, + }; +} diff --git a/packages/openapi-metadata/src/generators/operation-parameters.ts b/packages/openapi-metadata/src/generators/operation-parameters.ts new file mode 100644 index 000000000..b4097fe3c --- /dev/null +++ b/packages/openapi-metadata/src/generators/operation-parameters.ts @@ -0,0 +1,16 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { Context } from "../context.js"; +import type { OperationParameterMetadata } from "../metadata/operation-parameter.js"; +import { loadType } from "../loaders/type.js"; + +export async function generateOperationParameters( + context: Context, + metadata: OperationParameterMetadata, +): Promise { + const { schema: s, enum: e, type, ...parameter } = metadata as any; + + return { + ...parameter, + schema: await loadType(context, { type: "string", ...metadata }), + }; +} diff --git a/packages/openapi-metadata/src/generators/operation-response.ts b/packages/openapi-metadata/src/generators/operation-response.ts new file mode 100644 index 000000000..37119c4c6 --- /dev/null +++ b/packages/openapi-metadata/src/generators/operation-response.ts @@ -0,0 +1,21 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { Context } from "../context.js"; +import type { OperationResponseMetadata } from "../metadata/operation-response.js"; +import { loadType } from "../loaders/type.js"; + +export async function generateOperationResponse( + context: Context, + metadata: OperationResponseMetadata, +): Promise { + const { type, schema: s, enum: e, ...response } = metadata as any; + + return { + description: "", + ...response, + content: { + [metadata.mediaType]: { + schema: await loadType(context, metadata), + }, + }, + }; +} diff --git a/packages/openapi-metadata/src/generators/operation.ts b/packages/openapi-metadata/src/generators/operation.ts new file mode 100644 index 000000000..fe8341191 --- /dev/null +++ b/packages/openapi-metadata/src/generators/operation.ts @@ -0,0 +1,49 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { Context } from "../context.js"; +import type { OperationMetadata } from "../metadata/operation.js"; +import { OperationBodyMetadataStorage } from "../metadata/operation-body.js"; +import { generateOperationBody } from "./operation-body.js"; +import { OperationParameterMetadataStorage } from "../metadata/operation-parameter.js"; +import { generateOperationParameters } from "./operation-parameters.js"; +import { OperationResponseMetadataStorage } from "../metadata/operation-response.js"; +import { generateOperationResponse } from "./operation-response.js"; +import { OperationSecurityMetadataStorage } from "../metadata/operation-security.js"; +import { ExtraModelsMetadataStorage } from "../metadata/extra-models.js"; +import { loadType } from "../loaders/type.js"; + +export async function generateOperation( + context: Context, + controller: Function, + propertyKey: string, + { path, methods, ...metadata }: OperationMetadata, +): Promise { + const operation: OpenAPIV3.OperationObject = { ...metadata, responses: {} }; + + const target = controller.prototype; + + const extraModels = ExtraModelsMetadataStorage.getMetadata(target); + + await Promise.all(extraModels.map((m) => loadType(context, { type: m }))); + + const body = OperationBodyMetadataStorage.getMetadata(target, propertyKey); + if (body) { + operation.requestBody = await generateOperationBody(context, body); + } + + const parameters = OperationParameterMetadataStorage.getMetadata(target, propertyKey); + operation.parameters = []; + for (const parameter of parameters) { + operation.parameters.push(await generateOperationParameters(context, parameter)); + } + + const responses = OperationResponseMetadataStorage.getMetadata(target, propertyKey); + for (const [status, response] of Object.entries(responses)) { + operation.responses[status] = await generateOperationResponse(context, response); + } + + const security = OperationSecurityMetadataStorage.getMetadata(target, propertyKey, true); + + operation.security = [security]; + + return operation; +} diff --git a/packages/openapi-metadata/src/generators/paths.ts b/packages/openapi-metadata/src/generators/paths.ts new file mode 100644 index 000000000..d91d2b67e --- /dev/null +++ b/packages/openapi-metadata/src/generators/paths.ts @@ -0,0 +1,43 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { Context } from "../context.js"; +import { generateOperation } from "./operation.js"; +import { ExcludeMetadataStorage, OperationMetadataStorage } from "../metadata/index.js"; + +export async function generatePaths(context: Context, controllers: Function[]): Promise { + const paths: OpenAPIV3.PathsObject = {}; + + for (const controller of controllers) { + const target = controller.prototype; + const keys = Object.getOwnPropertyNames(target); + for (const key of keys) { + const metadata = OperationMetadataStorage.getMetadata(target, key); + if (!metadata) { + continue; + } + + if (!metadata.path || !metadata.methods) { + continue; + } + + const excludeController = ExcludeMetadataStorage.getMetadata(target); + if (excludeController === true) { + continue; + } + + for (const method of metadata.methods) { + const excludeOperation = ExcludeMetadataStorage.getMetadata(target, key); + + if (excludeOperation === true) { + continue; + } + + paths[metadata.path] = { + ...paths[metadata.path], + [method]: await generateOperation(context, controller, key, metadata), + }; + } + } + } + + return paths; +} diff --git a/packages/openapi-metadata/src/index.ts b/packages/openapi-metadata/src/index.ts new file mode 100644 index 000000000..5ae9692f6 --- /dev/null +++ b/packages/openapi-metadata/src/index.ts @@ -0,0 +1,5 @@ +import type { OpenAPIV3 } from "openapi-types"; + +export { generateDocument } from "./generators/document.js"; +export type { TypeLoaderFn } from "./types.js"; +export type OpenAPIDocument = OpenAPIV3.Document; diff --git a/packages/openapi-metadata/src/loaders/type.ts b/packages/openapi-metadata/src/loaders/type.ts new file mode 100644 index 000000000..db751d8d3 --- /dev/null +++ b/packages/openapi-metadata/src/loaders/type.ts @@ -0,0 +1,107 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { Context } from "../context.js"; +import type { TypeLoaderFn, TypeOptions } from "../types.js"; +import type { SetRequired } from "type-fest"; +import { getEnumType, getEnumValues } from "../utils/enum.js"; +import { PropertyMetadataStorage } from "../metadata/property.js"; +import { schemaPath } from "../utils/schema.js"; +import { isThunk } from "../utils/metadata.js"; + +const PrimitiveTypeLoader: TypeLoaderFn = async (_context, value) => { + if (typeof value === "string") { + return { type: value }; + } + + // biome-ignore lint/suspicious/noDoubleEquals: strict comparaison might fail to catch it + if (value == String) { + return { type: "string" }; + } + + // biome-ignore lint/suspicious/noDoubleEquals: strict comparaison might fail to catch it + if (value == Boolean) { + return { type: "boolean" }; + } + + // biome-ignore lint/suspicious/noDoubleEquals: strict comparaison might fail to catch it + if (value == Number) { + return { type: "number" }; + } +}; + +const ClassTypeLoader: TypeLoaderFn = async (context, value) => { + if (typeof value !== "function" || !value.prototype) { + return; + } + + const model = value.name; + + if (context.schemas[model]) { + return { $ref: schemaPath(model) }; + } + + const schema: SetRequired = { + type: "object", + properties: {}, + required: [], + }; + + const properties = PropertyMetadataStorage.getMetadata(value.prototype); + + if (!properties) { + context.logger.warn(`You tried to use '${model}' as a type but it does not contain any ApiProperty.`); + + return; + } + + context.schemas[model] = schema; + + for (const [key, property] of Object.entries(properties)) { + const { required, type, name, enum: e, schema: s, ...metadata } = property as any; + schema.properties[key] = { + ...(await loadType(context, property)), + ...metadata, + }; + + if (property.required) { + schema.required.push(key); + } + } + + return { $ref: schemaPath(model) }; +}; + +export async function loadType( + context: Context, + options: TypeOptions, +): Promise { + if (options.schema) { + return options.schema; + } + + if (options.enum) { + const enumValues = getEnumValues(options.enum); + const enumType = getEnumType(enumValues); + + return { + type: enumType, + enum: enumValues, + }; + } + + if (!options.type) { + context.logger.warn("Failed to infer type from property"); + return; + } + + const thunk = isThunk(options.type); + const value = thunk ? (options.type as Function)(context) : options.type; + + for (const loader of [PrimitiveTypeLoader, ...context.typeLoaders, ClassTypeLoader]) { + const result = await loader(context, value, options.type); + if (result) { + return result; + } + } + + context.logger.warn(`You tried to use '${options.type.toString()}' as a type but no loader supports it ${thunk}`); +} diff --git a/packages/openapi-metadata/src/metadata/exclude.ts b/packages/openapi-metadata/src/metadata/exclude.ts new file mode 100644 index 000000000..0da9537c3 --- /dev/null +++ b/packages/openapi-metadata/src/metadata/exclude.ts @@ -0,0 +1,5 @@ +import { createMetadataStorage } from "./factory.js"; + +export const ExcludeMetadataKey = Symbol("Exclude"); + +export const ExcludeMetadataStorage = createMetadataStorage(ExcludeMetadataKey, false); diff --git a/packages/openapi-metadata/src/metadata/extra-models.ts b/packages/openapi-metadata/src/metadata/extra-models.ts new file mode 100644 index 000000000..c32494ff8 --- /dev/null +++ b/packages/openapi-metadata/src/metadata/extra-models.ts @@ -0,0 +1,9 @@ +import type { Thunk, TypeValue } from "../types.js"; +import { createMetadataStorage } from "./factory.js"; + +export const ExtraModelsMetadataKey = Symbol("ExtraModels"); + +export const ExtraModelsMetadataStorage = createMetadataStorage<(TypeValue | Thunk)[]>( + ExtraModelsMetadataKey, + [], +); diff --git a/packages/openapi-metadata/src/metadata/factory.ts b/packages/openapi-metadata/src/metadata/factory.ts new file mode 100644 index 000000000..bca9286e7 --- /dev/null +++ b/packages/openapi-metadata/src/metadata/factory.ts @@ -0,0 +1,38 @@ +import deepmerge from "deepmerge"; + +export function createMetadataStorage(key: Symbol | string, defaultMetadata?: T) { + function defineMetadata(object: Object, metadata: T, propertyKey?: string | symbol) { + if (propertyKey) { + Reflect.defineMetadata(key, metadata, object, propertyKey); + } else { + Reflect.defineMetadata(key, metadata, object); + } + } + + function getMetadata(object: Object, propertyKey?: string | symbol, withParent = false): T { + if (propertyKey) { + let metadata = Reflect.getMetadata(key, object, propertyKey) ?? defaultMetadata; + + if (withParent) { + metadata = deepmerge(getMetadata(object.constructor), metadata); + } + + return metadata; + } else { + return Reflect.getMetadata(key, object) ?? defaultMetadata; + } + } + + function mergeMetadata(object: Object, metadata: T, propertyKey?: string | symbol): T { + const existing = getMetadata(object, propertyKey); + const merged = deepmerge(existing, metadata) as T; + defineMetadata(object, merged, propertyKey); + return merged; + } + + return { + defineMetadata, + getMetadata, + mergeMetadata, + }; +} diff --git a/packages/openapi-metadata/src/metadata/index.ts b/packages/openapi-metadata/src/metadata/index.ts new file mode 100644 index 000000000..6c39b17c4 --- /dev/null +++ b/packages/openapi-metadata/src/metadata/index.ts @@ -0,0 +1,9 @@ +export * from "./operation-body.js"; +export * from "./operation-header.js"; +export * from "./operation-parameter.js"; +export * from "./operation-response.js"; +export * from "./operation-security.js"; +export * from "./operation.js"; +export * from "./property.js"; +export * from "./exclude.js"; +export * from "./extra-models.js"; diff --git a/packages/openapi-metadata/src/metadata/operation-body.ts b/packages/openapi-metadata/src/metadata/operation-body.ts new file mode 100644 index 000000000..1c4bc8432 --- /dev/null +++ b/packages/openapi-metadata/src/metadata/operation-body.ts @@ -0,0 +1,9 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { TypeOptions } from "../types.js"; +import { createMetadataStorage } from "./factory.js"; + +export type OperationBodyMetadata = Omit & { mediaType: string } & TypeOptions; + +export const OperationBodyMetadataKey = Symbol("OperationBody"); + +export const OperationBodyMetadataStorage = createMetadataStorage(OperationBodyMetadataKey); diff --git a/packages/openapi-metadata/src/metadata/operation-header.ts b/packages/openapi-metadata/src/metadata/operation-header.ts new file mode 100644 index 000000000..0aef15a19 --- /dev/null +++ b/packages/openapi-metadata/src/metadata/operation-header.ts @@ -0,0 +1,12 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { TypeOptions } from "../types.js"; +import { createMetadataStorage } from "./factory.js"; + +export type OperationHeaderMetadata = Omit & { + name: string; +} & Partial; + +export const OperationHeaderSymbol = Symbol("OperationHeader"); + +export const OperationHeaderMetadataStorage = + createMetadataStorage>(OperationHeaderSymbol); diff --git a/packages/openapi-metadata/src/metadata/operation-parameter.ts b/packages/openapi-metadata/src/metadata/operation-parameter.ts new file mode 100644 index 000000000..47daadd4f --- /dev/null +++ b/packages/openapi-metadata/src/metadata/operation-parameter.ts @@ -0,0 +1,15 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { TypeOptions } from "../types.js"; +import { createMetadataStorage } from "./factory.js"; + +export type OperationParameterMetadata = Omit & { + name: string; + in: "path" | "query" | "header" | "cookie"; +} & Partial; + +export const OperationParameterMetadataKey = Symbol("OperationParameter"); + +export const OperationParameterMetadataStorage = createMetadataStorage( + OperationParameterMetadataKey, + [], +); diff --git a/packages/openapi-metadata/src/metadata/operation-response.ts b/packages/openapi-metadata/src/metadata/operation-response.ts new file mode 100644 index 000000000..71b86e3d7 --- /dev/null +++ b/packages/openapi-metadata/src/metadata/operation-response.ts @@ -0,0 +1,16 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { TypeOptions } from "../types.js"; +import type { SetOptional } from "type-fest"; +import { createMetadataStorage } from "./factory.js"; + +export type OperationResponseMetadata = Omit, "content"> & { + status: number | "default"; + mediaType: string; +} & TypeOptions; + +export const OperationResponseMetadataKey = Symbol("OperationResponse"); + +export const OperationResponseMetadataStorage = createMetadataStorage>( + OperationResponseMetadataKey, + {}, +); diff --git a/packages/openapi-metadata/src/metadata/operation-security.ts b/packages/openapi-metadata/src/metadata/operation-security.ts new file mode 100644 index 000000000..45b2b53ed --- /dev/null +++ b/packages/openapi-metadata/src/metadata/operation-security.ts @@ -0,0 +1,11 @@ +import type { OpenAPIV3 } from "openapi-types"; +import { createMetadataStorage } from "./factory.js"; + +export type OperationSecurityMetadata = OpenAPIV3.SecurityRequirementObject; + +export const OperationSecurityMetadataKey = Symbol("OperationSecurity"); + +export const OperationSecurityMetadataStorage = createMetadataStorage( + OperationSecurityMetadataKey, + {}, +); diff --git a/packages/openapi-metadata/src/metadata/operation.ts b/packages/openapi-metadata/src/metadata/operation.ts new file mode 100644 index 000000000..3965524b4 --- /dev/null +++ b/packages/openapi-metadata/src/metadata/operation.ts @@ -0,0 +1,12 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { HttpMethods } from "../types.js"; +import { createMetadataStorage } from "./factory.js"; + +export type OperationMetadata = Omit & { + path?: string; + methods?: HttpMethods[]; +}; + +export const OperationMetadataKey = Symbol("Operation"); + +export const OperationMetadataStorage = createMetadataStorage(OperationMetadataKey, {}); diff --git a/packages/openapi-metadata/src/metadata/property.ts b/packages/openapi-metadata/src/metadata/property.ts new file mode 100644 index 000000000..83d6d7721 --- /dev/null +++ b/packages/openapi-metadata/src/metadata/property.ts @@ -0,0 +1,11 @@ +import type { TypeOptions } from "../types.js"; +import { createMetadataStorage } from "./factory.js"; + +export type PropertyMetadata = { + name: string; + required: boolean; +} & TypeOptions; + +export const PropertyMetadataKey = Symbol("Property"); + +export const PropertyMetadataStorage = createMetadataStorage>(PropertyMetadataKey); diff --git a/packages/openapi-metadata/src/types.ts b/packages/openapi-metadata/src/types.ts new file mode 100644 index 000000000..d5ddd0c02 --- /dev/null +++ b/packages/openapi-metadata/src/types.ts @@ -0,0 +1,26 @@ +import type { OpenAPIV3 } from "openapi-types"; +import type { Context } from "./context.js"; + +export type HttpMethods = `${OpenAPIV3.HttpMethods}`; + +export type PrimitiveType = OpenAPIV3.NonArraySchemaObjectType; + +export type TypeValue = Function | PrimitiveType; +export type Thunk = (context: Context) => T; +export type EnumTypeValue = string[] | number[] | Record; + +export type Logger = { + warn: (typeof console)["warn"]; +}; + +export type TypeOptions = { + type?: Thunk | TypeValue; + schema?: OpenAPIV3.SchemaObject; + enum?: EnumTypeValue; +}; + +export type TypeLoaderFn = ( + context: Context, + value: TypeValue, + original?: Thunk | TypeValue, +) => Promise; diff --git a/packages/openapi-metadata/src/ui/index.ts b/packages/openapi-metadata/src/ui/index.ts new file mode 100644 index 000000000..d8c0f868d --- /dev/null +++ b/packages/openapi-metadata/src/ui/index.ts @@ -0,0 +1,3 @@ +export { generateScalarUI } from "./scalar.js"; +export { generateSwaggerUI } from "./swagger.js"; +export { generateRapidocUI } from "./rapidoc.js"; diff --git a/packages/openapi-metadata/src/ui/rapidoc.ts b/packages/openapi-metadata/src/ui/rapidoc.ts new file mode 100644 index 000000000..442267e6f --- /dev/null +++ b/packages/openapi-metadata/src/ui/rapidoc.ts @@ -0,0 +1,39 @@ +export function generateRapidocUI(url: string) { + return ` + + + + + + Documentation + + + + + + + `; +} diff --git a/packages/openapi-metadata/src/ui/scalar.ts b/packages/openapi-metadata/src/ui/scalar.ts new file mode 100644 index 000000000..240fdad14 --- /dev/null +++ b/packages/openapi-metadata/src/ui/scalar.ts @@ -0,0 +1,21 @@ +export function generateScalarUI(url: string) { + return ` + + + + API Reference + + + + + + + + + `; +} diff --git a/packages/openapi-metadata/src/ui/swagger.ts b/packages/openapi-metadata/src/ui/swagger.ts new file mode 100644 index 000000000..159c20954 --- /dev/null +++ b/packages/openapi-metadata/src/ui/swagger.ts @@ -0,0 +1,40 @@ +export type GenerateSwaggerUIOptions = { + persistAuthorization?: boolean; +}; + +export function generateSwaggerUI(url: string, options?: GenerateSwaggerUIOptions) { + const swaggerOptions = { + url, + ...options, + }; + + return ` + + + + + + + + + Documentation + + +
+ + + `; +} diff --git a/packages/openapi-metadata/src/utils/enum.ts b/packages/openapi-metadata/src/utils/enum.ts new file mode 100644 index 000000000..079ccb8cd --- /dev/null +++ b/packages/openapi-metadata/src/utils/enum.ts @@ -0,0 +1,31 @@ +import type { EnumTypeValue } from "../types.js"; + +export function getEnumType(values: (string | number)[]): "string" | "number" { + return values.some((v) => typeof v === "string") ? "string" : "number"; +} + +export function getEnumValues(enumType: EnumTypeValue) { + if (Array.isArray(enumType)) { + return enumType; + } + + // Enums with numeric values + // enum Size { + // SMALL = 1, + // BIG = 2 + // } + // are transpiled to include a reverse mapping + // const Size = { + // "1": "SMALL", + // "2": "BIG", + // "SMALL": 1, + // "BIG": 2, + // } + const numericValues = Object.values(enumType) + .filter((value) => typeof value === "number") + .map((value: any) => value.toString()); + + return Object.keys(enumType) + .filter((key) => !numericValues.includes(key)) + .map((key) => enumType[key as any]) as (string | number)[]; +} diff --git a/packages/openapi-metadata/src/utils/metadata.ts b/packages/openapi-metadata/src/utils/metadata.ts new file mode 100644 index 000000000..395e2a066 --- /dev/null +++ b/packages/openapi-metadata/src/utils/metadata.ts @@ -0,0 +1,48 @@ +import type { Context } from "../context.js"; +import type { TypeValue } from "../types.js"; +import { NoExplicitTypeError } from "../errors/no-explicit-type.js"; +import { ReflectMetadataMissingError } from "../errors/reflect-metadata-missing.js"; + +export function ensureReflectMetadataExists() { + if (typeof Reflect !== "object" || typeof Reflect.getMetadata !== "function") { + throw new ReflectMetadataMissingError(); + } +} + +export type MetadataKey = "design:type" | "design:returntype" | "design:paramtypes"; + +export type FindTypeOptions = { + context: Context; + metadataKey: MetadataKey; + prototype: Object; + propertyKey: string; +}; + +export function findType({ metadataKey, prototype, propertyKey }: FindTypeOptions) { + ensureReflectMetadataExists(); + const reflectedType: Function | undefined = Reflect.getMetadata(metadataKey, prototype, propertyKey); + + if (!reflectedType) { + throw new NoExplicitTypeError(prototype.constructor.name, propertyKey); + } + + return reflectedType; +} + +const IS_THUNK_REG = /.+=>[\w\d\s\t\n\r]*/; + +export function isThunk(value: any): boolean { + if (typeof value !== "function") { + return false; + } + + return Boolean(IS_THUNK_REG.exec(value)); +} + +export function typeToString(value: TypeValue) { + if (typeof value === "function") { + return value.name; + } + + return value.toString(); +} diff --git a/packages/openapi-metadata/src/utils/schema.ts b/packages/openapi-metadata/src/utils/schema.ts new file mode 100644 index 000000000..ef1dfb0b3 --- /dev/null +++ b/packages/openapi-metadata/src/utils/schema.ts @@ -0,0 +1,4 @@ +export function schemaPath(model: string | Function): string { + const modelName = typeof model === "string" ? model : model.name; + return `#/components/schemas/${modelName}`; +} diff --git a/packages/openapi-metadata/test/decorators.test.ts b/packages/openapi-metadata/test/decorators.test.ts new file mode 100644 index 000000000..a04772f7d --- /dev/null +++ b/packages/openapi-metadata/test/decorators.test.ts @@ -0,0 +1,191 @@ +import "reflect-metadata"; +import { + ApiBody, + ApiCookie, + ApiExcludeController, + ApiExcludeOperation, + ApiExtraModels, + ApiHeader, + ApiOperation, + ApiParam, + ApiQuery, + ApiResponse, + ApiSecurity, + ApiTags, +} from "../src/decorators"; +import { + ExcludeMetadataStorage, + ExtraModelsMetadataStorage, + OperationBodyMetadataStorage, + OperationMetadataStorage, + OperationParameterMetadataStorage, + OperationResponseMetadataStorage, + OperationSecurityMetadataStorage, +} from "../src/metadata"; +import { ApiBasicAuth, ApiBearerAuth, ApiCookieAuth, ApiOauth2 } from "../src/decorators/api-security"; + +test("@ApiOperation", () => { + class MyController { + @ApiOperation({ summary: "Hello", path: "/test", methods: ["get"] }) + operation() {} + } + + const metadata = OperationMetadataStorage.getMetadata(MyController.prototype, "operation"); + + expect(metadata).toEqual({ + summary: "Hello", + path: "/test", + methods: ["get"], + }); +}); + +test("@ApiBody", () => { + class MyController { + @ApiBody({ type: "string" }) + operation() {} + } + + const metadata = OperationBodyMetadataStorage.getMetadata(MyController.prototype, "operation"); + + expect(metadata).toEqual({ + type: "string", + mediaType: "application/json", + }); +}); + +test("@ApiParam", () => { + @ApiParam({ name: "test" }) + class MyController { + @ApiParam({ name: "hello" }) + operation() {} + } + + const metadata = OperationParameterMetadataStorage.getMetadata(MyController.prototype, "operation", true); + + expect(metadata).toEqual([ + { in: "path", name: "test" }, + { in: "path", name: "hello" }, + ]); +}); + +test("@ApiHeader", () => { + @ApiHeader({ name: "test" }) + class MyController { + @ApiHeader({ name: "hello" }) + operation() {} + } + + const metadata = OperationParameterMetadataStorage.getMetadata(MyController.prototype, "operation", true); + + expect(metadata).toEqual([ + { in: "header", name: "test" }, + { in: "header", name: "hello" }, + ]); +}); + +test("@ApiCookie", () => { + @ApiCookie({ name: "test" }) + class MyController { + @ApiCookie({ name: "hello" }) + operation() {} + } + + const metadata = OperationParameterMetadataStorage.getMetadata(MyController.prototype, "operation", true); + + expect(metadata).toEqual([ + { in: "cookie", name: "test" }, + { in: "cookie", name: "hello" }, + ]); +}); + +test("@ApiQuery", () => { + @ApiQuery({ name: "test" }) + class MyController { + @ApiQuery({ name: "hello" }) + operation() {} + } + + const metadata = OperationParameterMetadataStorage.getMetadata(MyController.prototype, "operation", true); + + expect(metadata).toEqual([ + { in: "query", name: "test" }, + { in: "query", name: "hello" }, + ]); +}); + +test("@ApiResponse", () => { + @ApiResponse({ type: "string", mediaType: "text/html" }) + class MyController { + @ApiResponse({ status: 404, type: "number" }) + operation() {} + } + + const metadata = OperationResponseMetadataStorage.getMetadata(MyController.prototype, "operation", true); + + expect(metadata).toEqual({ + default: { status: "default", mediaType: "text/html", type: "string" }, + "404": { status: 404, mediaType: "application/json", type: "number" }, + }); +}); + +test("@ApiTags", () => { + @ApiTags("Root") + class MyController { + @ApiTags("Hello", "World") + operation() {} + } + + const metadata = OperationMetadataStorage.getMetadata(MyController.prototype, "operation", true); + + expect(metadata.tags).toEqual(["Root", "Hello", "World"]); +}); + +test("@ApiSecurity", () => { + @ApiBasicAuth() + @ApiCookieAuth() + class MyController { + @ApiSecurity("custom") + @ApiBearerAuth() + @ApiOauth2("pets:write") + operation() {} + } + + const metadata = OperationSecurityMetadataStorage.getMetadata(MyController.prototype, "operation", true); + + expect(metadata).toEqual({ + custom: [], + cookie: [], + basic: [], + bearer: [], + oauth2: ["pets:write"], + }); +}); + +test("@ApiExcludeController", () => { + @ApiExcludeController() + class MyController {} + + const metadata = ExcludeMetadataStorage.getMetadata(MyController); + expect(metadata).toBe(true); +}); + +test("@ApiExcludeOperation", () => { + class MyController { + @ApiExcludeOperation() + operation() {} + } + + const metadata = ExcludeMetadataStorage.getMetadata(MyController.prototype, "operation"); + expect(metadata).toBe(true); +}); + +test("@ApiExtraModels", () => { + @ApiExtraModels("string") + class MyController { + operation() {} + } + + const metadata = ExtraModelsMetadataStorage.getMetadata(MyController); + + expect(metadata).toEqual(["string"]); +}); diff --git a/packages/openapi-metadata/test/utils/metadata.test.ts b/packages/openapi-metadata/test/utils/metadata.test.ts new file mode 100644 index 000000000..9bd238511 --- /dev/null +++ b/packages/openapi-metadata/test/utils/metadata.test.ts @@ -0,0 +1,20 @@ +import { isThunk } from "../../src/utils/metadata"; + +test("isThunk", () => { + expect(isThunk(() => true)).toBe(true); + expect(isThunk(true)).toBe(false); + expect(isThunk(() => ({ hello: "world" }))).toBe(true); + + const data = { type: () => "string" }; + expect(isThunk(data.type)).toBe(true); + + class User {} + expect(isThunk(User)).toBe(false); + expect(isThunk(() => User)).toBe(true); + + expect( + isThunk(() => { + return "TESTEST"; + }), + ).toBe(true); +}); diff --git a/packages/openapi-metadata/tsconfig.build.json b/packages/openapi-metadata/tsconfig.build.json new file mode 100644 index 000000000..b90fc83e0 --- /dev/null +++ b/packages/openapi-metadata/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"] +} diff --git a/packages/openapi-metadata/tsconfig.json b/packages/openapi-metadata/tsconfig.json new file mode 100644 index 000000000..4a1b94fdc --- /dev/null +++ b/packages/openapi-metadata/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "downlevelIteration": false, + "esModuleInterop": true, + "lib": ["ESNext", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noUncheckedIndexedAccess": true, + "outDir": "dist", + "skipLibCheck": false, + "strict": true, + "target": "ESNext", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "types": ["vitest/globals", "reflect-metadata"] + }, + "include": ["src", "test"], + "exclude": ["example", "node_modules"] +} diff --git a/packages/openapi-metadata/tsup.config.ts b/packages/openapi-metadata/tsup.config.ts new file mode 100644 index 000000000..f45c89948 --- /dev/null +++ b/packages/openapi-metadata/tsup.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts", "src/decorators/index.ts", "src/errors/index.ts", "src/metadata/index.ts", "src/ui/index.ts"], + format: ["esm"], + dts: true, + sourcemap: true, +}); diff --git a/packages/openapi-metadata/vitest.config.ts b/packages/openapi-metadata/vitest.config.ts new file mode 100644 index 000000000..8debcd30d --- /dev/null +++ b/packages/openapi-metadata/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; +import swc from "unplugin-swc"; + +export default defineConfig({ + plugins: [ + // Required to have typescript metadata working. See https://github.com/vitest-dev/vitest/discussions/3320 + swc.vite(), + ], + test: { + globals: true, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8f282ffc..3ae0fba4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -215,6 +215,49 @@ importers: specifier: ^2.1.6 version: 2.1.6(typescript@5.6.3) + packages/openapi-metadata: + dependencies: + deepmerge: + specifier: ^4.3.1 + version: 4.3.1 + openapi-types: + specifier: ^12.1.3 + version: 12.1.3 + devDependencies: + '@types/lodash': + specifier: ^4.17.7 + version: 4.17.7 + '@types/node': + specifier: ^22.1.0 + version: 22.8.0 + '@vitest/coverage-v8': + specifier: ^2.0.5 + version: 2.1.4(vitest@2.1.3(@types/node@22.8.0)(jsdom@20.0.3)(msw@2.5.1(@types/node@22.8.0)(typescript@5.6.3))) + del-cli: + specifier: ^5.1.0 + version: 5.1.0 + esbuild: + specifier: ^0.20.2 + version: 0.20.2 + execa: + specifier: ^8.0.1 + version: 8.0.1 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + tsup: + specifier: ^8.2.4 + version: 8.3.5(@swc/core@1.7.39(@swc/helpers@0.5.13))(jiti@2.3.3)(postcss@8.4.47)(typescript@5.6.3)(yaml@2.5.1) + type-fest: + specifier: ^4.26.1 + version: 4.26.1 + typescript: + specifier: ^5.4.5 + version: 5.6.3 + unplugin-swc: + specifier: ^1.5.1 + version: 1.5.1(@swc/core@1.7.39(@swc/helpers@0.5.13))(rollup@4.24.0) + packages/openapi-react-query: dependencies: openapi-typescript-helpers: @@ -531,6 +574,9 @@ packages: resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@biomejs/biome@1.9.4': resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} engines: {node: '>=14.21.3'} @@ -1377,6 +1423,14 @@ packages: peerDependencies: '@types/node': '>=18' + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -1480,6 +1534,10 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@playwright/test@1.48.1': resolution: {integrity: sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==} engines: {node: '>=18'} @@ -1498,6 +1556,15 @@ packages: resolution: {integrity: sha512-tod9bhIxBJQOeGQ3Ot5Zk707Wcg5YgEcEG766cGxwHqjRUNF5kwZnleBO7CpaLusOMWnQefj7RfFWLZS+IqnhA==} engines: {node: '>=14.19.0', npm: '>=7.0.0'} + '@rollup/pluginutils@5.1.3': + resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.24.0': resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} cpu: [arm] @@ -1872,6 +1939,15 @@ packages: vite: ^5.0.0 vue: ^3.2.25 + '@vitest/coverage-v8@2.1.4': + resolution: {integrity: sha512-FPKQuJfR6VTfcNMcGpqInmtJuVXFSCd9HQltYncfR01AzXhLucMEtQ5SinPdZxsT5x/5BK7I5qFJ5/ApGCmyTQ==} + peerDependencies: + '@vitest/browser': 2.1.4 + vitest: 2.1.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@2.0.5': resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} @@ -2050,6 +2126,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -2175,6 +2256,12 @@ packages: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} + bundle-require@5.0.0: + resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -2332,6 +2419,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} @@ -2341,6 +2432,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2484,6 +2579,9 @@ packages: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.45: resolution: {integrity: sha512-vOzZS6uZwhhbkZbcRyiy99Wg+pYFV5hk+5YaECvx0+Z31NR3Tt5zS6dze2OepT6PCTzVzT0dIJItti+uAW5zmw==} @@ -2493,6 +2591,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + emojilib@2.4.0: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} @@ -2642,6 +2743,10 @@ packages: debug: optional: true + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + form-data@4.0.1: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} @@ -2701,6 +2806,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -2792,6 +2901,9 @@ packages: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -2930,10 +3042,33 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@2.3.3: resolution: {integrity: sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==} hasBin: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-levenshtein@1.1.6: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'} @@ -3009,6 +3144,10 @@ packages: resolution: {integrity: sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==} engines: {node: '>=18.0.0'} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -3023,6 +3162,9 @@ packages: lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -3060,6 +3202,13 @@ packages: magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -3165,6 +3314,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + minisearch@7.1.0: resolution: {integrity: sha512-tv7c/uefWdEhcu6hvrfTihflgeEi2tN6VV7HJnCjK6VxM75QQJh4t9FwJCsA2EsRS8LCnu3W87CuGPWMocOLCA==} @@ -3296,6 +3449,9 @@ packages: oniguruma-to-js@0.4.3: resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + openapi-typescript-codegen@0.25.0: resolution: {integrity: sha512-nN/TnIcGbP58qYgwEEy5FrAAjePcYgfMaCe3tsmYyTgI3v4RR9v8os14L+LEWDvV50+CmqiyTzRkKKtJeb6Ybg==} hasBin: true @@ -3346,6 +3502,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@0.2.2: resolution: {integrity: sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==} @@ -3388,6 +3547,10 @@ packages: resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} engines: {node: '>=12'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -3428,6 +3591,10 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + playwright-core@1.48.1: resolution: {integrity: sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==} engines: {node: '>=18'} @@ -3442,6 +3609,24 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -3542,6 +3727,9 @@ packages: resolution: {integrity: sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag==} engines: {node: '>=12'} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -3709,6 +3897,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -3759,6 +3951,10 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string-width@7.2.0: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} @@ -3803,6 +3999,11 @@ packages: babel-plugin-macros: optional: true + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + superagent@10.1.1: resolution: {integrity: sha512-9pIwrHrOj3uAnqg9gDlW7EA2xv+N5au/dSM0kM22HTqmUu8jBxNT+8uA7tA3UoCnmiqzpSbu8rasIUZvbyamMQ==} engines: {node: '>=14.18.0'} @@ -3891,6 +4092,10 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -3942,10 +4147,17 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tr46@3.0.0: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -3953,12 +4165,34 @@ packages: resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==} engines: {node: '>=12'} + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-results-es@4.2.0: resolution: {integrity: sha512-GfpRk+qvHxa/6gADH8WMN/jXvs5oHYbKtMQc6X9L3VhToy5Lri3iQowyYSytaRcvPDiTT2z3vurzQZXFQFXKRA==} tslib@2.8.0: resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==} + tsup@8.3.5: + resolution: {integrity: sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -4030,6 +4264,20 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unplugin-swc@1.5.1: + resolution: {integrity: sha512-/ZLrPNjChhGx3Z95pxJ4tQgfI6rWqukgYHKflrNB4zAV1izOQuDhkTn55JWeivpBxDCoK7M/TStb2aS/14PS/g==} + peerDependencies: + '@swc/core': ^1.2.108 + + unplugin@1.15.0: + resolution: {integrity: sha512-jTPIs63W+DUEDW207ztbaoO7cQ4p5aVaB823LSlxpsFEU3Mykwxf3ZGC/wzxFJeZlASZYgVrWeo7LgOrqJZ8RA==} + engines: {node: '>=14.0.0'} + peerDependencies: + webpack-sources: ^3 + peerDependenciesMeta: + webpack-sources: + optional: true + update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true @@ -4206,10 +4454,16 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} @@ -4225,6 +4479,9 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true @@ -4250,6 +4507,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrap-ansi@9.0.0: resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} engines: {node: '>=18'} @@ -4578,6 +4839,8 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@bcoe/v8-coverage@0.2.3': {} + '@biomejs/biome@1.9.4': optionalDependencies: '@biomejs/cli-darwin-arm64': 1.9.4 @@ -5206,6 +5469,17 @@ snapshots: dependencies: '@types/node': 22.8.0 + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -5297,6 +5571,9 @@ snapshots: '@open-draft/until@2.1.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@playwright/test@1.48.1': dependencies: playwright: 1.48.1 @@ -5329,6 +5606,14 @@ snapshots: - encoding - supports-color + '@rollup/pluginutils@5.1.3(rollup@4.24.0)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.24.0 + '@rollup/rollup-android-arm-eabi@4.24.0': optional: true @@ -5684,6 +5969,24 @@ snapshots: vite: 5.4.10(@types/node@22.8.0) vue: 3.5.12(typescript@5.6.3) + '@vitest/coverage-v8@2.1.4(vitest@2.1.3(@types/node@22.8.0)(jsdom@20.0.3)(msw@2.5.1(@types/node@22.8.0)(typescript@5.6.3)))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.7(supports-color@9.4.0) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.12 + magicast: 0.3.5 + std-env: 3.7.0 + test-exclude: 7.0.1 + tinyrainbow: 1.2.0 + vitest: 2.1.3(@types/node@22.8.0)(jsdom@20.0.3)(msw@2.5.1(@types/node@22.8.0)(typescript@5.6.3)) + transitivePeerDependencies: + - supports-color + '@vitest/expect@2.0.5': dependencies: '@vitest/spy': 2.0.5 @@ -5902,17 +6205,19 @@ snapshots: acorn-globals@7.0.1: dependencies: - acorn: 8.13.0 + acorn: 8.14.0 acorn-walk: 8.3.4 optional: true acorn-walk@8.3.4: dependencies: - acorn: 8.13.0 + acorn: 8.14.0 optional: true acorn@8.13.0: {} + acorn@8.14.0: {} + agent-base@6.0.2: dependencies: debug: 4.3.7(supports-color@9.4.0) @@ -6042,6 +6347,11 @@ snapshots: buffer-crc32@1.0.0: {} + bundle-require@5.0.0(esbuild@0.24.0): + dependencies: + esbuild: 0.24.0 + load-tsconfig: 0.2.5 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -6206,12 +6516,16 @@ snapshots: commander@12.1.0: {} + commander@4.1.1: {} + component-emitter@1.3.1: {} computeds@0.0.1: {} concat-map@0.0.1: {} + consola@3.2.3: {} + convert-source-map@2.0.0: {} cookie@0.5.0: {} @@ -6344,12 +6658,16 @@ snapshots: dotenv@8.6.0: {} + eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.45: {} emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + emojilib@2.4.0: {} enquirer@2.4.1: @@ -6578,6 +6896,11 @@ snapshots: follow-redirects@1.15.9: {} + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + form-data@4.0.1: dependencies: asynckit: 0.4.0 @@ -6638,6 +6961,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -6740,6 +7072,8 @@ snapshots: whatwg-encoding: 2.0.0 optional: true + html-escaper@2.0.2: {} + html-void-elements@3.0.0: {} http-proxy-agent@5.0.0: @@ -6854,8 +7188,37 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.3.7(supports-color@9.4.0) + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jiti@2.3.3: {} + joycon@3.1.1: {} + js-levenshtein@1.1.6: {} js-tokens@4.0.0: {} @@ -6872,7 +7235,7 @@ snapshots: jsdom@20.0.3: dependencies: abab: 2.0.6 - acorn: 8.13.0 + acorn: 8.14.0 acorn-globals: 7.0.1 cssom: 0.5.0 cssstyle: 2.3.0 @@ -6957,6 +7320,8 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.0 + load-tsconfig@0.2.5: {} + locate-character@3.0.0: {} locate-path@5.0.0: @@ -6969,6 +7334,8 @@ snapshots: lodash.isequal@4.5.0: {} + lodash.sortby@4.7.0: {} + lodash.startcase@4.4.0: {} lodash@4.17.21: {} @@ -7008,6 +7375,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.26.0 + '@babel/types': 7.26.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.6.3 + map-obj@1.0.1: {} map-obj@4.3.0: {} @@ -7117,6 +7494,8 @@ snapshots: minimist@1.2.8: {} + minipass@7.1.2: {} + minisearch@7.1.0: {} mitt@3.0.1: {} @@ -7250,6 +7629,8 @@ snapshots: dependencies: regex: 4.3.3 + openapi-types@12.1.3: {} + openapi-typescript-codegen@0.25.0: dependencies: camelcase: 6.3.0 @@ -7294,6 +7675,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + package-manager-detector@0.2.2: {} parse-json@5.2.0: @@ -7332,6 +7715,11 @@ snapshots: path-key@4.0.0: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + path-to-regexp@6.3.0: {} path-type@4.0.0: {} @@ -7358,6 +7746,8 @@ snapshots: pify@4.0.1: {} + pirates@4.0.6: {} + playwright-core@1.48.1: {} playwright@1.48.1: @@ -7368,6 +7758,14 @@ snapshots: pluralize@8.0.0: {} + postcss-load-config@6.0.1(jiti@2.3.3)(postcss@8.4.47)(yaml@2.5.1): + dependencies: + lilconfig: 3.1.2 + optionalDependencies: + jiti: 2.3.3 + postcss: 8.4.47 + yaml: 2.5.1 + postcss@8.4.31: dependencies: nanoid: 3.3.7 @@ -7462,6 +7860,8 @@ snapshots: indent-string: 5.0.0 strip-indent: 4.0.0 + reflect-metadata@0.2.2: {} + regenerator-runtime@0.14.1: {} regex@4.3.3: {} @@ -7667,6 +8067,10 @@ snapshots: source-map@0.6.1: {} + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + space-separated-tokens@2.0.2: {} spawndamnit@2.0.0: @@ -7710,6 +8114,12 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + string-width@7.2.0: dependencies: emoji-regex: 10.4.0 @@ -7746,6 +8156,16 @@ snapshots: client-only: 0.0.1 react: 18.3.1 + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + superagent@10.1.1: dependencies: component-emitter: 1.3.1 @@ -7842,6 +8262,12 @@ snapshots: term-size@2.2.1: {} + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 9.0.5 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -7889,19 +8315,55 @@ snapshots: tr46@0.0.3: {} + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + tr46@3.0.0: dependencies: punycode: 2.3.1 optional: true + tree-kill@1.2.2: {} + trim-lines@3.0.1: {} trim-newlines@4.1.1: {} + ts-interface-checker@0.1.13: {} + ts-results-es@4.2.0: {} tslib@2.8.0: {} + tsup@8.3.5(@swc/core@1.7.39(@swc/helpers@0.5.13))(jiti@2.3.3)(postcss@8.4.47)(typescript@5.6.3)(yaml@2.5.1): + dependencies: + bundle-require: 5.0.0(esbuild@0.24.0) + cac: 6.7.14 + chokidar: 4.0.1 + consola: 3.2.3 + debug: 4.3.7(supports-color@9.4.0) + esbuild: 0.24.0 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.3.3)(postcss@8.4.47)(yaml@2.5.1) + resolve-from: 5.0.0 + rollup: 4.24.0 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.1 + tinyglobby: 0.2.10 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.7.39(@swc/helpers@0.5.13) + postcss: 8.4.47 + typescript: 5.6.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + type-fest@0.21.3: {} type-fest@1.4.0: {} @@ -7952,6 +8414,21 @@ snapshots: universalify@2.0.1: {} + unplugin-swc@1.5.1(@swc/core@1.7.39(@swc/helpers@0.5.13))(rollup@4.24.0): + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@4.24.0) + '@swc/core': 1.7.39(@swc/helpers@0.5.13) + load-tsconfig: 0.2.5 + unplugin: 1.15.0 + transitivePeerDependencies: + - rollup + - webpack-sources + + unplugin@1.15.0: + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: browserslist: 4.24.2 @@ -8181,9 +8658,13 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} + webidl-conversions@7.0.0: optional: true + webpack-virtual-modules@0.6.2: {} + whatwg-encoding@2.0.0: dependencies: iconv-lite: 0.6.3 @@ -8203,6 +8684,12 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + which@1.3.1: dependencies: isexe: 2.0.0 @@ -8230,6 +8717,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + wrap-ansi@9.0.0: dependencies: ansi-styles: 6.2.1