Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c5d6577
WIP refactorings
sea-grass Aug 18, 2025
01a7a41
refactor(opapi): Explicit exports in library file
sea-grass Aug 11, 2025
1f6e573
chore(opapi): Upgrade to latest vitest
sea-grass Aug 11, 2025
88bc418
chore(opapi): Replace prettier with biome
sea-grass Aug 11, 2025
c1fdd54
chore(opapi): Run biome format
sea-grass Aug 11, 2025
3bd8c6d
chore(opapi): Modify the included files for format checking
sea-grass Aug 11, 2025
dff60e6
chore(opapi): Disable linter and assist
sea-grass Aug 11, 2025
1ab9e0e
chore(opapi): Fix biome file includes
sea-grass Aug 11, 2025
49c4821
core(opapi): Add test for handler-generator
sea-grass Aug 11, 2025
ddb8f66
Make exportHandler test fail
sea-grass Aug 18, 2025
9b1f5b8
Skip some tests that make network calls
sea-grass Sep 12, 2025
64767df
Fix build warnings
sea-grass Sep 12, 2025
824be5d
Restore export handler function
sea-grass Sep 12, 2025
f69b58b
Bump @anatine/zod-openapi and openapi3-ts
sea-grass Sep 12, 2025
084789f
Rename state.test.ts to opapi.test.ts
sea-grass Sep 12, 2025
ee97c2c
Add empty state tests
sea-grass Sep 12, 2025
bd59eca
Fix export of OpenApiZodAny
sea-grass Sep 12, 2025
91bdc87
Fix VError import
sea-grass Sep 12, 2025
c7041dc
Add simple opapi api
sea-grass Sep 12, 2025
ac5572c
Simplify createState
sea-grass Sep 12, 2025
10924ad
skip flaky test
sea-grass Sep 12, 2025
cf9f107
Fix typing in generateSchemaFromZod
sea-grass Sep 12, 2025
574f9cf
refactor openapi
sea-grass Sep 13, 2025
61fa5c8
Simplify jsonschema
sea-grass Sep 13, 2025
8c94817
Fix schema property mapping
sea-grass Sep 13, 2025
1e55477
refactor handler-generator
sea-grass Sep 16, 2025
3a822f7
fix:format
sea-grass Sep 16, 2025
d495134
Fix readme typo
sea-grass Sep 16, 2025
87fd87e
Rename exportClient
sea-grass Sep 16, 2025
46ffb2b
Add NewOpapi export
sea-grass Sep 16, 2025
619eb0b
Fix build error
sea-grass Sep 16, 2025
f9aee84
Fix import
sea-grass Sep 16, 2025
040ef90
update
sea-grass Sep 16, 2025
eda78c5
WIP something about creating opapi from state or from spec
sea-grass May 22, 2026
648c057
Merge master
sea-grass May 22, 2026
23a3ebe
tests pass
sea-grass May 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .tool-versions
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
5 changes: 5 additions & 0 deletions entities/pkg/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 0 additions & 22 deletions opapi/.prettierignore

This file was deleted.

8 changes: 0 additions & 8 deletions opapi/.prettierrc

This file was deleted.

3 changes: 3 additions & 0 deletions opapi/__mocks__/fs/promises.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { fs } from 'memfs'

export default fs.promises
43 changes: 43 additions & 0 deletions opapi/biome.json
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 }
}
203 changes: 203 additions & 0 deletions opapi/foo.js
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
}
Comment on lines +1 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Accidental WIP file with invalid syntax

foo.js uses TypeScript syntax (type annotations, generics, class access modifiers like private, public, readonly) inside a plain .js file — this is not valid JavaScript and will fail at runtime if executed without a transpiler. Additionally, it imports from ./errors and ./typings, relative paths that don't exist in the opapi package root. The content appears to be a scratch copy of the generated export-handler.ts handler code. This file should be removed before merging.

| {
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),
}
}
}

25 changes: 14 additions & 11 deletions opapi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,21 @@
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
"import": "./dist/index.mjs"
}
},
"scripts": {
"test": "vitest run",
"build": "tsup src/index.ts --dts --format cjs,esm --clean",
"build": "tsup src/index.ts --dts --format cjs,esm --clean --sourcemap",
"check:type": "tsc --noEmit",
"check:format": "prettier --check .",
"fix:format": "prettier --write .",
"check:format": "biome format .",
"fix:format": "biome format --write .",
"check": "pnpm run check:type && pnpm run check:format"
},
"devDependencies": {
"@biomejs/biome": "2.1.4",
"@swc/core": "1.9.3",
"@swc/helpers": "0.5.15",
"@types/decompress": "4.2.7",
Expand All @@ -32,17 +33,18 @@
"@types/lodash": "^4.17.0",
"@types/node": "^22.16.4",
"@types/qs": "^6.9.15",
"@types/ramda": "^0.31.1",
"@types/verror": "1.10.10",
"prettier": "3.4.1",
"memfs": "^4.36.0",
"ts-node": "10.9.2",
"tsup": "8.3.5",
"typescript": "5.7.2",
"vite": "5.4.11",
"vite-node": "1.6.0",
"vitest": "1.6.0"
"vitest": "3.2.4"
},
"dependencies": {
"@anatine/zod-openapi": "1.12.1",
"@anatine/zod-openapi": "2.2.8",
"@readme/openapi-parser": "2.6.0",
"axios": "1.7.8",
"chalk": "4.1.2",
Expand All @@ -52,8 +54,9 @@
"json-schema-to-zod": "1.1.1",
"lodash": "^4.17.21",
"openapi-typescript": "6.7.6",
"openapi3-ts": "2.0.2",
"openapi3-ts": "4.5.0",
"radash": "12.1.0",
"ramda": "^0.31.3",
"tsconfig-paths": "4.2.0",
"verror": "1.10.1",
"winston": "3.17.0",
Expand All @@ -63,7 +66,7 @@
"license": "MIT",
"engines": {
"node": ">=16.0.0",
"pnpm": "8.6.2"
"pnpm": "8.15.9"
},
"packageManager": "pnpm@8.6.2"
"packageManager": "pnpm@8.15.9"
}
Loading
Loading