293 lines
11 KiB
TypeScript
293 lines
11 KiB
TypeScript
// ---------------------------------------------------------------------------
|
|
// useCollection + useMutation — pre-built AnyDB data hooks
|
|
// ---------------------------------------------------------------------------
|
|
// PRE-INSTALLED in every AUTO_DB React project. CodeWriterAgent just imports:
|
|
// import { useCollection, useMutation } from "@/hooks/use-anydb"
|
|
// const { data, loading, error, refetch } = useCollection('products', { sort: { created_at: -1 }, limit: 20 })
|
|
// const { mutate, loading: saving } = useMutation('products')
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
|
|
// AnyDB import — always available (stub if not connected, real client if connected)
|
|
import anydb from "@/lib/anydb";
|
|
|
|
// Seed data fallback — pre-populated per category so the site looks functional
|
|
// even before AnyDB is connected. Replace with real data by connecting AnyDB.
|
|
const SEED_DATA: Record<string, any[]> = {
|
|
users: [
|
|
{ _id: "u1", name: "Alice Johnson", email: "alice@example.com", plan: "pro", role: "admin", avatar_url: "", created_at: "2025-01-15T10:00:00Z" },
|
|
{ _id: "u2", name: "Bob Smith", email: "bob@example.com", plan: "free", role: "user", avatar_url: "", created_at: "2025-02-20T14:30:00Z" },
|
|
{ _id: "u3", name: "Carol Williams", email: "carol@example.com", plan: "enterprise", role: "user", avatar_url: "", created_at: "2025-03-10T09:15:00Z" },
|
|
],
|
|
products: [
|
|
{ _id: "p1", name: "Classic Leather Jacket", price_cents: 18900, category: "clothing", stock: 45, rating: 4.8, created_at: "2025-01-10T00:00:00Z" },
|
|
{ _id: "p2", name: "Premium Denim Jeans", price_cents: 8900, category: "clothing", stock: 120, rating: 4.6, created_at: "2025-01-15T00:00:00Z" },
|
|
{ _id: "p3", name: "Cashmere Sweater", price_cents: 12900, category: "clothing", stock: 30, rating: 4.9, created_at: "2025-02-01T00:00:00Z" },
|
|
{ _id: "p4", name: "Silk Blend Shirt", price_cents: 6900, category: "clothing", stock: 85, rating: 4.5, created_at: "2025-02-15T00:00:00Z" },
|
|
],
|
|
orders: [
|
|
{ _id: "o1", customer: "Alice Johnson", total_cents: 28900, status: "completed", created_at: "2025-03-01T10:00:00Z" },
|
|
{ _id: "o2", customer: "Bob Smith", total_cents: 8900, status: "processing", created_at: "2025-03-05T14:00:00Z" },
|
|
{ _id: "o3", customer: "Carol Williams", total_cents: 45800, status: "completed", created_at: "2025-03-08T09:00:00Z" },
|
|
{ _id: "o4", customer: "David Lee", total_cents: 12900, status: "pending", created_at: "2025-03-10T16:00:00Z" },
|
|
{ _id: "o5", customer: "Eve Chen", total_cents: 6900, status: "cancelled", created_at: "2025-03-12T11:00:00Z" },
|
|
],
|
|
metrics: [
|
|
{ _id: "m1", name: "revenue", value: 48500, unit: "dollars", recorded_at: "2025-03-01T00:00:00Z" },
|
|
{ _id: "m2", name: "users", value: 2340, unit: "count", recorded_at: "2025-03-01T00:00:00Z" },
|
|
{ _id: "m3", name: "conversion", value: 3.2, unit: "percent", recorded_at: "2025-03-01T00:00:00Z" },
|
|
{ _id: "m4", name: "uptime", value: 99.99, unit: "percent", recorded_at: "2025-03-01T00:00:00Z" },
|
|
],
|
|
posts: [
|
|
{ _id: "post1", title: "Getting Started with Our Platform", slug: "getting-started", excerpt: "Learn how to set up your account and start building.", category: "Tutorial", author_id: "u1", status: "published", published_at: "2025-02-15T10:00:00Z" },
|
|
{ _id: "post2", title: "Advanced Analytics Deep Dive", slug: "analytics-deep-dive", excerpt: "Explore powerful analytics features for data-driven decisions.", category: "Technology", author_id: "u1", status: "published", published_at: "2025-03-01T10:00:00Z" },
|
|
{ _id: "post3", title: "Best Practices for Team Collaboration", slug: "team-collaboration", excerpt: "Tips for getting the most out of collaborative features.", category: "Business", author_id: "u2", status: "published", published_at: "2025-03-10T10:00:00Z" },
|
|
],
|
|
subscribers: [],
|
|
contact_messages: [],
|
|
};
|
|
|
|
// Seed-data collection simulator
|
|
function createSeedCollection(name: string) {
|
|
const data = SEED_DATA[name] || [];
|
|
return {
|
|
find: async (filter: any = {}, opts: any = {}) => {
|
|
let results = [...data];
|
|
// Basic filter
|
|
if (Object.keys(filter).length > 0) {
|
|
results = results.filter(item =>
|
|
Object.entries(filter).every(([k, v]) => item[k] === v)
|
|
);
|
|
}
|
|
// Sort
|
|
if (opts.sort) {
|
|
const [key, dir] = Object.entries(opts.sort)[0] as [string, number];
|
|
results.sort((a, b) => dir === 1 ? (a[key] > b[key] ? 1 : -1) : (a[key] < b[key] ? 1 : -1));
|
|
}
|
|
const total = results.length;
|
|
// Pagination
|
|
if (opts.skip) results = results.slice(opts.skip);
|
|
if (opts.limit) results = results.slice(0, opts.limit);
|
|
return { data: results, total, page: 1, limit: opts.limit || 50, pages: 1 };
|
|
},
|
|
insertOne: async (doc: any) => {
|
|
const newDoc = { ...doc, _id: `seed_${Date.now()}` };
|
|
data.push(newDoc);
|
|
return newDoc;
|
|
},
|
|
updateOne: async (filter: any, update: any) => {
|
|
const idx = data.findIndex(d => Object.entries(filter).every(([k, v]) => d[k] === v));
|
|
if (idx >= 0) Object.assign(data[idx], update);
|
|
return { modifiedCount: idx >= 0 ? 1 : 0 };
|
|
},
|
|
deleteOne: async (filter: any) => {
|
|
const idx = data.findIndex(d => Object.entries(filter).every(([k, v]) => d[k] === v));
|
|
if (idx >= 0) data.splice(idx, 1);
|
|
return { deletedCount: idx >= 0 ? 1 : 0 };
|
|
},
|
|
aggregate: async (pipeline: any[]) => {
|
|
// Basic count aggregation support
|
|
if (pipeline.some(s => s.$count)) return [{ value: data.length }];
|
|
if (pipeline.some(s => s.$group)) {
|
|
const group = pipeline.find(s => s.$group);
|
|
if (group.$group._id === null) {
|
|
const sumField = Object.entries(group.$group).find(([k, v]: any) => v?.$sum);
|
|
if (sumField) {
|
|
const field = (sumField[1] as any).$sum?.replace('$', '');
|
|
const total = data.reduce((acc, d) => acc + (d[field] || 0), 0);
|
|
return [{ value: total }];
|
|
}
|
|
return [{ value: data.length }];
|
|
}
|
|
}
|
|
return data;
|
|
},
|
|
};
|
|
}
|
|
|
|
// Unified collection getter — tries AnyDB first, falls back to seed data
|
|
async function getCollection(name: string) {
|
|
try {
|
|
const col = await anydb.collection(name);
|
|
// Test if it's a real connection (stub returns empty instantly)
|
|
const test = await col.find({}, { limit: 1 });
|
|
if (test && test.data !== undefined) return col;
|
|
} catch {
|
|
// AnyDB not connected — use seed data
|
|
}
|
|
return createSeedCollection(name);
|
|
}
|
|
|
|
interface CollectionOptions {
|
|
filter?: Record<string, any>;
|
|
sort?: Record<string, 1 | -1>;
|
|
limit?: number;
|
|
skip?: number;
|
|
autoFetch?: boolean;
|
|
}
|
|
|
|
interface CollectionResult<T = any> {
|
|
data: T[];
|
|
total: number;
|
|
loading: boolean;
|
|
error: string | null;
|
|
refetch: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Fetch data from an AnyDB collection with loading/error states.
|
|
*
|
|
* Usage:
|
|
* const { data: products, loading, refetch } = useCollection('products', { limit: 20 })
|
|
* const { data: orders } = useCollection('orders', { filter: { status: 'active' }, sort: { created_at: -1 } })
|
|
*/
|
|
export function useCollection<T = any>(
|
|
collectionName: string,
|
|
options: CollectionOptions = {}
|
|
): CollectionResult<T> {
|
|
const { filter = {}, sort, limit = 50, skip = 0, autoFetch = true } = options;
|
|
const [data, setData] = useState<T[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchData = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const col = await getCollection(collectionName);
|
|
const result = await col.find(filter, { sort, limit, skip });
|
|
setData((result.data as T[]) || []);
|
|
setTotal(result.total || 0);
|
|
} catch (err: any) {
|
|
setError(err.message || "Failed to fetch data");
|
|
console.error(`[useCollection] ${collectionName}:`, err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [collectionName, JSON.stringify(filter), JSON.stringify(sort), limit, skip]);
|
|
|
|
useEffect(() => {
|
|
if (autoFetch) fetchData();
|
|
}, [fetchData, autoFetch]);
|
|
|
|
return { data, total, loading, error, refetch: fetchData };
|
|
}
|
|
|
|
interface MutationResult {
|
|
mutate: (document: Record<string, any>) => Promise<any>;
|
|
update: (filter: Record<string, any>, updates: Record<string, any>) => Promise<any>;
|
|
remove: (filter: Record<string, any>) => Promise<any>;
|
|
loading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
/**
|
|
* Write to an AnyDB collection (insert, update, delete).
|
|
*
|
|
* Usage:
|
|
* const { mutate, loading } = useMutation('contact_messages')
|
|
* await mutate({ name, email, message }) // insertOne
|
|
* await update({ _id: id }, { status: 'read' }) // updateOne
|
|
* await remove({ _id: id }) // deleteOne
|
|
*/
|
|
export function useMutation(collectionName: string): MutationResult {
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const mutate = useCallback(async (document: Record<string, any>) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const col = await getCollection(collectionName);
|
|
const result = await col.insertOne({
|
|
...document,
|
|
created_at: new Date().toISOString(),
|
|
});
|
|
return result;
|
|
} catch (err: any) {
|
|
setError(err.message || "Failed to save");
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [collectionName]);
|
|
|
|
const update = useCallback(async (filter: Record<string, any>, updates: Record<string, any>) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const col = await getCollection(collectionName);
|
|
const result = await col.updateOne(filter, updates);
|
|
return result;
|
|
} catch (err: any) {
|
|
setError(err.message || "Failed to update");
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [collectionName]);
|
|
|
|
const remove = useCallback(async (filter: Record<string, any>) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const col = await getCollection(collectionName);
|
|
const result = await col.deleteOne(filter);
|
|
return result;
|
|
} catch (err: any) {
|
|
setError(err.message || "Failed to delete");
|
|
throw err;
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [collectionName]);
|
|
|
|
return { mutate, update, remove, loading, error };
|
|
}
|
|
|
|
interface AggregateResult<T = any> {
|
|
data: T[];
|
|
loading: boolean;
|
|
error: string | null;
|
|
refetch: () => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Run an aggregation pipeline on an AnyDB collection.
|
|
*
|
|
* Usage:
|
|
* const { data: stats } = useAggregate('orders', [
|
|
* { $group: { _id: '$status', count: { $sum: 1 }, total: { $sum: '$total_cents' } } }
|
|
* ])
|
|
*/
|
|
export function useAggregate<T = any>(
|
|
collectionName: string,
|
|
pipeline: Record<string, any>[]
|
|
): AggregateResult<T> {
|
|
const [data, setData] = useState<T[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchData = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const col = await getCollection(collectionName);
|
|
const result = await col.aggregate(pipeline);
|
|
setData((result as T[]) || []);
|
|
} catch (err: any) {
|
|
setError(err.message || "Aggregation failed");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [collectionName, JSON.stringify(pipeline)]);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
return { data, loading, error, refetch: fetchData };
|
|
}
|