diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..eb14e27 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json index dd0f781..6ca7242 100644 --- a/package.json +++ b/package.json @@ -16,53 +16,54 @@ "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'", "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'", "postinstall": "run-p build:icons", - "generate:api": "openapi-typescript http://localhost:8000/api/docs-yaml -o src/generated/api-schema.d.ts" + "generate:api": "openapi-typescript src/openapi/api-schema.yaml -o src/generated/api-schema.d.ts" }, "dependencies": { - "@unocss/reset": "^0.58.4", - "@vueuse/core": "^10.7.2", - "@vueuse/integrations": "^10.8.0", - "openapi-fetch": "^0.9.2", - "openapi-typescript-helpers": "^0.0.7", - "pinia": "^2.1.7", - "ufo": "^1.5.3", - "universal-cookie": "^7.1.0", + "@unocss/reset": "^0.58.9", + "@vueuse/core": "^10.11.0", + "@vueuse/integrations": "^10.11.0", + "ofetch": "^1.3.4", + "openapi-typescript": "^7.3.0", + "openapi-typescript-helpers": "^0.0.11", + "pinia": "^2.2.0", + "ufo": "^1.5.4", + "universal-cookie": "^7.2.0", "valibot": "^0.26.0", - "vue": "^3.4.15", - "vue-router": "^4.2.5" + "vue": "^3.4.35", + "vue-router": "^4.4.2" }, "devDependencies": { - "@iconify/tools": "^4.0.0", + "@iconify/tools": "^4.0.4", "@iconify/types": "^2.0.0", - "@iconify/utils": "^2.1.16", - "@iconify/vue": "^4.1.1", - "@sxzz/eslint-config": "^3.7.6", - "@sxzz/prettier-config": "^2.0.0", - "@types/node": "^20.11.5", - "@vitejs/plugin-vue": "^5.0.3", - "@vue/compiler-sfc": "^3.4.15", - "@vue/test-utils": "^2.4.3", - "cypress": "^13.6.3", - "eslint": "^8.56.0", - "jsdom": "^24.0.0", + "@iconify/utils": "^2.1.29", + "@iconify/vue": "^4.1.2", + "@sxzz/eslint-config": "^3.16.1", + "@sxzz/prettier-config": "^2.0.2", + "@types/node": "^20.14.14", + "@vitejs/plugin-vue": "^5.1.2", + "@vue/compiler-sfc": "^3.4.35", + "@vue/test-utils": "^2.4.6", + "cypress": "^13.13.2", + "eslint": "^8.57.0", + "jsdom": "^24.1.1", "npm-run-all": "^4.1.5", - "openapi-typescript": "^6.7.4", - "picocolors": "^1.0.0", - "prettier": "^3.2.4", - "start-server-and-test": "^2.0.3", + "picocolors": "^1.0.1", + "prettier": "^3.3.3", + "start-server-and-test": "^2.0.5", "ts-reset": "^0.0.1", - "tsx": "^4.7.1", - "typescript": "^5.3.3", - "unocss": "^0.58.4", - "unplugin-auto-import": "^0.17.3", + "tsx": "^4.16.5", + "type-fest": "^4.23.0", + "typescript": "^5.5.4", + "unocss": "^0.58.9", + "unplugin-auto-import": "^0.17.8", "unplugin-vue-components": "^0.26.0", "unplugin-vue-router": "^0.7.0", - "vite": "^5.0.12", - "vite-plugin-pages": "^0.32.0", - "vite-plugin-vue-devtools": "^7.0.11", + "vite": "^5.3.5", + "vite-plugin-pages": "^0.32.3", + "vite-plugin-vue-devtools": "^7.3.7", "vite-plugin-vue-layouts": "^0.11.0", - "vite-plugin-webfont-dl": "^3.9.1", - "vitest": "^1.2.1", + "vite-plugin-webfont-dl": "^3.9.4", + "vitest": "^1.6.0", "vue-tsc": "^1.8.27" }, "simple-git-hooks": { diff --git a/src/composables/use-open-fetch.ts b/src/composables/use-open-fetch.ts new file mode 100644 index 0000000..4e01f1c --- /dev/null +++ b/src/composables/use-open-fetch.ts @@ -0,0 +1,71 @@ +import { + type CreateFetchOptions, + type UseFetchOptions, + type UseFetchReturn, + createFetch, +} from '@vueuse/core' +import { type MaybeRefOrGetter, toValue } from 'vue' +import { + type FetchResponseData, + type FilterMethods, + type ParamsOption, + type RequestBodyOption, + createOpenFetch, +} from '#/utils/fetch' + +type MethodOption = 'get' extends keyof P ? { method?: M } : { method: M } + +type UseOpenFetchOptions< + Method, + LowercasedMethod, + Params, + Operation = 'get' extends LowercasedMethod + ? 'get' extends keyof Params + ? Params['get'] + : never + : LowercasedMethod extends keyof Params + ? Params[LowercasedMethod] + : never, +> = MethodOption & + ParamsOption & + RequestBodyOption + +export type UseOpenFetchClient = < + Path extends Extract, + Methods extends FilterMethods, + Method extends + | Extract + | Uppercase>, + LowercasedMethod extends Lowercase extends keyof Methods + ? Lowercase + : never, + DefaultMethod extends 'get' extends LowercasedMethod + ? 'get' + : LowercasedMethod, + ResT = FetchResponseData, +>( + path: Path | (() => Path), + options: UseOpenFetchOptions, + useFetchOptions?: UseFetchOptions, +) => UseFetchReturn & PromiseLike> + +export function createUseOpenFetch( + config: CreateFetchOptions, +): UseOpenFetchClient { + const useFetch = createFetch({ + ...config, + options: { + fetch: createOpenFetch({ + baseURL: toValue(config.baseUrl), + }), + }, + }) + + return ( + url: MaybeRefOrGetter, + requests: any, //TODO: find a way to type this + useFetchOptions?: UseFetchOptions, + ) => { + return useFetch(toValue(url), requests, useFetchOptions) + } +} diff --git a/src/composables/use-query.ts b/src/composables/use-query.ts deleted file mode 100644 index 4003a8f..0000000 --- a/src/composables/use-query.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { type Stoppable, createEventHook, useTimeoutFn } from '@vueuse/core' -import type { paths } from '#/generated/api-schema' -import type { FetchResponse, MaybeOptionalInit } from 'openapi-fetch' -import type { PathsWithMethod } from 'openapi-typescript-helpers' -import { client } from '#/api/client' - -interface UseFetchOptions { - /** - * Initial data before the request finished - * - * @default null - */ - initialData?: any - - /** - * Timeout for abort request after number of millisecond - * `0` means use browser default - * - * @default 0 - */ - timeout?: number - - /** - * Allow update the `data` ref when fetch error whenever provided, or mutated in the `onFetchError` callback - * - * @default false - */ - updateDataOnError?: boolean -} - -interface QueryParams

{ - /** - * Path to fetch - * @example '/users' - * - */ - path: P - - /** - * Fetch options - */ - fetchOptions?: UseFetchOptions -} - -export function useQuery

>( - queryParams: QueryParams

, -) { - const supportsAbort = typeof AbortController === 'function' - - const { path, fetchOptions = {} } = queryParams - - let requestOptions: RequestInit = {} - - const options: UseFetchOptions = { - timeout: 0, - updateDataOnError: false, - ...fetchOptions, - } - - const { initialData, timeout } = options - - // Event Hooks - const responseEvent = createEventHook() - const errorEvent = createEventHook() - const finallyEvent = createEventHook() - - const isFinished = ref(false) - const isFetching = ref(false) - const aborted = ref(false) - const response = shallowRef(null) - const error = shallowRef(null) - - const data = shallowRef< - FetchResponse['data'] | null - >(initialData || null) - - const canAbort = computed(() => supportsAbort && isFetching.value) - - let controller: AbortController | undefined - let timer: Stoppable | undefined - - const abort = () => { - if (supportsAbort) { - controller?.abort() - controller = new AbortController() - controller.signal.addEventListener('abort', () => (aborted.value = true)) - requestOptions = { - ...requestOptions, - signal: controller.signal, - } - } - } - - const loading = (isLoading: boolean) => { - isFetching.value = isLoading - isFinished.value = !isLoading - } - - if (timeout) timer = useTimeoutFn(abort, timeout, { immediate: false }) - - let executeCounter = 0 - - const execute = (...args: MaybeOptionalInit) => { - abort() - loading(true) - error.value = null - aborted.value = false - - executeCounter += 1 - - const currentExecuteCounter = executeCounter - - const isCanceled = false - - if (isCanceled) { - loading(false) - - return Promise.resolve(null) - } - - if (timer) timer.start() - - // It's hard to type this part, so I just use `as unknown as` to bypass the type check - const queryParams = [ - { ...args[0], ...requestOptions }, - ] as unknown as MaybeOptionalInit - - return client.GET(path, ...queryParams).then((fetchResponse) => { - if (timer) timer.stop() - - if (currentExecuteCounter === executeCounter) loading(false) - finallyEvent.trigger(null) - - if (fetchResponse.data) { - response.value = fetchResponse.response - data.value = fetchResponse.data - - responseEvent.trigger(fetchResponse.response) - - return fetchResponse - } - if (fetchResponse.error) { - data.value = initialData || null - error.value = fetchResponse.error - if (options.updateDataOnError) errorEvent.trigger(fetchResponse.error) - - return null - } - }) - } - - return { - isFinished: readonly(isFinished), - isFetching: readonly(isFetching), - error, - canAbort, - aborted, - abort, - data, - statusCode: computed(() => response.value?.status), - - onFetchResponse: responseEvent.on, - onFetchError: errorEvent.on, - onFetchFinally: finallyEvent.on, - - execute, - } -} diff --git a/src/generated/api-schema.d.ts b/src/generated/api-schema.d.ts index aa76bd6..8666477 100644 --- a/src/generated/api-schema.d.ts +++ b/src/generated/api-schema.d.ts @@ -4,311 +4,595 @@ */ export interface paths { - '/auth/register': { - post: operations['AuthController_register'] - } - '/auth/resend-email': { - post: operations['AuthController_resendEmail'] - } - '/auth/verify': { - get: operations['AuthController_verify'] - } - '/auth/login': { - post: operations['AuthController_login'] - } - '/auth/refresh-token': { - get: operations['AuthController_refreshToken'] - } - '/auth/logout': { - delete: operations['AuthController_logout'] - } - '/auth/me': { - get: operations['AuthController_me'] - } - '/posts': { - get: operations['PostsController_find'] - post: operations['PostsController_create'] - } - '/posts/{postId}': { - get: operations['PostsController_findOne'] - patch: operations['PostsController_update'] - } - '/posts/{id}': { - delete: operations['PostsController_remove'] - } + "/auth/register": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthController_register"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/resend-email": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthController_resendEmail"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/verify": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["AuthController_verify"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["AuthController_login"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/refresh-token": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["AuthController_refreshToken"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["AuthController_logout"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["AuthController_me"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["UsersController_findAll"]; + put?: never; + post: operations["UsersController_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/posts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["PostsController_find"]; + put?: never; + post: operations["PostsController_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/posts/{postId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["PostsController_findOne"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: operations["PostsController_update"]; + trace?: never; + }; + "/posts/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["PostsController_remove"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } - -export type webhooks = Record - +export type webhooks = Record; export interface components { - schemas: { - RegisterAuthDto: { - /** @example example */ - fullName: string - /** @example example@example.com */ - email: string - /** @example example */ - password: string - } - MessageResponse: { - message: string - } - LoginAuthDto: { - /** @example example@example.com */ - email: string - /** @example example */ - password: string - } - AuthTokenResponse: { - /** @description The JWT token */ - idToken: string - /** @description The access token */ - accessToken: string - /** @description The refresh token */ - refreshToken: string - } - UserEntity: { - /** - * Format: date-time - * @description createdAt - */ - createdAt: string - /** - * Format: date-time - * @description updatedAt - */ - updatedAt: string - /** @description createdBy */ - createdBy: string - /** - * @description The name of User - * @example example - */ - fullName: string - /** - * @description The email of User - * @example example@gmail.com - */ - email: string - /** - * @description The password of User - * @example example - */ - password: string - /** - * @description The verify code of User - * @example example - */ - isVerified: boolean - /** - * @description The role of User - * @default user - * @example user - */ - role: string - } - CreatePostDto: { - /** - * @description The name of User - * @example example - */ - title: string - /** - * @description The content of the post - * @example This is content of this post - */ - content: string - /** - * @description The slug of the post - * @example :this-is-a-post - */ - slug: string - } - UpdatePostDto: { - /** - * @description The name of User - * @example example - */ - title?: string - /** - * @description The content of the post - * @example This is content of this post - */ - content?: string - /** - * @description The slug of the post - * @example :this-is-a-post - */ - slug?: string - } - } - responses: never - parameters: never - requestBodies: never - headers: never - pathItems: never + schemas: { + RegisterAuthDto: { + /** @example example */ + fullName: string; + /** @example example@example.com */ + email: string; + /** @example example */ + password: string; + }; + MessageResponse: { + message: string; + }; + LoginAuthDto: { + /** @example example@example.com */ + email: string; + /** @example example */ + password: string; + }; + AuthTokenResponse: { + /** @description The JWT token */ + idToken: string; + /** @description The access token */ + accessToken: string; + /** @description The refresh token */ + refreshToken: string; + }; + UserEntity: { + /** + * Format: date-time + * @description createdAt + */ + createdAt: string; + /** + * Format: date-time + * @description updatedAt + */ + updatedAt: string; + /** @description createdBy */ + createdBy: string; + /** + * @description The name of User + * @example example + */ + fullName: string; + /** + * @description The email of User + * @example example@gmail.com + */ + email: string; + /** + * @description The password of User + * @example example + */ + password: string; + /** + * @description The verify code of User + * @example example + */ + isVerified: boolean; + /** + * @description The role of User + * @default user + * @example user + */ + role: string; + }; + CreateUserDto: { + /** @example example@example.com */ + email: string; + }; + CreatePostDto: { + /** + * @description The name of User + * @example example + */ + title: string; + /** + * @description The content of the post + * @example This is content of this post + */ + content: string; + /** + * @description The slug of the post + * @example :this-is-a-post + */ + slug: string; + }; + UpdatePostDto: { + /** + * @description The name of User + * @example example + */ + title?: string; + /** + * @description The content of the post + * @example This is content of this post + */ + content?: string; + /** + * @description The slug of the post + * @example :this-is-a-post + */ + slug?: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } - -export type $defs = Record - -export type external = Record - +export type $defs = Record; export interface operations { - AuthController_register: { - requestBody: { - content: { - 'application/json': components['schemas']['RegisterAuthDto'] - } - } - responses: { - /** @description The user has been successfully created. */ - 201: { - content: { - 'application/json': components['schemas']['MessageResponse'] - } - } - } - } - AuthController_resendEmail: { - parameters: { - query: { - email: string - } - } - responses: { - /** @description The email has been successfully sent. */ - 200: { - content: { - 'application/json': components['schemas']['MessageResponse'] - } - } - } - } - AuthController_verify: { - parameters: { - query: { - token: string - } - } - responses: { - /** @description The user has been verified successfully */ - 200: { - content: { - 'application/json': components['schemas']['MessageResponse'] - } - } - } - } - AuthController_login: { - requestBody: { - content: { - 'application/json': components['schemas']['LoginAuthDto'] - } - } - responses: { - /** @description The user has been login successfully */ - 200: { - content: { - 'application/json': components['schemas']['AuthTokenResponse'] - } - } - } - } - AuthController_refreshToken: { - responses: { - /** @description The user has been refresh token successfully */ - 200: { - content: { - 'application/json': components['schemas']['AuthTokenResponse'] - } - } - } - } - AuthController_logout: { - responses: { - /** @description The user has been logout successfully */ - 200: { - content: { - 'application/json': components['schemas']['MessageResponse'] - } - } - } - } - AuthController_me: { - responses: { - /** @description The user has been get successfully */ - 200: { - content: { - 'application/json': components['schemas']['UserEntity'] - } - } - /** @description Unauthorized */ - 401: { - content: never - } - } - } - PostsController_find: { - responses: { - 200: { - content: never - } - } - } - PostsController_create: { - requestBody: { - content: { - 'application/json': components['schemas']['CreatePostDto'] - } - } - responses: { - /** @description The post has been successfully created. */ - 201: { - content: { - 'application/json': components['schemas']['CreatePostDto'] - } - } - } - } - PostsController_findOne: { - parameters: { - path: { - postId: number - } - } - responses: { - 200: { - content: never - } - } - } - PostsController_update: { - parameters: { - path: { - postId: number - } - } - requestBody: { - content: { - 'application/json': components['schemas']['UpdatePostDto'] - } - } - responses: { - 200: { - content: never - } - } - } - PostsController_remove: { - parameters: { - path: { - id: number - } - } - responses: { - 200: { - content: never - } - } - } + AuthController_register: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["RegisterAuthDto"]; + }; + }; + responses: { + /** @description The user has been successfully created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageResponse"]; + }; + }; + }; + }; + AuthController_resendEmail: { + parameters: { + query: { + email: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The email has been successfully sent. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageResponse"]; + }; + }; + }; + }; + AuthController_verify: { + parameters: { + query: { + token: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The user has been verified successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageResponse"]; + }; + }; + }; + }; + AuthController_login: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginAuthDto"]; + }; + }; + responses: { + /** @description The user has been login successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthTokenResponse"]; + }; + }; + }; + }; + AuthController_refreshToken: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The user has been refresh token successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthTokenResponse"]; + }; + }; + }; + }; + AuthController_logout: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The user has been logout successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["MessageResponse"]; + }; + }; + }; + }; + AuthController_me: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The user has been get successfully */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserEntity"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + UsersController_findAll: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + UsersController_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateUserDto"]; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PostsController_find: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PostsController_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreatePostDto"]; + }; + }; + responses: { + /** @description The post has been successfully created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CreatePostDto"]; + }; + }; + }; + }; + PostsController_findOne: { + parameters: { + query?: never; + header?: never; + path: { + postId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PostsController_update: { + parameters: { + query?: never; + header?: never; + path: { + postId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdatePostDto"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PostsController_remove: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; } diff --git a/src/openapi/api-schema.yaml b/src/openapi/api-schema.yaml new file mode 100644 index 0000000..b0ca1b1 --- /dev/null +++ b/src/openapi/api-schema.yaml @@ -0,0 +1,366 @@ +openapi: 3.0.0 +paths: + /auth/register: + post: + operationId: AuthController_register + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterAuthDto' + responses: + '201': + description: The user has been successfully created. + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + tags: + - Auth + /auth/resend-email: + post: + operationId: AuthController_resendEmail + parameters: + - name: email + required: true + in: query + schema: + type: string + responses: + '200': + description: The email has been successfully sent. + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + tags: + - Auth + /auth/verify: + get: + operationId: AuthController_verify + parameters: + - name: token + required: true + in: query + schema: + type: string + responses: + '200': + description: The user has been verified successfully + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + tags: + - Auth + /auth/login: + post: + operationId: AuthController_login + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginAuthDto' + responses: + '200': + description: The user has been login successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponse' + tags: + - Auth + /auth/refresh-token: + get: + operationId: AuthController_refreshToken + parameters: [] + responses: + '200': + description: The user has been refresh token successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AuthTokenResponse' + tags: + - Auth + /auth/logout: + delete: + operationId: AuthController_logout + parameters: [] + responses: + '200': + description: The user has been logout successfully + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + tags: + - Auth + /auth/me: + get: + operationId: AuthController_me + parameters: [] + responses: + '200': + description: The user has been get successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UserEntity' + '401': + description: Unauthorized + tags: + - Auth + security: + - defaultBearerAuth: [] + /users: + post: + operationId: UsersController_create + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserDto' + responses: + '201': + description: '' + get: + operationId: UsersController_findAll + parameters: [] + responses: + '200': + description: '' + /posts: + post: + operationId: PostsController_create + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePostDto' + responses: + '201': + description: The post has been successfully created. + content: + application/json: + schema: + $ref: '#/components/schemas/CreatePostDto' + tags: + - Posts + get: + operationId: PostsController_find + parameters: [] + responses: + '200': + description: '' + tags: + - Posts + /posts/{postId}: + get: + operationId: PostsController_findOne + parameters: + - name: postId + required: true + in: path + schema: + type: number + responses: + '200': + description: '' + tags: + - Posts + patch: + operationId: PostsController_update + parameters: + - name: postId + required: true + in: path + schema: + type: number + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdatePostDto' + responses: + '200': + description: '' + tags: + - Posts + /posts/{id}: + delete: + operationId: PostsController_remove + parameters: + - name: id + required: true + in: path + schema: + type: number + responses: + '200': + description: '' + tags: + - Posts +info: + title: NestJS API Starter + description: NestJS APIs Documentation + version: 1.0.0 + contact: {} +tags: [] +servers: [] +components: + securitySchemes: + defaultBearerAuth: + description: 'Please enter token in following format: Bearer ' + name: Authorization + bearerFormat: JWT + scheme: bearer + type: http + in: header + schemas: + RegisterAuthDto: + type: object + properties: + fullName: + type: string + example: example + email: + type: string + example: example@example.com + password: + type: string + example: example + required: + - fullName + - email + - password + MessageResponse: + type: object + properties: + message: + type: string + required: + - message + LoginAuthDto: + type: object + properties: + email: + type: string + example: example@example.com + password: + type: string + example: example + required: + - email + - password + AuthTokenResponse: + type: object + properties: + idToken: + type: string + description: The JWT token + accessToken: + type: string + description: The access token + refreshToken: + type: string + description: The refresh token + required: + - idToken + - accessToken + - refreshToken + UserEntity: + type: object + properties: + createdAt: + format: date-time + type: string + description: createdAt + updatedAt: + format: date-time + type: string + description: updatedAt + createdBy: + type: string + description: createdBy + fullName: + type: string + example: example + description: The name of User + email: + type: string + example: example@gmail.com + description: The email of User + password: + type: string + example: example + description: The password of User + isVerified: + type: boolean + example: example + description: The verify code of User + role: + type: string + example: user + description: The role of User + default: user + required: + - createdAt + - updatedAt + - createdBy + - fullName + - email + - password + - isVerified + - role + CreateUserDto: + type: object + properties: + email: + type: string + example: example@example.com + required: + - email + CreatePostDto: + type: object + properties: + title: + type: string + example: example + description: The name of User + content: + type: string + example: This is content of this post + description: The content of the post + slug: + type: string + example: ':this-is-a-post' + description: The slug of the post + required: + - title + - content + - slug + UpdatePostDto: + type: object + properties: + title: + type: string + example: example + description: The name of User + content: + type: string + example: This is content of this post + description: The content of the post + slug: + type: string + example: ':this-is-a-post' + description: The slug of the post diff --git a/src/utils/__tests__/fetch.spec.ts b/src/utils/__tests__/fetch.spec.ts new file mode 100644 index 0000000..25870ff --- /dev/null +++ b/src/utils/__tests__/fetch.spec.ts @@ -0,0 +1,26 @@ +import { describe, it } from 'vitest' +import { createOpenFetch } from '../fetch' +import type { paths } from '#/generated/api-schema' + +describe('fetch', () => { + it('should fill path correctly', async () => { + const $fetch = createOpenFetch({ + baseURL: 'https://api.example.com', + }) + + await $fetch('/auth/login', { + method: 'POST', + body: { + email: 'admin@example.com', + password: 'password', + }, + }) + + await $fetch('/posts/{postId}', { + method: 'GET', + path: { + postId: 1, + }, + }) + }) +}) diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts new file mode 100644 index 0000000..3f5be11 --- /dev/null +++ b/src/utils/fetch.ts @@ -0,0 +1,112 @@ +import { + $fetch, + type FetchContext, + type FetchError, + type FetchOptions, +} from 'ofetch' +import type { + ErrorResponse, + MediaType, + OperationRequestBodyContent, + ResponseObjectMap, + SuccessResponse, +} from 'openapi-typescript-helpers' + +export type FetchResponseData< + T, + Media extends MediaType = MediaType, +> = SuccessResponse, Media> + +export type FetchResponseError< + T, + Media extends MediaType = MediaType, +> = FetchError, Media>> + +export type MethodOption = 'get' extends keyof P + ? { method?: M } + : { method: M } + +export type ParamsOption = T extends { parameters?: any; query?: any } + ? T['parameters'] + : Record + +export type RequestBodyOption = + OperationRequestBodyContent extends never + ? { body?: never } + : undefined extends OperationRequestBodyContent + ? { body?: OperationRequestBodyContent } + : { body: OperationRequestBodyContent } + +export type FilterMethods = { + [K in keyof Omit as T[K] extends never | undefined + ? never + : K]: T[K] +} + +export type OpenFetchOptions< + Method, + LowercasedMethod, + Params, + Operation = 'get' extends LowercasedMethod + ? 'get' extends keyof Params + ? Params['get'] + : never + : LowercasedMethod extends keyof Params + ? Params[LowercasedMethod] + : never, +> = MethodOption & + ParamsOption & + RequestBodyOption & + Omit + +export type OpenFetchClient = < + ReqT extends Extract, + Methods extends FilterMethods, + Method extends + | Extract + | Uppercase>, + LowercasedMethod extends Lowercase extends keyof FilterMethods< + Paths[ReqT] + > + ? Lowercase + : never, + DefaultMethod extends 'get' extends LowercasedMethod + ? 'get' + : LowercasedMethod, + ResT = FetchResponseData, + ErrorT = FetchResponseError, +>( + url: ReqT, + options?: OpenFetchOptions, +) => Promise + +// More flexible way to rewrite the request path, +// but has problems - https://github.com/unjs/ofetch/issues/319 +export function openFetchRequestInterceptor(ctx: FetchContext) { + ctx.request = fillPath( + ctx.request as string, + (ctx.options as { path: Record }).path, + ) +} + +export function createOpenFetch( + options: FetchOptions | ((options: FetchOptions) => FetchOptions), +): OpenFetchClient { + return (url: string, opts: any = {}) => { + return $fetch( + fillPath(url, opts?.path), + typeof options === 'function' + ? options(opts) + : { + ...options, + ...opts, + }, + ) + } +} + +function fillPath(path: string, params: object = {}) { + for (const [k, v] of Object.entries(params)) + path = path.replace(`{${k}}`, encodeURIComponent(String(v))) + return path +} diff --git a/tsconfig.json b/tsconfig.json index ea50e25..f1fe92f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,9 +9,9 @@ "baseUrl": ".", /* Bundler mode */ "module": "ESNext", - "moduleResolution": "bundler", + "moduleResolution": "Bundler", "paths": { - "#/*": ["src/*"], + "#/*": ["src/*"] }, "resolveJsonModule": true, "allowImportingTsExtensions": true, @@ -24,11 +24,12 @@ "isolatedModules": true, "verbatimModuleSyntax": true, "skipLibCheck": true, + "noUncheckedIndexedAccess": true }, "include": [ "src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue", - "typed-router.d.ts", - ], + "typed-router.d.ts" + ] }