Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: lukeautry/tsoa
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v6.6.0
Choose a base ref
...
head repository: lukeautry/tsoa
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
  • 8 commits
  • 29 files changed
  • 4 contributors

Commits on Dec 20, 2024

  1. Fixed TypeResolver issue for negative number literal types

    Also added negativeNumberLiteralType to the tests
    Fixes #1733
    crycode-de committed Dec 20, 2024
    Copy the full SHA
    b942878 View commit details

Commits on Jan 5, 2025

  1. Copy the full SHA
    4e0d80b View commit details
  2. Copy the full SHA
    59918ba View commit details
  3. Copy the full SHA
    04dd319 View commit details
  4. provide test cases for required file changes and always invoke verify…

    …Response even in case of error to properly execute the assertions even in case of an error
    kchobantonov committed Jan 5, 2025
    Copy the full SHA
    18ced30 View commit details

Commits on Jan 27, 2025

  1. Merge pull request #1739 from kchobantonov/file-validation

     required validation for file is not executed closes #1737 and closes #1128 and closes #1738
    WoH authored Jan 27, 2025
    Copy the full SHA
    fba2b81 View commit details
  2. Merge pull request #1734 from crycode-de/issue-1733

    Fixed TypeResolver issue for negative number literal types
    WoH authored Jan 27, 2025
    Copy the full SHA
    82fc6ae View commit details
  3. v6.6.1

    WoH committed Jan 27, 2025
    Copy the full SHA
    0c53697 View commit details
Showing with 143 additions and 35 deletions.
  1. +1 −1 lerna.json
  2. +2 −2 packages/cli/package.json
  3. +4 −0 packages/cli/src/metadataGeneration/typeResolver.ts
  4. +2 −2 packages/cli/src/routeGeneration/templates/hapi.hbs
  5. +1 −1 packages/runtime/package.json
  6. +20 −10 packages/runtime/src/routeGeneration/templates/express/expressTemplateService.ts
  7. +1 −1 packages/runtime/src/routeGeneration/templates/hapi/hapiTemplateService.ts
  8. +3 −7 packages/runtime/src/routeGeneration/templates/koa/koaTemplateService.ts
  9. +3 −3 packages/tsoa/package.json
  10. +1 −0 tests/esm/integration/express.spec.ts
  11. +1 −1 tests/esm/package.json
  12. +1 −0 tests/fixtures/testModel.ts
  13. +1 −0 tests/integration/dynamic-controllers-express-server.spec.ts
  14. +1 −0 tests/integration/express-router-server.spec.ts
  15. +17 −0 tests/integration/express-server-custom-multer.spec.ts
  16. +1 −0 tests/integration/express-server-root-security.spec.ts
  17. +19 −2 tests/integration/express-server.spec.ts
  18. +19 −2 tests/integration/hapi-server.spec.ts
  19. +1 −0 tests/integration/inversify-async-server.spec.ts
  20. +2 −1 tests/integration/inversify-dynamic-container.spec.ts
  21. +1 −0 tests/integration/inversify-glob.spec.ts
  22. +1 −0 tests/integration/inversify-server.spec.ts
  23. +1 −0 tests/integration/koa-multer-options.spec.ts
  24. +1 −0 tests/integration/koa-server-no-additional-allowed.spec.ts
  25. +18 −1 tests/integration/koa-server.spec.ts
  26. +1 −0 tests/integration/openapi3-express.spec.ts
  27. +1 −1 tests/package.json
  28. +9 −0 tests/unit/swagger/definitionsGeneration/definitions.spec.ts
  29. +9 −0 tests/unit/swagger/schemaDetails3.spec.ts
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "6.6.0",
"version": "6.6.1",
"npmClient": "yarn",
"command": {
"version": {
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@tsoa/cli",
"description": "Build swagger-compliant REST APIs using TypeScript and Node",
"version": "6.6.0",
"version": "6.6.1",
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
@@ -30,7 +30,7 @@
"author": "Luke Autry <lukeautry@gmail.com> (http://www.lukeautry.com)",
"license": "MIT",
"dependencies": {
"@tsoa/runtime": "^6.6.0",
"@tsoa/runtime": "^6.6.1",
"@types/multer": "^1.4.12",
"fs-extra": "^11.2.0",
"glob": "^10.3.10",
4 changes: 4 additions & 0 deletions packages/cli/src/metadataGeneration/typeResolver.ts
Original file line number Diff line number Diff line change
@@ -508,6 +508,10 @@ export class TypeResolver {
return typeNode.literal.text;
case ts.SyntaxKind.NumericLiteral:
return parseFloat(typeNode.literal.text);
case ts.SyntaxKind.PrefixUnaryExpression:
// make sure to only handle the MinusToken here
throwUnless((typeNode.literal as ts.PrefixUnaryExpression).operator === ts.SyntaxKind.MinusToken, new GenerateMetadataError(`Couldn't resolve literal node: ${typeNode.literal.getText()}`));
return parseFloat(typeNode.literal.getText());
case ts.SyntaxKind.NullKeyword:
return null;
default:
4 changes: 2 additions & 2 deletions packages/cli/src/routeGeneration/templates/hapi.hbs
Original file line number Diff line number Diff line change
@@ -107,7 +107,7 @@ export function RegisterRoutes(server: any) {
throw error;
}

const boomErr = boomify(error instanceof Error ? error : new Error(error.message));
const boomErr = boomify(error instanceof Error ? error : new Error(error.message), { statusCode: error.status || 500 });
boomErr.output.statusCode = error.status || 500;
boomErr.output.payload = {
name: error.name,
@@ -199,7 +199,7 @@ export function RegisterRoutes(server: any) {
throw error;
}

const boomErr = boomify(error instanceof Error ? error : new Error(error.message));
const boomErr = boomify(error instanceof Error ? error : new Error(error.message), { statusCode: error.status || 500 });
boomErr.output.statusCode = error.status || 401;
boomErr.output.payload = {
name: error.name,
2 changes: 1 addition & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@tsoa/runtime",
"description": "Build swagger-compliant REST APIs using TypeScript and Node",
"version": "6.6.0",
"version": "6.6.1",
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
Original file line number Diff line number Diff line change
@@ -73,22 +73,19 @@ export class ExpressTemplateService extends TemplateService<ExpressApiHandlerPar
case 'body':
return this.validationService.ValidateParam(param, request.body, name, fieldErrors, true, undefined);
case 'body-prop':
return this.validationService.ValidateParam(param, request.body[name], name, fieldErrors, true, 'body.');
return this.validationService.ValidateParam(param, request.body?.[name], name, fieldErrors, true, 'body.');
case 'formData': {
const files = Object.values(args).filter(p => p.dataType === 'file' || (p.dataType === 'array' && p.array && p.array.dataType === 'file'));
if ((param.dataType === 'file' || (param.dataType === 'array' && param.array && param.array.dataType === 'file')) && files.length > 0) {
const requestFiles = request.files as { [fileName: string]: Express.Multer.File[] };
if (requestFiles[name] === undefined) {
return undefined;
}
const requestFiles = request.files as { [fileName: string]: Express.Multer.File[] } | undefined;

const fileArgs = this.validationService.ValidateParam(param, requestFiles[name], name, fieldErrors, false, undefined);
const fileArgs = this.validationService.ValidateParam(param, requestFiles?.[name], name, fieldErrors, false, undefined);
if (param.dataType === 'array') {
return fileArgs;
}
return fileArgs.length === 1 ? fileArgs[0] : fileArgs;
return Array.isArray(fileArgs) && fileArgs.length === 1 ? fileArgs[0] : fileArgs;
}
return this.validationService.ValidateParam(param, request.body[name], name, fieldErrors, false, undefined);
return this.validationService.ValidateParam(param, request.body?.[name], name, fieldErrors, false, undefined);
}
case 'res':
return (status: number | undefined, data: any, headers: any) => {
@@ -114,11 +111,24 @@ export class ExpressTemplateService extends TemplateService<ExpressApiHandlerPar
Object.keys(headers).forEach((name: string) => {
response.set(name, headers[name]);
});

// Check if the response is marked to be JSON
const isJsonResponse = response.get('Content-Type')?.includes('json') || false;

if (data && typeof data.pipe === 'function' && data.readable && typeof data._read === 'function') {
response.status(statusCode || 200);
(data as Readable).pipe(response);
} else if (data !== null && data !== undefined) {
response.status(statusCode || 200).json(data);
} else if (data !== undefined && (data !== null || isJsonResponse)) {
// allow null response when it is a json response
if (typeof data === 'number' || isJsonResponse) {
// express treats number data as status code so use the json method instead
// or if the response was marked as json then use the json so for example strings are quoted
response.status(statusCode || 200).json(data);
} else {
// do not use json for every type since internally the send will invoke json if needed
// but for string data it will not quote it, so we can send string as plain/text data
response.status(statusCode || 200).send(data);
}
} else {
response.status(statusCode || 204).end();
}
Original file line number Diff line number Diff line change
@@ -127,7 +127,7 @@ export class HapiTemplateService extends TemplateService<HapiApiHandlerParameter
return tsoaResponsed.value;
}

const response = data !== null && data !== undefined ? h.response(data).code(200) : h.response('').code(204);
const response = data !== null && data !== undefined ? h.response(data).code(200) : h.response().code(204);

Object.keys(headers).forEach((name: string) => {
response.header(name, headers[name]);
Original file line number Diff line number Diff line change
@@ -86,17 +86,13 @@ export class KoaTemplateService extends TemplateService<KoaApiHandlerParameters,
const files = Object.values(args).filter(p => p.dataType === 'file' || (p.dataType === 'array' && p.array && p.array.dataType === 'file'));
const contextRequest = context.request as any;
if ((param.dataType === 'file' || (param.dataType === 'array' && param.array && param.array.dataType === 'file')) && files.length > 0) {
if (contextRequest.files[name] === undefined) {
return undefined;
}

const fileArgs = this.validationService.ValidateParam(param, contextRequest.files[name], name, errorFields, false, undefined);
const fileArgs = this.validationService.ValidateParam(param, contextRequest.files?.[name], name, errorFields, false, undefined);
if (param.dataType === 'array') {
return fileArgs;
}
return fileArgs.length === 1 ? fileArgs[0] : fileArgs;
return Array.isArray(fileArgs) && fileArgs.length === 1 ? fileArgs[0] : fileArgs;
}
return this.validationService.ValidateParam(param, contextRequest.body[name], name, errorFields, false, undefined);
return this.validationService.ValidateParam(param, contextRequest.body?.[name], name, errorFields, false, undefined);
}
case 'res':
return async (status: number | undefined, data: any, headers: any): Promise<void> => {
6 changes: 3 additions & 3 deletions packages/tsoa/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tsoa",
"description": "Build swagger-compliant REST APIs using TypeScript and Node",
"version": "6.6.0",
"version": "6.6.1",
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
@@ -28,8 +28,8 @@
"author": "Luke Autry <lukeautry@gmail.com> (http://www.lukeautry.com)",
"license": "MIT",
"dependencies": {
"@tsoa/cli": "^6.6.0",
"@tsoa/runtime": "^6.6.0"
"@tsoa/cli": "^6.6.1",
"@tsoa/runtime": "^6.6.1"
},
"devDependencies": {
"@types/node": "^18.0.0",
1 change: 1 addition & 0 deletions tests/esm/integration/express.spec.ts
Original file line number Diff line number Diff line change
@@ -33,6 +33,7 @@ describe('Express Server', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
2 changes: 1 addition & 1 deletion tests/esm/package.json
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
"name": "tsoa-tests-esm",
"private": true,
"description": "Build swagger-compliant REST APIs using TypeScript and Node",
"version": "6.6.0",
"version": "6.6.1",
"type": "module",
"sideEffects": false,
"scripts": {
1 change: 1 addition & 0 deletions tests/fixtures/testModel.ts
Original file line number Diff line number Diff line change
@@ -72,6 +72,7 @@ export interface TestModel extends Model {
nullableUnionPrimitiveType?: 'String' | 1 | 20.0 | true | false | null;
undefineableUnionPrimitiveType: 'String' | 1 | 20.0 | true | false | undefined;
singleFloatLiteralType?: 3.1415;
negativeNumberLiteralType?: -1;
dateValue?: Date;
optionalString?: string;
anyType?: any;
Original file line number Diff line number Diff line change
@@ -1242,6 +1242,7 @@ describe('Express Server', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
1 change: 1 addition & 0 deletions tests/integration/express-router-server.spec.ts
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ describe('Express Router Server', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
17 changes: 17 additions & 0 deletions tests/integration/express-server-custom-multer.spec.ts
Original file line number Diff line number Diff line change
@@ -41,6 +41,22 @@ describe('Express Server With custom multer', () => {
});
});

it('cannot post a file with no file', async () => {
const formData = { notAFileAttribute: 'not a file' };
verifyFileUploadRequest(basePath + '/PostTest/File', formData, (_err, res) => {
expect(res.status).to.equal(400);
expect(res.text).to.equal('{"fields":{"someFile":{"message":"\'someFile\' is required"}},"message":"An error occurred during the request.","name":"ValidateError","status":400}');
});
});

it('can post a file with no file', async () => {
const formData = { notAFileAttribute: 'not a file' };
verifyFileUploadRequest(basePath + '/PostTest/FileOptional', formData, (_err, res) => {
expect(res.status).to.equal(200);
expect(res.text).to.equal('no file');
});
});

it('can post multiple files with other form fields', () => {
const formData = {
a: 'b',
@@ -197,6 +213,7 @@ describe('Express Server With custom multer', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
1 change: 1 addition & 0 deletions tests/integration/express-server-root-security.spec.ts
Original file line number Diff line number Diff line change
@@ -68,6 +68,7 @@ describe('Express Server with api_key Root Security', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
21 changes: 19 additions & 2 deletions tests/integration/express-server.spec.ts
Original file line number Diff line number Diff line change
@@ -1514,7 +1514,7 @@ describe('Express Server', () => {
const mainResourceId = 'main-123';

return verifyGetRequest(basePath + `/SubResourceTest/${mainResourceId}/SubResource`, (_err, res) => {
expect(res.body).to.equal(mainResourceId);
expect(res.text).to.equal(mainResourceId);
});
});

@@ -1523,7 +1523,7 @@ describe('Express Server', () => {
const subResourceId = 'sub-456';

return verifyGetRequest(basePath + `/SubResourceTest/${mainResourceId}/SubResource/${subResourceId}`, (_err, res) => {
expect(res.body).to.equal(`${mainResourceId}:${subResourceId}`);
expect(res.text).to.equal(`${mainResourceId}:${subResourceId}`);
});
});
});
@@ -1559,6 +1559,22 @@ describe('Express Server', () => {
});
});

it('cannot post a file with no file', async () => {
const formData = { notAFileAttribute: 'not a file' };
verifyFileUploadRequest(basePath + '/PostTest/File', formData, (_err, res) => {
expect(res.status).to.equal(400);
expect(res.text).to.equal('{"fields":{"someFile":{"message":"\'someFile\' is required"}},"message":"An error occurred during the request.","name":"ValidateError","status":400}');
});
});

it('can post a file with no file', async () => {
const formData = { notAFileAttribute: 'not a file' };
verifyFileUploadRequest(basePath + '/PostTest/FileOptional', formData, (_err, res) => {
expect(res.status).to.equal(200);
expect(res.text).to.equal('no file');
});
});

it('can post multiple files with other form fields', () => {
const formData = {
a: 'b',
@@ -1723,6 +1739,7 @@ describe('Express Server', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
21 changes: 19 additions & 2 deletions tests/integration/hapi-server.spec.ts
Original file line number Diff line number Diff line change
@@ -1437,8 +1437,24 @@ describe('Hapi Server', () => {
it('cannot post a file with wrong attribute name', async () => {
const formData = { wrongAttributeName: '@../package.json' };
verifyFileUploadRequest(basePath + '/PostTest/File', formData, (_err, res) => {
expect(res.status).to.equal(500);
expect(res.text).to.equal('{"message":"Unexpected field","name":"MulterError","status":500}');
expect(res.status).to.equal(400);
expect(res.text).to.equal('{"name":"ValidateError","fields":{"someFile":{"message":"\'someFile\' is required"}},"message":"Bad Request"}');
});
});

it('cannot post a file with no file', async () => {
const formData = { notAFileAttribute: 'not a file' };
verifyFileUploadRequest(basePath + '/PostTest/File', formData, (_err, res) => {
expect(res.status).to.equal(400);
expect(res.text).to.equal('{"name":"ValidateError","fields":{"someFile":{"message":"\'someFile\' is required"}},"message":"Bad Request"}');
});
});

it('can post a file with no file', async () => {
const formData = { notAFileAttribute: 'not a file' };
verifyFileUploadRequest(basePath + '/PostTest/FileOptional', formData, (_err, res) => {
expect(res.status).to.equal(200);
expect(res.text).to.equal('no file');
});
});

@@ -1606,6 +1622,7 @@ describe('Hapi Server', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
1 change: 1 addition & 0 deletions tests/integration/inversify-async-server.spec.ts
Original file line number Diff line number Diff line change
@@ -43,6 +43,7 @@ describe('Inversify async IoC Express Server', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
3 changes: 2 additions & 1 deletion tests/integration/inversify-dynamic-container.spec.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ const basePath = '/v1';
describe('Inversify Express Server Dynamic Container', () => {
it('can handle get request with no path argument', () => {
return verifyGetRequest(basePath + '/ManagedTest?tsoa=abc123456', (err, res) => {
const model = res.body as string;
const model = res.text;
expect(model).to.equal(basePath + '/ManagedTest');
});
});
@@ -32,6 +32,7 @@ describe('Inversify Express Server Dynamic Container', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
1 change: 1 addition & 0 deletions tests/integration/inversify-glob.spec.ts
Original file line number Diff line number Diff line change
@@ -50,6 +50,7 @@ describe('Inversify Express Server with ControllerPathGlob', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
1 change: 1 addition & 0 deletions tests/integration/inversify-server.spec.ts
Original file line number Diff line number Diff line change
@@ -92,6 +92,7 @@ describe('Inversify Express Server', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
1 change: 1 addition & 0 deletions tests/integration/koa-multer-options.spec.ts
Original file line number Diff line number Diff line change
@@ -97,6 +97,7 @@ describe('Koa Server (with multerOpts)', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
1 change: 1 addition & 0 deletions tests/integration/koa-server-no-additional-allowed.spec.ts
Original file line number Diff line number Diff line change
@@ -482,6 +482,7 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => {
}

if (err) {
verifyResponse(err, res);
reject({
error: err,
response: parsedError,
Loading