Platform backbone (plan 2026-10-13-plattform-rueckgrat), all tasks reviewed: - webhooks with delivery table, retries and stable delivery id - calendar handlers; a finished task removes its Outlook event - tasks, automations, inspections, varroa, treatments, feedings, harvests, colonies, wintering, stock and bottling record domain events in the same transaction; legacy post-commit calls removed - row locks through one helper (lockRowForUpdate, lockRowsForShare, lockRowsForUpdate) - job framework with JobRun, cli exit codes, cleanup, task templates, daily evaluation, hourly app-backup and the rekey job - system API and page "Verwaltung > System" with retry - @nestjs/schedule removed, guard tests against in-process schedulers Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), all tasks reviewed: - inspection photo offline without metadata - bulk capture for treatments, feedings and varroa (backend + frontend) - offline stand mode with service worker, offline pages and packs - offline session, cold start and logout on shared devices Docs: ARCHITECTURE.md assumptions and sections updated, TASKS.md. Gates: backend lint, typecheck, 1770 unit, 420 e2e, build, no schema drift; frontend lint, typecheck, 913 tests, build. Not rolled out. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
import { applyDecorators } from '@nestjs/common';
|
|
import { ApiProperty } from '@nestjs/swagger';
|
|
import { Type } from 'class-transformer';
|
|
import {
|
|
ArrayMaxSize,
|
|
ArrayMinSize,
|
|
ArrayUnique,
|
|
IsArray,
|
|
IsNotEmpty,
|
|
IsString,
|
|
ValidateNested,
|
|
} from 'class-validator';
|
|
import { BULK_MAX_COLONIES } from './bulk-limits';
|
|
|
|
const DUPLICATE_COLONY_MESSAGE = '$property enthält ein Volk doppelt';
|
|
|
|
// The size rule every bulk list shares: at least one colony, at most
|
|
// BULK_MAX_COLONIES.
|
|
function bulkListSize(): PropertyDecorator[] {
|
|
return [
|
|
IsArray({ message: '$property muss eine Liste sein' }),
|
|
ArrayMinSize(1, { message: '$property braucht mindestens ein Volk' }),
|
|
ArrayMaxSize(BULK_MAX_COLONIES, {
|
|
message: `$property darf höchstens ${BULK_MAX_COLONIES} Völker enthalten`,
|
|
}),
|
|
];
|
|
}
|
|
|
|
// The rule for one colony id - in colonyIds (each) and in every entry - so an
|
|
// empty id is invalid input on both paths, never an "unknown colony".
|
|
function colonyIdChecks(each: boolean): PropertyDecorator[] {
|
|
return [
|
|
IsString({ each, message: '$property enthält eine ungültige Volk-ID' }),
|
|
IsNotEmpty({ each, message: '$property enthält eine leere Volk-ID' }),
|
|
];
|
|
}
|
|
|
|
/** `colonyIds` of a bulk entry with shared values: distinct, non-empty ids. */
|
|
export function IsBulkColonyIds(): PropertyDecorator {
|
|
return applyDecorators(
|
|
ApiProperty({
|
|
type: [String],
|
|
minItems: 1,
|
|
maxItems: BULK_MAX_COLONIES,
|
|
uniqueItems: true,
|
|
description: `Die Völker, für die derselbe Eintrag gilt (1 bis ${BULK_MAX_COLONIES}, jedes nur einmal)`,
|
|
}),
|
|
...bulkListSize(),
|
|
ArrayUnique({ message: DUPLICATE_COLONY_MESSAGE }),
|
|
...colonyIdChecks(true),
|
|
);
|
|
}
|
|
|
|
/** `colonyId` of one entry in `entries`: a non-empty id. */
|
|
export function IsBulkEntryColonyId(): PropertyDecorator {
|
|
return applyDecorators(
|
|
ApiProperty({ description: 'Das Volk dieses Eintrags' }),
|
|
...colonyIdChecks(false),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* `entries` of a bulk entry with one value set per colony: each entry is
|
|
* validated as its class, and each colony appears once.
|
|
*/
|
|
export function IsBulkEntries(
|
|
entryType: new () => { colonyId: string },
|
|
): PropertyDecorator {
|
|
return applyDecorators(
|
|
ApiProperty({
|
|
type: [entryType],
|
|
minItems: 1,
|
|
maxItems: BULK_MAX_COLONIES,
|
|
description: `Ein Eintrag je Volk (1 bis ${BULK_MAX_COLONIES}, jedes Volk nur einmal)`,
|
|
}),
|
|
...bulkListSize(),
|
|
// class-validator skips null entries before calling the selector.
|
|
ArrayUnique((entry: { colonyId?: unknown }) => entry.colonyId, {
|
|
message: DUPLICATE_COLONY_MESSAGE,
|
|
}),
|
|
ValidateNested({ each: true }),
|
|
Type(() => entryType),
|
|
);
|
|
}
|