Skip to content

harness-software/Typesafety-temp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lts-typesafety-issue

Issue One

The original issue was that queryParams from useSearchHeaderStore was a black box. That did not show the true type that it could be. Meaning it did not show all the properties that it could be included on the queryParams object.

searchHeaderStore.ts file queryParams was defined as below with the setQueryParams function that was used to set the queryParams object.

export const useSearchHeaderStore = defineStore("searchHeader", () => {
  // other refs
  const queryParams = ref({
    PageSize: PAGE_SIZE,
    PageNumber: 1,
  });

  function setQueryParams<T>(_queryParams: T) {
    queryParams.value = Object.assign(queryParams.value, _queryParams);
  }

  function setDefaultQueryParams() {
    queryParams.value = {
      PageSize: PAGE_SIZE,
      PageNumber: 1,
    };
  }

  return {
    // other refs
    queryParams,
    setQueryParams,
    setDefaultQueryParams,
  };
});

NOTE: In the useSearchHeaderStore function, if queryParams was referenced it only accessed PageNumber and PageSize properties. It did not expect or care if there were other properties on the queryParams object.

Usage:

src/views/SDS/SDSListView.vue

<script setup lang="ts">
import { onMounted, ref } from "vue";

const { search, queryParams, inputSearch } = storeToRefs(
  useSearchHeaderStore()
);
const {
  setHeaderOptions,
  setReload,
  setFiltersCount,
  setQueryParams,
  setSearch,
  showFilter,
} = useSearchHeaderStore();

async function loadListSDSSearch() {
  setReload(true);
  try {
    const safetyDataSheetsQuery: SafetyDataSheetsQuery =
      SafetyDataSheetsQuerySchema.parse({
        ...queryParams.value,
        keywords: inputSearch.value,
      });
    const response = await safetyDataSheetStore.getListSafetyDataSheet(
      safetyDataSheetsQuery
    );
    if (response) {
      totalItem.value = response.length;
      listSafetyDataSheet.value = response;
    }
  } finally {
    setReload(false);
  }
}

function setFilter() {
  showFilter();
  setQueryParams<SafetyDataSheetsQuery>(
    SafetyDataSheetsQuerySchema.parse({
      manufacturerIds: manufacturerSelect.value.map((x) => Number(x.value)),
      keywords: inputSearch.value,
    })
  );

  setSearch();
}
</script>
<template></template>

SDS Types

import { z } from "zod";

export const SafetyDataSheetsQuerySchema = z.object({
  keywords: z.string().optional(),
  manufacturerIds: z.array(z.number()).optional(),
  pageNumber: z.number().default(1),
  pageSize: z.number().default(PAGE_SIZE),
});

export type SafetyDataSheetsQuery = z.infer<typeof SafetyDataSheetsQuerySchema>;

When you would hover over the queryParams in the loadListSDSSearch function, it would show the type as below.

image

So one would expect that only those properties are present and the final param object would be of type of only PageNumber, PageSize, and keywords. Whereas in reality, the param object would have hidden properties that make up the SafetyDataSheetsQuery type.

As you can see below, the param object has the manufacturerIds property that is not shown in the type of queryParams. This is because the queryParams object is a black box and does not show the true type of the object. LTS was able to get around this by using a spread operator and was then leveraging the fact that the zod schema had optional properties. Therefore, it resulted in the param object looking like it has the right type, since less properties are still a subset of the type. However, in reality there were more properties on the param object that were not shown in the type.

image2

You will also notice that because of this black box the properties do not even have the same casing.

Issue Two

Then after a meeting LTS came back with the following.

./src/axios/instances/equipmentApi.ts

export class EquipmentApi {
  // other code

  public async getListEquipmentsByQuery(
    query: string
  ): Promise<EquipmentResultList> {
    try {
      const request = `${this.urlEquipment}?${query}`;
      const response = await this.axiosInstance.get(request);
      if (response && response.status === 200) {
        return EquipmentResultListSchema.parse(response.data);
      } else {
        throw new Error(`Failed to parse list ${this.name}`);
      }
    } catch (error) {
      console.error(
        `An unexpected error occurred attempting to retrieve the list ${this.name}.`,
        error
      );
      return [];
    }
  }
}

./src/stores/searchHeaderStore.ts

export const useEquipmentStore = defineStore("EquipmentStore", () => {
  const listEquipments = ref<EquipmentResultList>([]);
  const totalItems = ref<number>(0);
  const currentEquipment = ref<EquipmentDetailResult>();

  async function getListEquipments(): Promise<EquipmentResultList> {
    const res = await equipmentApi.getListEquipments(1, PAGE_SIZE_MAX);
    totalItems.value = res.length;
    listEquipments.value = res;
    return listEquipments.value;
  }

  async function getListEquipmentsByQuery(
    query: string
  ): Promise<EquipmentResultList> {
    const res = await equipmentApi.getListEquipmentsByQuery(query);
    totalItems.value = res.length;
    listEquipments.value = res;
    return listEquipments.value;
  }
});

Which got used in ./src/view/Equipment/EquipmentFilter.vue

async function handleSetFilters() {
  const data = {
    OverdueInDays:
      EquipmentOverdueInDaysSchema.parse(filters.value.overdues?.value) ??
      EquipmentOverdueInDays.OVERDUE,
    ManufacturerIds: filters.value.manufacturers
      ? filters.value.manufacturers.map((x) => x.manufacturerId)
      : [],
    InService: filters.value.status?.value === "true" ?? false,
    TagIds: filters.value.tags ? filters.value.tags.map((x) => x.tagId) : [],
    InspectionIntervals: filters.value.inspectionIntervals
      ? filters.value.inspectionIntervals.map((x) => Number.parseInt(x.value))
      : [],
    WorkerIds: filters.value.workers
      ? filters.value.workers.map((x) => x.workerId)
      : [],
    DocumentIds: filters.value.documents
      ? filters.value.documents.map((x) => x.documentId)
      : [],
    ProjectIds: filters.value.projects
      ? filters.value.projects.map((x) => x.projectId)
      : [],
    CompanyIds: filters.value.companies
      ? filters.value.companies.map((x) => x.companyId)
      : [],
    DivisionIds: filters.value.divisions
      ? filters.value.divisions.map((x) => x.divisionId)
      : [],
  };
  const validData = await EquipmentQueryParamSchema.safeParseAsync(data);
  if (validData.success) {
    filterData.value = {
      ...filterData.value,
      ...validData.data,
    };
    const query = convertToStringQueryParam(filterData.value);
    if (query) {
      setSearchQuery(query);
    }
    setFiltersCount(totalQueryParam(filterData.value));
    setSearch();
    showFilter();
  }
}

function convertToStringQueryParam(data: EquipmentQueryParam) {
  if (!data) return null;

  const keyValuePairArray = Object.entries(data).map((c) => ({
    key: c[0],
    value: c[1],
  }));
  return keyValuePairArray
    .map((obj) =>
      Array.isArray(obj.value)
        ? obj.value.map((v) => `${obj.key}=${v}`).join("&")
        : `${obj.key}=${obj.value}`
    )
    .join("&")
    .replace(/&{2,}/, "");
}

function totalQueryParam(data: EquipmentQueryParam) {
  let total = 0;
  if (!data) return total;
  const keyValuePairArray = Object.entries(data).map((c) => ({
    key: c[0],
    value: c[1],
  }));
  keyValuePairArray.forEach((obj) =>
    Array.isArray(obj.value) ? (total += obj.value.length) : total++
  );
  return total;
}

This change made is so instead of a queryParams black box we now had a string black box. Whereas the value given to getListEquipmentsByQuery was a string and not an object. This meant that it was up to the programmer to ensure the correct string shape for query params was given to the store function.

My suggestion was to type the store's function argument as EquipmentQueryParams and inside the equipment store covert the object to a real url search string with the help of URLSearchParams. This would ensure the developer passes in the expected query params to the store function and the store is setup to handle the conversion from the EquipmentQueryParams object to the string url search params.

We would also make the helper functions convertToStringQueryParam and totalQueryParam work on Record<string, unknown> types so they could be used in other stores.

function convertObjectToSearchParams(
  data: Record<string, unknown>
): URLSearchParams | null {
  if (!data) return null;

  const result = new URLSearchParams();

  for (const [key, value] of Object.entries(data)) {
    if (value === null || value === undefined) continue;
    if (typeof value === "string") {
      result.set(key, value);
    } else if (typeof value === "number") {
      result.set(key, value.toString());
    } else if (typeof value === "boolean") {
      result.set(key, value.toString());
    } else if (Array.isArray(value)) {
      value.forEach((item) => {
        result.append(key, item.toString());
      });
    } else {
      result.set(key, JSON.stringify(value));
    }
  }

  return result;
}

export const useEquipmentStore = defineStore("EquipmentStore", () => {
  const listEquipments = ref<EquipmentResultList>([]);
  const totalItems = ref<number>(0);
  const currentEquipment = ref<EquipmentDetailResult>();

  async function getListEquipments(): Promise<EquipmentResultList> {
    const res = await equipmentApi.getListEquipments(1, PAGE_SIZE_MAX);
    totalItems.value = res.length;
    listEquipments.value = res;
    return listEquipments.value;
  }

  async function getListEquipmentsByQuery(
    params: EquipmentQueryParams
  ): Promise<EquipmentResultList> {
    const query = convertObjectToSearchParams(params); // convert to URLSearchParams inside the store

    const res = await equipmentApi.getListEquipmentsByQuery(query);
    totalItems.value = res.length;
    listEquipments.value = res;
    return listEquipments.value;
  }
});

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published