@datalayer/core
Version:
[](https://datalayer.io)
1,336 lines • 228 kB
JavaScript
/*
* 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(),
});
},
});
};
// ======================================================