/** * Routines repository — Cosmos DB CRUD + sync + batch upsert. * * Container: routines (partition key: /userId) */ import { getContainer } from '../../lib/cosmos.js'; import type { RoutineDoc, RoutineQuery, BatchUpsertRoutinesResult } from './types.js'; function container() { return getContainer('routines'); } export async function listRoutines( userId: string, productId: string, query: RoutineQuery ): Promise<{ items: RoutineDoc[]; total: number }> { const conditions: string[] = ['c.userId = @userId', 'c.productId = @productId']; const params: { name: string; value: string | number | boolean }[] = [ { name: '@userId', value: userId }, { name: '@productId', value: productId }, ]; if (query.status) { conditions.push('c.status = @status'); params.push({ name: '@status', value: query.status }); } if (query.isTemplate !== undefined) { conditions.push('c.isTemplate = @isTemplate'); params.push({ name: '@isTemplate', value: query.isTemplate }); } if (query.category) { conditions.push('c.category = @category'); params.push({ name: '@category', value: query.category }); } const where = `WHERE ${conditions.join(' AND ')}`; const sortField = `c.${query.sortBy}`; const orderDir = query.sortOrder.toUpperCase(); const countResult = await container() .items.query({ query: `SELECT VALUE COUNT(1) FROM c ${where}`, parameters: params, }) .fetchAll(); const total = countResult.resources[0] ?? 0; const { resources } = await container() .items.query({ query: `SELECT * FROM c ${where} ORDER BY ${sortField} ${orderDir} OFFSET @offset LIMIT @limit`, parameters: [ ...params, { name: '@offset', value: query.offset }, { name: '@limit', value: query.limit }, ], }) .fetchAll(); return { items: resources, total }; } export async function getRoutine(id: string, userId: string): Promise { try { const { resource } = await container().item(id, userId).read(); return resource ?? null; } catch { return null; } } export async function createRoutine(doc: RoutineDoc): Promise { const { resource } = await container().items.create(doc); return resource as RoutineDoc; } export async function updateRoutine( id: string, userId: string, updates: Partial, expectedSyncVersion: number ): Promise<{ doc: RoutineDoc | null; conflict: boolean; serverVersion?: number }> { try { const { resource: existing } = await container().item(id, userId).read(); if (!existing) return { doc: null, conflict: false }; if (expectedSyncVersion <= existing.syncVersion) { return { doc: null, conflict: true, serverVersion: existing.syncVersion }; } const now = new Date().toISOString(); const merged: RoutineDoc = { ...existing, ...updates, syncVersion: expectedSyncVersion, lastSyncedAt: now, }; const { resource } = await container().item(id, userId).replace(merged); return { doc: resource as RoutineDoc, conflict: false }; } catch { return { doc: null, conflict: false }; } } export async function deleteRoutine(id: string, userId: string): Promise { try { const { resource: existing } = await container().item(id, userId).read(); if (!existing) return false; await container().item(id, userId).delete(); return true; } catch { return false; } } export async function getRoutinesSince( userId: string, productId: string, sinceTimestamp: string, limit: number ): Promise { const { resources } = await container() .items.query({ query: 'SELECT * FROM c WHERE c.userId = @userId AND c.productId = @productId AND c.lastSyncedAt >= @since ORDER BY c.lastSyncedAt ASC OFFSET 0 LIMIT @limit', parameters: [ { name: '@userId', value: userId }, { name: '@productId', value: productId }, { name: '@since', value: sinceTimestamp }, { name: '@limit', value: limit }, ], }) .fetchAll(); return resources; } export async function batchUpsertRoutines( userId: string, productId: string, routines: Array & { id: string; syncVersion: number }> ): Promise { const synced: string[] = []; const conflicts: Array<{ id: string; serverVersion: number }> = []; const errors: Array<{ id: string; error: string }> = []; for (const routine of routines) { try { const existing = await getRoutine(routine.id, userId); const now = new Date().toISOString(); if (existing) { if (routine.syncVersion >= existing.syncVersion) { const merged: RoutineDoc = { ...existing, ...routine, userId, productId, lastSyncedAt: now, } as RoutineDoc; await container().item(routine.id, userId).replace(merged); synced.push(routine.id); } else { conflicts.push({ id: routine.id, serverVersion: existing.syncVersion }); } } else { const doc = { ...routine, userId, productId, lastSyncedAt: now, }; await container().items.create(doc); synced.push(routine.id); } } catch (err) { errors.push({ id: routine.id, error: err instanceof Error ? err.message : 'Unknown error' }); } } return { synced, conflicts, errors }; }