Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(framework): add class-validator support #6840

Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
369ffaf
feat: add class-validator support
paulwer Nov 4, 2024
19c03c2
Merge branch 'next' into feat-package-class-validator-support
paulwer Nov 4, 2024
a16f147
Merge branch 'next' into feat-package-class-validator-support
paulwer Nov 5, 2024
1016672
Merge branch 'next' into feat-package-class-validator-support
rifont Nov 8, 2024
c2c0512
Update packages/framework/src/validators/class-validator.validator.ts
paulwer Nov 8, 2024
f343a9d
fixes
paulwer Nov 8, 2024
b91fe48
Merge branch 'next' into feat-package-class-validator-support
paulwer Nov 8, 2024
8662b2b
workaround for classValidator defaultStorage
paulwer Nov 8, 2024
df9128e
Update packages/framework/src/validators/class-validator.validator.ts
paulwer Nov 8, 2024
86bd4f9
fixes
paulwer Nov 8, 2024
5d2b8e9
Merge remote-tracking branch 'refs/remotes/origin/feat-package-class-…
paulwer Nov 8, 2024
2b8f000
Merge branch 'next' into feat-package-class-validator-support
paulwer Nov 8, 2024
f74470a
refactor(types): replace any with unknown in T_Controls
rifont Nov 8, 2024
e7f3ef8
Update packages/framework/src/validators/class-validator.validator.ts
rifont Nov 8, 2024
c5daec5
refactor(validator): simplify metadata storage import
rifont Nov 8, 2024
255115c
docs(validator): update TODO comment with @see link
rifont Nov 8, 2024
68e5c9c
test(validation): update tests for validation changes
rifont Nov 8, 2024
ecec05d
refactor(types): update class type inference logic
rifont Nov 8, 2024
836bde3
test: remove JSON stringify from test assertion
rifont Nov 8, 2024
f3decf0
docs(schema.types): update ClassType type description
rifont Nov 8, 2024
ed7c857
refactor(class-validator): improve type checking logic
rifont Nov 8, 2024
322d118
test(validator): extract class-validator fixtures
rifont Nov 8, 2024
7fb6368
Merge branch 'next' into feat-package-class-validator-support
rifont Nov 8, 2024
b432668
refactor: Remove IsOptional from name property
rifont Nov 8, 2024
11210c4
Merge branch 'feat-package-class-validator-support' of https://github…
rifont Nov 8, 2024
ff1577e
fix(framework): Add dependency check import utilities
rifont Nov 10, 2024
34f9add
Merge branch 'import-utils-refactor-schemas' into feat-package-class-…
rifont Nov 11, 2024
d5c304b
Update packages/framework/src/types/schema.types/zod.schema.types.ts
rifont Nov 11, 2024
1281a14
Update packages/framework/src/client.test.ts
rifont Nov 11, 2024
d29556d
Update packages/framework/package.json
rifont Nov 11, 2024
0ab32ec
Update packages/framework/src/validators/class-validator.validator.ts
rifont Nov 11, 2024
dbf050e
Merge branch 'import-utils-refactor-schemas' into feat-package-class-…
rifont Nov 11, 2024
37bc8bc
Update packages/framework/src/validators/validator.test.ts
rifont Nov 11, 2024
b918978
Merge branch 'import-utils-refactor-schemas' into feat-package-class-…
rifont Nov 11, 2024
4140bda
Merge branch 'import-utils-refactor-schemas' into feat-package-class-…
rifont Nov 11, 2024
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
26 changes: 24 additions & 2 deletions packages/framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"next",
"nuxt",
"remix",
"sveltekit",
"sveltekit",
"README.md"
],
"scripts": {
Expand Down Expand Up @@ -170,9 +170,13 @@
"@sveltejs/kit": ">=1.27.3",
"@vercel/node": ">=2.15.9",
"aws-lambda": ">=1.0.7",
"class-transformer": ">=0.5.1",
"class-validator": ">=0.14.0",
"class-validator-jsonschema": ">=5.0.0",
"express": ">=4.19.2",
"h3": ">=1.8.1",
"next": ">=12.0.0",
"reflect-metadata": ">=0.2.2",
"zod": ">=3.0.0",
"zod-to-json-schema": ">=3.0.0"
},
Expand Down Expand Up @@ -201,6 +205,18 @@
"next": {
"optional": true
},
"class-transformer": {
"optional": true
},
"class-validator": {
"optional": true
},
"class-validator-jsonschema": {
"optional": true
},
"reflect-metadata": {
"optional": true
},
"zod": {
"optional": true
},
Expand All @@ -219,11 +235,15 @@
"@types/sanitize-html": "2.11.0",
"@vercel/node": "^2.15.9",
"aws-lambda": "^1.0.7",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"class-validator-jsonschema": "^5.0.1",
"express": "^4.19.2",
"h3": "^1.11.1",
"madge": "^8.0.0",
"next": "^13.5.4",
"prettier": "^3.2.5",
"reflect-metadata": "^0.2.2",
"ts-node": "^10.9.2",
"tsup": "^8.0.2",
"tsx": "4.16.2",
Expand All @@ -244,6 +264,8 @@
"sanitize-html": "^2.13.0"
},
"nx": {
"tags": ["package:public"]
"tags": [
"package:public"
]
rifont marked this conversation as resolved.
Show resolved Hide resolved
}
}
179 changes: 179 additions & 0 deletions packages/framework/src/client.validation.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */
import { expect, it, describe, beforeEach } from 'vitest';
import { z } from 'zod';
import { IsNumber, IsOptional, IsString } from 'class-validator';
import { Client } from './client';
import { workflow } from './resources/workflow';
import { ExecutionStateControlsInvalidError } from './errors';
Expand Down Expand Up @@ -187,6 +188,184 @@ describe('validation', () => {
});
});

describe('class-validator', () => {
class ClassValidatorSchema {
@IsString()
@IsOptional()
foo?: string;
@IsNumber()
@IsOptional()
baz?: number;
}

it('should infer types in the step controls', async () => {
workflow('class-validator-validation', async ({ step }) => {
await step.email(
'class-validator-validation',
async (controls) => {
// @ts-expect-error - Type 'number' is not assignable to type 'string'.
controls.foo = 123;
// @ts-expect-error - Type 'string' is not assignable to type 'number'.
controls.baz = '123';

return {
subject: 'Test subject',
body: 'Test body',
};
},
{
controlSchema: ClassValidatorSchema,
skip: (controls) => {
// @ts-expect-error - Type 'number' is not assignable to type 'string'.
controls.foo = 123;
// @ts-expect-error - Type 'string' is not assignable to type 'number'.
controls.baz = '123';

return true;
},
providers: {
sendgrid: async ({ controls, outputs }) => {
// @ts-expect-error - Type 'number' is not assignable to type 'string'.
controls.foo = 123;
// @ts-expect-error - Type 'string' is not assignable to type 'number'.
controls.baz = '123';

// @ts-expect-error - Type 'number' is not assignable to type 'string'.
outputs.body = 123;
// @ts-expect-error - Type 'number' is not assignable to type 'string'.
outputs.subject = 123;

return {
ipPoolName: 'test',
};
},
},
}
);
});
});

it('should infer types in the workflow payload', async () => {
workflow(
'class-validator-validation',
async ({ step, payload }) => {
await step.email('class-validator-validation', async () => {
// @ts-expect-error - Type 'number' is not assignable to type 'string'.
payload.foo = 123;
// @ts-expect-error - Type 'string' is not assignable to type 'number'.
payload.baz = '123';

return {
subject: 'Test subject',
body: 'Test body',
};
});
},
{
payloadSchema: ClassValidatorSchema,
}
);
});

it('should infer types in the workflow controls', async () => {
workflow(
'class-validator-validation',
async ({ step, controls }) => {
await step.email('class-validator-validation', async () => {
// @ts-expect-error - Type 'number' is not assignable to type 'string'.
controls.foo = 123;
// @ts-expect-error - Type 'string' is not assignable to type 'number'.
controls.baz = '123';

return {
subject: 'Test subject',
body: 'Test body',
};
});
},
{
controlSchema: ClassValidatorSchema,
}
);
});

it('should transform a class-validator schema to a json schema during discovery', async () => {
client.addWorkflows([
workflow('class-validator-validation', async ({ step }) => {
await step.email(
'class-validator-validation',
async () => ({
subject: 'Test subject',
body: 'Test body',
}),
{
controlSchema: ClassValidatorSchema,
}
);
}),
]);

const discoverResult = client.discover();
const stepControlSchema = discoverResult.workflows[0].steps[0].controls.schema;

expect(stepControlSchema).to.deep.include({
additionalProperties: false,
properties: {
foo: {
type: 'string',
},
baz: {
type: 'number',
},
},
required: ['foo', 'baz'],
type: 'object',
});
});

it('should throw an error if a property is missing', async () => {
client.addWorkflows([
workflow('class-validator-validation', async ({ step }) => {
await step.email(
'test-email',
async () => ({
subject: 'Test subject',
body: 'Test body',
}),
{
controlSchema: ClassValidatorSchema,
}
);
}),
]);

try {
await client.executeWorkflow({
action: PostActionEnum.EXECUTE,
workflowId: 'class-validator-validation',
controls: {
foo: '341',
},
payload: {},
stepId: 'test-email',
state: [],
subscriber: {},
});
} catch (error) {
expect(error).to.be.instanceOf(ExecutionStateControlsInvalidError);
expect((error as ExecutionStateControlsInvalidError).message).to.equal(
'Workflow with id: `class-validator-validation` has an invalid state. Step with id: `test-email` has invalid `controls`. Please provide the correct step controls.'
);
expect((error as ExecutionStateControlsInvalidError).data).to.deep.equal([
{
message: 'Required',
path: '/baz',
},
]);
}
});
});

describe('json-schema', () => {
const jsonSchema = {
type: 'object',
Expand Down
19 changes: 14 additions & 5 deletions packages/framework/src/types/schema.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import zod from 'zod';

export type JsonSchema = JSONSchema;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ClassType<T = any> = new (...args: any[]) => T;
rifont marked this conversation as resolved.
Show resolved Hide resolved

/**
* A schema used to validate a JSON object.
*
* Supported schemas:
* - JSONSchema
* - ZodSchema
*/
export type Schema = JsonSchema | zod.ZodSchema;
export type Schema = JsonSchema | zod.ZodSchema | ClassType;

/**
* Infer the type of a Schema for unvalidated data.
Expand All @@ -36,8 +39,11 @@ export type FromSchemaUnvalidated<T extends Schema> =
: // ZodSchema
T extends zod.ZodSchema
? zod.input<T>
: // All schema types exhausted.
never;
: // ClassValidatorSchema
T extends ClassType<infer U>
? U
: // All schema types exhausted.
never;

/**
* Infer the type of a Schema for validated data.
Expand All @@ -63,5 +69,8 @@ export type FromSchema<T extends Schema> =
: // ZodSchema
T extends zod.ZodSchema
? zod.infer<T>
: // All schema types exhausted.
never;
: // ClassValidatorSchema
T extends ClassType<infer U>
? U
: // All schema types exhausted.
never;
9 changes: 6 additions & 3 deletions packages/framework/src/types/step.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ export type ActionStep<
/**
* The controls for the step.
*/
T_Controls extends Record<string, unknown> = FromSchema<T_ControlSchema>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T_Controls extends Record<string, any> = FromSchema<T_ControlSchema>,
rifont marked this conversation as resolved.
Show resolved Hide resolved
>(
/**
* The name of the step. This is used to identify the step in the workflow.
Expand Down Expand Up @@ -94,7 +95,8 @@ export type CustomStep = <
/**
* The controls for the step.
*/
T_Controls extends Record<string, unknown> = FromSchema<T_ControlSchema>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T_Controls extends Record<string, any> = FromSchema<T_ControlSchema>,
/*
* These intermediary types are needed to capture the types in a single type instance
* to stop Typescript from erroring with:
Expand Down Expand Up @@ -153,7 +155,8 @@ export type ChannelStep<
/**
* The controls for the step.
*/
T_Controls extends Record<string, unknown> = FromSchema<T_ControlSchema>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T_Controls extends Record<string, any> = FromSchema<T_ControlSchema>,
>(
/**
* The name of the step. This is used to identify the step in the workflow.
Expand Down
6 changes: 6 additions & 0 deletions packages/framework/src/validators/base.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import type { FromSchema, FromSchemaUnvalidated, JsonSchema, Schema } from '../t
import type { ValidateResult } from '../types/validator.types';
import { JsonSchemaValidator } from './json-schema.validator';
import { ZodValidator } from './zod.validator';
import { ClassValidatorValidator } from './class-validator.validator';

const zodValidator = new ZodValidator();
const classValidatorValidator = new ClassValidatorValidator();
const jsonSchemaValidator = new JsonSchemaValidator();

export const validateData = async <
Expand All @@ -16,6 +18,8 @@ export const validateData = async <
): Promise<ValidateResult<T_Validated>> => {
if (zodValidator.canHandle(schema)) {
return zodValidator.validate(data, schema);
} else if (classValidatorValidator.canHandle(schema)) {
return classValidatorValidator.validate(data, schema);
} else if (jsonSchemaValidator.canHandle(schema)) {
return jsonSchemaValidator.validate(data, schema);
}
Expand All @@ -26,6 +30,8 @@ export const validateData = async <
export const transformSchema = async (schema: Schema): Promise<JsonSchema> => {
if (zodValidator.canHandle(schema)) {
return zodValidator.transformToJsonSchema(schema);
} else if (classValidatorValidator.canHandle(schema)) {
return classValidatorValidator.transformToJsonSchema(schema);
} else if (jsonSchemaValidator.canHandle(schema)) {
return jsonSchemaValidator.transformToJsonSchema(schema);
}
Expand Down
Loading
Loading