├── .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 |
2 | Home
3 |
--------------------------------------------------------------------------------
/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 |
2 | basic
3 |
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 |
2 |
3 |
4 | {{ user.name }}
5 |
6 |
7 |
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 |
2 |
7 |
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 |
8 |
9 |
{{ msg }}
10 |
11 | You’ve successfully created a project with
12 | Vite +
13 | Vue 3. What's next?
14 |
15 |
16 |
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 |
3 |
19 |
20 |
--------------------------------------------------------------------------------
/apps/vue-example/src/components/icons/IconCommunity.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
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 |
2 |
7 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | loading
8 |
9 |
10 |
11 |
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 |
2 |
3 |
User Directory
4 |
7 |
27 |
28 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
2 |
7 |
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 |
2 |
3 |
6 |
7 |
8 |
9 |
![]()
10 |
11 |
12 |
{{ user.data.value.name }}
13 |
{{ user.data.value.email }}
14 |
Total Projects: {{ user.data.value.totalProjects }}
15 |
16 |
17 |
18 |
19 |
Departments and Projects
20 |
21 |
{{ dept.title }} ({{ deptName }})
22 |
Role: {{ dept.role }}
23 |
Projects:
24 |
29 |
30 |
31 |
32 |
33 |
Skills
34 |
35 |
37 | {{ skill }}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | ← Return to users list
46 |
47 |
48 |
49 |
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 |
11 |
12 |
13 |
14 |
15 | Documentation
16 |
17 | Vue’s
18 | official documentation
19 | provides you with all information you need to get started.
20 |
21 |
22 |
23 |
24 |
25 |
26 | Tooling
27 |
28 | This project is served and bundled with
29 | Vite. The
30 | recommended IDE setup is
31 | VSCode +
32 | Volar. If
33 | you need to test your components and web pages, check out
34 | Cypress and
35 | Cypress Component Testing.
36 |
37 |
38 |
39 | More instructions are available in README.md.
40 |
41 |
42 |
43 |
44 |
45 |
46 | Ecosystem
47 |
48 | Get official tools and libraries for your project:
49 | Pinia,
50 | Vue Router,
51 | Vue Test Utils, and
52 | Vue Dev Tools. If
53 | you need more resources, we suggest paying
54 | Awesome Vue
55 | a visit.
56 |
57 |
58 |
59 |
60 |
61 |
62 | Community
63 |
64 | Got stuck? Ask your question on
65 | Vue Land, our official
66 | Discord server, or
67 | StackOverflow. You should also subscribe to
70 | our mailing list and follow
71 | the official
72 | @vuejs
73 | twitter account for latest news in the Vue world.
74 |
75 |
76 |
77 |
78 |
79 |
80 | Support Vue
81 |
82 | As an independent project, Vue relies on community backing for its sustainability. You can help
83 | us by
84 | becoming a sponsor.
85 |
86 |
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.
--------------------------------------------------------------------------------