{"id":15046587,"url":"https://github.com/soraping/lenneth","last_synced_at":"2025-10-27T18:30:28.516Z","repository":{"id":122503615,"uuid":"93402995","full_name":"soraping/lenneth","owner":"soraping","description":"a node web framework on top of Koa2","archived":false,"fork":false,"pushed_at":"2019-04-04T06:10:19.000Z","size":142,"stargazers_count":42,"open_issues_count":0,"forks_count":9,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-09-26T11:19:50.980Z","etag":null,"topics":["decorators","koa2","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/soraping.png","metadata":{"files":{"readme":"README.MD","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-06-05T12:46:42.000Z","updated_at":"2025-05-24T09:07:07.000Z","dependencies_parsed_at":"2023-04-29T21:47:15.546Z","dependency_job_id":null,"html_url":"https://github.com/soraping/lenneth","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/soraping/lenneth","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soraping%2Flenneth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soraping%2Flenneth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soraping%2Flenneth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soraping%2Flenneth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soraping","download_url":"https://codeload.github.com/soraping/lenneth/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soraping%2Flenneth/sbom","scorecard":{"id":838423,"data":{"date":"2025-08-11","repo":{"name":"github.com/soraping/lenneth","commit":"240c2b57003da8280e0e77d4b1d10202afbf876f"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.6,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-23T19:48:29.251Z","repository_id":122503615,"created_at":"2025-08-23T19:48:29.252Z","updated_at":"2025-08-23T19:48:29.252Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":281319654,"owners_count":26481039,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-27T02:00:05.855Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["decorators","koa2","typescript"],"created_at":"2024-09-24T20:53:16.041Z","updated_at":"2025-10-27T18:30:28.512Z","avatar_url":"https://github.com/soraping.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"### lenneth(蕾娜斯)\n\n\u003e 说明\n\n封装 lenneth 旨在快速方便的搭建出一个 node web 应用，不过度封装也不随波逐流，koa 的 node 是简单的，lenneth 也是。\n\n基于 ES6+typescript 的一些特性，做了一些类似 spring 的注解，对开发本身不增加复杂度，并且不失 koa 的灵活性。\nlenneth 内部已经集成了 koa, koa-bodyparser, koa-router 这 3 个基础库，已经满足了大部分的开发，如果需要引入其他的中间件，可以在入口文件中引入。\n\nlenneth 抽象了 3 个模块，分别是 controller,middleware,service，内部不接入任何 db 和 cache。\n\n\u003e 安装\n\n```bash\nyarn add lenneth\n# or\nnpm install lenneth\n```\n\n也可以客户端安装\n\n```bash\n# lic 全局安装\nnpm install -g lenneth-cli\n\n# 初始化工程目录\nlenneth init yourApp\n```\n\n\u003e 瞄一眼\n\n```javascript\nimport { LennethApplication, ServerSettings, ILenneth } from \"lenneth\";\n\n@ServerSettings({\n  port: 8081\n})\nclass App extends LennethApplication implements ILenneth {\n  $onMountingMiddlewares() {\n    this.use(async (ctx, next) =\u003e {\n      ctx.body = \"hello world\";\n    });\n  }\n}\n\nnew App().start();\n```\n\nopen in browser\n\n```\nhttp://localhost:8081\n```\n\n\u003e lenneth 核心\n\nkoa 最精髓的就是它的洋葱模型，而洋葱模型的元素就是一个一个的中间件，lenneth 的封装就是将普通的类方法转化成 koa 的中间件\n\n```javascript\n  /**\n   * koa中间件，有两个参数\n   * @params ctx 上下文\n   * @params next 洋葱模型执行下一个中间件\n   */\n  async (ctx, next) =\u003e {\n\n  }\n\n  /**\n   * 这个类方法并不是koa的中间件\n   * 按照设计思路，类方法的两个参数一个是获取path参数，一个是获取返回对象，和koa的中间件参数不同\n   */\n  @Get(\"/detail/:id\")\n  @UseBefore(UserRuleAuth)\n  async getUserDetail(\n    @PathVariable(\"id\") id: string,\n    @Response() response: TResponse\n  ) {\n    response.body = this.userService.getUserById(id);\n  }\n```\n\n转换函数\n\n这个方法就是将上述的类方法转成 koa 的中间件。在类方法的上层封装了一个 koa 的中间件方法，在这个方法内部自动执行类方法，并将这个方法的 this 指向原来的类。\n\n```javascript\n// lenneth封装koa2的核心\nconst toAsyncMiddleware = (\n  target: Object | any,\n  middleware: TApiMiddleware,\n  key?: string,\n  cb?: (key: string, ctx: IContext, next: TNext) =\u003e any[]\n) =\u003e {\n  return async (ctx: IContext, next: TNext) =\u003e {\n    if (key) {\n      // 此处一定要用call来重新设置this指向\n      return middleware.call(target, ...cb(key, ctx, next), ctx, next);\n    }\n    return middleware.call(target, ctx, next);\n  };\n};\n```\n\n\u003e 各个模块\n\n- application\n\n入口文件处，使用 ServerSettings 修饰，里面的参数都是一些全局方法，如 interceptor，response 等，这些都是一个 middleware，lenneth 只是依照 koa 的洋葱模型调整了他们的执行顺利\n\n```javascript\n@ServerSettings({\n  port: 8081,\n  // controller\n  imports: {\n    \"/apis\": UserController\n  },\n  // 拦截器\n  interceptor: Interceptor,\n  // 返回值封装\n  response: AppResponse,\n  // error事件捕获\n  globalError: GlobalError\n})\nclass App extends LennethApplication implements ILenneth {\n  $onMountingMiddlewares() {\n    this.use(logger());\n  }\n}\n```\n\n- interceptor\n\n其实也是一个中间件，只不过在最前执行\n\n```javascript\nimport { IMiddleware, Middleware, HeaderParams, Next, TNext } from \"lenneth\";\n\n@Middleware()\nexport class Interceptor implements IMiddleware {\n  async use(@HeaderParams() header: any, @Next() next: TNext) {\n    console.log(\"Interceptor\", header);\n    await next();\n  }\n}\n```\n\n- response\n\n中间件，在最后执行，默认开启，可以覆盖\n\n```javascript\nimport { IMiddleware, IContext, TResponse, TNext } from \"@interfaces\";\nimport { Middleware, Response, Next } from \"@decorators\";\nimport { HttpStatus, ResponseStatus } from \"@common\";\nimport { LennethError } from \"./Lenneth-error\";\n@Middleware()\nexport class LennethResponse implements IMiddleware {\n  async use(\n    @Response() response: TResponse,\n    @Next() next: TNext,\n    ctx: IContext\n  ) {\n    try {\n      // 执行前面所有的中间件\n      await next();\n      // 统一处理返回\n      if (response.body) {\n        return (response.body = {\n          code: 0,\n          message: ResponseStatus.SUCCESS,\n          data: response.body\n        });\n      }\n      return (response.body = { code: 0, message: ResponseStatus.SUCCESS });\n    } catch (err) {\n      ctx.status = err.code;\n      response.status = HttpStatus.OK;\n      if (err instanceof LennethError) {\n        response.body = {\n          code: err.code,\n          message: err.message || ResponseStatus.ERROR\n        };\n      } else {\n        response.body = {\n          code: err.code || HttpStatus.INTERNAL_SERVER_ERROR,\n          message: err.message || ResponseStatus.ERROR\n        };\n        // 未识别错误 抛至最外层error全局处理\n        throw err;\n      }\n    }\n  }\n}\n```\n\n- controller\n\ncontroller 主要是设置 router 和注入 services\n\nrouter 的修饰器有 Post,Get 等,params 参数的获取同 spring，注入 service 使用修饰器 Autowired,这个也和 spring 一致\n\n```javascript\nimport {\n  Controller,\n  Autowired,\n  Post,\n  Get,\n  RequestBody,\n  PathVariable,\n  Response,\n  TResponse,\n  UseBefore,\n  Description\n} from \"lenneth\";\nimport { UserService } from \"../services\";\nimport { IUserInfo } from \"../interface\";\nimport { UserAuth, RuleAuth } from \"../middleware\";\n\n@Controller(\"/user\")\nexport class UserController {\n  @Autowired()\n  userService: UserService;\n\n  @Post(\"/add\")\n  @Description(\"添加会员\")\n  @UseBefore(UserAuth, RuleAuth)\n  async addUser(\n    @RequestBody() user: IUserInfo,\n    @Response() response: TResponse\n  ) {\n    response.body = this.userService.addUser(user);\n  }\n\n  @Get(\"/detail/:userId\")\n  @UseBefore(UserAuth)\n  @Description(\"查询会员\")\n  async getUser(\n    @PathVariable(\"userId\") userId: string,\n    @Response() response: TResponse\n  ) {\n    response.body = this.userService.getUserInfo(userId);\n  }\n}\n```\n\n- middleware\n\nmiddleware 本质上其实就是 koa 的中间件，只不过我在此基础上又抽象出一层方法来引入获取 params 的方法，用来方便开发\n\n在 controller 每个 api 上，使用 UseBefore 修饰器即可使用这些 middleware，在运行期，middleware 先于 controller 定义的接口，如果 middleware 没有调用 next 函数，则不会调用下一个中间件(kao 洋葱模型)\n\n```javascript\nimport { IMiddleware, Middleware, Next, TNext, HeaderParams } from \"lenneth\";\n\n@Middleware()\nexport class UserAuth implements IMiddleware {\n  async use(@HeaderParams() headers: any, @Next() next: TNext) {\n    await next();\n  }\n}\n\n@Middleware()\nexport class RuleAuth implements IMiddleware {\n  async use(@HeaderParams() headers: any, @Next() next: TNext) {\n    await next();\n  }\n}\n```\n\n- service\n\n这个模块只是做一个类输出方法\n\n```javascript\nexport class UserService {\n  addUser(userInfo: IUserInfo) {\n    return userInfo;\n  }\n\n  getUserInfo(id: string) {\n    return {\n      name: \"zhangsan\",\n      age: 30\n    };\n  }\n}\n```\n\n\u003e 单元测试\n\n```bash\nyarn test\n```\n\n\u003e 案例\n\n[lenneth-demo](https://github.com/soraping/lenneth-demo)\n\n\u003e 项目地址\n\n[lenneth](https://github.com/soraping/lenneth)\n\n\u003e 脚手架工具\n\n[lenneth-cli](https://github.com/soraping/lenneth-cli)\n\n\u003e 说在最后\n\n当初做项目的时候，在 github 上搜过一个项目，是基于 express 的--[ts-express-decorators](https://github.com/Romakita/ts-express-decorators)，里面有很多不错的设计，lenneth 里的服务启动生命周期就是照搬其中的。不过我不喜欢把 node 弄得那么大，那么全，而且，koa 本身就是一个极简的应用，所以，lenneth 仅仅只是做了一层封装，繁简自然。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoraping%2Flenneth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoraping%2Flenneth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoraping%2Flenneth/lists"}