@prmichaelsen/remember-mcp 3.13.0 → 3.14.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.
Files changed (61) hide show
  1. package/agent/milestones/milestone-17-remember-core-migration.md +140 -0
  2. package/agent/progress.yaml +123 -6
  3. package/agent/tasks/milestone-17-remember-core-migration/task-193-foundation-setup.md +58 -0
  4. package/agent/tasks/milestone-17-remember-core-migration/task-194-migrate-relationship-tools.md +47 -0
  5. package/agent/tasks/milestone-17-remember-core-migration/task-195-migrate-preference-tools.md +34 -0
  6. package/agent/tasks/milestone-17-remember-core-migration/task-196-migrate-memory-tools.md +46 -0
  7. package/agent/tasks/milestone-17-remember-core-migration/task-197-migrate-space-confirmation-tools.md +49 -0
  8. package/agent/tasks/milestone-17-remember-core-migration/task-198-migrate-space-search-moderate.md +46 -0
  9. package/agent/tasks/milestone-17-remember-core-migration/task-199-migrate-delete-memory.md +43 -0
  10. package/agent/tasks/milestone-17-remember-core-migration/task-200-code-cleanup-verification.md +52 -0
  11. package/dist/core-services.d.ts +25 -0
  12. package/dist/server-factory.js +3578 -4485
  13. package/dist/server.js +3070 -3973
  14. package/dist/tools/confirm-publish-moderation.spec.d.ts +3 -2
  15. package/dist/tools/create-memory.d.ts +1 -1
  16. package/dist/tools/query-space.d.ts +1 -1
  17. package/dist/tools/search-space.d.ts +10 -14
  18. package/jest.config.js +11 -0
  19. package/package.json +2 -1
  20. package/src/core-services.ts +50 -0
  21. package/src/tools/confirm-publish-moderation.spec.ts +120 -176
  22. package/src/tools/confirm.ts +70 -1035
  23. package/src/tools/create-memory.ts +16 -67
  24. package/src/tools/create-relationship.ts +13 -181
  25. package/src/tools/delete-memory.ts +7 -72
  26. package/src/tools/delete-relationship.ts +7 -91
  27. package/src/tools/deny.ts +4 -14
  28. package/src/tools/find-similar.ts +16 -110
  29. package/src/tools/get-preferences.ts +3 -8
  30. package/src/tools/moderate.spec.ts +65 -81
  31. package/src/tools/moderate.ts +18 -121
  32. package/src/tools/publish.ts +7 -204
  33. package/src/tools/query-space.ts +28 -140
  34. package/src/tools/retract.ts +7 -185
  35. package/src/tools/revise.ts +4 -136
  36. package/src/tools/search-relationship.ts +17 -116
  37. package/src/tools/search-space.ts +58 -304
  38. package/src/tools/set-preference.ts +3 -8
  39. package/src/tools/update-memory.ts +22 -190
  40. package/src/tools/update-relationship.ts +16 -90
  41. package/src/v2-smoke.e2e.ts +3 -2
  42. package/dist/collections/composite-ids.d.ts +0 -106
  43. package/dist/collections/core-infrastructure.spec.d.ts +0 -11
  44. package/dist/collections/dot-notation.d.ts +0 -106
  45. package/dist/collections/tracking-arrays.d.ts +0 -176
  46. package/dist/constants/content-types.d.ts +0 -61
  47. package/dist/services/confirmation-token.service.d.ts +0 -99
  48. package/dist/services/confirmation-token.service.spec.d.ts +0 -5
  49. package/dist/services/preferences-database.service.d.ts +0 -22
  50. package/dist/services/space-config.service.d.ts +0 -23
  51. package/dist/services/space-config.service.spec.d.ts +0 -2
  52. package/src/collections/composite-ids.ts +0 -193
  53. package/src/collections/core-infrastructure.spec.ts +0 -353
  54. package/src/collections/dot-notation.ts +0 -212
  55. package/src/collections/tracking-arrays.ts +0 -298
  56. package/src/constants/content-types.ts +0 -490
  57. package/src/services/confirmation-token.service.spec.ts +0 -254
  58. package/src/services/confirmation-token.service.ts +0 -328
  59. package/src/services/preferences-database.service.ts +0 -120
  60. package/src/services/space-config.service.spec.ts +0 -102
  61. package/src/services/space-config.service.ts +0 -79
@@ -1,254 +0,0 @@
1
- /**
2
- * Unit tests for Confirmation Token Service
3
- */
4
-
5
- import { ConfirmationTokenService, type ConfirmationRequest } from '../../src/services/confirmation-token.service';
6
- import * as firestoreInit from '../../src/firestore/init';
7
-
8
- // Mock Firestore functions
9
- jest.mock('../../src/firestore/init', () => ({
10
- getDocument: jest.fn(),
11
- addDocument: jest.fn(),
12
- updateDocument: jest.fn(),
13
- queryDocuments: jest.fn(),
14
- }));
15
-
16
- describe('ConfirmationTokenService', () => {
17
- let service: ConfirmationTokenService;
18
- const mockUserId = 'test-user-123';
19
- const mockToken = '550e8400-e29b-41d4-a716-446655440000';
20
-
21
- beforeEach(() => {
22
- service = new ConfirmationTokenService();
23
- jest.clearAllMocks();
24
- });
25
-
26
- afterEach(() => {
27
- jest.restoreAllMocks();
28
- });
29
-
30
- describe('createRequest', () => {
31
- it('should create a new confirmation request with token', async () => {
32
- const mockDocRef = { id: 'request-123', path: 'users/test-user-123/requests/request-123' };
33
- (firestoreInit.addDocument as jest.Mock).mockResolvedValue(mockDocRef);
34
-
35
- const payload = { memory_id: 'mem-123', additional_tags: [] };
36
- const result = await service.createRequest(mockUserId, 'publish_memory', payload, 'the_void');
37
-
38
- expect(result.requestId).toBe('request-123');
39
- expect(result.token).toBeDefined();
40
- expect(typeof result.token).toBe('string');
41
- expect(result.token.length).toBeGreaterThan(0);
42
- expect(firestoreInit.addDocument).toHaveBeenCalledWith(
43
- 'users/test-user-123/requests',
44
- expect.objectContaining({
45
- user_id: mockUserId,
46
- action: 'publish_memory',
47
- target_collection: 'the_void',
48
- payload,
49
- status: 'pending',
50
- })
51
- );
52
- });
53
-
54
- it('should set expiry to 5 minutes from now', async () => {
55
- const mockDocRef = { id: 'request-123', path: 'path' };
56
- (firestoreInit.addDocument as jest.Mock).mockResolvedValue(mockDocRef);
57
-
58
- const beforeTime = Date.now();
59
- await service.createRequest(mockUserId, 'publish_memory', {});
60
- const afterTime = Date.now();
61
-
62
- const call = (firestoreInit.addDocument as jest.Mock).mock.calls[0][1];
63
- const expiresAt = new Date(call.expires_at).getTime();
64
- const createdAt = new Date(call.created_at).getTime();
65
-
66
- // Should be 5 minutes (300000ms) after creation
67
- const expectedExpiry = createdAt + 5 * 60 * 1000;
68
- expect(expiresAt).toBe(expectedExpiry);
69
- expect(createdAt).toBeGreaterThanOrEqual(beforeTime);
70
- expect(createdAt).toBeLessThanOrEqual(afterTime);
71
- });
72
- });
73
-
74
- describe('validateToken', () => {
75
- it('should return request if token is valid and not expired', async () => {
76
- const mockRequest: ConfirmationRequest = {
77
- user_id: mockUserId,
78
- token: mockToken,
79
- action: 'publish_memory',
80
- payload: { memory_id: 'mem-123' },
81
- created_at: new Date().toISOString(),
82
- expires_at: new Date(Date.now() + 60000).toISOString(), // 1 minute from now
83
- status: 'pending',
84
- };
85
-
86
- (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
87
- { id: 'request-123', data: mockRequest }
88
- ]);
89
-
90
- const result = await service.validateToken(mockUserId, mockToken);
91
-
92
- expect(result).toEqual({
93
- ...mockRequest,
94
- request_id: 'request-123',
95
- });
96
- });
97
-
98
- it('should return null if token not found', async () => {
99
- (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
100
-
101
- const result = await service.validateToken(mockUserId, mockToken);
102
-
103
- expect(result).toBeNull();
104
- });
105
-
106
- it('should return null and mark expired if token is expired', async () => {
107
- const mockRequest: ConfirmationRequest = {
108
- user_id: mockUserId,
109
- token: mockToken,
110
- action: 'publish_memory',
111
- payload: {},
112
- created_at: new Date(Date.now() - 10 * 60 * 1000).toISOString(), // 10 minutes ago
113
- expires_at: new Date(Date.now() - 5 * 60 * 1000).toISOString(), // 5 minutes ago (expired)
114
- status: 'pending',
115
- };
116
-
117
- (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
118
- { id: 'request-123', data: mockRequest }
119
- ]);
120
- (firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
121
-
122
- const result = await service.validateToken(mockUserId, mockToken);
123
-
124
- expect(result).toBeNull();
125
- expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
126
- 'users/test-user-123/requests',
127
- 'request-123',
128
- { status: 'expired' }
129
- );
130
- });
131
- });
132
-
133
- describe('confirmRequest', () => {
134
- it('should confirm a valid request', async () => {
135
- const mockRequest: ConfirmationRequest = {
136
- user_id: mockUserId,
137
- token: mockToken,
138
- action: 'publish_memory',
139
- payload: { memory_id: 'mem-123' },
140
- created_at: new Date().toISOString(),
141
- expires_at: new Date(Date.now() + 60000).toISOString(),
142
- status: 'pending',
143
- };
144
-
145
- (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
146
- { id: 'request-123', data: mockRequest }
147
- ]);
148
- (firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
149
-
150
- const result = await service.confirmRequest(mockUserId, mockToken);
151
-
152
- expect(result).not.toBeNull();
153
- expect(result?.status).toBe('confirmed');
154
- expect(result?.confirmed_at).toBeDefined();
155
- expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
156
- 'users/test-user-123/requests',
157
- 'request-123',
158
- expect.objectContaining({
159
- status: 'confirmed',
160
- confirmed_at: expect.any(String),
161
- })
162
- );
163
- });
164
-
165
- it('should return null if token is invalid', async () => {
166
- (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
167
-
168
- const result = await service.confirmRequest(mockUserId, mockToken);
169
-
170
- expect(result).toBeNull();
171
- expect(firestoreInit.updateDocument).not.toHaveBeenCalled();
172
- });
173
- });
174
-
175
- describe('denyRequest', () => {
176
- it('should deny a valid request', async () => {
177
- const mockRequest: ConfirmationRequest = {
178
- user_id: mockUserId,
179
- token: mockToken,
180
- action: 'publish_memory',
181
- payload: {},
182
- created_at: new Date().toISOString(),
183
- expires_at: new Date(Date.now() + 60000).toISOString(),
184
- status: 'pending',
185
- };
186
-
187
- (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
188
- { id: 'request-123', data: mockRequest }
189
- ]);
190
- (firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
191
-
192
- const result = await service.denyRequest(mockUserId, mockToken);
193
-
194
- expect(result).toBe(true);
195
- expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
196
- 'users/test-user-123/requests',
197
- 'request-123',
198
- { status: 'denied' }
199
- );
200
- });
201
-
202
- it('should return false if token is invalid', async () => {
203
- (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
204
-
205
- const result = await service.denyRequest(mockUserId, mockToken);
206
-
207
- expect(result).toBe(false);
208
- });
209
- });
210
-
211
- describe('retractRequest', () => {
212
- it('should retract a valid request', async () => {
213
- const mockRequest: ConfirmationRequest = {
214
- user_id: mockUserId,
215
- token: mockToken,
216
- action: 'publish_memory',
217
- payload: {},
218
- created_at: new Date().toISOString(),
219
- expires_at: new Date(Date.now() + 60000).toISOString(),
220
- status: 'pending',
221
- };
222
-
223
- (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([
224
- { id: 'request-123', data: mockRequest }
225
- ]);
226
- (firestoreInit.updateDocument as jest.Mock).mockResolvedValue(undefined);
227
-
228
- const result = await service.retractRequest(mockUserId, mockToken);
229
-
230
- expect(result).toBe(true);
231
- expect(firestoreInit.updateDocument).toHaveBeenCalledWith(
232
- 'users/test-user-123/requests',
233
- 'request-123',
234
- { status: 'retracted' }
235
- );
236
- });
237
-
238
- it('should return false if token is invalid', async () => {
239
- (firestoreInit.queryDocuments as jest.Mock).mockResolvedValue([]);
240
-
241
- const result = await service.retractRequest(mockUserId, mockToken);
242
-
243
- expect(result).toBe(false);
244
- });
245
- });
246
-
247
- describe('cleanupExpired', () => {
248
- it('should return 0 (not implemented - relies on Firestore TTL)', async () => {
249
- const result = await service.cleanupExpired();
250
-
251
- expect(result).toBe(0);
252
- });
253
- });
254
- });
@@ -1,328 +0,0 @@
1
- /**
2
- * Confirmation Token Service
3
- *
4
- * Manages confirmation tokens for sensitive operations like publishing memories.
5
- * Tokens are one-time use with 5-minute expiry.
6
- */
7
-
8
- import { randomUUID } from 'crypto';
9
- import {
10
- getDocument,
11
- addDocument,
12
- updateDocument,
13
- queryDocuments,
14
- type QueryOptions
15
- } from '../firestore/init.js';
16
- import { logger } from '../utils/logger.js';
17
-
18
- /**
19
- * Confirmation request stored in Firestore
20
- */
21
- export interface ConfirmationRequest {
22
- user_id: string;
23
- token: string;
24
- action: string;
25
- target_collection?: string;
26
- payload: any;
27
- created_at: string; // ISO 8601 timestamp
28
- expires_at: string; // ISO 8601 timestamp
29
- status: 'pending' | 'confirmed' | 'denied' | 'expired' | 'retracted';
30
- confirmed_at?: string; // ISO 8601 timestamp
31
- }
32
-
33
- /**
34
- * Service for managing confirmation tokens
35
- */
36
- export class ConfirmationTokenService {
37
- private readonly EXPIRY_MINUTES = 5;
38
-
39
- /**
40
- * Create a new confirmation request
41
- *
42
- * @param userId - User ID who initiated the request
43
- * @param action - Action type (e.g., 'publish_memory')
44
- * @param payload - Data to store with the request
45
- * @param targetCollection - Optional target collection (e.g., 'the_void')
46
- * @returns Request ID and token
47
- */
48
- async createRequest(
49
- userId: string,
50
- action: string,
51
- payload: any,
52
- targetCollection?: string
53
- ): Promise<{ requestId: string; token: string }> {
54
- try {
55
- const token = randomUUID();
56
-
57
- const now = new Date();
58
- const expiresAt = new Date(now.getTime() + this.EXPIRY_MINUTES * 60 * 1000);
59
-
60
- const request: ConfirmationRequest = {
61
- user_id: userId,
62
- token,
63
- action,
64
- target_collection: targetCollection,
65
- payload,
66
- created_at: now.toISOString(),
67
- expires_at: expiresAt.toISOString(),
68
- status: 'pending',
69
- };
70
-
71
- // Add document to Firestore (auto-generates ID)
72
- const collectionPath = `users/${userId}/requests`;
73
- logger.info('Creating confirmation request', {
74
- service: 'ConfirmationTokenService',
75
- userId,
76
- action,
77
- targetCollection,
78
- collectionPath,
79
- payloadKeys: Object.keys(payload),
80
- });
81
-
82
- logger.debug('Calling Firestore addDocument', {
83
- service: 'ConfirmationTokenService',
84
- collectionPath,
85
- });
86
- const docRef = await addDocument(collectionPath, request);
87
- logger.debug('Firestore addDocument returned', {
88
- service: 'ConfirmationTokenService',
89
- hasDocRef: !!docRef,
90
- hasId: !!docRef?.id,
91
- docRefId: docRef?.id,
92
- });
93
-
94
- // Validate docRef
95
- if (!docRef) {
96
- const error = new Error('Firestore addDocument returned null/undefined');
97
- logger.error('CRITICAL: addDocument returned null', {
98
- service: 'ConfirmationTokenService',
99
- userId,
100
- collectionPath,
101
- });
102
- throw error;
103
- }
104
-
105
- if (!docRef.id) {
106
- const error = new Error('Firestore addDocument returned docRef without ID');
107
- logger.error('CRITICAL: docRef has no ID', {
108
- service: 'ConfirmationTokenService',
109
- userId,
110
- collectionPath,
111
- docRef,
112
- });
113
- throw error;
114
- }
115
-
116
- logger.info('Confirmation request created successfully', {
117
- service: 'ConfirmationTokenService',
118
- requestId: docRef.id,
119
- token,
120
- expiresAt: request.expires_at,
121
- });
122
-
123
- return { requestId: docRef.id, token };
124
- } catch (error) {
125
- logger.error('Failed to create confirmation request', {
126
- service: 'ConfirmationTokenService',
127
- error: error instanceof Error ? error.message : String(error),
128
- stack: error instanceof Error ? error.stack : undefined,
129
- userId,
130
- action,
131
- collectionPath: `users/${userId}/requests`,
132
- });
133
-
134
- // Re-throw so caller (tool handler) can catch and return error response
135
- throw error;
136
- }
137
- }
138
-
139
- /**
140
- * Validate and retrieve a confirmation request
141
- *
142
- * @param userId - User ID
143
- * @param token - Confirmation token
144
- * @returns Request with request_id if valid, null otherwise
145
- */
146
- async validateToken(
147
- userId: string,
148
- token: string
149
- ): Promise<(ConfirmationRequest & { request_id: string }) | null> {
150
- const collectionPath = `users/${userId}/requests`;
151
-
152
- logger.debug('Validating confirmation token', {
153
- service: 'ConfirmationTokenService',
154
- userId,
155
- token,
156
- collectionPath,
157
- });
158
-
159
- // Query for the token
160
- const queryOptions: QueryOptions = {
161
- where: [
162
- { field: 'token', op: '==', value: token },
163
- { field: 'status', op: '==', value: 'pending' },
164
- ],
165
- limit: 1,
166
- };
167
-
168
- const results = await queryDocuments(collectionPath, queryOptions);
169
-
170
- logger.debug('Token query results', {
171
- service: 'ConfirmationTokenService',
172
- resultsFound: results.length,
173
- hasResults: results.length > 0,
174
- });
175
-
176
- if (results.length === 0) {
177
- logger.info('Token not found or not pending', {
178
- service: 'ConfirmationTokenService',
179
- userId,
180
- });
181
- return null;
182
- }
183
-
184
- const doc = results[0];
185
- const request = doc.data as ConfirmationRequest;
186
-
187
- logger.info('Confirmation request found', {
188
- service: 'ConfirmationTokenService',
189
- requestId: doc.id,
190
- action: request.action,
191
- status: request.status,
192
- expiresAt: request.expires_at,
193
- });
194
-
195
- // Check expiry
196
- const expiresAt = new Date(request.expires_at);
197
- if (expiresAt.getTime() < Date.now()) {
198
- logger.info('Token expired', {
199
- service: 'ConfirmationTokenService',
200
- requestId: doc.id,
201
- expiresAt: request.expires_at,
202
- });
203
- await this.updateStatus(userId, doc.id, 'expired');
204
- return null;
205
- }
206
-
207
- return {
208
- ...request,
209
- request_id: doc.id,
210
- };
211
- }
212
-
213
- /**
214
- * Confirm a request
215
- *
216
- * @param userId - User ID
217
- * @param token - Confirmation token
218
- * @returns Confirmed request if valid, null otherwise
219
- */
220
- async confirmRequest(
221
- userId: string,
222
- token: string
223
- ): Promise<(ConfirmationRequest & { request_id: string }) | null> {
224
- const request = await this.validateToken(userId, token);
225
- if (!request) {
226
- return null;
227
- }
228
-
229
- await this.updateStatus(userId, request.request_id, 'confirmed');
230
-
231
- return {
232
- ...request,
233
- status: 'confirmed',
234
- confirmed_at: new Date().toISOString(),
235
- };
236
- }
237
-
238
- /**
239
- * Deny a request
240
- *
241
- * @param userId - User ID
242
- * @param token - Confirmation token
243
- * @returns True if denied successfully, false otherwise
244
- */
245
- async denyRequest(
246
- userId: string,
247
- token: string
248
- ): Promise<boolean> {
249
- const request = await this.validateToken(userId, token);
250
- if (!request) {
251
- return false;
252
- }
253
-
254
- await this.updateStatus(userId, request.request_id, 'denied');
255
- return true;
256
- }
257
-
258
- /**
259
- * Retract a request
260
- *
261
- * @param userId - User ID
262
- * @param token - Confirmation token
263
- * @returns True if retracted successfully, false otherwise
264
- */
265
- async retractRequest(
266
- userId: string,
267
- token: string
268
- ): Promise<boolean> {
269
- const request = await this.validateToken(userId, token);
270
- if (!request) {
271
- return false;
272
- }
273
-
274
- await this.updateStatus(userId, request.request_id, 'retracted');
275
- return true;
276
- }
277
-
278
- /**
279
- * Update request status
280
- *
281
- * @param userId - User ID
282
- * @param requestId - Request document ID
283
- * @param status - New status
284
- */
285
- private async updateStatus(
286
- userId: string,
287
- requestId: string,
288
- status: ConfirmationRequest['status']
289
- ): Promise<void> {
290
- const collectionPath = `users/${userId}/requests`;
291
-
292
- const updateData: Partial<ConfirmationRequest> = {
293
- status,
294
- };
295
-
296
- if (status === 'confirmed') {
297
- updateData.confirmed_at = new Date().toISOString();
298
- }
299
-
300
- await updateDocument(collectionPath, requestId, updateData);
301
- }
302
-
303
- /**
304
- * Clean up expired requests (optional - Firestore TTL handles deletion)
305
- *
306
- * Note: Configure Firestore TTL policy on 'requests' collection group
307
- * with 'expires_at' field for automatic deletion within 24 hours.
308
- *
309
- * This method is optional for immediate cleanup if needed.
310
- *
311
- * @returns Count of deleted requests
312
- */
313
- async cleanupExpired(): Promise<number> {
314
- // Note: firebase-admin-sdk-v8 doesn't support collectionGroup queries
315
- // This would need to be implemented differently or rely on Firestore TTL
316
- // For now, return 0 and rely on Firestore TTL policy
317
- logger.warn('cleanupExpired not implemented - relying on Firestore TTL', {
318
- service: 'ConfirmationTokenService',
319
- note: 'Configure Firestore TTL policy on requests collection group',
320
- });
321
- return 0;
322
- }
323
- }
324
-
325
- /**
326
- * Singleton instance of the confirmation token service
327
- */
328
- export const confirmationTokenService = new ConfirmationTokenService();
@@ -1,120 +0,0 @@
1
- /**
2
- * Preferences Database Service
3
- * Handles all Firestore operations for user preferences
4
- */
5
-
6
- import { getDocument, setDocument } from '../firestore/init.js';
7
- import { getUserPreferencesPath } from '../firestore/paths.js';
8
- import { logger } from '../utils/logger.js';
9
- import {
10
- UserPreferences,
11
- DEFAULT_PREFERENCES,
12
- } from '../types/preferences.js';
13
-
14
- export class PreferencesDatabaseService {
15
- /**
16
- * Get user preferences
17
- * Returns defaults if preferences don't exist
18
- */
19
- static async getPreferences(userId: string): Promise<UserPreferences> {
20
- try {
21
- const pathParts = getUserPreferencesPath(userId).split('/');
22
- const docId = pathParts.pop()!;
23
- const collectionPath = pathParts.join('/');
24
-
25
- const doc = await getDocument(collectionPath, docId);
26
-
27
- if (!doc) {
28
- // Return defaults with user_id
29
- const now = new Date().toISOString();
30
- return {
31
- user_id: userId,
32
- ...DEFAULT_PREFERENCES,
33
- created_at: now,
34
- updated_at: now,
35
- };
36
- }
37
-
38
- return doc as UserPreferences;
39
- } catch (error) {
40
- logger.error('Failed to get preferences:', error);
41
- throw new Error(`Failed to get preferences: ${error instanceof Error ? error.message : String(error)}`);
42
- }
43
- }
44
-
45
- /**
46
- * Update user preferences (partial update with merge)
47
- * Creates with defaults if preferences don't exist
48
- */
49
- static async updatePreferences(
50
- userId: string,
51
- updates: Partial<Omit<UserPreferences, 'user_id' | 'created_at'>>
52
- ): Promise<UserPreferences> {
53
- try {
54
- const pathParts = getUserPreferencesPath(userId).split('/');
55
- const docId = pathParts.pop()!;
56
- const collectionPath = pathParts.join('/');
57
-
58
- const now = new Date().toISOString();
59
- const doc = await getDocument(collectionPath, docId);
60
-
61
- if (!doc) {
62
- // Create with defaults + updates
63
- const newPrefs: UserPreferences = {
64
- user_id: userId,
65
- ...DEFAULT_PREFERENCES,
66
- ...updates,
67
- created_at: now,
68
- updated_at: now,
69
- };
70
-
71
- await setDocument(collectionPath, docId, newPrefs);
72
- logger.info('Preferences created with defaults', { userId });
73
- return newPrefs;
74
- }
75
-
76
- // Update existing preferences with merge
77
- const updateData = {
78
- ...updates,
79
- updated_at: now,
80
- };
81
-
82
- await setDocument(collectionPath, docId, updateData, { merge: true });
83
- logger.info('Preferences updated', { userId });
84
-
85
- // Return updated preferences
86
- const updatedDoc = await getDocument(collectionPath, docId);
87
- return updatedDoc as UserPreferences;
88
- } catch (error) {
89
- logger.error('Failed to update preferences:', error);
90
- throw new Error(`Failed to update preferences: ${error instanceof Error ? error.message : String(error)}`);
91
- }
92
- }
93
-
94
- /**
95
- * Create preferences with defaults
96
- */
97
- static async createPreferences(userId: string): Promise<UserPreferences> {
98
- try {
99
- const pathParts = getUserPreferencesPath(userId).split('/');
100
- const docId = pathParts.pop()!;
101
- const collectionPath = pathParts.join('/');
102
-
103
- const now = new Date().toISOString();
104
- const preferences: UserPreferences = {
105
- user_id: userId,
106
- ...DEFAULT_PREFERENCES,
107
- created_at: now,
108
- updated_at: now,
109
- };
110
-
111
- await setDocument(collectionPath, docId, preferences);
112
- logger.info('Preferences created', { userId });
113
-
114
- return preferences;
115
- } catch (error) {
116
- logger.error('Failed to create preferences:', error);
117
- throw new Error(`Failed to create preferences: ${error instanceof Error ? error.message : String(error)}`);
118
- }
119
- }
120
- }