bake canonical build harness (deterministic /ship for forks) [editorial]
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user