-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[CM] Client-side content client: setup update delete search methods
#151041
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
e8f8396
116dd81
370b3c0
c9bdcc8
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 |
|---|---|---|
|
|
@@ -8,20 +8,19 @@ | |
|
|
||
| import { lastValueFrom } from 'rxjs'; | ||
| import { takeWhile, toArray } from 'rxjs/operators'; | ||
| import type { CrudClient } from '../crud_client'; | ||
| import { createCrudClientMock } from '../crud_client/crud_client.mock'; | ||
| import { ContentClient } from './content_client'; | ||
| import type { GetIn, CreateIn } from '../../common'; | ||
| import type { GetIn, CreateIn, UpdateIn, DeleteIn, SearchIn, SearchOut } from '../../common'; | ||
|
|
||
| let contentClient: ContentClient; | ||
| let crudClient: jest.Mocked<CrudClient>; | ||
| beforeEach(() => { | ||
| crudClient = createCrudClientMock(); | ||
| contentClient = new ContentClient(() => crudClient); | ||
| }); | ||
| const setup = () => { | ||
|
Contributor
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. Making tests cleaner following @vadimkibana's suggestion from another pr |
||
| const crudClient = createCrudClientMock(); | ||
| const contentClient = new ContentClient(() => crudClient); | ||
| return { crudClient, contentClient }; | ||
| }; | ||
|
|
||
| describe('#get', () => { | ||
| it('calls rpcClient.get with input and returns output', async () => { | ||
| const { crudClient, contentClient } = setup(); | ||
| const input: GetIn = { id: 'test', contentType: 'testType' }; | ||
| const output = { test: 'test' }; | ||
| crudClient.get.mockResolvedValueOnce(output); | ||
|
|
@@ -30,6 +29,7 @@ describe('#get', () => { | |
| }); | ||
|
|
||
| it('calls rpcClient.get$ with input and returns output', async () => { | ||
| const { crudClient, contentClient } = setup(); | ||
| const input: GetIn = { id: 'test', contentType: 'testType' }; | ||
| const output = { test: 'test' }; | ||
| crudClient.get.mockResolvedValueOnce(output); | ||
|
|
@@ -52,6 +52,7 @@ describe('#get', () => { | |
|
|
||
| describe('#create', () => { | ||
| it('calls rpcClient.create with input and returns output', async () => { | ||
| const { crudClient, contentClient } = setup(); | ||
| const input: CreateIn = { contentType: 'testType', data: { foo: 'bar' } }; | ||
| const output = { test: 'test' }; | ||
| crudClient.create.mockResolvedValueOnce(output); | ||
|
|
@@ -60,3 +61,59 @@ describe('#create', () => { | |
| expect(crudClient.create).toBeCalledWith(input); | ||
| }); | ||
| }); | ||
|
|
||
| describe('#update', () => { | ||
| it('calls rpcClient.update with input and returns output', async () => { | ||
| const { crudClient, contentClient } = setup(); | ||
| const input: UpdateIn = { contentType: 'testType', data: { id: 'test', foo: 'bar' } }; | ||
| const output = { test: 'test' }; | ||
| crudClient.update.mockResolvedValueOnce(output); | ||
|
|
||
| expect(await contentClient.update(input)).toEqual(output); | ||
| expect(crudClient.update).toBeCalledWith(input); | ||
| }); | ||
| }); | ||
|
|
||
| describe('#delete', () => { | ||
| it('calls rpcClient.delete with input and returns output', async () => { | ||
| const { crudClient, contentClient } = setup(); | ||
| const input: DeleteIn = { contentType: 'testType', data: { id: 'test' } }; | ||
| const output = { test: 'test' }; | ||
| crudClient.delete.mockResolvedValueOnce(output); | ||
|
|
||
| expect(await contentClient.delete(input)).toEqual(output); | ||
| expect(crudClient.delete).toBeCalledWith(input); | ||
| }); | ||
| }); | ||
|
|
||
| describe('#search', () => { | ||
| it('calls rpcClient.search with input and returns output', async () => { | ||
| const { crudClient, contentClient } = setup(); | ||
| const input: SearchIn = { contentType: 'testType', params: {} }; | ||
| const output: SearchOut = { hits: [{ test: 'test' }] }; | ||
| crudClient.search.mockResolvedValueOnce(output); | ||
| expect(await contentClient.search(input)).toEqual(output); | ||
| expect(crudClient.search).toBeCalledWith(input); | ||
| }); | ||
|
|
||
| it('calls rpcClient.search$ with input and returns output', async () => { | ||
| const { crudClient, contentClient } = setup(); | ||
| const input: SearchIn = { contentType: 'testType', params: {} }; | ||
| const output: SearchOut = { hits: [{ test: 'test' }] }; | ||
| crudClient.search.mockResolvedValueOnce(output); | ||
| const search$ = contentClient.search$(input).pipe( | ||
| takeWhile((result) => { | ||
| return result.data == null; | ||
| }, true), | ||
| toArray() | ||
| ); | ||
|
|
||
| const [loadingState, loadedState] = await lastValueFrom(search$); | ||
|
|
||
| expect(loadingState.isLoading).toBe(true); | ||
| expect(loadingState.data).toBeUndefined(); | ||
|
|
||
| expect(loadedState.isLoading).toBe(false); | ||
| expect(loadedState.data).toEqual(output); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,13 +9,16 @@ | |
| import { QueryClient } from '@tanstack/react-query'; | ||
| import { createQueryObservable } from './query_observable'; | ||
| import type { CrudClient } from '../crud_client'; | ||
| import type { CreateIn, GetIn } from '../../common'; | ||
| import type { CreateIn, GetIn, UpdateIn, DeleteIn, SearchIn, SearchOut } from '../../common'; | ||
|
|
||
| const queryKeyBuilder = { | ||
| all: (type: string) => [type] as const, | ||
| item: (type: string, id: string) => { | ||
| return [...queryKeyBuilder.all(type), id] as const; | ||
| }, | ||
| search: (type: string, params: unknown) => { | ||
|
Contributor
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. Can we call it
Contributor
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. Also, can we now rename
Contributor
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.
Sure don't mind, I so you already did that in your pr.
Hm, ContentTypeRegistry id means id of the type. But here the main subject is Content (Content item itself), so id refer to the content. e.g. see item which is a key for get: item: (type: string, id: string) => { maybe it should be? item: (contentTypeId: string, itemId: string) => { search: (contentTypeId: string, query: unknown) => {
Contributor
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. Yes, server side I am using |
||
| return [...queryKeyBuilder.all(type), 'search', params] as const; | ||
|
Contributor
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. Shouldn't we
Contributor
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. Done under the hood |
||
| }, | ||
| }; | ||
|
|
||
| const createQueryOptionBuilder = ({ | ||
|
|
@@ -30,6 +33,12 @@ const createQueryOptionBuilder = ({ | |
| queryFn: () => crudClientProvider(input.contentType).get<I, O>(input), | ||
| }; | ||
| }, | ||
| search: <I extends SearchIn = SearchIn, O extends SearchOut = SearchOut>(input: I) => { | ||
| return { | ||
| queryKey: queryKeyBuilder.search(input.contentType, input.params), | ||
|
Contributor
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 think this should be
Contributor
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. In the current types it seems it is I understand that the current type will change so I'd not worry about the current state of them |
||
| queryFn: () => crudClientProvider(input.contentType).search<I, O>(input), | ||
| }; | ||
| }, | ||
| }; | ||
| }; | ||
|
|
||
|
|
@@ -55,4 +64,20 @@ export class ContentClient { | |
| create<I extends CreateIn, O = unknown>(input: I): Promise<O> { | ||
| return this.crudClientProvider(input.contentType).create(input); | ||
| } | ||
|
|
||
| update<I extends UpdateIn, O = unknown>(input: I): Promise<O> { | ||
| return this.crudClientProvider(input.contentType).update(input); | ||
| } | ||
|
|
||
| delete<I extends DeleteIn, O = unknown>(input: I): Promise<O> { | ||
| return this.crudClientProvider(input.contentType).delete(input); | ||
| } | ||
|
|
||
| search<I extends SearchIn, O extends SearchOut = SearchOut>(input: I): Promise<O> { | ||
| return this.crudClientProvider(input.contentType).search(input); | ||
| } | ||
|
|
||
| search$<I extends SearchIn, O extends SearchOut = SearchOut>(input: I) { | ||
| return createQueryObservable(this.queryClient, this.queryOptionBuilder.search<I, O>(input)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |
|
|
||
| import { useMutation } from '@tanstack/react-query'; | ||
| import { useContentClient } from './content_client_context'; | ||
| import type { CreateIn } from '../../common'; | ||
| import type { CreateIn, UpdateIn, DeleteIn } from '../../common'; | ||
|
|
||
| export const useCreateContentMutation = <I extends CreateIn = CreateIn, O = unknown>() => { | ||
| const contentClient = useContentClient(); | ||
|
|
@@ -18,3 +18,21 @@ export const useCreateContentMutation = <I extends CreateIn = CreateIn, O = unkn | |
| }, | ||
| }); | ||
| }; | ||
|
|
||
| export const useUpdateContentMutation = <I extends UpdateIn = UpdateIn, O = unknown>() => { | ||
|
Contributor
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 think we want to expose types that unique content can use to declare their mutation (and avoid the problem of declaring the generic everywhere). Looking at the export type CreateContentMutationHook<
I extends CreateIn = CreateIn,
O = unknown
> = UseMutationResult<O, unknown, I, unknown>;Each content can then expose their mutation types type CreateFooMutation = CreateContentMutationHook<
{
contentTypeId: 'foo';
data: { title: string }; // Shape of the input
},
{ result: 'success' | 'error' } // Shape of the response
>;And when consumed it can be used like this const createFoo: CreateFooMutation = useCreateContentMutation();It is properly typed
Contributor
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. If that works we can apply the same for queries
Contributor
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. Sounds good. I'll try and see if there are any issues |
||
| const contentClient = useContentClient(); | ||
| return useMutation({ | ||
| mutationFn: (input: I) => { | ||
| return contentClient.update(input); | ||
| }, | ||
| }); | ||
| }; | ||
|
|
||
| export const useDeleteContentMutation = <I extends DeleteIn = DeleteIn, O = unknown>() => { | ||
| const contentClient = useContentClient(); | ||
| return useMutation({ | ||
| mutationFn: (input: I) => { | ||
| return contentClient.delete(input); | ||
| }, | ||
| }); | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
Just following the older examples for now. It is clear that these types will change when the real RPC implementation will be implemented.