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

Feature: Implementing restful route generator and api server according to api schema #8

Merged
merged 4 commits into from
Jul 4, 2022
Merged
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
18 changes: 16 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@
"name": "vulcan",
"version": "0.0.0",
"license": "MIT",
"scripts": {},
"scripts": {
"test": "jest"
},
"private": true,
"dependencies": {
"dayjs": "^1.11.2",
"glob": "^8.0.1",
"joi": "^17.6.0",
"js-yaml": "^4.1.0",
"koa": "^2.13.4",
"koa-bodyparser": "^4.3.0",
"koa-compose": "^4.1.0",
"koa-router": "^10.1.1",
"lodash": "^4.17.21",
"nunjucks": "^3.2.3",
"tslib": "^2.3.0"
"tslib": "^2.3.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@faker-js/faker": "^6.3.1",
"@nrwl/cli": "14.0.3",
"@nrwl/eslint-plugin-nx": "14.0.3",
"@nrwl/jest": "14.0.3",
Expand All @@ -22,9 +31,13 @@
"@types/glob": "^7.2.0",
"@types/jest": "27.4.1",
"@types/js-yaml": "^4.0.5",
"@types/koa": "^2.13.4",
"@types/koa-compose": "^3.2.5",
"@types/koa-router": "^7.4.4",
"@types/lodash": "^4.14.182",
"@types/node": "16.11.7",
"@types/supertest": "^2.0.12",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "~5.18.0",
"@typescript-eslint/parser": "~5.18.0",
"cz-conventional-changelog": "3.3.0",
Expand All @@ -33,6 +46,7 @@
"jest": "27.5.1",
"nx": "14.0.3",
"prettier": "^2.5.1",
"supertest": "^6.2.3",
"ts-essentials": "^9.1.2",
"ts-jest": "27.1.4",
"ts-node": "9.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export const checkValidator =
const validator = loader.getLoader(validatorRequest.name);

// TODO: indicate the detail of error
if (!validator.validateSchema(validatorRequest.args)) {
throw new Error(`Validator ${validatorRequest.name} schema invalid`);
}
validator.validateSchema(validatorRequest.args);
}
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { APISchema, RequestParameter, ValidatorDefinition } from '@vulcan/core';
import {
APISchema,
RequestSchema as RequestParameter,
ValidatorDefinition,
} from '@vulcan/core';
import { DeepPartial } from 'ts-essentials';

export interface RawRequestParameter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ it('Should pass if there is no error', async () => {
const stubValidatorLoader = sinon.stubInterface<ValidatorLoader>();
stubValidatorLoader.getLoader.returns({
name: 'validator1',
validateSchema: () => true,
validateData: () => true,
validateSchema: () => null,
validateData: () => null,
});

// Act Assert
Expand All @@ -38,8 +38,8 @@ it('Should throw if some validators have no name', async () => {
const stubValidatorLoader = sinon.stubInterface<ValidatorLoader>();
stubValidatorLoader.getLoader.returns({
name: 'validator1',
validateSchema: () => true,
validateData: () => true,
validateSchema: () => null,
validateData: () => null,
});

// Act Assert
Expand All @@ -61,12 +61,14 @@ it('Should throw if the arguments of a validator is invalid', async () => {
const stubValidatorLoader = sinon.stubInterface<ValidatorLoader>();
stubValidatorLoader.getLoader.returns({
name: 'validator1',
validateSchema: () => false,
validateData: () => true,
validateSchema: () => {
throw new Error();
},
validateData: () => null,
});

// Act Assert
await expect(
checkValidator(stubValidatorLoader)(schema, async () => Promise.resolve())
).rejects.toThrow('Validator validator1 schema invalid');
).rejects.toThrow();
});
2 changes: 1 addition & 1 deletion packages/build/test/schema-parser/schemaParser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ request:
stubValidatorLoader.getLoader.returns({
name: 'validator1',
validateSchema: () => true,
validateData: () => true,
validateData: () => null,
});
const schemaParser = new SchemaParser({
schemaReader: stubSchemaReader,
Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "@vulcan/core",
"version": "0.0.1",
"type": "commonjs"
"type": "commonjs",
"dependencies": {}
}
27 changes: 25 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
export * from './lib/template-engine';
// import built-in validators
import builtInValidatorClasses from './lib/validators';
import IValidator from './lib/validators/validator';

// TODO: Import user-defined validators dynamically in specific directory

// defined all validators and change to key-value object format for export
const loadedValidators: { [name: string]: IValidator } = Object.assign(
{},
...builtInValidatorClasses.map((validatorClass) => {
const validator = new validatorClass();
return {
[validator.name as string]: validator,
};
})
);

// export all other non-default objects of validators module
export * from './lib/validators';
// Export all other modules
export * from './models';
export * from './validators';
export * from './lib/utils';
export * from './lib/validators/validator';
export * from './lib/template-engine';
export * from './lib/artifact-builder';

export { loadedValidators, IValidator };
1 change: 1 addition & 0 deletions packages/core/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './normalizedStringValue';
46 changes: 46 additions & 0 deletions packages/core/src/lib/utils/normalizedStringValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export const canBeNormalized = (type: string) => {
return (
['number', 'boolean', 'string', 'date'].indexOf(type.toLowerCase()) !== -1
);
};

export const normalizeStringValue = (
value: string,
dataName: string,
dataType: string
) => {
switch (dataType.toLowerCase()) {
case 'number': {
if (value === '') {
throw new Error(`${dataName} must be number`);
}
const valueNumber = +value;
if (isNaN(valueNumber)) {
throw new Error(`${dataName} must be number`);
}
return valueNumber;
}

case 'boolean': {
if (value === 'true' || value === '1' || value === '') {
return true;
} else if (value === 'false' || value === '0') {
return false;
} else {
throw new Error(`${dataName} must be boolean`);
}
}

case 'date': {
const parsedDate = new Date(value);
if (Number.isNaN(parsedDate.getTime())) {
throw new Error(`${dataName} must be date`);
}
return parsedDate;
}

case 'string':
default:
return value;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as Joi from 'joi';
import { isUndefined } from 'lodash';
import * as dayjs from 'dayjs';
import customParseFormat = require('dayjs/plugin/customParseFormat');
import IValidator from '../validator';

// Support custom date format -> dayjs.format(...)
dayjs.extend(customParseFormat);

export interface DateInputArgs {
// The date needed format, supported ISO_8601 token, ref: https://www.w3.org/TR/NOTE-datetime
// e.g: "YYYYMMDD", "YYYY-MM-DD", "YYYY-MM-DD HH:mm",
format?: string;
}

export class DateTypeValidator implements IValidator {
public readonly name = 'date';
// Validator for arguments schema in schema.yaml, should match DateInputArgs
private argsValidator = Joi.object({
format: Joi.string().optional(),
});

public validateSchema(args: DateInputArgs) {
try {
// validate arguments schema
Joi.assert(args, this.argsValidator);
} catch {
throw new Error(
'The arguments schema for "date" type validator is incorrect'
);
}
}

public validateData(value: string, args?: DateInputArgs) {
let valid = dayjs(value).isValid();
// if there are args passed
if (!isUndefined(args)) {
// validate date, support format validator if input field existed
valid = args.format ? dayjs(value, args.format, true).isValid() : valid;
}
if (!valid)
throw new Error('The input parameter is invalid, it should be date type');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './integerTypeValidator';
export * from './stringTypeValidator';
export * from './dateTypeValidator';
export * from './uuidTypeValidator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as Joi from 'joi';
import { isUndefined } from 'lodash';
import IValidator from '../validator';

export interface IntInputArgs {
// The integer minimum value
min?: number;
// The integer maximum value
max?: number;
// The integer should greater than value
greater?: number;
// The integer should less than value
less?: number;
}

export class IntegerTypeValidator implements IValidator {
public readonly name = 'integer';
// Validator for arguments schema in schema.yaml, should match IntInputArgs
private argsValidator = Joi.object({
min: Joi.number().integer().optional(),
max: Joi.number().integer().optional(),
greater: Joi.number().integer().optional(),
less: Joi.number().integer().optional(),
});

public validateSchema(args: IntInputArgs) {
try {
// validate arguments schema
Joi.assert(args, this.argsValidator);
} catch {
throw new Error(
'The arguments schema for "integer" type validator is incorrect'
);
}
}

public validateData(value: string, args?: IntInputArgs) {
// parse arguments

// schema is integer type
let schema = Joi.number().integer();

// if there are args passed
if (!isUndefined(args)) {
// support min, max, greater, less validator if input field existed
schema = args.min ? schema.min(args.min) : schema;
schema = args.max ? schema.max(args.max) : schema;
schema = args.greater ? schema.greater(args.greater) : schema;
schema = args.less ? schema.less(args.less) : schema;
}
try {
Joi.assert(value, schema);
} catch {
throw new Error(
'The input parameter is invalid, it should be integer type'
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as Joi from 'joi';
import { isUndefined } from 'lodash';
import IValidator from '../validator';

export interface StringInputArgs {
// The string regex format pattern
format?: string;
// The string length
length?: number;
// The string minimum value
min?: number;
// The string maximum value
max?: number;
}

export class StringTypeValidator implements IValidator {
public readonly name = 'string';
// Validator for arguments schema in schema.yaml, should match StringInputArgs
private argsValidator = Joi.object({
format: Joi.string().optional(),
length: Joi.number().optional(),
min: Joi.number().optional(),
max: Joi.number().optional(),
});

public validateSchema(args: StringInputArgs) {
try {
// validate arguments schema
Joi.assert(args, this.argsValidator);
} catch {
throw new Error(
'The arguments schema for "string" type validator is incorrect'
);
}
}

public validateData(value: string, args?: StringInputArgs) {
// schema is string type
let schema = Joi.string();

// if there are args passed
if (!isUndefined(args)) {
// support length, min, max validator if input field existed
schema = args.length ? schema.length(args.length) : schema;
schema = args.min ? schema.min(args.min) : schema;
schema = args.max ? schema.max(args.max) : schema;
// support regular expression pattern when input field existed
schema = args.format ? schema.pattern(new RegExp(args.format)) : schema;
}
try {
// validate data value
Joi.assert(value, schema);
} catch {
throw new Error(
'The input parameter is invalid, it should be string type'
);
}
}
}
Loading