fix(mail-ingest): send an uploaded .eml as multipart; publish the booking origin as a contract

- The upload posted its FormData through the client's JSON default, so the
  form went as JSON and the server saw no file (400). It goes through
  postMultipart now, like every other upload.
- reservation-origin.ts becomes reservation-origin.contract.ts: the booking
  import sets an origin, and a contract is what another domain may import.
- CI: the client's strict type check holds the whole program with strict
  checks and outgrew the default ~4 GB heap; it gets 8 GB like the server's.
This commit is contained in:
Maurice
2026-11-13 20:49:08 +01:00
parent 4337969c2c
commit 29816f81a9
10 changed files with 40 additions and 9 deletions
+4 -1
View File
@@ -458,7 +458,10 @@ jobs:
# tsconfig.strict.json is the same program with strictNullChecks and
# noImplicitAny; its errors per file may not rise above
# scripts/strict-baseline.json, and a file without an entry, every new
# one included, has to be clean (scripts/strict-lint.mjs).
# one included, has to be clean (scripts/strict-lint.mjs). It holds the
# whole program with strict checks, past Node's default ~4 GB heap.
env:
NODE_OPTIONS: --max-old-space-size=8192
run: cd client && npm run lint:strict
- name: Page pattern check
+21
View File
@@ -0,0 +1,21 @@
// FE-MAILINGEST-API-001
import { describe, expect, it, vi } from 'vitest';
vi.mock('./client', () => ({
default: { get: vi.fn(), post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn() },
postMultipart: vi.fn(async () => ({ duplicate: false })),
}));
import apiClient, { postMultipart } from './client';
import { mailIngestApi } from './mailIngest';
describe('mailIngestApi.upload', () => {
it('FE-MAILINGEST-API-001: sends the .eml as a multipart form, never through the JSON post', async () => {
const file = new File(['From: a@b.c\r\n\r\nhi'], 'booking.eml', { type: 'message/rfc822' });
expect(await mailIngestApi.upload(file)).toEqual({ duplicate: false });
const [url, form] = vi.mocked(postMultipart).mock.calls[0];
expect(url).toBe('/mail-ingest/upload');
expect((form as FormData).get('file')).toBe(file);
expect(apiClient.post).not.toHaveBeenCalled();
});
});
+3 -2
View File
@@ -1,4 +1,4 @@
import apiClient from './client';
import apiClient, { postMultipart } from './client';
/**
* The booking mail import (server nest/mail-ingest): the person's settings and
@@ -43,6 +43,7 @@ export const mailIngestApi = {
upload: (file: File): Promise<unknown> => {
const form = new FormData();
form.append('file', file);
return apiClient.post('/mail-ingest/upload', form).then(data);
// As multipart: under the client's default JSON content type the form went as JSON, without the file.
return postMultipart('/mail-ingest/upload', form);
},
};
@@ -14,7 +14,7 @@ import { McpToolGuardsService } from '../mcp-shared/mcp-tool-guards.service';
import { PlaceOriginsService } from '../places/place-origins.service';
import { PlacesService } from '../places/places.service';
import { RealtimeService } from '../realtime/realtime.service';
import { withoutOrigin } from '../reservations/reservation-origin';
import { withoutOrigin } from '../reservations/reservation-origin.contract';
import { ReservationsService, type CreateAccommodation } from '../reservations/reservations.service';
import { IMPORT_SOURCE, importExternalId } from './booking-origin';
import { KitineraryExtractorService } from './kitinerary-extractor.service';
@@ -1,4 +1,4 @@
import type { ReservationOrigin } from './reservation-origin';
import type { ReservationOrigin } from './reservation-origin.contract';
/*
* The shapes the reservations service takes in: a booking's endpoints, the
@@ -1,3 +1,9 @@
/*
* The origin an imported booking carries, published by the reservations
* domain: the importers (booking import, mail import) set it, and every other
* write path strips it from what a client sends.
*/
/**
* Where an imported booking came from (`import`, `mail`) and its key there,
* unique per trip. Both or neither; without them the row is a hand-made
@@ -2,7 +2,7 @@ import type { User } from '../../types';
import { CurrentUser } from '../auth-core/current-user.decorator';
import { JwtAuthGuard } from '../auth-core/jwt-auth.guard';
import { RequirePermission, TripAccessGuard } from '../permissions/trip-access.guard';
import { withoutOrigin } from './reservation-origin';
import { withoutOrigin } from './reservation-origin.contract';
import { ReservationWritesService } from './reservation-writes.service';
import {
ReservationCreateDto,
@@ -4,7 +4,7 @@ import { PluginController, PluginMethod } from '../../nest-rpc/rpc-kit/decorator
import type { PluginRpcContext } from '../../nest-rpc/rpc-kit/types';
import { num, schemaMessage } from '../../nest-rpc/rpc-params';
import { RealtimeService } from '../realtime/realtime.service';
import { withoutOrigin } from './reservation-origin';
import { withoutOrigin } from './reservation-origin.contract';
import { ReservationsService } from './reservations.service';
import {
reservationCreateRequestSchema,
@@ -1,4 +1,4 @@
import { originColumns } from './reservation-origin';
import { originColumns } from './reservation-origin.contract';
import type { CreateAccommodation, CreateReservationData, EndpointInput, ReservationEndpoint, UpdateReservationData } from './reservation-inputs';
export type { CreateAccommodation, CreateReservationData, EndpointInput, ReservationEndpoint, UpdateReservationData } from './reservation-inputs';
import { BadRequestException, Injectable } from '@nestjs/common';
@@ -33,7 +33,7 @@ import { HttpException } from '@nestjs/common';
import { resetTestDb } from '../../helpers/test-db';
import { createUser, createTrip, createReservation, createBudgetItem, createPlace, createDay, createDayAccommodation, createDayAssignment, addTripMember } from '../../helpers/factories';
import type { PermissionsService } from '../../../src/nest/permissions/permissions.service';
import { withoutOrigin } from '../../../src/nest/reservations/reservation-origin';
import { withoutOrigin } from '../../../src/nest/reservations/reservation-origin.contract';
import { ReservationsService, type UpdateReservationData } from '../../../src/nest/reservations/reservations.service';
import { ReservationsReadService } from '../../../src/nest/reservations/reservations-read.service';
import type { BudgetService } from '../../../src/nest/budget/budget.service';