diff --git a/package-lock.json b/package-lock.json index 4e0271864..e67048a80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@rxfork/r2wc-react-to-web-component": "^2.4.0", "@seamapi/fake-devicedb": "^1.6.1", "@seamapi/fake-seam-connect": "^1.76.0", - "@seamapi/http": "^1.38.3", + "@seamapi/http": "^1.40.0", "@seamapi/types": "^1.395.3", "@storybook/addon-designs": "^7.0.1", "@storybook/addon-essentials": "^7.0.2", @@ -5993,13 +5993,13 @@ } }, "node_modules/@seamapi/http": { - "version": "1.38.3", - "resolved": "https://registry.npmjs.org/@seamapi/http/-/http-1.38.3.tgz", - "integrity": "sha512-KBUZpSCGs2UpnzP5480Qa4W8IioCKH0fssrTt7DyMGJ/T9o1mWuxgvqGf6l89ISNo2Z0H6I27Q/K9dWzg7sqYw==", + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@seamapi/http/-/http-1.40.0.tgz", + "integrity": "sha512-Ae7l3ZVD6s4wkwDD/csB+c8dQQLq24dLThRCH3iIcMhKSlPmyFJieCVuVa+4v1dIKvRbA6FHXAwokzHh1OQjtA==", "dev": true, "license": "MIT", "dependencies": { - "@seamapi/url-search-params-serializer": "^2.0.0-beta.2", + "@seamapi/url-search-params-serializer": "^2.0.0", "axios": "^1.9.0", "axios-retry": "^4.4.2" }, @@ -6031,9 +6031,9 @@ } }, "node_modules/@seamapi/url-search-params-serializer": { - "version": "2.0.0-beta.2", - "resolved": "https://registry.npmjs.org/@seamapi/url-search-params-serializer/-/url-search-params-serializer-2.0.0-beta.2.tgz", - "integrity": "sha512-ASHYo5/0IY7iB/cWcZA9b6meAa5b22NfEZyIuZQ2I90L7WeKzeWxEmusA4nfc26giQOeuEF1xAIGSdGrT+lGZg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@seamapi/url-search-params-serializer/-/url-search-params-serializer-2.0.0.tgz", + "integrity": "sha512-mXHTk9b4pVueFOKkpWyQG6yVK8YWPjr3ksdvBoKhSn8inrBM3Blp539BHuiT4GDfjYtgWq+/j3GN3JTDMuXxJQ==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 31631b46c..4f5c9982a 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "@rxfork/r2wc-react-to-web-component": "^2.4.0", "@seamapi/fake-devicedb": "^1.6.1", "@seamapi/fake-seam-connect": "^1.76.0", - "@seamapi/http": "^1.38.3", + "@seamapi/http": "^1.40.0", "@seamapi/types": "^1.395.3", "@storybook/addon-designs": "^7.0.1", "@storybook/addon-essentials": "^7.0.2", diff --git a/src/lib/seam/SeamQueryProvider.tsx b/src/lib/seam/SeamQueryProvider.tsx index 892859011..55f0f49db 100644 --- a/src/lib/seam/SeamQueryProvider.tsx +++ b/src/lib/seam/SeamQueryProvider.tsx @@ -25,6 +25,8 @@ export interface SeamQueryContext { publishableKey?: string | undefined userIdentifierKey?: string | undefined clientSessionToken?: string | undefined + consoleSessionToken?: string | undefined + workspaceId?: string | undefined queryKeyPrefix?: string | undefined } @@ -32,6 +34,7 @@ export type SeamQueryProviderProps = | SeamQueryProviderPropsWithClient | SeamQueryProviderPropsWithPublishableKey | SeamQueryProviderPropsWithClientSessionToken + | SeamQueryProviderPropsWithConsoleSessionToken export interface SeamQueryProviderPropsWithClient extends SeamQueryProviderBaseProps { @@ -52,6 +55,13 @@ export interface SeamQueryProviderPropsWithClientSessionToken clientSessionToken: string } +export interface SeamQueryProviderPropsWithConsoleSessionToken + extends SeamQueryProviderBaseProps, + SeamQueryProviderClientOptions { + consoleSessionToken: string + workspaceId?: string | undefined +} + interface SeamQueryProviderBaseProps extends PropsWithChildren { queryClient?: QueryClient | undefined onSessionUpdate?: (client: SeamHttp) => void @@ -74,7 +84,8 @@ export function SeamQueryProvider({ if ( context.client == null && context.publishableKey == null && - context.clientSessionToken == null + context.clientSessionToken == null && + context.consoleSessionToken == null ) { return defaultSeamQueryContextValue } @@ -84,10 +95,11 @@ export function SeamQueryProvider({ if ( value.client == null && value.publishableKey == null && - value.clientSessionToken == null + value.clientSessionToken == null && + value.consoleSessionToken == null ) { throw new Error( - `Must provide either a Seam client, clientSessionToken, or a publishableKey.` + `Must provide either a Seam client, clientSessionToken, publishableKey or consoleSessionToken.` ) } @@ -177,6 +189,17 @@ const createSeamQueryContextValue = ( } } + if (isSeamQueryProviderPropsWithConsoleSessionToken(options)) { + const { consoleSessionToken, workspaceId, ...clientOptions } = options + return { + consoleSessionToken, + workspaceId, + clientOptions, + client: null, + endpointClient: null, + } + } + return { client: null, endpointClient: null } } @@ -229,6 +252,18 @@ const isSeamQueryProviderPropsWithPublishableKey = ( ) } + 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 } @@ -259,6 +294,54 @@ const isSeamQueryProviderPropsWithClientSessionToken = ( ) } + 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 } diff --git a/src/lib/seam/index.ts b/src/lib/seam/index.ts index 68df2751c..097759f7e 100644 --- a/src/lib/seam/index.ts +++ b/src/lib/seam/index.ts @@ -13,5 +13,7 @@ export * from './devices/use-devices.js' export * from './SeamProvider.js' export * from './use-seam-client.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-result.js' +export * from './use-seam-query-without-workspace.js' diff --git a/src/lib/seam/use-seam-client.ts b/src/lib/seam/use-seam-client.ts index 0cb95820e..697bb9dd5 100644 --- a/src/lib/seam/use-seam-client.ts +++ b/src/lib/seam/use-seam-client.ts @@ -1,4 +1,9 @@ -import { SeamHttp, SeamHttpEndpoints } from '@seamapi/http/connect' +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' @@ -8,6 +13,8 @@ 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 @@ -18,6 +25,8 @@ export function useSeamClient(): { clientOptions, publishableKey, clientSessionToken, + consoleSessionToken, + workspaceId, queryKeyPrefix, ...context } = useSeamQueryContext() @@ -25,9 +34,12 @@ export function useSeamClient(): { clientSessionToken != null ? '' : context.userIdentifierKey ) - const { isPending, isError, error, data } = useQuery< - [SeamHttp, SeamHttpEndpoints] - >({ + const { isPending, isError, error, data } = useQuery<{ + client: SeamHttp | null + endpointClient: SeamHttpEndpoints | null + clientWithoutWorkspace: SeamHttpWithoutWorkspace | null + endpointClientWithoutWorkspace: SeamHttpEndpointsWithoutWorkspace | null + }>({ queryKey: [ ...getQueryKeyPrefixes({ queryKeyPrefix }), 'client', @@ -41,46 +53,93 @@ export function useSeamClient(): { ], queryFn: async () => { if (client != null) - return [client, SeamHttpEndpoints.fromClient(client.client)] + return { + client, + endpointClient: SeamHttpEndpoints.fromClient(client.client), + clientWithoutWorkspace: null, + endpointClientWithoutWorkspace: null, + } if (clientSessionToken != null) { - const clientSessionTokenClient = SeamHttp.fromClientSessionToken( + const seam = SeamHttp.fromClientSessionToken( clientSessionToken, clientOptions ) - return [ - clientSessionTokenClient, - SeamHttpEndpoints.fromClient(clientSessionTokenClient.client), - ] + return { + client: seam, + endpointClient: SeamHttpEndpoints.fromClient(seam.client), + clientWithoutWorkspace: null, + endpointClientWithoutWorkspace: null, + } } - if (publishableKey == null) { - throw new Error( - 'Missing either a client, publishableKey, or clientSessionToken' + if (publishableKey != null) { + const seam = await SeamHttp.fromPublishableKey( + publishableKey, + userIdentifierKey, + clientOptions ) + + return { + client: seam, + endpointClient: SeamHttpEndpoints.fromClient(seam.client), + clientWithoutWorkspace: null, + endpointClientWithoutWorkspace: null, + } } - const publishableKeyClient = await SeamHttp.fromPublishableKey( - publishableKey, - userIdentifierKey, - clientOptions + 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 [ - publishableKeyClient, - SeamHttpEndpoints.fromClient(publishableKeyClient.client), - ] }, }) return { - client: data?.[0] ?? null, - endpointClient: data?.[1] ?? null, + 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, @@ -132,11 +191,15 @@ const getQueryKeyPrefixes = ({ 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' @@ -150,5 +213,13 @@ const getQueryKeyPrefixes = ({ 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/seam/use-seam-mutation-without-workspace.ts b/src/lib/seam/use-seam-mutation-without-workspace.ts new file mode 100644 index 000000000..e1890d6b6 --- /dev/null +++ b/src/lib/seam/use-seam-mutation-without-workspace.ts @@ -0,0 +1,53 @@ +import type { + SeamHttpApiError, + SeamHttpEndpointsWithoutWorkspace, + SeamHttpEndpointWithoutWorkspaceMutationPaths, +} from '@seamapi/http/connect' +import { + useMutation, + type UseMutationOptions, + type UseMutationResult, +} from '@tanstack/react-query' + +import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js' + +export type UseSeamMutationWithoutWorkspaceVariables< + T extends SeamHttpEndpointWithoutWorkspaceMutationPaths, +> = Parameters[0] + +export type UseSeamMutationWithoutWorkspaceResult< + T extends SeamHttpEndpointWithoutWorkspaceMutationPaths, +> = UseMutationResult< + MutationData, + SeamHttpApiError, + UseSeamMutationWithoutWorkspaceVariables +> + +export function UseSeamMutationWithoutWorkspace< + T extends SeamHttpEndpointWithoutWorkspaceMutationPaths, +>( + endpointPath: T, + options: Parameters[1] & + MutationOptions< + MutationData, + SeamHttpApiError, + 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 MutationOptions = Omit, 'mutationFn'> diff --git a/src/lib/seam/use-seam-query-without-workspace.ts b/src/lib/seam/use-seam-query-without-workspace.ts new file mode 100644 index 000000000..923b35666 --- /dev/null +++ b/src/lib/seam/use-seam-query-without-workspace.ts @@ -0,0 +1,53 @@ +import type { + SeamHttpApiError, + SeamHttpEndpointsWithoutWorkspace, + SeamHttpEndpointWithoutWorkspaceQueryPaths, +} from '@seamapi/http/connect' +import { + useQuery, + type UseQueryOptions, + type UseQueryResult, +} from '@tanstack/react-query' + +import { useSeamClient } from 'lib/seam/use-seam-client.js' + +export type UseSeamQueryWithoutWorkspaceParameters< + T extends SeamHttpEndpointWithoutWorkspaceQueryPaths, +> = Parameters[0] + +export type UseSeamQueryWithoutWorkspaceResult< + T extends SeamHttpEndpointWithoutWorkspaceQueryPaths, +> = UseQueryResult, SeamHttpApiError> + +export function useSeamQueryWithoutWorkspace< + T extends SeamHttpEndpointWithoutWorkspaceQueryPaths, +>( + endpointPath: T, + parameters?: UseSeamQueryWithoutWorkspaceParameters, + options: Parameters[1] & + QueryOptions, SeamHttpApiError> = {} +): UseSeamQueryWithoutWorkspaceResult { + const { endpointClient: client, queryKeyPrefixes } = useSeamClient() + return useQuery({ + enabled: client != null, + ...options, + queryKey: [ + ...queryKeyPrefixes, + ...endpointPath.split('/').filter((v) => v !== ''), + parameters, + ], + 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) + }, + }) +} + +type QueryData = Awaited< + ReturnType +> + +type QueryOptions = Omit, 'queryKey' | 'queryFn'>