import { collection, doc, onSnapshot, addDoc, updateDoc, query, orderBy, serverTimestamp, Timestamp, type DocumentData, type FirestoreDataConverter, type QueryDocumentSnapshot, type SnapshotOptions, type Unsubscribe, } from "firebase/firestore"; import { firestoreDb } from "@/firebase"; import { ParticleSchema } from "@/api/types"; import type { Particle, ParticleType, ParticlePropertiesMap } from "@/api/types"; // --- Converter --- const particleConverter: FirestoreDataConverter = { toFirestore(particle: Particle): DocumentData { const { id: _id, created_at, updated_at, ...rest } = particle; return { ...rest, created_at: Timestamp.fromDate(created_at), ...(updated_at && { updated_at: Timestamp.fromDate(updated_at) }), }; }, fromFirestore( snap: QueryDocumentSnapshot, options?: SnapshotOptions, ): Particle { const raw = snap.data(options); return ParticleSchema.parse({ id: snap.id, type: raw.type, properties: raw.properties, created_at: (raw.created_at as Timestamp).toDate(), created_by_email: raw.created_by_email, updated_at: raw.updated_at ? (raw.updated_at as Timestamp).toDate() : undefined, visible_to: raw.visible_to, }); }, }; // --- Typed reference helpers --- function typedDoc(path: string) { return doc(firestoreDb, path).withConverter(particleConverter); } function typedCollection(path: string) { return collection(firestoreDb, path).withConverter(particleConverter); } // --- Exported operations --- export function subscribeToParticle( docPath: string, onData: (particle: Particle | null) => void, onError: (error: Error) => void, ): Unsubscribe { return onSnapshot( typedDoc(docPath), (snap) => { onData(snap.exists() ? snap.data() : null); }, onError, ); } export function subscribeToParticleChildren( collectionPath: string, onData: (children: Particle[]) => void, onError: (error: Error) => void, ): Unsubscribe { const q = query(typedCollection(collectionPath), orderBy("created_at")); return onSnapshot( q, (snap) => { onData(snap.docs.map((d) => d.data())); }, onError, ); } export async function createParticle( collectionPath: string, type: T, properties: ParticlePropertiesMap[T], createdByEmail: string, visibleTo: string[], ): Promise { const particle: Particle = ParticleSchema.parse({ id: "", // ignored by toFirestore, but needed to satisfy the type type, properties, created_at: new Date(), created_by_email: createdByEmail, updated_at: null, visible_to: visibleTo, }); const ref = await addDoc(typedCollection(collectionPath), particle); return ref.id; } // This allows updating properties without overwriting the entire properties object export async function updateParticle( docPath: string, properties: Partial, visibleTo?: string[], ): Promise { const particleRef = typedDoc(docPath); // Take the partial and create a new object with dot notation // e.g. { title: "New Title" } becomes { "properties.title": "New Title" } const updatedProperties: Record = {}; for (const key in properties) { updatedProperties[`properties.${key}`] = properties[key]; } await updateDoc(particleRef, { ...updatedProperties, updated_at: serverTimestamp(), ...(visibleTo ? { visible_to: visibleTo } : {}), }); }