TS: Proxy function call interception error

Viewed 92

I using Vue 3 and Quasar Framework (latest).

I create a Api class as wrapper for Axios with some methods (get, post, etc...).

And i'm need to intercept this methods calling.

For this, i create Proxy for Api class instance, and try to intercept. It's need to redirect to login page if user Unauthenticated and get CSRF cookies if needed and repeat request.

When i trying use Api instance (with Proxy):

    async function signIn() {
      loading.value = true;
      const payload: object = {
        email: login.value,
        password: password.value,
        remember: remember.value
      }
      try {
        const response = await api.post('/login', payload);
        console.log(response)
      } catch (e) {
        console.error(e);
      }
      loading.value = false;
    }

it's thrown error:

  LoginForm.vue?0a10:29 TypeError: boot_api__WEBPACK_IMPORTED_MODULE_1__.api.post is not a function
       at eval (LoginForm.vue?0a10:26:1)
       at Generator.next (<anonymous>)
       at eval (VM2781 LoginForm.vue:13:71)
       at new Promise (<anonymous>)
       at __awaiter (VM2781 LoginForm.vue:9:12)
       at signIn (LoginForm.vue?0a10:17:1)
       at callWithErrorHandling (runtime-core.esm-bundler.js?f781:155:1)
       at callWithAsyncErrorHandling (runtime-core.esm-bundler.js?f781:164:1)
       at emit$1 (runtime-core.esm-bundler.js?f781:720:1)
       at eval (runtime-core.esm-bundler.js?f781:7292:1)

What's a problem? In IDE has no errors, and if not use Proxy all works...

Full code of LoginForm vue component:

<template>
  <q-form
    @submit="signIn"
    class="column q-gutter-y-md login-form-width"
  >
    <q-input v-model="login"
             label="Login"
             class="col"
             filled
             :disable="loading"
    />
    <q-input v-model="password"
             label="Password"
             type="password"
             class="col"
             filled
             :disable="loading"
    />
    <q-checkbox v-model="remember"
                label="Remember"
                class="col"
                filled
                :disable="loading"
    />
    <q-btn label="Login"
           type="submit"
           unelevated
           color="primary"
           :loading="loading"
    />
  </q-form>
</template>

<script lang="ts">
import {defineComponent, Ref, ref} from 'vue';
import {api} from 'boot/api';

export default defineComponent({
  name: 'LoginForm',
  setup() {
    const login: Ref<string> = ref('');
    const password: Ref<string> = ref('');
    const remember: Ref<boolean> = ref(true);
    const loading: Ref<boolean> = ref(false);
    const errors: Ref<object> = ref({
      login: [],
      password: [],
    });

    async function signIn() {
      loading.value = true;
      const payload: object = {
        email: login.value,
        password: password.value,
        remember: remember.value
      }
      try {
        const response = await api.post('/login', payload);
        console.log(response)
      } catch (e) {
        console.error(e);
      }
      loading.value = false;
    }

    return {login, password, remember, loading, errors, signIn}
  }
})
</script>

<style scoped>
.login-form-width {
  width: 100%;
  max-width: 350px;
}
</style>

Full code of api (from quasar boot file):

import {boot} from 'quasar/wrappers'
import {AxiosError, AxiosInstance, Method} from 'axios';
import {axiosInstance} from 'boot/axios';

type ApiMethod = 'get' | 'post';

class ApiResponse {
  public data: object
  public code: number | null
  public message: string | null

  constructor(data: object = {}, code: number | null = 0, message: string | null = '') {
    this.data = data;
    this.code = code;
    this.message = message;
  }
}

class ApiError {
  public data: object
  public code: number | null
  public message: string | null

  constructor(data: object = {}, code: number | null = 0, message: string | null = '') {
    this.data = data;
    this.code = code;
    this.message = message;
  }
}

interface ApiRequestConfig {
  url: string,
  method: Method,
  headers?: object,
  params?: object,
  data?: object | string,
}

class Api {
  axios: AxiosInstance

  constructor(axios: AxiosInstance) {
    this.axios = axios;
    this.axios.defaults.withCredentials = true;
  }

  public async get(url: string, params: object = {}): Promise<ApiResponse> {
    return this.request({method: 'GET', url, params});
  }

  public async post(url: string, payload: object = {}): Promise<ApiResponse> {
    return this.request({method: 'POST', url, data: payload});
  }

  private async request(config: ApiRequestConfig): Promise<ApiResponse> {
    try {
      const response = await this.axios.request(config);
      return new ApiResponse(response.data, response.status, response.statusText)
    } catch (e) {
      const error = e as AxiosError;
      if (error.response) {
        throw new ApiError(error.response.data, error.response.status, error.response.statusText);
      } else {
        throw new ApiError({}, 0, '')
      }
    }
  }
}


declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $api: Api;
  }
}

let api: Api = new Api(axiosInstance);

// "async" is optional;
// more info on params: https://v2.quasar.dev/quasar-cli/boot-files
export default boot(async ({router}) => {
  api = new Proxy(api, {
    async get(target: Api, prop: ApiMethod) {
      if (typeof target[prop] === 'function') {
        return async function func(args: unknown[]): Promise<unknown> {
          try {
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            return await target[prop](...args);
          } catch (e) {
            const error = e as ApiError;
            if (error.code === 401 || error.code === 403) {
              await router.push('login');
            }
            if (error.code === 419) {
              await target.get('/sanctum/csrf-cookie');
              return func(args);
            }

          }
        }
      }
    }
  })
})

export {api};


1 Answers

Make it easer, change a request method...

  private async request(config: ApiRequestConfig): Promise<ApiResponse> {
    try {
      const response = await this.axios.request(config);
      return new ApiResponse(response.data, response.status, response.statusText)
    } catch (e) {
      const error = e as AxiosError;
      if (error.response) {
        if (error.response.status === 401 || error.response.status === 403) {
          await this.router.push('login');
        } else if (error.response.status === 419) {
          await this.get('/sanctum/csrf-cookie');
          return await this.request(config);
        } else {
          throw new ApiError(error.response.data, error.response.status, error.response.data?.message ?? error.response.statusText);
        }
      } else {
        throw new ApiError();
      }
    }
    return new ApiResponse();
  }
}
Related