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);
|
const deleted = await repo.deleteRoute(sessionId, route.id);
|
||||||
if (!deleted) throw new NotFoundError('Route delete failed');
|
if (!deleted) throw new NotFoundError('Route delete failed');
|
||||||
reply.code(204);
|
reply.code(204);
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -120,6 +120,25 @@ describe('CreatePeakSessionSchema', () => {
|
|||||||
});
|
});
|
||||||
expect(result.success).toBe(false);
|
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 ──
|
// ── 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(
|
export async function listSessions(
|
||||||
userId: string,
|
userId: string,
|
||||||
query: PeakSessionQuery
|
query: PeakSessionQuery
|
||||||
|
|||||||
@ -49,7 +49,7 @@ export async function peakSessionRoutes(app: FastifyInstance) {
|
|||||||
return session;
|
return session;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create session
|
// Create session (idempotent — if clientId is provided and already exists, return existing)
|
||||||
app.post('/peak/sessions', async (req, reply) => {
|
app.post('/peak/sessions', async (req, reply) => {
|
||||||
const auth = await extractAuth(req);
|
const auth = await extractAuth(req);
|
||||||
const pid = getRequestProductId(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('; '));
|
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||||
}
|
}
|
||||||
const input = parsed.data;
|
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 now = new Date().toISOString();
|
||||||
|
|
||||||
const doc: PeakSessionDoc = {
|
const doc: PeakSessionDoc = {
|
||||||
id: `ps_${crypto.randomUUID()}`,
|
id: `ps_${crypto.randomUUID()}`,
|
||||||
|
clientId: input.clientId,
|
||||||
userId: auth.sub,
|
userId: auth.sub,
|
||||||
productId: pid,
|
productId: pid,
|
||||||
activityType: input.activityType,
|
activityType: input.activityType,
|
||||||
@ -92,7 +106,10 @@ export async function peakSessionRoutes(app: FastifyInstance) {
|
|||||||
updatedAt: now,
|
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);
|
const created = await repo.createSession(doc);
|
||||||
reply.code(201);
|
reply.code(201);
|
||||||
return created;
|
return created;
|
||||||
@ -127,5 +144,6 @@ export async function peakSessionRoutes(app: FastifyInstance) {
|
|||||||
const deleted = await repo.deleteSession(auth.sub, id);
|
const deleted = await repo.deleteSession(auth.sub, id);
|
||||||
if (!deleted) throw new NotFoundError('Peak session delete failed');
|
if (!deleted) throw new NotFoundError('Peak session delete failed');
|
||||||
reply.code(204);
|
reply.code(204);
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,6 +57,7 @@ export interface SkiMetricsDoc {
|
|||||||
|
|
||||||
export interface PeakSessionDoc {
|
export interface PeakSessionDoc {
|
||||||
id: string;
|
id: string;
|
||||||
|
clientId?: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
productId: string;
|
productId: string;
|
||||||
activityType: ActivityType;
|
activityType: ActivityType;
|
||||||
@ -105,6 +106,7 @@ const SkiMetricsSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const CreatePeakSessionSchema = z.object({
|
export const CreatePeakSessionSchema = z.object({
|
||||||
|
clientId: z.string().max(128).optional(),
|
||||||
activityType: z.enum(ACTIVITY_TYPES),
|
activityType: z.enum(ACTIVITY_TYPES),
|
||||||
status: z.enum(SESSION_STATUSES).default('completed'),
|
status: z.enum(SESSION_STATUSES).default('completed'),
|
||||||
startTime: z.string().datetime(),
|
startTime: z.string().datetime(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user