This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { protocolFieldsToJson } from './protocol-uint32';
|
||||
|
||||
@Injectable()
|
||||
export class ProtocolFieldsInterceptor implements NestInterceptor {
|
||||
intercept(_context: ExecutionContext, next: CallHandler) {
|
||||
return next.handle().pipe(map(protocolFieldsToJson));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
protocolFieldsToJson,
|
||||
protocolUint32,
|
||||
protocolUint32FromDb,
|
||||
protocolUint32ToDb,
|
||||
parseProtocolSequence,
|
||||
} from './protocol-uint32';
|
||||
|
||||
describe('CMPP unsigned protocol fields', () => {
|
||||
it.each([0, 2147483647, 2147483648, 4294967295])(
|
||||
'round trips %s without changing the JSON number contract',
|
||||
(value) => {
|
||||
expect(protocolUint32FromDb(protocolUint32ToDb(value))).toBe(value);
|
||||
expect(
|
||||
JSON.parse(
|
||||
JSON.stringify(protocolFieldsToJson({ rows: [{ sequenceId: BigInt(value), ackResult: BigInt(value) }] })),
|
||||
),
|
||||
).toEqual({ rows: [{ sequenceId: value, ackResult: value }] });
|
||||
},
|
||||
);
|
||||
it.each([-1, 4294967296, 1.5, NaN, Infinity, '', '0', ' ', {}, true])('rejects invalid wire value %s', (value) => {
|
||||
expect(() => protocolUint32(value)).toThrow();
|
||||
expect(() => protocolUint32ToDb(value)).toThrow();
|
||||
});
|
||||
it('preserves optional historical nulls and unrelated serializers', () => {
|
||||
expect(protocolUint32ToDb(null)).toBeUndefined();
|
||||
expect(protocolUint32FromDb(null)).toBeUndefined();
|
||||
const date = new Date();
|
||||
expect(
|
||||
protocolFieldsToJson({ date, money: 10000n, sequenceId: null, gatewayMessageId: '18446744073709551615' }),
|
||||
).toEqual({ date, money: 10000n, sequenceId: null, gatewayMessageId: '18446744073709551615' });
|
||||
expect(() => protocolUint32FromDb(4294967296n)).toThrow();
|
||||
});
|
||||
it('distinguishes text zero from missing or malformed historical sequences', () => {
|
||||
for (const value of [null, undefined, '', ' ', '-1', '1.5', '1e2', '4294967296'])
|
||||
expect(parseProtocolSequence(value)).toBeUndefined();
|
||||
expect(parseProtocolSequence('0')).toBe(0);
|
||||
expect(parseProtocolSequence('4294967295')).toBe(4294967295);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
/** Protocol integers are exact JS numbers on the wire and bigint in PostgreSQL. */
|
||||
export function protocolUint32(value: unknown, field = 'sequenceId'): number {
|
||||
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > 0xffffffff) {
|
||||
throw new BadRequestException(`${field} must be an unsigned 32-bit integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function protocolUint32ToDb(value: unknown, field = 'sequenceId'): bigint | undefined {
|
||||
return value == null ? undefined : BigInt(protocolUint32(value, field));
|
||||
}
|
||||
|
||||
export function protocolUint32FromDb(value: bigint | number | null | undefined): number | undefined {
|
||||
if (value == null) return undefined;
|
||||
return protocolUint32(typeof value === 'bigint' ? Number(value) : value);
|
||||
}
|
||||
|
||||
/** Historical Submit sequence columns are text; blanks must never become zero. */
|
||||
export function parseProtocolSequence(value: string | null | undefined): number | undefined {
|
||||
if (value == null || !/^\d+$/.test(value)) return undefined;
|
||||
const number = Number(value);
|
||||
return Number.isInteger(number) && number <= 0xffffffff ? number : undefined;
|
||||
}
|
||||
|
||||
/** Only protocol fields are converted, leaving money and dates to their existing serializers. */
|
||||
export function protocolFieldsToJson(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(protocolFieldsToJson);
|
||||
if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype) return value;
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, item]) => [
|
||||
key,
|
||||
(key === 'sequenceId' || key === 'ackResult') && typeof item === 'bigint'
|
||||
? protocolUint32FromDb(item)
|
||||
: protocolFieldsToJson(item),
|
||||
]),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user