-
Notifications
You must be signed in to change notification settings - Fork 15
WIP refactorings #686
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
sea-grass
wants to merge
36
commits into
master
Choose a base branch
from
cg-opapi-updates
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
WIP refactorings #686
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
c5d6577
WIP refactorings
sea-grass 01a7a41
refactor(opapi): Explicit exports in library file
sea-grass 1f6e573
chore(opapi): Upgrade to latest vitest
sea-grass 88bc418
chore(opapi): Replace prettier with biome
sea-grass c1fdd54
chore(opapi): Run biome format
sea-grass 3bd8c6d
chore(opapi): Modify the included files for format checking
sea-grass dff60e6
chore(opapi): Disable linter and assist
sea-grass 1ab9e0e
chore(opapi): Fix biome file includes
sea-grass 49c4821
core(opapi): Add test for handler-generator
sea-grass ddb8f66
Make exportHandler test fail
sea-grass 9b1f5b8
Skip some tests that make network calls
sea-grass 64767df
Fix build warnings
sea-grass 824be5d
Restore export handler function
sea-grass f69b58b
Bump @anatine/zod-openapi and openapi3-ts
sea-grass 084789f
Rename state.test.ts to opapi.test.ts
sea-grass ee97c2c
Add empty state tests
sea-grass bd59eca
Fix export of OpenApiZodAny
sea-grass 91bdc87
Fix VError import
sea-grass c7041dc
Add simple opapi api
sea-grass ac5572c
Simplify createState
sea-grass 10924ad
skip flaky test
sea-grass cf9f107
Fix typing in generateSchemaFromZod
sea-grass 574f9cf
refactor openapi
sea-grass 61fa5c8
Simplify jsonschema
sea-grass 8c94817
Fix schema property mapping
sea-grass 1e55477
refactor handler-generator
sea-grass 3a822f7
fix:format
sea-grass d495134
Fix readme typo
sea-grass 87fd87e
Rename exportClient
sea-grass 46ffb2b
Add NewOpapi export
sea-grass 619eb0b
Fix build error
sea-grass f9aee84
Fix import
sea-grass 040ef90
update
sea-grass eda78c5
WIP something about creating opapi from state or from spec
sea-grass 648c057
Merge master
sea-grass 23a3ebe
tests pass
sea-grass File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| pnpm 8.6.2 | ||
| pnpm 8.15.9 | ||
| nodejs 22.17.0 |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { fs } from 'memfs' | ||
|
|
||
| export default fs.promises |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| { | ||
| "$schema": "https://biomejs.dev/schemas/2.1.4/schema.json", | ||
| "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, | ||
| "files": { "ignoreUnknown": false }, | ||
| "formatter": { | ||
| "enabled": true, | ||
| "formatWithErrors": false, | ||
| "indentStyle": "space", | ||
| "indentWidth": 2, | ||
| "lineEnding": "lf", | ||
| "lineWidth": 120, | ||
| "bracketSameLine": false, | ||
| "bracketSpacing": true, | ||
| "expand": "auto", | ||
| "useEditorconfig": true, | ||
| "includes": [ | ||
| "**/*.ts", | ||
| "**/*.tsx", | ||
| "**/*.json", | ||
| "**/*.yaml", | ||
| "**/*.yml", | ||
| "**/*.md", | ||
| "!**/pnpm-lock.yaml", | ||
| "!**/gen/**/*", | ||
| "!**/dist/**/*", | ||
| "!**/.botpress/**/*" | ||
| ] | ||
| }, | ||
| "linter": { "enabled": false }, | ||
| "javascript": { | ||
| "formatter": { | ||
| "quoteProperties": "asNeeded", | ||
| "trailingCommas": "all", | ||
| "semicolons": "asNeeded", | ||
| "arrowParentheses": "always", | ||
| "bracketSameLine": false, | ||
| "quoteStyle": "single", | ||
| "bracketSpacing": true | ||
| } | ||
| }, | ||
| "html": { "formatter": { "selfCloseVoidElements": "always" } }, | ||
| "assist": { "enabled": false } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| import qs from 'qs' | ||
| import { isAxiosError } from 'axios' | ||
| import { isApiError } from './errors' | ||
| import * as types from './typings' | ||
|
|
||
| type RoutePart = | ||
| | { | ||
| type: 'argument' | ||
| name: string | ||
| } | ||
| | { | ||
| type: 'namespace' | ||
| name: string | ||
| } | ||
|
|
||
| const tokenize = (path: string) => path.split('/').filter((x) => !!x) | ||
|
|
||
| class Route { | ||
| private _parts: RoutePart[] | ||
|
|
||
| public constructor(public readonly path: string) { | ||
| this._parts = this._parse(path) | ||
| } | ||
|
|
||
| public match(path: string): Record<string, string> | null { | ||
| const tokens = tokenize(path) | ||
|
|
||
| if (tokens.length !== this._parts.length) { | ||
| return null | ||
| } | ||
|
|
||
| const args: Record<string, string> = {} | ||
| const zipped = tokens.map((token: string, index: number) => [token, this._parts[index]!] as const) | ||
| for (const [token, part] of zipped) { | ||
| if (part.type === 'argument') { | ||
| args[part.name] = token | ||
| continue | ||
| } | ||
|
|
||
| if (part.name !== token) { | ||
| return null | ||
| } | ||
| } | ||
|
|
||
| return args | ||
| } | ||
|
|
||
| private _parse(path: string): RoutePart[] { | ||
| const tokens = tokenize(path) | ||
| const parts: RoutePart[] = [] | ||
|
|
||
| for (const token of tokens) { | ||
| const match = /{(.+?)}/.exec(token) | ||
| if (match) { | ||
| parts.push({ | ||
| type: 'argument', | ||
| name: match[1]!, | ||
| }) | ||
| continue | ||
| } | ||
|
|
||
| parts.push({ | ||
| type: 'namespace', | ||
| name: token, | ||
| }) | ||
| } | ||
|
|
||
| return parts | ||
| } | ||
| } | ||
|
|
||
| type RouteMatch = { path: string; params: Record<string, string> } | ||
| export class Router { | ||
| private _parsed: Route[] = [] | ||
|
|
||
| public constructor(routes: string[]) { | ||
| this._parsed = routes.map((route) => new Route(route)) | ||
| } | ||
|
|
||
| public match(path: string): RouteMatch | null { | ||
| const matches: RouteMatch[] = [] | ||
| for (const route of this._parsed) { | ||
| const params = route.match(path) | ||
| if (params !== null) { | ||
| matches.push({ | ||
| path: route.path, | ||
| params, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| if (matches.length === 0) { | ||
| return null | ||
| } | ||
|
|
||
| // find the match with the least amount of params (i.e. the most specific match) | ||
| matches.sort((a, b) => Object.keys(a.params).length - Object.keys(b.params).length) | ||
| return matches[0]! | ||
| } | ||
| } | ||
|
|
||
| const getErrorBody = (thrown: unknown) => { | ||
| if (isAxiosError(thrown)) { | ||
| const data = thrown.response?.data | ||
| const statusCode = thrown.response?.status | ||
|
|
||
| if (!data) { | ||
| return `${thrown.message} (no response data) (Status Code: ${statusCode})` | ||
| } | ||
|
|
||
| return `${data.message || data.error?.message || data.error || data.body || thrown.message} (Status Code: ${statusCode})` | ||
| } else if (thrown instanceof Error) { | ||
| return thrown.message | ||
| } | ||
| try { | ||
| return JSON.stringify(thrown) | ||
| } catch { | ||
| return 'Unknown error' | ||
| } | ||
| } | ||
|
|
||
| type PlainRequest = { | ||
| body?: string; | ||
| path: string; | ||
| query: string; | ||
| method: string; | ||
| headers: { | ||
| [key: string]: string | undefined; | ||
| }; | ||
| } | ||
|
|
||
| type PlainResponse = { | ||
| body?: string | ||
| headers?: { | ||
| [key: string]: string | ||
| } | ||
| status?: number | ||
| } | ||
|
|
||
| export const handleRequest = async <T extends { req: PlainRequest }>(routes: Record<string, Record<string, types.Route<T>>>, props: T): Promise<PlainResponse> => { | ||
| try { | ||
| const router = new Router(Object.keys(routes)) | ||
| const match = router.match(props.req.path) | ||
| if (!match) { | ||
| return { | ||
| status: 404, | ||
| body: JSON.stringify({ message: "Route doesn't exist" }), | ||
| } | ||
| } | ||
|
|
||
| const route = routes[match.path]! | ||
| const method = props.req.method.toLowerCase() | ||
| const leaf = route[method] | ||
| if (!leaf) { | ||
| return { | ||
| status: 404, | ||
| body: JSON.stringify({ message: "Method doesn't exist" }), | ||
| } | ||
| } | ||
|
|
||
| let body: any | ||
| if (props.req.body) { | ||
| try { | ||
| body = JSON.parse(props.req.body) | ||
| } catch { | ||
| return { | ||
| status: 400, | ||
| body: JSON.stringify({ message: 'Invalid JSON body' }), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const res = await leaf( | ||
| props, | ||
| { | ||
| path: match.path, | ||
| method, | ||
| body, | ||
| params: match.params, | ||
| headers: props.req.headers, | ||
| query: qs.parse(props.req.query) as Record<string, string | undefined>, | ||
| } | ||
| ) | ||
|
|
||
| return { | ||
| body: typeof res.body === 'string' ? res.body : JSON.stringify(res.body), | ||
| status: res.status ?? 200, | ||
| headers: res.headers, | ||
| } | ||
| } catch (thrown) { | ||
| if (isApiError(thrown)) { | ||
| return { | ||
| status: thrown.code, | ||
| body: JSON.stringify(thrown.toJSON()), | ||
| } | ||
| } | ||
| return { | ||
| status: 500, | ||
| body: getErrorBody(thrown), | ||
| } | ||
| } | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
foo.jsuses TypeScript syntax (type annotations, generics, class access modifiers likeprivate,public,readonly) inside a plain.jsfile — this is not valid JavaScript and will fail at runtime if executed without a transpiler. Additionally, it imports from./errorsand./typings, relative paths that don't exist in theopapipackage root. The content appears to be a scratch copy of the generatedexport-handler.tshandler code. This file should be removed before merging.