From 4bead4577d1aa824ed5b161472db55fa381c0178 Mon Sep 17 00:00:00 2001 From: Evan Sosenko Date: Tue, 8 Jul 2025 13:29:11 -0700 Subject: [PATCH 1/2] Add hooks and provider --- src/lib/SeamQueryProvider.tsx | 348 ++++++++++++++++++ src/lib/index.ts | 8 +- src/lib/use-seam-client.ts | 225 +++++++++++ src/lib/use-seam-infinite-query.ts | 103 ++++++ .../use-seam-mutation-without-workspace.ts | 56 +++ src/lib/use-seam-mutation.ts | 64 ++++ src/lib/use-seam-query-without-workspace.ts | 59 +++ src/lib/use-seam-query.ts | 66 ++++ 8 files changed, 928 insertions(+), 1 deletion(-) create mode 100644 src/lib/SeamQueryProvider.tsx create mode 100644 src/lib/use-seam-client.ts create mode 100644 src/lib/use-seam-infinite-query.ts create mode 100644 src/lib/use-seam-mutation-without-workspace.ts create mode 100644 src/lib/use-seam-mutation.ts create mode 100644 src/lib/use-seam-query-without-workspace.ts create mode 100644 src/lib/use-seam-query.ts diff --git a/src/lib/SeamQueryProvider.tsx b/src/lib/SeamQueryProvider.tsx new file mode 100644 index 0000000..088412f --- /dev/null +++ b/src/lib/SeamQueryProvider.tsx @@ -0,0 +1,348 @@ +import type { + SeamHttp, + SeamHttpEndpoints, + SeamHttpOptionsWithClientSessionToken, +} from '@seamapi/http/connect' +import { + QueryClient, + QueryClientContext, + QueryClientProvider, +} from '@tanstack/react-query' +import { + createContext, + type JSX, + type PropsWithChildren, + useContext, + useEffect, + useMemo, +} from 'react' + +import { useSeamClient } from './use-seam-client.js' + +export interface SeamQueryContext { + client: SeamHttp | null + endpointClient: SeamHttpEndpoints | null + clientOptions?: SeamQueryProviderClientOptions | undefined + publishableKey?: string | undefined + userIdentifierKey?: string | undefined + clientSessionToken?: string | undefined + consoleSessionToken?: string | undefined + workspaceId?: string | undefined + queryKeyPrefix?: string | undefined +} + +export type SeamQueryProviderProps = + | SeamQueryProviderPropsWithClient + | SeamQueryProviderPropsWithPublishableKey + | SeamQueryProviderPropsWithClientSessionToken + | SeamQueryProviderPropsWithConsoleSessionToken + +export interface SeamQueryProviderPropsWithClient + extends SeamQueryProviderBaseProps { + client: SeamHttp + queryKeyPrefix: string +} + +export interface SeamQueryProviderPropsWithPublishableKey + extends SeamQueryProviderBaseProps, + SeamQueryProviderClientOptions { + publishableKey: string + userIdentifierKey?: string +} + +export interface SeamQueryProviderPropsWithClientSessionToken + extends SeamQueryProviderBaseProps, + SeamQueryProviderClientOptions { + clientSessionToken: string +} + +export interface SeamQueryProviderPropsWithConsoleSessionToken + extends SeamQueryProviderBaseProps, + SeamQueryProviderClientOptions { + consoleSessionToken: string + workspaceId?: string | undefined +} + +interface SeamQueryProviderBaseProps extends PropsWithChildren { + queryClient?: QueryClient | undefined + onSessionUpdate?: (client: SeamHttp) => void +} + +type SeamClientOptions = SeamHttpOptionsWithClientSessionToken + +export type SeamQueryProviderClientOptions = Pick< + SeamClientOptions, + 'endpoint' | 'isUndocumentedApiEnabled' +> + +const defaultQueryClient = new QueryClient() + +export function SeamQueryProvider({ + children, + onSessionUpdate = () => {}, + queryClient, + ...props +}: SeamQueryProviderProps): JSX.Element { + const value = useMemo(() => { + const context = createSeamQueryContextValue(props) + if ( + context.client == null && + context.publishableKey == null && + context.clientSessionToken == null && + context.consoleSessionToken == null + ) { + return defaultSeamQueryContextValue + } + return context + }, [props]) + + if ( + value.client == null && + value.publishableKey == null && + value.clientSessionToken == null && + value.consoleSessionToken == null + ) { + throw new Error( + `Must provide either a Seam client, clientSessionToken, publishableKey or consoleSessionToken.`, + ) + } + + const { Provider } = seamContext + const queryClientFromContext = useContext(QueryClientContext) + + if ( + queryClientFromContext != null && + queryClient != null && + queryClientFromContext !== queryClient + ) { + throw new Error( + 'The QueryClient passed into SeamQueryProvider is different from the one in the existing QueryClientContext. Omit the queryClient prop from SeamProvider or SeamQueryProvider to use the existing QueryClient provided by the QueryClientProvider.', + ) + } + + return ( + + + {children} + + + ) +} + +function Session({ + onSessionUpdate, + children, +}: Required> & + PropsWithChildren): JSX.Element | null { + const { client } = useSeamClient() + useEffect(() => { + if (client != null) onSessionUpdate(client) + }, [onSessionUpdate, client]) + + return <>{children} +} + +const createDefaultSeamQueryContextValue = (): SeamQueryContext => { + return { client: null, endpointClient: null } +} + +const createSeamQueryContextValue = ( + options: SeamQueryProviderProps, +): SeamQueryContext => { + if (isSeamQueryProviderPropsWithClient(options)) { + if (options.queryKeyPrefix == null) { + throw new InvalidSeamQueryProviderProps( + 'The client prop must be used with a queryKeyPrefix prop.', + ) + } + return { + ...options, + endpointClient: null, + } + } + + if (isSeamQueryProviderPropsWithClientSessionToken(options)) { + const { clientSessionToken, ...clientOptions } = options + return { + clientSessionToken, + clientOptions, + client: null, + endpointClient: null, + } + } + + if (isSeamQueryProviderPropsWithPublishableKey(options)) { + const { publishableKey, userIdentifierKey, ...clientOptions } = options + return { + publishableKey, + userIdentifierKey, + clientOptions, + client: null, + endpointClient: null, + } + } + + if (isSeamQueryProviderPropsWithConsoleSessionToken(options)) { + const { consoleSessionToken, workspaceId, ...clientOptions } = options + return { + consoleSessionToken, + workspaceId, + clientOptions, + client: null, + endpointClient: null, + } + } + + return { client: null, endpointClient: null } +} + +const defaultSeamQueryContextValue = createDefaultSeamQueryContextValue() + +export const seamContext = createContext( + defaultSeamQueryContextValue, +) + +export function useSeamQueryContext(): SeamQueryContext { + return useContext(seamContext) +} + +const isSeamQueryProviderPropsWithClient = ( + props: SeamQueryProviderProps, +): props is SeamQueryProviderPropsWithClient => { + if (!('client' in props)) return false + + const { client, ...otherProps } = props + if (client == null) return false + + const otherNonNullProps = Object.values(otherProps).filter((v) => v != null) + if (otherNonNullProps.length > 0) { + throw new InvalidSeamQueryProviderProps( + `The client prop cannot be used with ${otherNonNullProps.join(' or ')}.`, + ) + } + + return true +} + +const isSeamQueryProviderPropsWithPublishableKey = ( + props: SeamQueryProviderProps, +): props is SeamQueryProviderPropsWithPublishableKey & + SeamQueryProviderClientOptions => { + if (!('publishableKey' in props)) return false + + const { publishableKey } = props + if (publishableKey == null) return false + + if ('client' in props && props.client != null) { + throw new InvalidSeamQueryProviderProps( + 'The client prop cannot be used with the publishableKey prop.', + ) + } + + if ('clientSessionToken' in props && props.clientSessionToken != null) { + throw new InvalidSeamQueryProviderProps( + 'The clientSessionToken prop cannot be used with the publishableKey prop.', + ) + } + + if ('consoleSessionToken' in props && props.consoleSessionToken != null) { + throw new InvalidSeamQueryProviderProps( + 'The consoleSessionToken prop cannot be used with the publishableKey prop.', + ) + } + + if ('workspaceId' in props && props.workspaceId != null) { + throw new InvalidSeamQueryProviderProps( + 'The workspaceId prop cannot be used with the publishableKey prop.', + ) + } + + return true +} + +const isSeamQueryProviderPropsWithClientSessionToken = ( + props: SeamQueryProviderProps, +): props is SeamQueryProviderPropsWithClientSessionToken & + SeamQueryProviderClientOptions => { + if (!('clientSessionToken' in props)) return false + + const { clientSessionToken } = props + if (clientSessionToken == null) return false + + if ('client' in props && props.client != null) { + throw new InvalidSeamQueryProviderProps( + 'The client prop cannot be used with the clientSessionToken prop.', + ) + } + + if ('publishableKey' in props && props.publishableKey != null) { + throw new InvalidSeamQueryProviderProps( + 'The publishableKey prop cannot be used with the clientSessionToken prop.', + ) + } + + if ('userIdentifierKey' in props && props.userIdentifierKey != null) { + throw new InvalidSeamQueryProviderProps( + 'The userIdentifierKey prop cannot be used with the clientSessionToken prop.', + ) + } + + if ('consoleSessionToken' in props && props.consoleSessionToken != null) { + throw new InvalidSeamQueryProviderProps( + 'The consoleSessionToken prop cannot be used with the clientSessionToken prop.', + ) + } + + if ('workspaceId' in props && props.workspaceId != null) { + throw new InvalidSeamQueryProviderProps( + 'The workspaceId prop cannot be used with the clientSessionToken prop.', + ) + } + + return true +} + +const isSeamQueryProviderPropsWithConsoleSessionToken = ( + props: SeamQueryProviderProps, +): props is SeamQueryProviderPropsWithConsoleSessionToken & + SeamQueryProviderClientOptions => { + if (!('consoleSessionToken' in props)) return false + + const { consoleSessionToken } = props + if (consoleSessionToken == null) return false + + if ('client' in props && props.client != null) { + throw new InvalidSeamQueryProviderProps( + 'The client prop cannot be used with the publishableKey prop.', + ) + } + + if ('clientSessionToken' in props && props.clientSessionToken != null) { + throw new InvalidSeamQueryProviderProps( + 'The clientSessionToken prop cannot be used with the publishableKey prop.', + ) + } + + if ('publishableKey' in props && props.publishableKey != null) { + throw new InvalidSeamQueryProviderProps( + 'The publishableKey prop cannot be used with the consoleSessionToken prop.', + ) + } + + if ('userIdentifierKey' in props && props.userIdentifierKey != null) { + throw new InvalidSeamQueryProviderProps( + 'The userIdentifierKey prop cannot be used with the consoleSessionToken prop.', + ) + } + + return true +} + +class InvalidSeamQueryProviderProps extends Error { + constructor(message: string) { + super(`SeamQueryProvider received invalid props: ${message}`) + this.name = this.constructor.name + } +} diff --git a/src/lib/index.ts b/src/lib/index.ts index 7b85954..71da3e2 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1,7 @@ -export default null +export * from './SeamQueryProvider.js' +export * from './use-seam-client.js' +export * from './use-seam-infinite-query.js' +export * from './use-seam-mutation.js' +export * from './use-seam-mutation-without-workspace.js' +export * from './use-seam-query.js' +export * from './use-seam-query-without-workspace.js' diff --git a/src/lib/use-seam-client.ts b/src/lib/use-seam-client.ts new file mode 100644 index 0000000..697bb9d --- /dev/null +++ b/src/lib/use-seam-client.ts @@ -0,0 +1,225 @@ +import { + SeamHttp, + SeamHttpEndpoints, + SeamHttpEndpointsWithoutWorkspace, + SeamHttpWithoutWorkspace, +} from '@seamapi/http/connect' +import { useQuery } from '@tanstack/react-query' +import { useEffect } from 'react' +import { v4 as uuidv4 } from 'uuid' + +import { useSeamQueryContext } from './SeamQueryProvider.js' + +export function useSeamClient(): { + client: SeamHttp | null + endpointClient: SeamHttpEndpoints | null + clientWithoutWorkspace: SeamHttpWithoutWorkspace | null + endpointClientWithoutWorkspace: SeamHttpEndpointsWithoutWorkspace | null + queryKeyPrefixes: string[] + isPending: boolean + isError: boolean + error: unknown +} { + const { + client, + clientOptions, + publishableKey, + clientSessionToken, + consoleSessionToken, + workspaceId, + queryKeyPrefix, + ...context + } = useSeamQueryContext() + const userIdentifierKey = useUserIdentifierKeyOrFingerprint( + clientSessionToken != null ? '' : context.userIdentifierKey + ) + + const { isPending, isError, error, data } = useQuery<{ + client: SeamHttp | null + endpointClient: SeamHttpEndpoints | null + clientWithoutWorkspace: SeamHttpWithoutWorkspace | null + endpointClientWithoutWorkspace: SeamHttpEndpointsWithoutWorkspace | null + }>({ + queryKey: [ + ...getQueryKeyPrefixes({ queryKeyPrefix }), + 'client', + { + client, + clientOptions, + publishableKey, + userIdentifierKey, + clientSessionToken, + }, + ], + queryFn: async () => { + if (client != null) + return { + client, + endpointClient: SeamHttpEndpoints.fromClient(client.client), + clientWithoutWorkspace: null, + endpointClientWithoutWorkspace: null, + } + + if (clientSessionToken != null) { + const seam = SeamHttp.fromClientSessionToken( + clientSessionToken, + clientOptions + ) + + return { + client: seam, + endpointClient: SeamHttpEndpoints.fromClient(seam.client), + clientWithoutWorkspace: null, + endpointClientWithoutWorkspace: null, + } + } + + if (publishableKey != null) { + const seam = await SeamHttp.fromPublishableKey( + publishableKey, + userIdentifierKey, + clientOptions + ) + + return { + client: seam, + endpointClient: SeamHttpEndpoints.fromClient(seam.client), + clientWithoutWorkspace: null, + endpointClientWithoutWorkspace: null, + } + } + + if (consoleSessionToken != null) { + const clientWithoutWorkspace = + SeamHttpWithoutWorkspace.fromConsoleSessionToken(consoleSessionToken) + + const endpointClientWithoutWorkspace = + SeamHttpEndpointsWithoutWorkspace.fromClient( + clientWithoutWorkspace.client + ) + + if (workspaceId == null) { + return { + client: null, + endpointClient: null, + clientWithoutWorkspace, + endpointClientWithoutWorkspace, + } + } + + const seam = SeamHttp.fromConsoleSessionToken( + consoleSessionToken, + workspaceId, + clientOptions + ) + + return { + client: seam, + endpointClient: SeamHttpEndpoints.fromClient(seam.client), + clientWithoutWorkspace, + endpointClientWithoutWorkspace, + } + } + + throw new Error( + 'Missing either a client, publishableKey, clientSessionToken, or consoleSessionToken.' + ) + }, + }) + + return { + client: data?.client ?? null, + endpointClient: data?.endpointClient ?? null, + clientWithoutWorkspace: data?.clientWithoutWorkspace ?? null, + endpointClientWithoutWorkspace: + data?.endpointClientWithoutWorkspace ?? null, + queryKeyPrefixes: getQueryKeyPrefixes({ + queryKeyPrefix, + userIdentifierKey, + publishableKey, + clientSessionToken, + consoleSessionToken, + workspaceId, + }), + isPending, + isError, + error, + } +} + +export class NullSeamClientError extends Error { + constructor() { + super( + [ + 'Attempted to use a null Seam client.', + 'Either a hook using useSeamClient was called outside of a SeamProvider or SeamQueryProvider,', + 'or there was an error when creating the Seam client in useSeamClient,', + 'or useSeamClient is still loading the client.', + ].join(' ') + ) + this.name = this.constructor.name + Error.captureStackTrace(this, this.constructor) + } +} + +function useUserIdentifierKeyOrFingerprint( + userIdentifierKey: string | undefined +): string { + useEffect(() => { + if (userIdentifierKey != null) return + // eslint-disable-next-line no-console + console.warn(`Using an automatically generated fingerprint for the Seam userIdentifierKey! +The user interface will show warnings when using a fingerprint. +This is not recommended because the client session is now bound to this machine and is effectively ephemeral.`) + }, [userIdentifierKey]) + + if (userIdentifierKey != null) { + return userIdentifierKey + } + + const fingerprint = + globalThis.localStorage?.getItem('seam_user_fingerprint') ?? + `fingerprint_${uuidv4()}` + + globalThis.localStorage?.setItem('seam_user_fingerprint', fingerprint) + + return fingerprint +} + +const getQueryKeyPrefixes = ({ + queryKeyPrefix, + userIdentifierKey, + publishableKey, + clientSessionToken, + consoleSessionToken, + workspaceId, +}: { + queryKeyPrefix: string | undefined + userIdentifierKey?: string + publishableKey?: string | undefined + clientSessionToken?: string | undefined + consoleSessionToken?: string | undefined + workspaceId?: string | undefined +}): string[] => { + const seamPrefix = 'seam' + + if (queryKeyPrefix != null) return [seamPrefix, queryKeyPrefix] + + if (clientSessionToken != null) { + return [seamPrefix, clientSessionToken] + } + + if (publishableKey != null && userIdentifierKey != null) { + return [seamPrefix, publishableKey, userIdentifierKey] + } + + if (consoleSessionToken != null) { + if (workspaceId != null) { + return [seamPrefix, consoleSessionToken, workspaceId] + } + + return [seamPrefix, consoleSessionToken, 'without_workspace'] + } + + return [seamPrefix] +} diff --git a/src/lib/use-seam-infinite-query.ts b/src/lib/use-seam-infinite-query.ts new file mode 100644 index 0000000..e356d1a --- /dev/null +++ b/src/lib/use-seam-infinite-query.ts @@ -0,0 +1,103 @@ +import type { + SeamActionAttemptFailedError, + SeamActionAttemptTimeoutError, + SeamHttpApiError, + SeamHttpEndpointPaginatedQueryPaths, + SeamHttpEndpoints, + SeamHttpInvalidInputError, + SeamHttpRequest, + SeamPageCursor, +} from '@seamapi/http/connect' +import type { ActionAttempt } from '@seamapi/types/connect' +import { + type QueryKey, + useInfiniteQuery, + type UseInfiniteQueryOptions, + type UseInfiniteQueryResult, +} from '@tanstack/react-query' + +import { useSeamClient } from 'lib/use-seam-client.js' + +export type UseSeamInfiniteQueryParameters< + T extends SeamHttpEndpointPaginatedQueryPaths, +> = Parameters[0] + +export type UseSeamInfiniteQueryResult< + T extends SeamHttpEndpointPaginatedQueryPaths, +> = UseInfiniteQueryResult, QueryError> + +export function useSeamInfiniteQuery< + T extends SeamHttpEndpointPaginatedQueryPaths, +>( + endpointPath: T, + parameters: UseSeamInfiniteQueryParameters = {}, + options: Parameters[1] & + QueryOptions, QueryError> = {} +): UseSeamInfiniteQueryResult & { queryKey: QueryKey } { + const { endpointClient: client, queryKeyPrefixes } = useSeamClient() + + if ('page_cursor' in (parameters ?? {})) { + throw new Error('Cannot use page_cursor with useSeamInfiniteQuery') + } + + const queryKey = [ + ...queryKeyPrefixes, + ...endpointPath.split('/').filter((v) => v !== ''), + parameters ?? {}, + ] + const result = useInfiniteQuery({ + enabled: client != null, + ...options, + queryKey, + initialPageParam: null, + getNextPageParam: (lastPage) => lastPage.nextPageCursor, + queryFn: async ({ pageParam }) => { + if (client == null) + return { + data: [] as Awaited>, + nextPageCursor: null, + } + // Using @ts-expect-error over any is preferred, but not possible here because TypeScript will run out of memory. + // Type assertion is needed here for performance reasons. The types are correct at runtime. + const endpoint = client[endpointPath] as (...args: any) => any + const request = endpoint(parameters, options) + const pages = client.createPaginator(request as SeamHttpRequest) + if (pageParam == null) { + const [data, { nextPageCursor }] = await pages.firstPage() + return { + data: data as Awaited>, + nextPageCursor, + } + } + // Type assertion is needed for pageParam since the Seam API expects a branded PageCursor type. + const [data, { nextPageCursor }] = await pages.nextPage( + pageParam as SeamPageCursor + ) + return { + data: data as Awaited>, + nextPageCursor, + } + }, + }) + return { ...result, queryKey } +} + +interface QueryData { + data: Awaited> + nextPageCursor: SeamPageCursor | null +} + +type QueryError = + | Error + | SeamHttpApiError + | SeamHttpInvalidInputError + | (QueryData['data'] extends ActionAttempt + ? + | SeamActionAttemptFailedError['data']> + | SeamActionAttemptTimeoutError['data']> + : never) + +type QueryOptions = Omit< + UseInfiniteQueryOptions, + 'queryKey' | 'queryFn' | 'initialPageParam' | 'getNextPageParam' +> diff --git a/src/lib/use-seam-mutation-without-workspace.ts b/src/lib/use-seam-mutation-without-workspace.ts new file mode 100644 index 0000000..4e03a78 --- /dev/null +++ b/src/lib/use-seam-mutation-without-workspace.ts @@ -0,0 +1,56 @@ +import type { + SeamHttpApiError, + SeamHttpEndpointsWithoutWorkspace, + SeamHttpEndpointWithoutWorkspaceMutationPaths, + SeamHttpInvalidInputError, +} from '@seamapi/http/connect' +import { + useMutation, + type UseMutationOptions, + type UseMutationResult, +} from '@tanstack/react-query' + +import { NullSeamClientError, useSeamClient } from 'lib/use-seam-client.js' + +export type UseSeamMutationWithoutWorkspaceVariables< + T extends SeamHttpEndpointWithoutWorkspaceMutationPaths, +> = Parameters[0] + +export type UseSeamMutationWithoutWorkspaceResult< + T extends SeamHttpEndpointWithoutWorkspaceMutationPaths, +> = UseMutationResult< + MutationData, + MutationError, + UseSeamMutationWithoutWorkspaceVariables +> + +export function useSeamMutationWithoutWorkspace< + T extends SeamHttpEndpointWithoutWorkspaceMutationPaths, +>( + endpointPath: T, + options: Parameters[1] & + MutationOptions< + MutationData, + MutationError, + UseSeamMutationWithoutWorkspaceVariables + > = {} +): UseSeamMutationWithoutWorkspaceResult { + const { endpointClient: client } = useSeamClient() + return useMutation({ + ...options, + mutationFn: async (variables) => { + if (client === null) throw new NullSeamClientError() + // Using @ts-expect-error over any is preferred, but not possible here because TypeScript will run out of memory. + // Type assertion is needed here for performance reasons. The types are correct at runtime. + const endpoint = client[endpointPath] as (...args: any) => Promise + return await endpoint(variables, options) + }, + }) +} + +type MutationData = + Awaited> + +type MutationError = Error | SeamHttpApiError | SeamHttpInvalidInputError + +type MutationOptions = Omit, 'mutationFn'> diff --git a/src/lib/use-seam-mutation.ts b/src/lib/use-seam-mutation.ts new file mode 100644 index 0000000..9e001dc --- /dev/null +++ b/src/lib/use-seam-mutation.ts @@ -0,0 +1,64 @@ +import type { + SeamActionAttemptFailedError, + SeamActionAttemptTimeoutError, + SeamHttpApiError, + SeamHttpEndpointMutationPaths, + SeamHttpEndpoints, + SeamHttpInvalidInputError, +} from '@seamapi/http/connect' +import type { ActionAttempt } from '@seamapi/types/connect' +import { + useMutation, + type UseMutationOptions, + type UseMutationResult, +} from '@tanstack/react-query' + +import { NullSeamClientError, useSeamClient } from 'lib/use-seam-client.js' + +export type UseSeamMutationVariables = + Parameters[0] + +export type UseSeamMutationResult = + UseMutationResult< + MutationData, + MutationError, + UseSeamMutationVariables + > + +export function useSeamMutation( + endpointPath: T, + options: Parameters[1] & + MutationOptions< + MutationData, + MutationError, + UseSeamMutationVariables + > = {} +): UseSeamMutationResult { + const { endpointClient: client } = useSeamClient() + return useMutation({ + ...options, + mutationFn: async (variables) => { + if (client === null) throw new NullSeamClientError() + // Using @ts-expect-error over any is preferred, but not possible here because TypeScript will run out of memory. + // Type assertion is needed here for performance reasons. The types are correct at runtime. + const endpoint = client[endpointPath] as (...args: any) => Promise + return await endpoint(variables, options) + }, + }) +} + +type MutationData = Awaited< + ReturnType +> + +type MutationError = + | Error + | SeamHttpApiError + | SeamHttpInvalidInputError + | (MutationData extends ActionAttempt + ? + | SeamActionAttemptFailedError> + | SeamActionAttemptTimeoutError> + : never) + +type MutationOptions = Omit, 'mutationFn'> diff --git a/src/lib/use-seam-query-without-workspace.ts b/src/lib/use-seam-query-without-workspace.ts new file mode 100644 index 0000000..abb610a --- /dev/null +++ b/src/lib/use-seam-query-without-workspace.ts @@ -0,0 +1,59 @@ +import type { + SeamHttpApiError, + SeamHttpEndpointsWithoutWorkspace, + SeamHttpEndpointWithoutWorkspaceQueryPaths, + SeamHttpInvalidInputError, +} from '@seamapi/http/connect' +import { + type QueryKey, + useQuery, + type UseQueryOptions, + type UseQueryResult, +} from '@tanstack/react-query' + +import { useSeamClient } from 'lib/use-seam-client.js' + +export type UseSeamQueryWithoutWorkspaceParameters< + T extends SeamHttpEndpointWithoutWorkspaceQueryPaths, +> = Parameters[0] + +export type UseSeamQueryWithoutWorkspaceResult< + T extends SeamHttpEndpointWithoutWorkspaceQueryPaths, +> = UseQueryResult, QueryError> + +export function useSeamQueryWithoutWorkspace< + T extends SeamHttpEndpointWithoutWorkspaceQueryPaths, +>( + endpointPath: T, + parameters: UseSeamQueryWithoutWorkspaceParameters = {}, + options: Parameters[1] & + QueryOptions, QueryError> = {} +): UseSeamQueryWithoutWorkspaceResult & { queryKey: QueryKey } { + const { endpointClient: client, queryKeyPrefixes } = useSeamClient() + const queryKey = [ + ...queryKeyPrefixes, + ...endpointPath.split('/').filter((v) => v !== ''), + parameters ?? {}, + ] + const result = useQuery({ + enabled: client != null, + ...options, + queryKey, + queryFn: async () => { + if (client == null) return null + // Using @ts-expect-error over any is preferred, but not possible here because TypeScript will run out of memory. + // Type assertion is needed here for performance reasons. The types are correct at runtime. + const endpoint = client[endpointPath] as (...args: any) => Promise + return await endpoint(parameters, options) + }, + }) + return { ...result, queryKey } +} + +type QueryData = Awaited< + ReturnType +> + +type QueryError = Error | SeamHttpApiError | SeamHttpInvalidInputError + +type QueryOptions = Omit, 'queryKey' | 'queryFn'> diff --git a/src/lib/use-seam-query.ts b/src/lib/use-seam-query.ts new file mode 100644 index 0000000..bc462c3 --- /dev/null +++ b/src/lib/use-seam-query.ts @@ -0,0 +1,66 @@ +import type { + SeamActionAttemptFailedError, + SeamActionAttemptTimeoutError, + SeamHttpApiError, + SeamHttpEndpointQueryPaths, + SeamHttpEndpoints, + SeamHttpInvalidInputError, +} from '@seamapi/http/connect' +import type { ActionAttempt } from '@seamapi/types/connect' +import { + type QueryKey, + useQuery, + type UseQueryOptions, + type UseQueryResult, +} from '@tanstack/react-query' + +import { useSeamClient } from 'lib/use-seam-client.js' + +export type UseSeamQueryParameters = + Parameters[0] + +export type UseSeamQueryResult = + UseQueryResult, QueryError> + +export function useSeamQuery( + endpointPath: T, + parameters: UseSeamQueryParameters = {}, + options: Parameters[1] & + QueryOptions, SeamHttpApiError> = {} +): UseSeamQueryResult & { queryKey: QueryKey } { + const { endpointClient: client, queryKeyPrefixes } = useSeamClient() + const queryKey = [ + ...queryKeyPrefixes, + ...endpointPath.split('/').filter((v) => v !== ''), + parameters ?? {}, + ] + const result = useQuery({ + enabled: client != null, + ...options, + queryKey, + queryFn: async () => { + if (client == null) return null + // Using @ts-expect-error over any is preferred, but not possible here because TypeScript will run out of memory. + // Type assertion is needed here for performance reasons. The types are correct at runtime. + const endpoint = client[endpointPath] as (...args: any) => Promise + return await endpoint(parameters, options) + }, + }) + return { ...result, queryKey } +} + +type QueryData = Awaited< + ReturnType +> + +type QueryError = + | Error + | SeamHttpApiError + | SeamHttpInvalidInputError + | (QueryData extends ActionAttempt + ? + | SeamActionAttemptFailedError> + | SeamActionAttemptTimeoutError> + : never) + +type QueryOptions = Omit, 'queryKey' | 'queryFn'> From 0b8c62ec409e4367914884c4cd105f8a2c816fb6 Mon Sep 17 00:00:00 2001 From: Seam Bot Date: Tue, 8 Jul 2025 20:30:10 +0000 Subject: [PATCH 2/2] ci: Format code --- src/lib/SeamQueryProvider.tsx | 2 +- src/lib/use-seam-client.ts | 16 ++++++++-------- src/lib/use-seam-infinite-query.ts | 4 ++-- src/lib/use-seam-mutation-without-workspace.ts | 2 +- src/lib/use-seam-mutation.ts | 2 +- src/lib/use-seam-query-without-workspace.ts | 2 +- src/lib/use-seam-query.ts | 2 +- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/lib/SeamQueryProvider.tsx b/src/lib/SeamQueryProvider.tsx index 088412f..755141b 100644 --- a/src/lib/SeamQueryProvider.tsx +++ b/src/lib/SeamQueryProvider.tsx @@ -145,7 +145,7 @@ function Session({ } const createDefaultSeamQueryContextValue = (): SeamQueryContext => { - return { client: null, endpointClient: null } + return { client: null, endpointClient: null } } const createSeamQueryContextValue = ( diff --git a/src/lib/use-seam-client.ts b/src/lib/use-seam-client.ts index 697bb9d..c7cdc2c 100644 --- a/src/lib/use-seam-client.ts +++ b/src/lib/use-seam-client.ts @@ -31,7 +31,7 @@ export function useSeamClient(): { ...context } = useSeamQueryContext() const userIdentifierKey = useUserIdentifierKeyOrFingerprint( - clientSessionToken != null ? '' : context.userIdentifierKey + clientSessionToken != null ? '' : context.userIdentifierKey, ) const { isPending, isError, error, data } = useQuery<{ @@ -63,7 +63,7 @@ export function useSeamClient(): { if (clientSessionToken != null) { const seam = SeamHttp.fromClientSessionToken( clientSessionToken, - clientOptions + clientOptions, ) return { @@ -78,7 +78,7 @@ export function useSeamClient(): { const seam = await SeamHttp.fromPublishableKey( publishableKey, userIdentifierKey, - clientOptions + clientOptions, ) return { @@ -95,7 +95,7 @@ export function useSeamClient(): { const endpointClientWithoutWorkspace = SeamHttpEndpointsWithoutWorkspace.fromClient( - clientWithoutWorkspace.client + clientWithoutWorkspace.client, ) if (workspaceId == null) { @@ -110,7 +110,7 @@ export function useSeamClient(): { const seam = SeamHttp.fromConsoleSessionToken( consoleSessionToken, workspaceId, - clientOptions + clientOptions, ) return { @@ -122,7 +122,7 @@ export function useSeamClient(): { } throw new Error( - 'Missing either a client, publishableKey, clientSessionToken, or consoleSessionToken.' + 'Missing either a client, publishableKey, clientSessionToken, or consoleSessionToken.', ) }, }) @@ -155,7 +155,7 @@ export class NullSeamClientError extends Error { 'Either a hook using useSeamClient was called outside of a SeamProvider or SeamQueryProvider,', 'or there was an error when creating the Seam client in useSeamClient,', 'or useSeamClient is still loading the client.', - ].join(' ') + ].join(' '), ) this.name = this.constructor.name Error.captureStackTrace(this, this.constructor) @@ -163,7 +163,7 @@ export class NullSeamClientError extends Error { } function useUserIdentifierKeyOrFingerprint( - userIdentifierKey: string | undefined + userIdentifierKey: string | undefined, ): string { useEffect(() => { if (userIdentifierKey != null) return diff --git a/src/lib/use-seam-infinite-query.ts b/src/lib/use-seam-infinite-query.ts index e356d1a..5ff93f0 100644 --- a/src/lib/use-seam-infinite-query.ts +++ b/src/lib/use-seam-infinite-query.ts @@ -32,7 +32,7 @@ export function useSeamInfiniteQuery< endpointPath: T, parameters: UseSeamInfiniteQueryParameters = {}, options: Parameters[1] & - QueryOptions, QueryError> = {} + QueryOptions, QueryError> = {}, ): UseSeamInfiniteQueryResult & { queryKey: QueryKey } { const { endpointClient: client, queryKeyPrefixes } = useSeamClient() @@ -71,7 +71,7 @@ export function useSeamInfiniteQuery< } // Type assertion is needed for pageParam since the Seam API expects a branded PageCursor type. const [data, { nextPageCursor }] = await pages.nextPage( - pageParam as SeamPageCursor + pageParam as SeamPageCursor, ) return { data: data as Awaited>, diff --git a/src/lib/use-seam-mutation-without-workspace.ts b/src/lib/use-seam-mutation-without-workspace.ts index 4e03a78..12af4d8 100644 --- a/src/lib/use-seam-mutation-without-workspace.ts +++ b/src/lib/use-seam-mutation-without-workspace.ts @@ -33,7 +33,7 @@ export function useSeamMutationWithoutWorkspace< MutationData, MutationError, UseSeamMutationWithoutWorkspaceVariables - > = {} + > = {}, ): UseSeamMutationWithoutWorkspaceResult { const { endpointClient: client } = useSeamClient() return useMutation({ diff --git a/src/lib/use-seam-mutation.ts b/src/lib/use-seam-mutation.ts index 9e001dc..8fb0933 100644 --- a/src/lib/use-seam-mutation.ts +++ b/src/lib/use-seam-mutation.ts @@ -32,7 +32,7 @@ export function useSeamMutation( MutationData, MutationError, UseSeamMutationVariables - > = {} + > = {}, ): UseSeamMutationResult { const { endpointClient: client } = useSeamClient() return useMutation({ diff --git a/src/lib/use-seam-query-without-workspace.ts b/src/lib/use-seam-query-without-workspace.ts index abb610a..c3c3b23 100644 --- a/src/lib/use-seam-query-without-workspace.ts +++ b/src/lib/use-seam-query-without-workspace.ts @@ -27,7 +27,7 @@ export function useSeamQueryWithoutWorkspace< endpointPath: T, parameters: UseSeamQueryWithoutWorkspaceParameters = {}, options: Parameters[1] & - QueryOptions, QueryError> = {} + QueryOptions, QueryError> = {}, ): UseSeamQueryWithoutWorkspaceResult & { queryKey: QueryKey } { const { endpointClient: client, queryKeyPrefixes } = useSeamClient() const queryKey = [ diff --git a/src/lib/use-seam-query.ts b/src/lib/use-seam-query.ts index bc462c3..8d25d2e 100644 --- a/src/lib/use-seam-query.ts +++ b/src/lib/use-seam-query.ts @@ -26,7 +26,7 @@ export function useSeamQuery( endpointPath: T, parameters: UseSeamQueryParameters = {}, options: Parameters[1] & - QueryOptions, SeamHttpApiError> = {} + QueryOptions, SeamHttpApiError> = {}, ): UseSeamQueryResult & { queryKey: QueryKey } { const { endpointClient: client, queryKeyPrefixes } = useSeamClient() const queryKey = [