129 lines
3.5 KiB
TypeScript
129 lines
3.5 KiB
TypeScript
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<Particle> = {
|
|
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<T extends ParticleType>(
|
|
collectionPath: string,
|
|
type: T,
|
|
properties: ParticlePropertiesMap[T],
|
|
createdByEmail: string,
|
|
visibleTo: string[],
|
|
): Promise<string> {
|
|
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<T extends ParticleType>(
|
|
docPath: string,
|
|
properties: Partial<ParticlePropertiesMap[T]>,
|
|
visibleTo?: string[],
|
|
): Promise<void> {
|
|
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<string, any> = {};
|
|
for (const key in properties) {
|
|
updatedProperties[`properties.${key}`] = properties[key];
|
|
}
|
|
await updateDoc(particleRef, {
|
|
...updatedProperties,
|
|
updated_at: serverTimestamp(),
|
|
...(visibleTo ? { visible_to: visibleTo } : {}),
|
|
});
|
|
}
|