Created
July 14, 2023 12:37
-
-
Save genox/b482cb806df1ab1abf4f136cd1468619 to your computer and use it in GitHub Desktop.
qwik graphql requests with server side in-memory cache
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { graphql } from '~/library/saleor/gql'; | |
import { server$ } from '@builder.io/qwik-city'; | |
import { gqlQuery } from '~/library/gql-fetch/gql-fetch'; | |
import { FetchProductBySlugDocument } from '~/library/saleor/gql/graphql'; | |
import type { VariablesOf } from '@graphql-typed-document-node/core'; | |
export const queryFetchProductBySlug = server$(async function ( | |
variables: VariablesOf<typeof FetchProductBySlugDocument> | |
) { | |
const tokens = extractTokens(this); | |
return await gqlQuery(SALEOR_API_URL, FetchProductBySlugDocument, variables, { | |
...tokens, | |
allowCache: true, | |
}); | |
}); | |
export const FetchProductBySlug = graphql(` | |
query FetchProductBySlug($slug: String!, $channel: String!, $languageCode: LanguageCodeEnum!) { | |
product(channel: $channel, slug: $slug) { | |
...ProductFragment | |
description | |
isAvailableForPurchase | |
} | |
} | |
`); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { component$, useSignal, useTask$ } from '@builder.io/qwik'; | |
import { useSpeakLocale } from 'qwik-speak'; | |
import { SALEOR_API_CHANNEL, SITE_URL } from '~/config/env'; | |
import { useRequestLocale, useRouteQueryParams } from '~/routes/layout'; | |
import { saleorLanguageCode } from '~/components/saleor/utils/saleor-language-code'; | |
import { queryFetchProductBySlug } from '~/library/saleor/queries/products'; | |
import type { AwaitedQuery } from '~/components/saleor/products/products-grid-view'; | |
import { getTranslatedName } from '~/components/saleor/utils/get-translated-properties'; | |
import { ProductPageItem } from '~/components/saleor/products/product-page-item'; | |
export default component$(() => { | |
const routeData = useRouteQueryParams(); | |
const productData = useSignal<AwaitedQuery<typeof queryFetchProductBySlug>>(null); | |
const isBusy = useSignal(false); | |
const locale = useSpeakLocale(); | |
useRequestLocale(); | |
useTask$(async ({ track }) => { | |
const route = track(() => routeData); | |
const productSlug = route.value.params.catchall; | |
isBusy.value = true; | |
productData.value = await queryFetchProductBySlug({ | |
slug: productSlug, | |
languageCode: saleorLanguageCode(locale.lang), | |
channel: SALEOR_API_CHANNEL, | |
}); | |
isBusy.value = false; | |
}); | |
const product = productData.value?.data?.product; | |
return ( | |
<> | |
{product && ( | |
<div> | |
<div class={'prose-sm prose !max-w-full sm:prose lg:prose-lg xl:prose-xl'}> | |
<h1>{getTranslatedName(product)}</h1> | |
</div> | |
<div class={'mt-16 flex gap-8'}> | |
<ProductPageItem item={product} /> | |
</div> | |
</div> | |
)} | |
</> | |
); | |
}); | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import NodeCache from 'node-cache'; | |
class FetchCache { | |
private instance: NodeCache | null = null; | |
getInstance() { | |
if (!this.instance) { | |
this.instance = new NodeCache({ stdTTL: 60 * 60 * 24, checkperiod: 60 * 60 }); | |
} | |
return this.instance; | |
} | |
} | |
export const FetchCacheInstance = new FetchCache(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { DocumentNode } from 'graphql'; | |
import { print } from 'graphql'; | |
import { logger } from '~/utils/logger'; | |
import type { TypedDocumentNode } from '@graphql-typed-document-node/core'; | |
import type { RequestEventBase } from '@builder.io/qwik-city'; | |
import { mutateTokenRefresh } from '~/library/saleor/queries/user'; | |
import { getMd5Hash } from '~/library/gql-fetch/md5-hash'; | |
import { FetchCacheInstance } from '~/library/gql-fetch/fetch-cache'; | |
export type OperationVariables = Record<string, any>; | |
export const extractTokens = (event: RequestEventBase<QwikCityPlatform>) => { | |
const token = event?.cookie?.get('token')?.value; | |
const refreshToken = event?.cookie?.get('refreshToken')?.value; | |
if (!token || !refreshToken) { | |
return { token: '', refreshToken: '' }; | |
} | |
return { token, refreshToken }; | |
}; | |
type Options = { | |
token?: string; | |
refreshToken?: string; | |
allowCache?: boolean; | |
}; | |
export const gqlQuery = async function <T, V extends OperationVariables>( | |
endpoint: string, | |
query: DocumentNode | TypedDocumentNode<T, V>, | |
variables: V, | |
options?: Options | |
): Promise<{ data: T | null; error: Error | null; newToken: string | null }> { | |
const { token, refreshToken, allowCache = false } = options || {}; | |
const start = Date.now(); | |
const cache = FetchCacheInstance.getInstance(); | |
let newToken: string | null = null; | |
const queryString = print(query); | |
const queryType = queryString.startsWith('mutation') ? 'mutation' : 'query'; | |
logger.log( | |
`----------------------------------- ${queryType} -----------------------------------` | |
); | |
const queryInfo = `${'operation' in query.definitions[0] && query.definitions[0].operation} ${ | |
('name' in query.definitions[0] && query.definitions[0].name?.value) || '' | |
}`; | |
const body = JSON.stringify({ | |
query: queryString, | |
variables, | |
}); | |
const queryHash = getMd5Hash(body); | |
if (cache.has(queryHash) && allowCache) { | |
const cachedData = cache.get(queryHash); | |
logger.log(`gqlQuery::Cache ${queryInfo}`, 'Cache: HIT'); | |
return { data: JSON.parse(cachedData as string) as T | null, error: null, newToken }; | |
} | |
if (allowCache) { | |
logger.log(cache.getStats()); | |
} | |
const chooseToken = () => { | |
if (newToken) { | |
return newToken; | |
} | |
return token; | |
}; | |
const refreshInvalidToken = async () => { | |
logger.log('gqlQuery::RefreshToken'); | |
if (!refreshToken) { | |
logger.log('gqlQuery::RefreshToken::Error: No refresh token provided'); | |
return false; | |
} | |
const data = await mutateTokenRefresh({ refreshToken }); | |
if (data.data?.tokenRefresh?.token) { | |
newToken = data.data?.tokenRefresh?.token; | |
} else { | |
logger.log('gqlQuery::RefreshToken::Error: No token received while trying to refresh'); | |
} | |
}; | |
const fetchData = () => | |
fetch(endpoint, { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json', | |
...(chooseToken() ? { Authorization: `Bearer ${chooseToken()}` } : {}), | |
}, | |
body, | |
}) | |
.then(async (result) => { | |
const data = await result.json(); | |
const requestDuration = `${(Date.now() - start) / 100}ms`; | |
const cleanedVariables = Object.keys(variables).map((key) => { | |
if ( | |
key.includes('password') || | |
key.includes('token') || | |
key.includes('Token') || | |
key.includes('email') | |
) { | |
return { [key]: '*****' }; | |
} | |
return { [key]: variables[key] }; | |
}); | |
logger.log(`gqlQuery:: ${queryInfo}`, { | |
variables: cleanedVariables, | |
data: data.data, | |
extensions: data.extensions, | |
requestDuration, | |
}); | |
return data; | |
}) | |
.then(async (response) => { | |
if (response.errors) { | |
logger.log(`gqlQuery::Errors ${queryInfo}`, response.errors); | |
if ( | |
response.errors.some( | |
(error: any) => error.extensions.exception.code === 'ExpiredSignatureError' | |
) | |
) { | |
await refreshInvalidToken(); | |
response = await fetchData(); | |
} | |
// return { data: null, error: data.errors, newToken }; | |
} | |
if (allowCache && response.data) { | |
cache.set(queryHash, JSON.stringify(response.data)); | |
} | |
return { data: response.data as T, error: null, newToken }; | |
}); | |
return await fetchData(); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createHash } from 'crypto'; | |
export const getMd5Hash = (str: string) => createHash('md5').update(str).digest('hex'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment