refactor: remove all any types, add strict GraphQL scalar typing
All checks were successful
Build Docker Image / build (push) Successful in 4m3s

- Add strictScalars: true to codegen.ts with proper scalar mappings
  (Date, Decimal, JSONString, JSON, UUID, BigInt → string/Record)
- Replace all ref<any[]> with proper GraphQL-derived types
- Add type guards for null filtering in arrays
- Fix bugs exposed by typing (locationLatitude vs latitude, etc.)
- Add interfaces for external components (MapboxSearchBox)

This enables end-to-end type safety from GraphQL schema to frontend.
This commit is contained in:
Ruslan Bakiev
2026-01-27 11:34:12 +07:00
parent ff34c564e1
commit 2dbe600d8a
42 changed files with 614 additions and 324 deletions

View File

@@ -118,7 +118,9 @@ import { FormKitSchema } from '@formkit/vue'
import type { FormKitSchemaNode } from '@formkit/core'
import { GetProductsDocument } from '~/composables/graphql/public/exchange-generated'
import { CreateOfferDocument } from '~/composables/graphql/team/exchange-generated'
import { GetTeamAddressesDocument } from '~/composables/graphql/team/teams-generated'
import { GetTeamAddressesDocument, type GetTeamAddressesQueryResult } from '~/composables/graphql/team/teams-generated'
type TeamAddress = NonNullable<NonNullable<GetTeamAddressesQueryResult['teamAddresses']>[number]>
definePageMeta({
layout: 'topnav',
@@ -147,7 +149,7 @@ const productName = ref<string>('')
const schemaId = ref<string | null>(null)
const schemaDescription = ref<string | null>(null)
const formkitSchema = ref<FormKitSchemaNode[]>([])
const addresses = ref<any[]>([])
const addresses = ref<TeamAddress[]>([])
const selectedAddressUuid = ref<string | null>(null)
const formKitConfig = {
classes: {
@@ -169,8 +171,8 @@ const loadAddresses = async () => {
try {
const { data, error: addressesError } = await useServerQuery('offer-form-addresses', GetTeamAddressesDocument, {}, 'team', 'teams')
if (addressesError.value) throw addressesError.value
addresses.value = data.value?.teamAddresses || []
const defaultAddress = addresses.value.find((address: any) => address.isDefault)
addresses.value = (data.value?.teamAddresses || []).filter((a): a is TeamAddress => a !== null)
const defaultAddress = addresses.value.find((address) => address.isDefault)
selectedAddressUuid.value = defaultAddress?.uuid || addresses.value[0]?.uuid || null
} catch (err) {
console.error('Failed to load addresses:', err)
@@ -189,7 +191,7 @@ const loadData = async () => {
const { data: productsData, error: productsError } = await useServerQuery('offer-form-products', GetProductsDocument, {}, 'public', 'exchange')
if (productsError.value) throw productsError.value
const products = productsData.value?.getProducts || []
const product = products.find((p: any) => p.uuid === productUuid.value)
const product = products.find((p) => p?.uuid === productUuid.value)
if (!product) {
throw new Error(t('clientOfferForm.errors.productNotFound', { uuid: productUuid.value }))
@@ -219,9 +221,9 @@ const loadData = async () => {
formkitSchema.value = schemaToFormKit(terminusClass, enums)
await loadAddresses()
} catch (err: any) {
} catch (err: unknown) {
hasError.value = true
error.value = err.message || t('clientOfferForm.error.load')
error.value = err instanceof Error ? err.message : t('clientOfferForm.error.load')
console.error('Load error:', err)
} finally {
isLoading.value = false
@@ -237,7 +239,7 @@ const handleSubmit = async (data: Record<string, unknown>) => {
throw new Error(t('clientOfferForm.error.load'))
}
const selectedAddress = addresses.value.find((address: any) => address.uuid === selectedAddressUuid.value)
const selectedAddress = addresses.value.find((address) => address?.uuid === selectedAddressUuid.value)
if (!selectedAddress) {
throw new Error(t('clientOfferForm.error.save'))
}
@@ -253,14 +255,14 @@ const handleSubmit = async (data: Record<string, unknown>) => {
locationCountryCode: selectedAddress.countryCode || '',
locationLatitude: selectedAddress.latitude,
locationLongitude: selectedAddress.longitude,
quantity: data.quantity || 0,
quantity: String(data.quantity || '0'),
unit: String(data.unit || 'ton'),
pricePerUnit: data.price_per_unit || data.pricePerUnit || null,
pricePerUnit: String(data.price_per_unit || data.pricePerUnit || ''),
currency: String(data.currency || 'USD'),
description: String(data.description || ''),
validUntil: data.valid_until || data.validUntil || null,
validUntil: (data.valid_until as string | undefined) ?? (data.validUntil as string | undefined) ?? undefined,
terminusSchemaId: schemaId.value,
terminusPayload: JSON.stringify(data),
terminusPayload: data,
}
const result = await mutate(CreateOfferDocument, { input }, 'team', 'exchange')
@@ -270,8 +272,8 @@ const handleSubmit = async (data: Record<string, unknown>) => {
await navigateTo(localePath('/clientarea/offers'))
} catch (err: any) {
error.value = err.message || t('clientOfferForm.error.save')
} catch (err: unknown) {
error.value = err instanceof Error ? err.message : t('clientOfferForm.error.save')
hasError.value = true
} finally {
isSubmitting.value = false

View File

@@ -122,7 +122,9 @@
<script setup lang="ts">
import type { MapBounds } from '~/components/catalog/CatalogMap.vue'
import { GetOffersDocument } from '~/composables/graphql/public/exchange-generated'
import { GetOffersDocument, type GetOffersQueryResult } from '~/composables/graphql/public/exchange-generated'
type Offer = NonNullable<NonNullable<GetOffersQueryResult['getOffers']>[number]>
definePageMeta({
layout: 'topnav',
@@ -135,7 +137,7 @@ const { activeTeamId } = useActiveTeam()
const { execute } = useGraphQL()
const PAGE_SIZE = 24
const offers = ref<any[]>([])
const offers = ref<Offer[]>([])
const totalOffers = ref(0)
const isLoadingMore = ref(false)
@@ -164,7 +166,7 @@ const {
watchEffect(() => {
if (offersData.value?.getOffers) {
offers.value = offersData.value.getOffers
offers.value = offersData.value.getOffers.filter((o): o is Offer => o !== null)
totalOffers.value = offersData.value.getOffersCount ?? offersData.value.getOffers.length
}
})
@@ -231,9 +233,11 @@ const onSearch = () => {
// TODO: Implement search
}
const onSelectOffer = (offer: any) => {
selectedOfferId.value = offer.uuid
navigateTo(localePath(`/clientarea/offers/${offer.uuid}`))
const onSelectOffer = (offer: { uuid?: string | null }) => {
if (offer.uuid) {
selectedOfferId.value = offer.uuid
navigateTo(localePath(`/clientarea/offers/${offer.uuid}`))
}
}
const getStatusVariant = (status: string) => {
@@ -293,7 +297,7 @@ const fetchOffers = async (offset = 0, replace = false) => {
'public',
'exchange'
)
const next = data?.getOffers || []
const next = (data?.getOffers || []).filter((o): o is Offer => o !== null)
offers.value = replace ? next : offers.value.concat(next)
totalOffers.value = data?.getOffersCount ?? totalOffers.value
}

View File

@@ -26,8 +26,8 @@
<template v-else>
<Grid v-if="products.length" :cols="1" :md="2" :lg="3" :gap="4">
<Card
v-for="product in products"
:key="product.uuid"
v-for="(product, index) in products"
:key="product.uuid ?? index"
padding="lg"
class="cursor-pointer hover:shadow-md transition-shadow"
@click="selectProduct(product)"
@@ -51,7 +51,9 @@
</template>
<script setup lang="ts">
import { GetProductsDocument } from '~/composables/graphql/public/exchange-generated'
import { GetProductsDocument, type GetProductsQueryResult } from '~/composables/graphql/public/exchange-generated'
type Product = NonNullable<NonNullable<GetProductsQueryResult['getProducts']>[number]>
definePageMeta({
layout: 'topnav',
@@ -62,7 +64,7 @@ const localePath = useLocalePath()
const { t } = useI18n()
const { execute } = useGraphQL()
const products = ref<any[]>([])
const products = ref<Product[]>([])
const isLoading = ref(true)
const hasError = ref(false)
const error = ref('')
@@ -73,19 +75,19 @@ const loadProducts = async () => {
hasError.value = false
const { data, error: productsError } = await useServerQuery('offers-new-products', GetProductsDocument, {}, 'public', 'exchange')
if (productsError.value) throw productsError.value
products.value = data.value?.getProducts || []
} catch (err: any) {
products.value = (data.value?.getProducts || []).filter((p): p is Product => p !== null)
} catch (err: unknown) {
hasError.value = true
error.value = err.message || t('offersNew.errors.load_failed')
error.value = err instanceof Error ? err.message : t('offersNew.errors.load_failed')
products.value = []
} finally {
isLoading.value = false
}
}
const selectProduct = (product: any) => {
const selectProduct = (product: { uuid?: string | null }) => {
// Navigate to product details page
navigateTo(localePath(`/clientarea/offers/${product.uuid}`))
if (product.uuid) navigateTo(localePath(`/clientarea/offers/${product.uuid}`))
}
await loadProducts()