feat(peak-sessions): add clientId for idempotent sync, findByClientId repo method, explicit 204 returns
This commit is contained in:
parent
137266a284
commit
383a8dad32
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@ -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 ──
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user