@flavoai/fastfold
Version:
Zero-boilerplate backend for React apps with auto-generated CRUD and declarative security
322 lines • 11.5 kB
JavaScript
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