@prmichaelsen/remember-mcp 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/.env.example +65 -0
- package/AGENT.md +840 -0
- package/README.md +72 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/access-control-result-pattern.md +458 -0
- package/agent/design/action-audit-memory-types.md +637 -0
- package/agent/design/common-template-fields.md +282 -0
- package/agent/design/complete-tool-set.md +407 -0
- package/agent/design/content-types-expansion.md +521 -0
- package/agent/design/cross-database-id-strategy.md +358 -0
- package/agent/design/default-template-library.md +423 -0
- package/agent/design/firestore-wrapper-analysis.md +606 -0
- package/agent/design/llm-provider-abstraction.md +691 -0
- package/agent/design/location-handling-architecture.md +523 -0
- package/agent/design/memory-templates-design.md +364 -0
- package/agent/design/permissions-storage-architecture.md +680 -0
- package/agent/design/relationship-storage-strategy.md +361 -0
- package/agent/design/remember-mcp-implementation-tasks.md +417 -0
- package/agent/design/remember-mcp-progress.yaml +141 -0
- package/agent/design/requirements-enhancements.md +468 -0
- package/agent/design/requirements.md +56 -0
- package/agent/design/template-storage-strategy.md +412 -0
- package/agent/design/template-suggestion-system.md +853 -0
- package/agent/design/trust-escalation-prevention.md +343 -0
- package/agent/design/trust-system-implementation.md +592 -0
- package/agent/design/user-preferences.md +683 -0
- package/agent/design/weaviate-collection-strategy.md +461 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-foundation.md +121 -0
- package/agent/milestones/milestone-2-core-memory-system.md +150 -0
- package/agent/milestones/milestone-3-relationships-graph.md +116 -0
- package/agent/milestones/milestone-4-user-preferences.md +103 -0
- package/agent/milestones/milestone-5-template-system.md +126 -0
- package/agent/milestones/milestone-6-auth-multi-tenancy.md +124 -0
- package/agent/milestones/milestone-7-trust-permissions.md +133 -0
- package/agent/milestones/milestone-8-testing-quality.md +137 -0
- package/agent/milestones/milestone-9-deployment-documentation.md +147 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.md +1271 -0
- package/agent/patterns/firebase-admin-sdk-v8-usage.md +950 -0
- package/agent/patterns/firestore-users-pattern-best-practices.md +347 -0
- package/agent/patterns/library-services.md +454 -0
- package/agent/patterns/testing-colocated.md +316 -0
- package/agent/progress.yaml +395 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/task-1-initialize-project-structure.md +266 -0
- package/agent/tasks/task-2-install-dependencies.md +199 -0
- package/agent/tasks/task-3-setup-weaviate-client.md +330 -0
- package/agent/tasks/task-4-setup-firestore-client.md +362 -0
- package/agent/tasks/task-5-create-basic-mcp-server.md +114 -0
- package/agent/tasks/task-6-create-integration-tests.md +195 -0
- package/agent/tasks/task-7-finalize-milestone-1.md +363 -0
- package/agent/tasks/task-8-setup-utility-scripts.md +382 -0
- package/agent/tasks/task-9-create-server-factory.md +404 -0
- package/dist/config.d.ts +26 -0
- package/dist/constants/content-types.d.ts +60 -0
- package/dist/firestore/init.d.ts +14 -0
- package/dist/firestore/paths.d.ts +53 -0
- package/dist/firestore/paths.spec.d.ts +2 -0
- package/dist/server-factory.d.ts +40 -0
- package/dist/server-factory.js +1741 -0
- package/dist/server-factory.spec.d.ts +2 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +1690 -0
- package/dist/tools/create-memory.d.ts +94 -0
- package/dist/tools/delete-memory.d.ts +47 -0
- package/dist/tools/search-memory.d.ts +88 -0
- package/dist/types/memory.d.ts +183 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/weaviate/client.d.ts +39 -0
- package/dist/weaviate/client.spec.d.ts +2 -0
- package/dist/weaviate/schema.d.ts +29 -0
- package/esbuild.build.js +60 -0
- package/esbuild.watch.js +25 -0
- package/jest.config.js +31 -0
- package/jest.e2e.config.js +17 -0
- package/package.json +68 -0
- package/src/.gitkeep +0 -0
- package/src/config.ts +56 -0
- package/src/constants/content-types.ts +454 -0
- package/src/firestore/init.ts +68 -0
- package/src/firestore/paths.spec.ts +75 -0
- package/src/firestore/paths.ts +124 -0
- package/src/server-factory.spec.ts +60 -0
- package/src/server-factory.ts +215 -0
- package/src/server.ts +243 -0
- package/src/tools/create-memory.ts +198 -0
- package/src/tools/delete-memory.ts +126 -0
- package/src/tools/search-memory.ts +216 -0
- package/src/types/memory.ts +276 -0
- package/src/utils/logger.ts +42 -0
- package/src/weaviate/client.spec.ts +58 -0
- package/src/weaviate/client.ts +114 -0
- package/src/weaviate/schema.ts +288 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
# Firestore Client Wrapper - Do We Need It?
|
|
2
|
+
|
|
3
|
+
**Question**: Should we create a Firestore client wrapper, or use firebase-admin-sdk-v8 directly?
|
|
4
|
+
**Created**: 2026-02-11
|
|
5
|
+
**Status**: Design Analysis
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The Question
|
|
10
|
+
|
|
11
|
+
The `@prmichaelsen/firebase-admin-sdk-v8` library already provides:
|
|
12
|
+
- `getDocument()`, `setDocument()`, `addDocument()`, `updateDocument()`, `deleteDocument()`
|
|
13
|
+
- `queryDocuments()` with filters, ordering, pagination
|
|
14
|
+
- `batchWrite()` for atomic operations
|
|
15
|
+
- `FieldValue` operations (increment, arrayUnion, etc.)
|
|
16
|
+
- `verifyIdToken()` for authentication
|
|
17
|
+
|
|
18
|
+
**Do we need a wrapper?** Or should we use it directly?
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Analysis
|
|
23
|
+
|
|
24
|
+
### Option 1: Use firebase-admin-sdk-v8 Directly (No Wrapper)
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// In tools or services
|
|
28
|
+
import { getDocument, setDocument } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
29
|
+
|
|
30
|
+
export async function getUserPreferences(userId: string) {
|
|
31
|
+
return await getDocument('user_preferences', userId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function updatePreferences(userId: string, updates: any) {
|
|
35
|
+
await setDocument('user_preferences', userId, updates, { merge: true });
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Pros**:
|
|
40
|
+
- ✅ **Simpler** - No extra abstraction layer
|
|
41
|
+
- ✅ **Less code** - Fewer files to maintain
|
|
42
|
+
- ✅ **Direct access** - Use library features directly
|
|
43
|
+
- ✅ **Clear API** - Library API is already clean
|
|
44
|
+
- ✅ **No duplication** - Don't reimplement what library provides
|
|
45
|
+
|
|
46
|
+
**Cons**:
|
|
47
|
+
- ❌ **Scattered initialization** - `initializeApp()` called in multiple places
|
|
48
|
+
- ❌ **No centralized error handling** - Each call handles errors separately
|
|
49
|
+
- ❌ **No testing helpers** - No mock-friendly interface
|
|
50
|
+
- ❌ **No connection state** - Can't check if initialized
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Option 2: Minimal Wrapper (Initialization Only)
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// src/firestore/client.ts
|
|
58
|
+
import { initializeApp } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
59
|
+
import { config } from '../config.js';
|
|
60
|
+
|
|
61
|
+
let initialized = false;
|
|
62
|
+
|
|
63
|
+
export async function initFirestore(): Promise<void> {
|
|
64
|
+
if (initialized) return;
|
|
65
|
+
|
|
66
|
+
initializeApp({
|
|
67
|
+
serviceAccount: JSON.parse(config.firebase.serviceAccount),
|
|
68
|
+
projectId: config.firebase.projectId
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
initialized = true;
|
|
72
|
+
console.log('[Firestore] Initialized');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function isFirestoreInitialized(): boolean {
|
|
76
|
+
return initialized;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Re-export library functions for convenience
|
|
80
|
+
export {
|
|
81
|
+
getDocument,
|
|
82
|
+
setDocument,
|
|
83
|
+
addDocument,
|
|
84
|
+
updateDocument,
|
|
85
|
+
deleteDocument,
|
|
86
|
+
queryDocuments,
|
|
87
|
+
batchWrite,
|
|
88
|
+
FieldValue,
|
|
89
|
+
verifyIdToken
|
|
90
|
+
} from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Pros**:
|
|
94
|
+
- ✅ **Centralized initialization** - Single place to initialize
|
|
95
|
+
- ✅ **Connection state tracking** - Can check if initialized
|
|
96
|
+
- ✅ **Simple** - Just initialization + re-exports
|
|
97
|
+
- ✅ **Direct access** - Still use library functions directly
|
|
98
|
+
- ✅ **Easy testing** - Can mock initialization
|
|
99
|
+
|
|
100
|
+
**Cons**:
|
|
101
|
+
- ❌ **Minimal value** - Just wraps initialization
|
|
102
|
+
- ❌ **Extra import** - Need to import from wrapper instead of library
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### Option 3: Service Layer (No Client Wrapper)
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// src/services/user-preferences.service.ts
|
|
110
|
+
import { getDocument, setDocument } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
111
|
+
|
|
112
|
+
export class UserPreferencesService {
|
|
113
|
+
static async get(userId: string): Promise<UserPreferences> {
|
|
114
|
+
const doc = await getDocument('user_preferences', userId);
|
|
115
|
+
return doc || DEFAULT_PREFERENCES;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static async update(userId: string, updates: Partial<UserPreferences>): Promise<void> {
|
|
119
|
+
await setDocument('user_preferences', userId, {
|
|
120
|
+
...updates,
|
|
121
|
+
updated_at: new Date().toISOString()
|
|
122
|
+
}, { merge: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/services/permissions.service.ts
|
|
127
|
+
import { getDocument, setDocument, queryDocuments } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
128
|
+
|
|
129
|
+
export class PermissionsService {
|
|
130
|
+
static async grant(ownerUserId: string, accessorUserId: string, trustLevel: number): Promise<void> {
|
|
131
|
+
const path = `user_permissions/${ownerUserId}/allowed_accessors`;
|
|
132
|
+
await setDocument(path, accessorUserId, {
|
|
133
|
+
trust_level: trustLevel,
|
|
134
|
+
granted_at: new Date().toISOString()
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static async check(ownerUserId: string, accessorUserId: string): Promise<UserPermission | null> {
|
|
139
|
+
const path = `user_permissions/${ownerUserId}/allowed_accessors`;
|
|
140
|
+
return await getDocument(path, accessorUserId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Pros**:
|
|
146
|
+
- ✅ **Domain-focused** - Services organized by business logic
|
|
147
|
+
- ✅ **Type-safe** - Each service has typed methods
|
|
148
|
+
- ✅ **Testable** - Can mock services easily
|
|
149
|
+
- ✅ **Clear responsibilities** - Each service handles one domain
|
|
150
|
+
- ✅ **No wrapper needed** - Use library directly in services
|
|
151
|
+
- ✅ **Follows agentbase.me pattern** - Proven approach
|
|
152
|
+
|
|
153
|
+
**Cons**:
|
|
154
|
+
- ❌ **More files** - One service per domain
|
|
155
|
+
- ❌ **Initialization scattered** - Each service might call initializeApp
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Recommendation: Service Layer Pattern (Option 3)
|
|
160
|
+
|
|
161
|
+
### Why Service Layer is Better
|
|
162
|
+
|
|
163
|
+
**1. Firebase Admin SDK is Already a "Client"**
|
|
164
|
+
- The library provides clean functions: `getDocument()`, `setDocument()`, etc.
|
|
165
|
+
- No need to wrap what's already well-designed
|
|
166
|
+
- Adding a wrapper just adds indirection
|
|
167
|
+
|
|
168
|
+
**2. Security Rules Don't Apply to Admin SDK**
|
|
169
|
+
- Admin SDK **bypasses** Firestore security rules
|
|
170
|
+
- Security rules only apply to client SDKs (web, mobile)
|
|
171
|
+
- Admin SDK has full access to all data
|
|
172
|
+
- Security must be enforced in **application logic**, not database rules
|
|
173
|
+
|
|
174
|
+
**3. Service Layer Provides Better Organization**
|
|
175
|
+
- Groups related operations by domain
|
|
176
|
+
- Type-safe interfaces per service
|
|
177
|
+
- Clear business logic separation
|
|
178
|
+
- Easier to test and maintain
|
|
179
|
+
|
|
180
|
+
**4. Proven Pattern from agentbase.me**
|
|
181
|
+
- agentbase.me uses service classes successfully
|
|
182
|
+
- No Firestore client wrapper needed
|
|
183
|
+
- Services use firebase-admin-sdk-v8 directly
|
|
184
|
+
- Clean, maintainable code
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Proposed Architecture
|
|
189
|
+
|
|
190
|
+
### Initialization (Minimal Wrapper)
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// src/firestore/init.ts
|
|
194
|
+
import { initializeApp } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
195
|
+
import { config } from '../config.js';
|
|
196
|
+
|
|
197
|
+
let initialized = false;
|
|
198
|
+
|
|
199
|
+
export function initFirestore(): void {
|
|
200
|
+
if (initialized) return;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
initializeApp({
|
|
204
|
+
serviceAccount: JSON.parse(config.firebase.serviceAccount),
|
|
205
|
+
projectId: config.firebase.projectId
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
initialized = true;
|
|
209
|
+
console.log('[Firestore] Initialized');
|
|
210
|
+
} catch (error) {
|
|
211
|
+
console.error('[Firestore] Initialization failed:', error);
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function isFirestoreInitialized(): boolean {
|
|
217
|
+
return initialized;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Service Classes
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// src/services/user-preferences.service.ts
|
|
225
|
+
import { getDocument, setDocument } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
226
|
+
import type { UserPreferences } from '../types/preferences.js';
|
|
227
|
+
|
|
228
|
+
export class UserPreferencesService {
|
|
229
|
+
static async get(userId: string): Promise<UserPreferences> {
|
|
230
|
+
const doc = await getDocument('user_preferences', userId);
|
|
231
|
+
return doc || DEFAULT_PREFERENCES;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
static async update(userId: string, updates: Partial<UserPreferences>): Promise<void> {
|
|
235
|
+
await setDocument('user_preferences', userId, {
|
|
236
|
+
...updates,
|
|
237
|
+
updated_at: new Date().toISOString()
|
|
238
|
+
}, { merge: true });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/services/permissions.service.ts
|
|
243
|
+
import { getDocument, setDocument, queryDocuments } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
244
|
+
import type { UserPermission } from '../types/permissions.js';
|
|
245
|
+
|
|
246
|
+
export class PermissionsService {
|
|
247
|
+
static async grant(ownerUserId: string, accessorUserId: string, permission: UserPermission): Promise<void> {
|
|
248
|
+
const path = `user_permissions/${ownerUserId}/allowed_accessors`;
|
|
249
|
+
await setDocument(path, accessorUserId, permission);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static async check(ownerUserId: string, accessorUserId: string): Promise<UserPermission | null> {
|
|
253
|
+
const path = `user_permissions/${ownerUserId}/allowed_accessors`;
|
|
254
|
+
return await getDocument(path, accessorUserId);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
static async list(ownerUserId: string): Promise<UserPermission[]> {
|
|
258
|
+
const path = `user_permissions/${ownerUserId}/allowed_accessors`;
|
|
259
|
+
const results = await queryDocuments(path, {
|
|
260
|
+
where: [{ field: 'revoked', op: '!=', value: true }],
|
|
261
|
+
orderBy: [{ field: 'trust_level', direction: 'DESCENDING' }]
|
|
262
|
+
});
|
|
263
|
+
return results.map(doc => ({ id: doc.id, ...doc.data }));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/services/templates.service.ts
|
|
268
|
+
import { getDocument, setDocument, queryDocuments } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
269
|
+
import type { Template } from '../types/template.js';
|
|
270
|
+
|
|
271
|
+
export class TemplatesService {
|
|
272
|
+
static async getDefault(): Promise<Template[]> {
|
|
273
|
+
const results = await queryDocuments('templates/default', {
|
|
274
|
+
where: [{ field: 'is_default', op: '==', value: true }]
|
|
275
|
+
});
|
|
276
|
+
return results.map(doc => ({ id: doc.id, ...doc.data }));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
static async getUserTemplates(userId: string): Promise<Template[]> {
|
|
280
|
+
const path = `users/${userId}/templates`;
|
|
281
|
+
const results = await queryDocuments(path, {
|
|
282
|
+
orderBy: [{ field: 'created_at', direction: 'DESCENDING' }]
|
|
283
|
+
});
|
|
284
|
+
return results.map(doc => ({ id: doc.id, ...doc.data }));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Security Considerations
|
|
292
|
+
|
|
293
|
+
### Admin SDK Bypasses Security Rules
|
|
294
|
+
|
|
295
|
+
**Important**: Firebase Admin SDK has **full access** to all data, regardless of security rules.
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
// Firestore security rules (these DON'T apply to Admin SDK)
|
|
299
|
+
rules_version = '2';
|
|
300
|
+
service cloud.firestore {
|
|
301
|
+
match /databases/{database}/documents {
|
|
302
|
+
match /user_preferences/{userId} {
|
|
303
|
+
allow read, write: if request.auth.uid == userId; // ❌ Admin SDK bypasses this
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Security must be enforced in application code**:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
export class UserPreferencesService {
|
|
313
|
+
static async get(userId: string, requestingUserId: string): Promise<UserPreferences> {
|
|
314
|
+
// ✅ Enforce security in code
|
|
315
|
+
if (userId !== requestingUserId) {
|
|
316
|
+
throw new Error('Unauthorized: Cannot access another user\'s preferences');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return await getDocument('user_preferences', userId);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### When Security Rules Matter
|
|
325
|
+
|
|
326
|
+
Security rules **do matter** for:
|
|
327
|
+
- ✅ Firebase Client SDK (web, mobile apps)
|
|
328
|
+
- ✅ Direct Firestore REST API calls from clients
|
|
329
|
+
- ✅ Protection against compromised client apps
|
|
330
|
+
|
|
331
|
+
Security rules **don't matter** for:
|
|
332
|
+
- ❌ Firebase Admin SDK (full access)
|
|
333
|
+
- ❌ Server-side code with service account
|
|
334
|
+
- ❌ Our MCP server (uses Admin SDK)
|
|
335
|
+
|
|
336
|
+
**For remember-mcp**:
|
|
337
|
+
- We use Admin SDK on server → Security rules don't apply
|
|
338
|
+
- We must enforce security in **application logic**
|
|
339
|
+
- Trust system enforced in **code**, not database rules
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## Comparison with Weaviate
|
|
344
|
+
|
|
345
|
+
### Why We Have Weaviate Wrapper
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// Weaviate needs wrapper because:
|
|
349
|
+
export function getMemoryCollectionName(userId: string): string {
|
|
350
|
+
return `Memory_${sanitizeUserId(userId)}`; // ✅ Multi-tenant logic
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function sanitizeUserId(userId: string): string {
|
|
354
|
+
// ✅ Weaviate-specific naming rules
|
|
355
|
+
return userId.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Weaviate wrapper provides**:
|
|
360
|
+
- ✅ Multi-tenant collection naming
|
|
361
|
+
- ✅ User ID sanitization (Weaviate naming rules)
|
|
362
|
+
- ✅ Collection existence checking
|
|
363
|
+
- ✅ Connection state management
|
|
364
|
+
|
|
365
|
+
### Why Firestore Doesn't Need Wrapper
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
// Firestore paths are simple strings
|
|
369
|
+
const path = `user_preferences/${userId}`; // ✅ No sanitization needed
|
|
370
|
+
await getDocument(path, documentId); // ✅ Library API is clean
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Firestore doesn't need wrapper because**:
|
|
374
|
+
- ❌ No special naming rules (paths are just strings)
|
|
375
|
+
- ❌ No sanitization needed (Firebase handles all characters)
|
|
376
|
+
- ❌ Library API is already clean and simple
|
|
377
|
+
- ❌ Service layer provides better organization
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Final Recommendation
|
|
382
|
+
|
|
383
|
+
### ✅ DO: Use Service Layer Pattern
|
|
384
|
+
|
|
385
|
+
**Create domain-specific services**:
|
|
386
|
+
- `src/services/user-preferences.service.ts`
|
|
387
|
+
- `src/services/permissions.service.ts`
|
|
388
|
+
- `src/services/templates.service.ts`
|
|
389
|
+
- `src/services/trust-history.service.ts`
|
|
390
|
+
|
|
391
|
+
**Each service**:
|
|
392
|
+
- Uses firebase-admin-sdk-v8 functions directly
|
|
393
|
+
- Provides type-safe methods
|
|
394
|
+
- Handles domain-specific logic
|
|
395
|
+
- Enforces security in code
|
|
396
|
+
|
|
397
|
+
### ✅ DO: Minimal Initialization Helper
|
|
398
|
+
|
|
399
|
+
**Create simple init helper**:
|
|
400
|
+
- `src/firestore/init.ts` - Just initialization + state tracking
|
|
401
|
+
- Re-exports library functions for convenience
|
|
402
|
+
- No complex wrapper logic
|
|
403
|
+
|
|
404
|
+
### ❌ DON'T: Create Full Client Wrapper
|
|
405
|
+
|
|
406
|
+
**Don't create**:
|
|
407
|
+
- `FirestoreClientWrapper` class
|
|
408
|
+
- Methods that just proxy to library functions
|
|
409
|
+
- Unnecessary abstraction layer
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Updated Task 4 Approach
|
|
414
|
+
|
|
415
|
+
### What to Create
|
|
416
|
+
|
|
417
|
+
1. **src/firestore/init.ts** (minimal)
|
|
418
|
+
- `initFirestore()` - Initialize once
|
|
419
|
+
- `isFirestoreInitialized()` - Check state
|
|
420
|
+
- Re-export library functions
|
|
421
|
+
|
|
422
|
+
2. **src/services/** (domain services)
|
|
423
|
+
- `user-preferences.service.ts`
|
|
424
|
+
- `permissions.service.ts`
|
|
425
|
+
- `templates.service.ts`
|
|
426
|
+
|
|
427
|
+
3. **src/types/** (type definitions)
|
|
428
|
+
- `preferences.ts`
|
|
429
|
+
- `permissions.ts`
|
|
430
|
+
- `template.ts`
|
|
431
|
+
|
|
432
|
+
4. **tests/unit/** (service tests)
|
|
433
|
+
- `user-preferences.service.test.ts`
|
|
434
|
+
- `permissions.service.test.ts`
|
|
435
|
+
|
|
436
|
+
### What NOT to Create
|
|
437
|
+
|
|
438
|
+
- ❌ `src/firestore/client.ts` with full wrapper class
|
|
439
|
+
- ❌ Methods that just proxy library functions
|
|
440
|
+
- ❌ Complex abstraction layer
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Code Examples
|
|
445
|
+
|
|
446
|
+
### Minimal Init (Recommended)
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// src/firestore/init.ts
|
|
450
|
+
import { initializeApp } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
451
|
+
import { config } from '../config.js';
|
|
452
|
+
|
|
453
|
+
let initialized = false;
|
|
454
|
+
|
|
455
|
+
export function initFirestore(): void {
|
|
456
|
+
if (initialized) return;
|
|
457
|
+
|
|
458
|
+
initializeApp({
|
|
459
|
+
serviceAccount: JSON.parse(config.firebase.serviceAccount),
|
|
460
|
+
projectId: config.firebase.projectId
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
initialized = true;
|
|
464
|
+
console.log('[Firestore] Initialized');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function isFirestoreInitialized(): boolean {
|
|
468
|
+
return initialized;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Re-export for convenience
|
|
472
|
+
export {
|
|
473
|
+
getDocument,
|
|
474
|
+
setDocument,
|
|
475
|
+
addDocument,
|
|
476
|
+
updateDocument,
|
|
477
|
+
deleteDocument,
|
|
478
|
+
queryDocuments,
|
|
479
|
+
batchWrite,
|
|
480
|
+
FieldValue,
|
|
481
|
+
verifyIdToken,
|
|
482
|
+
type QueryOptions
|
|
483
|
+
} from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Service Layer (Recommended)
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
// src/services/user-preferences.service.ts
|
|
490
|
+
import { getDocument, setDocument } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
491
|
+
import type { UserPreferences } from '../types/preferences.js';
|
|
492
|
+
import { DEFAULT_PREFERENCES } from '../constants/defaults.js';
|
|
493
|
+
|
|
494
|
+
export class UserPreferencesService {
|
|
495
|
+
/**
|
|
496
|
+
* Get user preferences (returns defaults if not found)
|
|
497
|
+
*/
|
|
498
|
+
static async get(userId: string): Promise<UserPreferences> {
|
|
499
|
+
const doc = await getDocument('user_preferences', userId);
|
|
500
|
+
|
|
501
|
+
if (!doc) {
|
|
502
|
+
// Create with defaults
|
|
503
|
+
await this.create(userId);
|
|
504
|
+
return DEFAULT_PREFERENCES;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return doc as UserPreferences;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Update user preferences
|
|
512
|
+
*/
|
|
513
|
+
static async update(userId: string, updates: Partial<UserPreferences>): Promise<void> {
|
|
514
|
+
await setDocument('user_preferences', userId, {
|
|
515
|
+
...updates,
|
|
516
|
+
updated_at: new Date().toISOString()
|
|
517
|
+
}, { merge: true });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Create user preferences with defaults
|
|
522
|
+
*/
|
|
523
|
+
static async create(userId: string): Promise<void> {
|
|
524
|
+
await setDocument('user_preferences', userId, {
|
|
525
|
+
...DEFAULT_PREFERENCES,
|
|
526
|
+
user_id: userId,
|
|
527
|
+
created_at: new Date().toISOString(),
|
|
528
|
+
updated_at: new Date().toISOString()
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Comparison with agentbase.me
|
|
537
|
+
|
|
538
|
+
### agentbase.me Pattern (What They Do)
|
|
539
|
+
|
|
540
|
+
```typescript
|
|
541
|
+
// They use services directly, no wrapper
|
|
542
|
+
import { getDocument, setDocument } from '@prmichaelsen/firebase-admin-sdk-v8';
|
|
543
|
+
|
|
544
|
+
export class ConversationDatabaseService {
|
|
545
|
+
static async createConversation(userId: string, title: string) {
|
|
546
|
+
const path = getUserConversations(userId);
|
|
547
|
+
const docRef = await addDocument(path, { title, created_at: now });
|
|
548
|
+
return { id: docRef.id, ...conversation };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**They DON'T have**:
|
|
554
|
+
- ❌ Firestore client wrapper
|
|
555
|
+
- ❌ Abstraction layer over firebase-admin-sdk-v8
|
|
556
|
+
|
|
557
|
+
**They DO have**:
|
|
558
|
+
- ✅ Service classes for each domain
|
|
559
|
+
- ✅ Collection path helpers
|
|
560
|
+
- ✅ Direct use of library functions
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Conclusion
|
|
565
|
+
|
|
566
|
+
### ✅ Recommended Approach
|
|
567
|
+
|
|
568
|
+
**Use firebase-admin-sdk-v8 directly through service layer**:
|
|
569
|
+
|
|
570
|
+
1. **Minimal init helper** (`src/firestore/init.ts`)
|
|
571
|
+
- Just initialization + state tracking
|
|
572
|
+
- Re-export library functions
|
|
573
|
+
|
|
574
|
+
2. **Service classes** (`src/services/*.service.ts`)
|
|
575
|
+
- Domain-specific logic
|
|
576
|
+
- Type-safe methods
|
|
577
|
+
- Direct use of library functions
|
|
578
|
+
|
|
579
|
+
3. **No client wrapper**
|
|
580
|
+
- Library API is already clean
|
|
581
|
+
- Wrapper adds no value
|
|
582
|
+
- Service layer provides better organization
|
|
583
|
+
|
|
584
|
+
### Why This is Better
|
|
585
|
+
|
|
586
|
+
- ✅ **Simpler** - Less code, less complexity
|
|
587
|
+
- ✅ **Proven** - agentbase.me uses this pattern successfully
|
|
588
|
+
- ✅ **Maintainable** - Clear separation of concerns
|
|
589
|
+
- ✅ **Flexible** - Easy to add new services
|
|
590
|
+
- ✅ **Testable** - Mock services, not wrappers
|
|
591
|
+
- ✅ **Direct** - Use library features without indirection
|
|
592
|
+
|
|
593
|
+
### Security Note
|
|
594
|
+
|
|
595
|
+
**Admin SDK bypasses security rules** - we must enforce security in code:
|
|
596
|
+
- Check user permissions in service methods
|
|
597
|
+
- Validate user_id matches requesting user
|
|
598
|
+
- Enforce trust levels in application logic
|
|
599
|
+
- Use PermissionsService to check access rights
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
**Status**: Design Recommendation
|
|
604
|
+
**Decision**: Use service layer pattern, no client wrapper
|
|
605
|
+
**Rationale**: Library API is clean, service layer provides better organization
|
|
606
|
+
**Next**: Update Task 4 to create services instead of wrapper
|