feat: T6 wave 1 – compliance schema, permissions, lock order, backup
Backend Image / build-and-push (push) Successful in 6m39s
Frontend Image / build-and-push (push) Successful in 11m25s

- Migrations (additive): veterinary medicines with lots and movement
  ledger, treatment medicine/lot/dose links, label particulars, authority
  notices, apiary restrictions, compliance settings and acknowledgements;
  notification category COMPLIANCE, added to non-empty Telegram lists.
- Permissions veterinary-medicines:* and compliance:* with role grants.
- LOCKABLE_TABLES: Apiary locks before Colony; T6 tables appended.
- Backup exports and restores the new tables in FK order; unsealed files
  skip them and report dropped medicine links; an open TSK draft in the
  target wins.
- describeNullContracts shared helper; Telegram backlog e2e no longer
  depends on the time of day.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-15 13:04:13 +02:00
1 parent 7a1480138d
commit 8a31c42578
23 files changed
+2028 -84

No files matched your search

@@ -1,5 +1,3 @@
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { UpdateApiaryDto } from '../../apiaries/dto/update-apiary.dto';
import { UpdateAutomationRuleDto } from '../../automation/dto/update-automation-rule.dto';
import { UpdateBackupSettingsDto } from '../../backup/dto/update-backup-settings.dto';
@@ -35,15 +33,10 @@ import { UpdateUserDto } from '../../users/dto/update-user.dto';
import { UpdateVarroaMeasurementDto } from '../../varroa/dto/update-varroa-measurement.dto';
import { UpdateWebhookDto } from '../../webhooks/dto/update-webhook.dto';
import { UpdateWinterCheckDto } from '../../winter-checks/dto/update-winter-check.dto';
// PATCH contract of every update DTO: an omitted field stays unchanged, null
// clears a field whose column is nullable, and null on a column that cannot
// hold it is a 400 - never silently ignored, never a 500 from the write.
interface NullContract {
dto: new () => object;
notNull: string[];
clearable: string[];
}
import {
describeNullContracts,
NullContract,
} from './null-contract.spec-helper';
const contracts: Record<string, NullContract> = {
UpdateApiaryDto: {
@@ -374,37 +367,4 @@ const contracts: Record<string, NullContract> = {
},
};
async function errorPropertiesFor(
dto: new () => object,
body: Record<string, unknown>,
): Promise<string[]> {
const errors = await validate(plainToInstance(dto, body), {
whitelist: true,
forbidNonWhitelisted: true,
});
return errors.map((error) => error.property);
}
describe('update DTO null handling', () => {
describe.each(Object.entries(contracts))('%s', (_name, contract) => {
it('accepts an empty patch', async () => {
await expect(errorPropertiesFor(contract.dto, {})).resolves.toEqual([]);
});
if (contract.notNull.length > 0) {
it.each(contract.notNull)('rejects null for %s', async (field) => {
await expect(
errorPropertiesFor(contract.dto, { [field]: null }),
).resolves.toEqual([field]);
});
}
if (contract.clearable.length > 0) {
it.each(contract.clearable)('accepts null for %s', async (field) => {
await expect(
errorPropertiesFor(contract.dto, { [field]: null }),
).resolves.toEqual([]);
});
}
});
});
describeNullContracts('update DTO null handling', contracts);