UNPKG

@flavoai/fastfold

Version:

Zero-boilerplate backend for React apps with auto-generated CRUD and declarative security

322 lines 11.5 kB
import { useQuery as useReactQuery, useMutation as useReactMutation, useQueryClient } from '@tanstack/react-query'; let clientConfig = { baseUrl: 'http://localhost:3001/api' }; /** * Configure the Fastfold client */ export function configureFastfold(config) { clientConfig = { ...clientConfig, ...config }; } /** * Set authentication token for all requests */ export function setAuthToken(token) { clientConfig.headers = { ...clientConfig.headers, 'Authorization': `Bearer ${token}` }; } /** * Clear authentication token */ export function clearAuthToken() { if (clientConfig.headers) { delete clientConfig.headers['Authorization']; } } // ============================================================================ // HTTP CLIENT // ============================================================================ class HttpClient { async request(method, url, data) { const fullUrl = `${clientConfig.baseUrl}${url}`; const options = { method, headers: { 'Content-Type': 'application/json', ...clientConfig.headers, }, }; if (data && method !== 'GET') { options.body = JSON.stringify(data); } const response = await fetch(fullUrl, options); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const result = await response.json(); if (!result.success) { throw new Error(result.error || 'Request failed'); } return result.data; } async get(url) { return this.request('GET', url); } async post(url, data) { return this.request('POST', url, data); } async put(url, data) { return this.request('PUT', url, data); } async delete(url) { return this.request('DELETE', url); } } const httpClient = new HttpClient(); // ============================================================================ // QUERY KEY FACTORIES // ============================================================================ /** * Generate consistent query keys for React Query */ export const queryKeys = { all: (tableName) => [tableName], lists: (tableName) => [tableName, 'list'], list: (tableName, params) => [tableName, 'list', params], details: (tableName) => [tableName, 'detail'], detail: (tableName, id) => [tableName, 'detail', id], }; /** * 📋 QUERY HOOK - Fetch multiple records with React Query * * @param tableName The table to query * @param params Query parameters (where, orderBy, limit, with, etc.) * @param options React Query options * * @example * const { data: posts, isLoading, error } = useQuery('posts', { * where: { published: true }, * with: { author: true } * }); */ export function useFastfoldQuery(tableName, params = {}, options = {}) { return useReactQuery({ queryKey: queryKeys.list(tableName, params), queryFn: async () => { const queryString = new URLSearchParams(); if (Object.keys(params).length > 0) { queryString.append('params', JSON.stringify(params)); } const url = `/${tableName}${queryString.toString() ? '?' + queryString.toString() : ''}`; return httpClient.get(url); }, staleTime: options.staleTime ?? 5 * 60 * 1000, // 5 minutes gcTime: options.cacheTime ?? 10 * 60 * 1000, // 10 minutes refetchOnWindowFocus: options.refetchOnWindowFocus ?? false, refetchInterval: options.refetchInterval, enabled: options.enabled, }); } /** * 🎯 QUERY ONE HOOK - Fetch single record by ID with React Query * * @param tableName The table to query * @param id The record ID * @param params Additional query parameters (with, etc.) * @param options React Query options * * @example * const { data: post } = useQueryOne('posts', '123', { * with: { author: true, comments: true } * }); */ export function useFastfoldQueryOne(tableName, id, params = {}, options = {}) { return useReactQuery({ queryKey: queryKeys.detail(tableName, id), queryFn: async () => { const queryString = new URLSearchParams(); if (Object.keys(params).length > 0) { queryString.append('params', JSON.stringify(params)); } const url = `/${tableName}/${id}${queryString.toString() ? '?' + queryString.toString() : ''}`; return httpClient.get(url); }, staleTime: options.staleTime ?? 5 * 60 * 1000, gcTime: options.cacheTime ?? 10 * 60 * 1000, refetchOnWindowFocus: options.refetchOnWindowFocus ?? false, refetchInterval: options.refetchInterval, enabled: options.enabled && !!id, }); } /** * ✏️ CREATE HOOK - Create new records with automatic cache invalidation * * @param tableName The table to create records in * @param options Mutation options * * @example * const createPost = useCreate('posts', { * onSuccess: (newPost) => console.log('Created:', newPost) * }); * * await createPost.mutateAsync({ title: 'Hello', content: 'World' }); */ export function useFastfoldCreate(tableName, options = {}) { const queryClient = useQueryClient(); return useReactMutation({ mutationFn: async (variables) => { return httpClient.post(`/${tableName}`, variables); }, onSuccess: (data, variables) => { // Automatic cache invalidation if (options.invalidateQueries !== false) { queryClient.invalidateQueries({ queryKey: queryKeys.lists(tableName) }); } // Custom cache update if (options.updateCache) { options.updateCache(data, variables, queryClient); } // User callback options.onSuccess?.(data, variables); }, onError: options.onError, onSettled: options.onSettled, }); } /** * 🔄 UPDATE HOOK - Update existing records with automatic cache invalidation * * @param tableName The table to update records in * @param options Mutation options * * @example * const updatePost = useUpdate('posts', { * onSuccess: (updatedPost) => console.log('Updated:', updatedPost) * }); * * await updatePost.mutateAsync({ id: '123', data: { title: 'New Title' } }); */ export function useFastfoldUpdate(tableName, options = {}) { const queryClient = useQueryClient(); return useReactMutation({ mutationFn: async (variables) => { return httpClient.put(`/${tableName}/${variables.id}`, variables.data); }, onSuccess: (data, variables) => { // Automatic cache invalidation if (options.invalidateQueries !== false) { queryClient.invalidateQueries({ queryKey: queryKeys.lists(tableName) }); queryClient.invalidateQueries({ queryKey: queryKeys.detail(tableName, variables.id) }); } // Custom cache update if (options.updateCache) { options.updateCache(data, variables, queryClient); } // User callback options.onSuccess?.(data, variables); }, onError: options.onError, onSettled: options.onSettled, }); } /** * 🗑️ DELETE HOOK - Delete records with automatic cache invalidation * * @param tableName The table to delete records from * @param options Mutation options * * @example * const deletePost = useDelete('posts', { * onSuccess: () => console.log('Deleted successfully') * }); * * await deletePost.mutateAsync('123'); */ export function useFastfoldDelete(tableName, options = {}) { const queryClient = useQueryClient(); return useReactMutation({ mutationFn: async (id) => { return httpClient.delete(`/${tableName}/${id}`); }, onSuccess: (data, variables) => { // Automatic cache invalidation if (options.invalidateQueries !== false) { queryClient.invalidateQueries({ queryKey: queryKeys.lists(tableName) }); queryClient.invalidateQueries({ queryKey: queryKeys.detail(tableName, variables) }); } // Custom cache update if (options.updateCache) { options.updateCache(data, variables, queryClient); } // User callback options.onSuccess?.(data, variables); }, onError: options.onError, onSettled: options.onSettled, }); } // ============================================================================ // CONVENIENCE ALIASES (for backward compatibility) // ============================================================================ export const useQuery = useFastfoldQuery; export const useQueryOne = useFastfoldQueryOne; export const useCreate = useFastfoldCreate; export const useUpdate = useFastfoldUpdate; export const useDelete = useFastfoldDelete; // ============================================================================ // CACHE UTILITIES // ============================================================================ /** * 🔄 INVALIDATE CACHE - Manually invalidate queries * * @example * const invalidate = useInvalidateCache(); * invalidate.table('posts'); // Invalidate all posts queries * invalidate.record('posts', '123'); // Invalidate specific post */ export function useInvalidateCache() { const queryClient = useQueryClient(); return { table: (tableName) => { queryClient.invalidateQueries({ queryKey: queryKeys.all(tableName) }); }, lists: (tableName) => { queryClient.invalidateQueries({ queryKey: queryKeys.lists(tableName) }); }, record: (tableName, id) => { queryClient.invalidateQueries({ queryKey: queryKeys.detail(tableName, id) }); }, all: () => { queryClient.invalidateQueries(); } }; } /** * 📝 UPDATE CACHE - Manually update cached data * * @example * const updateCache = useUpdateCache(); * updateCache.record('posts', '123', updatedPost); * updateCache.addToList('posts', {}, newPost); */ export function useUpdateCache() { const queryClient = useQueryClient(); return { record: (tableName, id, data) => { queryClient.setQueryData(queryKeys.detail(tableName, id), data); }, addToList: (tableName, params, newItem) => { queryClient.setQueryData(queryKeys.list(tableName, params), (old) => { return old ? [newItem, ...old] : [newItem]; }); }, removeFromList: (tableName, params, id) => { queryClient.setQueryData(queryKeys.list(tableName, params), (old) => { return old ? old.filter((item) => item.id !== id) : []; }); }, updateInList: (tableName, params, id, data) => { queryClient.setQueryData(queryKeys.list(tableName, params), (old) => { return old ? old.map((item) => item.id === id ? { ...item, ...data } : item) : []; }); } }; } // Export provider export { FastfoldProvider, createFastfoldQueryClient } from './provider'; // Export all types export * from '../types'; //# sourceMappingURL=react-query.js.map