├── .nvmrc ├── packages ├── vue │ ├── .prettierrc │ ├── renovate.json │ ├── .eslintrc │ ├── src │ │ ├── index.ts │ │ └── runtime │ │ │ └── plugin.ts │ ├── .gitignore │ ├── test │ │ └── index.test.ts │ ├── .editorconfig │ ├── tsconfig.json │ ├── .github │ │ └── workflows │ │ │ └── ci.yml │ ├── LICENSE │ ├── package.json │ ├── README.md │ └── CHANGELOG.md ├── core │ ├── src │ │ ├── runtime │ │ │ ├── providers │ │ │ │ ├── http │ │ │ │ │ ├── axios │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── ofetch │ │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── utils │ │ │ │ ├── context.ts │ │ │ │ ├── index.ts │ │ │ │ ├── export.ts │ │ │ │ ├── transform.test.ts │ │ │ │ ├── transform.ts │ │ │ │ └── transform.debug.ts │ │ │ └── bin │ │ │ │ └── cli.ts │ │ ├── node.ts │ │ └── index.ts │ ├── README.md │ ├── tsconfig.node.json │ ├── vitest.config.ts │ ├── .versionrc │ ├── .gitignore │ ├── tsconfig.json │ ├── build.config.ts │ ├── package.json │ └── CHANGELOG.md └── nuxt │ ├── .npmrc │ ├── .eslintrc │ ├── test │ ├── fixtures │ │ └── basic │ │ │ ├── package.json │ │ │ ├── app.vue │ │ │ └── nuxt.config.ts │ └── basic.test.ts │ ├── tsconfig.json │ ├── .editorconfig │ ├── eslint.config.mjs │ ├── .gitignore │ ├── package.json │ ├── src │ ├── module.ts │ └── runtime │ │ └── composables │ │ └── useFetchModel.ts │ ├── README.md │ └── CHANGELOG.md ├── apps ├── docs │ ├── .gitignore │ ├── docs │ │ ├── .vitepress │ │ │ ├── theme │ │ │ │ ├── index.js │ │ │ │ └── style.css │ │ │ └── config.mts │ │ ├── examples.md │ │ ├── index.md │ │ ├── api │ │ │ ├── core.md │ │ │ └── nuxt.md │ │ └── guide │ │ │ ├── composables │ │ │ ├── structuring-api-functions.md │ │ │ ├── introduction.md │ │ │ └── mapping.md │ │ │ └── getting-started │ │ │ ├── introduction.md │ │ │ └── installation.md │ ├── CHANGELOG.md │ └── package.json ├── vue-example │ ├── env.d.ts │ ├── api │ │ ├── _composables_ │ │ │ └── index.ts │ │ └── users │ │ │ └── index.ts │ ├── .gitignore │ ├── public │ │ └── favicon.ico │ ├── .vscode │ │ └── extensions.json │ ├── tsconfig.json │ ├── src │ │ ├── main.ts │ │ ├── views │ │ │ └── HomePage.vue │ │ ├── assets │ │ │ ├── logo.svg │ │ │ ├── main.css │ │ │ └── base.css │ │ ├── components │ │ │ ├── icons │ │ │ │ ├── IconSupport.vue │ │ │ │ ├── IconTooling.vue │ │ │ │ ├── IconCommunity.vue │ │ │ │ ├── IconDocumentation.vue │ │ │ │ └── IconEcosystem.vue │ │ │ ├── HelloWorld.vue │ │ │ ├── WelcomeItem.vue │ │ │ └── TheWelcome.vue │ │ ├── router │ │ │ └── index.ts │ │ └── App.vue │ ├── .eslintrc.cjs │ ├── index.html │ ├── tsconfig.app.json │ ├── tsconfig.node.json │ ├── .versionrc │ ├── vite.config.ts │ ├── auto-imports.d.ts │ ├── package.json │ ├── CHANGELOG.md │ └── README.md └── nuxt-example │ ├── api │ ├── _utils.ts │ ├── auth │ │ └── index.ts │ ├── cms │ │ └── blogs │ │ │ ├── article.ts │ │ │ └── index.ts │ ├── _composables_ │ │ └── index.ts │ └── users │ │ └── index.ts │ ├── pages │ ├── test.vue │ ├── index.vue │ └── [id].vue │ ├── server │ └── tsconfig.json │ ├── .gitignore │ ├── tsconfig.json │ ├── nuxt.config.ts │ ├── .versionrc │ ├── package.json │ ├── README.md │ └── CHANGELOG.md ├── pnpm-workspace.yaml ├── .changeset ├── config.json └── README.md ├── .cursorignore ├── .gitignore ├── turbo.json ├── package.json ├── README.md └── TODO.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.9.0 2 | -------------------------------------------------------------------------------- /packages/vue/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/core/src/runtime/providers/http/axios/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/core/src/node.ts: -------------------------------------------------------------------------------- 1 | export * from './runtime/utils/export' -------------------------------------------------------------------------------- /apps/docs/.gitignore: -------------------------------------------------------------------------------- 1 | docs/.vitepress/dist 2 | docs/.vitepress/cache -------------------------------------------------------------------------------- /apps/vue-example/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/core/src/runtime/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http' -------------------------------------------------------------------------------- /packages/nuxt/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /packages/vue/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>unjs/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' 3 | - 'packages/*' 4 | - 'docs/*' 5 | -------------------------------------------------------------------------------- /apps/nuxt-example/api/_utils.ts: -------------------------------------------------------------------------------- 1 | export function someUtils() { 2 | return 'someUtils' 3 | } -------------------------------------------------------------------------------- /apps/nuxt-example/api/auth/index.ts: -------------------------------------------------------------------------------- 1 | export default function () { 2 | return 'login it' 3 | } -------------------------------------------------------------------------------- /apps/vue-example/api/_composables_/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useApiUsers } from '../users' -------------------------------------------------------------------------------- /packages/vue/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint-config-unjs"], 3 | "rules": {} 4 | } 5 | -------------------------------------------------------------------------------- /apps/nuxt-example/api/cms/blogs/article.ts: -------------------------------------------------------------------------------- 1 | export default function () { 2 | return 'test' 3 | } -------------------------------------------------------------------------------- /apps/nuxt-example/api/cms/blogs/index.ts: -------------------------------------------------------------------------------- 1 | export default function () { 2 | return 'test' 3 | } -------------------------------------------------------------------------------- /apps/nuxt-example/pages/test.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/nuxt/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["@nuxt/eslint-config"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/nuxt-example/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/src/runtime/utils/context.ts: -------------------------------------------------------------------------------- 1 | export interface IContext { 2 | [key: string]: any; 3 | } -------------------------------------------------------------------------------- /apps/vue-example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nitro 4 | .cache 5 | .output 6 | .env 7 | dist 8 | -------------------------------------------------------------------------------- /packages/vue/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@vue-api/core' 2 | export { default as plugin } from './runtime/plugin' -------------------------------------------------------------------------------- /apps/nuxt-example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log* 3 | .nuxt 4 | .nitro 5 | .cache 6 | .output 7 | .env 8 | dist 9 | -------------------------------------------------------------------------------- /apps/vue-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaetansenn/vue-api/HEAD/apps/vue-example/public/favicon.ico -------------------------------------------------------------------------------- /packages/nuxt/test/fixtures/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "basic", 4 | "type": "module" 5 | } 6 | -------------------------------------------------------------------------------- /packages/nuxt/test/fixtures/basic/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /apps/nuxt-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://v3.nuxtjs.org/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /apps/vue-example/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | // { 2 | // "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | // } 4 | -------------------------------------------------------------------------------- /apps/docs/docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import './style.css' 3 | 4 | export default DefaultTheme -------------------------------------------------------------------------------- /apps/docs/docs/.vitepress/theme/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-c-brand-1: #00dc82; 3 | --vp-c-brand-2: #00b969; 4 | --vp-c-brand-3: #009e5a; 5 | } -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './runtime/utils/context' 2 | export * from './runtime/utils/transform' 3 | export * from './runtime/providers' -------------------------------------------------------------------------------- /packages/vue/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist 4 | types 5 | .vscode 6 | .DS_Store 7 | .eslintcache 8 | *.log* 9 | *.conf* 10 | *.env* 11 | -------------------------------------------------------------------------------- /packages/nuxt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | "exclude": [ 4 | "dist", 5 | "node_modules", 6 | "playground", 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | TODO: 2 | Add dropdown component 3 | Replace icons with https://icones.js.org/ (https://github.com/antfu/unplugin-icons) 4 | https://github.com/dcastil/tailwind-merge -------------------------------------------------------------------------------- /packages/nuxt/test/fixtures/basic/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import MyModule from '../../../src/module' 2 | 3 | export default defineNuxtConfig({ 4 | modules: [ 5 | MyModule, 6 | ], 7 | }) 8 | -------------------------------------------------------------------------------- /apps/vue-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/vue/test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, describe } from "vitest"; 2 | import {} from "../src"; 3 | 4 | describe("packageName", () => { 5 | it.todo("pass", () => { 6 | expect(true).toBe(true); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /apps/docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # docs 2 | 3 | ## 2.0.1 4 | 5 | ### Patch Changes 6 | 7 | - Handle error typing for useFetch 8 | 9 | ## 2.0.0 10 | 11 | ### Major Changes 12 | 13 | - f42f806: refactoring usage of useFetch / fetch with nuxt 14 | -------------------------------------------------------------------------------- /apps/nuxt-example/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://v3.nuxtjs.org/api/configuration/nuxt.config 2 | export default defineNuxtConfig({ 3 | modules: ['@vue-api/nuxt', '@nuxtjs/tailwindcss'], 4 | compatibilityDate: '2024-04-03', 5 | devtools: { enabled: true }, 6 | }) -------------------------------------------------------------------------------- /apps/vue-example/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | import './assets/main.css' 6 | 7 | const app = createApp(App) 8 | 9 | app.use(router) 10 | 11 | app.mount('#app') 12 | -------------------------------------------------------------------------------- /packages/nuxt/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /packages/core/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "strictNullChecks": false, 7 | "noImplicitAny": false, 8 | "allowSyntheticDefaultImports": true, 9 | } 10 | } -------------------------------------------------------------------------------- /apps/nuxt-example/api/_composables_/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useApiAuth } from '../auth' 2 | export { default as useApiUsers } from '../users' 3 | export { default as useApiCmsBlogsArticle } from '../cms/blogs/article' 4 | export { default as useApiCmsBlogsIndex } from '../cms/blogs/index' -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", 3 | "changelog": "@changesets/changelog-git", 4 | "fixed": [], 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /apps/vue-example/src/views/HomePage.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /packages/vue/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | 9 | [*.js] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [{package.json,*.yml,*.cjson}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /apps/vue-example/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/vue-example/src/components/icons/IconSupport.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /apps/vue-example/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 'latest' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | reporters: ["verbose"], 6 | include: ['src/**/*.test.ts'], 7 | globals: true, 8 | environment: 'jsdom', 9 | onConsoleLog: (log) => { 10 | console.log(log); 11 | } 12 | } 13 | }) -------------------------------------------------------------------------------- /.cursorignore: -------------------------------------------------------------------------------- 1 | .turbo 2 | node_modules 3 | .changesets 4 | packages/core/node_modules 5 | packages/core/dist 6 | package/nuxt/node_modules 7 | package/nuxt/dist 8 | package/vue/node_modules 9 | package/vue/dist 10 | package/docs/node_modules 11 | package/docs/dist 12 | package/vue-example/node_modules 13 | package/vue-example/dist 14 | package/nuxt-example/node_modules 15 | package/nuxt-example/dist -------------------------------------------------------------------------------- /apps/vue-example/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import HomePage from '../views/HomePage.vue' 3 | 4 | const router = createRouter({ 5 | history: createWebHistory(import.meta.env.BASE_URL), 6 | routes: [ 7 | { 8 | path: '/', 9 | name: 'index', 10 | component: HomePage 11 | } 12 | ] 13 | }) 14 | 15 | export default router 16 | -------------------------------------------------------------------------------- /packages/vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "preserve", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "outDir": "./dist", 10 | "declaration": true 11 | }, 12 | "include": ["src"], 13 | "paths": { 14 | "@vue-api/core": ["../core/src"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/vue-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/docs/docs/examples.md: -------------------------------------------------------------------------------- 1 | # Usage and Installation Examples 2 | 3 | This project includes usage and installation examples in the following packages: 4 | 5 | - [Nuxt Example](https://github.com/gaetansenn/vue-api/tree/main/apps/nuxt-example) 6 | - [Vue Example](https://github.com/gaetansenn/vue-api/tree/main/apps/vue-example) 7 | 8 | For more details, check out the [project repository](https://github.com/gaetansenn/vue-api/tree/main/apps). 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # misc 12 | .DS_Store 13 | *.pem 14 | 15 | # debug 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | .pnpm-debug.log* 20 | 21 | # local env files 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | # turbo 28 | .turbo -------------------------------------------------------------------------------- /packages/nuxt/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat' 3 | 4 | // Run `npx @eslint/config-inspector` to inspect the resolved config interactively 5 | export default createConfigForNuxt({ 6 | features: { 7 | // Rules for module authors 8 | tooling: true, 9 | }, 10 | dirs: { 11 | src: [ 12 | './playground', 13 | ], 14 | }, 15 | }) 16 | .append( 17 | // your custom flat config here... 18 | ) 19 | -------------------------------------------------------------------------------- /apps/vue-example/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "auto-imports.d.ts", "api/**/*"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 8 | // "types": [ 9 | // "vue-api/vue/volar" 10 | // ], 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": ["./src/*"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/vue-example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node20/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "noEmit": true, 13 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 14 | 15 | "module": "ESNext", 16 | "moduleResolution": "Bundler", 17 | "types": ["node"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/core/.versionrc: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | {"type": "feat", "section": "Features", "scope": "core" }, 4 | {"type": "fix", "section": "Bug Fixes", "scope": "core" }, 5 | {"type": "chore", "hidden": true, "scope": "core" }, 6 | {"type": "docs", "hidden": true, "scope": "core" }, 7 | {"type": "style", "hidden": true, "scope": "core" }, 8 | {"type": "refactor", "hidden": true, "scope": "core"}, 9 | {"type": "perf", "hidden": true, "scope": "core"}, 10 | {"type": "test", "hidden": true, "scope": "core"} 11 | ] 12 | } -------------------------------------------------------------------------------- /packages/nuxt/test/basic.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { describe, it, expect } from 'vitest' 3 | import { setup, $fetch } from '@nuxt/test-utils' 4 | 5 | describe('ssr', async () => { 6 | await setup({ 7 | rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url)), 8 | }) 9 | 10 | it('renders the index page', async () => { 11 | // Get response to a server-rendered page with `$fetch`. 12 | const html = await $fetch('/') 13 | expect(html).toContain('
basic
') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Env 20 | .env 21 | 22 | # Testing 23 | reports 24 | coverage 25 | *.lcov 26 | .nyc_output 27 | 28 | # VSCode 29 | .vscode 30 | 31 | # Intellij idea 32 | *.iml 33 | .idea 34 | 35 | # OSX 36 | .DS_Store 37 | .AppleDouble 38 | .LSOverride 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | -------------------------------------------------------------------------------- /packages/vue/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | ci: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - run: corepack enable 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: 18 20 | cache: "pnpm" 21 | - run: pnpm install 22 | - run: pnpm lint 23 | - run: pnpm build 24 | - run: pnpm vitest --coverage 25 | - uses: codecov/codecov-action@v3 26 | -------------------------------------------------------------------------------- /apps/vue-example/.versionrc: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | {"type": "feat", "section": "Features", "scope": "vue-example" }, 4 | {"type": "fix", "section": "Bug Fixes", "scope": "vue-example" }, 5 | {"type": "chore", "hidden": true, "scope": "vue-example" }, 6 | {"type": "docs", "hidden": true, "scope": "vue-example" }, 7 | {"type": "style", "hidden": true, "scope": "vue-example" }, 8 | {"type": "refactor", "hidden": true, "scope": "vue-example" }, 9 | {"type": "perf", "hidden": true, "scope": "vue-example" }, 10 | {"type": "test", "hidden": true, "scope": "vue-example" } 11 | ] 12 | } -------------------------------------------------------------------------------- /apps/nuxt-example/.versionrc: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | {"type": "feat", "section": "Features", "scope": "nuxt-example" }, 4 | {"type": "fix", "section": "Bug Fixes", "scope": "nuxt-example" }, 5 | {"type": "chore", "hidden": true, "scope": "nuxt-example" }, 6 | {"type": "docs", "hidden": true, "scope": "nuxt-example" }, 7 | {"type": "style", "hidden": true, "scope": "nuxt-example" }, 8 | {"type": "refactor", "hidden": true, "scope": "nuxt-example" }, 9 | {"type": "perf", "hidden": true, "scope": "nuxt-example" }, 10 | {"type": "test", "hidden": true, "scope": "nuxt-example" } 11 | ] 12 | } -------------------------------------------------------------------------------- /apps/vue-example/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import "./base.css"; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "2.0.1", 4 | "private": true, 5 | "description": "A flexible and provider-agnostic API handling library for Vue 3 and Nuxt 3. Supports multiple data providers like axios, ofetch, and GraphQL, and includes a robust model mapping feature.", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "vitepress dev docs", 10 | "build": "vitepress build docs", 11 | "preview": "vitepress preview docs" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "vitepress": "^1.3.4" 18 | } 19 | } -------------------------------------------------------------------------------- /apps/vue-example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueJsx from '@vitejs/plugin-vue-jsx' 6 | import vueDevTools from 'vite-plugin-vue-devtools' 7 | import { plugin as vueApiPlugin } from '@vue-api/vue' 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | vue(), 13 | vueJsx(), 14 | vueDevTools(), 15 | vueApiPlugin(), 16 | ], 17 | resolve: { 18 | alias: { 19 | '@': fileURLToPath(new URL('./src', import.meta.url)) 20 | } 21 | }, 22 | preview: { 23 | port: 3001 24 | }, 25 | server: { 26 | port: 3001 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /apps/nuxt-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "nuxt-example", 4 | "version": "2.0.9", 5 | "scripts": { 6 | "build": "nuxt build", 7 | "dev": "nuxt dev", 8 | "generate": "nuxt generate", 9 | "preview": "nuxt preview", 10 | "changelog": "standard-version" 11 | }, 12 | "dependencies": { 13 | "@vue-api/core": "workspace:*", 14 | "@vue-api/nuxt": "workspace:*" 15 | }, 16 | "devDependencies": { 17 | "@nuxtjs/tailwindcss": "^6.12.1", 18 | "nuxt": "latest", 19 | "vue": "latest" 20 | }, 21 | "standard-version": { 22 | "skip": { 23 | "bump": true, 24 | "commit": true, 25 | "tag": true 26 | }, 27 | "tag-prefix": "nuxt-example@" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/runtime/bin/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { defineCommand, runMain } from 'citty' 3 | import { generateComposables } from '../utils/export' 4 | 5 | const main = defineCommand({ 6 | meta: { 7 | name: 'generate-composables', 8 | version: '1.0.0', 9 | description: 'A tool to generate composables', 10 | }, 11 | args: { 12 | dir: { 13 | type: 'positional', 14 | description: 'Path to the base directory', 15 | required: false 16 | }, 17 | }, 18 | async run({ args }) { 19 | await generateComposables(args).catch((error) => { 20 | console.error('Error while generating composables:', error) 21 | process.exit(1) 22 | }) 23 | }, 24 | }) 25 | 26 | runMain(main) 27 | -------------------------------------------------------------------------------- /apps/nuxt-example/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt 3 Minimal Starter 2 | 3 | Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install the dependencies: 8 | 9 | ```bash 10 | # yarn 11 | yarn install 12 | 13 | # npm 14 | npm install 15 | 16 | # pnpm 17 | pnpm install --shamefully-hoist 18 | ``` 19 | 20 | ## Development Server 21 | 22 | Start the development server on http://localhost:3000 23 | 24 | ```bash 25 | npm run dev 26 | ``` 27 | 28 | ## Production 29 | 30 | Build the application for production: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | Locally preview production build: 37 | 38 | ```bash 39 | npm run preview 40 | ``` 41 | 42 | Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information. 43 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "isolatedModules": true, 9 | "strict": true, 10 | "jsx": "preserve", 11 | "sourceMap": true, 12 | "resolveJsonModule": true, 13 | "strictNullChecks": false, 14 | "noImplicitAny": false, 15 | "esModuleInterop": true, 16 | "paths": { 17 | "@core/*": ["*"] 18 | }, 19 | "lib": ["esnext", "dom"], 20 | "skipLibCheck": true, 21 | }, 22 | "include": ["env.d.ts", "vite.config.*", "src/*.d.ts", "src/**/*.ts"], 23 | "exclude": ["dist", "node_modules"], 24 | "references": [ 25 | { 26 | "path": "./tsconfig.node.json" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /packages/nuxt/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode/* 38 | !.vscode/settings.json 39 | !.vscode/tasks.json 40 | !.vscode/launch.json 41 | !.vscode/extensions.json 42 | !.vscode/*.code-snippets 43 | 44 | # Intellij idea 45 | *.iml 46 | .idea 47 | 48 | # OSX 49 | .DS_Store 50 | .AppleDouble 51 | .LSOverride 52 | .AppleDB 53 | .AppleDesktop 54 | Network Trash Folder 55 | Temporary Items 56 | .apdisk 57 | -------------------------------------------------------------------------------- /packages/core/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | import { promises as fsp } from "node:fs"; 3 | import { join } from "path"; 4 | 5 | export default defineBuildConfig({ 6 | entries: [ 7 | 'src/index.ts', 8 | 'src/node.ts', 9 | { input: 'src/runtime/utils/', outDir: 'dist/runtime/utils', ext: 'mjs' }, 10 | { input: 'src/runtime/bin/', outDir: 'dist/runtime/bin', ext: 'mjs' }, 11 | ], 12 | // Generates .d.ts declaration file 13 | declaration: true, 14 | failOnWarn: false, 15 | dependencies: [ 16 | 'fast-glob', 17 | ], 18 | externals: [ 19 | 'ofetch', 20 | 'vue' 21 | ], 22 | hooks: { 23 | "build:done": async () => { 24 | await fsp.chmod(join(__dirname, 'dist/runtime/utils/cli.mjs'), 0o755 /* rwx r-x r-x */).catch(() => {}); 25 | } 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /apps/vue-example/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 41 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "dev": { 5 | "dependsOn": ["^dev"], 6 | "cache": false 7 | }, 8 | "build": { 9 | "dependsOn": ["^build"], 10 | "outputs": ["dist/**", ".nuxt/**", ".output/**"], 11 | "cache": false 12 | }, 13 | "postinstall": { 14 | "dependsOn": ["dev:prepare"], 15 | "cache": false 16 | }, 17 | "generate": { 18 | "dependsOn": ["^build"], 19 | "outputs": ["dist/**", ".nuxt/**", ".output/**"], 20 | "cache": false 21 | }, 22 | "lint": { 23 | "outputs": [] 24 | }, 25 | "test": { 26 | "cache": false 27 | }, 28 | "dev:prepare": { 29 | "cache": false 30 | }, 31 | "deploy": { 32 | "dependsOn": ["test", "build"], 33 | "outputs": [ 34 | "dist/**" 35 | ] 36 | }, 37 | "changelog": { 38 | "cache": false 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/vue-example/api/users/index.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | id: String; 3 | name: String; 4 | to?: String; 5 | } 6 | 7 | export default function () { 8 | const $fetch = useOfetchModel({ 9 | baseURL: 'https://64cbdfbd2eafdcdc85196e4c.mockapi.io/users' 10 | }) 11 | 12 | const USER_FIELD = ['id', 'name'] 13 | 14 | const USER_FIELDS: typeof Field[] = [...USER_FIELD] 15 | 16 | return { 17 | findOne: async (userId: string, options?: typeof IRequestOptions>) => { 18 | return $fetch.get(userId, { 19 | ...options, 20 | transform: { 21 | fields: USER_FIELD, 22 | context: {} 23 | } 24 | }) 25 | }, 26 | get: async (options?: typeof IRequestOptions>) => { 27 | return $fetch.get({ 28 | ...options, 29 | transform: { 30 | fields: USER_FIELDS, 31 | context: {} 32 | } 33 | }) 34 | }, 35 | } 36 | } -------------------------------------------------------------------------------- /apps/vue-example/src/components/icons/IconTooling.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /apps/vue-example/src/components/icons/IconCommunity.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /packages/vue/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 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. 22 | -------------------------------------------------------------------------------- /apps/vue-example/src/components/icons/IconDocumentation.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /packages/vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-api/vue", 3 | "version": "1.0.19", 4 | "description": "Vue plugin of vue-api module", 5 | "repository": "gaetansenn/vue-api", 6 | "license": "MIT", 7 | "sideEffects": false, 8 | "type": "module", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.mjs", 13 | "require": "./dist/index.cjs" 14 | } 15 | }, 16 | "main": "./dist/index.cjs", 17 | "module": "./dist/index.mjs", 18 | "types": "./dist/index.d.ts", 19 | "files": [ 20 | "dist" 21 | ], 22 | "scripts": { 23 | "build": "unbuild", 24 | "dev": "unbuild --stub", 25 | "lint": "eslint .", 26 | "lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test -w", 27 | "prepack": "pnpm run build", 28 | "release": "pnpm test && changelogen --release && npm publish && git push --follow-tags" 29 | }, 30 | "devDependencies": { 31 | "@vitest/coverage-v8": "^2.1.0", 32 | "changelogen": "^0.5.5", 33 | "eslint": "^9.8.0", 34 | "eslint-config-unjs": "^0.3.2", 35 | "typescript": "^5.6.2", 36 | "unbuild": "^2.0.0", 37 | "vitest": "^2.1.0", 38 | "vite": "^5.4.5", 39 | "unplugin-auto-import": "^0.18.2" 40 | }, 41 | "dependencies": { 42 | "@vue-api/core": "workspace:*" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/vue-example/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const FetchOptions: typeof import('@vue-api/core')['FetchOptions'] 10 | const Field: typeof import('@vue-api/core')['Field'] 11 | const IHttpModel: typeof import('@vue-api/core')['IHttpModel'] 12 | const IRequestOptions: typeof import('@vue-api/core')['IRequestOptions'] 13 | const useApiAuth: typeof import('./api/_composables_/index')['useApiAuth'] 14 | const useApiCmsBlogsArticle: typeof import('./api/_composables_/index')['useApiCmsBlogsArticle'] 15 | const useApiCmsBlogsIndex: typeof import('./api/_composables_/index')['useApiCmsBlogsIndex'] 16 | const useApiUsers: typeof import('./api/_composables_/index')['useApiUsers'] 17 | const useAuth: typeof import('./api/_composables_/index')['useAuth'] 18 | const useCmsBlogsArticle: typeof import('./api/_composables_/index')['useCmsBlogsArticle'] 19 | const useCmsBlogsIndex: typeof import('./api/_composables_/index')['useCmsBlogsIndex'] 20 | const useOfetchModel: typeof import('@vue-api/core')['useOfetchModel'] 21 | const useTransform: typeof import('@vue-api/core')['useTransform'] 22 | const useUsers: typeof import('./api/_composables_/index')['useUsers'] 23 | } 24 | -------------------------------------------------------------------------------- /apps/vue-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-example", 3 | "version": "1.0.20", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "run-p type-check \"build-only {@}\" --", 9 | "preview": "vite preview", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --build --force", 12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 13 | "changelog": "standard-version" 14 | }, 15 | "dependencies": { 16 | "@vue-api/vue": "workspace:*", 17 | "unplugin-auto-import": "^0.18.2", 18 | "vue": "^3.4.29", 19 | "vue-router": "^4.3.3" 20 | }, 21 | "devDependencies": { 22 | "@rushstack/eslint-patch": "^1.8.0", 23 | "@tsconfig/node20": "^20.1.4", 24 | "@types/node": "^20.14.5", 25 | "@vitejs/plugin-vue": "^5.0.5", 26 | "@vitejs/plugin-vue-jsx": "^4.0.0", 27 | "@vue/eslint-config-typescript": "^13.0.0", 28 | "@vue/tsconfig": "^0.5.1", 29 | "eslint": "^8.57.0", 30 | "eslint-plugin-vue": "^9.23.0", 31 | "npm-run-all2": "^6.2.0", 32 | "typescript": "~5.4.0", 33 | "vite": "^5.3.1", 34 | "vite-plugin-vue-devtools": "^7.3.1", 35 | "vue-tsc": "^2.0.21" 36 | }, 37 | "standard-version": { 38 | "skip": { 39 | "bump": true, 40 | "commit": true, 41 | "tag": true 42 | }, 43 | "tag-prefix": "vue-example@" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/runtime/providers/http/index.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from '../../utils/context'; 2 | import type { Field, ITransformOptions } from '../../utils/transform' 3 | 4 | export interface IHttpModel { 5 | get(urlOrOptions?: string | IRequestOptions>, options?: IRequestOptions>): Promise; 6 | post(urlOrOptions?: string | IRequestOptions, options?: IRequestOptions): Promise; 7 | put(urlOrOptions?: string | IRequestOptions, options?: IRequestOptions): Promise; 8 | patch(urlOrOptions?: string | IRequestOptions, options?: IRequestOptions): Promise; 9 | delete(urlOrOptions?: string | IRequestOptions>, options?: IRequestOptions>): Promise; 10 | head(urlOrOptions?: string | IRequestOptions>, options?: IRequestOptions>): Promise; 11 | } 12 | 13 | export type handleRequestFunction = (urlOrOptions?: string | IRequestOptions, options?: IRequestOptions & { method: methodType }) => Promise; 14 | 15 | export type methodType = 'get' | 'post' | 'patch' | 'put' | 'delete' | 'head' 16 | 17 | export interface ITransformRequestOptions extends ITransformOptions { 18 | fields: (Field | string )[] 19 | } 20 | 21 | export interface IRequestOptions { 22 | transform?: ITransformRequestOptions; 23 | context?: IContext; 24 | options?: T 25 | } 26 | 27 | export { useOfetchModel } from './ofetch' -------------------------------------------------------------------------------- /apps/docs/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: Vue API 7 | text: A powerful and flexible library for managing API calls 8 | tagline: Streamline your API management in Vue 3 / Nuxt 3 9 | actions: 10 | - theme: brand 11 | text: Get Started 12 | link: /guide/getting-started/installation 13 | - theme: alt 14 | text: API Examples 15 | link: /examples 16 | 17 | features: 18 | - title: Organized API Management 19 | details: Structure your API calls within directories for clear organization and reuse across your application. 20 | 21 | - title: Automatic Composable Generation 22 | details: Generate composables based on your directory structure, with intuitive naming for easy usage across your project. 23 | 24 | - title: Advanced Data Mapping 25 | details: Transform and optimize API responses to fit your front-end needs using powerful mapping functions. 26 | 27 | - title: Framework Agnostic 28 | details: Core functionality works with Vue 3, with additional modules for seamless integration with Vue and Nuxt 3. 29 | 30 | - title: SSR Compatible 31 | details: Designed to work with server-side rendering, including Nuxt 3's useFetch for hydration. 32 | 33 | - title: Flexible Provider Support 34 | details: Currently supporting `ofetch`, with plans to expand to `axios`, `GraphQL`, and more in the future. 35 | --- 36 | -------------------------------------------------------------------------------- /packages/vue/src/runtime/plugin.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import type { Plugin } from 'vite' 3 | import { generateComposables } from '@vue-api/core/node' 4 | import AutoImport from 'unplugin-auto-import/vite' 5 | 6 | interface VueApiPluginOptions { 7 | apiPath?: string; 8 | ignorePatterns?: string[]; 9 | ignorePrefixes?: string[]; 10 | } 11 | 12 | export default async function vueApiPlugin(options: VueApiPluginOptions = {}): Promise { 13 | const { 14 | apiPath = 'api', 15 | ignorePatterns = [], 16 | ignorePrefixes = ['_'] 17 | } = options; 18 | 19 | return { 20 | name: 'vue-api-plugin', 21 | enforce: 'pre', 22 | ...AutoImport({ 23 | dirs: [ 24 | `${apiPath}/_composables_`, 25 | ], 26 | imports: [ 27 | { 28 | '@vue-api/core': [ 29 | 'useOfetchModel', 30 | 'useTransform' 31 | ], 32 | }, 33 | { 34 | from: '@vue-api/core', 35 | imports: ['FetchOptions', 'IHttpModel', 'IRequestOptions', 'Field'] 36 | } 37 | ] 38 | }), 39 | async config(config) { 40 | const rootDir = config.root || process.cwd(); 41 | const apiDirectoryPath = path.resolve(rootDir, apiPath); 42 | 43 | // Generate the composables 44 | await generateComposables({ 45 | dir: apiDirectoryPath, 46 | ignorePatterns, 47 | ignorePrefixes 48 | }); 49 | } 50 | }; 51 | } -------------------------------------------------------------------------------- /apps/vue-example/src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 76 | -------------------------------------------------------------------------------- /packages/nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-api/nuxt", 3 | "version": "2.0.9", 4 | "description": "Nuxt plugin of vue-api module", 5 | "repository": "gaetansenn/vue-api", 6 | "license": "MIT", 7 | "type": "module", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/types.d.ts", 11 | "import": "./dist/module.mjs", 12 | "require": "./dist/module.cjs" 13 | } 14 | }, 15 | "main": "./dist/module.cjs", 16 | "types": "./dist/types.d.ts", 17 | "files": [ 18 | "dist" 19 | ], 20 | "scripts": { 21 | "prepack": "nuxt prepare && nuxt-module-build build", 22 | "build": "nuxt-module-build build", 23 | "dev": "nuxt-module-build --stub", 24 | "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare", 25 | "release": "npm run lint && npm run prepack && changelogen --release && npm publish && git push --follow-tags", 26 | "lint": "eslint .", 27 | "test:watch": "vitest watch", 28 | "test:types": "vue-tsc --noEmit" 29 | }, 30 | "dependencies": { 31 | "@nuxt/kit": "^3.12.4", 32 | "@vue-api/core": "workspace:*" 33 | }, 34 | "devDependencies": { 35 | "@nuxt/devtools": "^1.3.9", 36 | "@nuxt/eslint-config": "^0.3.13", 37 | "@nuxt/module-builder": "^0.8.1", 38 | "@nuxt/schema": "^3.12.4", 39 | "@nuxt/test-utils": "^3.14.1", 40 | "@types/node": "^20.14.11", 41 | "changelogen": "^0.5.5", 42 | "eslint": "^9.13.0", 43 | "nuxt": "^3.12.4", 44 | "typescript": "latest", 45 | "vitest": "^2.0.3", 46 | "vue-tsc": "^2.0.26" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-api/core", 3 | "version": "2.0.1", 4 | "description": "Vue-api core module", 5 | "repository": "gaetansenn/vue-api", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "./dist/index.mjs", 9 | "types": "./dist/index.d.ts", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.mjs", 13 | "types": "./dist/index.d.ts" 14 | }, 15 | "./node": { 16 | "import": "./dist/node.mjs", 17 | "types": "./dist/node.d.ts" 18 | } 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "bin": { 24 | "vue-api": "./dist/runtime/bin/cli.mjs" 25 | }, 26 | "devDependencies": { 27 | "@vitest/coverage-c8": "^0.23.4", 28 | "@vue/test-utils": "^2.0.2", 29 | "c8": "^7.12.0", 30 | "citty": "^0.1.2", 31 | "jsdom": "^20.0.0", 32 | "typescript": "latest", 33 | "unbuild": "^2.0.0", 34 | "vitest": "^0.28.5", 35 | "vue": "^3.5.5", 36 | "fast-glob": "^3.3.2" 37 | }, 38 | "scripts": { 39 | "prepare": "unbuild", 40 | "dev": "unbuild", 41 | "build": "unbuild", 42 | "changelog": "standard-version", 43 | "test": "vitest run" 44 | }, 45 | "dependencies": { 46 | "@vueuse/core": "^9.10.0" 47 | }, 48 | "standard-version": { 49 | "skip": { 50 | "bump": true, 51 | "commit": true, 52 | "tag": true 53 | }, 54 | "tag-prefix": "@vunix/core@" 55 | }, 56 | "peerDependencies": { 57 | "ofetch": "^1.1.1" 58 | }, 59 | "peerDependenciesMeta": { 60 | "ofetch": { 61 | "optional": true 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /apps/nuxt-example/pages/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 37 | -------------------------------------------------------------------------------- /apps/docs/docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: "Vue API", 6 | description: "Organized API management for Vue 3 and Nuxt 3", 7 | themeConfig: { 8 | socialLinks: [ 9 | { icon: 'github', link: 'https://github.com/gaetansenn/vue-api' }, 10 | ], 11 | nav: [ 12 | { text: 'Home', link: '/' }, 13 | { text: 'Guide', link: '/guide/getting-started/installation', activeMatch: '/guide/' }, 14 | { text: 'API', items: [ 15 | { 16 | text: 'Core', 17 | link: '/api/core', 18 | }, 19 | { 20 | text: 'Nuxt', 21 | link: '/api/nuxt' 22 | }, 23 | ], }, 24 | { text: 'Playground', link: '/examples' }, 25 | ], 26 | sidebar: { 27 | '/guide/': { 28 | base: '/guide/', 29 | items: [ 30 | { 31 | text: 'Getting Started', 32 | base: '/guide/getting-started/', 33 | items: [ 34 | { text: 'Introduction', link: 'introduction' }, 35 | { text: 'Installation', link: 'installation' }, 36 | ], 37 | // collapsed: true 38 | }, 39 | { 40 | text: 'Composables', 41 | base: '/guide/composables/', 42 | items: [ 43 | { text: 'Introduction', link: 'introduction' }, 44 | { text: 'Structuring API Functions', link: 'structuring-api-functions' }, 45 | { text: 'Mapping data', link: 'mapping'} 46 | ], 47 | // collapsed: true 48 | } 49 | ] 50 | } 51 | } 52 | } 53 | }) 54 | -------------------------------------------------------------------------------- /packages/core/src/runtime/utils/index.ts: -------------------------------------------------------------------------------- 1 | export function get(obj: any, path: string | string[], defaultValue?: any): any { 2 | const travel = (regexp: RegExp) => 3 | String.prototype.split 4 | .call(path, regexp) 5 | .filter(Boolean) 6 | .reduce((res, key) => (res !== null && res !== undefined ? res[key] : res), obj); 7 | const result = travel(/[,[\]]+?/) || travel(/[,[\].]+?/); 8 | return result === undefined || result === obj ? defaultValue : result; 9 | } 10 | 11 | export function set(obj: any, path: string, value: any): void { 12 | const keys = path.split('.'); 13 | let current = obj; 14 | for (let i = 0; i < keys.length - 1; i++) { 15 | if (current[keys[i]] === undefined) { 16 | current[keys[i]] = {}; 17 | } 18 | current = current[keys[i]]; 19 | } 20 | current[keys[keys.length - 1]] = value; 21 | } 22 | 23 | export function upperFirst(str: string): string { 24 | return str.charAt(0).toUpperCase() + str.slice(1); 25 | } 26 | 27 | export function camelCase(str: string): string { 28 | return str 29 | .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => 30 | index === 0 ? word.toLowerCase() : word.toUpperCase() 31 | ) 32 | .replace(/\s+/g, '') 33 | .replace(/[-_]+/g, ''); 34 | } 35 | 36 | export function isEmpty(value: any): boolean { 37 | if (value == null) { 38 | return true; 39 | } 40 | if (Array.isArray(value) || typeof value === 'string') { 41 | return value.length === 0; 42 | } 43 | if (typeof value === 'object') { 44 | return Object.keys(value).length === 0; 45 | } 46 | return false; 47 | } 48 | 49 | export function isObject(value: any): boolean { 50 | return value !== null && typeof value === 'object' && !Array.isArray(value); 51 | } -------------------------------------------------------------------------------- /apps/vue-example/src/components/WelcomeItem.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 87 | -------------------------------------------------------------------------------- /apps/vue-example/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # vue-example 2 | 3 | ## 1.0.20 4 | 5 | ### Patch Changes 6 | 7 | - @vue-api/vue@1.0.19 8 | 9 | ## 1.0.19 10 | 11 | ### Patch Changes 12 | 13 | - @vue-api/vue@1.0.18 14 | 15 | ## 1.0.18 16 | 17 | ### Patch Changes 18 | 19 | - @vue-api/vue@1.0.17 20 | 21 | ## 1.0.17 22 | 23 | ### Patch Changes 24 | 25 | - @vue-api/vue@1.0.16 26 | 27 | ## 1.0.16 28 | 29 | ### Patch Changes 30 | 31 | - @vue-api/vue@1.0.15 32 | 33 | ## 1.0.15 34 | 35 | ### Patch Changes 36 | 37 | - @vue-api/vue@1.0.14 38 | 39 | ## 1.0.14 40 | 41 | ### Patch Changes 42 | 43 | - @vue-api/vue@1.0.13 44 | 45 | ## 1.0.13 46 | 47 | ### Patch Changes 48 | 49 | - @vue-api/vue@1.0.12 50 | 51 | ## 1.0.12 52 | 53 | ### Patch Changes 54 | 55 | - @vue-api/vue@1.0.11 56 | 57 | ## 1.0.11 58 | 59 | ### Patch Changes 60 | 61 | - @vue-api/vue@1.0.10 62 | 63 | ## 1.0.10 64 | 65 | ### Patch Changes 66 | 67 | - @vue-api/vue@1.0.9 68 | 69 | ## 1.0.9 70 | 71 | ### Patch Changes 72 | 73 | - @vue-api/vue@1.0.8 74 | 75 | ## 1.0.8 76 | 77 | ### Patch Changes 78 | 79 | - @vue-api/vue@1.0.7 80 | 81 | ## 1.0.7 82 | 83 | ### Patch Changes 84 | 85 | - @vue-api/vue@1.0.6 86 | 87 | ## 1.0.6 88 | 89 | ### Patch Changes 90 | 91 | - @vue-api/vue@1.0.5 92 | 93 | ## 1.0.5 94 | 95 | ### Patch Changes 96 | 97 | - @vue-api/vue@1.0.4 98 | 99 | ## 1.0.4 100 | 101 | ### Patch Changes 102 | 103 | - @vue-api/vue@1.0.3 104 | 105 | ## 1.0.3 106 | 107 | ### Patch Changes 108 | 109 | - Updated dependencies 110 | - @vue-api/vue@1.0.3 111 | 112 | ## 1.0.2 113 | 114 | ### Patch Changes 115 | 116 | - Updated dependencies 117 | - @vue-api/vue@1.0.2 118 | 119 | ## 1.0.1 120 | 121 | ### Patch Changes 122 | 123 | - @vue-api/vue@1.0.1 124 | -------------------------------------------------------------------------------- /apps/vue-example/README.md: -------------------------------------------------------------------------------- 1 | # vue-project 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types. 12 | 13 | If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | 42 | ### Lint with [ESLint](https://eslint.org/) 43 | 44 | ```sh 45 | npm run lint 46 | ``` 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-api", 3 | "version": "1.0.0", 4 | "description": "A flexible and provider-agnostic API handling library for Vue 3 and Nuxt 3. Supports multiple data providers like axios, ofetch, and GraphQL, and includes a robust model mapping feature.", 5 | "main": "index.js", 6 | "repository": "git@github.com:gaetansenn/vue-api.git", 7 | "author": "Gaetan SENN ", 8 | "license": "MIT", 9 | "private": true, 10 | "workspaces": [ 11 | "apps/*", 12 | "packages/*" 13 | ], 14 | "scripts": { 15 | "dev": "turbo dev", 16 | "build": "turbo build --filter=!playground", 17 | "dev:docs": "turbo dev --filter=docs", 18 | "build:nuxt-example": "turbo build --filter=nuxt-example", 19 | "build:vue-example": "turbo build --filter=vue-example", 20 | "build:docs": "turbo build --filter=docs", 21 | "dev:vue-example": "turbo dev --filter=vue-example", 22 | "dev:nuxt-example": "turbo dev --filter=nuxt-example", 23 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 24 | "test": "turbo test", 25 | "test:core": "turbo test --filter=@vue-api/core", 26 | "dev:prepare": "turbo dev:prepare", 27 | "deploy": "turbo deploy", 28 | "changeset": "changeset", 29 | "bump": "changeset version", 30 | "release": "turbo build --filter='./packages/*' lint test && changeset version && changeset publish", 31 | "tag": "changeset tag", 32 | "removechangelogs": "find . -path ./node_modules -prune -o -name CHANGELOG.md -print | xargs rm" 33 | }, 34 | "devDependencies": { 35 | "@changesets/changelog-git": "^0.2.0", 36 | "@changesets/cli": "^2.26.2", 37 | "@commitlint/cli": "^17.2.0", 38 | "@commitlint/config-conventional": "^17.2.0", 39 | "husky": "^8.0.2", 40 | "prettier": "^3.2.5", 41 | "standard-version": "^9.5.0", 42 | "turbo": "^2.1.2", 43 | "typescript": "^5.4.5" 44 | }, 45 | "packageManager": "pnpm@8.15.6", 46 | "engines": { 47 | "node": ">=18" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/vue-example/src/components/icons/IconEcosystem.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue API 2 | 3 | This is the official Vue API repository. 4 | 5 | ## What is Vue API 6 | 7 | Vue API is a powerful and flexible library designed to streamline API management in Vue 3 and Nuxt 3 applications. It provides an organized approach to handling API calls, making your codebase more maintainable and efficient. 8 | 9 | The core is built on top of the Vue.js framework and is compatible with both Vue 3 and Nuxt 3. 10 | 11 | * 📄 [Documentation](https://vue-api.dewib.com) 12 | 13 | ## Key Features 14 | 15 | - **Organized API Management**: Structure your API calls within directories for clear organization and reuse across your application. 16 | - **Automatic Composable Generation**: Generate composables based on your directory structure, with intuitive naming for easy usage across your project. 17 | - **Advanced Data Mapping**: Transform and optimize API responses to fit your front-end needs using powerful mapping functions. 18 | - **Framework Agnostic**: Core functionality works with Vue 3, with additional modules for seamless integration with Vue and Nuxt 3. 19 | - **SSR Compatible**: Designed to work with server-side rendering, including Nuxt 3's useAsyncData for hydration. 20 | - **Flexible Provider Support**: Currently supporting `ofetch`, with plans to expand to `axios`, `GraphQL`, and more in the future. 21 | 22 | ## What's inside? 23 | 24 | This repository uses pnpm as a package manager and turbo as a build system. It includes the following packages/apps: 25 | 26 | ### Apps and Packages 27 | 28 | * `docs`: a VitePress app for documentation 29 | * `nuxt-example`: a Nuxt.js example of usage and installation 30 | * `vue-example`: a Vue.js example of usage and installation 31 | * `@vue-api/core`: the core Vue API library used by all applications 32 | * `@vue-api/vue`: a Vue.js compatible module that bundles the core 33 | * `@vue-api/nuxt`: a Nuxt compatible module that bundles the core 34 | 35 | Each package/app is 100% TypeScript. 36 | 37 | ## Release / Publish 38 | 39 | We use [Changesets](https://github.com/changesets/changesets) for managing releases and publishing. 40 | 41 | ## License 42 | 43 | This project is licensed under the MIT License. 44 | -------------------------------------------------------------------------------- /packages/vue/README.md: -------------------------------------------------------------------------------- 1 | # packageName 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![Codecov][codecov-src]][codecov-href] 7 | [![License][license-src]][license-href] 8 | [![JSDocs][jsdocs-src]][jsdocs-href] 9 | 10 | This is my package description. 11 | 12 | ## Usage 13 | 14 | Install package: 15 | 16 | ```sh 17 | # npm 18 | npm install packageName 19 | 20 | # yarn 21 | yarn add packageName 22 | 23 | # pnpm 24 | pnpm install packageName 25 | ``` 26 | 27 | Import: 28 | 29 | ```js 30 | // ESM 31 | import {} from "packageName"; 32 | 33 | // CommonJS 34 | const {} = require("packageName"); 35 | ``` 36 | 37 | ## Development 38 | 39 | - Clone this repository 40 | - Install latest LTS version of [Node.js](https://nodejs.org/en/) 41 | - Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable` 42 | - Install dependencies using `pnpm install` 43 | - Run interactive tests using `pnpm dev` 44 | 45 | ## License 46 | 47 | Made with 💛 48 | 49 | Published under [MIT License](./LICENSE). 50 | 51 | 52 | 53 | [npm-version-src]: https://img.shields.io/npm/v/packageName?style=flat&colorA=18181B&colorB=F0DB4F 54 | [npm-version-href]: https://npmjs.com/package/packageName 55 | [npm-downloads-src]: https://img.shields.io/npm/dm/packageName?style=flat&colorA=18181B&colorB=F0DB4F 56 | [npm-downloads-href]: https://npmjs.com/package/packageName 57 | [codecov-src]: https://img.shields.io/codecov/c/gh/unjs/packageName/main?style=flat&colorA=18181B&colorB=F0DB4F 58 | [codecov-href]: https://codecov.io/gh/unjs/packageName 59 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/packageName?style=flat&colorA=18181B&colorB=F0DB4F 60 | [bundle-href]: https://bundlephobia.com/result?p=packageName 61 | [license-src]: https://img.shields.io/github/license/unjs/packageName.svg?style=flat&colorA=18181B&colorB=F0DB4F 62 | [license-href]: https://github.com/unjs/packageName/blob/main/LICENSE 63 | [jsdocs-src]: https://img.shields.io/badge/jsDocs.io-reference-18181B?style=flat&colorA=18181B&colorB=F0DB4F 64 | [jsdocs-href]: https://www.jsdocs.io/package/packageName 65 | -------------------------------------------------------------------------------- /apps/docs/docs/api/core.md: -------------------------------------------------------------------------------- 1 | # Core API Reference 2 | 3 | ## `useOfetchModel` 4 | 5 | A composable for fetching and managing data models using `ofetch`. 6 | 7 | ### Usage 8 | 9 | ```typescript 10 | import { useOfetchModel } from '@vue-api/core' 11 | 12 | const $fetch = useOfetchModel({ 13 | baseURL: 'https://api.example.com', 14 | headers: { 15 | Authorization: 'Bearer token' 16 | } 17 | }) 18 | 19 | // Basic usage 20 | const user = await $fetch.get('/users/1') 21 | 22 | // With transform options 23 | const users = await $fetch.get('/users', { 24 | transform: { 25 | fields: ['id', 'name', 'email'] 26 | } 27 | }) 28 | ``` 29 | 30 | ### Parameters 31 | 32 | #### Options 33 | 34 | ```typescript 35 | interface FetchOptions { 36 | baseURL?: string 37 | headers?: Record 38 | context?: IContext 39 | } 40 | ``` 41 | 42 | - `baseURL`: The base URL for all requests 43 | - `headers`: Default headers to be sent with every request 44 | - `context`: Context object that can be accessed in transform functions 45 | 46 | ### Transform Options 47 | 48 | ```typescript 49 | interface TransformOptions { 50 | fields: (Field | string)[] 51 | scope?: string 52 | format?: 'camelCase' 53 | context?: IContext 54 | } 55 | ``` 56 | 57 | ### Available Methods 58 | 59 | The `$fetch` instance provides standard HTTP methods: 60 | - `get(url, options?)` 61 | - `post(url, options?)` 62 | - `put(url, options?)` 63 | - `patch(url, options?)` 64 | - `delete(url, options?)` 65 | - `head(url, options?)` 66 | 67 | ### Example with Advanced Transform 68 | 69 | ```typescript 70 | const users = await $fetch.get('/users', { 71 | transform: { 72 | fields: [ 73 | '*', // Include all fields 74 | { 75 | key: 'fullName', 76 | mapping: ({ model }) => `${model.firstName} ${model.lastName}` 77 | }, 78 | { 79 | key: 'projects.*.status', 80 | fields: ['id', 'name', 'progress'] 81 | } 82 | ], 83 | format: 'camelCase' 84 | } 85 | }) 86 | ``` 87 | 88 | ::: tip 89 | For Nuxt applications, consider using `@vue-api/nuxt` which provides additional SSR capabilities through `useFetchModel`. 90 | ::: 91 | -------------------------------------------------------------------------------- /packages/nuxt/src/module.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtModule, createResolver, addImportsDir, resolvePath } from '@nuxt/kit' 2 | import { generateComposables } from '@vue-api/core/node' 3 | import { name, version } from '../package.json' 4 | 5 | // Module options TypeScript interface definition 6 | export interface ModuleOptions { 7 | rootPath: string 8 | ignorePatterns: string[] 9 | ignorePrefixes: string[] 10 | } 11 | 12 | export type { 13 | Field, 14 | ITransformOptions, 15 | IRequestOptions, 16 | ITransformRequestOptions, 17 | IContext, 18 | } from '@vue-api/core' 19 | 20 | 21 | export default defineNuxtModule({ 22 | meta: { 23 | name, 24 | version, 25 | configKey: 'vueApi', 26 | }, 27 | // Default configuration options of the Nuxt module 28 | defaults: { 29 | rootPath: 'api', 30 | ignorePatterns: [], 31 | ignorePrefixes: ['_'], 32 | }, 33 | async setup(options, nuxt) { 34 | const { resolve } = createResolver(import.meta.url) 35 | 36 | // Create resolver to resolve dist paths within @vunix/core 37 | const core = createResolver(await resolvePath('@vue-api/core', { cwd: import.meta.url })) 38 | 39 | // Generate composables on build 40 | nuxt.hook('ready', async () => { 41 | const rootDir = nuxt.options.srcDir 42 | const rootDirectoryPath = resolve(rootDir, options.rootPath) 43 | 44 | await generateComposables({ 45 | dir: rootDirectoryPath, 46 | ignorePatterns: options.ignorePatterns, 47 | ignorePrefixes: options.ignorePrefixes, 48 | }) 49 | }) 50 | 51 | // Add composables directory to auto-imports 52 | addImportsDir(resolve(nuxt.options.srcDir, options.rootPath, '_composables_')) 53 | // Add useFetchModel composable 54 | addImportsDir(resolve(__dirname, 'runtime/composables')) 55 | 56 | // Transpile @vue-api/core 57 | const coreRootPath = core.resolve('..') // root dist directory 58 | nuxt.options.build.transpile.push(coreRootPath) 59 | nuxt.options.alias['@vue-api/core'] = coreRootPath 60 | 61 | // Add types 62 | nuxt.hook('prepare:types', ({ references }) => { 63 | references.push({ types: '@vue-api/core' }) 64 | }) 65 | }, 66 | }) 67 | -------------------------------------------------------------------------------- /apps/docs/docs/guide/composables/structuring-api-functions.md: -------------------------------------------------------------------------------- 1 | ## Structuring API Functions 2 | 3 | When creating functions within the `api` folder, it's important to understand how to use the Vue API module correctly. This guide will walk you through the process of structuring your API functions. 4 | 5 | ## Choosing a Provider 6 | 7 | ### Vue.js (@vue-api/core) 8 | In Vue.js applications, you can choose different providers to handle HTTP requests. Currently, the main provider is `ofetch`, with plans to support other providers like Axios or GraphQL in the future. 9 | 10 | ```typescript 11 | // api/users/index.ts 12 | export default function () { 13 | const $fetch = useOfetchModel({ 14 | baseURL: 'https://api.example.com/users' 15 | }) 16 | 17 | return { 18 | get: () => $fetch.get('/users') 19 | } 20 | } 21 | ``` 22 | 23 | ### Nuxt (@vue-api/nuxt) 24 | For Nuxt applications, we recommend using the built-in `useFetchModel` composable which provides both `$fetch` and `useFetch` methods: 25 | 26 | ```typescript 27 | // api/users/index.ts 28 | export default function () { 29 | const { $fetch, useFetch } = useFetchModel({ 30 | baseURL: 'https://api.example.com/users' 31 | }) 32 | 33 | return { 34 | // For client-side operations 35 | create: (userData) => $fetch.post('/users', { body: userData }), 36 | // For SSR/hydration 37 | list: () => useFetch.get('/users') 38 | } 39 | } 40 | ``` 41 | 42 | ::: warning 43 | The main difference between Vue and Nuxt implementations is that Nuxt's `useFetchModel` returns an object with both `$fetch` and `useFetch` methods, while Vue's `useOfetchModel` returns `$fetch` directly. 44 | ::: 45 | 46 | ## Example: Basic API Structure 47 | ```typescript 48 | // api/users/index.ts 49 | export default function () { 50 | const { $fetch, useFetch } = useFetchModel({ 51 | baseURL: 'https://api.example.com/users' 52 | }) 53 | 54 | const USER_FIELDS = ['id', 'name', 'email'] 55 | 56 | return { 57 | get: () => useFetch.get({ 58 | transform: { 59 | fields: USER_FIELDS 60 | } 61 | }), 62 | create: (userData) => $fetch.post({ 63 | body: userData, 64 | transform: { 65 | fields: USER_FIELDS 66 | } 67 | }) 68 | } 69 | } 70 | ``` -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.0.1 4 | 5 | ### Patch Changes 6 | 7 | - fix: fix declaration and type / function exposition from core 8 | 9 | ## 2.0.0 10 | 11 | ### Major Changes 12 | 13 | - f42f806: refactoring usage of useFetch / fetch with nuxt 14 | 15 | ## 1.0.17 16 | 17 | ### Patch Changes 18 | 19 | - fix(core:transform): fix default value 20 | 21 | ## 1.0.16 22 | 23 | ### Patch Changes 24 | 25 | - fix: handle model array in useTransform instead of fetch providers 26 | 27 | ## 1.0.15 28 | 29 | ### Patch Changes 30 | 31 | - refactor: remove debug 32 | 33 | ## 1.0.14 34 | 35 | ### Patch Changes 36 | 37 | - fix: add tests and fix some issues 38 | 39 | ## 1.0.13 40 | 41 | ### Patch Changes 42 | 43 | - refactor: clean up code 44 | 45 | ## 1.0.12 46 | 47 | ### Patch Changes 48 | 49 | - fix: handle path with expandWildcardFields 50 | 51 | ## 1.0.11 52 | 53 | ### Patch Changes 54 | 55 | - feat: add context as optional for transform 56 | 57 | ## 1.0.10 58 | 59 | ### Patch Changes 60 | 61 | - fix(core): fix some issues with mapping and improve doc" 62 | 63 | ## 1.0.9 64 | 65 | ### Patch Changes 66 | 67 | - feat: Enhance mapping system and user data management 68 | 69 | ## 1.0.8 70 | 71 | ### Patch Changes 72 | 73 | - fix: fix default value case if mapping 74 | 75 | ## 1.0.7 76 | 77 | ### Patch Changes 78 | 79 | - feat(core): implement automatic scope inheritance for nested fields 80 | 81 | ## 1.0.6 82 | 83 | ### Patch Changes 84 | 85 | - feat(core): Ignore .d.ts files in composable generation and update docs 86 | 87 | ## 1.0.5 88 | 89 | ### Patch Changes 90 | 91 | - fix: fix window glob path composable generation 92 | 93 | ## 1.0.4 94 | 95 | ### Patch Changes 96 | 97 | - fix: fix composable generation path 98 | 99 | ## 1.0.3 100 | 101 | ### Patch Changes 102 | 103 | - fix: handle compatiblity for window 104 | 105 | ## 1.0.2 106 | 107 | ### Patch Changes 108 | 109 | - Path packages to 1.0.2 110 | 111 | ## 1.0.1 112 | 113 | ### Patch Changes 114 | 115 | - remove lodash-es and fix transform issues 116 | 117 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 118 | 119 | ## 1.0.0 120 | 121 | Initial commit 122 | -------------------------------------------------------------------------------- /packages/vue/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vue-api/vue 2 | 3 | ## 1.0.19 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies 8 | - @vue-api/core@2.0.1 9 | 10 | ## 1.0.18 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [f42f806] 15 | - @vue-api/core@2.0.0 16 | 17 | ## 1.0.17 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies 22 | - @vue-api/core@1.0.17 23 | 24 | ## 1.0.16 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies 29 | - @vue-api/core@1.0.16 30 | 31 | ## 1.0.15 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies 36 | - @vue-api/core@1.0.15 37 | 38 | ## 1.0.14 39 | 40 | ### Patch Changes 41 | 42 | - Updated dependencies 43 | - @vue-api/core@1.0.14 44 | 45 | ## 1.0.13 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies 50 | - @vue-api/core@1.0.13 51 | 52 | ## 1.0.12 53 | 54 | ### Patch Changes 55 | 56 | - Updated dependencies 57 | - @vue-api/core@1.0.12 58 | 59 | ## 1.0.11 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies 64 | - @vue-api/core@1.0.11 65 | 66 | ## 1.0.10 67 | 68 | ### Patch Changes 69 | 70 | - Updated dependencies 71 | - @vue-api/core@1.0.10 72 | 73 | ## 1.0.9 74 | 75 | ### Patch Changes 76 | 77 | - Updated dependencies 78 | - @vue-api/core@1.0.9 79 | 80 | ## 1.0.8 81 | 82 | ### Patch Changes 83 | 84 | - Updated dependencies 85 | - @vue-api/core@1.0.8 86 | 87 | ## 1.0.7 88 | 89 | ### Patch Changes 90 | 91 | - Updated dependencies 92 | - @vue-api/core@1.0.7 93 | 94 | ## 1.0.6 95 | 96 | ### Patch Changes 97 | 98 | - Updated dependencies 99 | - @vue-api/core@1.0.6 100 | 101 | ## 1.0.5 102 | 103 | ### Patch Changes 104 | 105 | - Updated dependencies 106 | - @vue-api/core@1.0.5 107 | 108 | ## 1.0.4 109 | 110 | ### Patch Changes 111 | 112 | - Updated dependencies 113 | - @vue-api/core@1.0.4 114 | 115 | ## 1.0.3 116 | 117 | ### Patch Changes 118 | 119 | - Updated dependencies 120 | - @vue-api/core@1.0.3 121 | 122 | ## 1.0.3 123 | 124 | ### Patch Changes 125 | 126 | - build: fix @vue-api/core resolver 127 | 128 | ## 1.0.2 129 | 130 | ### Patch Changes 131 | 132 | - Path packages to 1.0.2 133 | - Updated dependencies 134 | - @vue-api/core@1.0.2 135 | 136 | ## 1.0.1 137 | 138 | ### Patch Changes 139 | 140 | - Updated dependencies 141 | - @vue-api/core@1.0.1 142 | -------------------------------------------------------------------------------- /apps/docs/docs/guide/getting-started/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Vue API is a powerful and flexible library designed to streamline API management in Vue 3 and Nuxt 3 applications. It provides an organized approach to handling API calls, making your codebase more maintainable and efficient. 4 | 5 | ## Key Features 6 | 7 | 1. **Organized API Management**: Vue API allows you to structure your API calls within directories, promoting clear organization and easy reuse across your application. 8 | 9 | 2. **Automatic Composable Generation**: Based on your directory structure, Vue API automatically generates composables with intuitive naming conventions, simplifying usage throughout your project. 10 | 11 | 3. **Advanced Data Mapping**: Transform and optimize API responses to fit your front-end needs using powerful mapping functions, ensuring data consistency and reducing redundant processing. 12 | 13 | 4. **Framework Agnostic**: While the core functionality is designed to work seamlessly with Vue 3, Vue API also offers additional modules for integration with Nuxt 3, making it versatile for different project setups. 14 | 15 | 5. **SSR Compatibility**: Vue API is built with server-side rendering in mind, including support for Nuxt 3's useFetch for smooth hydration. 16 | 17 | ## Implementation Differences 18 | 19 | ### Vue.js (@vue-api/core) 20 | The core package provides a single fetch instance: 21 | 22 | ```typescript 23 | import { useOfetchModel } from '@vue-api/core' 24 | 25 | const $fetch = useOfetchModel({ 26 | baseURL: 'https://api.example.com' 27 | }) 28 | ``` 29 | 30 | ### Nuxt (@vue-api/nuxt) 31 | The Nuxt module provides both client-side and SSR-compatible methods: 32 | 33 | ```typescript 34 | const { $fetch, useFetch } = useFetchModel({ 35 | baseURL: 'https://api.example.com' 36 | }) 37 | ``` 38 | 39 | #### Why Two Methods? 40 | 41 | As explained in the [Nuxt documentation](https://nuxt.com/docs/getting-started/data-fetching#the-need-for-usefetch-and-useasyncdata): 42 | 43 | - Use `$fetch` when: 44 | - Making client-side only requests (like form submissions) 45 | - Handling event-based interactions 46 | - Making requests in store actions or utility functions 47 | 48 | - Use `useFetch` when: 49 | - You need SSR support 50 | - You want to prevent duplicate requests during hydration 51 | - You need automatic data hydration between server and client 52 | - You're fetching data in component setup -------------------------------------------------------------------------------- /apps/vue-example/src/assets/base.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* color palette from */ 6 | :root { 7 | --vt-c-white: #ffffff; 8 | --vt-c-white-soft: #f8f8f8; 9 | --vt-c-white-mute: #f2f2f2; 10 | 11 | --vt-c-black: #181818; 12 | --vt-c-black-soft: #222222; 13 | --vt-c-black-mute: #282828; 14 | 15 | --vt-c-indigo: #2c3e50; 16 | 17 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 18 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 19 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 20 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 21 | 22 | --vt-c-text-light-1: var(--vt-c-indigo); 23 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 24 | --vt-c-text-dark-1: var(--vt-c-white); 25 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 26 | } 27 | 28 | /* semantic color variables for this project */ 29 | :root { 30 | --color-background: var(--vt-c-white); 31 | --color-background-soft: var(--vt-c-white-soft); 32 | --color-background-mute: var(--vt-c-white-mute); 33 | 34 | --color-border: var(--vt-c-divider-light-2); 35 | --color-border-hover: var(--vt-c-divider-light-1); 36 | 37 | --color-heading: var(--vt-c-text-light-1); 38 | --color-text: var(--vt-c-text-light-1); 39 | 40 | --section-gap: 160px; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --color-background: var(--vt-c-black); 46 | --color-background-soft: var(--vt-c-black-soft); 47 | --color-background-mute: var(--vt-c-black-mute); 48 | 49 | --color-border: var(--vt-c-divider-dark-2); 50 | --color-border-hover: var(--vt-c-divider-dark-1); 51 | 52 | --color-heading: var(--vt-c-text-dark-1); 53 | --color-text: var(--vt-c-text-dark-2); 54 | } 55 | } 56 | 57 | *, 58 | *::before, 59 | *::after { 60 | box-sizing: border-box; 61 | margin: 0; 62 | position: relative; 63 | font-weight: normal; 64 | } 65 | 66 | body { 67 | min-height: 100vh; 68 | color: var(--color-text); 69 | background: var(--color-background); 70 | transition: color 0.5s, background-color 0.5s; 71 | line-height: 1.6; 72 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 73 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 74 | font-size: 15px; 75 | text-rendering: optimizeLegibility; 76 | -webkit-font-smoothing: antialiased; 77 | -moz-osx-font-smoothing: grayscale; 78 | } 79 | -------------------------------------------------------------------------------- /packages/nuxt/README.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | # My Module 11 | 12 | [![npm version][npm-version-src]][npm-version-href] 13 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 14 | [![License][license-src]][license-href] 15 | [![Nuxt][nuxt-src]][nuxt-href] 16 | 17 | My new Nuxt module for doing amazing things. 18 | 19 | - [✨  Release Notes](/CHANGELOG.md) 20 | 21 | 22 | 23 | ## Features 24 | 25 | 26 | - ⛰  Foo 27 | - 🚠  Bar 28 | - 🌲  Baz 29 | 30 | ## Quick Setup 31 | 32 | 1. Add `my-module` dependency to your project 33 | 34 | ```bash 35 | # Using pnpm 36 | pnpm add -D my-module 37 | 38 | # Using yarn 39 | yarn add --dev my-module 40 | 41 | # Using npm 42 | npm install --save-dev my-module 43 | ``` 44 | 45 | 2. Add `my-module` to the `modules` section of `nuxt.config.ts` 46 | 47 | ```js 48 | export default defineNuxtConfig({ 49 | modules: [ 50 | 'my-module' 51 | ] 52 | }) 53 | ``` 54 | 55 | That's it! You can now use My Module in your Nuxt app ✨ 56 | 57 | ## Development 58 | 59 | ```bash 60 | # Install dependencies 61 | npm install 62 | 63 | # Generate type stubs 64 | npm run dev:prepare 65 | 66 | # Develop with the playground 67 | npm run dev 68 | 69 | # Build the playground 70 | npm run dev:build 71 | 72 | # Run ESLint 73 | npm run lint 74 | 75 | # Run Vitest 76 | npm run test 77 | npm run test:watch 78 | 79 | # Release new version 80 | npm run release 81 | ``` 82 | 83 | 84 | [npm-version-src]: https://img.shields.io/npm/v/my-module/latest.svg?style=flat&colorA=18181B&colorB=28CF8D 85 | [npm-version-href]: https://npmjs.com/package/my-module 86 | 87 | [npm-downloads-src]: https://img.shields.io/npm/dm/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D 88 | [npm-downloads-href]: https://npmjs.com/package/my-module 89 | 90 | [license-src]: https://img.shields.io/npm/l/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D 91 | [license-href]: https://npmjs.com/package/my-module 92 | 93 | [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js 94 | [nuxt-href]: https://nuxt.com 95 | -------------------------------------------------------------------------------- /apps/nuxt-example/pages/[id].vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | -------------------------------------------------------------------------------- /packages/core/src/runtime/utils/export.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs/promises' 3 | import glob from 'fast-glob' 4 | import { convertPathToPattern } from 'fast-glob/out/utils/path'; 5 | 6 | import { camelCase, upperFirst } from '.'; 7 | 8 | export async function generateComposables(args: { 9 | dir: string, 10 | ignorePatterns?: string[], 11 | ignorePrefixes?: string[] 12 | }) { 13 | const { dir, ignorePatterns = [], ignorePrefixes = ['_'] } = args; 14 | const rootDir = path.resolve(process.cwd(), dir || ".", dir || 'api'); 15 | const composableExport = path.join(rootDir, '_composables_/index.ts') 16 | const composablesDir = path.join(rootDir, '_composables_'); 17 | 18 | try { 19 | await fs.mkdir(composablesDir); 20 | } catch (err) { 21 | // Directory already exists, ignore error 22 | } 23 | 24 | // Convert paths to patterns 25 | const normalizedRootDir = convertPathToPattern(rootDir); 26 | const normalizedComposableExport = convertPathToPattern(composableExport); 27 | const normalizedComposablesDir = convertPathToPattern(composablesDir); 28 | 29 | const files = glob.sync(`${normalizedRootDir}/**/*.ts`, { 30 | ignore: [ 31 | `${normalizedRootDir}/index.ts`, 32 | `${normalizedRootDir}/**/*.d.ts`, 33 | normalizedComposablesDir, 34 | ...ignorePatterns.map(pattern => `${normalizedRootDir}/${convertPathToPattern(pattern)}`) 35 | ], 36 | }); 37 | 38 | if (files.length === 0) { 39 | console.log('No files found. Please check the directory and patterns.'); 40 | return; 41 | } 42 | 43 | const exports = files 44 | .filter(filePath => { 45 | const relativePath = path.relative(rootDir, filePath); 46 | const segments = relativePath.split(path.sep); 47 | // Check if any segment starts with an ignored prefix 48 | return !segments.some(segment => 49 | ignorePrefixes.some(prefix => segment.startsWith(prefix)) 50 | ); 51 | }) 52 | .map(filePath => { 53 | let relativePath = path.relative(rootDir, filePath).replace(/\\/g, '/') 54 | relativePath = relativePath.replace('.ts', '') 55 | 56 | const directoryPath = path.posix.dirname(filePath); 57 | const tsFilesInSameDirectory = glob.sync(path.posix.join(directoryPath, '*.ts')) 58 | 59 | if (tsFilesInSameDirectory.length === 1 && relativePath.endsWith('/index')) { 60 | relativePath = relativePath.replace('/index', '') 61 | } 62 | 63 | const pathSegments = relativePath.split('/'); 64 | const pascalCasedSegments = pathSegments.map(segment => upperFirst(camelCase(segment))); 65 | const formattedPath = 'useApi' + pascalCasedSegments.join(''); 66 | 67 | const exportStatement = `export { default as ${formattedPath} } from '../${relativePath}'`; 68 | return exportStatement; 69 | }); 70 | 71 | const content = exports.join('\n'); 72 | 73 | try { 74 | await fs.writeFile(normalizedComposableExport, content, 'utf8'); 75 | } catch (err) { 76 | console.log(`Error writing file: ${err}`); 77 | } 78 | } -------------------------------------------------------------------------------- /apps/docs/docs/api/nuxt.md: -------------------------------------------------------------------------------- 1 | # Nuxt Module API Reference 2 | 3 | ## `useFetchModel` 4 | 5 | A composable that provides both `$fetch` and Nuxt's `useFetch` with integrated transform capabilities. 6 | 7 | ### Usage 8 | 9 | ```typescript 10 | const { $fetch, useFetch } = useFetchModel({ 11 | baseURL: 'https://api.example.com' 12 | }) 13 | 14 | // Client-side operations with $fetch 15 | const data = await $fetch.get('/users', { 16 | transform: { 17 | fields: ['id', 'name'] 18 | } 19 | }) 20 | 21 | // Generic types: 22 | const { data, pending, error } = await useFetch.get('/users', { 23 | transform: { 24 | fields: ['id', 'name'] 25 | } 26 | }) 27 | ``` 28 | 29 | ### Generic Types for useFetch methods 30 | 31 | The `useFetch` methods accept three generic type parameters: 32 | 1. `ResponseType`: The type of the raw API response 33 | 2. `TransformedType`: The type after transformation (must specify ResponseType again if no transformation needed) 34 | 3. `ErrorType`: The type of error that can occur (defaults to Error) 35 | 36 | ### Type Examples 37 | 38 | ```typescript 39 | // When no transformation is needed, specify the same type twice 40 | const { data } = await useFetch.get('/users') 41 | 42 | // With transformation 43 | const { data } = await useFetch.get('/users', { 44 | transform: { 45 | fields: ['id', 'name'] 46 | } 47 | }) 48 | 49 | // Complete example with error type 50 | const { data, error } = await useFetch.get('/users') 51 | 52 | // With transformation and error type 53 | const { data, error } = await useFetch.get('/users', { 54 | transform: { 55 | fields: ['id', 'name'] 56 | } 57 | }) 58 | ``` 59 | 60 | Note: When no transformation is needed, you must specify the response type twice in the generic parameters. The first time for `ResponseType` and the second time for `TransformedType`. 61 | 62 | ### When to use which method 63 | 64 | As explained in the [Nuxt documentation](https://nuxt.com/docs/getting-started/data-fetching#the-need-for-usefetch-and-useasyncdata): 65 | 66 | - Use `$fetch` when: 67 | - Making client-side only requests 68 | - Handling form submissions 69 | - Working in event handlers 70 | - Making requests in store actions 71 | 72 | - Use `useFetch` when: 73 | - You need SSR support 74 | - You want automatic data hydration 75 | - You're fetching data in component setup 76 | - You want request deduplication 77 | 78 | ### Available Methods 79 | 80 | Both `$fetch` and `useFetch` provide these HTTP methods: 81 | - `get` 82 | - `post` 83 | - `put` 84 | - `patch` 85 | - `delete` 86 | - `head` 87 | 88 | ### Transform Options 89 | 90 | Both methods support the same transform options: 91 | 92 | ```typescript 93 | interface TransformOptions { 94 | fields: (Field | string)[] 95 | scope?: string 96 | format?: 'camelCase' 97 | context?: IContext 98 | } 99 | ``` -------------------------------------------------------------------------------- /packages/core/src/runtime/providers/http/ofetch/index.ts: -------------------------------------------------------------------------------- 1 | import { ofetch } from 'ofetch' 2 | import type { FetchOptions } from 'ofetch' 3 | import { IHttpModel, IRequestOptions, handleRequestFunction } from '..' 4 | import { useTransform } from '../../../utils/transform' 5 | import { IContext } from '../../../utils/context' 6 | import { get } from '../../../utils' 7 | 8 | export function useOfetchModel(options?: FetchOptions & { context?: IContext }): IHttpModel { 9 | const $fetch = options ? ofetch.create(options) : ofetch 10 | 11 | const handleRequest: handleRequestFunction = (urlOrOptions, _params?) => { 12 | let url: string; 13 | let params: any; 14 | 15 | if (typeof urlOrOptions === 'string') { 16 | url = urlOrOptions; 17 | params = _params || {}; 18 | } else { 19 | url = ''; 20 | params = urlOrOptions || {}; 21 | } 22 | 23 | const context = { ...options?.context || {}, ...params?.context || {} } 24 | 25 | return $fetch(url, { 26 | ...params?.options, 27 | method: params?.method 28 | }).then((response) => { 29 | if (!params) return response 30 | 31 | const fields = params.transform?.fields 32 | 33 | // Inject scope 34 | if (params.transform?.scope) response = get(response, params.transform?.scope) 35 | 36 | // Ignore transform if no fields provided 37 | if (!fields) return response 38 | 39 | // Transform response 40 | return useTransform(response, fields, { ...params, context }).value 41 | }) 42 | } 43 | 44 | return { 45 | get(urlOrOptions?: string | IRequestOptions>, options?: IRequestOptions>) { 46 | return handleRequest(urlOrOptions as any, { 47 | method: 'get', 48 | ...options, 49 | }); 50 | }, 51 | patch(urlOrOptions: string | IRequestOptions>, options?: IRequestOptions>) { 52 | return handleRequest(urlOrOptions as any, { 53 | method: 'patch', 54 | ...options, 55 | }) 56 | }, 57 | post(urlOrOptions: string | IRequestOptions>, options?: IRequestOptions>) { 58 | return handleRequest(urlOrOptions as any, { 59 | method: 'post', 60 | ...options, 61 | }) 62 | }, 63 | put(urlOrOptions: string | IRequestOptions>, options?: IRequestOptions>) { 64 | return handleRequest(urlOrOptions as any, { 65 | method: 'put', 66 | ...options, 67 | }) 68 | }, 69 | delete(urlOrOptions: string | IRequestOptions>, options?: IRequestOptions>) { 70 | return handleRequest(urlOrOptions as any, { 71 | method: 'delete', 72 | ...options, 73 | }) 74 | }, 75 | head(urlOrOptions: string | IRequestOptions>, options?: IRequestOptions>) { 76 | return handleRequest(urlOrOptions as any, { 77 | method: 'head', 78 | ...options, 79 | }) 80 | }, 81 | } 82 | } -------------------------------------------------------------------------------- /apps/docs/docs/guide/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | 2 | # Installation 3 | 4 | Welcome to **Vue API**! This guide will help you get started by explaining how to install and configure the module in your [Vue 3](#vue-installation) or [Nuxt 3](#nuxt-installation) projects. 5 | 6 | ## For Nuxt 3 {#nuxt-installation} 7 | The **Vue API** module for Nuxt 3 is available as the `@vue-api/nuxt` package. 8 | 9 | ### Quick Start 10 | 1. Install **@vue-api/nuxt** to your project: 11 | 12 | ```bash 13 | npx nuxi@latest module add @vue-api/nuxt 14 | ``` 15 | 16 | 2. Add `@vue-api/nuxt` to your `nuxt.config` modules 17 | 18 | ```ts [nuxt.config.ts] 19 | export default defineNuxtConfig({ 20 | modules: ['@vue-api/nuxt'] 21 | }) 22 | ``` 23 | 24 | ### Module Options 25 | 26 | You can set the module options by using the `vueAPI` property in `nuxt.config` root. 27 | 28 | ```ts [nuxt.config.ts] 29 | export default defineNuxtConfig({ 30 | vueAPI: { 31 | rootPath: 'api', // The directory where the API files are located 32 | ignorePatterns: [], // The patterns to ignore when generating composables 33 | ignorePrefixes: ['_'], // The prefixes to ignore when generating composables 34 | } 35 | }) 36 | ``` 37 | 38 | ::: tip 39 | When using Nuxt, the module provides both `$fetch` and `useFetch` methods through `useFetchModel`. Use `$fetch` for client-side operations and `useFetch` for SSR-compatible data fetching. 40 | ::: 41 | 42 | That's it! You can now use **Vue API** in your Nuxt 3 project. 43 | 44 | ## For Vue 3 {#vue-installation} 45 | The **Vue API** module for Vue 3 is available as the `@vue-api/vue` package. 46 | 47 | ::: code-group 48 | 49 | ```sh [npm] 50 | $ npm add -D @vue-api/vue 51 | ``` 52 | 53 | ```sh [pnpm] 54 | $ pnpm add -D @vue-api/vue 55 | ``` 56 | 57 | ```sh [yarn] 58 | $ yarn add -D @vue-api/vue 59 | ``` 60 | 61 | ```sh [yarn (pnp)] 62 | $ yarn add -D @vue-api/vue 63 | ``` 64 | 65 | ```sh [bun] 66 | $ bun add -D @vue-api/vue 67 | ``` 68 | ::: 69 | 70 | To use this module please add the vueApiPlugin to your vite configuration. 71 | 72 | ```ts:line-numbers {15} [vite.config.ts] 73 | import { fileURLToPath, URL } from 'node:url' 74 | 75 | import { defineConfig } from 'vite' 76 | import vue from '@vitejs/plugin-vue' 77 | import vueJsx from '@vitejs/plugin-vue-jsx' 78 | import vueDevTools from 'vite-plugin-vue-devtools' 79 | import { plugin as vueApiPlugin } from '@vue-api/vue' 80 | 81 | // https://vitejs.dev/config/ 82 | export default defineConfig({ 83 | plugins: [ 84 | vue(), 85 | vueJsx(), 86 | vueDevTools(), 87 | vueApiPlugin({ 88 | rootPath: 'api', // The directory where the API files are located 89 | ignorePatterns: [], // The patterns to ignore when generating composables 90 | ignorePrefixes: ['_'], // The prefixes to ignore when generating composables 91 | }), 92 | ], 93 | resolve: { 94 | alias: { 95 | '@': fileURLToPath(new URL('./src', import.meta.url)) 96 | } 97 | }, 98 | preview: { 99 | port: 3001 100 | }, 101 | server: { 102 | port: 3001 103 | } 104 | }) 105 | ``` 106 | 107 | By default, this plugin searches for API files in the `api` folder. However, you can customize this path by passing an `rootPath` parameter to the plugin. 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /apps/docs/docs/guide/composables/introduction.md: -------------------------------------------------------------------------------- 1 | # How to use composables 2 | 3 | Vue API automatically generates composables based on your API directory structure. This feature simplifies the process of making API calls in your Vue or Nuxt application by creating intuitive, easy-to-use composables that mirror your API structure. 4 | 5 | ## Automatic Composable Generation 6 | 7 | The library scans your API directory and creates composables for each endpoint defined. The naming convention for these composables is based on the directory structure and file names within your API folder. 8 | 9 | ### Naming Convention 10 | 11 | The general format for generated composable names is: 12 | 13 | `useApi[Folder1][Folder2]...[FileName]()` 14 | 15 | Here are some examples to illustrate this convention: 16 | 17 | 1. For a file `api/cms/article.ts`: 18 | The generated composable name would be `useApiCmsArticle` 19 | 20 | 2. For a file `api/cms/index.ts`: 21 | The generated composable name would be `useApiCmsIndex` 22 | 23 | 3. In the case where there's only an `index.ts` file in a folder: 24 | For `api/cms/blogs/index.ts`, the generated composable name would be `useApiCms` 25 | 26 | This convention works recursively, so there's no limit to the depth of the folder structure. 27 | 28 | ## Generating the Composables Declaration File 29 | 30 | A CLI tool `vue-api` is provided by the `@vue-api/core` package to generate the composables declaration file. The `generateComposables` function is used to generate the composables declaration file. Here's what the tool does: 31 | 32 | 1. Creates a `_composables_` folder in the root directory (default `api`). 33 | 2. Generates an `index.ts` file in this folder, which exports all the composables. 34 | 35 | This `index.ts` file is then injected into the project: 36 | - In Nuxt, via the Nuxt module `addImportsDir` 37 | - In Vue, via the Vue Vite plugin using the `unplugin-auto-import/vite` module to inject the composables in the project. 38 | 39 | This allows for a centralized declaration of all generated composables, making it easier to import and use them in your application. 40 | 41 | 42 | ## Using Generated Composables 43 | 44 | Now that you understand how composables are automatically generated based on your API structure, let's explore how to effectively use these composables in your Vue or Nuxt application. The next section will guide you through the process of structuring your API functions to work seamlessly with these generated composables. 45 | 46 | ## TypeScript Declaration Files 47 | 48 | You can add TypeScript declaration files (*.d.ts) to your API directory without affecting the composable generation process. These declaration files are automatically ignored during the composable generation, allowing you to define types and interfaces for your API without interfering with the auto-generated composables. 49 | 50 | This feature is particularly useful for: 51 | - Defining shared types and interfaces for your API 52 | - Enhancing type safety in your API implementations 53 | 54 | For example, you can create a file like `api/types.d.ts` to declare shared types: 55 | 56 | ```typescript 57 | interface User { 58 | id: number; 59 | name: string; 60 | email: string; 61 | } 62 | 63 | interface ApiResponse { 64 | data: T; 65 | status: number; 66 | message: string; 67 | } 68 | ``` 69 | 70 | These types can then be used in your API implementation files without being included in the generated composables. -------------------------------------------------------------------------------- /apps/vue-example/src/components/TheWelcome.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 87 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## High Priority 4 | - [ ] Resolve npm install isssue of @vue-api/core (need to install @vue-api/core) manually actually 5 | - [ ] Remove map on 6 | - [ ] Omit value when parent has * but field has newKey we should omit the newKey 7 | - [ ] Fix bug of key * with omit with parent path ? 8 | ```ts 9 | const model = { 10 | data: { 11 | facets: [ 12 | { 13 | id: 11, 14 | __component: "algolia.facet-checkbox-group", 15 | key: "pays", 16 | label: "Pays", 17 | searchable: true, 18 | theme: "default", 19 | searchPlaceholder: "Je recherche un pays...", 20 | seeMore: "J'affiche les {0} pays", 21 | cols: null, 22 | displayCustomOptionsOnly: false, 23 | seeLess: "J'affiche moins de pays", 24 | }, 25 | ], 26 | }, 27 | }; 28 | fields: [{ 29 | key: 'facets', 30 | path: 'data.facets', 31 | fields: [ 32 | { key: '*', omit: ['__component'] }, 33 | { 34 | key: 'component', 35 | mapping: ({ model }) => { 36 | switch (model.__component) { 37 | case 'algolia.facet-checkbox-group': 38 | return resolveComponent('AlgoliaFacetCheckBoxGroup') 39 | } 40 | } 41 | } 42 | ] 43 | }] 44 | ``` 45 | - [ ] Fix bug omit with path 46 | ```ts 47 | fields: [{ 48 | key: 'facets', 49 | path: 'data.facets', 50 | omit: ['id'] 51 | }] 52 | ``` 53 | 54 | - [ ] Improve `parentModel` handling in `extractModel` function for nested fields and wildcards 55 | - Update logic to correctly pass `parentModel` through nested levels 56 | - Ensure `parentModel` is correctly set for wildcard expansions 57 | - [ ] Implement comprehensive test suite for @vue-api/core 58 | - Focus on testing the mapping functionality 59 | - Cover basic, nested, and wildcard mapping scenarios 60 | - Ensure edge cases are properly handled 61 | - [ ] Implement clean-up functionality for inconsistent data structures 62 | - Add option to handle cases where mapped items have different structures 63 | - Implement logic to skip or provide default values for missing fields 64 | - Consider adding a configuration option to control this behavior 65 | - Handle optimisation to handle expandWildcardFields in each sub fields children and also not call it if current model is an array and has no wildcard type 'user.*.name' has the structure could be different 66 | 67 | ## Medium Priority 68 | - [ ] Update `expandWildcardFields` to properly manage `parentModel` for wildcard expansions 69 | - Implement logic to set `parentModel` for each expanded field 70 | - [ ] Expand test coverage for other core functionalities 71 | - Test utility functions (get, set, camelCase, etc.) 72 | - Test error handling and edge cases 73 | - [ ] Design and implement configuration options for clean-up behavior 74 | - Add ability to specify default values for missing fields 75 | - Implement option to skip fields entirely if not present in the source data 76 | 77 | ## Low Priority 78 | - [ ] Test and verify `parentModel` behavior in complex nested scenarios 79 | - Create comprehensive test suite for various nested and wildcard scenarios 80 | - Document edge cases and expected behavior 81 | - [ ] Set up continuous integration for automated testing 82 | - Integrate with GitHub Actions or similar CI tool 83 | - Ensure tests run on each pull request 84 | - [ ] Document new clean-up functionality and configuration options 85 | - Update user guide with examples of handling inconsistent data structures 86 | - Provide best practices for using the clean-up feature 87 | 88 | 89 | ## Completed 90 | - [x] Implement initial version of `extractModel` function 91 | - [x] Create basic wildcard expansion functionality 92 | -------------------------------------------------------------------------------- /apps/nuxt-example/api/users/index.ts: -------------------------------------------------------------------------------- 1 | import type { Field, IRequestOptions } from "@vue-api/nuxt" 2 | 3 | interface Project { 4 | name: string; 5 | status: string; 6 | statusSummary?: string; 7 | } 8 | 9 | interface Department { 10 | title: string; 11 | role: string; 12 | projects: Project[]; 13 | } 14 | 15 | export interface User { 16 | id: string; 17 | name: string; 18 | email: string; 19 | avatar: string; 20 | departments: { 21 | [key: string]: Department; 22 | }; 23 | skills: string[]; 24 | departmentSummary?: { [key: string]: string }; 25 | totalProjects?: number; 26 | } 27 | 28 | export interface UserListItem { 29 | id: string; 30 | name: string; 31 | email: string; 32 | avatar: string; 33 | totalProjects?: number; 34 | departmentSummary?: { [key: string]: string }; 35 | } 36 | 37 | export default function () { 38 | const { useFetch } = useFetchModel({ 39 | baseURL: "https://64cbdfbd2eafdcdc85196e4c.mockapi.io/users", 40 | }); 41 | 42 | const USER_FIELDS: Field[] = [ 43 | { key: "*", omit: ["password"] }, 44 | "departments", 45 | { 46 | key: "departments.*.projects", 47 | fields: [ 48 | "*", 49 | { 50 | key: "statusSummary", 51 | mapping: ({ model }: { model: Project }) => { 52 | const statusEmoji = 53 | model.status.toLowerCase() === "completed" 54 | ? "✅" 55 | : model.status.toLowerCase() === "in progress" 56 | ? "🚧" 57 | : "🔜"; 58 | return `${statusEmoji} ${model.name} (${model.status})`; 59 | }, 60 | }, 61 | ], 62 | }, 63 | { 64 | key: "departmentSummary", 65 | mapping: ({ model }: { model: User }) => { 66 | return Object.entries(model.departments).reduce( 67 | (acc, [key, dept]) => { 68 | acc[key] = 69 | `${dept.role} in ${dept.title} (${dept.projects.length} projects)`; 70 | return acc; 71 | }, 72 | {} as { [key: string]: string } 73 | ); 74 | }, 75 | }, 76 | { 77 | key: "totalProjects", 78 | mapping: ({ model }: { model: User }) => { 79 | return Object.values(model.departments).reduce( 80 | (total, dept) => total + dept.projects.length, 81 | 0 82 | ); 83 | }, 84 | }, 85 | ]; 86 | 87 | const USERS_FIELDS: Field[] = [ 88 | "id", 89 | "name", 90 | "email", 91 | "avatar", 92 | { 93 | key: "totalProjects", 94 | mapping: ({ model }: { model: User }) => { 95 | return Object.values(model.departments).reduce( 96 | (total, dept) => total + dept.projects.length, 97 | 0 98 | ); 99 | }, 100 | }, 101 | { 102 | key: "departmentSummary", 103 | mapping: ({ model }: { model: User }) => { 104 | return Object.entries(model.departments).reduce( 105 | (acc, [key, dept]) => { 106 | acc[key] = 107 | `${dept.role} in ${dept.title} (${dept.projects.length} projects)`; 108 | return acc; 109 | }, 110 | {} as { [key: string]: string } 111 | ); 112 | }, 113 | }, 114 | ]; 115 | 116 | return { 117 | findOne: async ( 118 | userId: string, 119 | options?: IRequestOptions> 120 | ) => { 121 | return useFetch.get(userId, { 122 | ...options, 123 | transform: { 124 | fields: USER_FIELDS, 125 | context: {}, 126 | }, 127 | }); 128 | }, 129 | get: async (options?: IRequestOptions>) => { 130 | return useFetch.get({ 131 | ...options, 132 | transform: { 133 | fields: USERS_FIELDS, 134 | context: {}, 135 | }, 136 | }); 137 | } 138 | }; 139 | } 140 | -------------------------------------------------------------------------------- /packages/nuxt/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vue-api/nuxt 2 | 3 | ## 2.0.9 4 | 5 | ### Patch Changes 6 | 7 | - Handle error typing for useFetch 8 | 9 | ## 2.0.8 10 | 11 | ### Patch Changes 12 | 13 | - fix: fix returning type of methods 14 | 15 | ## 2.0.7 16 | 17 | ### Patch Changes 18 | 19 | - feat: create @vue-api/core alias from nuxt 20 | 21 | ## 2.0.6 22 | 23 | ### Patch Changes 24 | 25 | - fix: fix useTransform export 26 | 27 | ## 2.0.5 28 | 29 | ### Patch Changes 30 | 31 | - fix: fix declaration and type / function exposition from core 32 | - Updated dependencies 33 | - @vue-api/core@2.0.1 34 | 35 | ## 2.0.4 36 | 37 | ### Patch Changes 38 | 39 | - fix: fix method type to uppercase 40 | 41 | ## 2.0.3 42 | 43 | ### Patch Changes 44 | 45 | - fix: try using #imports for useFetch 46 | 47 | ## 2.0.2 48 | 49 | ### Patch Changes 50 | 51 | - Fix d.ts generation type 52 | 53 | ## 2.0.1 54 | 55 | ### Patch Changes 56 | 57 | - Fix useFetch injection composable 58 | 59 | ## 2.0.0 60 | 61 | ### Major Changes 62 | 63 | - f42f806: refactoring usage of useFetch / fetch with nuxt 64 | 65 | ### Patch Changes 66 | 67 | - Updated dependencies [f42f806] 68 | - @vue-api/core@2.0.0 69 | 70 | ## 1.0.25 71 | 72 | ### Patch Changes 73 | 74 | - Updated dependencies 75 | - @vue-api/core@1.0.17 76 | 77 | ## 1.0.24 78 | 79 | ### Patch Changes 80 | 81 | - Updated dependencies 82 | - @vue-api/core@1.0.16 83 | 84 | ## 1.0.23 85 | 86 | ### Patch Changes 87 | 88 | - Updated dependencies 89 | - @vue-api/core@1.0.15 90 | 91 | ## 1.0.22 92 | 93 | ### Patch Changes 94 | 95 | - Updated dependencies 96 | - @vue-api/core@1.0.14 97 | 98 | ## 1.0.21 99 | 100 | ### Patch Changes 101 | 102 | - Updated dependencies 103 | - @vue-api/core@1.0.13 104 | 105 | ## 1.0.20 106 | 107 | ### Patch Changes 108 | 109 | - Updated dependencies 110 | - @vue-api/core@1.0.12 111 | 112 | ## 1.0.19 113 | 114 | ### Patch Changes 115 | 116 | - Updated dependencies 117 | - @vue-api/core@1.0.11 118 | 119 | ## 1.0.18 120 | 121 | ### Patch Changes 122 | 123 | - Updated dependencies 124 | - @vue-api/core@1.0.10 125 | 126 | ## 1.0.17 127 | 128 | ### Patch Changes 129 | 130 | - Updated dependencies 131 | - @vue-api/core@1.0.9 132 | 133 | ## 1.0.16 134 | 135 | ### Patch Changes 136 | 137 | - Updated dependencies 138 | - @vue-api/core@1.0.8 139 | 140 | ## 1.0.15 141 | 142 | ### Patch Changes 143 | 144 | - Updated dependencies 145 | - @vue-api/core@1.0.7 146 | 147 | ## 1.0.14 148 | 149 | ### Patch Changes 150 | 151 | - Updated dependencies 152 | - @vue-api/core@1.0.6 153 | 154 | ## 1.0.13 155 | 156 | ### Patch Changes 157 | 158 | - Updated dependencies 159 | - @vue-api/core@1.0.5 160 | 161 | ## 1.0.12 162 | 163 | ### Patch Changes 164 | 165 | - fix: fix composable hook stage 166 | 167 | ## 1.0.11 168 | 169 | ### Patch Changes 170 | 171 | - fix: fix composable generation path 172 | - Updated dependencies 173 | - @vue-api/core@1.0.4 174 | 175 | ## 1.0.11 176 | 177 | ### Patch Changes 178 | 179 | - build: try bump core for nuxt package 180 | 181 | ## 1.0.10 182 | 183 | ### Patch Changes 184 | 185 | - build: bump core version 186 | 187 | ## 1.0.9 188 | 189 | ### Patch Changes 190 | 191 | - Updated dependencies 192 | - @vue-api/core@1.0.3 193 | 194 | ## 1.0.8 195 | 196 | ### Patch Changes 197 | 198 | - fix useAsyncData option 199 | 200 | ## 1.0.7 201 | 202 | ### Patch Changes 203 | 204 | - feat(nuxt): handle return type according to asyncData activation 205 | 206 | ## 1.0.4 207 | 208 | ### Patch Changes 209 | 210 | - Try @vue-api/core inclusion for build 211 | 212 | ## 1.0.4 213 | 214 | ### Patch Changes 215 | 216 | - build: fix @vue-api/core resolver 217 | 218 | ## 1.0.3 219 | 220 | ### Patch Changes 221 | 222 | - Fix nuxt module composable 223 | 224 | ## 1.0.2 225 | 226 | ### Patch Changes 227 | 228 | - Path packages to 1.0.2 229 | - Updated dependencies 230 | - @vue-api/core@1.0.2 231 | 232 | ## 1.0.1 233 | 234 | ### Patch Changes 235 | 236 | - remove lodash-es and fix transform issues 237 | - Updated dependencies 238 | - @vue-api/core@1.0.1 239 | -------------------------------------------------------------------------------- /packages/nuxt/src/runtime/composables/useFetchModel.ts: -------------------------------------------------------------------------------- 1 | import type { FetchOptions } from 'ofetch' 2 | import type { ITransformRequestOptions, IContext } from '@vue-api/core' 3 | import { useTransform } from '@vue-api/core' 4 | import type { UseFetchOptions, AsyncData } from 'nuxt/app' 5 | import { useFetch } from '#imports' 6 | 7 | type RequestType = "GET" | "HEAD" | "PATCH" | "POST" | "PUT" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" 8 | 9 | interface CustomTransformOptions extends FetchOptions { 10 | transform?: ITransformRequestOptions 11 | context?: IContext 12 | } 13 | 14 | type ExtendedUseFetchOptions = Omit, 'transform'> & { 15 | transform?: ITransformRequestOptions | ((response: T) => R) 16 | context?: IContext 17 | } 18 | 19 | function parseUrlAndOptions( 20 | urlOrOptions?: string | T, 21 | options?: T, 22 | ): [string, T | undefined] { 23 | if (typeof urlOrOptions === 'string') { 24 | return [urlOrOptions, options] 25 | } 26 | return ['', urlOrOptions] 27 | } 28 | 29 | export default function (defaultOptions?: CustomTransformOptions) { 30 | function createFetchMethod(methodName: RequestType) { 31 | return async ( 32 | urlOrOptions?: string | CustomTransformOptions, 33 | options?: CustomTransformOptions, 34 | ): Promise => { 35 | const [url, params] = parseUrlAndOptions(urlOrOptions, options) 36 | const mergedOptions = { 37 | ...defaultOptions, 38 | ...params, 39 | method: methodName, 40 | context: { ...defaultOptions?.context, ...params?.context }, 41 | transform: { ...defaultOptions?.transform, ...params?.transform }, 42 | } 43 | 44 | const response = await $fetch(url, mergedOptions) 45 | 46 | if (mergedOptions?.transform?.fields) { 47 | return useTransform(response, mergedOptions.transform.fields, { 48 | ...mergedOptions, 49 | context: mergedOptions.context, 50 | }).value as TransformedT 51 | } 52 | 53 | return response as unknown as TransformedT 54 | } 55 | } 56 | 57 | function createUseFetchMethod(methodName: RequestType) { 58 | return ( 59 | urlOrOptions?: string | ExtendedUseFetchOptions, 60 | options?: ExtendedUseFetchOptions 61 | ): AsyncData => { 62 | const [url, params] = parseUrlAndOptions(urlOrOptions, options) 63 | const mergedOptions = { 64 | ...defaultOptions, 65 | ...params, 66 | context: { ...defaultOptions?.context, ...params?.context }, 67 | transform: { ...defaultOptions?.transform, ...params?.transform }, 68 | } 69 | 70 | const transformFn = typeof mergedOptions?.transform === 'function' 71 | ? mergedOptions.transform 72 | : (response: T) => { 73 | if (mergedOptions?.transform?.fields) { 74 | return useTransform(response, mergedOptions.transform.fields, { 75 | ...mergedOptions, 76 | context: mergedOptions.context, 77 | }).value as TransformedT 78 | } 79 | return response as unknown as TransformedT 80 | } 81 | 82 | return useFetch(url || '/', { 83 | ...mergedOptions, 84 | method: methodName, 85 | transform: transformFn, 86 | }) 87 | } 88 | } 89 | 90 | return { 91 | $fetch: { 92 | get: createFetchMethod('GET'), 93 | post: createFetchMethod('POST'), 94 | put: createFetchMethod('PUT'), 95 | patch: createFetchMethod('PATCH'), 96 | delete: createFetchMethod('DELETE'), 97 | head: createFetchMethod('HEAD'), 98 | }, 99 | useFetch: { 100 | get: createUseFetchMethod('GET'), 101 | post: createUseFetchMethod('POST'), 102 | put: createUseFetchMethod('PUT'), 103 | patch: createUseFetchMethod('PATCH'), 104 | delete: createUseFetchMethod('DELETE'), 105 | head: createUseFetchMethod('HEAD'), 106 | }, 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /apps/nuxt-example/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # nuxt-example 2 | 3 | ## 2.0.9 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies 8 | - @vue-api/nuxt@2.0.9 9 | 10 | ## 2.0.8 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies 15 | - @vue-api/nuxt@2.0.8 16 | 17 | ## 2.0.7 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies 22 | - @vue-api/nuxt@2.0.7 23 | 24 | ## 2.0.6 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies 29 | - @vue-api/nuxt@2.0.6 30 | 31 | ## 2.0.5 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies 36 | - @vue-api/core@2.0.1 37 | - @vue-api/nuxt@2.0.5 38 | 39 | ## 2.0.4 40 | 41 | ### Patch Changes 42 | 43 | - Updated dependencies 44 | - @vue-api/nuxt@2.0.4 45 | 46 | ## 2.0.3 47 | 48 | ### Patch Changes 49 | 50 | - Updated dependencies 51 | - @vue-api/nuxt@2.0.3 52 | 53 | ## 2.0.2 54 | 55 | ### Patch Changes 56 | 57 | - Updated dependencies 58 | - @vue-api/nuxt@2.0.2 59 | 60 | ## 2.0.1 61 | 62 | ### Patch Changes 63 | 64 | - Updated dependencies 65 | - @vue-api/nuxt@2.0.1 66 | 67 | ## 2.0.0 68 | 69 | ### Major Changes 70 | 71 | - f42f806: refactoring usage of useFetch / fetch with nuxt 72 | 73 | ### Patch Changes 74 | 75 | - Updated dependencies [f42f806] 76 | - @vue-api/core@2.0.0 77 | - @vue-api/nuxt@2.0.0 78 | 79 | ## 1.0.25 80 | 81 | ### Patch Changes 82 | 83 | - Updated dependencies 84 | - @vue-api/core@1.0.17 85 | - @vue-api/nuxt@1.0.25 86 | 87 | ## 1.0.24 88 | 89 | ### Patch Changes 90 | 91 | - Updated dependencies 92 | - @vue-api/core@1.0.16 93 | - @vue-api/nuxt@1.0.24 94 | 95 | ## 1.0.23 96 | 97 | ### Patch Changes 98 | 99 | - Updated dependencies 100 | - @vue-api/core@1.0.15 101 | - @vue-api/nuxt@1.0.23 102 | 103 | ## 1.0.22 104 | 105 | ### Patch Changes 106 | 107 | - Updated dependencies 108 | - @vue-api/core@1.0.14 109 | - @vue-api/nuxt@1.0.22 110 | 111 | ## 1.0.21 112 | 113 | ### Patch Changes 114 | 115 | - Updated dependencies 116 | - @vue-api/core@1.0.13 117 | - @vue-api/nuxt@1.0.21 118 | 119 | ## 1.0.20 120 | 121 | ### Patch Changes 122 | 123 | - Updated dependencies 124 | - @vue-api/core@1.0.12 125 | - @vue-api/nuxt@1.0.20 126 | 127 | ## 1.0.19 128 | 129 | ### Patch Changes 130 | 131 | - Updated dependencies 132 | - @vue-api/core@1.0.11 133 | - @vue-api/nuxt@1.0.19 134 | 135 | ## 1.0.18 136 | 137 | ### Patch Changes 138 | 139 | - Updated dependencies 140 | - @vue-api/core@1.0.10 141 | - @vue-api/nuxt@1.0.18 142 | 143 | ## 1.0.17 144 | 145 | ### Patch Changes 146 | 147 | - Updated dependencies 148 | - @vue-api/core@1.0.9 149 | - @vue-api/nuxt@1.0.17 150 | 151 | ## 1.0.16 152 | 153 | ### Patch Changes 154 | 155 | - Updated dependencies 156 | - @vue-api/core@1.0.8 157 | - @vue-api/nuxt@1.0.16 158 | 159 | ## 1.0.15 160 | 161 | ### Patch Changes 162 | 163 | - Updated dependencies 164 | - @vue-api/core@1.0.7 165 | - @vue-api/nuxt@1.0.15 166 | 167 | ## 1.0.14 168 | 169 | ### Patch Changes 170 | 171 | - @vue-api/nuxt@1.0.14 172 | 173 | ## 1.0.13 174 | 175 | ### Patch Changes 176 | 177 | - @vue-api/nuxt@1.0.13 178 | 179 | ## 1.0.12 180 | 181 | ### Patch Changes 182 | 183 | - Updated dependencies 184 | - @vue-api/nuxt@1.0.12 185 | 186 | ## 1.0.11 187 | 188 | ### Patch Changes 189 | 190 | - Updated dependencies 191 | - @vue-api/nuxt@1.0.11 192 | 193 | ## 1.0.10 194 | 195 | ### Patch Changes 196 | 197 | - Updated dependencies 198 | - @vue-api/nuxt@1.0.11 199 | 200 | ## 1.0.9 201 | 202 | ### Patch Changes 203 | 204 | - Updated dependencies 205 | - @vue-api/nuxt@1.0.10 206 | 207 | ## 1.0.8 208 | 209 | ### Patch Changes 210 | 211 | - @vue-api/nuxt@1.0.9 212 | 213 | ## 1.0.7 214 | 215 | ### Patch Changes 216 | 217 | - Updated dependencies 218 | - @vue-api/nuxt@1.0.8 219 | 220 | ## 1.0.6 221 | 222 | ### Patch Changes 223 | 224 | - Updated dependencies 225 | - @vue-api/nuxt@1.0.7 226 | 227 | ## 1.0.5 228 | 229 | ### Patch Changes 230 | 231 | - Updated dependencies 232 | - @vue-api/nuxt@1.0.4 233 | 234 | ## 1.0.4 235 | 236 | ### Patch Changes 237 | 238 | - Updated dependencies 239 | - @vue-api/nuxt@1.0.4 240 | 241 | ## 1.0.3 242 | 243 | ### Patch Changes 244 | 245 | - Updated dependencies 246 | - @vue-api/nuxt@1.0.3 247 | 248 | ## 1.0.2 249 | 250 | ### Patch Changes 251 | 252 | - Updated dependencies 253 | - @vue-api/nuxt@1.0.2 254 | 255 | ## 1.0.1 256 | 257 | ### Patch Changes 258 | 259 | - Updated dependencies 260 | - @vue-api/nuxt@1.0.1 261 | -------------------------------------------------------------------------------- /packages/core/src/runtime/utils/transform.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { useTransform } from './transform'; 3 | 4 | describe('useTransform', () => { 5 | it('should transform a simple object with newKey', () => { 6 | const input = { 7 | name: 'John Doe', 8 | age: 30, 9 | }; 10 | 11 | const fields = [ 12 | 'name', 13 | { key: 'age', newKey: 'years' }, 14 | ]; 15 | 16 | const { value } = useTransform(input, fields); 17 | 18 | expect(value).toEqual({ 19 | name: 'John Doe', 20 | years: 30, 21 | }); 22 | }); 23 | 24 | it('should handle nested objects', () => { 25 | const input = { 26 | user: { 27 | name: 'Jane Doe', 28 | details: { 29 | age: 25, 30 | city: 'New York', 31 | }, 32 | }, 33 | }; 34 | 35 | const fields = [ 36 | { 37 | key: 'user', 38 | fields: [ 39 | 'name', 40 | { 41 | key: 'details', 42 | fields: [ 43 | 'age', 44 | { key: 'city', newKey: 'location' }, 45 | ] 46 | }, 47 | ] 48 | }, 49 | ]; 50 | 51 | const { value } = useTransform(input, fields); 52 | 53 | expect(value).toEqual({ 54 | user: { 55 | name: 'Jane Doe', 56 | details: { 57 | age: 25, 58 | location: 'New York', 59 | }, 60 | }, 61 | }); 62 | }); 63 | 64 | it('should handle arrays with newKey', () => { 65 | const input = { 66 | users: [ 67 | { id: 1, name: 'Alice' }, 68 | { id: 2, name: 'Bob' }, 69 | ], 70 | }; 71 | 72 | const fields = [ 73 | { 74 | key: 'users', 75 | fields: [ 76 | 'id', 77 | { key: 'name', newKey: 'fullName' }, 78 | ] 79 | }, 80 | ]; 81 | 82 | const { value } = useTransform(input, fields); 83 | 84 | expect(value).toEqual({ 85 | users: [ 86 | { id: 1, fullName: 'Alice' }, 87 | { id: 2, fullName: 'Bob' }, 88 | ], 89 | }); 90 | }); 91 | 92 | it('should handle wildcard fields and retrieve only specified fields', () => { 93 | const input = { 94 | data: { 95 | user1: { name: 'Alice', age: 30, sexe: 'F' }, 96 | user2: { name: 'Bob', age: 25, sexe: 'M' }, 97 | } 98 | }; 99 | 100 | const fields = [{ 101 | key: 'data.*', 102 | fields: ['name'], 103 | }]; 104 | 105 | const { value } = useTransform(input, fields); 106 | 107 | expect(value).toEqual({ 108 | data: { 109 | user1: { name: 'Alice' }, 110 | user2: { name: 'Bob' }, 111 | } 112 | }); 113 | }); 114 | 115 | it('should handle omit with wildcard key at root and nested levels', () => { 116 | const input = { 117 | name: 'John Doe', 118 | email: 'john@example.com', 119 | password: 'secret123', 120 | }; 121 | 122 | const fields = [ 123 | { 124 | key: '*', 125 | omit: ['password'] 126 | } 127 | ]; 128 | 129 | const { value } = useTransform(input, fields); 130 | 131 | expect(value).toEqual({ 132 | name: "John Doe", 133 | email: "john@example.com" 134 | }); 135 | }); 136 | 137 | it('should handle omit with path', () => { 138 | const input = { 139 | user: { 140 | name: 'John Doe', 141 | email: 'john@example.com', 142 | password: 'secret123', 143 | } 144 | }; 145 | 146 | const fields = [ 147 | { 148 | key: 'test', 149 | path: 'user', 150 | fields: [{ key: '*', omit: ['password'] }], 151 | } 152 | ]; 153 | 154 | const { value } = useTransform(input, fields); 155 | 156 | expect(value).toEqual({ 157 | test: { 158 | name: "John Doe", 159 | email: "john@example.com" 160 | } 161 | }); 162 | }); 163 | 164 | it('should handle wildcard fields and retrieve only specified fields', () => { 165 | const input = { 166 | data: { 167 | user1: { name: 'Alice', age: 30, sexe: 'F' }, 168 | user2: { name: 'Bob', age: 25, sexe: 'M' }, 169 | } 170 | }; 171 | 172 | const fields = [{ 173 | key: 'data.*', 174 | fields: [{ key: '*', omit: ['age', 'sexe'] }], 175 | }]; 176 | 177 | const { value } = useTransform(input, fields); 178 | 179 | expect(value).toEqual({ 180 | data: { 181 | user1: { name: 'Alice' }, 182 | user2: { name: 'Bob' }, 183 | } 184 | }); 185 | }); 186 | 187 | it('should handle wildcard fields with nested objects', () => { 188 | const input = { 189 | data: { 190 | user1: { name: 'Alice', age: 30 }, 191 | user2: { name: 'Bob', age: 25 }, 192 | } 193 | }; 194 | 195 | const fields = ['data.*.name']; 196 | 197 | const { value } = useTransform(input, fields); 198 | 199 | expect(value).toEqual({ 200 | data: { 201 | user1: { name: 'Alice' }, 202 | user2: { name: 'Bob' }, 203 | } 204 | }); 205 | }) 206 | 207 | it('should handle nested wildcard fields with renaming and mapping', () => { 208 | const input = { 209 | test: { 210 | a: { 211 | subField: { 212 | name: 'Sub 1', 213 | age: [10, 20], 214 | } 215 | }, 216 | b: { 217 | subField: { 218 | name: 'Sub 2', 219 | age: [20, 30], 220 | } 221 | } 222 | } 223 | }; 224 | 225 | const fields = [ 226 | { 227 | key: 'test.*.subField', 228 | fields: [{ 229 | key: 'name', 230 | newKey: 'fullName' 231 | }, 232 | { 233 | key: 'age', 234 | mapping: ({ model }) => { 235 | return model.age.map((age) => age * 2); 236 | } 237 | }] 238 | }, 239 | ]; 240 | 241 | const { value } = useTransform(input, fields); 242 | 243 | expect(value).toEqual({ 244 | test: { 245 | a: { 246 | subField: { 247 | fullName: 'Sub 1', 248 | age: [20, 40], 249 | } 250 | }, 251 | b: { 252 | subField: { 253 | fullName: 'Sub 2', 254 | age: [40, 60], 255 | } 256 | } 257 | } 258 | }); 259 | }); 260 | 261 | it('should handle scope with nested fields', () => { 262 | const input = { 263 | user: { 264 | profile: { 265 | name: 'Jane Doe', 266 | address: { 267 | city: 'New York', 268 | country: 'USA' 269 | } 270 | } 271 | } 272 | }; 273 | 274 | const fields = [ 275 | { 276 | key: 'userInfo', 277 | scope: 'user.profile', 278 | fields: [ 279 | 'name', 280 | { key: 'location', fields: ['city', 'country'], scope: 'user.profile.address' } 281 | ] 282 | } 283 | ]; 284 | 285 | const { value } = useTransform(input, fields); 286 | 287 | expect(value).toEqual({ 288 | userInfo: { 289 | name: 'Jane Doe', 290 | location: { 291 | city: 'New York', 292 | country: 'USA' 293 | } 294 | } 295 | }); 296 | }); 297 | 298 | it('should handle path with wildcard', () => { 299 | const input = { 300 | data: { 301 | users: [ 302 | { id: 1, name: 'Alice' }, 303 | { id: 2, name: 'Bob' } 304 | ] 305 | } 306 | }; 307 | 308 | const fields = [ 309 | { 310 | key: 'users', 311 | path: 'data.users', 312 | fields: ['id', 'name'] 313 | } 314 | ]; 315 | 316 | const { value } = useTransform(input, fields); 317 | 318 | expect(value).toEqual({ 319 | users: [ 320 | { id: 1, name: 'Alice' }, 321 | { id: 2, name: 'Bob' } 322 | ] 323 | }); 324 | }); 325 | 326 | it('should handle scope with mapping', () => { 327 | const input = { 328 | company: { 329 | employees: [ 330 | { name: 'Alice', role: 'developer' }, 331 | { name: 'Bob', role: 'designer' } 332 | ] 333 | } 334 | }; 335 | 336 | const fields = [ 337 | { 338 | key: 'staff', 339 | scope: 'company.employees', 340 | fields: [ 341 | 'name', 342 | { 343 | key: 'position', 344 | mapping: ({ model }) => model.role.toUpperCase() 345 | } 346 | ] 347 | } 348 | ]; 349 | 350 | const { value } = useTransform(input, fields); 351 | 352 | expect(value).toEqual({ 353 | staff: [ 354 | { name: 'Alice', position: 'DEVELOPER' }, 355 | { name: 'Bob', position: 'DESIGNER' } 356 | ] 357 | }); 358 | }); 359 | 360 | it('should handle path with default value', () => { 361 | const input = { 362 | settings: { 363 | theme: 'dark' 364 | } 365 | }; 366 | 367 | const fields = [ 368 | { key: 'theme', path: 'settings.theme' }, 369 | { key: 'language', path: 'settings.language', default: 'en' } 370 | ]; 371 | 372 | const { value } = useTransform(input, fields); 373 | 374 | expect(value).toEqual({ 375 | theme: 'dark', 376 | language: 'en' 377 | }); 378 | }); 379 | 380 | it('should handle wildcard with specific field override', () => { 381 | const input = { 382 | firstName: 'John', 383 | lastName: 'Doe', 384 | email: 'john@example.com', 385 | } 386 | 387 | const fields = [ 388 | '*', 389 | { key: 'email', mapping: ({ model }) => model.email.replace('@example.com', '@gmail.com') } 390 | ] 391 | 392 | const { value } = useTransform(input, fields); 393 | 394 | expect(value).toEqual({ 395 | firstName: 'John', 396 | lastName: 'Doe', 397 | email: 'john@gmail.com', 398 | }); 399 | }) 400 | 401 | it('should handle wildcard with specific array field override', () => { 402 | const input = { 403 | users: [{ 404 | firstName: 'John', 405 | lastName: 'Doe', 406 | email: 'john@example.com', 407 | }, { 408 | firstName: 'Jane', 409 | lastName: 'Doe', 410 | email: 'jane@example.com', 411 | }] 412 | } 413 | 414 | const fields = [ 415 | { 416 | key: 'users', 417 | fields: [ 418 | '*', 419 | { key: 'email', mapping: ({ model }) => model.email.replace('@example.com', '@gmail.com') } 420 | ] 421 | } 422 | ] 423 | 424 | const { value } = useTransform(input, fields); 425 | 426 | expect(value).toEqual({ 427 | users: [ 428 | { firstName: 'John', lastName: 'Doe', email: 'john@gmail.com' }, 429 | { firstName: 'Jane', lastName: 'Doe', email: 'jane@gmail.com' } 430 | ] 431 | }); 432 | }) 433 | 434 | it('should handle wildcard on root level with array', () => { 435 | const input = [{ 436 | firstName: 'John', 437 | lastName: 'Doe', 438 | email: 'john@example.com', 439 | }, { 440 | firstName: 'Jane', 441 | lastName: 'Doe', 442 | email: 'jane@example.com', 443 | }] 444 | 445 | const fields = [ 446 | '*', 447 | { key: 'email', mapping: ({ model }) => model.email.replace('@example.com', '@gmail.com') } 448 | ] 449 | 450 | const { value } = useTransform(input, fields); 451 | 452 | expect(value).toEqual( 453 | [ 454 | { firstName: 'John', lastName: 'Doe', email: 'john@gmail.com' }, 455 | { firstName: 'Jane', lastName: 'Doe', email: 'jane@gmail.com' } 456 | ] 457 | ); 458 | }) 459 | 460 | it('should create field with key if default value is provided', () => { 461 | const input = { 462 | firstName: 'John', 463 | lastName: 'Doe', 464 | email: 'john@example.com' 465 | } 466 | 467 | const fields = [ 468 | { 469 | key: 'password', 470 | default: '123456' 471 | } 472 | ] 473 | 474 | const { value } = useTransform(input, fields); 475 | 476 | expect(value).toEqual({ 477 | password: '123456' 478 | }) 479 | }) 480 | 481 | it('should create field with key if default value is provided and wildcard', () => { 482 | const input = { 483 | firstName: 'John', 484 | lastName: 'Doe', 485 | email: 'john@example.com' 486 | } 487 | 488 | const fields = [ 489 | '*', 490 | { 491 | key: 'password', 492 | default: '123456' 493 | } 494 | ] 495 | 496 | const { value } = useTransform(input, fields); 497 | 498 | expect(value).toEqual({ 499 | firstName: 'John', 500 | lastName: 'Doe', 501 | email: 'john@example.com', 502 | password: '123456' 503 | }) 504 | }) 505 | 506 | }); 507 | -------------------------------------------------------------------------------- /packages/core/src/runtime/utils/transform.ts: -------------------------------------------------------------------------------- 1 | import { unref, type MaybeRef } from 'vue'; 2 | import { IContext } from './context'; 3 | import { camelCase, isEmpty, isObject, get, set } from '.'; 4 | 5 | export type MappingFunction = (args: { model: any, key?: string, newModel: any, parentModel?: any, originModel?: any, context?: IContext }) => any; 6 | export type FilterFunction = (m: any) => boolean; 7 | export type Field = FieldObject | string 8 | 9 | type WithoutBoth = 10 | (T & { [K in K1]?: never } & { [K in K2]?: never }) | 11 | (T & { [K in K1]: T[K1] } & { [K in K2]?: never }) | 12 | (T & { [K in K1]?: never } & { [K in K2]: T[K2] }); 13 | 14 | export interface FieldObjectBase { 15 | key: string; 16 | newKey?: string; 17 | fields?: Field[]; 18 | mapping?: MappingFunction; 19 | filter?: FilterFunction; 20 | default?: any; 21 | merge?: boolean; 22 | scope?: string; 23 | path?: string; 24 | omit?: string[]; 25 | } 26 | 27 | export type FieldObject = WithoutBoth; 28 | 29 | export type TransformFormat = 'camelCase'; 30 | 31 | export interface ITransformOptions { 32 | scope?: string; 33 | format?: TransformFormat; 34 | context?: IContext; 35 | } 36 | 37 | function formatKey(key: string, format?: TransformFormat) { 38 | switch (format) { 39 | case 'camelCase': { 40 | return camelCase(key) 41 | } 42 | default: return key 43 | } 44 | } 45 | 46 | function extractModel(fields: Field[] = [], model: any, context?: IContext, format?: TransformFormat, parentModel?: any, originModel?: any): T | null | undefined { 47 | if (isEmpty(model)) return null; 48 | 49 | // Inject originInput 50 | if (!originModel) originModel = model 51 | 52 | // Init output model 53 | const newModel: any = {} 54 | 55 | fields.forEach((field: Field) => { 56 | let updatedModel = model 57 | 58 | const hasPath = (field as FieldObject).path 59 | const hasScope = (field as FieldObject).scope 60 | const hasFields = (field as FieldObject).fields 61 | const hasMapping = (field as FieldObject).mapping 62 | 63 | let key: string = (isObject(field) ? (field as FieldObject).newKey || (field as FieldObject).key : field) as string 64 | 65 | // We normalize to camelCase if format is true 66 | const normalizedKey = formatKey(key, format) 67 | 68 | // Update current value with custom path 69 | if (isObject(field) && ((field as FieldObject).path || (field as FieldObject).scope)) { 70 | const pathValue = get(originModel, (field as FieldObject).path || (field as FieldObject).scope) 71 | 72 | if ((field as FieldObject).path) set(newModel, normalizedKey, pathValue) 73 | 74 | updatedModel = pathValue 75 | // We set key as new path has been created 76 | if ((field as FieldObject).path) key = '' 77 | } 78 | 79 | // Check for empty value and handle default value 80 | if (isObject(field)) { 81 | if (updatedModel === null || isEmpty(updatedModel) || get(updatedModel, (field as FieldObject).key) === null || get(updatedModel, (field as FieldObject).key) === undefined) { 82 | if ((field as FieldObject).default) set(newModel, normalizedKey, (typeof (field as FieldObject).default === 'function') ? (field as FieldObject).default(context) : (field as FieldObject).default) 83 | if (!hasFields && !hasMapping) return 84 | } 85 | } else if (model === null || isEmpty(model) || get(model, field as string) === null) return 86 | 87 | // Return value if only key access 88 | if ((isObject(field) && (!hasMapping && !hasFields && !hasPath) || !isObject(field))) { 89 | set(newModel, normalizedKey, get(model, isObject(field) ? (field as FieldObject).key as string : field as string)) 90 | return get(newModel, normalizedKey) 91 | } 92 | 93 | const mapFields = (sourceModel: any) => { 94 | const hasFieldsScopeOrPath = (field as FieldObject).fields.findIndex((f: Field) => isObject(f) && ((f as FieldObject).scope || (f as FieldObject).path)) !== -1 95 | 96 | // Ignore mapping if currentVaule is undefined and no field with scope / path provided 97 | if (!hasFieldsScopeOrPath && !sourceModel) return 98 | 99 | if (!sourceModel && (field as FieldObject).default) return (field as FieldObject).default 100 | 101 | // We inject model with key if no scope or path 102 | if (!hasScope && !hasPath) { 103 | sourceModel = get(sourceModel, (field as FieldObject).key as string) 104 | } 105 | 106 | // Handle array 107 | if (Array.isArray(sourceModel)) 108 | return sourceModel.filter((m: any) => { 109 | if ((field as FieldObject).filter) return (field as FieldObject).filter(m) 110 | else return true 111 | }).map((m: any) => { 112 | return extractModel((field as FieldObject).fields, m, context, format, sourceModel, originModel) 113 | }) 114 | else { 115 | return extractModel((field as FieldObject).fields, sourceModel, context, format, sourceModel, originModel) 116 | } 117 | } 118 | 119 | let result = undefined 120 | 121 | // Handle mapping 122 | if ((field as FieldObject).mapping) { 123 | try { 124 | // Mapping method should always return a value (`return` will break the `forEach` method) 125 | result = ((field as FieldObject).mapping as MappingFunction)({ model: (field as FieldObject).scope ? get(originModel, (field as FieldObject).scope) : updatedModel, key: (field as FieldObject).key, newModel, parentModel, originModel, context }) 126 | } catch (err) { 127 | console.error('error of mapping', err) 128 | } 129 | } 130 | // Handle fields and inject mapping result if present 131 | if ((field as FieldObject).fields) result = mapFields(result ? { [`${(field as FieldObject).key}`]: result, ...parentModel } : updatedModel) 132 | if (!(field as FieldObject).mapping && !((field as FieldObject).fields) && (field as FieldObject).default) result = get(model, (field as FieldObject).key) || (field as FieldObject).default 133 | 134 | // Avoid adding mapping result when undefined 135 | if (result !== undefined) { 136 | if ((field as FieldObject).merge) Object.assign(newModel, result) 137 | else set(newModel, normalizedKey, result) 138 | } 139 | }) 140 | 141 | return newModel 142 | } 143 | 144 | function expandWildcardFields(fields: Field[], model: any): Field[] { 145 | const createFieldObject = (field: FieldObjectBase): FieldObject => { 146 | const { scope, path, ...rest } = field; 147 | 148 | if (scope && path) { 149 | throw new Error("FieldObject cannot have both 'scope' and 'path'."); 150 | } 151 | 152 | return { 153 | ...rest, 154 | ...(scope ? { scope } : {}), 155 | ...(path ? { path } : {}), 156 | } as FieldObject 157 | }; 158 | 159 | function expandField(field: Field, currentModel: any): Field[] { 160 | if (typeof field === 'string') { 161 | return expandWildcardString(field, currentModel).map(expandedField => expandedField); 162 | } 163 | 164 | if (typeof field !== 'object') return [field]; 165 | 166 | const { key, newKey, fields: subFields, omit = [], path, ...rest } = field; 167 | 168 | let expandedFields: Field[] = []; 169 | let subModel = path ? get(model, path) : get(currentModel, key); 170 | 171 | if (key.includes('*')) { 172 | const expandedKeys = expandWildcardString(key, currentModel); 173 | expandedFields = expandedKeys.flatMap(expandedKey => { 174 | if (subFields) { 175 | return { 176 | key: expandedKey, 177 | fields: expandSubFields(subFields, get(currentModel, expandedKey), omit), 178 | ...rest 179 | }; 180 | } else { 181 | return expandedKey; 182 | } 183 | }); 184 | } else if (key === '*' && Array.isArray(subModel)) { 185 | expandedFields = subModel.map((_, index) => `${index}`); 186 | } else { 187 | if (subFields) { 188 | expandedFields.push(createFieldObject({ 189 | ...(newKey ? { newKey } : {}), 190 | key, 191 | fields: expandSubFields(subFields, subModel, omit), 192 | ...rest, 193 | ...(path ? { path } : {}) 194 | })) 195 | } else if (key === '*') { 196 | if (isObject(subModel)) { 197 | expandedFields = Object.keys(subModel) 198 | .filter(k => !omit.includes(k)); 199 | } 200 | } else if (key.endsWith('.*')) { 201 | const baseKey = key.slice(0, -2); 202 | if (isObject(subModel)) { 203 | expandedFields = Object.keys(subModel) 204 | .filter(k => !omit.includes(k)) 205 | .map(k => `${baseKey}.${k}`); 206 | } 207 | } else { 208 | expandedFields.push(createFieldObject({ ...field, ...(path ? { path } : {}) })); 209 | } 210 | } 211 | 212 | // Apply omit filter after expansion for wildcard fields 213 | if ((key === '*' || key.endsWith('.*')) && omit.length > 0) { 214 | expandedFields = expandedFields.filter(f => { 215 | const fieldKey = typeof f === 'string' ? f : f.key; 216 | return !omit.includes(fieldKey); 217 | }); 218 | } 219 | 220 | return expandedFields; 221 | } 222 | 223 | function expandSubFields(subFields: Field[], subModel: any, parentOmit: string[]): Field[] { 224 | let expandedSubFields: Field[] = []; 225 | let customFields: Field[] = []; 226 | 227 | subFields.forEach(subField => { 228 | if (typeof subField === 'object' && subField.key === '*') { 229 | if (isObject(subModel)) { 230 | const omit = [...parentOmit, ...(subField.omit || [])]; 231 | expandedSubFields = Object.keys(subModel) 232 | .filter(k => !omit.includes(k)) 233 | .map(k => k); 234 | } 235 | } else if (typeof subField === 'object' && !subField.key?.includes('*')) { 236 | customFields.push(subField); 237 | } else { 238 | const subSubModel = Array.isArray(subModel) ? subModel[0] : subModel; 239 | expandedSubFields = [...expandedSubFields, ...expandField(subField, subSubModel)]; 240 | } 241 | }); 242 | 243 | if (subFields.includes('*')) { 244 | const subSubModel = Array.isArray(subModel) ? subModel[0] : subModel; 245 | const allKeys = isObject(subSubModel) ? Object.keys(subSubModel) : []; 246 | expandedSubFields = [ 247 | ...expandedSubFields, 248 | ...allKeys.filter(k => !parentOmit.includes(k) && !expandedSubFields.some(f => (typeof f === 'string' ? f : f.key) === k)) 249 | ]; 250 | } 251 | 252 | return [...expandedSubFields, ...customFields]; 253 | } 254 | 255 | // Apply omit filter at the end of the entire expansion process 256 | const expandedFields = fields.flatMap(field => expandField(field, model)); 257 | return expandedFields.filter(field => { 258 | if (typeof field === 'object' && field.key === '*' && field.omit) { 259 | return !field.omit.includes(field.key); 260 | } 261 | return true; 262 | }); 263 | } 264 | 265 | function expandWildcardString(str: string, model: any): string[] { 266 | const parts = str.split('.'); 267 | const wildcardIndex = parts.indexOf('*'); 268 | 269 | if (wildcardIndex === -1) return [str]; 270 | 271 | const beforeWildcard = parts.slice(0, wildcardIndex); 272 | const afterWildcard = parts.slice(wildcardIndex + 1); 273 | 274 | let currentObject = model; 275 | for (const part of beforeWildcard) { 276 | if (!currentObject || typeof currentObject !== 'object') return [str]; 277 | currentObject = currentObject[part]; 278 | } 279 | 280 | if (!currentObject || typeof currentObject !== 'object') return [str]; 281 | 282 | return Object.keys(currentObject).flatMap(key => { 283 | const newStr = [...beforeWildcard, key, ...afterWildcard].join('.'); 284 | if (newStr.includes('*')) { 285 | return expandWildcardString(newStr, model); 286 | } 287 | return [newStr]; 288 | }); 289 | } 290 | 291 | export function useTransform(model: MaybeRef, fields: Field[], options?: ITransformOptions) { 292 | const unrefModel = unref(model); 293 | 294 | function getEmpty(): Partial { 295 | const emptyModel: Partial = {}; 296 | 297 | function processFields(fields: Field[], currentModel: any) { 298 | fields.forEach(field => { 299 | if (typeof field === 'string') { 300 | set(currentModel, field, null); 301 | } else if (typeof field === 'object') { 302 | const { key, fields: subFields, default: defaultValue } = field; 303 | const value = defaultValue !== undefined ? defaultValue : null; 304 | 305 | if (subFields) { 306 | const subModel = {}; 307 | set(currentModel, key, subModel); 308 | processFields(subFields, subModel); 309 | } else { 310 | set(currentModel, key, value); 311 | } 312 | } 313 | }); 314 | } 315 | 316 | const expandedFields = Array.isArray(unrefModel) 317 | ? unrefModel.map(item => expandWildcardFields(fields, item)) 318 | : expandWildcardFields(fields, unrefModel); 319 | 320 | if (Array.isArray(expandedFields)) { 321 | expandedFields.forEach(fields => processFields(fields, emptyModel)); 322 | } else { 323 | processFields(expandedFields, emptyModel); 324 | } 325 | 326 | return emptyModel; 327 | } 328 | 329 | const transformedValue = Array.isArray(unrefModel) 330 | ? unrefModel.map(item => extractModel(expandWildcardFields(fields, item), item, options?.context || {}, options?.format)) 331 | : extractModel(expandWildcardFields(fields, unrefModel), unrefModel, options?.context || {}, options?.format); 332 | 333 | return { 334 | getEmpty, 335 | value: transformedValue 336 | }; 337 | } 338 | 339 | // const input = { 340 | // user: { 341 | // name: 'Jane Doe', 342 | // details: { 343 | // age: 25, 344 | // city: 'New York', 345 | // }, 346 | // }, 347 | // }; 348 | 349 | // const fields = [ 350 | // { 351 | // key: 'user', 352 | // fields: [ 353 | // 'name', 354 | // { 355 | // key: 'details', 356 | // fields: [ 357 | // 'age', 358 | // { key: 'city', newKey: 'location' }, 359 | // ] 360 | // }, 361 | // ] 362 | // }, 363 | // ]; 364 | 365 | export function newExtractModel(fields: Field[], model: any, format?: TransformFormat, parentModel?: any, originModel?: any) { 366 | // Ignore empty model 367 | if (isEmpty(model)) return null; 368 | 369 | // Init originModel for recursive 370 | if (!originModel) originModel = model 371 | 372 | // Init output model 373 | const outputModel: any = {} 374 | 375 | const currentModel = model 376 | 377 | fields.forEach((field: Field) => { 378 | const isObjectField = isObject(field) 379 | const hasMapping = (field as FieldObject).mapping 380 | const hasFields = (field as FieldObject).fields 381 | const hasPath = (field as FieldObject).path 382 | 383 | let key: string = (isObjectField ? (field as FieldObject).newKey || (field as FieldObject).key : field) as string 384 | 385 | // We normalize to camelCase if format is true 386 | const normalizedKey = formatKey(key, format) 387 | 388 | // Return value if only key access 389 | if (isObjectField && !hasMapping && !hasFields && !hasPath || !isObject(field)) { 390 | set(outputModel, normalizedKey, get(model, isObject(field) ? (field as FieldObject).key as string : field as string)) 391 | 392 | // Exist current field iteration 393 | return 394 | } 395 | 396 | let result: any = undefined 397 | 398 | // Map field function 399 | const mapField = () => { 400 | // We check if key is 401 | const sourceModel = currentModel[normalizedKey] 402 | 403 | if (Array.isArray(sourceModel)) return sourceModel.map((item: any) => newExtractModel((field as FieldObject).fields, item, format, currentModel, originModel)) 404 | else return newExtractModel((field as FieldObject).fields, sourceModel, format, currentModel, originModel) 405 | } 406 | 407 | // Handle object field 408 | if (isObjectField && hasFields) { 409 | result = mapField() 410 | } 411 | 412 | if (result !== undefined) { 413 | set(outputModel, normalizedKey, result) 414 | } 415 | }) 416 | 417 | return outputModel 418 | } 419 | 420 | export function newUseTransform(model: MaybeRef, fields: Field[], options?: ITransformOptions) { 421 | const unrefModel = unref(model); 422 | 423 | return newExtractModel(fields, unrefModel, options?.format) 424 | } -------------------------------------------------------------------------------- /packages/core/src/runtime/utils/transform.debug.ts: -------------------------------------------------------------------------------- 1 | import { ref, unref, type MaybeRef } from 'vue'; 2 | import { IContext } from './context'; 3 | import { camelCase, isEmpty, isObject, get, set } from '.'; 4 | 5 | export type MappingFunction = (args: { model: any, key?: string, newModel: any, parentModel?: any, originModel?: any, context?: IContext }) => any; 6 | export type FilterFunction = (m: any) => boolean; 7 | export type Field = FieldObject | string 8 | 9 | type WithoutBoth = 10 | (T & { [K in K1]?: never } & { [K in K2]?: never }) | 11 | (T & { [K in K1]: T[K1] } & { [K in K2]?: never }) | 12 | (T & { [K in K1]?: never } & { [K in K2]: T[K2] }); 13 | 14 | export interface FieldObjectBase { 15 | key: string; 16 | newKey?: string; 17 | fields?: Field[]; 18 | mapping?: MappingFunction; 19 | filter?: FilterFunction; 20 | default?: any; 21 | merge?: boolean; 22 | scope?: string; 23 | path?: string; 24 | omit?: string[]; 25 | } 26 | 27 | export type FieldObject = WithoutBoth; 28 | 29 | export type TransformFormat = 'camelCase'; 30 | 31 | export interface ITransformOptions { 32 | scope?: string; 33 | format?: TransformFormat; 34 | context?: IContext; 35 | } 36 | 37 | function formatKey(key: string, format?: TransformFormat) { 38 | switch (format) { 39 | case 'camelCase': { 40 | return camelCase(key) 41 | } 42 | default: return key 43 | } 44 | } 45 | 46 | function extractModel(fields: Field[] = [], model: any, context?: IContext, format?: TransformFormat, parentModel?: any, originModel?: any): T | null | undefined { 47 | if (isEmpty(model)) return null; 48 | 49 | // Inject originInput 50 | if (!originModel) originModel = model 51 | 52 | // Init output model 53 | const newModel: any = {} 54 | 55 | fields.forEach((field: Field) => { 56 | console.log('handle field', field, 'with model', model) 57 | let updatedModel = model 58 | 59 | const hasPath = (field as FieldObject).path 60 | const hasScope = (field as FieldObject).scope 61 | 62 | let key: string = (isObject(field) ? (field as FieldObject).newKey || (field as FieldObject).key : field) as string 63 | 64 | // We normalize to camelCase if format is true 65 | const normalizedKey = formatKey(key, format) 66 | 67 | // Update current value with custom path 68 | if (isObject(field) && ((field as FieldObject).path || (field as FieldObject).scope)) { 69 | const pathValue = get(originModel, (field as FieldObject).path || (field as FieldObject).scope) 70 | 71 | if ((field as FieldObject).path) set(newModel, normalizedKey, pathValue) 72 | 73 | updatedModel = pathValue 74 | // We set key as new path has been created 75 | if ((field as FieldObject).path) key = '' 76 | } 77 | 78 | console.log('updateModel is now', updatedModel) 79 | 80 | // Check for empty value and handle default value 81 | if (isObject(field)) { 82 | if (updatedModel === null || isEmpty(updatedModel) || !(field as FieldObject).mapping || ((field as FieldObject).path || (!(field as FieldObject).fields)) ? false : get(updatedModel, (field as FieldObject).key) === null || get(updatedModel, (field as FieldObject).key) === undefined) { 83 | if (!(field as FieldObject).default) return 84 | 85 | set(newModel, normalizedKey, (typeof (field as FieldObject).default === 'function') ? (field as FieldObject).default(context) : (field as FieldObject).default) 86 | return 87 | } 88 | } else if (model === null || isEmpty(model) || get(model, field as string) === null) return 89 | 90 | console.log('not emtpy value') 91 | 92 | // Return value if only key access 93 | if ((isObject(field) && (!(field as FieldObject).mapping && !((field as FieldObject).fields)) && !((field as FieldObject).path)) || !isObject(field)) { 94 | console.log('return value if only key access') 95 | set(newModel, normalizedKey, get(model, isObject(field) ? (field as FieldObject).key as string : field as string)) 96 | return get(newModel, normalizedKey) 97 | } 98 | 99 | const mapFields = (sourceModel: any) => { 100 | console.log('map fields', sourceModel) 101 | const hasFieldsScopeOrPath = (field as FieldObject).fields.findIndex((f: Field) => isObject(f) && ((f as FieldObject).scope || (f as FieldObject).path)) !== -1 102 | 103 | // Ignore mapping if currentVaule is undefined and no field with scope / path provided 104 | if (!hasFieldsScopeOrPath && !sourceModel) return 105 | 106 | if (!sourceModel && (field as FieldObject).default) return (field as FieldObject).default 107 | 108 | // We inject model with key if no scope or path 109 | if (!hasScope && !hasPath) { 110 | console.log('inject sourcemodel') 111 | sourceModel = get(sourceModel, (field as FieldObject).key as string) 112 | } 113 | 114 | console.log('handle fields') 115 | // Handle array 116 | if (Array.isArray(sourceModel)) 117 | return sourceModel.filter((m: any) => { 118 | if ((field as FieldObject).filter) return (field as FieldObject).filter(m) 119 | else return true 120 | }).map((m: any) => { 121 | return extractModel((field as FieldObject).fields, m, context, format, sourceModel, originModel) 122 | }) 123 | else { 124 | return extractModel((field as FieldObject).fields, sourceModel, context, format, sourceModel, originModel) 125 | } 126 | } 127 | 128 | let result = undefined 129 | 130 | // Handle mapping 131 | if ((field as FieldObject).mapping) { 132 | console.log('handle mapping') 133 | try { 134 | // Mapping method should always return a value (`return` will break the `forEach` method) 135 | result = ((field as FieldObject).mapping as MappingFunction)({ model: (field as FieldObject).scope ? get(originModel, (field as FieldObject).scope) : updatedModel, key: (field as FieldObject).key, newModel, parentModel, originModel, context }) 136 | } catch (err) { 137 | console.error('error of mapping', err) 138 | } 139 | } 140 | // Handle fields and inject mapping result if present 141 | if ((field as FieldObject).fields) result = mapFields(result ? { [`${(field as FieldObject).key}`]: result, ...parentModel } : updatedModel) 142 | if (!(field as FieldObject).mapping && !((field as FieldObject).fields) && (field as FieldObject).default) result = get(model, (field as FieldObject).key) || (field as FieldObject).default 143 | 144 | // Avoid adding mapping result when undefined 145 | if (result !== undefined) { 146 | if ((field as FieldObject).merge) Object.assign(newModel, result) 147 | else set(newModel, normalizedKey, result) 148 | } 149 | }) 150 | 151 | return newModel 152 | } 153 | 154 | function expandWildcardFields(fields: Field[], model: any): Field[] { 155 | const createFieldObject = (field: FieldObjectBase): FieldObject => { 156 | const { scope, path, ...rest } = field; 157 | 158 | if (scope && path) { 159 | throw new Error("FieldObject cannot have both 'scope' and 'path'."); 160 | } 161 | 162 | return { 163 | ...rest, 164 | ...(scope ? { scope } : {}), 165 | ...(path ? { path } : {}), 166 | } as FieldObject 167 | }; 168 | 169 | function expandField(field: Field, currentModel: any): Field[] { 170 | if (typeof field === 'string') { 171 | return expandWildcardString(field, currentModel).map(expandedField => expandedField); 172 | } 173 | 174 | if (typeof field !== 'object') return [field]; 175 | 176 | const { key, newKey, fields: subFields, omit = [], path, ...rest } = field; 177 | 178 | let expandedFields: Field[] = []; 179 | let subModel = path ? get(model, path) : get(currentModel, key); 180 | 181 | if (key.includes('*')) { 182 | const expandedKeys = expandWildcardString(key, currentModel); 183 | expandedFields = expandedKeys.flatMap(expandedKey => { 184 | if (subFields) { 185 | return { 186 | key: expandedKey, 187 | fields: expandSubFields(subFields, get(currentModel, expandedKey), omit), 188 | ...rest 189 | }; 190 | } else { 191 | return expandedKey; 192 | } 193 | }); 194 | } else if (key === '*' && Array.isArray(subModel)) { 195 | expandedFields = subModel.map((_, index) => `${index}`); 196 | } else { 197 | if (subFields) { 198 | expandedFields.push(createFieldObject({ 199 | ...(newKey ? { newKey } : {}), 200 | key, 201 | fields: expandSubFields(subFields, subModel, omit), 202 | ...rest, 203 | ...(path ? { path } : {}) 204 | })) 205 | } else if (key === '*') { 206 | if (isObject(subModel)) { 207 | expandedFields = Object.keys(subModel) 208 | .filter(k => !omit.includes(k)); 209 | } 210 | } else if (key.endsWith('.*')) { 211 | const baseKey = key.slice(0, -2); 212 | if (isObject(subModel)) { 213 | expandedFields = Object.keys(subModel) 214 | .filter(k => !omit.includes(k)) 215 | .map(k => `${baseKey}.${k}`); 216 | } 217 | } else { 218 | expandedFields.push(createFieldObject({ ...field, ...(path ? { path } : {}) })); 219 | } 220 | } 221 | 222 | // Apply omit filter after expansion for wildcard fields 223 | if ((key === '*' || key.endsWith('.*')) && omit.length > 0) { 224 | expandedFields = expandedFields.filter(f => { 225 | const fieldKey = typeof f === 'string' ? f : f.key; 226 | return !omit.includes(fieldKey); 227 | }); 228 | } 229 | 230 | return expandedFields; 231 | } 232 | 233 | function expandSubFields(subFields: Field[], subModel: any, parentOmit: string[]): Field[] { 234 | let expandedSubFields: Field[] = []; 235 | let customFields: Field[] = []; 236 | 237 | subFields.forEach(subField => { 238 | if (typeof subField === 'object' && subField.key === '*') { 239 | if (isObject(subModel)) { 240 | const omit = [...parentOmit, ...(subField.omit || [])]; 241 | expandedSubFields = Object.keys(subModel) 242 | .filter(k => !omit.includes(k)) 243 | .map(k => k); 244 | } 245 | } else if (typeof subField === 'object' && !subField.key?.includes('*')) { 246 | customFields.push(subField); 247 | } else { 248 | const subSubModel = Array.isArray(subModel) ? subModel[0] : subModel; 249 | expandedSubFields = [...expandedSubFields, ...expandField(subField, subSubModel)]; 250 | } 251 | }); 252 | 253 | if (subFields.includes('*')) { 254 | const subSubModel = Array.isArray(subModel) ? subModel[0] : subModel; 255 | const allKeys = isObject(subSubModel) ? Object.keys(subSubModel) : []; 256 | expandedSubFields = [ 257 | ...expandedSubFields, 258 | ...allKeys.filter(k => !parentOmit.includes(k) && !expandedSubFields.some(f => (typeof f === 'string' ? f : f.key) === k)) 259 | ]; 260 | } 261 | 262 | return [...expandedSubFields, ...customFields]; 263 | } 264 | 265 | // Apply omit filter at the end of the entire expansion process 266 | const expandedFields = fields.flatMap(field => expandField(field, model)); 267 | return expandedFields.filter(field => { 268 | if (typeof field === 'object' && field.key === '*' && field.omit) { 269 | return !field.omit.includes(field.key); 270 | } 271 | return true; 272 | }); 273 | } 274 | 275 | function expandWildcardString(str: string, model: any): string[] { 276 | const parts = str.split('.'); 277 | const wildcardIndex = parts.indexOf('*'); 278 | 279 | if (wildcardIndex === -1) return [str]; 280 | 281 | const beforeWildcard = parts.slice(0, wildcardIndex); 282 | const afterWildcard = parts.slice(wildcardIndex + 1); 283 | 284 | let currentObject = model; 285 | for (const part of beforeWildcard) { 286 | if (!currentObject || typeof currentObject !== 'object') return [str]; 287 | currentObject = currentObject[part]; 288 | } 289 | 290 | if (!currentObject || typeof currentObject !== 'object') return [str]; 291 | 292 | return Object.keys(currentObject).flatMap(key => { 293 | const newStr = [...beforeWildcard, key, ...afterWildcard].join('.'); 294 | if (newStr.includes('*')) { 295 | return expandWildcardString(newStr, model); 296 | } 297 | return [newStr]; 298 | }); 299 | } 300 | 301 | export function useTransform(model: MaybeRef, fields: Field[], options?: ITransformOptions) { 302 | const unrefModel = unref(model); 303 | const expandedFields = expandWildcardFields(fields, unrefModel); 304 | 305 | function getEmpty(): Partial { 306 | const emptyModel: Partial = {}; 307 | 308 | function processFields(fields: Field[], currentModel: any) { 309 | fields.forEach(field => { 310 | if (typeof field === 'string') { 311 | set(currentModel, field, null); 312 | } else if (typeof field === 'object') { 313 | const { key, fields: subFields, default: defaultValue } = field; 314 | const value = defaultValue !== undefined ? defaultValue : null; 315 | 316 | if (subFields) { 317 | const subModel = {}; 318 | set(currentModel, key, subModel); 319 | processFields(subFields, subModel); 320 | } else { 321 | set(currentModel, key, value); 322 | } 323 | } 324 | }); 325 | } 326 | 327 | processFields(expandedFields, emptyModel); 328 | 329 | return emptyModel; 330 | } 331 | 332 | console.dir(expandedFields, { depth: null }) 333 | 334 | 335 | return { 336 | getEmpty, 337 | value: extractModel(expandedFields, unrefModel, options?.context || {}, options?.format) 338 | }; 339 | } 340 | 341 | // const input = { 342 | // user: { 343 | // name: 'Jane Doe', 344 | // details: { 345 | // age: 25, 346 | // city: 'New York', 347 | // }, 348 | // }, 349 | // }; 350 | 351 | // const fields = [ 352 | // { 353 | // key: 'user', 354 | // fields: [ 355 | // 'name', 356 | // { 357 | // key: 'details', 358 | // fields: [ 359 | // 'age', 360 | // { key: 'city', newKey: 'location' }, 361 | // ] 362 | // }, 363 | // ] 364 | // }, 365 | // ]; 366 | 367 | export function newExtractModel(fields: Field[], model: any, format?: TransformFormat, parentModel?: any, originModel?: any) { 368 | // Ignore empty model 369 | if (isEmpty(model)) return null; 370 | 371 | // Init originModel for recursive 372 | if (!originModel) originModel = model 373 | 374 | // Init output model 375 | const outputModel: any = {} 376 | 377 | const currentModel = model 378 | 379 | fields.forEach((field: Field) => { 380 | console.log('field', field, 'with model', model) 381 | const isObjectField = isObject(field) 382 | const hasMapping = (field as FieldObject).mapping 383 | const hasFields = (field as FieldObject).fields 384 | const hasPath = (field as FieldObject).path 385 | 386 | let key: string = (isObjectField ? (field as FieldObject).newKey || (field as FieldObject).key : field) as string 387 | 388 | // We normalize to camelCase if format is true 389 | const normalizedKey = formatKey(key, format) 390 | 391 | // Return value if only key access 392 | if (isObjectField && !hasMapping && !hasFields && !hasPath || !isObject(field)) { 393 | console.log('on set non ?') 394 | set(outputModel, normalizedKey, get(model, isObject(field) ? (field as FieldObject).key as string : field as string)) 395 | 396 | // Exist current field iteration 397 | return 398 | } 399 | 400 | let result: any = undefined 401 | 402 | // Map field function 403 | const mapField = () => { 404 | // We check if key is 405 | console.log('map field', normalizedKey) 406 | const sourceModel = currentModel[normalizedKey] 407 | 408 | if (Array.isArray(sourceModel)) return sourceModel.map((item: any) => newExtractModel((field as FieldObject).fields, item, format, currentModel, originModel)) 409 | else return newExtractModel((field as FieldObject).fields, sourceModel, format, currentModel, originModel) 410 | } 411 | 412 | // Handle object field 413 | if (isObjectField && hasFields) { 414 | result = mapField() 415 | } 416 | 417 | if (result !== undefined) { 418 | set(outputModel, normalizedKey, result) 419 | } 420 | }) 421 | 422 | return outputModel 423 | } 424 | 425 | export function newUseTransform(model: MaybeRef, fields: Field[], options?: ITransformOptions) { 426 | const unrefModel = unref(model); 427 | 428 | return newExtractModel(fields, unrefModel, options?.format) 429 | } -------------------------------------------------------------------------------- /apps/docs/docs/guide/composables/mapping.md: -------------------------------------------------------------------------------- 1 | # Mapping System 2 | 3 | The mapping system is a powerful feature that allows you to transform data structures according to defined rules. It's particularly useful for adapting complex data structures or normalizing data from various sources. 4 | 5 | ## Key Concepts 6 | 7 | ### Field 8 | 9 | ```ts 10 | export type Field = FieldObject | string 11 | ``` 12 | 13 | A `Field` can be either a simple string or a `FieldObject` 14 | 15 | ### FieldObject 16 | 17 | ```ts 18 | export interface FieldObject { 19 | key: string; 20 | newKey?: string; 21 | path?: string; 22 | fields?: Field[]; 23 | mapping?: MappingFunction; 24 | filter?: FilterFunction; 25 | default?: any; 26 | merge?: boolean; 27 | scope?: string; 28 | omit?: string[]; 29 | } 30 | ``` 31 | 32 | - `key`: The original key to read from the source object. 33 | - `newKey`: The new key if you need to change the original one. 34 | - `path`: A custom path to retrieve the value from the source object, allowing for more flexible data access. 35 | - `fields`: An array of `Field` for recursive mapping of nested objects or arrays. 36 | - `mapping`: A custom transformation function. 37 | - `filter`: A function to filter array elements. 38 | - `default`: A default value or function to use when the source value is empty. 39 | - `merge`: A boolean indicating whether to merge the result into the parent object. 40 | - `scope`: Specifies a different part of the model to use for this specific field's mapping. 41 | - `omit`: An array of fields to exclude from the transformation process. 42 | 43 | 44 | ### MappingFunction 45 | 46 | ```ts 47 | export type MappingFunction = (args: { 48 | model: any, 49 | key: string, 50 | newModel?: any, 51 | parentModel?: any, 52 | originModel?: any, 53 | context?: IContext 54 | }) => any; 55 | ``` 56 | 57 | - `model: any`: The current value of the field being transformed. 58 | - `key: string`: The key (or name) of the field being transformed. 59 | - `newModel?: any`: The new model being constructed during the transformation. 60 | - `parentModel?: any` 61 | - The parent model of the object being transformed. 62 | - Useful for accessing higher-level data when transforming nested structures. 63 | - `originModel?: any`: The complete original model, before any transformation. 64 | - `context?: IContext`: Allows injection of additional data or functions into the transformation process. 65 | 66 | The MappingFunction uses this information to perform a custom transformation and returns the new transformed value. 67 | 68 | Example: 69 | 70 | ```ts 71 | const model = { 72 | user: { 73 | name: 'John Doe', 74 | age: 30 75 | }, 76 | orders: [ 77 | { id: 1, total: 100 }, 78 | { id: 2, total: 200 } 79 | ] 80 | }; 81 | 82 | const fields = [ 83 | { 84 | key: 'user', 85 | fields: [ 86 | 'name', 87 | { 88 | key: 'age', 89 | mapping: ({ model, parentModel, originModel, context }) => { 90 | const currentYear = context.currentYear; 91 | const birthYear = currentYear - model; 92 | const orderCount = originModel.orders.length; 93 | return `${parentModel.name} was born in ${birthYear} and has ${orderCount} orders.`; 94 | } 95 | } 96 | ] 97 | } 98 | ]; 99 | 100 | const context = { currentYear: 2023 }; 101 | 102 | const { value } = useTransform(model, fields, context); 103 | ``` 104 | 105 | **Output** 106 | ```json 107 | { 108 | "user": { 109 | "name": "John Doe", 110 | "age": "John Doe was born in 1993 and has 2 orders." 111 | } 112 | } 113 | ``` 114 | 115 | In this example, the mapping function uses several properties to create a custom string: 116 | - `model` to access the current age 117 | - `parentModel` to access the user's name 118 | - `originModel` to count the total number of orders 119 | - `context` to get the current year 120 | 121 | This approach allows for very flexible and powerful transformations, giving access to different levels of data in the mapping process. 122 | 123 | ### FilterFunction 124 | 125 | ```ts 126 | export type FilterFunction = (m: any) => boolean; 127 | ``` 128 | 129 | A function that takes a model and returns a boolean, used to filter array elements. 130 | 131 | **Example:** 132 | 133 | ```ts 134 | const model = { 135 | items: [ 136 | { id: 1, name: 'Apple', category: 'Fruit' }, 137 | { id: 2, name: 'Carrot', category: 'Vegetable' }, 138 | { id: 3, name: 'Banana', category: 'Fruit' }, 139 | { id: 4, name: 'Broccoli', category: 'Vegetable' } 140 | ] 141 | }; 142 | 143 | const fields = [ 144 | { 145 | key: 'items', 146 | fields: ['id', 'name'], 147 | filter: (item) => item.category === 'Fruit' 148 | } 149 | ]; 150 | 151 | const { value } = useTransform(model, fields); 152 | ``` 153 | 154 | **Output:** 155 | 156 | ```json 157 | { 158 | "items": [ 159 | { "id": 1, "name": "Apple" }, 160 | { "id": 3, "name": "Banana" } 161 | ] 162 | } 163 | ``` 164 | 165 | In this example, the FilterFunction `(item) => item.category === 'Fruit'` is used to keep only the items with the category 'Fruit'. The resulting transformed object contains only the filtered items, with their `id` and `name` fields preserved as specified in the `fields` array. 166 | 167 | ## Usage 168 | 169 | The main function for using the mapping system is `useTransform`: 170 | 171 | ```ts 172 | const { value, getEmpty } = useTransform(model: MaybeRef, fields: Field[], options?: ITransformOptions) 173 | ``` 174 | 175 | ### Parameters 176 | 177 | - `model`: The source data to transform. 178 | - `fields`: An array of [`Field`](#field) objects 179 | - `options`: [`ITransformOptions`](#itransformoptions). 180 | 181 | ### Return Value 182 | 183 | - `value`: The transformed model. 184 | - `getEmpty`: A function that returns an empty model based on the provided fields. 185 | 186 | ## ITransformOptions 187 | 188 | An interface that defines additional options for the transformation process. 189 | 190 | ```ts 191 | export interface ITransformOptions { 192 | scope?: string; 193 | format?: TransformFormat; 194 | context: IContext; 195 | } 196 | ```` 197 | 198 | #### Properties 199 | 200 | - `scope`: Defines a specific scope for the transformation. This can be used to limit the transformation to a particular part of the model. 201 | - `format`: Specifies the format to be applied to the keys in the transformed object. This could be used for tasks like converting keys to camelCase or snake_case. 202 | - `context`: Provides additional context data that can be used within mapping functions. 203 | 204 | #### Example 205 | 206 | ````ts 207 | const model = { 208 | user: { 209 | user_name: 'John Doe', 210 | user_age: 30, 211 | user_role: 'developer' 212 | }, 213 | company: { 214 | company_name: 'Acme Inc' 215 | } 216 | }; 217 | 218 | const fields = [ 219 | 'user_name', 220 | 'user_age', 221 | { 222 | key: 'user_role', 223 | mapping: ({ model, context }) => `${context.rolePrefix}${model}` 224 | } 225 | ]; 226 | 227 | const options: ITransformOptions = { 228 | scope: 'user', 229 | format: 'camelCase', 230 | context: { 231 | rolePrefix: 'ROLE_' 232 | } 233 | }; 234 | 235 | const { value } = useTransform(model, fields, options); 236 | ```` 237 | 238 | **Output**: 239 | 240 | ```json 241 | { 242 | "userName": "John Doe", 243 | "userAge": 30, 244 | "userRole": "ROLE_developer" 245 | } 246 | ``` 247 | 248 | In this example: 249 | - The `scope` option is set to 'user', limiting the transformation to the 'user' object within the model. 250 | - The `format` option converts the keys to camelCase. 251 | - The `context` could be used in custom mapping functions if needed. 252 | - Note that the 'company' object is not included in the output due to the specified scope. 253 | 254 | ## Wildcard Mapping 255 | 256 | Wildcard mapping allows you to include and transform all fields at a certain level of your object structure or even deeper nested levels. The `expandWildcardFields` function handles the expansion of wildcards in both keys and scopes. 257 | 258 | ### Nested Wildcard with Scope 259 | 260 | You can use wildcards in the `key` property to handle complex nested structures. The `scope` is automatically inherited from the parent field unless explicitly specified: 261 | 262 | ```ts 263 | const model = { 264 | company: { 265 | departments: { 266 | engineering: { employees: 50, budget: { allocated: 1000000 } }, 267 | marketing: { employees: 30, budget: { allocated: 500000 } } 268 | } 269 | } 270 | }; 271 | 272 | const fields = [ 273 | 'company.name', 274 | { 275 | key: 'company.departments.*', 276 | fields: [ 277 | 'employees', 278 | { 279 | key: 'budget.allocated', 280 | mapping: ({ model }) => `$${model.budget.allocated / 1000000}M` 281 | } 282 | ] 283 | } 284 | ]; 285 | 286 | const { value } = useTransform(model, fields); 287 | ``` 288 | 289 | In this example: 290 | 1. The wildcard in `company.departments.*` expands to include all departments. 291 | 2. The `scope` for the nested fields (like `budget.allocated`) is automatically set to `company.departments.*`, inheriting from the parent field. 292 | 3. The mapping function for `budget.allocated` receives the entire department object as its `model`, so we need to access `model.budget.allocated`. 293 | 4. This approach allows for consistent transformations across multiple nested objects while maintaining the correct scope for each transformation. 294 | 295 | Note: You can still explicitly set a `scope` for any field if you need to override the inherited scope. For example: 296 | 297 | ```ts 298 | { 299 | key: 'budget.allocated', 300 | scope: 'company.departments.*.budget', 301 | mapping: ({ model }) => `$${model.allocated / 1000000}M` 302 | } 303 | ``` 304 | 305 | This would set the scope specifically to the budget object, allowing direct access to `allocated`. 306 | 307 | ## Scope 308 | 309 | The `scope` property in a `FieldObject` allows you to specify which part of the model to use for this specific field's mapping. By default, the scope is set to the current value of the `key` property. This means that when mapping nested objects, the scope automatically adjusts to the current level of nesting. 310 | 311 | However, there may be cases where you want to access data from a different part of the model. This is where explicitly setting the `scope` property becomes useful. 312 | 313 | ### Example of default scope behavior and custom scope 314 | 315 | ```ts 316 | const model = { 317 | user: { 318 | name: 'John Doe', 319 | age: 30, 320 | address: { 321 | street: '123 Main St', 322 | city: 'Anytown' 323 | } 324 | }, 325 | company: { 326 | name: 'Acme Inc', 327 | employees: 100 328 | } 329 | }; 330 | 331 | const fields = [ 332 | { 333 | key: 'user', 334 | fields: [ 335 | 'name', 336 | 'age', 337 | { 338 | key: 'location', 339 | mapping: ({ model }) => `${model.address.city}, ${model.address.street}` 340 | }, 341 | { 342 | key: 'companyInfo', 343 | scope: 'company', 344 | mapping: ({ model }) => `Works at ${model.name} with ${model.employees} colleagues` 345 | } 346 | ] 347 | } 348 | ]; 349 | 350 | const { value } = useTransform(model, fields); 351 | ``` 352 | 353 | Output: 354 | ```json 355 | { 356 | "user": { 357 | "name": "John Doe", 358 | "age": 30, 359 | "location": "Anytown, 123 Main St", 360 | "companyInfo": "Works at Acme Inc with 100 colleagues" 361 | } 362 | } 363 | ``` 364 | 365 | In this example: 366 | 1. The `location` field doesn't specify a `scope`, so it uses the default scope (which is `user`). This allows it to access `model.address` directly. 367 | 2. The `companyInfo` field sets its `scope` to `company`. This changes the context of the `model` parameter in its mapping function, allowing it to access company data even though it's being mapped within the `user` object. 368 | 369 | This approach demonstrates how `scope` can be used to access different parts of the model, regardless of where the field is positioned in the mapping structure. It's particularly useful for creating derived fields that combine data from various parts of your model. 370 | 371 | ## Path 372 | 373 | The `path` property in a `FieldObject` allows you to specify a custom path to retrieve the value from the source object. This is particularly useful when you need to access deeply nested properties or when the structure of your source object doesn't match your desired output structure. 374 | 375 | ### Example of using path 376 | 377 | ```ts 378 | const model = { 379 | user: { 380 | personalInfo: { 381 | name: { 382 | first: 'John', 383 | last: 'Doe' 384 | }, 385 | contact: { 386 | email: 'john.doe@example.com' 387 | } 388 | } 389 | } 390 | }; 391 | 392 | const fields = [ 393 | { 394 | key: 'fullName', 395 | path: 'user.personalInfo.name', 396 | mapping: ({ model }) => `${model.first} ${model.last}` 397 | }, 398 | { 399 | key: 'email', 400 | path: 'user.personalInfo.contact.email' 401 | } 402 | ]; 403 | 404 | const { value } = useTransform(model, fields); 405 | ``` 406 | 407 | Output: 408 | ```json 409 | { 410 | "fullName": "John Doe", 411 | "email": "john.doe@example.com" 412 | } 413 | ``` 414 | 415 | In this example: 416 | - The `path` property is used to directly access nested properties in the source object. 417 | - For `fullName`, we use both `path` and `mapping` to create a custom output. 418 | - For `email`, we use `path` to directly retrieve the deeply nested email value. 419 | 420 | The `path` property provides a flexible way to access data within complex object structures, allowing you to flatten nested objects or reorganize your data structure during the transformation process. 421 | 422 | ## Omitting Fields 423 | 424 | When using wildcards to include multiple fields, you may want to exclude specific fields. The `omit` property allows you to specify fields that should be ignored during the transformation process. 425 | 426 | ### Example of using omit 427 | 428 | ```ts 429 | const model = { 430 | name: 'John Doe', 431 | email: 'john@example.com', 432 | password: 'secret123', 433 | preferences: { 434 | theme: 'dark', 435 | notifications: true, 436 | privateInfo: 'sensitive data' 437 | } 438 | }; 439 | 440 | const fields = [ 441 | { 442 | key: '*', 443 | omit: ['password'] 444 | }, 445 | { 446 | key: 'preferences.*', 447 | omit: ['privateInfo'] 448 | } 449 | ]; 450 | 451 | const { value } = useTransform(model, fields); 452 | ``` 453 | 454 | Output: 455 | ```json 456 | { 457 | "name": "John Doe", 458 | "email": "john@example.com", 459 | "preferences": { 460 | "theme": "dark", 461 | "notifications": true 462 | } 463 | } 464 | ``` 465 | 466 | In this example, the `password` field is omitted from the top-level object, and `privateInfo` is omitted from the `preferences` object, even though we're using wildcards to include all other fields. 467 | 468 | ## Examples 469 | 470 | Let's explore a complex example that demonstrates various advanced features of the mapping system: 471 | 472 | ```ts 473 | const companyData = { 474 | info: { 475 | name: 'TechCorp', 476 | founded: 2005, 477 | headquarters: { 478 | city: 'San Francisco', 479 | country: 'USA' 480 | } 481 | }, 482 | departments: { 483 | engineering: { 484 | head: 'Jane Doe', 485 | employeeCount: 50, 486 | projects: [ 487 | { id: 'P1', name: 'Project Alpha', status: 'active', budget: 1000000 }, 488 | { id: 'P2', name: 'Project Beta', status: 'planning', budget: 500000 }, 489 | { id: 'P3', name: 'Project Gamma', status: 'completed', budget: 750000 }, 490 | { id: 'P4', name: 'Project Delta', status: 'active', budget: 1200000 } 491 | ] 492 | }, 493 | marketing: { 494 | head: 'John Smith', 495 | employeeCount: 30, 496 | projects: [ 497 | { id: 'M1', name: 'Brand Refresh', status: 'active', budget: 800000 }, 498 | { id: 'M2', name: 'Social Media Campaign', status: 'planning', budget: 300000 }, 499 | { id: 'M3', name: 'Product Launch', status: 'completed', budget: 500000 } 500 | ] 501 | }, 502 | finance: { 503 | head: 'Alice Johnson', 504 | employeeCount: 15, 505 | budget: 500000, 506 | projects: [ 507 | { id: 'F1', name: 'Cost Optimization', status: 'active', budget: 200000 }, 508 | { id: 'F2', name: 'Financial Reporting System', status: 'planning', budget: 350000 }, 509 | { id: 'F3', name: 'Budget Analysis', status: 'active', budget: 150000 } 510 | ] 511 | } 512 | }, 513 | clients: [ 514 | { id: 1, name: 'Acme Corp', contractValue: 500000, active: true }, 515 | { id: 2, name: 'GlobalTech', contractValue: 750000, active: false }, 516 | { id: 3, name: 'InnoSystems', contractValue: 1000000, active: true }, 517 | { id: 4, name: 'TechGiants', contractValue: 1200000, active: true } 518 | ] 519 | }; 520 | 521 | const currentYear = 2023; 522 | 523 | const fields = [ 524 | { 525 | key: 'companyOverview', 526 | fields: [ 527 | { key: 'name', path: 'info.name' }, 528 | { 529 | key: 'age', 530 | mapping: ({ model }) => currentYear - model.info.founded 531 | }, 532 | { 533 | key: 'location', 534 | mapping: ({ model }) => `${model.info.headquarters.city}, ${model.info.headquarters.country}` 535 | } 536 | ] 537 | }, 538 | { 539 | key: 'departments', 540 | fields: [ 541 | { 542 | key: '*', 543 | fields: [ 544 | 'head', 545 | 'employeeCount', 546 | { 547 | key: 'projects', 548 | fields: ['id', 'name', 'status'], 549 | filter: (project) => project.status !== 'completed' 550 | }, 551 | { 552 | key: 'budgetAllocation', 553 | mapping: ({ model, key }) => { 554 | if (key === 'finance') return model.budget; 555 | if (model.projects) return model.projects.reduce((sum, p) => sum + p.budget, 0); 556 | if (model.campaigns) return model.campaigns.reduce((sum, c) => sum + c.budget, 0); 557 | return 0; 558 | } 559 | } 560 | ], 561 | omit: ['budget'] 562 | } 563 | ] 564 | }, 565 | { 566 | key: 'activeClients', 567 | path: 'clients', 568 | filter: (client) => client.active, 569 | fields: [ 570 | 'id', 571 | 'name', 572 | { 573 | key: 'contractValue', 574 | mapping: ({ model }) => `$${(model.contractValue / 1000000).toFixed(2)}M` 575 | } 576 | ] 577 | }, 578 | { 579 | key: 'financialSummary', 580 | mapping: ({ originModel }) => { 581 | const totalBudget = Object.values(originModel.departments).reduce((sum, dept: any) => { 582 | if (dept.budget) return sum + dept.budget; 583 | if (dept.projects) return sum + dept.projects.reduce((pSum, p) => pSum + p.budget, 0); 584 | if (dept.campaigns) return sum + dept.campaigns.reduce((cSum, c) => cSum + c.budget, 0); 585 | return sum; 586 | }, 0); 587 | const activeClientRevenue = originModel.clients 588 | .filter(c => c.active) 589 | .reduce((sum, c) => sum + c.contractValue, 0); 590 | return { 591 | totalBudget: `$${(totalBudget / 1000000).toFixed(2)}M`, 592 | activeClientRevenue: `$${(activeClientRevenue / 1000000).toFixed(2)}M`, 593 | projectedProfit: `$${((activeClientRevenue - totalBudget) / 1000000).toFixed(2)}M` 594 | }; 595 | } 596 | } 597 | ]; 598 | 599 | const { value } = useTransform(companyData, fields); 600 | ``` 601 | 602 | **The resulting transformed data would look like this:** 603 | 604 | Output: 605 | ```json 606 | { 607 | "companyOverview": { 608 | "name": "TechCorp", 609 | "age": 18, 610 | "location": "San Francisco, USA" 611 | }, 612 | "departments": { 613 | "engineering": { 614 | "head": "Jane Doe", 615 | "employeeCount": 50, 616 | "projects": [ 617 | { "id": "P1", "name": "Project Alpha", "status": "active" }, 618 | { "id": "P2", "name": "Project Beta", "status": "planning" }, 619 | { "id": "P4", "name": "Project Delta", "status": "active" } 620 | ], 621 | "budgetAllocation": 2700000 622 | }, 623 | "marketing": { 624 | "head": "John Smith", 625 | "employeeCount": 30, 626 | "projects": [ 627 | { "id": "M1", "name": "Brand Refresh", "status": "active" }, 628 | { "id": "M2", "name": "Social Media Campaign", "status": "planning" } 629 | ], 630 | "budgetAllocation": 1100000 631 | }, 632 | "finance": { 633 | "head": "Alice Johnson", 634 | "employeeCount": 15, 635 | "projects": [ 636 | { "id": "F1", "name": "Cost Optimization", "status": "active" }, 637 | { "id": "F2", "name": "Financial Reporting System", "status": "planning" }, 638 | { "id": "F3", "name": "Budget Analysis", "status": "active" } 639 | ], 640 | "budgetAllocation": 500000 641 | } 642 | }, 643 | "activeClients": [ 644 | { "id": 1, "name": "Acme Corp", "contractValue": "$0.50M" }, 645 | { "id": 3, "name": "InnoSystems", "contractValue": "$1.00M" }, 646 | { "id": 4, "name": "TechGiants", "contractValue": "$1.20M" } 647 | ], 648 | "financialSummary": { 649 | "totalBudget": "$4.30M", 650 | "activeClientRevenue": "$2.70M", 651 | "projectedProfit": "-$1.60M" 652 | } 653 | } 654 | ``` 655 | 656 | This example showcases how the mapping system can handle complex data transformations, including nested structures, custom calculations, and selective data inclusion/exclusion. It demonstrates the power and flexibility of the system in reshaping and deriving insights from complex data structures. 657 | 658 | This complex example demonstrates: 659 | 660 | 1. **Nested structure handling**: The company data has multiple levels of nesting, which are handled efficiently. 661 | 662 | 2. **Custom mapping**: Several fields use custom mapping functions to derive new values or format existing ones, such as calculating the company's age and formatting the location. 663 | 664 | 3. **Path usage**: The `companyOverview.name` field uses a `path` to directly access nested data from the `info` object. 665 | 666 | 4. **Wildcard with omit**: In the departments section, `'*'` is used with `omit` to include all fields except 'budget'. 667 | 668 | 5. **Filtering**: The `projects` field uses a filter function to exclude completed projects, and `activeClients` filters out inactive clients. 669 | 670 | 6. **Array handling**: The projects array in each department is transformed and filtered. 671 | 672 | 7. **Complex calculations**: The `budgetAllocation` field performs calculations based on the projects' budgets, and `financialSummary` computes totals across all departments and clients. 673 | 674 | 8. **Conditional logic**: The `budgetAllocation` mapping function uses conditional logic to handle different department structures (finance vs. others). 675 | 676 | 9. **Global context usage**: The `currentYear` variable is used in a mapping function to calculate the company's age, demonstrating how external data can be incorporated. 677 | 678 | 10. **Formatting output**: The `contractValue` and financial summary fields format monetary values into millions of dollars with a specific format. 679 | 680 | This example illustrates the system's ability to handle diverse data structures and perform complex transformations, making it suitable for a wide range of data processing tasks. 681 | 682 | ## Transformation Process 683 | 684 | 1. The system first expands any wildcard fields in the `fields` array using the `expandWildcardFields` function. 685 | 686 | 2. For each field in the expanded fields array, the system applies the following rules in order: 687 | 688 | a. If the field is a simple string (key), it directly sets the value from the source model to the new model. 689 | 690 | b. If the field is an object (FieldObject): 691 | - It checks if the source model is null or empty, or if the value for the field's key is null or undefined. 692 | - If so, and a `default` value is specified, it uses the default value. 693 | - If not, it proceeds with the following steps: 694 | 695 | c. If a `path` is specified, it retrieves the value from the original source object using this custom path. 696 | 697 | d. If a `mapping` function is provided, it's called to transform the value. 698 | - The mapping function receives the model (or scoped model if `scope` is specified), key, new model, parent model, original model, and context. 699 | 700 | e. If `fields` are specified for an object or array: 701 | - For arrays, it applies any specified `filter` function to each element. 702 | - It recursively applies the transformation process to each element or nested object. 703 | 704 | f. If no `mapping` or `fields` are specified, but a `default` value exists, it uses the source value or the default if the source is empty. 705 | 706 | g. If the result of these operations is not undefined: 707 | - If `merge` is true, it merges the result into the new model. 708 | - Otherwise, it sets the result in the new model using the specified key (or `newKey` if provided). 709 | 710 | 3. Throughout this process, key formatting (e.g., camelCase) is applied if specified in the options. 711 | 712 | 4. The resulting transformed object is returned. 713 | 714 | This process allows for flexible data access and transformation, handling nested structures, arrays, and various transformation scenarios. It ensures that wildcard expansions, default values, custom paths, mappings, and nested transformations are all applied in a logical order. 715 | 716 | ## Advanced Features 717 | 718 | - **Recursive mapping**: Allows for deep transformation of nested objects. 719 | - **Array handling**: Can map and filter elements of array fields. 720 | - **Context injection**: Provides additional data to mapping and default value functions. 721 | - **Flexible key renaming**: Supports renaming keys during the transformation process. 722 | - **Wildcard mapping**: Enables mapping of all fields at a certain level or deeper nested levels. 723 | 724 | ## Best Practices 725 | 726 | - Use descriptive field names to improve readability. 727 | - Prefer simple and composable transformations over complex mappings. 728 | - Use context to inject dependencies or global configurations. 729 | - Consider performance when transforming large data structures. 730 | - Test your mappings with various input scenarios, including edge cases. 731 | 732 | ## Error Handling 733 | 734 | The mapping system handles errors silently by default. For more robust error handling: 735 | 736 | - Use default values to handle missing fields. 737 | - Implement checks in your mapping functions. 738 | - Consider using an external validation system for complex structures. --------------------------------------------------------------------------------