diff --git a/services/platform-service/src/modules/delivery/repository.ts b/services/platform-service/src/modules/delivery/repository.ts index 23727e3a..4478e6b7 100644 --- a/services/platform-service/src/modules/delivery/repository.ts +++ b/services/platform-service/src/modules/delivery/repository.ts @@ -1,48 +1,35 @@ -import { getContainer } from '../../lib/cosmos.js'; +import type { FilterMap } from '@bytelyst/datastore'; +import { getCollection } from '../../lib/datastore.js'; import type { DeliveryLogDoc } from './types.js'; -const CONTAINER = 'delivery_log'; - -function container() { - return getContainer(CONTAINER); +function collection() { + return getCollection('delivery_log', '/pk'); } export async function createDeliveryLog(doc: DeliveryLogDoc): Promise { - const { resource } = await container().items.create(doc); - return resource as DeliveryLogDoc; + return collection().create(doc); } export async function updateDeliveryLog(doc: DeliveryLogDoc): Promise { - const { resource } = await container().item(doc.id, doc.pk).replace(doc); - return resource as DeliveryLogDoc; + return collection().upsert(doc); } export async function listDeliveryLogs( productId: string, options?: { channel?: string; status?: string; limit?: number } ): Promise { - const limit = options?.limit ?? 50; - let query = 'SELECT TOP @limit * FROM c WHERE STARTSWITH(c.pk, @prefix)'; - const parameters: Array<{ name: string; value: string | number }> = [ - { name: '@limit', value: Math.min(limit, 200) }, - { name: '@prefix', value: productId }, - ]; + const limit = Math.min(options?.limit ?? 50, 200); + const filter: FilterMap = { + pk: { $startsWith: productId }, + }; + if (options?.channel) filter.channel = options.channel; + if (options?.status) filter.status = options.status; - if (options?.channel) { - query += ' AND c.channel = @channel'; - parameters.push({ name: '@channel', value: options.channel }); - } - if (options?.status) { - query += ' AND c.status = @status'; - parameters.push({ name: '@status', value: options.status }); - } - - query += ' ORDER BY c.createdAt DESC'; - - const { resources } = await container() - .items.query({ query, parameters }) - .fetchAll(); - return resources; + return collection().findMany({ + filter, + sort: { createdAt: -1 }, + limit, + }); } export async function getDeliveryStats(productId: string): Promise<{ @@ -51,32 +38,21 @@ export async function getDeliveryStats(productId: string): Promise<{ failed: number; byChannel: Record; }> { - const prefix = productId; - - const countQuery = await container() - .items.query<{ status: string; cnt: number }>({ - query: - 'SELECT c.status, COUNT(1) AS cnt FROM c WHERE STARTSWITH(c.pk, @prefix) GROUP BY c.status', - parameters: [{ name: '@prefix', value: prefix }], - }) - .fetchAll(); - - const channelQuery = await container() - .items.query<{ channel: string; cnt: number }>({ - query: - 'SELECT c.channel, COUNT(1) AS cnt FROM c WHERE STARTSWITH(c.pk, @prefix) GROUP BY c.channel', - parameters: [{ name: '@prefix', value: prefix }], - }) - .fetchAll(); + // Fetch all matching docs and aggregate in-memory + const docs = await collection().findMany({ + filter: { pk: { $startsWith: productId } }, + }); const byStatus: Record = {}; - for (const r of countQuery.resources) byStatus[r.status] = r.cnt; - const byChannel: Record = {}; - for (const r of channelQuery.resources) byChannel[r.channel] = r.cnt; + + for (const doc of docs) { + byStatus[doc.status] = (byStatus[doc.status] ?? 0) + 1; + byChannel[doc.channel] = (byChannel[doc.channel] ?? 0) + 1; + } return { - total: Object.values(byStatus).reduce((a, b) => a + b, 0), + total: docs.length, sent: byStatus['sent'] ?? 0, failed: byStatus['failed'] ?? 0, byChannel, diff --git a/services/platform-service/src/modules/exports/repository.ts b/services/platform-service/src/modules/exports/repository.ts index 939facba..ee4cc30b 100644 --- a/services/platform-service/src/modules/exports/repository.ts +++ b/services/platform-service/src/modules/exports/repository.ts @@ -1,44 +1,30 @@ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import type { ExportJobDoc } from './types.js'; -const CONTAINER = 'export_jobs'; - -function container() { - return getContainer(CONTAINER); +function collection() { + return getCollection('export_jobs', '/productId'); } export async function createExportJob(doc: ExportJobDoc): Promise { - const { resource } = await container().items.create(doc); - return resource as ExportJobDoc; + return collection().create(doc); } export async function getExportJob(id: string, productId: string): Promise { try { - const { resource } = await container().item(id, productId).read(); - return resource ?? null; + return await collection().findById(id, productId); } catch { return null; } } export async function updateExportJob(doc: ExportJobDoc): Promise { - const { resource } = await container().item(doc.id, doc.productId).replace(doc); - return resource as ExportJobDoc; + return collection().upsert(doc); } export async function listExportJobs(productId: string, limit = 20): Promise { - const { resources } = await container() - .items.query( - { - query: - 'SELECT TOP @limit * FROM c WHERE c.productId = @productId ORDER BY c.createdAt DESC', - parameters: [ - { name: '@productId', value: productId }, - { name: '@limit', value: limit }, - ], - }, - { partitionKey: productId } - ) - .fetchAll(); - return resources; + return collection().findMany({ + filter: { productId }, + sort: { createdAt: -1 }, + limit, + }); } diff --git a/services/platform-service/src/modules/ip-rules/repository.ts b/services/platform-service/src/modules/ip-rules/repository.ts index 49274885..de31b87a 100644 --- a/services/platform-service/src/modules/ip-rules/repository.ts +++ b/services/platform-service/src/modules/ip-rules/repository.ts @@ -1,51 +1,34 @@ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import type { IPRuleDoc } from './types.js'; -const CONTAINER = 'ip_rules'; - -function container() { - return getContainer(CONTAINER); +function collection() { + return getCollection('ip_rules', '/productId'); } export async function listRules(productId: string): Promise { - const { resources } = await container() - .items.query( - { - query: 'SELECT * FROM c WHERE c.productId = @productId ORDER BY c.createdAt DESC', - parameters: [{ name: '@productId', value: productId }], - }, - { partitionKey: productId } - ) - .fetchAll(); - return resources; + return collection().findMany({ + filter: { productId }, + sort: { createdAt: -1 }, + }); } export async function getActiveRules(productId: string): Promise { + // Fetch all rules, then filter in-memory for the OR condition + // (expiresAt not defined OR expiresAt > now) + const allRules = await collection().findMany({ + filter: { productId }, + }); const now = new Date().toISOString(); - const { resources } = await container() - .items.query( - { - query: - 'SELECT * FROM c WHERE c.productId = @productId AND (NOT IS_DEFINED(c.expiresAt) OR c.expiresAt > @now)', - parameters: [ - { name: '@productId', value: productId }, - { name: '@now', value: now }, - ], - }, - { partitionKey: productId } - ) - .fetchAll(); - return resources; + return allRules.filter(r => !r.expiresAt || r.expiresAt > now); } export async function createRule(doc: IPRuleDoc): Promise { - const { resource } = await container().items.create(doc); - return resource as IPRuleDoc; + return collection().create(doc); } export async function deleteRule(id: string, productId: string): Promise { try { - await container().item(id, productId).delete(); + await collection().delete(id, productId); return true; } catch { return false; diff --git a/services/platform-service/src/modules/plans/repository.test.ts b/services/platform-service/src/modules/plans/repository.test.ts index 13a3c82f..71e8507a 100644 --- a/services/platform-service/src/modules/plans/repository.test.ts +++ b/services/platform-service/src/modules/plans/repository.test.ts @@ -1,24 +1,10 @@ /** - * Repository tests for plans module — mocked Cosmos DB. + * Repository tests for plans module — in-memory datastore. */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -const mockFetchAll = vi.fn(); -const mockCreate = vi.fn(); -const mockRead = vi.fn(); -const mockReplace = vi.fn(); - -vi.mock('../../lib/cosmos.js', () => ({ - getContainer: vi.fn(() => ({ - items: { - query: () => ({ fetchAll: mockFetchAll }), - create: mockCreate, - }, - item: () => ({ read: mockRead, replace: mockReplace }), - })), -})); - +import { describe, it, expect, beforeEach, afterAll } from 'vitest'; +import { MemoryDatastoreProvider } from '@bytelyst/datastore'; +import { setProvider, _resetDatastoreProvider } from '../../lib/datastore.js'; import { list, getByName, create, update, getDefaults } from './repository.js'; import type { PlanConfig } from './types.js'; @@ -39,18 +25,22 @@ const basePlan: PlanConfig = { describe('plans repository', () => { beforeEach(() => { - vi.clearAllMocks(); + setProvider(new MemoryDatastoreProvider()); + }); + + afterAll(() => { + _resetDatastoreProvider(); }); describe('list', () => { - it('returns plans from Cosmos', async () => { - mockFetchAll.mockResolvedValue({ resources: [basePlan] }); + it('returns plans from datastore', async () => { + await create(basePlan); const result = await list('lysnrai'); - expect(result).toEqual([basePlan]); + expect(result).toHaveLength(1); + expect(result[0]!.name).toBe('pro'); }); it('returns defaults when no plans in DB', async () => { - mockFetchAll.mockResolvedValue({ resources: [] }); const result = await list('lysnrai'); expect(result).toHaveLength(3); expect(result[0].name).toBe('free'); @@ -61,13 +51,12 @@ describe('plans repository', () => { describe('getByName', () => { it('returns plan when found', async () => { - mockFetchAll.mockResolvedValue({ resources: [basePlan] }); + await create(basePlan); const result = await getByName('pro', 'lysnrai'); expect(result).toEqual(basePlan); }); it('returns null when not found', async () => { - mockFetchAll.mockResolvedValue({ resources: [] }); const result = await getByName('nonexistent', 'lysnrai'); expect(result).toBeNull(); }); @@ -75,7 +64,6 @@ describe('plans repository', () => { describe('create', () => { it('creates and returns plan', async () => { - mockCreate.mockResolvedValue({ resource: basePlan }); const result = await create(basePlan); expect(result).toEqual(basePlan); }); @@ -83,21 +71,13 @@ describe('plans repository', () => { describe('update', () => { it('merges updates and returns plan', async () => { - mockRead.mockResolvedValue({ resource: basePlan }); - const updated = { ...basePlan, price: 14.99 }; - mockReplace.mockResolvedValue({ resource: updated }); + await create(basePlan); const result = await update('plan_lysnrai_pro', { price: 14.99 }); - expect(result).toEqual(updated); + expect(result).not.toBeNull(); + expect(result!.price).toBe(14.99); }); it('returns null when plan not found', async () => { - mockRead.mockResolvedValue({ resource: undefined }); - const result = await update('nonexistent', { price: 14.99 }); - expect(result).toBeNull(); - }); - - it('returns null on error', async () => { - mockRead.mockRejectedValue(new Error('Not found')); const result = await update('nonexistent', { price: 14.99 }); expect(result).toBeNull(); }); diff --git a/services/platform-service/src/modules/plans/repository.ts b/services/platform-service/src/modules/plans/repository.ts index e0b2c46a..1613e1f9 100644 --- a/services/platform-service/src/modules/plans/repository.ts +++ b/services/platform-service/src/modules/plans/repository.ts @@ -1,55 +1,41 @@ /** - * Plans repository — Cosmos DB CRUD for plan configurations. + * Plans repository — cloud-agnostic via @bytelyst/datastore. */ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import type { PlanConfig } from './types.js'; import { DEFAULT_PLANS } from './types.js'; -function container() { - return getContainer('plans'); +function collection() { + return getCollection('plans', '/productId'); } export async function list(productId: string): Promise { - const { resources } = await container() - .items.query({ - query: 'SELECT * FROM c WHERE c.productId = @productId ORDER BY c.price ASC', - parameters: [{ name: '@productId', value: productId }], - }) - .fetchAll(); + const results = await collection().findMany({ + filter: { productId }, + sort: { price: 1 }, + }); // If no plans in DB yet, return defaults - if (resources.length === 0) { + if (results.length === 0) { return getDefaults(productId); } - return resources; + return results; } export async function getByName(name: string, productId: string): Promise { - const { resources } = await container() - .items.query({ - query: 'SELECT * FROM c WHERE c.productId = @productId AND c.name = @name', - parameters: [ - { name: '@productId', value: productId }, - { name: '@name', value: name }, - ], - }) - .fetchAll(); - return resources[0] ?? null; + return collection().findOne({ + filter: { productId, name }, + }); } export async function create(doc: PlanConfig): Promise { - const { resource } = await container().items.create(doc); - return resource as PlanConfig; + return collection().create(doc); } export async function update(id: string, updates: Partial): Promise { try { - const { resource: existing } = await container().item(id, id).read(); - if (!existing) return null; - const merged = { ...existing, ...updates, updatedAt: new Date().toISOString() }; - const { resource } = await container().item(id, id).replace(merged); - return resource as PlanConfig; + return await collection().update(id, id, { ...updates, updatedAt: new Date().toISOString() }); } catch { return null; } diff --git a/services/platform-service/src/modules/referrals/repository.test.ts b/services/platform-service/src/modules/referrals/repository.test.ts index 6b6da830..56bd1ad6 100644 --- a/services/platform-service/src/modules/referrals/repository.test.ts +++ b/services/platform-service/src/modules/referrals/repository.test.ts @@ -1,24 +1,10 @@ /** - * Repository tests for referrals module — mocked Cosmos DB. + * Repository tests for referrals module — in-memory datastore. */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -const mockFetchAll = vi.fn(); -const mockCreate = vi.fn(); -const mockRead = vi.fn(); -const mockReplace = vi.fn(); - -vi.mock('../../lib/cosmos.js', () => ({ - getContainer: vi.fn(() => ({ - items: { - query: () => ({ fetchAll: mockFetchAll }), - create: mockCreate, - }, - item: () => ({ read: mockRead, replace: mockReplace }), - })), -})); - +import { describe, it, expect, beforeEach, afterAll } from 'vitest'; +import { MemoryDatastoreProvider } from '@bytelyst/datastore'; +import { setProvider, _resetDatastoreProvider } from '../../lib/datastore.js'; import { listAll, getByReferrer, @@ -48,40 +34,44 @@ const baseReferral: ReferralDoc = { describe('referrals repository', () => { beforeEach(() => { - vi.clearAllMocks(); + setProvider(new MemoryDatastoreProvider()); + }); + + afterAll(() => { + _resetDatastoreProvider(); }); describe('listAll', () => { it('returns referrals', async () => { - mockFetchAll.mockResolvedValue({ resources: [baseReferral] }); + await create(baseReferral); const result = await listAll(100, 0, 'lysnrai'); - expect(result).toEqual([baseReferral]); + expect(result).toHaveLength(1); + expect(result[0]!.id).toBe('ref_1'); }); it('returns empty array when no referrals', async () => { - mockFetchAll.mockResolvedValue({ resources: [] }); - const result = await listAll(); + const result = await listAll(100, 0, 'lysnrai'); expect(result).toEqual([]); }); }); describe('getByReferrer', () => { it('returns referrals for referrer', async () => { - mockFetchAll.mockResolvedValue({ resources: [baseReferral] }); + await create(baseReferral); const result = await getByReferrer('user_1', 'lysnrai'); - expect(result).toEqual([baseReferral]); + expect(result).toHaveLength(1); + expect(result[0]!.referrerId).toBe('user_1'); }); }); describe('getByReferredEmail', () => { it('returns referral when found', async () => { - mockFetchAll.mockResolvedValue({ resources: [baseReferral] }); + await create(baseReferral); const result = await getByReferredEmail('new@example.com', 'lysnrai'); expect(result).toEqual(baseReferral); }); it('returns null when not found', async () => { - mockFetchAll.mockResolvedValue({ resources: [] }); const result = await getByReferredEmail('none@example.com', 'lysnrai'); expect(result).toBeNull(); }); @@ -89,27 +79,19 @@ describe('referrals repository', () => { describe('getById', () => { it('returns referral when found', async () => { - mockRead.mockResolvedValue({ resource: baseReferral }); + await create(baseReferral); const result = await getById('ref_1', 'user_1'); expect(result).toEqual(baseReferral); }); it('returns null when not found', async () => { - mockRead.mockRejectedValue(new Error('Not found')); - const result = await getById('ref_1', 'user_1'); - expect(result).toBeNull(); - }); - - it('returns null when resource is undefined', async () => { - mockRead.mockResolvedValue({ resource: undefined }); - const result = await getById('ref_1', 'user_1'); + const result = await getById('nonexistent', 'user_1'); expect(result).toBeNull(); }); }); describe('create', () => { it('creates and returns referral', async () => { - mockCreate.mockResolvedValue({ resource: baseReferral }); const result = await create(baseReferral); expect(result).toEqual(baseReferral); }); @@ -117,41 +99,30 @@ describe('referrals repository', () => { describe('update', () => { it('merges updates and returns referral', async () => { - mockRead.mockResolvedValue({ resource: baseReferral }); - const updated = { ...baseReferral, status: 'signed_up' as const }; - mockReplace.mockResolvedValue({ resource: updated }); + await create(baseReferral); const result = await update('ref_1', 'user_1', { status: 'signed_up' }); - expect(result).toEqual(updated); + expect(result).not.toBeNull(); + expect(result!.status).toBe('signed_up'); }); it('returns null when not found', async () => { - mockRead.mockResolvedValue({ resource: undefined }); - const result = await update('ref_1', 'user_1', { status: 'signed_up' }); - expect(result).toBeNull(); - }); - - it('returns null on error', async () => { - mockRead.mockRejectedValue(new Error('Not found')); - const result = await update('ref_1', 'user_1', { status: 'signed_up' }); + const result = await update('nonexistent', 'user_1', { status: 'signed_up' }); expect(result).toBeNull(); }); }); describe('countReferrals', () => { it('returns counts', async () => { - mockFetchAll - .mockResolvedValueOnce({ resources: [10] }) - .mockResolvedValueOnce({ resources: [5] }) - .mockResolvedValueOnce({ resources: [2] }); + await create(baseReferral); + await create({ ...baseReferral, id: 'ref_2', status: 'signed_up' }); + await create({ ...baseReferral, id: 'ref_3', status: 'rewarded' }); const result = await countReferrals('lysnrai'); - expect(result).toEqual({ total: 10, completed: 5, rewarded: 2 }); + expect(result.total).toBe(3); + expect(result.completed).toBe(2); // signed_up + rewarded + expect(result.rewarded).toBe(1); }); it('returns zeros when no data', async () => { - mockFetchAll - .mockResolvedValueOnce({ resources: [] }) - .mockResolvedValueOnce({ resources: [] }) - .mockResolvedValueOnce({ resources: [] }); const result = await countReferrals('lysnrai'); expect(result).toEqual({ total: 0, completed: 0, rewarded: 0 }); }); diff --git a/services/platform-service/src/modules/referrals/repository.ts b/services/platform-service/src/modules/referrals/repository.ts index 13dbcf98..71867066 100644 --- a/services/platform-service/src/modules/referrals/repository.ts +++ b/services/platform-service/src/modules/referrals/repository.ts @@ -1,75 +1,51 @@ /** - * Referrals repository — Cosmos DB CRUD operations. + * Referrals repository — cloud-agnostic via @bytelyst/datastore. * Consolidated from admin-dashboard-web + user-dashboard-web repos. */ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import type { ReferralDoc } from './types.js'; -const CONTAINER = 'referrals'; - -function container() { - return getContainer(CONTAINER); +function collection() { + return getCollection('referrals', '/referrerId'); } export async function listAll(limit = 100, offset = 0, productId?: string): Promise { - const { resources } = await container() - .items.query({ - query: - 'SELECT * FROM c WHERE c.productId = @productId ORDER BY c.createdAt DESC OFFSET @offset LIMIT @limit', - parameters: [ - { name: '@productId', value: productId ?? '' }, - { name: '@offset', value: offset }, - { name: '@limit', value: limit }, - ], - }) - .fetchAll(); - return resources; + const results = await collection().findMany({ + filter: { productId: productId ?? '' }, + sort: { createdAt: -1 }, + limit: limit + offset, + }); + return results.slice(offset); } export async function getByReferrer(referrerId: string, productId: string): Promise { - const { resources } = await container() - .items.query({ - query: - 'SELECT * FROM c WHERE c.productId = @productId AND c.referrerId = @rid ORDER BY c.createdAt DESC', - parameters: [ - { name: '@productId', value: productId }, - { name: '@rid', value: referrerId }, - ], - }) - .fetchAll(); - return resources; + return collection().findMany({ + filter: { productId, referrerId }, + sort: { createdAt: -1 }, + }); } export async function getByReferredEmail( email: string, productId: string ): Promise { - const { resources } = await container() - .items.query({ - query: - 'SELECT * FROM c WHERE c.productId = @productId AND c.referredEmail = @email ORDER BY c.createdAt DESC', - parameters: [ - { name: '@productId', value: productId }, - { name: '@email', value: email.toLowerCase() }, - ], - }) - .fetchAll(); - return resources[0] ?? null; + return collection().findOne({ + filter: { productId, referredEmail: email.toLowerCase() }, + sort: { createdAt: -1 }, + }); } export async function getById(id: string, referrerId: string): Promise { try { - const { resource } = await container().item(id, referrerId).read(); - return resource ?? null; + return await collection().findById(id, referrerId); } catch { return null; } } export async function create(doc: ReferralDoc): Promise { - const { resource } = await container().items.create(doc); - return resource as ReferralDoc; + return collection().create(doc); } export async function update( @@ -78,11 +54,7 @@ export async function update( updates: Partial ): Promise { try { - const { resource: existing } = await container().item(id, referrerId).read(); - if (!existing) return null; - const merged = { ...existing, ...updates }; - const { resource } = await container().item(id, referrerId).replace(merged); - return resource as ReferralDoc; + return await collection().update(id, referrerId, updates); } catch { return null; } @@ -93,29 +65,14 @@ export async function countReferrals(productId: string): Promise<{ completed: number; rewarded: number; }> { - const { resources: totalRes } = await container() - .items.query({ - query: 'SELECT VALUE COUNT(1) FROM c WHERE c.productId = @productId', - parameters: [{ name: '@productId', value: productId }], - }) - .fetchAll(); - const { resources: completedRes } = await container() - .items.query({ - query: - "SELECT VALUE COUNT(1) FROM c WHERE c.productId = @productId AND c.status IN ('signed_up', 'subscribed', 'rewarded')", - parameters: [{ name: '@productId', value: productId }], - }) - .fetchAll(); - const { resources: rewardedRes } = await container() - .items.query({ - query: - "SELECT VALUE COUNT(1) FROM c WHERE c.productId = @productId AND c.status = 'rewarded'", - parameters: [{ name: '@productId', value: productId }], - }) - .fetchAll(); - return { - total: totalRes[0] ?? 0, - completed: completedRes[0] ?? 0, - rewarded: rewardedRes[0] ?? 0, - }; + const total = await collection().count({ productId }); + const completed = await collection().count({ + productId, + status: { $in: ['signed_up', 'subscribed', 'rewarded'] }, + }); + const rewarded = await collection().count({ + productId, + status: 'rewarded', + }); + return { total, completed, rewarded }; } diff --git a/services/platform-service/src/modules/sessions/repository.ts b/services/platform-service/src/modules/sessions/repository.ts index 3bf69031..014e08e6 100644 --- a/services/platform-service/src/modules/sessions/repository.ts +++ b/services/platform-service/src/modules/sessions/repository.ts @@ -1,63 +1,43 @@ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import type { SessionDoc } from './types.js'; -const CONTAINER = 'sessions'; - -function container() { - return getContainer(CONTAINER); +function collection() { + return getCollection('sessions', '/userId'); } export async function createSession(doc: SessionDoc): Promise { - const { resource } = await container().items.create(doc); - return resource as SessionDoc; + return collection().create(doc); } export async function getSession(id: string, userId: string): Promise { try { - const { resource } = await container().item(id, userId).read(); - return resource ?? null; + return await collection().findById(id, userId); } catch { return null; } } export async function listUserSessions(userId: string): Promise { - const { resources } = await container() - .items.query( - { - query: - 'SELECT * FROM c WHERE c.userId = @userId AND NOT IS_DEFINED(c.revokedAt) ORDER BY c.lastActiveAt DESC', - parameters: [{ name: '@userId', value: userId }], - }, - { partitionKey: userId } - ) - .fetchAll(); - return resources; + return collection().findMany({ + filter: { userId, revokedAt: { $exists: false } }, + sort: { lastActiveAt: -1 }, + }); } export async function listAllUserSessions(userId: string): Promise { - const { resources } = await container() - .items.query( - { - query: 'SELECT * FROM c WHERE c.userId = @userId ORDER BY c.createdAt DESC', - parameters: [{ name: '@userId', value: userId }], - }, - { partitionKey: userId } - ) - .fetchAll(); - return resources; + return collection().findMany({ + filter: { userId }, + sort: { createdAt: -1 }, + }); } export async function revokeSession(id: string, userId: string): Promise { const session = await getSession(id, userId); if (!session || session.revokedAt) return false; - await container() - .item(id, userId) - .replace({ - ...session, - revokedAt: new Date().toISOString(), - }); + await collection().update(id, userId, { + revokedAt: new Date().toISOString(), + } as Partial); return true; } @@ -68,12 +48,9 @@ export async function revokeAllUserSessions(userId: string): Promise { for (const session of sessions) { try { - await container() - .item(session.id, userId) - .replace({ - ...session, - revokedAt: now, - }); + await collection().update(session.id, userId, { + revokedAt: now, + } as Partial); revoked++; } catch { // best-effort @@ -87,12 +64,9 @@ export async function touchSession(id: string, userId: string): Promise { const session = await getSession(id, userId); if (!session || session.revokedAt) return; - await container() - .item(id, userId) - .replace({ - ...session, - lastActiveAt: new Date().toISOString(), - }); + await collection().update(id, userId, { + lastActiveAt: new Date().toISOString(), + } as Partial); } export async function isSessionRevoked(id: string, userId: string): Promise { diff --git a/services/platform-service/src/modules/status/repository.ts b/services/platform-service/src/modules/status/repository.ts index 10823c63..a26b9c57 100644 --- a/services/platform-service/src/modules/status/repository.ts +++ b/services/platform-service/src/modules/status/repository.ts @@ -1,56 +1,36 @@ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import { NotFoundError } from '../../lib/errors.js'; import type { IncidentDoc } from './types.js'; -const CONTAINER = 'incidents'; - -function container() { - return getContainer(CONTAINER); +function collection() { + return getCollection('incidents', '/productId'); } export async function listIncidents(productId: string, limit = 20): Promise { - const { resources } = await container() - .items.query( - { - query: - 'SELECT TOP @limit * FROM c WHERE c.productId = @productId ORDER BY c.createdAt DESC', - parameters: [ - { name: '@productId', value: productId }, - { name: '@limit', value: limit }, - ], - }, - { partitionKey: productId } - ) - .fetchAll(); - return resources; + return collection().findMany({ + filter: { productId }, + sort: { createdAt: -1 }, + limit, + }); } export async function listActiveIncidents(productId: string): Promise { - const { resources } = await container() - .items.query( - { - query: - 'SELECT * FROM c WHERE c.productId = @productId AND c.status != "resolved" ORDER BY c.createdAt DESC', - parameters: [{ name: '@productId', value: productId }], - }, - { partitionKey: productId } - ) - .fetchAll(); - return resources; + return collection().findMany({ + filter: { productId, status: { $ne: 'resolved' } }, + sort: { createdAt: -1 }, + }); } export async function getIncident(id: string, productId: string): Promise { - const { resource } = await container().item(id, productId).read(); - if (!resource) throw new NotFoundError(`Incident '${id}' not found`); - return resource; + const doc = await collection().findById(id, productId); + if (!doc) throw new NotFoundError(`Incident '${id}' not found`); + return doc; } export async function createIncident(doc: IncidentDoc): Promise { - const { resource } = await container().items.create(doc); - return resource as IncidentDoc; + return collection().create(doc); } export async function updateIncident(doc: IncidentDoc): Promise { - const { resource } = await container().item(doc.id, doc.productId).replace(doc); - return resource as IncidentDoc; + return collection().upsert(doc); }