fix: export colmn

This commit is contained in:
nathnael
2026-09-02 13:47:23 +00:00
parent 75bbdbf84c
commit a06faa4254
4 changed files with 98 additions and 18 deletions

View File

@@ -122,6 +122,16 @@ export const customersDataset: ExportDataset = {
{ value: 'ethiopian', label: 'Ethiopian' },
{ value: 'foreign', label: 'Foreign' },
] },
// The operational role, NOT `type` above — the list's Role pill. One
// `customer` company routinely holds several profiles, so this asks "who
// does X?" rather than "what kind of company is this?".
{ key: 'profileType', label: 'Role', type: 'select', options: [
{ value: 'importer', label: 'Importer' },
{ value: 'exporter', label: 'Exporter' },
{ value: 'freight_forwarder', label: 'Freight forwarder' },
{ value: 'dj_freight_forwarder', label: 'DJ freight forwarder' },
{ value: 'transporter', label: 'Transporter' },
] },
// The list's Status filter folds the review queues in, and sends these two
// alongside `status`. They are predicates, not columns — see
// `company-scope.sql.ts`, shared with the list so both agree exactly.
@@ -147,6 +157,18 @@ export const customersDataset: ExportDataset = {
if (params.kind) qb.andWhere('c.kind = :kind', { kind: params.kind });
if (params.status) qb.andWhere('c.status = :status', { status: params.status });
if (params.nationality) qb.andWhere('c.nationality = :nationality', { nationality: params.nationality });
if (params.profileType) {
// EXISTS, matching the list repository exactly — a join here would
// multiply a company holding two profiles into two rows and put the file
// out of step with the count endpoint.
qb.andWhere(
`EXISTS (SELECT 1 FROM freight.company_profiles cp_type
WHERE cp_type.company_id = c.id
AND cp_type.deleted_at IS NULL
AND cp_type.type = :profileType)`,
{ profileType: params.profileType },
);
}
if (params.onboardingCompleted) {
const draft = companyDraftSql('c');
qb.andWhere(params.onboardingCompleted === 'true' ? `NOT ${draft}` : draft);

View File

@@ -2,6 +2,7 @@ import {
EXPORT_MIME,
formatRowCap,
pickByKey,
pickDatasetFields,
resolveExportFormat,
resolveRowLimit,
} from './export-request.util';
@@ -85,3 +86,48 @@ describe('pickByKey', () => {
expect(pickByKey(columns, 'ghost,also-ghost')).toEqual(columns);
});
});
describe('pickDatasetFields', () => {
const fields = [
{ key: 'name', label: 'Company', default: true },
{ key: 'tin', label: 'TIN', default: true },
{ key: 'website', label: 'Website' },
{ key: 'kebele', label: 'Kebele' },
];
const defaults = [fields[0], fields[1]];
it.each([undefined, '', ' ', ','])('%p means the default fields', (raw) => {
expect(pickDatasetFields(fields, raw)).toEqual(defaults);
});
it('a subset is honoured, in the dataset\'s own order', () => {
expect(pickDatasetFields(fields, 'kebele,name')).toEqual([fields[0], fields[3]]);
});
it('asking for EVERY field exports every field', () => {
// The dialog's "All columns" chip sends exactly this. Falling back to the
// defaults here was the bug: 37 ticked customer columns exported as 9.
expect(pickDatasetFields(fields, 'name,tin,website,kebele')).toEqual(fields);
});
it('a non-default field alone is not widened back to the defaults', () => {
expect(pickDatasetFields(fields, 'website')).toEqual([fields[2]]);
});
it('unknown keys are dropped, the recognised ones still stand', () => {
expect(pickDatasetFields(fields, 'ghost,website')).toEqual([fields[2]]);
});
it('all-unknown keys fall back to the defaults, not to everything', () => {
expect(pickDatasetFields(fields, 'ghost,also-ghost')).toEqual(defaults);
});
it('surrounding whitespace in a hand-built fields list is tolerated', () => {
expect(pickDatasetFields(fields, ' name , website ')).toEqual([fields[0], fields[2]]);
});
it('a dataset with no default flags falls back to every field', () => {
const flat = [{ key: 'a', label: 'A' }, { key: 'b', label: 'B' }];
expect(pickDatasetFields(flat, undefined)).toEqual(flat);
});
});

View File

@@ -56,3 +56,31 @@ export function pickByKey<T extends { key: string }>(all: T[], raw: string | und
const filtered = requested?.length ? all.filter((c) => requested.includes(c.key)) : all;
return filtered.length ? filtered : all;
}
/**
* A dataset's requested field subset, whitelisted against what the caller may
* have. Unlike `pickByKey`, "nothing recognised" falls back to the DEFAULT
* fields rather than to every field — a bookings export declares ~70 columns
* and dumping all of them on an unparameterised call is nobody's intent.
*
* Selecting every field is a legitimate request — the dialog's "All columns"
* chip sends exactly that — so the fallback keys off whether any requested key
* MATCHED, never off how many fields came back. Comparing the picked count to
* `all.length` (as this did originally) made "All columns" silently export the
* default columns instead.
*/
export function pickDatasetFields<T extends { key: string; default?: boolean }>(
all: T[],
raw: string | undefined,
): T[] {
const requested = new Set(
raw
?.split(',')
.map((k) => k.trim())
.filter(Boolean) ?? [],
);
const picked = requested.size ? all.filter((f) => requested.has(f.key)) : [];
if (picked.length) return picked;
const defaults = all.filter((f) => f.default);
return defaults.length ? defaults : all;
}

View File

@@ -13,7 +13,7 @@ import { resolveDatasetFields, resolveFilterOptions } from './export-filter.util
import {
EXPORT_MIME,
formatRowCap,
pickByKey,
pickDatasetFields,
resolveExportFormat,
resolveRowLimit,
} from './export-request.util';
@@ -105,7 +105,7 @@ export class ExportsController {
const dataset = this.resolve(key, user);
const directions = await this.userTradeAccessService.resolveAllowedDirections(user);
const format = resolveExportFormat(query.format);
const fields = ExportsController.pickFields(
const fields = pickDatasetFields(
await resolveDatasetFields(dataset, this.dataSource),
query.fields,
);
@@ -135,22 +135,6 @@ export class ExportsController {
res.send(buffer);
}
/**
* Requested fields, whitelisted against the dataset. No `fields=` means the
* DEFAULT set, not everything — a booking export has ~70 fields and dumping
* all of them on an unparameterised call is nobody's intent.
*/
private static pickFields(all: ExportField[], raw: string | undefined): ExportField[] {
if (raw?.trim()) {
const picked = pickByKey(all, raw);
// pickByKey falls back to everything when nothing matched; for a dataset
// the safer read of "all keys unknown" is still the default set.
if (picked.length !== all.length) return picked;
}
const defaults = all.filter((f) => f.default);
return defaults.length ? defaults : all;
}
private resolve(key: string, user: TCurrentUser): ExportDataset {
const dataset = getDataset(key);
if (!dataset) throw new NotFoundException(`Unknown export dataset: ${key}`);