|
| 1 | +// SPDX-License-Identifier: Apache-2.0 |
| 2 | +// Copyright 2026 Aaron K. Clark |
| 3 | +"use strict"; |
| 4 | + |
| 5 | +const { z } = require('zod'); |
| 6 | + |
| 7 | +const intIdParam = z.object({ |
| 8 | + id: z.coerce.number().int().positive(), |
| 9 | +}); |
| 10 | + |
| 11 | +const isoDatetime = z.string().datetime({ |
| 12 | + offset: true, |
| 13 | + message: 'Must be an ISO 8601 datetime (e.g. 2026-05-16T09:00:00Z).', |
| 14 | +}); |
| 15 | + |
| 16 | +/** |
| 17 | + * POST /v1/timeentry body. teCompId is optional for non-master keys |
| 18 | + * (defaults to authKey's company) and required for master keys |
| 19 | + * (controller enforces). teCustId + teStartedAt are required; |
| 20 | + * teEndedAt is optional (in-flight entries allowed). |
| 21 | + * |
| 22 | + * Server-managed fields (teId, teMinutes, teArch) are not accepted |
| 23 | + * from the body. |
| 24 | + */ |
| 25 | +const createTimeEntryBody = z.object({ |
| 26 | + teCustId: z.coerce.number().int().positive(), |
| 27 | + teCompId: z.coerce.number().int().positive().optional(), |
| 28 | + teDescription: z.string().max(10000).optional(), |
| 29 | + teStartedAt: isoDatetime, |
| 30 | + teEndedAt: isoDatetime.optional(), |
| 31 | + teBillable: z.boolean().optional(), |
| 32 | +}).strict({ |
| 33 | + message: 'Unexpected field in body. Whitelist: teCustId, teCompId, teDescription, teStartedAt, teEndedAt, teBillable.', |
| 34 | +}); |
| 35 | + |
| 36 | +/** |
| 37 | + * PATCH /v1/timeentry/:id body. None of the fields are required — |
| 38 | + * a PATCH is a partial update — but at least one must be present. |
| 39 | + * The controller already handles the "no updatable fields" case |
| 40 | + * with a 400; the schema just enforces shape. |
| 41 | + */ |
| 42 | +const updateTimeEntryBody = z.object({ |
| 43 | + teDescription: z.string().max(10000).optional(), |
| 44 | + teStartedAt: isoDatetime.optional(), |
| 45 | + teEndedAt: isoDatetime.nullable().optional(), |
| 46 | + teBillable: z.boolean().optional(), |
| 47 | +}).strict({ |
| 48 | + message: 'Unexpected field in body. Whitelist: teDescription, teStartedAt, teEndedAt, teBillable.', |
| 49 | +}); |
| 50 | + |
| 51 | +const listByCompanyQuery = z.object({ |
| 52 | + customerId: z.coerce.number().int().positive().optional(), |
| 53 | + from: isoDatetime.optional(), |
| 54 | + to: isoDatetime.optional(), |
| 55 | + limit: z.coerce.number().int().positive().max(500).optional(), |
| 56 | + offset: z.coerce.number().int().nonnegative().optional(), |
| 57 | +}).strict({ |
| 58 | + message: 'Unexpected query parameter. Allowed: customerId, from, to, limit, offset.', |
| 59 | +}); |
| 60 | + |
| 61 | +module.exports = { |
| 62 | + intIdParam, |
| 63 | + createTimeEntryBody, |
| 64 | + updateTimeEntryBody, |
| 65 | + listByCompanyQuery, |
| 66 | +}; |
0 commit comments