diff --git a/packages/datastore/src/types.ts b/packages/datastore/src/types.ts index bc9b34b2..02fd04e9 100644 --- a/packages/datastore/src/types.ts +++ b/packages/datastore/src/types.ts @@ -10,7 +10,6 @@ export interface BaseDocument { id: string; productId: string; - [key: string]: unknown; } // ── Filter operators ──────────────────────────────────────────────────────── diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 248a9cb8..80ea7a16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -558,6 +558,9 @@ importers: '@bytelyst/cosmos': specifier: workspace:* version: link:../../packages/cosmos + '@bytelyst/datastore': + specifier: workspace:* + version: link:../../packages/datastore '@bytelyst/errors': specifier: workspace:* version: link:../../packages/errors diff --git a/services/platform-service/package.json b/services/platform-service/package.json index e5d287cd..b1175c80 100644 --- a/services/platform-service/package.json +++ b/services/platform-service/package.json @@ -19,6 +19,7 @@ "@bytelyst/blob": "workspace:*", "@bytelyst/config": "workspace:*", "@bytelyst/cosmos": "workspace:*", + "@bytelyst/datastore": "workspace:*", "@bytelyst/errors": "workspace:*", "@bytelyst/events": "workspace:*", "@bytelyst/fastify-core": "workspace:*", diff --git a/services/platform-service/src/lib/datastore.ts b/services/platform-service/src/lib/datastore.ts new file mode 100644 index 00000000..0e7cf5c4 --- /dev/null +++ b/services/platform-service/src/lib/datastore.ts @@ -0,0 +1,67 @@ +/** + * Cloud-agnostic datastore bridge for platform-service. + * + * Wraps @bytelyst/datastore with platform-service container registry config. + * Repositories import getCollection() from here instead of getContainer() from cosmos. + * + * Migration: Replace `import { getContainer } from '../../lib/cosmos.js'` + * with `import { getCollection } from '../../lib/datastore.js'` + */ + +import { + type DatastoreProvider, + type DocumentCollection, + type BaseDocument, + setDatastore, + CosmosDatastoreProvider, + MemoryDatastoreProvider, +} from '@bytelyst/datastore'; + +let _provider: DatastoreProvider | null = null; + +/** + * Initialize the datastore provider. + * Call once at service startup (before any repository calls). + */ +export function initDatastore(): DatastoreProvider { + if (_provider) return _provider; + + const dbProvider = (process.env.DB_PROVIDER || 'cosmos').toLowerCase(); + + if (dbProvider === 'memory') { + _provider = new MemoryDatastoreProvider(); + } else { + _provider = new CosmosDatastoreProvider(); + } + + setDatastore(_provider); + return _provider; +} + +/** + * Inject a provider directly (for testing). + */ +export function setProvider(provider: DatastoreProvider): void { + _provider = provider; +} + +/** + * Get a typed collection from the datastore. + * Drop-in replacement for getContainer() — returns a DocumentCollection instead of a Cosmos Container. + */ +export function getCollection( + name: string, + partitionKeyPath: string = '/productId' +): DocumentCollection { + if (!_provider) { + initDatastore(); + } + return _provider!.getCollection(name, partitionKeyPath); +} + +/** + * @internal — for testing only + */ +export function _resetDatastoreProvider(): void { + _provider = null; +} diff --git a/services/platform-service/src/modules/comments/repository.ts b/services/platform-service/src/modules/comments/repository.ts index 8650c3a6..1e7f5f2d 100644 --- a/services/platform-service/src/modules/comments/repository.ts +++ b/services/platform-service/src/modules/comments/repository.ts @@ -1,45 +1,36 @@ /** - * Comments repository — Cosmos DB CRUD. + * Comments repository — cloud-agnostic via @bytelyst/datastore. */ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import type { CommentDoc } from './types.js'; -function container() { - return getContainer('tracker_comments'); +function collection() { + return getCollection('tracker_comments', '/id'); } export async function listByItem(itemId: string): Promise { - const { resources } = await container() - .items.query({ - query: 'SELECT * FROM c WHERE c.itemId = @itemId ORDER BY c.createdAt ASC', - parameters: [{ name: '@itemId', value: itemId }], - }) - .fetchAll(); - return resources; + return collection().findMany({ + filter: { itemId }, + sort: { createdAt: 1 }, + }); } export async function getById(id: string): Promise { try { - const { resource } = await container().item(id, id).read(); - return resource ?? null; + return await collection().findById(id, id); } catch { return null; } } export async function create(doc: CommentDoc): Promise { - const { resource } = await container().items.create(doc); - return resource as CommentDoc; + 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 CommentDoc; + return await collection().update(id, id, { ...updates, updatedAt: new Date().toISOString() }); } catch { return null; } @@ -47,7 +38,7 @@ export async function update(id: string, updates: Partial): Promise< export async function remove(id: string): Promise { try { - await container().item(id, id).delete(); + await collection().delete(id, id); return true; } catch { return false; @@ -55,11 +46,5 @@ export async function remove(id: string): Promise { } export async function countByItem(itemId: string): Promise { - const { resources } = await container() - .items.query({ - query: 'SELECT VALUE COUNT(1) FROM c WHERE c.itemId = @itemId', - parameters: [{ name: '@itemId', value: itemId }], - }) - .fetchAll(); - return resources[0] ?? 0; + return collection().count({ itemId }); } diff --git a/services/platform-service/src/modules/flags/repository.ts b/services/platform-service/src/modules/flags/repository.ts index 2bfb0347..c42d60a6 100644 --- a/services/platform-service/src/modules/flags/repository.ts +++ b/services/platform-service/src/modules/flags/repository.ts @@ -1,40 +1,29 @@ /** - * Feature flags repository — Cosmos DB CRUD. + * Feature flags repository — cloud-agnostic via @bytelyst/datastore. */ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import type { FeatureFlagDoc } from './types.js'; -function container() { - return getContainer('feature_flags'); +function collection() { + return getCollection('feature_flags', '/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.key ASC', - parameters: [{ name: '@productId', value: productId }], - }) - .fetchAll(); - return resources; + return collection().findMany({ + filter: { productId }, + sort: { key: 1 }, + }); } export async function getByKey(key: string, productId: string): Promise { - const { resources } = await container() - .items.query({ - query: 'SELECT * FROM c WHERE c.productId = @productId AND c.key = @key', - parameters: [ - { name: '@productId', value: productId }, - { name: '@key', value: key }, - ], - }) - .fetchAll(); - return resources[0] ?? null; + return collection().findOne({ + filter: { productId, key }, + }); } export async function create(doc: FeatureFlagDoc): Promise { - const { resource } = await container().items.create(doc); - return resource as FeatureFlagDoc; + return collection().create(doc); } export async function update( @@ -42,11 +31,7 @@ export async function update( 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 FeatureFlagDoc; + return await collection().update(id, id, { ...updates, updatedAt: new Date().toISOString() }); } catch { return null; } @@ -54,7 +39,7 @@ export async function update( export async function remove(id: string): Promise { try { - await container().item(id, id).delete(); + await collection().delete(id, id); return true; } catch { return false; diff --git a/services/platform-service/src/modules/products/repository.test.ts b/services/platform-service/src/modules/products/repository.test.ts index 1c632e67..f1387d20 100644 --- a/services/platform-service/src/modules/products/repository.test.ts +++ b/services/platform-service/src/modules/products/repository.test.ts @@ -1,24 +1,10 @@ /** - * Repository tests for products module — mocked Cosmos DB. + * Repository tests for products 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 { getAll, getById, create, update } from './repository.js'; import type { ProductDoc } from './types.js'; @@ -37,20 +23,27 @@ const baseProduct: ProductDoc = { updatedAt: '2026-02-16T00:00:00Z', }; +let memProvider: MemoryDatastoreProvider; + describe('products repository', () => { beforeEach(() => { - vi.clearAllMocks(); + memProvider = new MemoryDatastoreProvider(); + setProvider(memProvider); + }); + + afterAll(() => { + _resetDatastoreProvider(); }); describe('getAll', () => { it('returns all products', async () => { - mockFetchAll.mockResolvedValue({ resources: [baseProduct] }); + await create(baseProduct); const result = await getAll(); - expect(result).toEqual([baseProduct]); + expect(result).toHaveLength(1); + expect(result[0]!.id).toBe('lysnrai'); }); it('returns empty array when no products', async () => { - mockFetchAll.mockResolvedValue({ resources: [] }); const result = await getAll(); expect(result).toEqual([]); }); @@ -58,27 +51,19 @@ describe('products repository', () => { describe('getById', () => { it('returns product when found', async () => { - mockRead.mockResolvedValue({ resource: baseProduct }); + await create(baseProduct); const result = await getById('lysnrai'); expect(result).toEqual(baseProduct); }); it('returns null when not found', async () => { - mockRead.mockRejectedValue(new Error('Not found')); const result = await getById('nonexistent'); expect(result).toBeNull(); }); - - it('returns null when resource is undefined', async () => { - mockRead.mockResolvedValue({ resource: undefined }); - const result = await getById('lysnrai'); - expect(result).toBeNull(); - }); }); describe('create', () => { it('creates and returns product', async () => { - mockCreate.mockResolvedValue({ resource: baseProduct }); const result = await create(baseProduct); expect(result).toEqual(baseProduct); }); @@ -86,21 +71,14 @@ describe('products repository', () => { describe('update', () => { it('merges updates and returns product', async () => { - mockRead.mockResolvedValue({ resource: baseProduct }); - const updated = { ...baseProduct, displayName: 'LysnrAI Pro' }; - mockReplace.mockResolvedValue({ resource: updated }); + await create(baseProduct); const result = await update('lysnrai', { displayName: 'LysnrAI Pro' }); - expect(result).toEqual(updated); + expect(result).not.toBeNull(); + expect(result!.displayName).toBe('LysnrAI Pro'); + expect(result!.id).toBe('lysnrai'); }); it('returns null when not found', async () => { - mockRead.mockResolvedValue({ resource: undefined }); - const result = await update('nonexistent', { displayName: 'Test' }); - expect(result).toBeNull(); - }); - - it('returns null on error', async () => { - mockRead.mockRejectedValue(new Error('Not found')); const result = await update('nonexistent', { displayName: 'Test' }); expect(result).toBeNull(); }); diff --git a/services/platform-service/src/modules/products/repository.ts b/services/platform-service/src/modules/products/repository.ts index 9f8f12e2..b95b2897 100644 --- a/services/platform-service/src/modules/products/repository.ts +++ b/services/platform-service/src/modules/products/repository.ts @@ -1,36 +1,31 @@ /** - * Products repository — Cosmos DB CRUD. + * Products repository — cloud-agnostic via @bytelyst/datastore. * Products are the central registry; productId is the partition key. */ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import type { ProductDoc } from './types.js'; -function container() { - return getContainer('products'); +function collection() { + return getCollection('products', '/productId'); } export async function getAll(): Promise { - const { resources } = await container() - .items.query({ - query: 'SELECT * FROM c ORDER BY c.displayName ASC', - }) - .fetchAll(); - return resources; + return collection().findMany({ + sort: { displayName: 1 }, + }); } export async function getById(productId: string): Promise { try { - const { resource } = await container().item(productId, productId).read(); - return resource ?? null; + return await collection().findById(productId, productId); } catch { return null; } } export async function create(doc: ProductDoc): Promise { - const { resource } = await container().items.create(doc); - return resource as ProductDoc; + return collection().create(doc); } export async function update( @@ -38,11 +33,10 @@ export async function update( updates: Partial ): Promise { try { - const { resource: existing } = await container().item(productId, productId).read(); - if (!existing) return null; - const merged = { ...existing, ...updates, updatedAt: new Date().toISOString() }; - const { resource } = await container().item(productId, productId).replace(merged); - return resource as ProductDoc; + return await collection().update(productId, productId, { + ...updates, + updatedAt: new Date().toISOString(), + }); } catch { return null; } diff --git a/services/platform-service/src/modules/settings/repository.test.ts b/services/platform-service/src/modules/settings/repository.test.ts index 54295120..635e5aee 100644 --- a/services/platform-service/src/modules/settings/repository.test.ts +++ b/services/platform-service/src/modules/settings/repository.test.ts @@ -1,19 +1,10 @@ /** - * Repository tests for settings module — mocked Cosmos DB. + * Repository tests for settings module — in-memory datastore. */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -const mockRead = vi.fn(); -const mockUpsert = vi.fn(); - -vi.mock('../../lib/cosmos.js', () => ({ - getContainer: vi.fn(() => ({ - items: { upsert: mockUpsert }, - item: () => ({ read: mockRead }), - })), -})); - +import { describe, it, expect, beforeEach, afterAll } from 'vitest'; +import { MemoryDatastoreProvider } from '@bytelyst/datastore'; +import { setProvider, _resetDatastoreProvider } from '../../lib/datastore.js'; import { getSettingsId, getByUserId, upsert } from './repository.js'; import type { UserSettingsDoc } from './types.js'; @@ -29,7 +20,11 @@ const baseSettings: UserSettingsDoc = { describe('settings repository', () => { beforeEach(() => { - vi.clearAllMocks(); + setProvider(new MemoryDatastoreProvider()); + }); + + afterAll(() => { + _resetDatastoreProvider(); }); describe('getSettingsId', () => { @@ -44,19 +39,12 @@ describe('settings repository', () => { describe('getByUserId', () => { it('returns settings when found', async () => { - mockRead.mockResolvedValue({ resource: baseSettings }); + await upsert(baseSettings); const result = await getByUserId('user_1', 'lysnrai'); expect(result).toEqual(baseSettings); }); it('returns null when not found', async () => { - mockRead.mockRejectedValue(new Error('Not found')); - const result = await getByUserId('user_1', 'lysnrai'); - expect(result).toBeNull(); - }); - - it('returns null when resource is undefined', async () => { - mockRead.mockResolvedValue({ resource: undefined }); const result = await getByUserId('user_1', 'lysnrai'); expect(result).toBeNull(); }); @@ -64,7 +52,6 @@ describe('settings repository', () => { describe('upsert', () => { it('upserts and returns settings', async () => { - mockUpsert.mockResolvedValue({ resource: baseSettings }); const result = await upsert(baseSettings); expect(result).toEqual(baseSettings); }); diff --git a/services/platform-service/src/modules/settings/repository.ts b/services/platform-service/src/modules/settings/repository.ts index ea7ba268..8df06de3 100644 --- a/services/platform-service/src/modules/settings/repository.ts +++ b/services/platform-service/src/modules/settings/repository.ts @@ -1,12 +1,12 @@ /** - * User settings repository — Cosmos DB CRUD for settings + device overrides. + * User settings repository — cloud-agnostic via @bytelyst/datastore. */ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import type { UserSettingsDoc } from './types.js'; -function container() { - return getContainer('settings'); +function collection() { + return getCollection('settings', '/userId'); } export function getSettingsId(productId: string, userId: string): string { @@ -19,14 +19,12 @@ export async function getByUserId( ): Promise { const id = getSettingsId(productId, userId); try { - const { resource } = await container().item(id, userId).read(); - return resource ?? null; + return await collection().findById(id, userId); } catch { return null; } } export async function upsert(doc: UserSettingsDoc): Promise { - const { resource } = await container().items.upsert(doc); - return resource!; + return collection().upsert(doc); } diff --git a/services/platform-service/src/modules/themes/repository.ts b/services/platform-service/src/modules/themes/repository.ts index 0e65e552..8f392993 100644 --- a/services/platform-service/src/modules/themes/repository.ts +++ b/services/platform-service/src/modules/themes/repository.ts @@ -1,65 +1,53 @@ /** - * Theme repository — Cosmos DB. + * Theme repository — cloud-agnostic via @bytelyst/datastore. */ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import type { ThemeDoc } from './types.js'; -function container() { - return getContainer('themes'); +function collection() { + return getCollection('themes', '/id'); } export async function getAll(productId: string): Promise { - const { resources } = await container() - .items.query({ - query: - "SELECT * FROM c WHERE c.productId = @productId AND c.type = 'theme' ORDER BY c.created_at DESC", - parameters: [{ name: '@productId', value: productId }], - }) - .fetchAll(); - return resources; + return collection().findMany({ + filter: { productId, type: 'theme' }, + sort: { created_at: -1 }, + }); } export async function getById(id: string): Promise { try { - const { resource } = await container().item(id, id).read(); - return resource ?? null; + return await collection().findById(id, id); } catch { return null; } } export async function getActive(productId: string): Promise { - const { resources } = await container() - .items.query({ - query: - "SELECT * FROM c WHERE c.productId = @productId AND c.type = 'theme' AND c.is_active = true ORDER BY c.is_default DESC, c.created_at DESC OFFSET 0 LIMIT 1", - parameters: [{ name: '@productId', value: productId }], - }) - .fetchAll(); - return resources[0] ?? null; + return collection().findOne({ + filter: { productId, type: 'theme', is_active: true }, + sort: { is_default: -1, created_at: -1 }, + limit: 1, + }); } export async function create(theme: ThemeDoc): Promise { - const { resource } = await container().items.create(theme); - return resource as ThemeDoc; + return collection().create(theme); } export async function update( id: string, updates: Record ): Promise { - const existing = await getById(id); - if (!existing) return null; - - const merged = { - ...existing, - ...updates, - updated_at: new Date().toISOString(), - }; - - const { resource } = await container().item(id, id).replace(merged); - return resource as ThemeDoc; + try { + return await collection().update(id, id, { + ...updates, + updated_at: new Date().toISOString(), + } as Partial); + } catch { + return null; + } } export async function setActive(id: string, productId: string): Promise { @@ -67,25 +55,15 @@ export async function setActive(id: string, productId: string): Promise if (!theme) return false; // Deactivate all others for this product - const { resources: activeThemes } = await container() - .items.query({ - query: - "SELECT * FROM c WHERE c.productId = @productId AND c.type = 'theme' AND c.is_active = true AND c.id != @excludeId", - parameters: [ - { name: '@productId', value: productId }, - { name: '@excludeId', value: id }, - ], - }) - .fetchAll(); + const activeThemes = await collection().findMany({ + filter: { productId, type: 'theme', is_active: true, id: { $ne: id } }, + }); for (const item of activeThemes) { - await container() - .item(item.id, item.id) - .replace({ - ...item, - is_active: false, - updated_at: new Date().toISOString(), - }); + await collection().update(item.id, item.id, { + is_active: false, + updated_at: new Date().toISOString(), + } as Partial); } // Activate the target theme @@ -95,7 +73,7 @@ export async function setActive(id: string, productId: string): Promise export async function remove(id: string): Promise { try { - await container().item(id, id).delete(); + await collection().delete(id, id); return true; } catch { return false; diff --git a/services/platform-service/src/modules/votes/repository.test.ts b/services/platform-service/src/modules/votes/repository.test.ts index fb351d73..9ae24253 100644 --- a/services/platform-service/src/modules/votes/repository.test.ts +++ b/services/platform-service/src/modules/votes/repository.test.ts @@ -1,23 +1,10 @@ /** - * Repository tests for votes module — mocked Cosmos DB. + * Repository tests for votes module — in-memory datastore. */ -import { describe, it, expect, vi, beforeEach } from 'vitest'; - -const mockFetchAll = vi.fn(); -const mockCreate = vi.fn(); -const mockDelete = vi.fn(); - -vi.mock('../../lib/cosmos.js', () => ({ - getContainer: vi.fn(() => ({ - items: { - query: () => ({ fetchAll: mockFetchAll }), - create: mockCreate, - }, - item: () => ({ delete: mockDelete }), - })), -})); - +import { describe, it, expect, beforeEach, afterAll } from 'vitest'; +import { MemoryDatastoreProvider } from '@bytelyst/datastore'; +import { setProvider, _resetDatastoreProvider } from '../../lib/datastore.js'; import { getByItemAndUser, countByItem, listByItem, create, remove } from './repository.js'; import type { VoteDoc } from './types.js'; @@ -31,18 +18,21 @@ const baseVote: VoteDoc = { describe('votes repository', () => { beforeEach(() => { - vi.clearAllMocks(); + setProvider(new MemoryDatastoreProvider()); + }); + + afterAll(() => { + _resetDatastoreProvider(); }); describe('getByItemAndUser', () => { it('returns vote when found', async () => { - mockFetchAll.mockResolvedValue({ resources: [baseVote] }); + await create(baseVote); const result = await getByItemAndUser('item_1', 'user_1'); expect(result).toEqual(baseVote); }); it('returns null when not found', async () => { - mockFetchAll.mockResolvedValue({ resources: [] }); const result = await getByItemAndUser('item_1', 'user_1'); expect(result).toBeNull(); }); @@ -50,13 +40,13 @@ describe('votes repository', () => { describe('countByItem', () => { it('returns count', async () => { - mockFetchAll.mockResolvedValue({ resources: [5] }); + await create(baseVote); + await create({ ...baseVote, id: 'vote_2', userId: 'user_2' }); const result = await countByItem('item_1'); - expect(result).toBe(5); + expect(result).toBe(2); }); it('returns 0 when no votes', async () => { - mockFetchAll.mockResolvedValue({ resources: [] }); const result = await countByItem('item_1'); expect(result).toBe(0); }); @@ -64,13 +54,13 @@ describe('votes repository', () => { describe('listByItem', () => { it('returns votes for item', async () => { - mockFetchAll.mockResolvedValue({ resources: [baseVote] }); + await create(baseVote); const result = await listByItem('item_1'); - expect(result).toEqual([baseVote]); + expect(result).toHaveLength(1); + expect(result[0]!.id).toBe('vote_1'); }); it('returns empty array when no votes', async () => { - mockFetchAll.mockResolvedValue({ resources: [] }); const result = await listByItem('item_1'); expect(result).toEqual([]); }); @@ -78,7 +68,6 @@ describe('votes repository', () => { describe('create', () => { it('creates and returns vote', async () => { - mockCreate.mockResolvedValue({ resource: baseVote }); const result = await create(baseVote); expect(result).toEqual(baseVote); }); @@ -86,15 +75,14 @@ describe('votes repository', () => { describe('remove', () => { it('deletes and returns true', async () => { - mockDelete.mockResolvedValue(undefined); + await create(baseVote); const result = await remove('vote_1'); expect(result).toBe(true); }); - it('returns false on error', async () => { - mockDelete.mockRejectedValue(new Error('Not found')); - const result = await remove('vote_1'); - expect(result).toBe(false); + it('returns true even for nonexistent (idempotent delete)', async () => { + const result = await remove('nonexistent'); + expect(result).toBe(true); }); }); }); diff --git a/services/platform-service/src/modules/votes/repository.ts b/services/platform-service/src/modules/votes/repository.ts index bc42bc3c..f5956e9d 100644 --- a/services/platform-service/src/modules/votes/repository.ts +++ b/services/platform-service/src/modules/votes/repository.ts @@ -1,55 +1,38 @@ /** - * Votes repository — Cosmos DB CRUD. + * Votes repository — cloud-agnostic via @bytelyst/datastore. */ -import { getContainer } from '../../lib/cosmos.js'; +import { getCollection } from '../../lib/datastore.js'; import type { VoteDoc } from './types.js'; -function container() { - return getContainer('tracker_votes'); +function collection() { + return getCollection('tracker_votes', '/id'); } export async function getByItemAndUser(itemId: string, userId: string): Promise { - const { resources } = await container() - .items.query({ - query: 'SELECT * FROM c WHERE c.itemId = @itemId AND c.userId = @userId', - parameters: [ - { name: '@itemId', value: itemId }, - { name: '@userId', value: userId }, - ], - }) - .fetchAll(); - return resources[0] ?? null; + return collection().findOne({ + filter: { itemId, userId }, + }); } export async function countByItem(itemId: string): Promise { - const { resources } = await container() - .items.query({ - query: 'SELECT VALUE COUNT(1) FROM c WHERE c.itemId = @itemId', - parameters: [{ name: '@itemId', value: itemId }], - }) - .fetchAll(); - return resources[0] ?? 0; + return collection().count({ itemId }); } export async function listByItem(itemId: string): Promise { - const { resources } = await container() - .items.query({ - query: 'SELECT * FROM c WHERE c.itemId = @itemId ORDER BY c.createdAt DESC', - parameters: [{ name: '@itemId', value: itemId }], - }) - .fetchAll(); - return resources; + return collection().findMany({ + filter: { itemId }, + sort: { createdAt: -1 }, + }); } export async function create(doc: VoteDoc): Promise { - const { resource } = await container().items.create(doc); - return resource as VoteDoc; + return collection().create(doc); } export async function remove(id: string): Promise { try { - await container().item(id, id).delete(); + await collection().delete(id, id); return true; } catch { return false;