0

I have a Vue3 app with TypeScript and Pinia.

In the _Layout.vue component when I call currentUser.value.hasPermission() I get the error Uncaught (in promise) TypeError: currentUser.value.hasPermission is not a function.

Is there something with refs or pinia stores that would prevent me from calling a function on an object that had been stored in them?

Code below.

// @/stores/main-store.ts

import { defineStore } from "pinia";
import type { IUser } from "@/lib/user";

export const useStore = defineStore({
  id: "main",
  state: () => ({
    currentUser: {} as IUser,
    currentTenant: null as string | null,
    theme: null as string | null,
  }),
});
// @/lib/user.ts

import { getToken } from "@/lib/auth";

export interface IUser {
  id: string;
  tenants: IUserTenant[];
  hasPermission: (
    tenantIdentifier: string|null,
    permission: IUserPermission
  ) => boolean;
}

export interface IUserTenant {
  tenantIdentifier: string;
  tenantName: string;
  permissions: IUserPermission[];
}

export interface IUserPermission {
  permissionZone: PermissionZone;
  permissionType: PermissionType;
}

export enum PermissionZone {
  Me,
  Tenants,
  Users,
  Contacts,
}
export enum PermissionType {
  List,
  Read,
  Create,
  Update,
  Delete,
}

const API_EP = import.meta.env.VITE_API_ENDPOINT;
export class User implements IUser {

  public static async getCurrentUser(): Promise<IUser> {
    const token = await getToken();
    const response = await fetch(`${API_EP}/user`, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
      mode: "cors",
    });

    if (response.ok) {
      return (await response.json()) as User;
    }

    // TODO Define Error response model and parse for message
    throw new Error("Unable to retrieve current user.");
  }

  public id!: string;
  public tenants: IUserTenant[] = [];
  public hasPermission(tenantIdentifier: string | null, permission: IUserPermission): boolean {
    return this.tenants.some(
      (t) =>
        t.tenantIdentifier === tenantIdentifier &&
        t.permissions.some(
          (p) =>
            p.permissionZone === permission.permissionZone &&
            p.permissionType === permission.permissionType
        )
    );
  }
}
// @/views/_Layout.vue - <script> section

import { onMounted, ref } from "vue";
import { useStore } from "@/stores/main-store";
import { storeToRefs } from "pinia";
import { Navigation } from "@/lib/navigation";
import type { INavigationItem } from "@/lib/navigation";

const store = useStore();
const { currentUser, currentTenant } = storeToRefs(store);

const navItems = ref<INavigationItem[]>();
onMounted(async () => {
  navItems.value = Navigation.filter((i) =>
    currentUser.value.hasPermission(
      currentTenant.value,
      i.permission
    )
  );
});

Edit I forgot about where the currentUser gets populated - this is done in the router setup as follows:

// @/router/index.ts

import { createRouter, createWebHistory } from "vue-router";
import Layout from "@/views/_Layout.vue";
import { AuthenticationGuard } from "vue-auth0-plugin";
import { User } from "@/lib/user";
import { useStore } from "@/stores/main-store";

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      name: "default",
      path: "/",
      component: Layout,
      beforeEnter: async (to, from) => {
        // Handle login
        const authed = await AuthenticationGuard(to, from);

        useStore().$patch({
          currentUser: await User.getCurrentUser(),
        });

        if (authed && to.name === "default")
          await router.push({ name: "dashboard" });
        return authed;
      },
      children: [
        {
          name: "dashboard",
          path: "",
          component: () => import("@/views/DashboardPage.vue"),
        },
      ],
    },
  ],
});

export default router;
Hades
  • 1,975
  • 1
  • 23
  • 39
  • BTW, in TypeScript interfaces do not use the capital-i `I` prefix. TypeScript isn't COM nor .NET. – Dai Aug 13 '22 at 09:52
  • 1
    `currentUser: {} as IUser,` <-- This is incorrect. An empty object does not satisfy your `IUser` type. (Generally speaking you should avoid using `as` in TypeScript – Dai Aug 13 '22 at 09:53
  • @Hades You don't use User class anywhere, so you can't expect its methods to exist – Estus Flask Aug 13 '22 at 10:53
  • @Dai That's opinionated. A prefix is welcome if you don't want to rely on IDE highlighting distinguishing them or think about how to avoid name collisions (User and IUser). And no, you can't avoid using `as` but this needs to be justified. It cheats TS here. – Estus Flask Aug 13 '22 at 10:53
  • @EstusFlask It's not an opinion: https://github.com/microsoft/TypeScript-Handbook/issues/121 – Dai Aug 13 '22 at 11:52
  • @Dai The link discusses somebody's opinion on the subject, and you may notice there's no consensus on it, and it's not a part of the official docs. That's the meaning of "opinionated". You don't have to impose it like something obligatory, because it's not. That's just code style, which may be undesirable for the reason I explained above. – Estus Flask Aug 13 '22 at 13:00
  • @Dai Thank you for the input. As @EstusFlask mentioned, the use of `I` for interfaces is a personal style choice as I find it makes it much clearer what is an interface. In terms of the issue at hand, I missed an important piece from the question which I'll update after posting this comment. Namely, that I populate the `currentUser` in the store within the `beforeEnter` in the vue-router setup. – Hades Aug 13 '22 at 13:28

1 Answers1

0

currentUser is populated from getCurrentUser(), which returns the JSON object from a fetch():

// @/lib/user.ts
public static async getCurrentUser(): Promise<IUser> {
  ⋮
  if (response.ok) {
    return (await response.json()) as User;
  }
}

fetch() returns serializable data objects, which cannot contain functions, so I don't believe it's correct to cast the JSON response as User.

If I understand the problem correctly, getCurrentUser() should actually create an instance of User from the JSON response, and return that instance. You could do that by creating a class constructor that receives the JSON response, and copies over the data into its fields.

tony19
  • 125,647
  • 18
  • 229
  • 307
  • 1
    That was it! I'm from a C# background so had assumed that casting to a class would force it to include the functions defined in the class... guess not. – Hades Aug 14 '22 at 10:05