From 483e37c415534cb50f5a1374cba7de8a0f5dbc59 Mon Sep 17 00:00:00 2001 From: Basile Spaenlehauer Date: Tue, 19 Dec 2023 16:26:32 +0100 Subject: [PATCH] feat: add name to query param for setting (#223) fix: do not stringify app messages (resizing bug) --- src/api/appData.ts | 13 +++++---- src/api/appSetting.ts | 16 +++++++---- src/api/chatBot.ts | 2 +- src/api/routes.ts | 2 ++ src/config/keys.ts | 43 ++++++++++++++++++++++------- src/hooks/appAction.ts | 4 +-- src/hooks/appData.test.ts | 11 +++----- src/hooks/appData.ts | 21 ++++++++++----- src/hooks/appSetting.test.ts | 11 +++----- src/hooks/appSetting.ts | 16 ++++++----- src/hooks/postMessage.ts | 13 ++++----- src/mockServer/mirage/server.ts | 21 +++++++++------ src/mockServer/msw/handlers.ts | 32 ++++++++++++++++++++-- src/mutations/appAction.ts | 7 +++-- src/mutations/appData.test.ts | 12 ++++----- src/mutations/appData.ts | 34 +++++++++++------------ src/mutations/appSetting.test.ts | 19 +++++++------ src/mutations/appSetting.ts | 46 +++++++++++++------------------- src/utils/chatbot.ts | 2 +- src/ws/hooks/app.test.ts | 8 +++--- src/ws/hooks/app.ts | 8 +++--- tsconfig.json | 2 +- 22 files changed, 200 insertions(+), 143 deletions(-) diff --git a/src/api/appData.ts b/src/api/appData.ts index b45656f8..7e3d195a 100644 --- a/src/api/appData.ts +++ b/src/api/appData.ts @@ -1,6 +1,6 @@ -import { AppData, UUID } from '@graasp/sdk'; +import { AppData, UUID, appendQueryParamToUrl } from '@graasp/sdk'; -import { ApiData } from '../types'; +import { ApiData, Data } from '../types'; import configureAxios from './axios'; import { buildDeleteAppDataRoute, @@ -12,10 +12,13 @@ import { const axios = configureAxios(); -export const getAppData = async (args: ApiData) => { - const { token, itemId, apiHost } = args; +export const getAppData = async ( + args: ApiData & { filters?: { [key: string]: string } }, +) => { + const { token, itemId, apiHost, filters } = args; + const url = appendQueryParamToUrl(`${apiHost}/${buildGetAppDataRoute(itemId)}`, filters ?? {}); return axios - .get(`${apiHost}/${buildGetAppDataRoute(itemId)}`, { + .get[]>(url, { headers: { Authorization: `Bearer ${token}`, }, diff --git a/src/api/appSetting.ts b/src/api/appSetting.ts index 06d0ef4b..16c4f57d 100644 --- a/src/api/appSetting.ts +++ b/src/api/appSetting.ts @@ -1,6 +1,6 @@ -import { AppSetting } from '@graasp/sdk'; +import { AppSetting, appendQueryParamToUrl } from '@graasp/sdk'; -import { ApiData } from '../types'; +import { ApiData, Data } from '../types'; import configureAxios from './axios'; import { buildDeleteAppSettingRoute, @@ -12,10 +12,16 @@ import { const axios = configureAxios(); -export const getAppSettings = async (args: ApiData) => { - const { token, itemId, apiHost } = args; +export const getAppSettings = async ( + args: ApiData & { filters?: { name: string } }, +) => { + const { token, itemId, apiHost, filters } = args; + const url = appendQueryParamToUrl( + `${apiHost}/${buildGetAppSettingsRoute(itemId)}`, + filters ?? {}, + ); return axios - .get(`${apiHost}/${buildGetAppSettingsRoute(itemId)}`, { + .get[]>(url, { headers: { Authorization: `Bearer ${token}`, }, diff --git a/src/api/chatBot.ts b/src/api/chatBot.ts index 65f71958..e90d0b6a 100644 --- a/src/api/chatBot.ts +++ b/src/api/chatBot.ts @@ -1,6 +1,6 @@ import { ChatBotMessage } from '@graasp/sdk'; -import { ApiData, ChatBotCompletion } from 'src/types'; +import { ApiData, ChatBotCompletion } from 'types'; import configureAxios from './axios'; import { buildPostChatBotRoute } from './routes'; diff --git a/src/api/routes.ts b/src/api/routes.ts index 951de141..e2d23231 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -1,3 +1,5 @@ +import { appendQueryParamToUrl } from '@graasp/sdk'; + export const APP_DATA_ENDPOINT = 'app-data'; export const ITEMS_ROUTE = 'items'; export const APP_ITEMS_ROUTE = 'app-items'; diff --git a/src/config/keys.ts b/src/config/keys.ts index 30a4516f..e7b78728 100644 --- a/src/config/keys.ts +++ b/src/config/keys.ts @@ -1,14 +1,37 @@ import { UUID } from '@graasp/sdk'; -export const buildAppDataKey = (id?: UUID) => ['app-data', id] as const; -export const buildAppActionsKey = (id?: UUID) => ['app-action', id] as const; -export const buildAppSettingsKey = (id?: UUID) => ['app-setting', id] as const; -export const buildAppContextKey = (id?: UUID) => ['context', id] as const; +const APP_SETTING_KEY = 'app-setting'; +const APP_DATA_KEY = 'app-setting'; +const APP_ACTION_KEY = 'app-action'; + +export const appSettingKeys = { + all: [APP_SETTING_KEY] as const, + allSingles: () => [...appSettingKeys.all, 'single'] as const, + single: (id?: UUID, filters?: { [key: string]: unknown }) => + [...appSettingKeys.allSingles(), id, filters] as const, + allFileContents: () => [...appSettingKeys.all, 'file-content'] as const, + fileContent: (id?: UUID) => [...appSettingKeys.allFileContents(), id] as const, +}; + +export const appDataKeys = { + all: [APP_DATA_KEY] as const, + allSingles: () => [...appDataKeys.all, 'single'] as const, + single: (id?: UUID, filters?: { [key: string]: unknown }) => + [...appDataKeys.allSingles(), id, filters] as const, + allFileContents: () => [...appDataKeys.all, 'file-content'] as const, + fileContent: (id?: UUID) => [...appDataKeys.allFileContents(), id] as const, +}; + +export const appActionKeys = { + all: [APP_ACTION_KEY] as const, + allSingles: () => [...appActionKeys.all, 'single'] as const, + single: (id?: UUID) => [...appActionKeys.allSingles(), id] as const, +}; + export const AUTH_TOKEN_KEY = ['AUTH_TOKEN_KEY']; export const LOCAL_CONTEXT_KEY = ['LOCAL_CONTEXT_KEY']; -export const buildFileContentKey = (id?: UUID) => ['app-data', 'files', 'content', id] as const; -export const buildAppSettingFileContentKey = (id?: UUID) => - ['app-setting', 'files', 'content', id] as const; + +export const buildAppContextKey = (id?: UUID) => ['context', id] as const; export const buildPostMessageKeys = (itemId: UUID) => ({ @@ -22,8 +45,8 @@ export const buildPostMessageKeys = (itemId: UUID) => }) as const; export const QUERY_KEYS = { - buildAppDataKey, buildAppContextKey, - buildAppActionsKey, - buildAppSettingsKey, + appSettingKeys, + appDataKeys, + appActionKeys, }; diff --git a/src/hooks/appAction.ts b/src/hooks/appAction.ts index b8d6c9d0..11d4942f 100644 --- a/src/hooks/appAction.ts +++ b/src/hooks/appAction.ts @@ -3,7 +3,7 @@ import { PermissionLevel } from '@graasp/sdk'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api'; -import { buildAppActionsKey } from '../config/keys'; +import { appActionKeys } from '../config/keys'; import { getApiHost, getData, getDataOrThrow, getPermissionLevel } from '../config/utils'; import { QueryClientConfig } from '../types'; import { configureWsAppActionsHooks } from '../ws/hooks/app'; @@ -32,7 +32,7 @@ export default (queryConfig: QueryClientConfig, websocketClient?: WebsocketClien useAppActionsUpdates(enableWs && permissionLevel === PermissionLevel.Admin ? itemId : null); return useQuery({ - queryKey: buildAppActionsKey(itemId), + queryKey: appActionKeys.single(itemId), queryFn: () => { const { token } = getDataOrThrow(queryClient); diff --git a/src/hooks/appData.test.ts b/src/hooks/appData.test.ts index d4020fac..6c5b564b 100644 --- a/src/hooks/appData.test.ts +++ b/src/hooks/appData.test.ts @@ -11,12 +11,7 @@ import { import { Endpoint, mockHook, setUpTest } from '../../test/utils'; import { buildDownloadAppDataFileRoute, buildGetAppDataRoute } from '../api/routes'; import { MOCK_TOKEN } from '../config/constants'; -import { - AUTH_TOKEN_KEY, - LOCAL_CONTEXT_KEY, - buildAppDataKey, - buildFileContentKey, -} from '../config/keys'; +import { AUTH_TOKEN_KEY, LOCAL_CONTEXT_KEY, appDataKeys } from '../config/keys'; import { MissingApiHostError } from '../config/utils'; const { hooks, wrapper, queryClient } = setUpTest(); @@ -34,7 +29,7 @@ describe('App Data Hooks', () => { }); describe('useAppData', () => { - const key = buildAppDataKey(itemId); + const key = appDataKeys.single(itemId); const route = `/${buildGetAppDataRoute(itemId)}`; const hook = () => hooks.useAppData(); @@ -125,7 +120,7 @@ describe('App Data Hooks', () => { const id = 'some-id'; const route = `/${buildDownloadAppDataFileRoute(id)}`; const hook = () => hooks.useAppDataFile({ fileId: id }); - const key = buildFileContentKey(id); + const key = appDataKeys.fileContent(id); it('Receive file content', async () => { const endpoints = [ diff --git a/src/hooks/appData.ts b/src/hooks/appData.ts index 8c078798..3d498aa1 100644 --- a/src/hooks/appData.ts +++ b/src/hooks/appData.ts @@ -2,9 +2,9 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api'; import { MissingFileIdError } from '../config/errors'; -import { buildAppDataKey, buildFileContentKey } from '../config/keys'; +import { appDataKeys } from '../config/keys'; import { getApiHost, getData, getDataOrThrow } from '../config/utils'; -import { QueryClientConfig } from '../types'; +import { Data, QueryClientConfig } from '../types'; import { configureWsAppDataHooks } from '../ws/hooks/app'; import { WebsocketClient } from '../ws/ws-client'; @@ -22,8 +22,16 @@ export default ( }; const { useAppDataUpdates } = configureWsAppDataHooks(websocketClient); return { - useAppData: (options?: { refetchInterval?: number; getUpdates?: boolean }) => { + useAppData: ( + filters?: { type: string }, + options?: { + refetchInterval?: number; + enabled?: boolean; + getUpdates?: boolean; + }, + ) => { const refetchInterval = options?.refetchInterval ?? false; + const enabled = options?.enabled ?? true; const getUpdates = options?.getUpdates ?? true; const queryClient = useQueryClient(); @@ -35,12 +43,13 @@ export default ( useAppDataUpdates(enableWs ? itemId : null); return useQuery({ - queryKey: buildAppDataKey(itemId), + queryKey: appDataKeys.single(itemId), queryFn: () => { const { token } = getDataOrThrow(queryClient); - return Api.getAppData({ itemId, token, apiHost }); + return Api.getAppData({ itemId, token, apiHost, filters }); }, ...defaultOptions, + enabled, refetchInterval, }); }, @@ -53,7 +62,7 @@ export default ( const apiHost = getApiHost(queryClient); return useQuery({ - queryKey: buildFileContentKey(payload?.fileId), + queryKey: appDataKeys.fileContent(payload?.fileId), queryFn: () => { const { token } = getDataOrThrow(queryClient); diff --git a/src/hooks/appSetting.test.ts b/src/hooks/appSetting.test.ts index a53fa7f1..daea31cf 100644 --- a/src/hooks/appSetting.test.ts +++ b/src/hooks/appSetting.test.ts @@ -11,12 +11,7 @@ import { import { Endpoint, mockHook, setUpTest } from '../../test/utils'; import { buildDownloadAppSettingFileRoute, buildGetAppSettingsRoute } from '../api/routes'; import { MOCK_TOKEN } from '../config/constants'; -import { - AUTH_TOKEN_KEY, - LOCAL_CONTEXT_KEY, - buildAppSettingFileContentKey, - buildAppSettingsKey, -} from '../config/keys'; +import { AUTH_TOKEN_KEY, LOCAL_CONTEXT_KEY, appSettingKeys } from '../config/keys'; import { MissingApiHostError } from '../config/utils'; const { hooks, wrapper, queryClient } = setUpTest(); @@ -34,7 +29,7 @@ describe('App Settings Hooks', () => { }); describe('useAppSettings', () => { - const key = buildAppSettingsKey(itemId); + const key = appSettingKeys.single(itemId); const route = `/${buildGetAppSettingsRoute(itemId)}`; const hook = () => hooks.useAppSettings(); @@ -120,7 +115,7 @@ describe('App Settings Hooks', () => { const id = 'some-id'; const route = `/${buildDownloadAppSettingFileRoute(id)}`; const hook = () => hooks.useAppSettingFile({ appSettingId: id }); - const key = buildAppSettingFileContentKey(id); + const key = appSettingKeys.fileContent(id); it('Receive file content', async () => { const endpoints = [ diff --git a/src/hooks/appSetting.ts b/src/hooks/appSetting.ts index 5feaf76b..d4f85213 100644 --- a/src/hooks/appSetting.ts +++ b/src/hooks/appSetting.ts @@ -2,9 +2,9 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api'; import { MissingFileIdError } from '../config/errors'; -import { buildAppSettingFileContentKey, buildAppSettingsKey } from '../config/keys'; +import { appSettingKeys } from '../config/keys'; import { getApiHost, getData, getDataOrThrow } from '../config/utils'; -import { QueryClientConfig } from '../types'; +import { Data, QueryClientConfig } from '../types'; import { configureWsAppSettingHooks } from '../ws/hooks/app'; import { WebsocketClient } from '../ws/ws-client'; @@ -18,7 +18,10 @@ export default (queryConfig: QueryClientConfig, websocketClient?: WebsocketClien }; const { useAppSettingsUpdates } = configureWsAppSettingHooks(websocketClient); return { - useAppSettings: (options?: { getUpdates: boolean }) => { + useAppSettings: ( + filters?: { name: string }, + options?: { getUpdates: boolean }, + ) => { const getUpdates = options?.getUpdates ?? true; const queryClient = useQueryClient(); const apiHost = getApiHost(queryClient); @@ -29,16 +32,17 @@ export default (queryConfig: QueryClientConfig, websocketClient?: WebsocketClien useAppSettingsUpdates(enableWs ? itemId : null); return useQuery({ - queryKey: buildAppSettingsKey(itemId), + queryKey: appSettingKeys.single(itemId, filters), queryFn: () => { const { token: localToken, itemId: localItemId } = getDataOrThrow(queryClient, { shouldMemberExist: false, }); - return Api.getAppSettings({ + return Api.getAppSettings({ itemId: localItemId, token: localToken, apiHost, + filters, }); }, ...defaultOptions, @@ -56,7 +60,7 @@ export default (queryConfig: QueryClientConfig, websocketClient?: WebsocketClien const { token } = getData(queryClient, { shouldMemberExist: false }); return useQuery({ - queryKey: buildAppSettingFileContentKey(payload?.appSettingId), + queryKey: appSettingKeys.fileContent(payload?.appSettingId), queryFn: (): Promise => { const { token: localToken } = getDataOrThrow(queryClient, { shouldMemberExist: false, diff --git a/src/hooks/postMessage.ts b/src/hooks/postMessage.ts index 1c14bba3..fef1f0e3 100644 --- a/src/hooks/postMessage.ts +++ b/src/hooks/postMessage.ts @@ -79,6 +79,7 @@ class CommunicationChannel { if (this.isMobile) { window.parent.postMessage(JSON.stringify(data)); } else { + console.debug('this.messagePort is', this.messagePort); this.messagePort?.postMessage(JSON.stringify(data)); } } @@ -302,12 +303,12 @@ const configurePostMessageHooks = (queryConfig: QueryClientConfig) => { useEffect(() => { if (!queryConfig.isStandalone) { const sendHeight = (height: number): void => { - communicationChannel?.postMessage( - JSON.stringify({ - type: POST_MESSAGE_KEYS.POST_AUTO_RESIZE, - payload: height, - }), - ); + console.debug('[app-postMessage] Sending height', height); + console.debug('communication channel is', communicationChannel); + communicationChannel?.postMessage({ + type: POST_MESSAGE_KEYS.POST_AUTO_RESIZE, + payload: height, + }); }; if (!communicationChannel) { const error = new MissingMessageChannelPortError(); diff --git a/src/mockServer/mirage/server.ts b/src/mockServer/mirage/server.ts index c25d3095..8f22e12c 100644 --- a/src/mockServer/mirage/server.ts +++ b/src/mockServer/mirage/server.ts @@ -153,10 +153,12 @@ export const mockMirageServer = ({ }, routes() { // app data - this.get( - `/${buildGetAppDataRoute(currentItem.id)}`, - (schema) => schema.all('appDataResource') ?? [], - ); + this.get(`/${buildGetAppDataRoute(currentItem.id)}`, (schema, request) => { + const dataType = new URL(request.url).searchParams.get('type'); + return ( + schema.all('appDataResource').filter((x) => (dataType ? x.type === dataType : true)) ?? [] + ); + }); this.post(`/${buildPostAppDataRoute({ itemId: currentItem.id })}`, (schema, request) => { if (!currentMember) { return new Response(401, {}, { errors: ['user not authenticated'] }); @@ -232,10 +234,13 @@ export const mockMirageServer = ({ }); // app settings - this.get( - `/${buildGetAppSettingsRoute(currentItem.id)}`, - (schema) => schema.all('appSetting') ?? [], - ); + this.get(`/${buildGetAppSettingsRoute(currentItem.id)}`, (schema, request) => { + const settingName = new URL(request.url).searchParams.get('name'); + return ( + schema.all('appSetting').filter((x) => (settingName ? x.name === settingName : true)) ?? + [] + ); + }); this.post(`/${buildPostAppSettingRoute({ itemId: currentItem.id })}`, (schema, request) => { if (!currentMember) { diff --git a/src/mockServer/msw/handlers.ts b/src/mockServer/msw/handlers.ts index 4bcfb7a3..bb688e16 100644 --- a/src/mockServer/msw/handlers.ts +++ b/src/mockServer/msw/handlers.ts @@ -30,6 +30,7 @@ const { buildPatchAppSettingRoute, buildPostAppActionRoute, buildPostAppSettingRoute, + buildPostChatBotRoute, } = API_ROUTES; const getMemberIdFromToken = (bearer: string | null): string => { @@ -84,13 +85,19 @@ export const buildMSWMocks = ( // GET /app-items/:itemId/app-data rest.get(`${apiHost}/${buildGetAppDataRoute(':itemId')}`, async (req, res, ctx) => { const reqItemId = req.params.itemId; + const dataType = new URL(req.url).searchParams.get('type'); const memberId = getMemberIdFromToken(req.headers.get('Authorization')); const permission = await getPermissionForMember(memberId); let value; if (permission === PermissionLevel.Admin) { // return all app data of the item - value = await db.appData.where('item.id').equals(reqItemId).toArray(); + value = await db.appData + .where('item.id') + .equals(reqItemId) + // filter app data and return only app data with the given type if parameter was set otherwise return everything + .and((x) => (dataType ? x.type === dataType : true)) + .toArray(); } else { value = await db.appData .where('item.id') @@ -103,6 +110,8 @@ export const buildMSWMocks = ( // if app data is not "visibility item" only return app data that were created by the member or addressed to him return x.creator?.id === memberId || x.member.id === memberId; }) + // filter the app data by type if specified + .and((x) => (dataType ? x.type === dataType : true)) .toArray(); } @@ -181,7 +190,15 @@ export const buildMSWMocks = ( rest.get(`${apiHost}/${buildGetAppSettingsRoute(':itemId')}`, async (req, res, ctx) => { const reqItemId = req.params.itemId; - const value = await db.appSetting.where('item.id').equals(reqItemId).toArray(); + const url = new URL(req.url); + const settingName = url.searchParams.get('name'); + + const value = await db.appSetting + .where('item.id') + .equals(reqItemId) + // filter settings and return only setting with the given name if parameter was set otherwise return everything + .and((x) => (settingName ? x.name === settingName : true)) + .toArray(); return res(ctx.status(200), ctx.json(value)); }), @@ -330,6 +347,17 @@ export const buildMSWMocks = ( return res(ctx.status(200), ctx.json(value)); }), + // ************************* + // Chatbot + // ************************* + // /app-items/:itemId/chat-bot + rest.post(`${apiHost}/${buildPostChatBotRoute(':itemId')}`, async (_req, res, ctx) => + res( + ctx.status(200), + ctx.json({ completion: 'biiip boop I am a chatbot', model: 'fake-gpt' }), + ), + ), + // plumbing rest.delete('/__mocks/reset', (_req, res, ctx) => { db.resetDB({ ...database, appContext }); diff --git a/src/mutations/appAction.ts b/src/mutations/appAction.ts index 3dec7fe5..22cf8fe3 100644 --- a/src/mutations/appAction.ts +++ b/src/mutations/appAction.ts @@ -3,7 +3,7 @@ import { AppAction } from '@graasp/sdk'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api'; -import { buildAppActionsKey } from '../config/keys'; +import { appActionKeys } from '../config/keys'; import { getApiHost, getData, getDataOrThrow } from '../config/utils'; import { postAppActionRoutine } from '../routines'; import { QueryClientConfig } from '../types'; @@ -22,7 +22,7 @@ export default (queryConfig: QueryClientConfig) => { { onSuccess: (newAppAction: AppAction) => { const { itemId } = getData(queryClient); - const key = buildAppActionsKey(itemId); + const key = appActionKeys.single(itemId); const prevData = queryClient.getQueryData(key); const newData: AppAction = newAppAction; // check that the websocket event has not already been received and therefore the data were added @@ -38,8 +38,7 @@ export default (queryConfig: QueryClientConfig) => { }, onSettled: () => { if (!enableWebsocket) { - const { itemId } = getData(queryClient); - queryClient.invalidateQueries(buildAppActionsKey(itemId)); + queryClient.invalidateQueries(appActionKeys.allSingles()); } }, }, diff --git a/src/mutations/appData.test.ts b/src/mutations/appData.test.ts index 31ea3256..0cf1e2f5 100644 --- a/src/mutations/appData.test.ts +++ b/src/mutations/appData.test.ts @@ -19,7 +19,7 @@ import { buildPostAppDataRoute, } from '../api/routes'; import { MOCK_TOKEN } from '../config/constants'; -import { AUTH_TOKEN_KEY, LOCAL_CONTEXT_KEY, buildAppDataKey } from '../config/keys'; +import { AUTH_TOKEN_KEY, LOCAL_CONTEXT_KEY, appDataKeys } from '../config/keys'; import { patchAppDataRoutine, postAppDataRoutine } from '../routines'; const mockedNotifier = jest.fn(); @@ -35,7 +35,7 @@ describe('Apps Mutations', () => { describe('usePostAppData', () => { const itemId = v4(); - const key = buildAppDataKey(itemId); + const key = appDataKeys.single(itemId); const toAdd = buildAppData(); const initData = FIXTURE_APP_DATA; const route = `/${buildPostAppDataRoute({ itemId })}`; @@ -217,7 +217,7 @@ describe('Apps Mutations', () => { }), ); expect(queryClient.getQueryData(key)).toEqual(initData); - expect(queryClient.getQueryState(key)?.isInvalidated).toBeFalsy(); + expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); }); }); }); @@ -226,7 +226,7 @@ describe('Apps Mutations', () => { const initData = FIXTURE_APP_DATA; const itemId = v4(); const appDataId = initData[0]?.id ?? v4(); - const key = buildAppDataKey(itemId); + const key = appDataKeys.single(itemId); const toPatch = buildAppData({ id: appDataId, data: { new: 'data' } }); const updatedData = [toPatch, ...initData.slice(1)]; const route = `/${buildPatchAppDataRoute({ id: toPatch.id, itemId })}`; @@ -411,14 +411,14 @@ describe('Apps Mutations', () => { }), ); expect(queryClient.getQueryData(key)).toEqual(initData); - expect(queryClient.getQueryState(key)?.isInvalidated).toBeFalsy(); + expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); }); }); }); describe('useDeleteAppData', () => { const itemId = v4(); - const key = buildAppDataKey(itemId); + const key = appDataKeys.single(itemId); const toDelete = FIXTURE_APP_DATA[0]; const route = `/${buildDeleteAppDataRoute({ itemId, id: toDelete.id })}`; const mutation = mutations.useDeleteAppData; diff --git a/src/mutations/appData.ts b/src/mutations/appData.ts index dc896ac2..a71df547 100644 --- a/src/mutations/appData.ts +++ b/src/mutations/appData.ts @@ -3,7 +3,7 @@ import { AppData, Member, UUID } from '@graasp/sdk'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api'; -import { buildAppDataKey } from '../config/keys'; +import { appDataKeys } from '../config/keys'; import { getApiHost, getData, getDataOrThrow } from '../config/utils'; import { deleteAppDataRoutine, @@ -31,7 +31,7 @@ export default (queryConfig: QueryClientConfig) => { { onSuccess: (newData: AppData) => { const { itemId } = getData(queryClient); - const key = buildAppDataKey(itemId); + const key = appDataKeys.single(itemId); const prevData = queryClient.getQueryData(key); if (!prevData) { // we need to wrap the created AppData in an array because the cache key will receive all the data but the post call only return the current posted data @@ -47,8 +47,7 @@ export default (queryConfig: QueryClientConfig) => { }, onSettled: () => { if (!enableWebsocket) { - const { itemId } = getData(queryClient); - queryClient.invalidateQueries(buildAppDataKey(itemId)); + queryClient.invalidateQueries(appDataKeys.allSingles()); } }, }, @@ -67,12 +66,12 @@ export default (queryConfig: QueryClientConfig) => { onMutate: async (payload) => { let context; const { itemId } = getData(queryClient); - const prevData = queryClient.getQueryData(buildAppDataKey(itemId)); + const prevData = queryClient.getQueryData(appDataKeys.single(itemId)); if (itemId && prevData) { const newData = prevData.map((appData) => appData.id === payload.id ? { ...appData, ...payload } : appData, ); - queryClient.setQueryData(buildAppDataKey(itemId), newData); + queryClient.setQueryData(appDataKeys.single(itemId), newData); context = prevData; } return context; @@ -85,16 +84,15 @@ export default (queryConfig: QueryClientConfig) => { if (prevData) { const { itemId } = getData(queryClient); - const data = queryClient.getQueryData(buildAppDataKey(itemId)); + const data = queryClient.getQueryData(appDataKeys.single(itemId)); if (itemId && data) { - queryClient.setQueryData(buildAppDataKey(itemId), prevData); + queryClient.setQueryData(appDataKeys.single(itemId), prevData); } } }, onSettled: () => { if (!enableWebsocket) { - const data = getData(queryClient); - queryClient.invalidateQueries(buildAppDataKey(data?.itemId)); + queryClient.invalidateQueries(appDataKeys.allSingles()); } }, }, @@ -112,10 +110,10 @@ export default (queryConfig: QueryClientConfig) => { { onMutate: async (payload) => { const { itemId } = getDataOrThrow(queryClient); - const prevData = queryClient.getQueryData(buildAppDataKey(itemId)); + const prevData = queryClient.getQueryData(appDataKeys.single(itemId)); if (prevData && itemId) { queryClient.setQueryData( - buildAppDataKey(itemId), + appDataKeys.single(itemId), prevData?.filter(({ id: appDataId }) => appDataId !== payload.id), ); } @@ -128,16 +126,16 @@ export default (queryConfig: QueryClientConfig) => { queryConfig?.notifier?.({ type: deleteAppDataRoutine.FAILURE, payload: { error } }); if (prevData) { const { itemId } = getData(queryClient); - const data = queryClient.getQueryData(buildAppDataKey(itemId)); + const data = queryClient.getQueryData(appDataKeys.single(itemId)); if (itemId && data) { - queryClient.setQueryData(buildAppDataKey(itemId), prevData); + queryClient.setQueryData(appDataKeys.single(itemId), prevData); } } }, onSettled: () => { const { itemId } = getData(queryClient); if (itemId && !enableWebsocket) { - queryClient.invalidateQueries(buildAppDataKey(itemId)); + queryClient.invalidateQueries(appDataKeys.allSingles()); } }, }, @@ -146,8 +144,8 @@ export default (queryConfig: QueryClientConfig) => { // this mutation is used for its callback and invalidate the keys /** - * @param {UUID} id parent item id wher the file is uploaded in - * @param {error} [error] error occured during the file uploading + * @param {UUID} id parent item id where the file is uploaded in + * @param {error} [error] error occurred during the file uploading */ const useUploadAppDataFile = () => { const queryClient = useQueryClient(); @@ -169,7 +167,7 @@ export default (queryConfig: QueryClientConfig) => { onSettled: () => { const { itemId } = getData(queryClient); if (itemId) { - queryClient.invalidateQueries(buildAppDataKey(itemId)); + queryClient.invalidateQueries(appDataKeys.allSingles()); } }, }, diff --git a/src/mutations/appSetting.test.ts b/src/mutations/appSetting.test.ts index a9baafbc..44949177 100644 --- a/src/mutations/appSetting.test.ts +++ b/src/mutations/appSetting.test.ts @@ -19,7 +19,7 @@ import { buildPostAppSettingRoute, } from '../api/routes'; import { MOCK_TOKEN } from '../config/constants'; -import { AUTH_TOKEN_KEY, LOCAL_CONTEXT_KEY, buildAppSettingsKey } from '../config/keys'; +import { AUTH_TOKEN_KEY, LOCAL_CONTEXT_KEY, appSettingKeys } from '../config/keys'; import { patchAppSettingRoutine, postAppSettingRoutine } from '../routines'; const mockedNotifier = jest.fn(); @@ -35,7 +35,7 @@ describe('App Settings Mutations', () => { describe('usePostAppSetting', () => { const itemId = v4(); - const key = buildAppSettingsKey(itemId); + const key = appSettingKeys.single(itemId); const toAdd = buildAppSetting(); const initData = FIXTURE_APP_SETTINGS; const route = `/${buildPostAppSettingRoute({ itemId })}`; @@ -73,7 +73,6 @@ describe('App Settings Mutations', () => { }); expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); - expect(queryClient.getQueryData(key)).toEqual([...initData, toAdd]); }); }); @@ -112,8 +111,8 @@ describe('App Settings Mutations', () => { type: postAppSettingRoutine.FAILURE, }), ); - expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); expect(queryClient.getQueryData(key)).toEqual(initData); + expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); }); it('Throw if itemId is undefined', async () => { @@ -217,7 +216,7 @@ describe('App Settings Mutations', () => { }), ); expect(queryClient.getQueryData(key)).toEqual(initData); - expect(queryClient.getQueryState(key)?.isInvalidated).toBeFalsy(); + expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); }); }); }); @@ -226,7 +225,7 @@ describe('App Settings Mutations', () => { const initData = FIXTURE_APP_SETTINGS; const itemId = v4(); const appDataId = initData[0]?.id ?? v4(); - const key = buildAppSettingsKey(itemId); + const key = appSettingKeys.single(itemId); const toPatch = buildAppSetting({ id: appDataId, data: { new: 'data' } }); const updatedData = [toPatch, ...initData.slice(1)]; const route = `/${buildPatchAppSettingRoute({ id: toPatch.id, itemId })}`; @@ -339,7 +338,7 @@ describe('App Settings Mutations', () => { }), ); expect(queryClient.getQueryData(key)).toEqual(initData); - // since the itemid is not defined, we do not check data for its key + // since the itemId is not defined, we do not check data for its key }); it('Throw if memberId is undefined', async () => { @@ -409,14 +408,14 @@ describe('App Settings Mutations', () => { }), ); expect(queryClient.getQueryData(key)).toEqual(initData); - expect(queryClient.getQueryState(key)?.isInvalidated).toBeFalsy(); + expect(queryClient.getQueryState(key)?.isInvalidated).toBeTruthy(); }); }); }); describe('useDeleteAppSetting', () => { const itemId = v4(); - const key = buildAppSettingsKey(itemId); + const key = appSettingKeys.single(itemId); const toDelete = FIXTURE_APP_SETTINGS[0]; const route = `/${buildDeleteAppSettingRoute({ itemId, id: toDelete.id })}`; const mutation = mutations.useDeleteAppSetting; @@ -519,7 +518,7 @@ describe('App Settings Mutations', () => { }); expect(queryClient.getQueryData(key)).toEqual(initData); - // since the itemid is not defined, we do not check data for its key + // since the itemId is not defined, we do not check data for its key }); it('Throw if memberId is undefined', async () => { diff --git a/src/mutations/appSetting.ts b/src/mutations/appSetting.ts index aa5fb15d..f081e55d 100644 --- a/src/mutations/appSetting.ts +++ b/src/mutations/appSetting.ts @@ -3,7 +3,7 @@ import { AppSetting } from '@graasp/sdk'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import * as Api from '../api'; -import { buildAppSettingsKey } from '../config/keys'; +import { appSettingKeys } from '../config/keys'; import { getApiHost, getData, getDataOrThrow } from '../config/utils'; import { deleteAppSettingRoutine, @@ -26,25 +26,16 @@ export default (queryConfig: QueryClientConfig) => { }, { onSuccess: (newAppSetting: AppSetting) => { - const { itemId } = getData(queryClient); - const key = buildAppSettingsKey(itemId); - const prevData = queryClient.getQueryData(key); - const newData: AppSetting = newAppSetting; - if (!prevData) { - // we need to wrap the created AppSetting in an array because the cache key will receive all the settings but the post call only return the current posted data - queryClient.setQueryData(key, [newData]); - } else if (!prevData.some((a) => a.id === newData.id)) { - queryClient.setQueryData(key, [...(prevData ?? []), newData]); - } - queryConfig?.notifier?.({ type: postAppSettingRoutine.SUCCESS, payload: newData }); + queryConfig?.notifier?.({ type: postAppSettingRoutine.SUCCESS, payload: newAppSetting }); }, onError: (error: Error) => { queryConfig?.notifier?.({ type: postAppSettingRoutine.FAILURE, payload: { error } }); }, onSettled: () => { + // only invalidate when websockets are disabled (ws update the cache when they are enabled) if (!enableWebsocket) { - const { itemId } = getData(queryClient); - queryClient.invalidateQueries(buildAppSettingsKey(itemId)); + // invalidate all appSettings queries that depend on a single id + queryClient.invalidateQueries(appSettingKeys.allSingles()); } }, }, @@ -63,12 +54,12 @@ export default (queryConfig: QueryClientConfig) => { onMutate: async (payload) => { let context; const { itemId } = getData(queryClient); - const prevData = queryClient.getQueryData(buildAppSettingsKey(itemId)); + const prevData = queryClient.getQueryData(appSettingKeys.single(itemId)); if (itemId && prevData) { const newData = prevData.map((appData) => appData.id === payload.id ? { ...appData, ...payload } : appData, ); - queryClient.setQueryData(buildAppSettingsKey(itemId), newData); + queryClient.setQueryData(appSettingKeys.single(itemId), newData); context = prevData; } return context; @@ -81,16 +72,15 @@ export default (queryConfig: QueryClientConfig) => { if (prevData) { const { itemId } = getData(queryClient); - const data = queryClient.getQueryData(buildAppSettingsKey(itemId)); + const data = queryClient.getQueryData(appSettingKeys.single(itemId)); if (itemId && data) { - queryClient.setQueryData(buildAppSettingsKey(itemId), prevData); + queryClient.setQueryData(appSettingKeys.single(itemId), prevData); } } }, onSettled: () => { if (!enableWebsocket) { - const data = getData(queryClient); - queryClient.invalidateQueries(buildAppSettingsKey(data?.itemId)); + queryClient.invalidateQueries(appSettingKeys.allSingles()); } }, }, @@ -108,10 +98,10 @@ export default (queryConfig: QueryClientConfig) => { { onMutate: async (payload) => { const { itemId } = getDataOrThrow(queryClient); - const prevData = queryClient.getQueryData(buildAppSettingsKey(itemId)); + const prevData = queryClient.getQueryData(appSettingKeys.single(itemId)); if (prevData && itemId) { queryClient.setQueryData( - buildAppSettingsKey(itemId), + appSettingKeys.single(itemId), prevData?.filter(({ id: appDataId }) => appDataId !== payload.id), ); } @@ -125,9 +115,9 @@ export default (queryConfig: QueryClientConfig) => { if (prevData) { const { itemId } = getData(queryClient); - const data = queryClient.getQueryData(buildAppSettingsKey(itemId)); + const data = queryClient.getQueryData(appSettingKeys.single(itemId)); if (itemId && data) { - queryClient.setQueryData(buildAppSettingsKey(itemId), prevData); + queryClient.setQueryData(appSettingKeys.single(itemId), prevData); } } }, @@ -135,7 +125,7 @@ export default (queryConfig: QueryClientConfig) => { if (!enableWebsocket) { const { itemId } = getData(queryClient); if (itemId) { - queryClient.invalidateQueries(buildAppSettingsKey(itemId)); + queryClient.invalidateQueries(appSettingKeys.allSingles()); } } }, @@ -145,8 +135,8 @@ export default (queryConfig: QueryClientConfig) => { // this mutation is used for its callback and invalidate the keys /** - * @param {UUID} id parent item id wher the file is uploaded in - * @param {error} [error] error occured during the file uploading + * @param {UUID} id parent item id where the file is uploaded in + * @param {error} [error] error occurred during the file uploading */ const useUploadAppSettingFile = () => { const queryClient = useQueryClient(); @@ -168,7 +158,7 @@ export default (queryConfig: QueryClientConfig) => { onSettled: () => { const { itemId } = getData(queryClient); if (itemId) { - queryClient.invalidateQueries(buildAppSettingsKey(itemId)); + queryClient.invalidateQueries(appSettingKeys.allSingles()); } }, }, diff --git a/src/utils/chatbot.ts b/src/utils/chatbot.ts index 7bc9400c..fb6cb44b 100644 --- a/src/utils/chatbot.ts +++ b/src/utils/chatbot.ts @@ -1,6 +1,6 @@ import { ChatBotMessage, ChatbotRole } from '@graasp/sdk'; -import { ChatbotThreadMessage } from 'src/types'; +import { ChatbotThreadMessage } from 'types'; export const buildPrompt = ( initialPrompt: string | undefined, diff --git a/src/ws/hooks/app.test.ts b/src/ws/hooks/app.test.ts index 5aed6e80..d8ecb1a2 100644 --- a/src/ws/hooks/app.test.ts +++ b/src/ws/hooks/app.test.ts @@ -11,7 +11,7 @@ import { } from '../../../test/constants'; import { getHandlerByChannel, mockWsHook, setUpWsTest } from '../../../test/wsUtils'; import { APP_ACTIONS_TOPIC, APP_DATA_TOPIC, APP_SETTINGS_TOPIC } from '../../config/constants'; -import { buildAppActionsKey, buildAppDataKey, buildAppSettingsKey } from '../../config/keys'; +import { appActionKeys, appDataKeys, appSettingKeys } from '../../config/keys'; import { AppActionEvent, AppDataEvent, @@ -39,7 +39,7 @@ describe('Websockets App Hooks', () => { describe('useAppDataUpdates', () => { const appDataArray = FIXTURE_APP_DATA; const itemId = FIXTURE_CONTEXT.id; - const appDataKey = buildAppDataKey(itemId); + const appDataKey = appDataKeys.single(itemId); const channel = { name: itemId, topic: APP_DATA_TOPIC }; const hook = () => hooks.useAppDataUpdates(itemId); @@ -142,7 +142,7 @@ describe('Websockets App Hooks', () => { describe('useAppActionsUpdates', () => { const appActionsArray = FIXTURE_APP_ACTIONS; const itemId = FIXTURE_CONTEXT.id; - const appActionsKey = buildAppActionsKey(itemId); + const appActionsKey = appActionKeys.single(itemId); const channel = { name: itemId, topic: APP_ACTIONS_TOPIC }; const hook = () => hooks.useAppActionsUpdates(itemId); @@ -179,7 +179,7 @@ describe('Websockets App Hooks', () => { describe('useAppSettingsUpdates', () => { const appSettingsArray = FIXTURE_APP_SETTINGS; const itemId = FIXTURE_CONTEXT.id; - const appSettingsKey = buildAppSettingsKey(itemId); + const appSettingsKey = appSettingKeys.single(itemId); const channel = { name: itemId, topic: APP_SETTINGS_TOPIC }; const hook = () => hooks.useAppSettingsUpdates(itemId); diff --git a/src/ws/hooks/app.ts b/src/ws/hooks/app.ts index 462ce7aa..225e273f 100644 --- a/src/ws/hooks/app.ts +++ b/src/ws/hooks/app.ts @@ -9,7 +9,7 @@ import { AppAction, AppData, AppSetting, UUID } from '@graasp/sdk'; import { useQueryClient } from '@tanstack/react-query'; import { APP_ACTIONS_TOPIC, APP_DATA_TOPIC, APP_SETTINGS_TOPIC } from '../../config/constants'; -import { buildAppActionsKey, buildAppDataKey, buildAppSettingsKey } from '../../config/keys'; +import { appActionKeys, appDataKeys, appSettingKeys } from '../../config/keys'; import { AppActionEvent, AppDataEvent, @@ -40,7 +40,7 @@ export const configureWsAppDataHooks = (websocketClient?: WebsocketClient) => ({ } const channel: Channel = { name: itemId, topic: APP_DATA_TOPIC }; - const appDataKey = buildAppDataKey(itemId); + const appDataKey = appDataKeys.single(itemId); const handler = (event: AppDataEvent): void => { if (event.kind === AppEventKinds.AppData) { @@ -112,7 +112,7 @@ export const configureWsAppActionsHooks = (websocketClient?: WebsocketClient) => } const channel: Channel = { name: itemId, topic: APP_ACTIONS_TOPIC }; - const appActionKey = buildAppActionsKey(itemId); + const appActionKey = appActionKeys.single(itemId); const handler = (event: AppActionEvent): void => { if (event.kind === AppEventKinds.AppActions) { @@ -165,7 +165,7 @@ export const configureWsAppSettingHooks = (websocketClient?: WebsocketClient) => } const channel: Channel = { name: itemId, topic: APP_SETTINGS_TOPIC }; - const appSettingsKey = buildAppSettingsKey(itemId); + const appSettingsKey = appSettingKeys.single(itemId); const handler = (event: AppSettingEvent): void => { if (event.kind === AppEventKinds.AppSettings) { diff --git a/tsconfig.json b/tsconfig.json index 039f064b..06580654 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,7 @@ "skipLibCheck": true, "esModuleInterop": true, "declarationMap": true, - "baseUrl": ".", + "baseUrl": "src", "types": ["vite/client", "jest", "node"] }, "include": ["src"],