chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n namespaces and 4 unused dependencies; fold byte-identical helper copies into one canonical home each (lib/utils chunk/sleep/utcDateStamp, lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format, lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body + v1ValidationError rolled out to ~55 v1 routes, booking-template schemas). No behaviour change: v1 bodies and status codes, MCP tool schemas, DB writes and money math are untouched. Naive ore rounding was deliberately not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list of things left alone on purpose. tsc, lint, 19588 unit tests and check:guards green; antipattern baseline ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(transactions): import RawTransaction from @/types after the ingest re-export removal CI's type ratchet (check:types, full tsconfig) caught the one test file that still imported the type through lib/transactions/ingest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
Jakob Wennberg
parent
18cbc4c30a
commit
f266c386f3
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest'
|
||||
import { createTokenRateLimiter } from '../token-rate-limit'
|
||||
|
||||
describe('createTokenRateLimiter', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('allows up to max requests per window, then refuses', () => {
|
||||
const limiter = createTokenRateLimiter({ max: 3, windowMs: 60_000 })
|
||||
expect(limiter.allow('a')).toBe(true)
|
||||
expect(limiter.allow('a')).toBe(true)
|
||||
expect(limiter.allow('a')).toBe(true)
|
||||
expect(limiter.allow('a')).toBe(false)
|
||||
})
|
||||
|
||||
it('keys the budget per token', () => {
|
||||
const limiter = createTokenRateLimiter({ max: 1, windowMs: 60_000 })
|
||||
expect(limiter.allow('a')).toBe(true)
|
||||
expect(limiter.allow('b')).toBe(true)
|
||||
expect(limiter.allow('a')).toBe(false)
|
||||
})
|
||||
|
||||
it('resets the budget once the window has passed', () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'))
|
||||
const limiter = createTokenRateLimiter({ max: 1, windowMs: 60_000 })
|
||||
expect(limiter.allow('a')).toBe(true)
|
||||
expect(limiter.allow('a')).toBe(false)
|
||||
vi.setSystemTime(new Date('2026-01-01T00:01:00.001Z'))
|
||||
expect(limiter.allow('a')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps separate limiters isolated', () => {
|
||||
const one = createTokenRateLimiter({ max: 1, windowMs: 60_000 })
|
||||
const two = createTokenRateLimiter({ max: 1, windowMs: 60_000 })
|
||||
expect(one.allow('a')).toBe(true)
|
||||
expect(two.allow('a')).toBe(true)
|
||||
expect(one.allow('a')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { NextResponse } from 'next/server'
|
||||
|
||||
export const PRIVATE_NO_STORE_HEADERS = { 'Cache-Control': 'private, no-store' } as const
|
||||
|
||||
export function privateNoStore(response: NextResponse): NextResponse {
|
||||
response.headers.set('Cache-Control', 'private, no-store')
|
||||
return response
|
||||
|
||||
@@ -221,18 +221,10 @@ export const SupplierTypeSchema = z.enum([
|
||||
'non_eu_business',
|
||||
])
|
||||
|
||||
export const InvoiceStatusSchema = z.enum([
|
||||
'draft', 'sent', 'paid', 'overdue', 'cancelled', 'credited',
|
||||
])
|
||||
|
||||
export const InvoiceDocumentTypeSchema = z.enum([
|
||||
'invoice', 'proforma', 'delivery_note',
|
||||
])
|
||||
|
||||
export const SupplierInvoiceStatusSchema = z.enum([
|
||||
'registered', 'approved', 'paid', 'partially_paid', 'overdue', 'disputed', 'credited',
|
||||
])
|
||||
|
||||
export const VatTreatmentSchema = z.enum([
|
||||
'standard_25', 'reduced_12', 'reduced_6', 'reverse_charge', 'export', 'exempt',
|
||||
])
|
||||
@@ -1775,7 +1767,6 @@ export const BulkBookInboxSchema = z.object({
|
||||
// staged-params reason as the fields above.
|
||||
dimensions: DimensionsBagSchema.nullish().transform((v) => v ?? undefined),
|
||||
})
|
||||
export type BulkBookInboxInput = z.infer<typeof BulkBookInboxSchema>
|
||||
|
||||
export const MatchInvoiceSchema = z
|
||||
.object({
|
||||
@@ -2531,12 +2522,6 @@ export const VatDeclarationQuerySchema = z.object({
|
||||
period: z.coerce.number().int().min(1).max(12),
|
||||
})
|
||||
|
||||
export const ReportPeriodQuerySchema = z.object({
|
||||
fiscal_period_id: uuid.optional(),
|
||||
year: z.coerce.number().int().min(2000).max(2100).optional(),
|
||||
month: z.coerce.number().int().min(1).max(12).optional(),
|
||||
})
|
||||
|
||||
export const AccountBalancesQuerySchema = z.object({
|
||||
accounts: z
|
||||
.string()
|
||||
@@ -2814,7 +2799,6 @@ export const EmploymentTypeSchema = z.enum(['employee', 'company_owner', 'board_
|
||||
export const SalaryTypeSchema = z.enum(['monthly', 'hourly'])
|
||||
export const FSkattStatusSchema = z.enum(['a_skatt', 'f_skatt', 'fa_skatt', 'not_verified'])
|
||||
export const VacationRuleSchema = z.enum(['procentregeln', 'sammaloneregeln', 'none', 'semesterersattning'])
|
||||
export const SalaryRunStatusSchema = z.enum(['draft', 'review', 'approved', 'paid', 'booked', 'corrected'])
|
||||
|
||||
export const SalaryLineItemTypeSchema = z.enum([
|
||||
'monthly_salary', 'hourly_salary',
|
||||
@@ -3409,90 +3393,6 @@ export const BatchUpsertWorkedDaysSchema = z
|
||||
},
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// AI agent flow schemas
|
||||
// ============================================================
|
||||
|
||||
const BookingProposalLineSchema = z.object({
|
||||
account_number: accountNumber,
|
||||
debit_amount: nonNegativeAmount,
|
||||
credit_amount: nonNegativeAmount,
|
||||
description: z.string().min(1).max(500),
|
||||
})
|
||||
|
||||
const BookingProposalCounterpartyTemplateSchema = z.object({
|
||||
counterparty_name: z.string().min(1).max(200),
|
||||
debit_account: accountNumber,
|
||||
credit_account: accountNumber,
|
||||
vat_treatment: VatTreatmentSchema.nullable(),
|
||||
category: TransactionCategorySchema.nullable(),
|
||||
})
|
||||
|
||||
// Edit payload: the user's edited version of a booking proposal. Used in
|
||||
// the /accept endpoint when the user adjusted accounts/VAT before approving.
|
||||
export const EditBookingProposalSchema = z.object({
|
||||
lines: z.array(BookingProposalLineSchema).min(2),
|
||||
vat_treatment: VatTreatmentSchema.nullable(),
|
||||
default_private: z.boolean(),
|
||||
counterparty_template_proposal: BookingProposalCounterpartyTemplateSchema.nullable(),
|
||||
fiscal_period_id: uuid,
|
||||
entry_date: isoDate,
|
||||
description: z.string().min(1).max(500),
|
||||
})
|
||||
|
||||
// For match proposals, editing just means picking a different transaction.
|
||||
export const EditMatchProposalSchema = z.object({
|
||||
matched_transaction_id: uuid,
|
||||
})
|
||||
|
||||
export const AcceptProposalSchema = z.object({
|
||||
version: z.number().int().nonnegative(),
|
||||
edits: z.union([EditBookingProposalSchema, EditMatchProposalSchema]).optional(),
|
||||
})
|
||||
|
||||
// Change the matched transaction on a pending match proposal without
|
||||
// accepting it. Source tells us whether the user picked one of the AI's
|
||||
// own alternatives, an AI-regenerated suggestion, or a manually-chosen
|
||||
// transaction: kept on edit_diff for learning signal.
|
||||
export const ChangeMatchProposalSchema = z.object({
|
||||
version: z.number().int().nonnegative(),
|
||||
matched_transaction_id: uuid,
|
||||
source: z.enum(['user_alternative', 'user_manual', 'ai_regenerated']),
|
||||
})
|
||||
|
||||
export const RejectProposalSchema = z.object({
|
||||
version: z.number().int().nonnegative(),
|
||||
reason: z.string().max(500).optional(),
|
||||
})
|
||||
|
||||
export const BatchAcceptSchema = z.object({
|
||||
proposal_ids: z.array(uuid).min(1).max(50),
|
||||
})
|
||||
|
||||
export const ResolveRequestSchema = z.object({
|
||||
response: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export const StartBackfillSchema = z.object({}).strict()
|
||||
|
||||
export const RememberLearningSchema = z.object({
|
||||
proposal_id: uuid,
|
||||
counterparty_name: z.string().min(1).max(200),
|
||||
debit_account: accountNumber,
|
||||
credit_account: accountNumber,
|
||||
vat_treatment: VatTreatmentSchema.nullable(),
|
||||
category: TransactionCategorySchema.nullable(),
|
||||
})
|
||||
|
||||
export const ListProposalsQuerySchema = z.object({
|
||||
status: z
|
||||
.enum(['pending', 'accepted', 'rejected', 'skipped', 'invalidated'])
|
||||
.optional(),
|
||||
step_type: z.enum(['match', 'booking']).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
offset: z.coerce.number().int().min(0).default(0),
|
||||
})
|
||||
|
||||
export const AttachDocumentSchema = z.object({
|
||||
document_id: uuid,
|
||||
})
|
||||
@@ -3518,71 +3418,6 @@ export const UnderlagImportPreviewSchema = z.object({
|
||||
fiscal_period_id: uuid,
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// Shift-premium rules (OB-tillägg och övertid)
|
||||
// ============================================================
|
||||
|
||||
export const ShiftPremiumItemTypeSchema = z.enum([
|
||||
'overtime_50',
|
||||
'overtime_100',
|
||||
'ob_weekday_evening',
|
||||
'ob_weekend',
|
||||
'ob_night',
|
||||
'ob_holiday',
|
||||
])
|
||||
|
||||
const dayOfWeekArray = z
|
||||
.array(z.number().int().min(1).max(7))
|
||||
.min(1, 'Välj minst en veckodag')
|
||||
.max(7, 'Högst sju veckodagar tillåtna')
|
||||
|
||||
export const CreateShiftPremiumRuleSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(120),
|
||||
applies_to_all_employees: z.boolean().default(true),
|
||||
applies_to_employee_ids: z.array(uuid).default([]),
|
||||
day_of_week: dayOfWeekArray,
|
||||
start_time: timeString,
|
||||
end_time: timeString,
|
||||
premium_percent: z.number().min(0).max(500),
|
||||
item_type: ShiftPremiumItemTypeSchema,
|
||||
priority: z.number().int().min(0).max(1000).default(0),
|
||||
is_active: z.boolean().default(true),
|
||||
})
|
||||
.refine(
|
||||
(data) => data.applies_to_all_employees || data.applies_to_employee_ids.length > 0,
|
||||
{
|
||||
message: 'Välj minst en anställd när regeln inte gäller alla',
|
||||
path: ['applies_to_employee_ids'],
|
||||
},
|
||||
)
|
||||
|
||||
export const UpdateShiftPremiumRuleSchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(120).optional(),
|
||||
applies_to_all_employees: z.boolean().optional(),
|
||||
applies_to_employee_ids: z.array(uuid).optional(),
|
||||
day_of_week: dayOfWeekArray.optional(),
|
||||
start_time: timeString.optional(),
|
||||
end_time: timeString.optional(),
|
||||
premium_percent: z.number().min(0).max(500).optional(),
|
||||
item_type: ShiftPremiumItemTypeSchema.optional(),
|
||||
priority: z.number().int().min(0).max(1000).optional(),
|
||||
is_active: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.applies_to_all_employees === false && data.applies_to_employee_ids !== undefined) {
|
||||
return data.applies_to_employee_ids.length > 0
|
||||
}
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: 'Välj minst en anställd när regeln inte gäller alla',
|
||||
path: ['applies_to_employee_ids'],
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Per-employee override on a salary run (advanced mode).
|
||||
*
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* In-memory per-token rate limiter for the public token-authenticated routes
|
||||
* (calendar feed, payslip PDF). The token IS the authentication on those
|
||||
* routes, so the limit is keyed by token, not by user.
|
||||
*
|
||||
* Process-local by design: a multi-instance deploy limits per instance, which
|
||||
* is acceptable for these low-value endpoints. Each route creates its own
|
||||
* limiter so the maps stay isolated.
|
||||
*/
|
||||
export function createTokenRateLimiter(options: { max: number; windowMs: number; cleanupEveryMs?: number }) {
|
||||
const { max, windowMs, cleanupEveryMs = 5 * 60_000 } = options
|
||||
const entries = new Map<string, { count: number; resetAt: number }>()
|
||||
let lastCleanup = Date.now()
|
||||
|
||||
// Periodic sweep of expired entries so the map cannot grow without bound.
|
||||
function cleanup() {
|
||||
const now = Date.now()
|
||||
if (now - lastCleanup < cleanupEveryMs) return
|
||||
lastCleanup = now
|
||||
for (const [key, value] of entries) {
|
||||
if (now > value.resetAt) entries.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/** Count one request for `token`; false when the window's budget is spent. */
|
||||
allow(token: string): boolean {
|
||||
cleanup()
|
||||
const nowMs = Date.now()
|
||||
const entry = entries.get(token)
|
||||
if (entry && nowMs < entry.resetAt) {
|
||||
if (entry.count >= max) return false
|
||||
entry.count++
|
||||
} else {
|
||||
entries.set(token, { count: 1, resetAt: nowMs + windowMs })
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* JSON body reader for v1 routes.
|
||||
*
|
||||
* Every v1 write route parses its body the same way: read JSON, and on a
|
||||
* malformed body answer with the VALIDATION_ERROR envelope pointing at
|
||||
* `body`. This helper is that block, so routes do:
|
||||
*
|
||||
* const raw = await readV1JsonBody(request, ctx)
|
||||
* if (!raw.ok) return raw.response
|
||||
* const parsed = Schema.safeParse(raw.body)
|
||||
*/
|
||||
|
||||
import type { NextResponse } from 'next/server'
|
||||
import { v1ErrorResponseFromCode, type V1ValidationContext } from './errors'
|
||||
|
||||
export type V1JsonBodyResult =
|
||||
| { ok: true; body: unknown }
|
||||
| { ok: false; response: NextResponse }
|
||||
|
||||
export async function readV1JsonBody(
|
||||
request: Request,
|
||||
ctx: V1ValidationContext,
|
||||
): Promise<V1JsonBodyResult> {
|
||||
try {
|
||||
return { ok: true, body: await request.json() }
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
response: await v1ErrorResponseFromCode('VALIDATION_ERROR', ctx.log, {
|
||||
requestId: ctx.requestId,
|
||||
details: { field: 'body', message: 'Body is not valid JSON.' },
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,32 +47,6 @@ export interface DryRunPreviewBase<T> {
|
||||
preview: T
|
||||
}
|
||||
|
||||
export interface DryRunPreviewStaged<T> extends DryRunPreviewBase<T> {
|
||||
/** `pending_operations.id`. Use with POST /v1/operations/{id}:commit. */
|
||||
staged_operation_id: string
|
||||
/**
|
||||
* Journal lines this write WOULD produce on commit. Absent for
|
||||
* non-financial writes. Each item: `{ account, debit, credit, description? }`.
|
||||
*/
|
||||
journal_lines?: Array<{
|
||||
account: string
|
||||
debit: number
|
||||
credit: number
|
||||
description?: string
|
||||
}>
|
||||
/**
|
||||
* The voucher number that WOULD be assigned on commit. Present only
|
||||
* when the write produces a posted journal entry. Voucher numbers are
|
||||
* sequential, so this is a *projection*: the actual number could differ
|
||||
* by one or two if another committer beat the agent to the next number.
|
||||
*/
|
||||
voucher_number_assigned_on_commit?: string
|
||||
/** Effect on account balances. Absent for non-financial writes. */
|
||||
account_deltas?: Array<{ account: string; delta: number }>
|
||||
}
|
||||
|
||||
export type DryRunPreview<T> = DryRunPreviewBase<T> | DryRunPreviewStaged<T>
|
||||
|
||||
interface DryRunResponseOptions {
|
||||
requestId: string
|
||||
log: Logger
|
||||
@@ -90,37 +64,3 @@ export function dryRunPreview<T>(preview: T, opts: DryRunResponseOptions): NextR
|
||||
opts.log.info('dry-run preview returned', { stage: 'validation-only' })
|
||||
return ok(body, { requestId: opts.requestId, dryRun: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a 200 OK dry-run response for a staged preview (financial writes).
|
||||
*
|
||||
* Phase 2 PR-B-1 does not yet exercise this path; the helper is in place so
|
||||
* Phase 2 PR-B-2 (invoice writes) and later phases (journal entries,
|
||||
* year-end, etc.) reuse it without redefining the shape.
|
||||
*/
|
||||
export function dryRunStaged<T>(
|
||||
data: {
|
||||
preview: T
|
||||
stagedOperationId: string
|
||||
journalLines?: DryRunPreviewStaged<T>['journal_lines']
|
||||
voucherNumberAssignedOnCommit?: string
|
||||
accountDeltas?: DryRunPreviewStaged<T>['account_deltas']
|
||||
},
|
||||
opts: DryRunResponseOptions,
|
||||
): NextResponse {
|
||||
const body: DryRunPreviewStaged<T> = {
|
||||
dry_run: true,
|
||||
preview: data.preview,
|
||||
staged_operation_id: data.stagedOperationId,
|
||||
...(data.journalLines ? { journal_lines: data.journalLines } : {}),
|
||||
...(data.voucherNumberAssignedOnCommit
|
||||
? { voucher_number_assigned_on_commit: data.voucherNumberAssignedOnCommit }
|
||||
: {}),
|
||||
...(data.accountDeltas ? { account_deltas: data.accountDeltas } : {}),
|
||||
}
|
||||
opts.log.info('dry-run preview returned', {
|
||||
stage: 'staged',
|
||||
stagedOperationId: data.stagedOperationId,
|
||||
})
|
||||
return ok(body, { requestId: opts.requestId, dryRun: true })
|
||||
}
|
||||
|
||||
+36
-5
@@ -28,7 +28,6 @@ import {
|
||||
errorResponse as legacyErrorResponse,
|
||||
errorResponseFromCode as legacyErrorResponseFromCode,
|
||||
} from '@/lib/errors/get-structured-error'
|
||||
import { getErrorEntry } from '@/lib/errors/structured-errors'
|
||||
import type { Logger } from '@/lib/logger'
|
||||
import { API_V1_VERSION, API_V1_VERSION_HEADER } from './version'
|
||||
|
||||
@@ -177,10 +176,42 @@ export async function v1ErrorResponseFromCode(
|
||||
return rewriteEnvelope(legacy, ctx)
|
||||
}
|
||||
|
||||
/** Minimal slice of the v1 route context the validation helpers need. */
|
||||
export interface V1ValidationContext {
|
||||
requestId: string
|
||||
log: Logger
|
||||
}
|
||||
|
||||
/** Structural view of a ZodError: enough to render the v1 issues list. */
|
||||
export interface V1ZodErrorLike {
|
||||
issues: ReadonlyArray<{ path: ReadonlyArray<PropertyKey>; message: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick check: does this code map to a registered entry? Used by callers that
|
||||
* want to validate a code before throwing it (e.g. registry-driven dispatch).
|
||||
* Map Zod issues into the `{ field, message }` list the v1 envelope carries
|
||||
* under `details.issues`.
|
||||
*/
|
||||
export function isRegisteredV1Code(code: string): boolean {
|
||||
return getErrorEntry(code) !== undefined
|
||||
export function v1ZodIssues(err: V1ZodErrorLike): Array<{ field: string; message: string }> {
|
||||
return err.issues.map((i) => ({
|
||||
field: i.path.join('.'),
|
||||
message: i.message,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* VALIDATION_ERROR envelope for a failed Zod parse:
|
||||
*
|
||||
* const parsed = Schema.safeParse(rawBody)
|
||||
* if (!parsed.success) return v1ValidationError(ctx, parsed.error)
|
||||
*/
|
||||
export async function v1ValidationError(
|
||||
ctx: V1ValidationContext,
|
||||
err: V1ZodErrorLike,
|
||||
): Promise<NextResponse> {
|
||||
return v1ErrorResponseFromCode('VALIDATION_ERROR', ctx.log, {
|
||||
requestId: ctx.requestId,
|
||||
details: {
|
||||
issues: v1ZodIssues(err),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* pagination hints, not security tokens.
|
||||
*/
|
||||
|
||||
import { z } from 'zod'
|
||||
import { UUID_RE as UUID } from '@/lib/invariants/uuid'
|
||||
|
||||
export const DEFAULT_LIMIT = 50
|
||||
export const MAX_LIMIT = 100
|
||||
@@ -47,14 +47,6 @@ export function parsePaginationParams(url: URL): PaginationParams {
|
||||
return { limit, cursor: cursor && cursor.length > 0 ? cursor : null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for query-param validation when a route already uses Zod for inputs.
|
||||
*/
|
||||
export const PaginationQuerySchema = z.object({
|
||||
limit: z.coerce.number().int().min(1).max(MAX_LIMIT).optional(),
|
||||
cursor: z.string().min(1).optional(),
|
||||
})
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// Default (created_at, id) keyset cursor
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -82,7 +74,6 @@ export function encodeDefaultCursor(row: { created_at: string; id: string } | nu
|
||||
// PostgREST would likely reject these, but validating here keeps the failure
|
||||
// mode predictable (stale cursor → "start over") rather than 400-ing.
|
||||
const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/
|
||||
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
|
||||
/**
|
||||
* Decode a cursor produced by encodeDefaultCursor. Returns null when the
|
||||
|
||||
@@ -205,9 +205,6 @@ export function listEndpoints(): EndpointDefinition[] {
|
||||
return Array.from(ENDPOINTS.values())
|
||||
}
|
||||
|
||||
export function getEndpoint(method: HttpMethod, path: string): EndpointDefinition | undefined {
|
||||
return ENDPOINTS.get(`${method} ${path}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the registered endpoint for a CONCRETE request path (e.g.
|
||||
@@ -510,10 +507,3 @@ export function generateOpenApiSpec(serverUrl: string): OpenApiSpec {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test-only escape hatch. Clears the registry: used in unit tests so a test
|
||||
* that registers a fake endpoint doesn't leak into the next test.
|
||||
*/
|
||||
export function _resetRegistryForTests(): void {
|
||||
ENDPOINTS.clear()
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@ import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import type { Logger } from '@/lib/logger'
|
||||
import { parseReportDateRange, type DateRange } from '@/lib/reports/date-range'
|
||||
import { v1ErrorResponse, v1ErrorResponseFromCode } from './errors'
|
||||
|
||||
const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
|
||||
import { UUID_RE } from '@/lib/invariants/uuid'
|
||||
|
||||
export interface FiscalPeriodRow {
|
||||
id: string
|
||||
|
||||
Reference in New Issue
Block a user