UNPKG

@datalayer/core

Version:

[![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)

1,336 lines 228 kB
/* * Copyright (c) 2023-2025 Datalayer, Inc. * Distributed under the terms of the Modified BSD License. */ /** * TanStack Query-based cache hook for Datalayer API * * This is a modernized replacement for useCache.tsx that leverages TanStack Query * for automatic cache management, background refetching, and optimistic updates. * * Key improvements over useCache: * - Automatic cache management (no manual Map objects) * - Built-in loading/error states * - Automatic refetching and deduplication * - Optimistic updates support * - Better TypeScript inference * - React Query DevTools integration * * @example * ```tsx * const { useUser, useUpdateUser } = useCache2(); * * function UserProfile({ userId }: { userId: string }) { * const { data: user, isPending, isError, error } = useUser(userId); * const updateUser = useUpdateUser(); * * if (isPending) return <Spinner />; * if (isError) return <Error message={error.message} />; * * return ( * <div> * <h1>{user.displayName}</h1> * <button onClick={() => updateUser.mutate({ email: 'new@email.com' })}> * Update Email * </button> * </div> * ); * } * ``` */ import { useQuery, useMutation, useQueryClient, } from '@tanstack/react-query'; import { BOOTSTRAP_USER_ONBOARDING, asContact, asDatasource, asInvite, asOrganization, asPage, asSecret, asSpace, asTeam, asToken, asUser, } from '../models'; import { useCoreStore, useIAMStore } from '../state'; import { asDisplayName, namesAsInitials, asArray } from '../utils'; import { newUserMock } from './../mocks'; import { useDatalayer } from './useDatalayer'; import { useAuthorization } from './useAuthorization'; import { useUploadForm } from './useUpload'; import { OUTPUTSHOT_PLACEHOLDER_DEFAULT_SVG } from './assets'; // Kept for potential future use // Default query options for all queries const DEFAULT_QUERY_OPTIONS = { staleTime: 5 * 60 * 1000, // 5 minutes gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime) retry: 1, refetchOnMount: false, // Don't refetch on mount if data is still fresh refetchOnWindowFocus: true, refetchOnReconnect: true, // Ensure queries prioritize cache over network when data is fresh networkMode: 'online', }; // ============================================================================ // Query Key Factories // ============================================================================ /** * Centralized query key factories for all entities * Following TanStack Query best practices for key structure * @see https://tanstack.com/query/latest/docs/framework/react/guides/query-keys */ export const queryKeys = { // Authentication & Profile auth: { me: () => ['auth', 'me'], whoami: () => ['auth', 'whoami'], }, // Users users: { all: () => ['users'], lists: () => [...queryKeys.users.all(), 'list'], list: (filters) => [...queryKeys.users.lists(), { filters }], details: () => [...queryKeys.users.all(), 'detail'], detail: (id) => [...queryKeys.users.details(), id], byHandle: (handle) => [...queryKeys.users.all(), 'handle', handle], search: (pattern) => [...queryKeys.users.all(), 'search', pattern], settings: (userId) => [...queryKeys.users.detail(userId), 'settings'], onboarding: (userId) => [...queryKeys.users.detail(userId), 'onboarding'], surveys: (userId) => [...queryKeys.users.detail(userId), 'surveys'], credits: (userId) => [...queryKeys.users.detail(userId), 'credits'], }, // Organizations organizations: { all: () => ['organizations'], lists: () => [...queryKeys.organizations.all(), 'list'], details: () => [...queryKeys.organizations.all(), 'detail'], detail: (id) => [...queryKeys.organizations.details(), id], byHandle: (handle) => [...queryKeys.organizations.all(), 'handle', handle], userOrgs: () => [...queryKeys.organizations.all(), 'user'], members: (orgId) => [...queryKeys.organizations.detail(orgId), 'members'], }, // Teams teams: { all: () => ['teams'], details: () => [...queryKeys.teams.all(), 'detail'], detail: (id) => [...queryKeys.teams.details(), id], byHandle: (handle) => [...queryKeys.teams.all(), 'handle', handle], byOrganization: (orgId) => [...queryKeys.teams.all(), 'organization', orgId], members: (teamId) => [...queryKeys.teams.detail(teamId), 'members'], }, // Spaces spaces: { all: () => ['spaces'], details: () => [...queryKeys.spaces.all(), 'detail'], detail: (id) => [...queryKeys.spaces.details(), id], byHandle: (handle) => [...queryKeys.spaces.all(), 'handle', handle], byOrganization: (orgId) => [...queryKeys.spaces.all(), 'organization', orgId], orgSpaceByHandle: (orgId, handle) => [ ...queryKeys.spaces.all(), 'organization', orgId, 'handle', handle, ], byOrganizationAndHandle: (orgHandle, spaceHandle) => [ ...queryKeys.spaces.all(), 'organization', orgHandle, 'space', spaceHandle, ], userSpaces: () => [...queryKeys.spaces.all(), 'user'], items: (spaceId) => [...queryKeys.spaces.detail(spaceId), 'items'], members: (spaceId) => [...queryKeys.spaces.detail(spaceId), 'members'], }, // Notebooks notebooks: { all: () => ['notebooks'], details: () => [...queryKeys.notebooks.all(), 'detail'], detail: (id) => [...queryKeys.notebooks.details(), id], bySpace: (spaceId) => [...queryKeys.notebooks.all(), 'space', spaceId], model: (notebookId) => [...queryKeys.notebooks.detail(notebookId), 'model'], }, // Documents documents: { all: () => ['documents'], details: () => [...queryKeys.documents.all(), 'detail'], detail: (id) => [...queryKeys.documents.details(), id], bySpace: (spaceId) => [...queryKeys.documents.all(), 'space', spaceId], model: (documentId) => [...queryKeys.documents.detail(documentId), 'model'], }, // Cells cells: { all: () => ['cells'], details: () => [...queryKeys.cells.all(), 'detail'], detail: (id) => [...queryKeys.cells.details(), id], bySpace: (spaceId) => [...queryKeys.cells.all(), 'space', spaceId], }, // Datasets datasets: { all: () => ['datasets'], details: () => [...queryKeys.datasets.all(), 'detail'], detail: (id) => [...queryKeys.datasets.details(), id], bySpace: (spaceId) => [...queryKeys.datasets.all(), 'space', spaceId], }, // Lessons lessons: { all: () => ['lessons'], details: () => [...queryKeys.lessons.all(), 'detail'], detail: (id) => [...queryKeys.lessons.details(), id], bySpace: (spaceId) => [...queryKeys.lessons.all(), 'space', spaceId], }, // Exercises exercises: { all: () => ['exercises'], details: () => [...queryKeys.exercises.all(), 'detail'], detail: (id) => [...queryKeys.exercises.details(), id], bySpace: (spaceId) => [...queryKeys.exercises.all(), 'space', spaceId], }, // Assignments assignments: { all: () => ['assignments'], details: () => [...queryKeys.assignments.all(), 'detail'], detail: (id) => [...queryKeys.assignments.details(), id], bySpace: (spaceId) => [...queryKeys.assignments.all(), 'space', spaceId], forStudent: (assignmentId, courseId, studentId) => [ ...queryKeys.assignments.detail(assignmentId), 'course', courseId, 'student', studentId, ], studentVersion: (assignmentId) => [ ...queryKeys.assignments.detail(assignmentId), 'studentVersion', ], }, // Environments environments: { all: () => ['environments'], details: () => [...queryKeys.environments.all(), 'detail'], detail: (id) => [...queryKeys.environments.details(), id], bySpace: (spaceId) => [...queryKeys.environments.all(), 'space', spaceId], }, // Pages pages: { all: () => ['pages'], details: () => [...queryKeys.pages.all(), 'detail'], detail: (id) => [...queryKeys.pages.details(), id], }, // Datasources datasources: { all: () => ['datasources'], details: () => [...queryKeys.datasources.all(), 'detail'], detail: (id) => [...queryKeys.datasources.details(), id], }, // Secrets secrets: { all: () => ['secrets'], details: () => [...queryKeys.secrets.all(), 'detail'], detail: (id) => [...queryKeys.secrets.details(), id], }, // Tokens tokens: { all: () => ['tokens'], details: () => [...queryKeys.tokens.all(), 'detail'], detail: (id) => [...queryKeys.tokens.details(), id], }, // Contacts contacts: { all: () => ['contacts'], lists: () => [...queryKeys.contacts.all(), 'list'], details: () => [...queryKeys.contacts.all(), 'detail'], detail: (id) => [...queryKeys.contacts.details(), id], byHandle: (handle) => [...queryKeys.contacts.all(), 'handle', handle], search: (query) => [...queryKeys.contacts.all(), 'search', query], }, // Invites invites: { all: () => ['invites'], byToken: (token) => [...queryKeys.invites.all(), 'token', token], byAccount: (accountId) => [...queryKeys.invites.all(), 'account', accountId], }, // Courses courses: { all: () => ['courses'], details: () => [...queryKeys.courses.all(), 'detail'], detail: (id) => [...queryKeys.courses.details(), id], public: () => [...queryKeys.courses.all(), 'public'], instructor: (instructorId) => [...queryKeys.courses.all(), 'instructor', instructorId], enrollments: () => [...queryKeys.courses.all(), 'enrollments', 'me'], students: (courseId) => [...queryKeys.courses.detail(courseId), 'students'], student: (courseId, studentId) => [...queryKeys.courses.detail(courseId), 'student', studentId], }, // Schools schools: { all: () => ['schools'], }, // Inbounds inbounds: { all: () => ['inbounds'], detail: (id) => [...queryKeys.inbounds.all(), id], byHandle: (handle) => [...queryKeys.inbounds.all(), 'handle', handle], }, // Outbounds outbounds: { all: () => ['outbounds'], detail: (id) => [...queryKeys.outbounds.all(), id], }, // Items (generic) items: { all: () => ['items'], public: () => [...queryKeys.items.all(), 'public'], bySpace: (spaceId) => [...queryKeys.items.all(), 'space', spaceId], search: (opts) => [...queryKeys.items.all(), 'search', opts], }, // Layout layout: { byAccount: (accountHandle, spaceHandle) => spaceHandle ? ['layout', accountHandle, spaceHandle] : ['layout', accountHandle], }, // Usages usages: { user: () => ['usages', 'user'], userById: (userId) => ['usages', 'user', userId], platform: () => ['usages', 'platform'], }, // Prices prices: { stripe: () => ['prices', 'stripe'], }, // Growth growth: { kpi: () => ['growth', 'kpi'], }, // OAuth2 oauth2: { authorizationUrl: (queryArgs) => ['oauth2', 'authz', 'url', queryArgs], authorizationLinkUrl: (queryArgs) => ['oauth2', 'authz', 'url', 'link', queryArgs], }, }; // ============================================================================ // Main Hook // ============================================================================ /** * TanStack Query-based cache hook for Datalayer API * * This hook provides React Query-based data fetching and mutations for all * Datalayer entities. Unlike the original useCache hook, this returns hook * factories that components can use directly. * * @param options - Configuration options * @param options.loginRoute - Route to redirect to on authentication failure (default: '/login') * * @returns Object containing all query and mutation hook factories */ export const useCache = ({ loginRoute = '/login' } = {}) => { const coreStore = useCoreStore(); const { configuration } = coreStore; const { user } = useIAMStore(); const queryClient = useQueryClient(); const { requestDatalayer } = useDatalayer({ loginRoute }); const { checkIsOrganizationMember } = useAuthorization(); // Hook for notebook upload/creation const { isLoading: notebookUploadLoading, uploadAndSubmit: uploadNotebook, progress: notebookUploadProgress, reset: resetNotebookUpload, } = useUploadForm(`${coreStore.configuration.spacerRunUrl}/api/spacer/v1/notebooks`); // Hook for document upload/creation const { isLoading: documentUploadLoading, uploadAndSubmit: uploadDocument, progress: documentUploadProgress, reset: resetDocumentUpload, } = useUploadForm(`${coreStore.configuration.spacerRunUrl}/api/spacer/v1/lexicals`); // ============================================================================ // Transformation Functions (kept from original useCache) // Note: These functions use 'any' because they handle dynamic API responses // ============================================================================ /* eslint-disable @typescript-eslint/no-explicit-any */ const toUser = (u) => { if (u) { return asUser(u); } }; const toOrganization = (org) => { return asOrganization(org); }; const toSpace = (spc) => { return asSpace(spc); }; const toPage = (s) => { if (s) { return asPage(s); } }; const toDatasource = (s) => { if (s) { return asDatasource(s); } }; const toSecret = (s) => { if (s) { return asSecret(s); } }; const toToken = (s) => { if (s) { return asToken(s); } }; const toContact = (c) => { if (c) { return asContact(c); } }; const toTeam = (org, organizationId) => { return asTeam(org, organizationId); }; // Kept for potential future use // Kept for potential future use const toDataset = (raw_dataset) => { const owner = newUserMock(); return { id: raw_dataset.uid, type: 'dataset', name: raw_dataset.name_t, description: raw_dataset.description_t, fileName: raw_dataset.file_name_s, datasetExtension: raw_dataset.dataset_extension_s, contentLength: raw_dataset.content_length_i, contentType: raw_dataset.content_type_s, mimeType: raw_dataset.mimetype_s, path: raw_dataset.s3_path_s, cdnUrl: raw_dataset.cdn_url_s, creationDate: new Date(raw_dataset.creation_ts_dt), public: raw_dataset.public_b ?? false, lastPublicationDate: raw_dataset.creation_ts_dt ? new Date(raw_dataset.creation_ts_dt) : undefined, owner, space: { handle: raw_dataset.handle_s, }, organization: { handle: raw_dataset.handle_s, }, }; }; const toCell = (cl) => { const owner = newUserMock(); return { id: cl.uid, type: 'cell', name: cl.name_t, description: cl.description_t, source: cl.source_t, creationDate: new Date(cl.creation_ts_dt), public: cl.public_b ?? false, lastPublicationDate: cl.last_publication_ts_dt ? new Date(cl.last_publication_ts_dt) : undefined, outputshotUrl: cl.outputshot_url_s || '', outputshotData: OUTPUTSHOT_PLACEHOLDER_DEFAULT_SVG, owner, space: { handle: cl.handle_s, }, organization: { handle: cl.handle_s, }, }; }; const toNotebook = (raw_notebook) => { const owner = newUserMock(); return { id: raw_notebook.uid, type: 'notebook', name: raw_notebook.name_t, description: raw_notebook.description_t, nbformat: raw_notebook.model_s ? JSON.parse(raw_notebook.model_s) : undefined, public: raw_notebook.public_b ?? false, creationDate: new Date(raw_notebook.creation_ts_dt), lastUpdateDate: raw_notebook.last_update_ts_dt ? new Date(raw_notebook.last_update_ts_dt) : undefined, lastPublicationDate: raw_notebook.creation_ts_dt ? new Date(raw_notebook.creation_ts_dt) : undefined, datasets: [], owner, space: { handle: raw_notebook.handle_s, }, organization: { handle: raw_notebook.handle_s, }, }; }; const toDocument = (doc) => { const owner = newUserMock(); return { id: doc.uid, type: 'document', name: doc.name_t, description: doc.description_t, model: doc.model_s ? JSON.parse(doc.model_s) : undefined, public: doc.public_b ?? false, creationDate: new Date(doc.creation_ts_dt), lastUpdateDate: doc.last_update_ts_dt ? new Date(doc.last_update_ts_dt) : undefined, lastPublicationDate: doc.creation_ts_dt ? new Date(doc.creation_ts_dt) : undefined, owner, space: { handle: doc.handle_s, }, organization: { handle: doc.handle_s, }, }; }; const toLesson = (raw_lesson) => { const owner = newUserMock(); return { id: raw_lesson.uid, type: 'lesson', name: raw_lesson.name_t, description: raw_lesson.description_t, nbformat: raw_lesson.model_s ? JSON.parse(raw_lesson.model_s) : undefined, public: raw_lesson.public_b ?? false, creationDate: new Date(raw_lesson.creation_ts_dt), lastUpdateDate: raw_lesson.last_update_ts_dt ? new Date(raw_lesson.last_update_ts_dt) : undefined, lastPublicationDate: raw_lesson.creation_ts_dt ? new Date(raw_lesson.creation_ts_dt) : undefined, owner, space: { handle: raw_lesson.handle_s, }, organization: { handle: raw_lesson.handle_s, }, datasets: [], }; }; const toExercise = (ex) => { const owner = newUserMock(); return { id: ex.uid, type: 'exercise', name: ex.name_t, description: ex.description_t, help: ex.help_t, codePre: ex.code_pre_t, codeQuestion: ex.code_question_t, codeSolution: ex.code_solution_t, codeTest: ex.code_test_t, public: ex.public_b ?? false, creationDate: new Date(ex.creation_ts_dt), lastUpdateDate: ex.last_update_ts_dt ? new Date(ex.last_update_ts_dt) : undefined, lastPublicationDate: ex.creation_ts_dt ? new Date(ex.creation_ts_dt) : undefined, owner, space: { handle: ex.handle_s, }, organization: { handle: ex.handle_s, }, datasets: [], }; }; const toAssignment = (raw_assignment) => { const owner = newUserMock(); let studentItem = undefined; if (raw_assignment.student_items) { raw_assignment.student_items.forEach((student_item) => { studentItem = { id: student_item.uid, type: 'student_item', itemId: student_item.item_uid, itemType: student_item.item_type_s, nbgrades: student_item.nbgrades, nbgradesTotalPoints: student_item.nbgrades_total_points_f, nbgradesTotalScore: student_item.nbgrades_total_score_f, }; }); } return { id: raw_assignment.uid, type: 'assignment', name: raw_assignment.name_t, description: raw_assignment.description_t, nbformat: raw_assignment.model_s ? JSON.parse(raw_assignment.model_s) : undefined, public: raw_assignment.public_b ?? false, creationDate: new Date(raw_assignment.creation_ts_dt), lastUpdateDate: raw_assignment.last_update_ts_dt ? new Date(raw_assignment.last_update_ts_dt) : undefined, lastPublicationDate: raw_assignment.creation_ts_dt ? new Date(raw_assignment.creation_ts_dt) : undefined, studentItem, datasets: [], owner, space: { handle: raw_assignment.handle_s, }, organization: { handle: raw_assignment.handle_s, }, }; }; const toEnvironment = (env) => { const owner = newUserMock(); return { id: env.uid, type: 'environment', name: env.name_t, description: env.description_t, creationDate: new Date(env.creation_ts_dt), public: env.public_b ?? false, lastPublicationDate: env.creation_ts_dt ? new Date(env.creation_ts_dt) : undefined, owner, space: { handle: env.handle_s, }, organization: { handle: env.handle_s, }, }; }; const toItem = (item) => { if (!item.type_s) { console.error('No type_s found on item', item); return {}; } switch (item.type_s) { case 'assignment': return toAssignment(item); case 'cell': return toCell(item); case 'dataset': return toDataset(item); case 'document': return toDocument(item); case 'exercise': return toExercise(item); case 'lesson': return toLesson(item); case 'notebook': return toNotebook(item); case 'page': return toPage(item); default: return {}; } }; const toCourse = (raw_course) => { const owner = newUserMock(); let instructor = undefined; if (raw_course.members) { let raw_instructor = raw_course.members; if (Array.isArray(raw_instructor)) { raw_instructor = raw_instructor[0]; } instructor = { id: raw_instructor.uid, handle: raw_instructor.handle_s, email: raw_instructor.email_s, firstName: raw_instructor.first_name_t, lastName: raw_instructor.last_name_t, initials: namesAsInitials(raw_instructor.to_first_name_t, raw_instructor.to_last_name_t), displayName: asDisplayName(raw_instructor.first_name_t, raw_instructor.last_name_t), roles: [], iamProviders: [], setRoles: (_roles) => { }, unsubscribedFromOutbounds: false, onboarding: BOOTSTRAP_USER_ONBOARDING, linkedContactId: undefined, events: [], settings: {}, }; } let students = undefined; if (raw_course.students) { students = new Map(); raw_course.students.forEach((raw_stud) => { const student = toUser(raw_stud); if (student && students) { students.set(student.id, student); } }); } let itemIds = new Array(); let raw_item_uids = raw_course.item_uids_s; if (raw_item_uids && raw_item_uids !== '()') { raw_item_uids = raw_item_uids.replace('(', '').replace(')', ''); itemIds = raw_item_uids.split(' '); } const items = new Array(); if (raw_course.items) { raw_course.items.forEach((item) => { const i = toItem(item); items.push(i); }); } return { id: raw_course.uid, handle: raw_course.handle_s, type: 'space', variant: 'course', name: raw_course.name_t, description: raw_course.description_t, creationDate: new Date(raw_course.creation_ts_dt), public: raw_course.public_b ?? false, items, itemIds, instructor, students, owner, }; }; /* eslint-enable @typescript-eslint/no-explicit-any */ // ============================================================================ // Authentication & Profile Hooks // ============================================================================ /** * Login mutation * @example * ```tsx * const login = useLogin(); * login.mutate({ handle: 'user', password: 'pass' }); * ``` */ const useLogin = () => { const iamStore = useIAMStore(); return useMutation({ mutationFn: async ({ handle, password, }) => { return requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/login`, method: 'POST', body: { handle, password }, }); }, onSuccess: async (resp) => { // Set the token in IAM state if (resp.token) { await iamStore.refreshUserByToken(resp.token); } // Invalidate all queries on successful login queryClient.invalidateQueries(); }, }); }; /** * Logout mutation * @example * ```tsx * const logout = useLogout(); * logout.mutate(); * ``` */ const useLogout = () => { return useMutation({ mutationFn: async () => { return requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/logout`, method: 'GET', }); }, onSuccess: () => { // Clear all queries on logout queryClient.clear(); }, }); }; /** * Get current user profile * @example * ```tsx * const { data: user, isPending } = useMe(); * ``` */ const useMe = (token) => { return useQuery({ queryKey: queryKeys.auth.me(), queryFn: async () => { const resp = await requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/me`, method: 'GET', token, }); if (resp.me) { return toUser(resp.me); } return null; }, ...DEFAULT_QUERY_OPTIONS, }); }; /** * Update current user profile */ const useUpdateMe = () => { return useMutation({ mutationFn: async ({ email, firstName, lastName, }) => { return requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/me`, method: 'PUT', body: { email, firstName, lastName }, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.auth.me() }); }, }); }; /** * Whoami query */ const useWhoami = () => { return useQuery({ queryKey: queryKeys.auth.whoami(), queryFn: async () => { return requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/whoami`, method: 'GET', }); }, ...DEFAULT_QUERY_OPTIONS, }); }; // ============================================================================ // User Hooks // ============================================================================ /** * Get user by ID * @param userId - User ID * @example * ```tsx * const { data: user, isPending, isError } = useUser('user-123'); * ``` */ const useUser = (userId) => { return useQuery({ queryKey: queryKeys.users.detail(userId), queryFn: async () => { const resp = await requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/users/${userId}`, method: 'GET', }); if (resp.success && resp.user) { const user = toUser(resp.user); // Also populate handle cache if available if (user) { queryClient.setQueryData(queryKeys.users.byHandle(user.handle), user); } return user; } throw new Error(resp.message || 'Failed to fetch user'); }, ...DEFAULT_QUERY_OPTIONS, enabled: !!userId, }); }; /** * Get user by handle * @param handle - User handle */ const useUserByHandle = (handle) => { return useQuery({ queryKey: queryKeys.users.byHandle(handle), queryFn: async () => { // Implementation depends on your API // For now, using search as workaround const resp = await requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/users/search`, method: 'POST', body: { namingPattern: handle }, }); if (resp.success && resp.users) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const users = resp.users.map((u) => toUser(u)); const user = users.find((u) => u.handle === handle); if (user) { // Populate ID cache queryClient.setQueryData(queryKeys.users.detail(user.id), user); } return user; } return null; }, ...DEFAULT_QUERY_OPTIONS, enabled: !!handle, }); }; /** * Search users by naming pattern */ const useSearchUsers = (namingPattern) => { return useQuery({ queryKey: queryKeys.users.search(namingPattern), queryFn: async () => { const resp = await requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/users/search`, method: 'POST', body: { namingPattern }, }); if (resp.success && resp.users) { const users = resp.users.map((u) => { const user = toUser(u); // Pre-populate individual caches if (user) { queryClient.setQueryData(queryKeys.users.detail(user.id), user); queryClient.setQueryData(queryKeys.users.byHandle(user.handle), user); } return user; }); return users.filter(Boolean); } return []; }, ...DEFAULT_QUERY_OPTIONS, enabled: namingPattern !== undefined && namingPattern !== null, }); }; /** * Update user onboarding */ const useUpdateUserOnboarding = () => { return useMutation({ mutationFn: async (onboarding) => { return requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/onboardings`, method: 'PUT', body: { onboarding }, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: queryKeys.auth.me() }); }, }); }; /** * Update user settings */ const useUpdateUserSettings = () => { return useMutation({ mutationFn: async ({ userId, settings, }) => { return requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/users/${userId}/settings`, method: 'PUT', body: { aiagents_url_s: settings.aiAgentsUrl, can_invite_b: settings.canInvite || false, }, }); }, onSuccess: (_, variables) => { queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(variables.userId), }); }, }); }; // ============================================================================ // Organization Hooks // ============================================================================ /** * Get organization by ID */ const useOrganization = (organizationId) => { return useQuery({ queryKey: queryKeys.organizations.detail(organizationId), queryFn: async () => { const resp = await requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/organizations/${organizationId}`, method: 'GET', }); if (resp.success && resp.organization) { const org = toOrganization(resp.organization); // Also populate handle cache queryClient.setQueryData(queryKeys.organizations.byHandle(org.handle), org); return org; } throw new Error(resp.message || 'Failed to fetch organization'); }, ...DEFAULT_QUERY_OPTIONS, enabled: !!organizationId && !!user, }); }; /** * Get organization by handle */ const useOrganizationByHandle = (handle) => { return useQuery({ queryKey: queryKeys.organizations.byHandle(handle), queryFn: async () => { // Fetch via account endpoint const resp = await requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/accounts/${handle}`, method: 'GET', }); if (resp.success && resp.organization) { const org = toOrganization(resp.organization); // Populate ID cache queryClient.setQueryData(queryKeys.organizations.detail(org.id), org); return org; } return null; }, ...DEFAULT_QUERY_OPTIONS, enabled: !!handle, }); }; /** * Get user's organizations */ const useUserOrganizations = () => { return useQuery({ queryKey: queryKeys.organizations.userOrgs(), queryFn: async () => { const resp = await requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/organizations`, method: 'GET', }); if (resp.success && resp.organizations) { const orgs = resp.organizations.map((org) => { const organization = toOrganization(org); // Pre-populate caches queryClient.setQueryData(queryKeys.organizations.detail(organization.id), organization); queryClient.setQueryData(queryKeys.organizations.byHandle(organization.handle), organization); return organization; }); return orgs.filter((org) => user ? checkIsOrganizationMember(user, org) : false); } return []; }, ...DEFAULT_QUERY_OPTIONS, enabled: !!user, }); }; /** * Create organization */ const useCreateOrganization = () => { return useMutation({ mutationFn: async (organization) => { return requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/organizations`, method: 'POST', body: { handle: organization.handle, name: organization.name, description: organization.description, }, }); }, onSuccess: resp => { if (resp.organization) { const org = toOrganization(resp.organization); // Set detail cache queryClient.setQueryData(queryKeys.organizations.detail(org.id), org); // Invalidate all organization queries queryClient.invalidateQueries({ queryKey: queryKeys.organizations.all(), }); } }, }); }; /** * Update organization with optimistic update */ const useUpdateOrganization = () => { return useMutation({ mutationFn: async (organization) => { return requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/organizations/${organization.id}`, method: 'PUT', body: { name: organization.name, description: organization.description, }, }); }, onSuccess: (_, organization) => { const orgId = organization.id ?? ''; // Invalidate detail cache queryClient.invalidateQueries({ queryKey: queryKeys.organizations.detail(orgId), }); // Invalidate all organization queries queryClient.invalidateQueries({ queryKey: queryKeys.organizations.all(), }); }, }); }; // ============================================================================ // Team Hooks // ============================================================================ /** * Get team by ID */ const useTeam = (teamId, organizationId) => { return useQuery({ queryKey: queryKeys.teams.detail(teamId), queryFn: async () => { const resp = await requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/teams/${teamId}`, method: 'GET', }); if (resp.success && resp.team) { const team = toTeam(resp.team, organizationId); queryClient.setQueryData(queryKeys.teams.byHandle(team.handle), team); return team; } throw new Error(resp.message || 'Failed to fetch team'); }, ...DEFAULT_QUERY_OPTIONS, enabled: !!teamId && !!organizationId, }); }; /** * Get teams by organization */ const useTeamsByOrganization = (organizationId) => { return useQuery({ queryKey: queryKeys.teams.byOrganization(organizationId), queryFn: async () => { const resp = await requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/organizations/${organizationId}/teams`, method: 'GET', }); if (resp.success && resp.teams) { const teams = resp.teams.map((t) => { const team = toTeam(t, organizationId); // Pre-populate caches queryClient.setQueryData(queryKeys.teams.detail(team.id), team); queryClient.setQueryData(queryKeys.teams.byHandle(team.handle), team); return team; }); return teams; } return []; }, ...DEFAULT_QUERY_OPTIONS, enabled: !!organizationId, }); }; /** * Create team */ const useCreateTeam = () => { return useMutation({ mutationFn: async ({ team, organization, }) => { return requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/teams`, method: 'POST', body: { handle: team.handle, name: team.name, description: team.description, organizationId: organization.id, }, }); }, onSuccess: (resp, _variables) => { if (resp.team) { const team = toTeam(resp.team, _variables.organization.id); // Set detail cache queryClient.setQueryData(queryKeys.teams.detail(team.id), team); // Invalidate all team queries queryClient.invalidateQueries({ queryKey: queryKeys.teams.all(), }); } }, }); }; /** * Update team */ const useUpdateTeam = () => { return useMutation({ mutationFn: async (team) => { return requestDatalayer({ url: `${configuration.iamRunUrl}/api/iam/v1/teams/${team.id}`, method: 'PUT', body: { name: team.name, description: team.description, }, }); }, onSuccess: (_, team) => { // Invalidate detail cache queryClient.invalidateQueries({ queryKey: queryKeys.teams.detail(team.id ?? ''), }); // Invalidate all team queries queryClient.invalidateQueries({ queryKey: queryKeys.teams.all(), }); }, }); }; // ============================================================================ // Space Hooks // ============================================================================ /** * Get space by ID */ const useSpace = (spaceId) => { return useQuery({ queryKey: queryKeys.spaces.detail(spaceId), queryFn: async () => { // Note: This might need organization context const resp = await requestDatalayer({ url: `${configuration.spacerRunUrl}/api/spacer/v1/spaces/${spaceId}`, method: 'GET', }); if (resp.success && resp.space) { const space = toSpace(resp.space); queryClient.setQueryData(queryKeys.spaces.byHandle(space.handle), space); return space; } throw new Error(resp.message || 'Failed to fetch space'); }, ...DEFAULT_QUERY_OPTIONS, enabled: !!spaceId, }); }; /** * Get organization space */ const useOrganizationSpace = (organizationId, spaceId) => { return useQuery({ queryKey: queryKeys.spaces.detail(spaceId), queryFn: async () => { const resp = await requestDatalayer({ url: `${configuration.spacerRunUrl}/api/spacer/v1/spaces/${spaceId}/organizations/${organizationId}`, method: 'GET', }); if (resp.success && resp.space) { return toSpace(resp.space); } throw new Error(resp.message || 'Failed to fetch space'); }, ...DEFAULT_QUERY_OPTIONS, enabled: !!spaceId && !!organizationId, }); }; /** * Get spaces by organization */ const useOrganizationSpaces = (organizationId) => { return useQuery({ queryKey: queryKeys.spaces.byOrganization(organizationId), queryFn: async () => { const resp = await requestDatalayer({ url: `${configuration.spacerRunUrl}/api/spacer/v1/spaces/organizations/${organizationId}`, method: 'GET', }); if (resp.success && resp.spaces) { const spaces = resp.spaces.map((spc) => { const space = toSpace(spc); queryClient.setQueryData(queryKeys.spaces.detail(space.id), space); return space; }); return spaces; } return []; }, ...DEFAULT_QUERY_OPTIONS, enabled: !!organizationId, }); }; /** * Get user spaces */ const useUserSpaces = () => { return useQuery({ queryKey: queryKeys.spaces.userSpaces(), queryFn: async () => { const resp = await requestDatalayer({ url: `${configuration.spacerRunUrl}/api/spacer/v1/spaces/users/me`, method: 'GET', }); if (resp.success && resp.spaces) { const spaces = resp.spaces.map((spc) => { const space = toSpace(spc); queryClient.setQueryData(queryKeys.spaces.detail(space.id), space); queryClient.setQueryData(queryKeys.spaces.byHandle(space.handle), space); return space; }); return spaces; } return []; }, ...DEFAULT_QUERY_OPTIONS, enabled: !!user, }); }; /** * Create space */ const useCreateSpace = () => { return useMutation({ mutationFn: async ({ space, organization, }) => { const seedSpaceId = space.variant === 'course' ? space.seedSpace?.id : undefined; return requestDatalayer({ url: `${configuration.spacerRunUrl}/api/spacer/v1/spaces`, method: 'POST', body: { name: space.name, description: space.description, variant: space.variant, public: space.public, spaceHandle: space.handle, organizationId: organization?.id, seedSpaceId, }, }); }, onSuccess: (resp, _variables) => { if (resp.space) { const space = toSpace(resp.space); // Set detail cache queryClient.setQueryData(queryKeys.spaces.detail(space.id), space); // Invalidate all space queries queryClient.invalidateQueries({ queryKey: queryKeys.spaces.all(), }); } }, }); }; /** * Update space with optimistic update */ const useUpdateSpace = () => { return useMutation({ mutationFn: async (space) => { return requestDatalayer({ url: `${configuration.spacerRunUrl}/api/spacer/v1/spaces/${space.id}/users/${user?.id}`, method: 'PUT', body: { name: space.name, description: space.description, }, }); }, onSuccess: (_, space) => { const spaceId = space.id ?? ''; // Invalidate detail cache queryClient.invalidateQueries({ queryKey: queryKeys.spaces.detail(spaceId), }); // Invalidate all space queries queryClient.invalidateQueries({ queryKey: queryKeys.spaces.all(), }); }, }); }; // ======================================================