feat(platform-service): migrate 7 more repositories to @bytelyst/datastore (exports, plans, status, sessions, referrals, ip-rules, delivery) — 753/753 tests pass

This commit is contained in:
saravanakumardb1 2026-03-02 00:54:55 -08:00
parent 4d126cb051
commit e355cb0c1b
9 changed files with 184 additions and 391 deletions

View File

@ -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<DeliveryLogDoc>('delivery_log', '/pk');
}
export async function createDeliveryLog(doc: DeliveryLogDoc): Promise<DeliveryLogDoc> {
const { resource } = await container().items.create(doc);
return resource as DeliveryLogDoc;
return collection().create(doc);
}
export async function updateDeliveryLog(doc: DeliveryLogDoc): Promise<DeliveryLogDoc> {
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<DeliveryLogDoc[]> {
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<DeliveryLogDoc>({ 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<string, number>;
}> {
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<string, number> = {};
for (const r of countQuery.resources) byStatus[r.status] = r.cnt;
const byChannel: Record<string, number> = {};
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,

View File

@ -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<ExportJobDoc>('export_jobs', '/productId');
}
export async function createExportJob(doc: ExportJobDoc): Promise<ExportJobDoc> {
const { resource } = await container().items.create(doc);
return resource as ExportJobDoc;
return collection().create(doc);
}
export async function getExportJob(id: string, productId: string): Promise<ExportJobDoc | null> {
try {
const { resource } = await container().item(id, productId).read<ExportJobDoc>();
return resource ?? null;
return await collection().findById(id, productId);
} catch {
return null;
}
}
export async function updateExportJob(doc: ExportJobDoc): Promise<ExportJobDoc> {
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<ExportJobDoc[]> {
const { resources } = await container()
.items.query<ExportJobDoc>(
{
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,
});
}

View File

@ -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<IPRuleDoc>('ip_rules', '/productId');
}
export async function listRules(productId: string): Promise<IPRuleDoc[]> {
const { resources } = await container()
.items.query<IPRuleDoc>(
{
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<IPRuleDoc[]> {
// 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<IPRuleDoc>(
{
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<IPRuleDoc> {
const { resource } = await container().items.create(doc);
return resource as IPRuleDoc;
return collection().create(doc);
}
export async function deleteRule(id: string, productId: string): Promise<boolean> {
try {
await container().item(id, productId).delete();
await collection().delete(id, productId);
return true;
} catch {
return false;

View File

@ -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();
});

View File

@ -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<PlanConfig>('plans', '/productId');
}
export async function list(productId: string): Promise<PlanConfig[]> {
const { resources } = await container()
.items.query<PlanConfig>({
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<PlanConfig | null> {
const { resources } = await container()
.items.query<PlanConfig>({
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<PlanConfig> {
const { resource } = await container().items.create(doc);
return resource as PlanConfig;
return collection().create(doc);
}
export async function update(id: string, updates: Partial<PlanConfig>): Promise<PlanConfig | null> {
try {
const { resource: existing } = await container().item(id, id).read<PlanConfig>();
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;
}

View File

@ -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 });
});

View File

@ -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<ReferralDoc>('referrals', '/referrerId');
}
export async function listAll(limit = 100, offset = 0, productId?: string): Promise<ReferralDoc[]> {
const { resources } = await container()
.items.query<ReferralDoc>({
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<ReferralDoc[]> {
const { resources } = await container()
.items.query<ReferralDoc>({
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<ReferralDoc | null> {
const { resources } = await container()
.items.query<ReferralDoc>({
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<ReferralDoc | null> {
try {
const { resource } = await container().item(id, referrerId).read<ReferralDoc>();
return resource ?? null;
return await collection().findById(id, referrerId);
} catch {
return null;
}
}
export async function create(doc: ReferralDoc): Promise<ReferralDoc> {
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<ReferralDoc>
): Promise<ReferralDoc | null> {
try {
const { resource: existing } = await container().item(id, referrerId).read<ReferralDoc>();
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<number>({
query: 'SELECT VALUE COUNT(1) FROM c WHERE c.productId = @productId',
parameters: [{ name: '@productId', value: productId }],
})
.fetchAll();
const { resources: completedRes } = await container()
.items.query<number>({
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<number>({
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 };
}

View File

@ -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<SessionDoc>('sessions', '/userId');
}
export async function createSession(doc: SessionDoc): Promise<SessionDoc> {
const { resource } = await container().items.create(doc);
return resource as SessionDoc;
return collection().create(doc);
}
export async function getSession(id: string, userId: string): Promise<SessionDoc | null> {
try {
const { resource } = await container().item(id, userId).read<SessionDoc>();
return resource ?? null;
return await collection().findById(id, userId);
} catch {
return null;
}
}
export async function listUserSessions(userId: string): Promise<SessionDoc[]> {
const { resources } = await container()
.items.query<SessionDoc>(
{
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<SessionDoc[]> {
const { resources } = await container()
.items.query<SessionDoc>(
{
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<boolean> {
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<SessionDoc>);
return true;
}
@ -68,12 +48,9 @@ export async function revokeAllUserSessions(userId: string): Promise<number> {
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<SessionDoc>);
revoked++;
} catch {
// best-effort
@ -87,12 +64,9 @@ export async function touchSession(id: string, userId: string): Promise<void> {
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<SessionDoc>);
}
export async function isSessionRevoked(id: string, userId: string): Promise<boolean> {

View File

@ -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<IncidentDoc>('incidents', '/productId');
}
export async function listIncidents(productId: string, limit = 20): Promise<IncidentDoc[]> {
const { resources } = await container()
.items.query<IncidentDoc>(
{
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<IncidentDoc[]> {
const { resources } = await container()
.items.query<IncidentDoc>(
{
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<IncidentDoc> {
const { resource } = await container().item(id, productId).read<IncidentDoc>();
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<IncidentDoc> {
const { resource } = await container().items.create(doc);
return resource as IncidentDoc;
return collection().create(doc);
}
export async function updateIncident(doc: IncidentDoc): Promise<IncidentDoc> {
const { resource } = await container().item(doc.id, doc.productId).replace(doc);
return resource as IncidentDoc;
return collection().upsert(doc);
}