bake canonical build harness (deterministic /ship for forks) [editorial]

This commit is contained in:
vibeasite-bot
2026-08-22 05:57:10 +01:00
parent 596503e35a
commit b3111ebab7
77 changed files with 6580 additions and 161 deletions
+292
View File
@@ -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 };
}