refactor: extract properties for container particles to flat fields in firestore

This commit is contained in:
talksik
2026-03-19 09:05:12 -07:00
parent c36d72493d
commit 1bf5b466d1
8 changed files with 138 additions and 40 deletions
+81 -11
View File
@@ -17,7 +17,7 @@ import {
type Unsubscribe,
} from "firebase/firestore";
import { firestoreDb } from "@/firebase";
import { ParticleSchema } from "@/api/types";
import { isContainerType, ParticleSchema } from "@/api/types";
import type { Particle, ParticleType, ParticlePropertiesMap } from "@/api/types";
// --- Converter ---
@@ -36,15 +36,55 @@ const particleConverter: FirestoreDataConverter<Particle> = {
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,
});
if (typeof raw.type !== "string") {
throw new Error(`Invalid particle type: ${raw.type}`);
}
const type = raw.type as ParticleType;
switch (type) {
case "stream":
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,
markers: raw.markers
? Object.fromEntries(
Object.entries(raw.markers).map(([key, value]) => [
key,
(value as Timestamp).toDate(),
]),
)
: undefined,
});
case "folder":
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,
});
case "media":
case "file":
case "text":
case "quest":
case "paper":
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,
});
default:
throw new Error(`Unknown particle type: ${type}`);
}
},
};
@@ -123,20 +163,29 @@ export async function createParticle<T extends ParticleType>(
type: T,
properties: ParticlePropertiesMap[T],
createdByEmail: string,
// Must be passed for container types
visibleTo?: string[],
): Promise<string> {
if (isContainerType(type) && (!visibleTo || visibleTo.length === 0)) {
throw new Error(
`visibleTo is required for container type ${type} and cannot be empty`,
);
}
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,
...(visibleTo ? { 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>(
export async function updateParticleProperties<T extends ParticleType>(
docPath: string,
properties: Partial<ParticlePropertiesMap[T]>,
): Promise<void> {
@@ -152,3 +201,24 @@ export async function updateParticle<T extends ParticleType>(
updated_at: serverTimestamp(),
});
}
export async function updateParticleVisibleTo(
docPath: string,
visibleTo: string[],
): Promise<void> {
const particleRef = typedDoc(docPath);
const particle = await getParticle(docPath);
if (!particle) {
throw new Error(`Particle not found at path: ${docPath}`);
}
if (!isContainerType(particle.type)) {
throw new Error(
`Only container particles can have visible_to field. Particle at ${docPath} is of type ${particle.type}`,
);
}
await updateDoc(particleRef, {
visible_to: visibleTo,
updated_at: serverTimestamp(),
});
}