Skip to content

Commit

Permalink
feat: add isArray.force option for putting singular values into an ar…
Browse files Browse the repository at this point in the history
…ray of their own
  • Loading branch information
glebbash committed Apr 23, 2024
1 parent 01e562b commit 3576bc6
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 5 deletions.
32 changes: 27 additions & 5 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,29 @@ export type SingularPropertyOptions<T> = {
export type ArrayPropertyOptions<T> = {
example?: T[];
default?: T[];
isArray: true | { minLength?: number; maxLength?: number; length?: number };
isArray:
| true
| {
minLength?: number;
maxLength?: number;
length?: number;
/**
* Wheter to put singular value into an array of it's own.
* Useful if the decorated class is the query DTO.
*
* *NOTE*: This doesn't create an empty array if the value is not present.
*/
force?: boolean;
};
};

const ForceArrayTransform = Transform(({ value }) => {
if (value === undefined) {
return value;
}
return Array.isArray(value) ? value : [value];
});

export const noop = (): void => undefined;

export const compose = <T, CustomOptions>(
Expand All @@ -42,10 +62,11 @@ export const compose = <T, CustomOptions>(
}: PropertyOptions<T, CustomOptions>,
...decorators: PropertyDecorator[]
): PropertyDecorator => {
const isArrayObj = typeof isArray === 'object' ? isArray : {};
const length = isArrayObj.length;
const minLength = length ?? isArrayObj.minLength;
const maxLength = length ?? isArrayObj.maxLength;
const arrayProps = typeof isArray === 'object' ? isArray : {};
const length = arrayProps.length;
const minLength = length ?? arrayProps.minLength;
const maxLength = length ?? arrayProps.maxLength;
const forceArray = arrayProps.force ?? false;

return applyDecorators(
...decorators,
Expand All @@ -55,6 +76,7 @@ export const compose = <T, CustomOptions>(
!!isArray ? IsArray() : noop,
minLength ? ArrayMinSize(minLength) : noop,
maxLength ? ArrayMaxSize(maxLength) : noop,
forceArray ? ForceArrayTransform : noop,
def !== undefined ? Transform(({ value }) => (value === undefined ? def : value)) : noop,
ApiProperty({
...apiPropertyOptions,
Expand Down
55 changes: 55 additions & 0 deletions src/decorators/is-string.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,61 @@ describe('IsString', () => {
});
});

describe('array (in query)', () => {
class Test {
@IsString({ optional: true, isArray: { force: true } })
stringField?: string[];
}

it('generates correct schema', async () => {
expect(await generateSchemas([Test])).toStrictEqual({
Test: {
type: 'object',
properties: {
stringField: {
type: 'array',
items: {
type: 'string',
},
},
},
},
});
});

it('accepts string arrays', async () => {
expect(await input(Test, { stringField: ['a', 'b', 'c'] })).toStrictEqual(
Result.ok(make(Test, { stringField: ['a', 'b', 'c'] })),
);
expect(await input(Test, { stringField: [] })).toStrictEqual(
Result.ok(make(Test, { stringField: [] })),
);
});

it('accepts singular strings and transforms them into arrays', async () => {
expect(await input(Test, { stringField: 'a' })).toStrictEqual(
Result.ok(make(Test, { stringField: ['a'] })),
);

expect(await input(Test, { stringField: 'bcde' })).toStrictEqual(
Result.ok(make(Test, { stringField: ['bcde'] })),
);
});

it('works with optionals', async () => {
expect(await input(Test, {})).toStrictEqual(Result.ok(make(Test, {})));
});

it('rejects everything else', async () => {
expect(await input(Test, { stringField: true })).toStrictEqual(
Result.err('each value in stringField must be a string'),
);
expect(await input(Test, { stringField: [1, 2, 3] })).toStrictEqual(
Result.err('each value in stringField must be a string'),
);
});
});

describe('date', () => {
describe('date', () => {
class Test {
Expand Down

0 comments on commit 3576bc6

Please sign in to comment.