@prmichaelsen/acp-visualizer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/agent/commands/acp.clarification-address.md +417 -0
- package/agent/commands/acp.clarification-capture.md +386 -0
- package/agent/commands/acp.clarification-create.md +437 -0
- package/agent/commands/acp.clarifications-research.md +326 -0
- package/agent/commands/acp.command-create.md +432 -0
- package/agent/commands/acp.design-create.md +286 -0
- package/agent/commands/acp.design-reference.md +355 -0
- package/agent/commands/acp.handoff.md +270 -0
- package/agent/commands/acp.index.md +423 -0
- package/agent/commands/acp.init.md +546 -0
- package/agent/commands/acp.package-create.md +895 -0
- package/agent/commands/acp.package-info.md +212 -0
- package/agent/commands/acp.package-install.md +539 -0
- package/agent/commands/acp.package-list.md +280 -0
- package/agent/commands/acp.package-publish.md +541 -0
- package/agent/commands/acp.package-remove.md +293 -0
- package/agent/commands/acp.package-search.md +307 -0
- package/agent/commands/acp.package-update.md +361 -0
- package/agent/commands/acp.package-validate.md +540 -0
- package/agent/commands/acp.pattern-create.md +386 -0
- package/agent/commands/acp.plan.md +587 -0
- package/agent/commands/acp.proceed.md +882 -0
- package/agent/commands/acp.project-create.md +675 -0
- package/agent/commands/acp.project-info.md +312 -0
- package/agent/commands/acp.project-list.md +226 -0
- package/agent/commands/acp.project-remove.md +379 -0
- package/agent/commands/acp.project-set.md +227 -0
- package/agent/commands/acp.project-update.md +307 -0
- package/agent/commands/acp.projects-restore.md +228 -0
- package/agent/commands/acp.projects-sync.md +347 -0
- package/agent/commands/acp.report.md +407 -0
- package/agent/commands/acp.resume.md +239 -0
- package/agent/commands/acp.sessions.md +301 -0
- package/agent/commands/acp.status.md +293 -0
- package/agent/commands/acp.sync.md +364 -0
- package/agent/commands/acp.task-create.md +500 -0
- package/agent/commands/acp.update.md +302 -0
- package/agent/commands/acp.validate.md +466 -0
- package/agent/commands/acp.version-check-for-updates.md +276 -0
- package/agent/commands/acp.version-check.md +191 -0
- package/agent/commands/acp.version-update.md +289 -0
- package/agent/commands/command.template.md +339 -0
- package/agent/commands/git.commit.md +526 -0
- package/agent/commands/git.init.md +514 -0
- package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
- package/agent/commands/tanstack-cloudflare.tail.md +275 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/design.template.md +154 -0
- package/agent/design/local.dashboard-layout-routing.md +288 -0
- package/agent/design/local.data-model-yaml-parsing.md +310 -0
- package/agent/design/local.search-filtering.md +331 -0
- package/agent/design/local.server-api-auto-refresh.md +235 -0
- package/agent/design/local.table-tree-views.md +299 -0
- package/agent/design/local.visualizer-requirements.md +349 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/index/.gitkeep +0 -0
- package/agent/index/acp.core.yaml +137 -0
- package/agent/index/local.main.template.yaml +37 -0
- package/agent/manifest.template.yaml +13 -0
- package/agent/manifest.yaml +302 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
- package/agent/package.template.yaml +86 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/pattern.template.md +382 -0
- package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
- package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
- package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
- package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
- package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
- package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
- package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
- package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
- package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
- package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
- package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
- package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
- package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
- package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
- package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
- package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
- package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
- package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
- package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
- package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
- package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
- package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
- package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
- package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
- package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
- package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
- package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
- package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
- package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
- package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
- package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
- package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
- package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
- package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
- package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
- package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
- package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
- package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
- package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
- package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
- package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
- package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
- package/agent/progress.template.yaml +161 -0
- package/agent/progress.yaml +145 -0
- package/agent/schemas/package.schema.yaml +276 -0
- package/agent/scripts/acp.common.sh +1781 -0
- package/agent/scripts/acp.install.sh +333 -0
- package/agent/scripts/acp.package-create.sh +924 -0
- package/agent/scripts/acp.package-info.sh +288 -0
- package/agent/scripts/acp.package-install.sh +893 -0
- package/agent/scripts/acp.package-list.sh +311 -0
- package/agent/scripts/acp.package-publish.sh +420 -0
- package/agent/scripts/acp.package-remove.sh +348 -0
- package/agent/scripts/acp.package-search.sh +156 -0
- package/agent/scripts/acp.package-update.sh +517 -0
- package/agent/scripts/acp.package-validate.sh +1018 -0
- package/agent/scripts/acp.uninstall.sh +85 -0
- package/agent/scripts/acp.version-check-for-updates.sh +98 -0
- package/agent/scripts/acp.version-check.sh +47 -0
- package/agent/scripts/acp.version-update.sh +176 -0
- package/agent/scripts/acp.yaml-parser.sh +985 -0
- package/agent/scripts/acp.yaml-validate.sh +205 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
- package/agent/tasks/task-1-{title}.template.md +244 -0
- package/bin/visualize.mjs +84 -0
- package/package.json +48 -0
- package/src/components/ExtraFieldsBadge.tsx +15 -0
- package/src/components/FilterBar.tsx +33 -0
- package/src/components/Header.tsx +23 -0
- package/src/components/MilestoneTable.tsx +167 -0
- package/src/components/MilestoneTree.tsx +84 -0
- package/src/components/ProgressBar.tsx +20 -0
- package/src/components/SearchInput.tsx +22 -0
- package/src/components/Sidebar.tsx +54 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/components/StatusDot.tsx +12 -0
- package/src/components/TaskList.tsx +36 -0
- package/src/components/ViewToggle.tsx +31 -0
- package/src/lib/config.ts +8 -0
- package/src/lib/file-watcher.ts +43 -0
- package/src/lib/search.ts +48 -0
- package/src/lib/types.ts +73 -0
- package/src/lib/useAutoRefresh.ts +31 -0
- package/src/lib/useCollapse.ts +31 -0
- package/src/lib/useFilteredData.ts +55 -0
- package/src/lib/yaml-loader-real.spec.ts +47 -0
- package/src/lib/yaml-loader.spec.ts +201 -0
- package/src/lib/yaml-loader.ts +265 -0
- package/src/routeTree.gen.ts +140 -0
- package/src/router.tsx +10 -0
- package/src/routes/__root.tsx +75 -0
- package/src/routes/api/watch.ts +29 -0
- package/src/routes/index.tsx +115 -0
- package/src/routes/milestones.tsx +50 -0
- package/src/routes/search.tsx +84 -0
- package/src/routes/tasks.tsx +63 -0
- package/src/services/progress-database.service.ts +46 -0
- package/src/styles.css +25 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +16 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
# Firebase Firestore
|
|
2
|
+
|
|
3
|
+
**Category**: Code
|
|
4
|
+
**Applicable To**: All Firestore CRUD operations, DatabaseService classes, collection path helpers, query patterns, and SSR/API data access
|
|
5
|
+
**Status**: Stable
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This pattern covers how Firestore is used via `@prmichaelsen/firebase-admin-sdk-v8` for all database operations: initialization, CRUD via static DatabaseService classes, centralized collection path helpers, query building (where, orderBy, pagination, chunked `in`), Zod validation on all reads, and the two contexts where Firestore is accessed (API route handlers and SSR `beforeLoad`). There are 37+ DatabaseService classes following this pattern.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## When to Use This Pattern
|
|
16
|
+
|
|
17
|
+
**Use this pattern when:**
|
|
18
|
+
- Creating a new DatabaseService for a Firestore entity
|
|
19
|
+
- Adding CRUD operations to an existing service
|
|
20
|
+
- Writing queries with filters, ordering, or pagination
|
|
21
|
+
- Accessing Firestore in API routes or SSR `beforeLoad`
|
|
22
|
+
- Adding new collection paths
|
|
23
|
+
|
|
24
|
+
**Don't use this pattern when:**
|
|
25
|
+
- Building client-side API wrappers (use `{Domain}Service` — see library-services.md)
|
|
26
|
+
- Working with Firebase Storage (see `tanstack-cloudflare.firebase-storage`)
|
|
27
|
+
- Working with external databases (Weaviate, Algolia)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Core Principles
|
|
32
|
+
|
|
33
|
+
1. **Always Initialize**: Call `initFirebaseAdmin()` before any Firestore operation
|
|
34
|
+
2. **Always Validate Reads**: Every read uses Zod `safeParse()` — never return raw `any`
|
|
35
|
+
3. **Reads Return Null, Writes Throw**: Consistent error handling across all services
|
|
36
|
+
4. **ISO Timestamps**: Always `new Date().toISOString()` — never `Date.now()`
|
|
37
|
+
5. **Collection Helpers**: Import paths from `@/constant/collections` — never hardcode
|
|
38
|
+
6. **Static Methods Only**: No instances — all DatabaseService methods are `static`
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Implementation
|
|
43
|
+
|
|
44
|
+
### SDK Functions
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import {
|
|
48
|
+
getDocument, // Read single document by ID
|
|
49
|
+
setDocument, // Write/overwrite document (supports merge)
|
|
50
|
+
deleteDocument, // Delete document
|
|
51
|
+
addDocument, // Add with auto-generated or custom ID
|
|
52
|
+
queryDocuments, // Query with where, orderBy, limit, startAfter
|
|
53
|
+
updateDocument, // Atomic field updates (FieldValue operations)
|
|
54
|
+
FieldValue, // arrayRemove, arrayUnion, delete, serverTimestamp
|
|
55
|
+
} from '@prmichaelsen/firebase-admin-sdk-v8'
|
|
56
|
+
import type { QueryOptions } from '@prmichaelsen/firebase-admin-sdk-v8'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Collection Path Helpers
|
|
60
|
+
|
|
61
|
+
**File**: `src/constant/collections.ts`
|
|
62
|
+
|
|
63
|
+
**Platform-level collections** (no user scoping):
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
export const USERS = `${BASE}.users`
|
|
67
|
+
export const PUBLIC_PROFILES = `${BASE}.public-profiles`
|
|
68
|
+
export const RELATIONSHIPS = `${BASE}.relationships`
|
|
69
|
+
export const SCHEDULED_MESSAGES = `${BASE}.scheduled-messages`
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**User-scoped subcollections** (with helper functions):
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
export function getUserProfileCollection(userId: string): string {
|
|
76
|
+
return `${BASE}.users/${userId}/profile`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getUserConversations(userId: string): string {
|
|
80
|
+
return `${BASE}.users/${userId}/conversations`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getUserConversationMessages(userId: string, conversationId: string): string {
|
|
84
|
+
return `${BASE}.users/${userId}/conversations/${conversationId}/messages`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getUserNotificationsCollection(userId: string): string {
|
|
88
|
+
return `${BASE}.users/${userId}/notifications`
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Shared collections** (multi-user, not user-scoped):
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
export function getSharedConversations(): string {
|
|
96
|
+
return `${BASE}.conversations`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getSharedConversationMessages(conversationId: string): string {
|
|
100
|
+
return `${BASE}.conversations/${conversationId}/messages`
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### CRUD Operations
|
|
105
|
+
|
|
106
|
+
#### Read — `getDocument()`
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
static async getProfile(userId: string): Promise<UserProfile | null> {
|
|
110
|
+
try {
|
|
111
|
+
const collection = getUserProfileCollection(userId)
|
|
112
|
+
const doc = await getDocument(collection, PROFILE_DOC_ID)
|
|
113
|
+
if (!doc) return null
|
|
114
|
+
|
|
115
|
+
const result = UserProfileSchema.safeParse(doc)
|
|
116
|
+
if (!result.success) {
|
|
117
|
+
console.error('[ProfileDatabaseService] Invalid data:', result.error)
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
return result.data
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('[ProfileDatabaseService] Failed to get:', error)
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Create — `addDocument()`
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
static async createConversation(userId: string, data: CreateConversationInput): Promise<Conversation> {
|
|
132
|
+
const collection = getUserConversations(userId)
|
|
133
|
+
const now = new Date().toISOString()
|
|
134
|
+
|
|
135
|
+
const conversation = {
|
|
136
|
+
title: data.title,
|
|
137
|
+
type: data.type ?? 'chat',
|
|
138
|
+
created_at: now,
|
|
139
|
+
updated_at: now,
|
|
140
|
+
message_count: 0,
|
|
141
|
+
last_message_preview: '',
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const docRef = await addDocument(collection, conversation)
|
|
145
|
+
return { id: docRef.id, user_id: userId, ...conversation }
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Optional custom document ID:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const docRef = await addDocument(collection, data, customDocumentId)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Write — `setDocument()` (Full or Merge)
|
|
156
|
+
|
|
157
|
+
Full overwrite:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
await setDocument(collection, docId, data)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Merge (partial update):
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
await setDocument(collection, conversationId, {
|
|
167
|
+
title,
|
|
168
|
+
updated_at: new Date().toISOString(),
|
|
169
|
+
}, { merge: true })
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Selective merge (recommended for nested objects):
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
await setDocument(collection, conversationId, {
|
|
176
|
+
title,
|
|
177
|
+
updated_at: new Date().toISOString(),
|
|
178
|
+
}, { mergeFields: ['title', 'updated_at'] })
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Update — `updateDocument()` (FieldValue operations)
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { FieldValue } from '@prmichaelsen/firebase-admin-sdk-v8'
|
|
185
|
+
|
|
186
|
+
// Remove element from array
|
|
187
|
+
await updateDocument(sharedPath, doc.id, {
|
|
188
|
+
participant_user_ids: FieldValue.arrayRemove(userId),
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Add element to array (no duplicates)
|
|
192
|
+
await updateDocument(collection, docId, {
|
|
193
|
+
tags: FieldValue.arrayUnion('new-tag'),
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### Delete — `deleteDocument()`
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
static async deleteProfile(userId: string): Promise<void> {
|
|
201
|
+
const collection = getUserProfileCollection(userId)
|
|
202
|
+
await deleteDocument(collection, PROFILE_DOC_ID)
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Query Patterns
|
|
207
|
+
|
|
208
|
+
#### Simple WHERE
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const docs = await queryDocuments(collection, {
|
|
212
|
+
where: [{ field: 'status', op: '==', value: 'pending' }],
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### WHERE + ORDER + LIMIT
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const options: QueryOptions = {
|
|
220
|
+
where: [
|
|
221
|
+
{ field: 'status', op: '==', value: 'pending' },
|
|
222
|
+
{ field: 'scheduled_at', op: '<=', value: cutoffTime },
|
|
223
|
+
],
|
|
224
|
+
orderBy: [{ field: 'scheduled_at', direction: 'ASCENDING' }],
|
|
225
|
+
limit: 10,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const docs = await queryDocuments(SCHEDULED_MESSAGES, options)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### Cursor Pagination with `startAfter`
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const options: QueryOptions = {
|
|
235
|
+
orderBy: [{ field: 'timestamp', direction: 'DESCENDING' }],
|
|
236
|
+
limit,
|
|
237
|
+
}
|
|
238
|
+
if (startAfter) options.startAfter = [startAfter]
|
|
239
|
+
|
|
240
|
+
const results = await queryDocuments(messagesCollection, options)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
#### Chunked `in` Queries (30-item limit)
|
|
244
|
+
|
|
245
|
+
Firestore `in` operator supports max 30 values. Chunk and parallelize:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
const chunks: string[][] = []
|
|
249
|
+
for (let i = 0; i < ids.length; i += 30) {
|
|
250
|
+
chunks.push(ids.slice(i, i + 30))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const results = await Promise.all(
|
|
254
|
+
chunks.map(async (chunk) => {
|
|
255
|
+
const docs = await queryDocuments(collection, {
|
|
256
|
+
where: [{ field: 'message_id', op: 'in', value: chunk }],
|
|
257
|
+
orderBy: [{ field: 'sequence_number', direction: 'ASCENDING' }],
|
|
258
|
+
})
|
|
259
|
+
return docs.map(doc => ({ id: doc.id, ...doc.data }))
|
|
260
|
+
})
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
const all = results.flat()
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
#### Simulated OR (Multiple Queries + Merge)
|
|
267
|
+
|
|
268
|
+
Firestore has no OR operator. Run separate queries and merge:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// Query 1: Public messages (visible_to_user_ids is null)
|
|
272
|
+
const publicDocs = await queryDocuments(messagesCollection, {
|
|
273
|
+
orderBy: [{ field: 'timestamp', direction: 'DESCENDING' }],
|
|
274
|
+
limit,
|
|
275
|
+
where: [{ field: 'visible_to_user_ids', op: '==', value: null }],
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
// Query 2: Messages visible to this user
|
|
279
|
+
const privateDocs = await queryDocuments(messagesCollection, {
|
|
280
|
+
orderBy: [{ field: 'timestamp', direction: 'DESCENDING' }],
|
|
281
|
+
limit,
|
|
282
|
+
where: [{ field: 'visible_to_user_ids', op: 'array-contains', value: userId }],
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Merge, dedupe, re-sort
|
|
286
|
+
const merged = [...publicDocs, ...privateDocs]
|
|
287
|
+
const deduped = Array.from(new Map(merged.map(d => [d.id, d])).values())
|
|
288
|
+
const sorted = deduped.sort((a, b) =>
|
|
289
|
+
new Date(b.data.timestamp).getTime() - new Date(a.data.timestamp).getTime()
|
|
290
|
+
).slice(0, limit)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Query Result Parsing
|
|
294
|
+
|
|
295
|
+
Always validate query results with Zod and filter invalid documents:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
const results = await queryDocuments(collection, options)
|
|
299
|
+
|
|
300
|
+
const valid = results
|
|
301
|
+
.map(doc => {
|
|
302
|
+
const result = EntitySchema.safeParse({ ...doc.data, id: doc.id })
|
|
303
|
+
if (!result.success) {
|
|
304
|
+
console.error('[ServiceName] Invalid data:', result.error)
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
return result.data
|
|
308
|
+
})
|
|
309
|
+
.filter((item): item is Entity => item !== null)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Document ID Patterns
|
|
313
|
+
|
|
314
|
+
| Strategy | Example | Use Case |
|
|
315
|
+
|---|---|---|
|
|
316
|
+
| Fixed ID | `'default'`, `'current'` | Singleton documents (profile, subscription, usage) |
|
|
317
|
+
| Provider-keyed | `'instagram'`, `'github'` | One doc per OAuth provider |
|
|
318
|
+
| Composite | `${userId}_${memoryId}` | Cross-entity lookups |
|
|
319
|
+
| Auto-generated | `addDocument(coll, data)` | Most entities (conversations, messages, notifications) |
|
|
320
|
+
|
|
321
|
+
### Firestore in SSR vs API Routes
|
|
322
|
+
|
|
323
|
+
#### SSR `beforeLoad`
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
export const Route = createFileRoute('/some-page')({
|
|
327
|
+
beforeLoad: (async () => {
|
|
328
|
+
const user = await getAuthSession()
|
|
329
|
+
if (!user) throw redirect({ to: '/auth' })
|
|
330
|
+
|
|
331
|
+
let preloadData = null
|
|
332
|
+
if (typeof window === 'undefined') {
|
|
333
|
+
initFirebaseAdmin()
|
|
334
|
+
preloadData = await SomeDatabaseService.getData(user.uid)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return { initialUser: user, preloadData }
|
|
338
|
+
}) as any,
|
|
339
|
+
})
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
#### API Routes
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
GET: async () => {
|
|
346
|
+
initFirebaseAdmin()
|
|
347
|
+
|
|
348
|
+
const user = await getAuthSession()
|
|
349
|
+
if (!user) return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })
|
|
350
|
+
|
|
351
|
+
const data = await SomeDatabaseService.getData(user.uid)
|
|
352
|
+
return new Response(JSON.stringify({ data }), {
|
|
353
|
+
status: 200,
|
|
354
|
+
headers: { 'Content-Type': 'application/json' },
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
#### Fire-and-Forget (Non-Critical Operations)
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// Don't block page load on optional operations
|
|
363
|
+
ProfileViewDatabaseService.trackProfileView(user.uid, targetUserId).catch(() => {})
|
|
364
|
+
|
|
365
|
+
// Dynamic import for optional services
|
|
366
|
+
import('./algolia.service').then(({ AlgoliaService }) =>
|
|
367
|
+
AlgoliaService.syncToIndex(id, data)
|
|
368
|
+
).catch(() => {})
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Denormalization Patterns
|
|
372
|
+
|
|
373
|
+
#### Bidirectional Index
|
|
374
|
+
|
|
375
|
+
Write index entries for both users in a relationship:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
const index1 = getUserRelationshipIndexCollection(userId1)
|
|
379
|
+
const index2 = getUserRelationshipIndexCollection(userId2)
|
|
380
|
+
|
|
381
|
+
await setDocument(index1, userId2, { related_user_id: userId2, relationship_id: id, flags })
|
|
382
|
+
await setDocument(index2, userId1, { related_user_id: userId1, relationship_id: id, flags })
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
#### Bulk Delete with Pagination
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
let hasMore = true
|
|
389
|
+
while (hasMore) {
|
|
390
|
+
const docs = await queryDocuments(collectionPath, { limit: 500 })
|
|
391
|
+
for (const doc of docs) {
|
|
392
|
+
await deleteDocument(collectionPath, doc.id)
|
|
393
|
+
}
|
|
394
|
+
hasMore = docs.length === 500
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Complex Object Serialization
|
|
399
|
+
|
|
400
|
+
Firestore doesn't support deeply nested arbitrary objects well. Serialize complex inputs:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
const toolCallData = {
|
|
404
|
+
tool_name: toolCall.tool_name,
|
|
405
|
+
timestamp: toolCall.timestamp.toISOString(),
|
|
406
|
+
// Serialize to prevent nested object issues
|
|
407
|
+
inputs: typeof toolCall.inputs === 'string'
|
|
408
|
+
? toolCall.inputs
|
|
409
|
+
: JSON.stringify(toolCall.inputs),
|
|
410
|
+
output: typeof toolCall.output === 'string'
|
|
411
|
+
? toolCall.output
|
|
412
|
+
: JSON.stringify(toolCall.output),
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Anti-Patterns
|
|
419
|
+
|
|
420
|
+
### Returning Unvalidated Data
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// Bad: Returns raw Firestore data as any
|
|
424
|
+
const doc = await getDocument(collection, id)
|
|
425
|
+
return doc
|
|
426
|
+
|
|
427
|
+
// Good: Always validate with Zod
|
|
428
|
+
const result = EntitySchema.safeParse(doc)
|
|
429
|
+
if (!result.success) return null
|
|
430
|
+
return result.data
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Hardcoded Collection Paths
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
// Bad
|
|
437
|
+
const doc = await getDocument('agentbase.users/' + userId + '/profile', 'default')
|
|
438
|
+
|
|
439
|
+
// Good
|
|
440
|
+
const collection = getUserProfileCollection(userId)
|
|
441
|
+
const doc = await getDocument(collection, PROFILE_DOC_ID)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### Using `merge: true` with Nested Objects
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// Bad: Can cause wildcard field issues with nested objects
|
|
448
|
+
await setDocument(coll, id, { nested: { a: 1 } }, { merge: true })
|
|
449
|
+
|
|
450
|
+
// Good: Use mergeFields for explicit control
|
|
451
|
+
await setDocument(coll, id, { nested: { a: 1 } }, { mergeFields: ['nested'] })
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Unbounded Queries
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// Bad: Could return millions of documents
|
|
458
|
+
const all = await queryDocuments(collection, {})
|
|
459
|
+
|
|
460
|
+
// Good: Always limit
|
|
461
|
+
const docs = await queryDocuments(collection, { limit: 500 })
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Numeric Timestamps
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
// Bad
|
|
468
|
+
const now = Date.now()
|
|
469
|
+
|
|
470
|
+
// Good
|
|
471
|
+
const now = new Date().toISOString()
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### `in` Queries Over 30 Items
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// Bad: Firestore rejects in queries with >30 values
|
|
478
|
+
const docs = await queryDocuments(coll, {
|
|
479
|
+
where: [{ field: 'id', op: 'in', value: hundredIds }],
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
// Good: Chunk into batches of 30
|
|
483
|
+
const chunks = []
|
|
484
|
+
for (let i = 0; i < ids.length; i += 30) chunks.push(ids.slice(i, i + 30))
|
|
485
|
+
const results = await Promise.all(chunks.map(chunk =>
|
|
486
|
+
queryDocuments(coll, { where: [{ field: 'id', op: 'in', value: chunk }] })
|
|
487
|
+
))
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Key Design Decisions
|
|
493
|
+
|
|
494
|
+
### Data Model
|
|
495
|
+
|
|
496
|
+
| Decision | Choice | Rationale |
|
|
497
|
+
|---|---|---|
|
|
498
|
+
| User data scoping | User-scoped subcollections | Natural security boundary; collection-group queries still possible |
|
|
499
|
+
| DM/group conversations | Shared collection (not user-scoped) | Multiple participants read/write same messages |
|
|
500
|
+
| Relationship data | Global + per-user index | Global doc is source of truth; index enables fast per-user queries |
|
|
501
|
+
| Document ID strategy | Auto-generated (default), fixed for singletons | Auto-gen prevents conflicts; fixed IDs simplify lookups |
|
|
502
|
+
|
|
503
|
+
### Query Patterns
|
|
504
|
+
|
|
505
|
+
| Decision | Choice | Rationale |
|
|
506
|
+
|---|---|---|
|
|
507
|
+
| OR queries | Multiple queries + merge | Firestore has no OR operator |
|
|
508
|
+
| Large `in` queries | Chunk at 30, parallelize | Firestore limit; Promise.all for speed |
|
|
509
|
+
| Pagination | Cursor-based (startAfter) | More reliable than offset for real-time data |
|
|
510
|
+
| Sort direction | Specified in QueryOptions | Prevents relying on default (undefined) ordering |
|
|
511
|
+
|
|
512
|
+
### Error Handling
|
|
513
|
+
|
|
514
|
+
| Decision | Choice | Rationale |
|
|
515
|
+
|---|---|---|
|
|
516
|
+
| Read errors | Return null / empty array | Callers handle gracefully; page still renders |
|
|
517
|
+
| Write errors | Throw | Callers need to know writes failed for user feedback |
|
|
518
|
+
| Validation errors | Log + return null | Invalid data in Firestore is non-fatal; logged for debugging |
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Checklist for Implementation
|
|
523
|
+
|
|
524
|
+
- [ ] `initFirebaseAdmin()` called before any Firestore operation
|
|
525
|
+
- [ ] Collection path uses helper from `@/constant/collections`
|
|
526
|
+
- [ ] All reads validated with Zod `safeParse()` — no raw `doc as Type`
|
|
527
|
+
- [ ] Reads return `null` on error; writes throw on error
|
|
528
|
+
- [ ] Timestamps use `new Date().toISOString()`
|
|
529
|
+
- [ ] Logging uses `[ClassName]` prefix
|
|
530
|
+
- [ ] Queries include `limit` to prevent unbounded reads
|
|
531
|
+
- [ ] `in` queries chunked at 30 items
|
|
532
|
+
- [ ] `mergeFields` used instead of `merge: true` for nested objects
|
|
533
|
+
- [ ] New collection paths added to `src/constant/collections.ts`
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
## Related Patterns
|
|
538
|
+
|
|
539
|
+
- **[Database Service Conventions](./database-service-conventions.md)**: Naming, structure, and testing conventions for DatabaseService classes
|
|
540
|
+
- **[Zod Schema Conventions](./zod-schema-conventions.md)**: Schema definitions consumed by DatabaseServices for `safeParse`
|
|
541
|
+
- **[Firebase Auth](./tanstack-cloudflare.firebase-auth.md)**: Auth-verified userId flows into all database calls
|
|
542
|
+
- **[SSR Preload](./ssr-preload.md)**: SSR `beforeLoad` calls DatabaseServices with `typeof window === 'undefined'` guard
|
|
543
|
+
- **[Entity Blueprint](./entity-blueprint.md)**: End-to-end recipe for creating a new entity with schema, service, API, and UI
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
**Status**: Stable
|
|
548
|
+
**Recommendation**: Follow this pattern for all Firestore operations. Always use DatabaseService classes — never call SDK functions directly from routes or components.
|
|
549
|
+
**Last Updated**: 2026-03-14
|
|
550
|
+
**Contributors**: Community
|