From 3576bc672b793470874bc4a1554db30c40ac0ade Mon Sep 17 00:00:00 2001 From: Gleb Bashkatov Date: Tue, 23 Apr 2024 13:55:00 +0000 Subject: [PATCH] feat: add isArray.force option for putting singular values into an array of their own --- src/core.ts | 32 ++++++++++++++++--- src/decorators/is-string.spec.ts | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/core.ts b/src/core.ts index 9779a3d..f347b9b 100644 --- a/src/core.ts +++ b/src/core.ts @@ -23,9 +23,29 @@ export type SingularPropertyOptions = { export type ArrayPropertyOptions = { 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 = ( @@ -42,10 +62,11 @@ export const compose = ( }: PropertyOptions, ...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, @@ -55,6 +76,7 @@ export const compose = ( !!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, diff --git a/src/decorators/is-string.spec.ts b/src/decorators/is-string.spec.ts index 4f06abf..11703f5 100644 --- a/src/decorators/is-string.spec.ts +++ b/src/decorators/is-string.spec.ts @@ -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 {