feat(peak-sessions): add clientId for idempotent sync, findByClientId repo method, explicit 204 returns

This commit is contained in:
saravanakumardb1 2026-03-01 07:55:02 -08:00
parent 137266a284
commit 383a8dad32
5 changed files with 58 additions and 2 deletions

View File

@ -70,5 +70,6 @@ export async function peakRouteRoutes(app: FastifyInstance) {
const deleted = await repo.deleteRoute(sessionId, route.id);
if (!deleted) throw new NotFoundError('Route delete failed');
reply.code(204);
return;
});
}

View File

@ -120,6 +120,25 @@ describe('CreatePeakSessionSchema', () => {
});
expect(result.success).toBe(false);
});
it('accepts optional clientId for idempotent sync', () => {
const result = CreatePeakSessionSchema.safeParse({
...validMinimal,
clientId: '550e8400-e29b-41d4-a716-446655440000',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.clientId).toBe('550e8400-e29b-41d4-a716-446655440000');
}
});
it('rejects clientId exceeding max length', () => {
const result = CreatePeakSessionSchema.safeParse({
...validMinimal,
clientId: 'x'.repeat(129),
});
expect(result.success).toBe(false);
});
});
// ── UpdatePeakSessionSchema ──

View File

@ -28,6 +28,22 @@ export async function getSession(
}
}
export async function findByClientId(
userId: string,
clientId: string
): Promise<PeakSessionDoc | null> {
const { resources } = await container()
.items.query<PeakSessionDoc>({
query: 'SELECT * FROM c WHERE c.userId = @userId AND c.clientId = @clientId',
parameters: [
{ name: '@userId', value: userId },
{ name: '@clientId', value: clientId },
],
})
.fetchAll();
return resources[0] ?? null;
}
export async function listSessions(
userId: string,
query: PeakSessionQuery

View File

@ -49,7 +49,7 @@ export async function peakSessionRoutes(app: FastifyInstance) {
return session;
});
// Create session
// Create session (idempotent — if clientId is provided and already exists, return existing)
app.post('/peak/sessions', async (req, reply) => {
const auth = await extractAuth(req);
const pid = getRequestProductId(req);
@ -58,10 +58,24 @@ export async function peakSessionRoutes(app: FastifyInstance) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
const input = parsed.data;
// Idempotency check: if clientId provided, look for existing session
if (input.clientId) {
const existing = await repo.findByClientId(auth.sub, input.clientId);
if (existing) {
req.log.info(
{ sessionId: existing.id, clientId: input.clientId },
'Returning existing peak session (idempotent)'
);
return existing;
}
}
const now = new Date().toISOString();
const doc: PeakSessionDoc = {
id: `ps_${crypto.randomUUID()}`,
clientId: input.clientId,
userId: auth.sub,
productId: pid,
activityType: input.activityType,
@ -92,7 +106,10 @@ export async function peakSessionRoutes(app: FastifyInstance) {
updatedAt: now,
};
req.log.info({ sessionId: doc.id, activityType: doc.activityType }, 'Creating peak session');
req.log.info(
{ sessionId: doc.id, clientId: doc.clientId, activityType: doc.activityType },
'Creating peak session'
);
const created = await repo.createSession(doc);
reply.code(201);
return created;
@ -127,5 +144,6 @@ export async function peakSessionRoutes(app: FastifyInstance) {
const deleted = await repo.deleteSession(auth.sub, id);
if (!deleted) throw new NotFoundError('Peak session delete failed');
reply.code(204);
return;
});
}

View File

@ -57,6 +57,7 @@ export interface SkiMetricsDoc {
export interface PeakSessionDoc {
id: string;
clientId?: string;
userId: string;
productId: string;
activityType: ActivityType;
@ -105,6 +106,7 @@ const SkiMetricsSchema = z.object({
});
export const CreatePeakSessionSchema = z.object({
clientId: z.string().max(128).optional(),
activityType: z.enum(ACTIVITY_TYPES),
status: z.enum(SESSION_STATUSES).default('completed'),
startTime: z.string().datetime(),