-
Notifications
You must be signed in to change notification settings - Fork 0
[Content management] client POC #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6f156e9
ca25c31
fb8c57c
ba7a4b0
8552476
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
| * in compliance with, at your election, the Elastic License 2.0 or the Server | ||
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| import { QueryClient, QueryClientProvider, useMutation, useQuery } from '@tanstack/react-query'; | ||
| import React from 'react'; | ||
| import type { RpcClient } from '../rpc'; | ||
| import { createQueryObservable } from './query_observable'; | ||
| import type { SearchIn } from '../../common'; | ||
| import { CreateIn, CreateOut } from '../../common'; | ||
|
|
||
| const contentQueryKeyBuilder = { | ||
| all: (type?: string) => [type ? type : '*'] as const, | ||
| // lists: (type: string) => [...contentQueryKeyBuilder.all(type), 'list'] as const, | ||
| // list: (type: string, filters: any) => | ||
| // [...contentQueryKeyBuilder.lists(type), { filters }] as const, | ||
| item: (type: string, id: string) => { | ||
| return [...contentQueryKeyBuilder.all(type), id] as const; | ||
| }, | ||
| itemPreview: (type: string, id: string) => { | ||
| return [...contentQueryKeyBuilder.item(type, id), 'preview'] as const; | ||
| }, | ||
| search: (searchParams: SearchIn = {}) => { | ||
| const { type, ...otherParams } = searchParams; | ||
| return [...contentQueryKeyBuilder.all(type), 'search', otherParams] as const; | ||
| }, | ||
| }; | ||
|
|
||
| const createContentQueryOptionBuilder = ({ rpcClient }: { rpcClient: RpcClient }) => { | ||
| return { | ||
| get: <TContentItem extends object = object>(type: string, id: string) => { | ||
| return { | ||
| queryKey: contentQueryKeyBuilder.item(type, id), | ||
| queryFn: () => rpcClient.get<TContentItem>({ type, id }), | ||
| }; | ||
| }, | ||
| getPreview: (type: string, id: string) => { | ||
| return { | ||
| queryKey: contentQueryKeyBuilder.itemPreview(type, id), | ||
| queryFn: () => rpcClient.getPreview({ type, id }), | ||
| }; | ||
| }, | ||
| search: (searchParams: SearchIn) => { | ||
| return { | ||
| queryKey: contentQueryKeyBuilder.search(searchParams), | ||
| queryFn: () => rpcClient.search(searchParams), | ||
| }; | ||
| }, | ||
| }; | ||
| }; | ||
|
|
||
| export class ContentClient { | ||
| private readonly queryClient: QueryClient; | ||
| private readonly contentQueryOptionBuilder: ReturnType<typeof createContentQueryOptionBuilder>; | ||
|
|
||
| public get _queryClient() { | ||
| return this.queryClient; | ||
| } | ||
|
|
||
| public get _contentItemQueryOptionBuilder() { | ||
| return this.contentQueryOptionBuilder; | ||
| } | ||
|
|
||
| constructor(private readonly rpcClient: RpcClient) { | ||
| this.queryClient = new QueryClient(); | ||
| this.contentQueryOptionBuilder = createContentQueryOptionBuilder({ | ||
| rpcClient: this.rpcClient, | ||
| }); | ||
| } | ||
|
|
||
| get<ContentItem extends object = object>({ | ||
| type, | ||
| id, | ||
| }: { | ||
| type: string; | ||
| id: string; | ||
| }): Promise<ContentItem> { | ||
| return this.queryClient.fetchQuery(this.contentQueryOptionBuilder.get<ContentItem>(type, id)); | ||
| } | ||
|
|
||
| get$<ContentItem extends object = object>({ type, id }: { type: string; id: string }) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure I fully understand what those observable version of the method do 😊
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you seen https://docs.google.com/document/d/13aOspEx_yGcdaRwXZTAOCaiPuAIZlcrkZopvR0cr2_k/edit#heading=h.oijoa468kc4q ? The idea is that cache is reactive, so the main way to access data would be through the react hook or RXJS observable. Regular promise based method can be used in the cases where reactivity is not needed
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see, indeed this is outside React context so we need a way to make it reactive. 👍 |
||
| return createQueryObservable<ContentItem>( | ||
| this.queryClient, | ||
| this.contentQueryOptionBuilder.get(type, id) | ||
| ); | ||
| } | ||
|
|
||
| getPreview({ type, id }: { type: string; id: string }) { | ||
| return this.queryClient.fetchQuery(this.contentQueryOptionBuilder.getPreview(type, id)); | ||
| } | ||
|
|
||
| getPreview$({ type, id }: { type: string; id: string }) { | ||
| return createQueryObservable( | ||
| this.queryClient, | ||
| this.contentQueryOptionBuilder.getPreview(type, id) | ||
| ); | ||
| } | ||
|
|
||
| search(searchParams: SearchIn) { | ||
| return this.queryClient.fetchQuery(this.contentQueryOptionBuilder.search(searchParams)); | ||
| } | ||
|
|
||
| search$(searchParams: SearchIn) { | ||
| return createQueryObservable( | ||
| this.queryClient, | ||
| this.contentQueryOptionBuilder.search(searchParams) | ||
| ); | ||
| } | ||
|
|
||
| create<I extends object, O extends CreateOut, Options extends object | undefined = undefined>( | ||
| input: CreateIn<I, Options> | ||
| ): Promise<O> { | ||
| return this.rpcClient.create(input); | ||
| } | ||
| } | ||
|
|
||
| const ContentClientContext = React.createContext<ContentClient>(null as unknown as ContentClient); | ||
|
|
||
| export const useContentClient = (): ContentClient => { | ||
| const contentClient = React.useContext(ContentClientContext); | ||
| if (!contentClient) throw new Error('contentClient not found'); | ||
| return contentClient; | ||
| }; | ||
|
|
||
| export const ContentClientProvider: React.FC<{ contentClient: ContentClient }> = ({ | ||
| contentClient, | ||
| children, | ||
| }) => { | ||
| return ( | ||
| <ContentClientContext.Provider value={contentClient}> | ||
| <QueryClientProvider client={contentClient._queryClient}>{children}</QueryClientProvider> | ||
| </ContentClientContext.Provider> | ||
| ); | ||
| }; | ||
|
|
||
| export const useContentItem = <ContentItem extends object = object>( | ||
| { | ||
| id, | ||
| type, | ||
| }: { | ||
| id: string; | ||
| type: string; | ||
| }, | ||
| queryOptions: { enabled?: boolean } = { enabled: true } | ||
| ) => { | ||
| const contentQueryClient = useContentClient(); | ||
| const query = useQuery({ | ||
| ...contentQueryClient._contentItemQueryOptionBuilder.get<ContentItem>(type, id), | ||
| ...queryOptions, | ||
| }); | ||
| return query; | ||
| }; | ||
|
|
||
| export const useContentItemPreview = ( | ||
| { | ||
| id, | ||
| type, | ||
| }: { | ||
| id: string; | ||
| type: string; | ||
| }, | ||
| queryOptions: { enabled?: boolean } = { enabled: true } | ||
| ) => { | ||
| const contentQueryClient = useContentClient(); | ||
| const query = useQuery({ | ||
| ...contentQueryClient._contentItemQueryOptionBuilder.getPreview(type, id), | ||
| ...queryOptions, | ||
| }); | ||
| return query; | ||
| }; | ||
|
|
||
| export const useContentSearch = ( | ||
| searchParams: SearchIn = {}, | ||
| queryOptions: { enabled?: boolean } = { enabled: true } | ||
| ) => { | ||
| const contentQueryClient = useContentClient(); | ||
| const query = useQuery({ | ||
| ...contentQueryClient._contentItemQueryOptionBuilder.search(searchParams), | ||
| ...queryOptions, | ||
| }); | ||
| return query; | ||
| }; | ||
|
|
||
| export const useCreateContentItemMutation = < | ||
| I extends object, | ||
| O extends CreateOut, | ||
| Options extends object | undefined = undefined | ||
| >() => { | ||
| const contentQueryClient = useContentClient(); | ||
| return useMutation({ | ||
| mutationFn: (input: CreateIn<I, Options>) => { | ||
| return contentQueryClient.create(input); | ||
| }, | ||
| }); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
| * in compliance with, at your election, the Elastic License 2.0 or the Server | ||
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| export * from './content_client'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| /* | ||
| * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
| * or more contributor license agreements. Licensed under the Elastic License | ||
| * 2.0 and the Server Side Public License, v 1; you may not use this file except | ||
| * in compliance with, at your election, the Elastic License 2.0 or the Server | ||
| * Side Public License, v 1. | ||
| */ | ||
|
|
||
| import { | ||
| notifyManager, | ||
| QueryObserver, | ||
| QueryObserverOptions, | ||
| QueryObserverResult, | ||
| QueryClient, | ||
| } from '@tanstack/react-query'; | ||
| import { Observable } from 'rxjs'; | ||
| import { QueryKey } from '@tanstack/query-core/src/types'; | ||
|
|
||
| export const createQueryObservable = < | ||
| TQueryFnData = unknown, | ||
| TError = unknown, | ||
| TData = TQueryFnData, | ||
| TQueryData = TQueryFnData, | ||
| TQueryKey extends QueryKey = QueryKey | ||
| >( | ||
| queryClient: QueryClient, | ||
| queryOptions: QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> | ||
| ) => { | ||
| const queryObserver = new QueryObserver( | ||
| queryClient, | ||
| queryClient.defaultQueryOptions(queryOptions) | ||
| ); | ||
|
|
||
| return new Observable<QueryObserverResult<TData, TError>>((subscriber) => { | ||
| const unsubscribe = queryObserver.subscribe( | ||
| // notifyManager is a singleton that batches updates across react query | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it is a public api. It is exported from query-core and is used by react (or other libs) implementations. I learned about it from the useQuery hook source
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. Isn't it risky to not use a public api? |
||
| notifyManager.batchCalls((result) => { | ||
| subscriber.next(result); | ||
| }) | ||
| ); | ||
| return () => { | ||
| unsubscribe(); | ||
| }; | ||
| }); | ||
| }; | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Can we maybe shorten this to
queryKeyBuilder?