From 86a410883b5872c2ab509075f907e9a92a332a8e Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 9 Jan 2025 15:26:13 -0500 Subject: [PATCH 1/4] base implementation of document write hooks --- src/hooks/documents.ts | 101 +++++++++++++++++++- src/index.ts | 7 +- src/lib/react-query/documents.ts | 152 ++++++++++++++++++++++++++++--- 3 files changed, 243 insertions(+), 17 deletions(-) diff --git a/src/hooks/documents.ts b/src/hooks/documents.ts index 700e461..8cd558f 100644 --- a/src/hooks/documents.ts +++ b/src/hooks/documents.ts @@ -1,11 +1,18 @@ import type { MapeoDoc } from '@comapeo/schema' with { 'resolution-mode': 'import' } -import { useSuspenseQuery } from '@tanstack/react-query' +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query' import { + createDocumentMutationOptions, + deleteDocumentMutationOptions, documentByDocumentIdQueryOptions, documentByVersionIdQueryOptions, documentsQueryOptions, - type DocumentType, + updateDocumentMutationOptions, + type MutableDocumentType, } from '../lib/react-query/documents.js' import { useSingleProject } from './projects.js' @@ -38,7 +45,7 @@ type ReadHookResult = { * } * ``` */ -export function useSingleDocByDocId({ +export function useSingleDocByDocId({ projectId, docType, docId, @@ -94,7 +101,7 @@ export function useSingleDocByDocId({ * } * ``` */ -export function useSingleDocByVersionId({ +export function useSingleDocByVersionId({ projectId, docType, versionId, @@ -159,7 +166,7 @@ export function useSingleDocByVersionId({ * } * ``` */ -export function useManyDocs({ +export function useManyDocs({ projectId, docType, includeDeleted, @@ -189,3 +196,87 @@ export function useManyDocs({ isRefetching, } } + +/** + * Create a document for a project. + * + * @param opts.docType Document type to create. + * @param opts.projectId Public ID of project to create document for. + */ +export function useCreateDocument({ + docType, + projectId, +}: { + docType: D + projectId: string +}) { + const queryClient = useQueryClient() + const { data: projectApi } = useSingleProject({ projectId }) + + const { mutate, reset, status } = useMutation( + createDocumentMutationOptions({ + docType, + projectApi, + projectId, + queryClient, + }), + ) + + return { mutate, reset, status } +} + +/** + * Update a document within a project. + * + * @param opts.docType Document type to update. + * @param opts.projectId Public ID of project document belongs to. + */ +export function useUpdateDocument({ + docType, + projectId, +}: { + docType: D + projectId: string +}) { + const queryClient = useQueryClient() + const { data: projectApi } = useSingleProject({ projectId }) + + const { mutate, reset, status } = useMutation( + updateDocumentMutationOptions({ + docType, + projectApi, + projectId, + queryClient, + }), + ) + + return { mutate, reset, status } +} + +/** + * Delete a document within a project. + * + * @param opts.docType Document type to delete. + * @param opts.projectId Public ID of project document belongs to. + */ +export function useDeleteDocument({ + docType, + projectId, +}: { + docType: D + projectId: string +}) { + const queryClient = useQueryClient() + const { data: projectApi } = useSingleProject({ projectId }) + + const { mutate, reset, status } = useMutation( + deleteDocumentMutationOptions({ + docType, + projectApi, + projectId, + queryClient, + }), + ) + + return { mutate, reset, status } +} diff --git a/src/index.ts b/src/index.ts index 80b269d..4f4df93 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,9 +7,12 @@ export { useSetOwnDeviceInfo, } from './hooks/client.js' export { + useCreateDocument, + useDeleteDocument, useManyDocs, useSingleDocByDocId, useSingleDocByVersionId, + useUpdateDocument, } from './hooks/documents.js' export { useAcceptInvite, @@ -44,7 +47,9 @@ export { getDocumentByVersionIdQueryKey, getDocumentsQueryKey, getManyDocumentsQueryKey, - type DocumentType, + type MutableDocument, + type MutableDocumentType, + type MutableValue, } from './lib/react-query/documents.js' export { getInvitesQueryKey, diff --git a/src/lib/react-query/documents.ts b/src/lib/react-query/documents.ts index 73487a0..2317e33 100644 --- a/src/lib/react-query/documents.ts +++ b/src/lib/react-query/documents.ts @@ -1,15 +1,34 @@ import type { MapeoProjectApi } from '@comapeo/ipc' with { 'resolution-mode': 'import' } -import type { MapeoDoc } from '@comapeo/schema' with { 'resolution-mode': 'import' } -import { queryOptions } from '@tanstack/react-query' +import type { + MapeoDoc, + MapeoValue, +} from '@comapeo/schema' with { 'resolution-mode': 'import' } +import { + queryOptions, + type QueryClient, + type UseMutationOptions, +} from '@tanstack/react-query' -import { baseQueryOptions, ROOT_QUERY_KEY } from './shared.js' +import { + baseMutationOptions, + baseQueryOptions, + ROOT_QUERY_KEY, +} from './shared.js' -export type DocumentType = Extract< +export type MutableDocumentType = Extract< MapeoDoc['schemaName'], 'field' | 'observation' | 'preset' | 'track' | 'remoteDetectionAlert' > +export type MutableValue = Extract< + MapeoValue, + { schemaName: D } +> +export type MutableDocument = Extract< + MapeoDoc, + { schemaName: D } +> -export function getDocumentsQueryKey({ +export function getDocumentsQueryKey({ projectId, docType, }: { @@ -19,7 +38,7 @@ export function getDocumentsQueryKey({ return [ROOT_QUERY_KEY, 'projects', projectId, docType] as const } -export function getManyDocumentsQueryKey({ +export function getManyDocumentsQueryKey({ projectId, docType, includeDeleted, @@ -39,7 +58,7 @@ export function getManyDocumentsQueryKey({ ] as const } -export function getDocumentByDocIdQueryKey({ +export function getDocumentByDocIdQueryKey({ projectId, docType, docId, @@ -60,7 +79,7 @@ export function getDocumentByDocIdQueryKey({ ] as const } -export function getDocumentByVersionIdQueryKey({ +export function getDocumentByVersionIdQueryKey({ projectId, docType, versionId, @@ -81,7 +100,7 @@ export function getDocumentByVersionIdQueryKey({ ] as const } -export function documentsQueryOptions({ +export function documentsQueryOptions({ projectApi, projectId, docType, @@ -111,7 +130,9 @@ export function documentsQueryOptions({ }) } -export function documentByDocumentIdQueryOptions({ +export function documentByDocumentIdQueryOptions< + D extends MutableDocumentType, +>({ projectApi, projectId, docType, @@ -142,7 +163,7 @@ export function documentByDocumentIdQueryOptions({ }) } -export function documentByVersionIdQueryOptions({ +export function documentByVersionIdQueryOptions({ projectApi, projectId, docType, @@ -168,3 +189,112 @@ export function documentByVersionIdQueryOptions({ }, }) } + +export function createDocumentMutationOptions({ + docType, + projectApi, + projectId, + queryClient, +}: { + docType: D + projectApi: MapeoProjectApi + projectId: string + queryClient: QueryClient +}) { + return { + ...baseMutationOptions(), + mutationFn: async ({ + value, + }): Promise & { forks: Array }> => { + // @ts-expect-error TS not handling this well + return projectApi[docType].create({ + ...value, + schemaName: docType, + }) + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: getDocumentsQueryKey({ + projectId, + docType, + }), + }) + }, + } satisfies UseMutationOptions< + MutableDocument & { forks: Array }, + Error, + { value: Omit, 'schemaName'> } + > +} + +export function updateDocumentMutationOptions({ + docType, + projectApi, + projectId, + queryClient, +}: { + docType: D + projectApi: MapeoProjectApi + projectId: string + queryClient: QueryClient +}) { + return { + ...baseMutationOptions(), + mutationFn: async ({ + versionId, + value, + }): Promise & { forks: Array }> => { + // @ts-expect-error TS not handling this well + return projectApi[docType].update(versionId, value) + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: getDocumentsQueryKey({ + projectId, + docType, + }), + }) + }, + } satisfies UseMutationOptions< + MutableDocument & { forks: Array }, + Error, + { + versionId: string + value: Omit, 'schemaName'> + } + > +} + +export function deleteDocumentMutationOptions({ + docType, + projectApi, + projectId, + queryClient, +}: { + docType: D + projectApi: MapeoProjectApi + projectId: string + queryClient: QueryClient +}) { + return { + ...baseMutationOptions(), + mutationFn: async ({ + docId, + }): Promise & { forks: Array }> => { + // @ts-expect-error TS not handling this well + return projectApi[docType].delete(docId) + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: getDocumentsQueryKey({ + projectId, + docType, + }), + }) + }, + } satisfies UseMutationOptions< + MutableDocument & { forks: Array }, + Error, + { docId: string } + > +} From 7613c3cd4c3341b8aa3af3d7f3130ceab3adc705 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 9 Jan 2025 15:28:02 -0500 Subject: [PATCH 2/4] use Writeable instead of Mutable in type naming --- src/hooks/documents.ts | 14 +++++----- src/index.ts | 6 ++--- src/lib/react-query/documents.ts | 46 +++++++++++++++++--------------- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/hooks/documents.ts b/src/hooks/documents.ts index 8cd558f..808c131 100644 --- a/src/hooks/documents.ts +++ b/src/hooks/documents.ts @@ -12,7 +12,7 @@ import { documentByVersionIdQueryOptions, documentsQueryOptions, updateDocumentMutationOptions, - type MutableDocumentType, + type WriteableDocumentType, } from '../lib/react-query/documents.js' import { useSingleProject } from './projects.js' @@ -45,7 +45,7 @@ type ReadHookResult = { * } * ``` */ -export function useSingleDocByDocId({ +export function useSingleDocByDocId({ projectId, docType, docId, @@ -101,7 +101,7 @@ export function useSingleDocByDocId({ * } * ``` */ -export function useSingleDocByVersionId({ +export function useSingleDocByVersionId({ projectId, docType, versionId, @@ -166,7 +166,7 @@ export function useSingleDocByVersionId({ * } * ``` */ -export function useManyDocs({ +export function useManyDocs({ projectId, docType, includeDeleted, @@ -203,7 +203,7 @@ export function useManyDocs({ * @param opts.docType Document type to create. * @param opts.projectId Public ID of project to create document for. */ -export function useCreateDocument({ +export function useCreateDocument({ docType, projectId, }: { @@ -231,7 +231,7 @@ export function useCreateDocument({ * @param opts.docType Document type to update. * @param opts.projectId Public ID of project document belongs to. */ -export function useUpdateDocument({ +export function useUpdateDocument({ docType, projectId, }: { @@ -259,7 +259,7 @@ export function useUpdateDocument({ * @param opts.docType Document type to delete. * @param opts.projectId Public ID of project document belongs to. */ -export function useDeleteDocument({ +export function useDeleteDocument({ docType, projectId, }: { diff --git a/src/index.ts b/src/index.ts index 4f4df93..caaf1f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,9 +47,9 @@ export { getDocumentByVersionIdQueryKey, getDocumentsQueryKey, getManyDocumentsQueryKey, - type MutableDocument, - type MutableDocumentType, - type MutableValue, + type WriteableDocument, + type WriteableDocumentType, + type WriteableValue, } from './lib/react-query/documents.js' export { getInvitesQueryKey, diff --git a/src/lib/react-query/documents.ts b/src/lib/react-query/documents.ts index 2317e33..4a904ea 100644 --- a/src/lib/react-query/documents.ts +++ b/src/lib/react-query/documents.ts @@ -15,20 +15,20 @@ import { ROOT_QUERY_KEY, } from './shared.js' -export type MutableDocumentType = Extract< +export type WriteableDocumentType = Extract< MapeoDoc['schemaName'], 'field' | 'observation' | 'preset' | 'track' | 'remoteDetectionAlert' > -export type MutableValue = Extract< +export type WriteableValue = Extract< MapeoValue, { schemaName: D } > -export type MutableDocument = Extract< +export type WriteableDocument = Extract< MapeoDoc, { schemaName: D } > -export function getDocumentsQueryKey({ +export function getDocumentsQueryKey({ projectId, docType, }: { @@ -38,7 +38,7 @@ export function getDocumentsQueryKey({ return [ROOT_QUERY_KEY, 'projects', projectId, docType] as const } -export function getManyDocumentsQueryKey({ +export function getManyDocumentsQueryKey({ projectId, docType, includeDeleted, @@ -58,7 +58,7 @@ export function getManyDocumentsQueryKey({ ] as const } -export function getDocumentByDocIdQueryKey({ +export function getDocumentByDocIdQueryKey({ projectId, docType, docId, @@ -79,7 +79,9 @@ export function getDocumentByDocIdQueryKey({ ] as const } -export function getDocumentByVersionIdQueryKey({ +export function getDocumentByVersionIdQueryKey< + D extends WriteableDocumentType, +>({ projectId, docType, versionId, @@ -100,7 +102,7 @@ export function getDocumentByVersionIdQueryKey({ ] as const } -export function documentsQueryOptions({ +export function documentsQueryOptions({ projectApi, projectId, docType, @@ -131,7 +133,7 @@ export function documentsQueryOptions({ } export function documentByDocumentIdQueryOptions< - D extends MutableDocumentType, + D extends WriteableDocumentType, >({ projectApi, projectId, @@ -163,7 +165,9 @@ export function documentByDocumentIdQueryOptions< }) } -export function documentByVersionIdQueryOptions({ +export function documentByVersionIdQueryOptions< + D extends WriteableDocumentType, +>({ projectApi, projectId, docType, @@ -190,7 +194,7 @@ export function documentByVersionIdQueryOptions({ }) } -export function createDocumentMutationOptions({ +export function createDocumentMutationOptions({ docType, projectApi, projectId, @@ -205,7 +209,7 @@ export function createDocumentMutationOptions({ ...baseMutationOptions(), mutationFn: async ({ value, - }): Promise & { forks: Array }> => { + }): Promise & { forks: Array }> => { // @ts-expect-error TS not handling this well return projectApi[docType].create({ ...value, @@ -221,13 +225,13 @@ export function createDocumentMutationOptions({ }) }, } satisfies UseMutationOptions< - MutableDocument & { forks: Array }, + WriteableDocument & { forks: Array }, Error, - { value: Omit, 'schemaName'> } + { value: Omit, 'schemaName'> } > } -export function updateDocumentMutationOptions({ +export function updateDocumentMutationOptions({ docType, projectApi, projectId, @@ -243,7 +247,7 @@ export function updateDocumentMutationOptions({ mutationFn: async ({ versionId, value, - }): Promise & { forks: Array }> => { + }): Promise & { forks: Array }> => { // @ts-expect-error TS not handling this well return projectApi[docType].update(versionId, value) }, @@ -256,16 +260,16 @@ export function updateDocumentMutationOptions({ }) }, } satisfies UseMutationOptions< - MutableDocument & { forks: Array }, + WriteableDocument & { forks: Array }, Error, { versionId: string - value: Omit, 'schemaName'> + value: Omit, 'schemaName'> } > } -export function deleteDocumentMutationOptions({ +export function deleteDocumentMutationOptions({ docType, projectApi, projectId, @@ -280,7 +284,7 @@ export function deleteDocumentMutationOptions({ ...baseMutationOptions(), mutationFn: async ({ docId, - }): Promise & { forks: Array }> => { + }): Promise & { forks: Array }> => { // @ts-expect-error TS not handling this well return projectApi[docType].delete(docId) }, @@ -293,7 +297,7 @@ export function deleteDocumentMutationOptions({ }) }, } satisfies UseMutationOptions< - MutableDocument & { forks: Array }, + WriteableDocument & { forks: Array }, Error, { docId: string } > From b07e7ec8d681e5c74b3f6052848f19609d58e344 Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 9 Jan 2025 16:21:10 -0500 Subject: [PATCH 3/4] update api docs --- docs/API.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/docs/API.md b/docs/API.md index 3feaee4..b05e7ad 100644 --- a/docs/API.md +++ b/docs/API.md @@ -23,6 +23,9 @@ - [useSingleDocByDocId](#usesingledocbydocid) - [useSingleDocByVersionId](#usesingledocbyversionid) - [useManyDocs](#usemanydocs) +- [useCreateDocument](#usecreatedocument) +- [useUpdateDocument](#useupdatedocument) +- [useDeleteDocument](#usedeletedocument) - [useAcceptInvite](#useacceptinvite) - [useRejectInvite](#userejectinvite) - [useSendInvite](#usesendinvite) @@ -439,7 +442,7 @@ Triggers the closest error boundary if the document cannot be found | Function | Type | | ---------- | ---------- | -| `useSingleDocByDocId` | `({ projectId, docType, docId, lang, }: { projectId: string; docType: D; docId: string; lang?: string or undefined; }) => ReadHookResult({ projectId, docType, docId, lang, }: { projectId: string; docType: D; docId: string; lang?: string or undefined; }) => ReadHookResult({ projectId, docType, versionId, lang, }: { projectId: string; docType: D; versionId: string; lang?: string or undefined; }) => ReadHookResult({ projectId, docType, versionId, lang, }: { projectId: string; docType: D; versionId: string; lang?: string or undefined; }) => ReadHookResult({ projectId, docType, includeDeleted, lang, }: { projectId: string; docType: D; includeDeleted?: boolean or undefined; lang?: string or undefined; }) => ReadHookResult({ projectId, docType, includeDeleted, lang, }: { projectId: string; docType: D; includeDeleted?: boolean or undefined; lang?: string or undefined; }) => ReadHookResult({ docType, projectId, }: { docType: D; projectId: string; }) => { mutate: UseMutateFunction and { forks: string[]; }, Error, { value: Omit, "schemaName">; }, unknown>; reset: () => void; status: "pending" or ... 2 more ... or "idle"; }` | + +Parameters: + +* `opts.docType`: Document type to create. +* `opts.projectId`: Public ID of project to create document for. + + +### useUpdateDocument + +Update a document within a project. + +| Function | Type | +| ---------- | ---------- | +| `useUpdateDocument` | `({ docType, projectId, }: { docType: D; projectId: string; }) => { mutate: UseMutateFunction and { forks: string[]; }, Error, { versionId: string; value: Omit<...>; }, unknown>; reset: () => void; status: "pending" or ... 2 more ... or "idle"; }` | + +Parameters: + +* `opts.docType`: Document type to update. +* `opts.projectId`: Public ID of project document belongs to. + + +### useDeleteDocument + +Delete a document within a project. + +| Function | Type | +| ---------- | ---------- | +| `useDeleteDocument` | `({ docType, projectId, }: { docType: D; projectId: string; }) => { mutate: UseMutateFunction and { forks: string[]; }, Error, { docId: string; }, unknown>; reset: () => void; status: "pending" or ... 2 more ... or "idle"; }` | + +Parameters: + +* `opts.docType`: Document type to delete. +* `opts.projectId`: Public ID of project document belongs to. + + ### useAcceptInvite Accept an invite that has been received. From 0cdcf431726336989791b67f5aaca88ac1e63c4c Mon Sep 17 00:00:00 2001 From: Andrew Chou Date: Thu, 9 Jan 2025 16:23:55 -0500 Subject: [PATCH 4/4] small formatting adjustment --- src/lib/react-query/documents.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib/react-query/documents.ts b/src/lib/react-query/documents.ts index 4a904ea..472b7a0 100644 --- a/src/lib/react-query/documents.ts +++ b/src/lib/react-query/documents.ts @@ -262,10 +262,7 @@ export function updateDocumentMutationOptions({ } satisfies UseMutationOptions< WriteableDocument & { forks: Array }, Error, - { - versionId: string - value: Omit, 'schemaName'> - } + { versionId: string; value: Omit, 'schemaName'> } > }