Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions packages/datasource-customizer/src/decorators/binary/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import type {
RecordData,
} from '@forestadmin/datasource-toolkit';

import { CollectionDecorator, SchemaUtils, TypeGetter } from '@forestadmin/datasource-toolkit';
import {
CollectionDecorator,
SchemaUtils,
TypeGetter,
ValidationError,
parseDataUri,
} from '@forestadmin/datasource-toolkit';
import { filetypemime } from 'magic-bytes.js';

/**
Expand Down Expand Up @@ -237,15 +243,31 @@ export default class BinaryCollectionDecorator extends CollectionDecorator {
return value;
}

private parseHex(value: string): Buffer {
if (!/^([0-9a-f]{2})*$/i.test(value)) {
throw new ValidationError(
`Expected an even-length hex string for a binary field, got "${value}"`,
);
}

return Buffer.from(value, 'hex');
}

private async convertScalar(
toBackend: boolean,
useHex: boolean,
value: unknown,
): Promise<unknown> {
if (toBackend) {
const string = value as string;
if (typeof value !== 'string') {
throw new ValidationError(
`Expected a string for a binary field, got ${typeof value}: ${JSON.stringify(value)}`,
);
}

if (useHex) return this.parseHex(value);

return useHex ? Buffer.from(string, 'hex') : Buffer.from(string.split(',')[1], 'base64');
return parseDataUri(value).buffer;
}

const buffer = value as Buffer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Filter,
PaginatedFilter,
Projection,
ValidationError,
} from '@forestadmin/datasource-toolkit';
import * as factories from '@forestadmin/datasource-toolkit/dist/test/__factories__';

Expand Down Expand Up @@ -238,6 +239,66 @@ describe('BinaryCollectionDecorator', () => {
});
});

describe('list filtering a binary column with a value that is not a data uri', () => {
it('should reject the filter instead of crashing on an undefined base64 payload', async () => {
const caller = factories.caller.build();
const filter = new PaginatedFilter({
conditionTree: new ConditionTreeLeaf('cover', 'Equal', 'Anthony'),
});

await expect(decoratedBook.list(caller, filter, new Projection('id'))).rejects.toThrow(
ValidationError,
);
expect(books.list).not.toHaveBeenCalled();
});

it('should reject a data uri that is not base64, rather than decode it to garbage', async () => {
const caller = factories.caller.build();
const filter = new PaginatedFilter({
conditionTree: new ConditionTreeLeaf('cover', 'Equal', 'data:text/plain,hello'),
});

await expect(decoratedBook.list(caller, filter, new Projection('id'))).rejects.toThrow(
/must be a data uri/,
);
expect(books.list).not.toHaveBeenCalled();
});
});

describe('list filtering a hex binary column with a value that is not hex', () => {
it('should reject it rather than query for an empty buffer', async () => {
const caller = factories.caller.build();
const filter = new PaginatedFilter({
conditionTree: new ConditionTreeLeaf('id', 'Equal', 'Anthony'),
});

await expect(decoratedBook.list(caller, filter, new Projection('id'))).rejects.toThrow(
/even-length hex string/,
);
expect(books.list).not.toHaveBeenCalled();
});

it('should reject an odd-length hex string, which silently drops its last digit', async () => {
const caller = factories.caller.build();
const filter = new PaginatedFilter({
conditionTree: new ConditionTreeLeaf('id', 'Equal', '303'),
});

await expect(decoratedBook.list(caller, filter, new Projection('id'))).rejects.toThrow(
/even-length hex string/,
);
});
});

describe('writing a non-string value into a binary column', () => {
it('should reject it with a validation error rather than an opaque TypeError', async () => {
const caller = factories.caller.build();

await expect(decoratedBook.create(caller, [{ cover: 42 }])).rejects.toThrow(ValidationError);
expect(books.create).not.toHaveBeenCalled();
});
});

describe('list with a more complex filter', () => {
// Build condition tree (30303030 is the hex representation of 0000)
const conditionTree = new ConditionTreeBranch('Or', [
Expand Down
Loading