Skip to content

Commit

Permalink
Merge pull request #481 from samchon/features/security
Browse files Browse the repository at this point in the history
Close #474 - support security scheme specification for swagger
  • Loading branch information
samchon authored Jul 26, 2023
2 parents 245a083 + 01cd3b0 commit 06a0e7d
Show file tree
Hide file tree
Showing 269 changed files with 4,709 additions and 1,639 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ jobs:

- name: test
working-directory: ./test
run: npm run start
run: npm run start -- --skipTest
4 changes: 2 additions & 2 deletions packages/migrate/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/migrate",
"version": "0.1.8",
"version": "0.1.10",
"description": "Migration program from swagger to NestJS",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -45,7 +45,7 @@
"typescript-transform-paths": "^3.4.6"
},
"dependencies": {
"typia": "^4.1.6"
"typia": "^4.1.8"
},
"files": [
"lib",
Expand Down
41 changes: 29 additions & 12 deletions packages/migrate/src/programmers/RouteProgrammer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,27 @@ export namespace RouteProgrammer {
JsonTypeChecker.isBoolean(p.schema) ||
JsonTypeChecker.isNumber(p.schema) ||
JsonTypeChecker.isInteger(p.schema) ||
JsonTypeChecker.isString(p.schema),
JsonTypeChecker.isString(p.schema) ||
JsonTypeChecker.isArray(p.schema),
);
if (objects.length === 1) return objects[0];
else if (
objects.length + primitives.length !==
parameters.length
)
if (objects.length === 1 && primitives.length === 0)
return objects[0];
else if (objects.length > 1)
throw new Error(
`Error on nestia.migrate.RouteProgrammer.analze(): ${type} typed parameters must be only one object type - ${StringUtil.capitalize(
props.method,
)} "${props.path}".`,
);

const dto: ISwaggerSchema.IObject | null = objects[0]
? JsonTypeChecker.isObject(objects[0])
? objects[0]
: ((swagger.components.schemas ?? {})[
(
objects[0] as ISwaggerSchema.IReference
).$ref.replace(`#/components/schemas/`, ``)
] as ISwaggerSchema.IObject)
: null;
const entire: ISwaggerSchema.IObject[] = [
...objects.map((o) =>
JsonTypeChecker.isObject(o)
Expand All @@ -79,19 +87,25 @@ export namespace RouteProgrammer {
),
{
type: "object",
properties: Object.fromEntries(
primitives.map((p) => [
properties: Object.fromEntries([
...primitives.map((p) => [
p.name,
{
...p.schema,
description:
p.schema.description ?? p.description,
},
]),
),
required: primitives
.filter((p) => p.required)
.map((p) => p.name),
...(dto
? Object.entries(dto.properties ?? {})
: []),
]),
required: [
...primitives
.filter((p) => p.required)
.map((p) => p.name),
...(dto ? dto.required ?? [] : []),
],
},
];
return parameters.length === 0
Expand Down Expand Up @@ -186,6 +200,9 @@ export namespace RouteProgrammer {
}
if (route.tags) route.tags.forEach((name) => add(`@tag ${name}`));
if (route.deprecated) add("@deprecated");
for (const security of route.security ?? [])
for (const [name, scopes] of Object.entries(security))
add(`@security ${[name, ...scopes].join("")}`);
return content.length ? content.join("\n") : undefined;
};

Expand Down
1 change: 1 addition & 0 deletions packages/migrate/src/structures/ISwaggerRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface ISwaggerRoute {
summary?: string;
description?: string;
deprecated?: boolean;
security?: Record<string, string[]>[];
tags?: string[];
}
export namespace ISwaggerRoute {
Expand Down
6 changes: 3 additions & 3 deletions packages/migrate/src/structures/ISwaggerSecurity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type ISwaggerSecurity =
export type ISwaggerSecurity =
| ISwaggerSecurity.IHttpBasic
| ISwaggerSecurity.IHttpBearer
| ISwaggerSecurity.IApiKey
Expand All @@ -7,7 +7,7 @@ export type ISwaggerSecurity =
export namespace ISwaggerSecurity {
export interface IHttpBasic {
type: "http";
schema: "basic";
scheme: "basic";
}
export interface IHttpBearer {
type: "http";
Expand Down Expand Up @@ -44,4 +44,4 @@ export namespace ISwaggerSecurity {
scopes?: Record<string, string>;
}
}
}
}
7 changes: 4 additions & 3 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/sdk",
"version": "1.4.11",
"version": "1.4.14",
"description": "Nestia SDK and Swagger generator",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -35,10 +35,11 @@
},
"homepage": "https://nestia.io",
"dependencies": {
"@nestia/fetcher": "^1.4.0",
"@nestia/fetcher": ">= 1.4.0",
"cli": "^1.0.1",
"glob": "^7.2.0",
"path-to-regexp": "^6.2.1",
"reflect-metadata": ">= 0.1.12",
"tgrid": "^0.8.7",
"tsconfck": "^2.0.1",
"tsconfig-paths": "^4.1.1",
Expand All @@ -52,7 +53,7 @@
"reflect-metadata": ">= 0.1.12",
"ts-node": ">= 10.6.0",
"typescript": ">= 4.7.4",
"typia": ">= 4.1.6"
"typia": ">= 4.1.8"
},
"devDependencies": {
"@nestjs/common": ">= 7.0.1",
Expand Down
38 changes: 16 additions & 22 deletions packages/sdk/src/INestiaConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type ts from "typescript";

import type { ISwaggerDocument } from "./structures/ISwaggerDocument";
import { ISwaggerSecurityScheme } from "./structures/ISwaggerSecurityScheme";
import type { StripEnums } from "./utils/StripEnums";

/**
Expand Down Expand Up @@ -178,28 +179,21 @@ export namespace INestiaConfig {

/**
* Security schemes.
*
* When generating `swagger.json` file through `nestia`, if your controllers or
* theirs methods have a security key which is not enrolled in here property,
* it would be an error.
*/
security?: Record<string, ISwaggerConfig.ISecurityScheme>;
}
export namespace ISwaggerConfig {
export type ISecurityScheme =
| IApiKey
| Exclude<
ISwaggerDocument.ISecurityScheme,
ISwaggerDocument.ISecurityScheme.IApiKey
>;
export interface IApiKey {
type: "apiKey";

/**
* @default header
*/
in?: "header" | "query" | "cookie";

/**
* @default Authorization
*/
name?: string;
}
security?: Record<string, ISwaggerSecurityScheme>;

/**
* Decompose query DTO.
*
* If you configure this property to be `true`, the query DTO would be decomposed
* into individual query parameters per each property.
*
* @default false
*/
decompose?: boolean;
}
}
29 changes: 29 additions & 0 deletions packages/sdk/src/NestiaSdkApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,18 @@ export class NestiaSdkApplication {
routeList.push(...ControllerAnalyzer.analyze(checker, file, c));
}

// FIND IMPLICIT TYPES
const implicit: IRoute[] = routeList.filter(is_implicit_return_typed);
if (implicit.length > 0)
throw new Error(
`NestiaApplication.${method}(): implicit return type is not allowed.\n` +
"\n" +
"List of implicit return typed routes:\n" +
implicit
.map((it) => ` - ${it.symbol} at "${it.location}"`)
.join("\n"),
);

// DO GENERATE
AccessorAnalyzer.analyze(routeList);
await archiver(checker)(config(this.config_))(routeList);
Expand Down Expand Up @@ -273,3 +285,20 @@ const title = (str: string): void => {
console.log(` ${str}`);
console.log("-----------------------------------------------------------");
};

const is_implicit_return_typed = (route: IRoute): boolean => {
const name: string = route.output.name;
if (name === "void") return false;
else if (name.indexOf("readonly [") !== -1) return true;

const pos: number = name.indexOf("__object");
if (pos === -1) return false;

const before: number = pos - 1;
const after: number = pos + "__object".length;
for (const i of [before, after])
if (name[i] === undefined) continue;
else if (VARIABLE.test(name[i])) return false;
return true;
};
const VARIABLE = /[a-zA-Z_$0-9]/;
25 changes: 24 additions & 1 deletion packages/sdk/src/analyses/ControllerAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PathUtil } from "../utils/PathUtil";
import { GenericAnalyzer } from "./GenericAnalyzer";
import { ImportAnalyzer } from "./ImportAnalyzer";
import { PathAnalyzer } from "./PathAnalyzer";
import { SecurityAnalyzer } from "./SecurityAnalyzer";

export namespace ControllerAnalyzer {
export function analyze(
Expand Down Expand Up @@ -134,8 +135,29 @@ export namespace ControllerAnalyzer {
.toJSON()
.map((pair) => [pair.first, pair.second.toJSON()]);

// CONSTRUCT COMMON DATA
// PARSE COMMENT TAGS
const tags = signature.getJsDocTags();
const security: Record<string, string[]>[] = SecurityAnalyzer.merge(
...controller.security,
...func.security,
...tags
.filter((tag) => tag.name === "security")
.map((tag) =>
(tag.text ?? []).map((text) => {
const line: string[] = text.text
.split(" ")
.filter((s) => s.trim())
.filter((s) => !!s.length);
if (line.length === 0) return {};
return {
[line[0]]: line.slice(1),
};
}),
)
.flat(),
);

// CONSTRUCT COMMON DATA
const common: Omit<IRoute, "path" | "accessors"> = {
...func,
parameters,
Expand Down Expand Up @@ -168,6 +190,7 @@ export namespace ControllerAnalyzer {
source: t.text![0].text,
},
),
security,
};

// CONFIGURE PATHS
Expand Down
12 changes: 10 additions & 2 deletions packages/sdk/src/analyses/ReflectAnalyzer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as Constants from "@nestjs/common/constants";
import "reflect-metadata";
import { equal } from "tstl/ranges/module";

import { IController } from "../structures/IController";
import { ParamCategory } from "../structures/ParamCategory";
import { ArrayUtil } from "../utils/ArrayUtil";
import { PathAnalyzer } from "./PathAnalyzer";

declare const Reflect: any;
import { SecurityAnalyzer } from "./SecurityAnalyzer";

type IModule = {
[key: string]: any;
Expand Down Expand Up @@ -89,6 +89,7 @@ export namespace ReflectAnalyzer {
name,
paths,
functions: [],
security: _Get_security(creator),
};

// PARSE CHILDREN DATA
Expand Down Expand Up @@ -125,6 +126,12 @@ export namespace ReflectAnalyzer {
else return value;
}

function _Get_security(value: any): Record<string, string[]>[] {
const entire: Record<string, string[]>[] | undefined =
Reflect.getMetadata("swagger/apiSecurity", value);
return entire ? SecurityAnalyzer.merge(...entire) : [];
}

/* ---------------------------------------------------------
FUNCTION
--------------------------------------------------------- */
Expand Down Expand Up @@ -173,6 +180,7 @@ export namespace ReflectAnalyzer {
typeof h?.value === "string" &&
h.name.toLowerCase() === "content-type",
)?.value ?? "application/json",
security: _Get_security(proto),
};

// PARSE CHILDREN DATA
Expand Down
20 changes: 20 additions & 0 deletions packages/sdk/src/analyses/SecurityAnalyzer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MapUtil } from "../utils/MapUtil";

export namespace SecurityAnalyzer {
export const merge = (...entire: Record<string, string[]>[]) => {
const dict: Map<string, Set<string>> = new Map();
for (const obj of entire)
for (const [key, value] of Object.entries(obj)) {
const set = MapUtil.take(dict, key, () => new Set());
for (const val of value) set.add(val);
}
const output: Record<string, string[]>[] = [];
for (const [key, set] of dict) {
const obj = {
[key]: [...set],
};
output.push(obj);
}
return output;
};
}
Loading

0 comments on commit 06a0e7d

Please sign in to comment.