Skip to content

Commit c4e2e0c

Browse files
committed
chore(release): bump version
- @yme/[email protected]
0 parents  commit c4e2e0c

21 files changed

+4064
-0
lines changed

.editorconfig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# http://editorconfig.org
2+
root = true
3+
4+
[*]
5+
end_of_line = lf
6+
charset = utf-8
7+
insert_final_newline = true
8+
trim_trailing_whitespace = true
9+
indent_size = 2
10+
indent_style = space

.gitignore

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
lerna-debug.log*
8+
.pnpm-debug.log*
9+
10+
# Diagnostic reports (https://nodejs.org/api/report.html)
11+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12+
13+
# Runtime data
14+
pids
15+
*.pid
16+
*.seed
17+
*.pid.lock
18+
19+
# Directory for instrumented libs generated by jscoverage/JSCover
20+
lib-cov
21+
22+
# Coverage directory used by tools like istanbul
23+
coverage
24+
*.lcov
25+
26+
# nyc test coverage
27+
.nyc_output
28+
29+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30+
.grunt
31+
32+
# Bower dependency directory (https://bower.io/)
33+
bower_components
34+
35+
# node-waf configuration
36+
.lock-wscript
37+
38+
# Compiled binary addons (https://nodejs.org/api/addons.html)
39+
build/Release
40+
41+
# Dependency directories
42+
node_modules/
43+
jspm_packages/
44+
45+
# Snowpack dependency directory (https://snowpack.dev/)
46+
web_modules/
47+
48+
# TypeScript cache
49+
*.tsbuildinfo
50+
51+
# Optional npm cache directory
52+
.npm
53+
54+
# Optional eslint cache
55+
.eslintcache
56+
57+
# Optional stylelint cache
58+
.stylelintcache
59+
60+
# Microbundle cache
61+
.rpt2_cache/
62+
.rts2_cache_cjs/
63+
.rts2_cache_es/
64+
.rts2_cache_umd/
65+
66+
# Optional REPL history
67+
.node_repl_history
68+
69+
# Output of 'npm pack'
70+
*.tgz
71+
72+
# Yarn Integrity file
73+
.yarn-integrity
74+
75+
# dotenv environment variable files
76+
.env
77+
.env.development.local
78+
.env.test.local
79+
.env.production.local
80+
.env.local
81+
82+
# parcel-bundler cache (https://parceljs.org/)
83+
.cache
84+
.parcel-cache
85+
86+
# Next.js build output
87+
.next
88+
out
89+
90+
# Nuxt.js build / generate output
91+
.nuxt
92+
dist
93+
94+
# Gatsby files
95+
.cache/
96+
# Comment in the public line in if your project uses Gatsby and not Next.js
97+
# https://nextjs.org/blog/next-9-1#public-directory-support
98+
# public
99+
100+
# vuepress build output
101+
.vuepress/dist
102+
103+
# vuepress v2.x temp and cache directory
104+
.temp
105+
.cache
106+
107+
# Docusaurus cache and generated files
108+
.docusaurus
109+
110+
# Serverless directories
111+
.serverless/
112+
113+
# FuseBox cache
114+
.fusebox/
115+
116+
# DynamoDB Local files
117+
.dynamodb/
118+
119+
# TernJS port file
120+
.tern-port
121+
122+
# Stores VSCode versions used for testing VSCode extensions
123+
.vscode-test
124+
125+
# yarn v2
126+
.yarn/cache
127+
.yarn/unplugged
128+
.yarn/build-state.yml
129+
.yarn/install-state.gz
130+
.pnp.*
131+
132+
.turbo

api.gif

752 KB
Loading

license

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) MINO <[email protected]>
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"private": true,
3+
"scripts": {
4+
"turbo": "turbo",
5+
"build": "turbo run build",
6+
"dev": "turbo run dev"
7+
},
8+
"devDependencies": {
9+
"@types/node": "^20.8.2",
10+
"@types/react": "^18.2.24",
11+
"@types/react-dom": "^18.2.8",
12+
"@yme/eslint-config-react": "^0.3.3",
13+
"@yme/eslint-config-typescript": "^0.3.3",
14+
"@yme/tsconfig": "^2.0.0",
15+
"eslint": "^8.50.0",
16+
"rimraf": "^5.0.5",
17+
"tsup": "^7.2.0",
18+
"turbo": "^1.10.14",
19+
"bun-types": "^1.0.7",
20+
"typescript": "^5.2.2"
21+
}
22+
}

packages/api/.eslintrc.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
extends: ['@yme/typescript'],
3+
};

packages/api/package.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "@yme/api",
3+
"description": "define type-safe API routes simply",
4+
"version": "1.0.0",
5+
"type": "module",
6+
"sideEffects": false,
7+
"author": "mino",
8+
"repository": "minosss/api",
9+
"license": "MIT",
10+
"exports": {
11+
".": {
12+
"import": {
13+
"types": "./dist/index.d.ts",
14+
"default": "./dist/index.js"
15+
},
16+
"require": {
17+
"types": "./dist/index.d.cts",
18+
"default": "./dist/index.cjs"
19+
}
20+
},
21+
"./package.json": "./package.json"
22+
},
23+
"main": "./dist/index.cjs",
24+
"module": "./dist/index.js",
25+
"types": "./dist/index.d.ts",
26+
"files": [
27+
"dist"
28+
],
29+
"scripts": {
30+
"clean": "rimraf ./dist",
31+
"build": "tsup",
32+
"dev": "tsup --watch",
33+
"test:types": "tsc --noEmit"
34+
},
35+
"engines": {
36+
"node": ">=16"
37+
},
38+
"publishConfig": {
39+
"access": "public"
40+
}
41+
}

packages/api/src/api.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import type { AnyRouteDef, AnyRoute } from './router';
2+
import type { AnyParser, IsUnknown } from './types';
3+
import { getParser, isPlainObject } from './types';
4+
import { getRouteDef } from './router';
5+
import { createProxy } from './proxy';
6+
import { ApiError } from './error';
7+
8+
export interface Register {
9+
// api: typeof api
10+
}
11+
12+
// export interface RoutesRegister {}
13+
14+
export type AnyHttpClient = (config: any) => Promise<any>;
15+
export type RequestConfig = Register extends { api: { http: AnyHttpClient } }
16+
? Parameters<Register['api']['http']>[0]
17+
: never;
18+
export type HttpClient = Register extends { api: ApiOptions }
19+
? Register['api']['http']
20+
: AnyHttpClient;
21+
22+
export interface Routes {
23+
readonly [key: string]: AnyRoute | Routes;
24+
}
25+
26+
type RouteToRequest<R extends AnyRoute> = IsUnknown<R['_def']['_input']> extends true
27+
? (input?: R['_def']['_input'], config?: RequestConfig) => Promise<R['_def']['_output']>
28+
: (input: R['_def']['_input'], config?: RequestConfig) => Promise<R['_def']['_output']>;
29+
30+
type ExtractRoutes<T> = T extends AnyRoute
31+
? RouteToRequest<T>
32+
: T extends Routes
33+
? { [K in keyof T]: ExtractRoutes<T[K]> }
34+
: never;
35+
36+
export interface ApiOptions {
37+
http: AnyHttpClient;
38+
routes: Routes;
39+
validator?(schema: AnyParser, input: unknown): unknown;
40+
guard?(config: any, route: AnyRoute): boolean;
41+
onSuccess?(output: any): void;
42+
onError?(error: any): void;
43+
onFinished?(config: any): void;
44+
}
45+
46+
export type ApiClient<A extends ApiOptions> = ExtractRoutes<A['routes']> & {
47+
http: A['http'];
48+
};
49+
50+
export function createApi<A extends ApiOptions>(options: A): ApiClient<A> {
51+
const { routes, http, validator, guard, onError, onFinished, onSuccess } = options;
52+
53+
return createProxy(async (opts) => {
54+
// return http client
55+
if (opts.path[0] === 'http') {
56+
return http.call(http, opts.args);
57+
}
58+
59+
const [input, config] = opts.args;
60+
const parts = [...opts.path];
61+
62+
// route
63+
64+
let route: any;
65+
for (const part of parts) {
66+
route = (route || routes)[part];
67+
}
68+
const def = getRouteDef(route);
69+
if (def == null) {
70+
throw ApiError.from('Can not found route def.', ApiError.ERR_BAD_ROUTE, route);
71+
}
72+
73+
const { path, method, schema, transform } = def as AnyRouteDef;
74+
75+
// input
76+
77+
let nextInput = validator ? validator(schema, input) : getParser(schema)(input);
78+
79+
// url
80+
81+
let url = path;
82+
const pathParams = url.match(/:[\dA-Za-z]+/g);
83+
if (pathParams) {
84+
for (const pathParam of pathParams) {
85+
const key = pathParam.slice(1);
86+
if (isPlainObject(nextInput)) {
87+
const value = nextInput[key];
88+
if (typeof value === 'number' || typeof value === 'string') {
89+
url = url.replace(pathParam, `${value}`);
90+
delete nextInput[key];
91+
} else {
92+
throw ApiError.from(`Bad params ${key} data.`, ApiError.ERR_BAD_INPUT, route);
93+
}
94+
} else if (typeof nextInput === 'number' || typeof nextInput === 'string') {
95+
url = url.replace(pathParam, `${nextInput}`);
96+
nextInput = undefined;
97+
break;
98+
} else {
99+
throw ApiError.from(`Bad params ${key} data.`, ApiError.ERR_BAD_INPUT, route);
100+
}
101+
}
102+
}
103+
104+
// request config
105+
106+
const requestConfig: any = {
107+
...config,
108+
method,
109+
url,
110+
};
111+
112+
if (['post', 'put', 'patch'].includes(method.toLowerCase())) {
113+
requestConfig.data = nextInput;
114+
} else {
115+
requestConfig.params = nextInput;
116+
}
117+
118+
// guard
119+
if (guard) {
120+
const isPass = guard(requestConfig, route);
121+
if (isPass !== true) {
122+
throw ApiError.from(`You don't have access to ${requestConfig.method} ${requestConfig.url}`, ApiError.ERR_ACCESS_DENIED, route);
123+
}
124+
}
125+
126+
// http request
127+
128+
try {
129+
const response = await http(requestConfig);
130+
const nextResponse = getParser(transform)(response);
131+
onSuccess?.(nextResponse);
132+
return nextResponse;
133+
} catch (error) {
134+
(error as any).request = requestConfig;
135+
onError?.(error);
136+
throw ApiError.from(error, ApiError.ERR_HTTP_ERROR);
137+
} finally {
138+
onFinished?.(requestConfig);
139+
}
140+
}, []) as any;
141+
}

0 commit comments

Comments
 (0)