├── .dockerignore ├── .env.example ├── .gitattributes ├── .github ├── FUNDING.yml ├── actions │ ├── setup-databases │ │ └── action.yml │ └── setup-node │ │ └── action.yml └── workflows │ ├── api-client.yml │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.mjs ├── .vscode └── settings.json ├── ADDITIONAL_TERMS.md ├── LICENSE ├── README.md ├── apps └── core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── assets-push.sh │ ├── download-latest-admin-assets-dev.js │ ├── download-latest-admin-assets.js │ ├── ecosystem.config.js │ ├── ecosystem.dev.config.js │ ├── external │ ├── pino │ │ ├── index.js │ │ └── package.json │ ├── readme.md │ └── request │ │ ├── index.js │ │ └── package.json │ ├── get-latest-admin-version.js │ ├── global.d.ts │ ├── nest-cli.json │ ├── package.json │ ├── readme.md │ ├── scripts │ ├── after-bundle.js │ └── bundle.sh │ ├── src │ ├── app.config.test.ts │ ├── app.config.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── bootstrap.ts │ ├── cluster.ts │ ├── common │ │ ├── adapters │ │ │ ├── fastify.adapter.ts │ │ │ └── socket.adapter.ts │ │ ├── contexts │ │ │ └── request.context.ts │ │ ├── decorators │ │ │ ├── api-controller.decorator.ts │ │ │ ├── auth.decorator.ts │ │ │ ├── cache.decorator.ts │ │ │ ├── cookie.decorator.ts │ │ │ ├── cron-description.decorator.ts │ │ │ ├── cron-once.decorator.ts │ │ │ ├── current-user.decorator.ts │ │ │ ├── demo.decorator.ts │ │ │ ├── http.decorator.ts │ │ │ ├── ip.decorator.ts │ │ │ ├── role.decorator.ts │ │ │ └── transform-boolean.decorator.ts │ │ ├── exceptions │ │ │ ├── ban-in-demo.exception.ts │ │ │ ├── biz.exception.ts │ │ │ ├── cant-find.exception.ts │ │ │ └── no-content-canbe-modified.exception.ts │ │ ├── filters │ │ │ └── any-exception.filter.ts │ │ ├── guards │ │ │ ├── auth.guard.ts │ │ │ ├── roles.guard.ts │ │ │ ├── spider.guard.ts │ │ │ └── throttler.guard.ts │ │ ├── interceptors │ │ │ ├── allow-all-cors.interceptor.ts │ │ │ ├── analyze.interceptor.ts │ │ │ ├── cache.interceptor.ts │ │ │ ├── db-query.interceptor.ts │ │ │ ├── idempotence.interceptor.ts │ │ │ ├── json-transform.interceptor.ts │ │ │ ├── logging.interceptor.ts │ │ │ └── response.interceptor.ts │ │ ├── middlewares │ │ │ └── request-context.middleware.ts │ │ └── pipes │ │ │ └── validation.pipe.ts │ ├── constants │ │ ├── article.constant.ts │ │ ├── business-event.constant.ts │ │ ├── cache.constant.ts │ │ ├── db.constant.ts │ │ ├── error-code.constant.ts │ │ ├── event-bus.constant.ts │ │ ├── meta.constant.ts │ │ ├── other.constant.ts │ │ ├── path.constant.ts │ │ └── system.constant.ts │ ├── decorators │ │ ├── dto │ │ │ ├── isAllowedUrl.ts │ │ │ ├── isBooleanOrString.ts │ │ │ ├── isMongoIdOrInt.ts │ │ │ ├── isNilOrString.ts │ │ │ └── transformEmptyNull.ts │ │ └── simpleValidatorFactory.ts │ ├── dump.ts │ ├── global │ │ ├── consola.global.ts │ │ ├── dayjs.global.ts │ │ ├── env.global.ts │ │ ├── index.global.ts │ │ └── json.global.ts │ ├── main.ts │ ├── migration │ │ ├── helper.ts │ │ ├── helper │ │ │ └── encrypt-configs.ts │ │ ├── history.ts │ │ ├── migrate.ts │ │ └── version │ │ │ ├── v2.0.0-alpha.1.ts │ │ │ ├── v3.30.0.ts │ │ │ ├── v3.36.0.ts │ │ │ ├── v3.39.3.ts │ │ │ ├── v4.6.0-1.ts │ │ │ ├── v4.6.0.ts │ │ │ ├── v4.6.2.ts │ │ │ ├── v5.0.0-1.ts │ │ │ ├── v5.1.1.ts │ │ │ ├── v5.6.0.ts │ │ │ └── v7.2.1.ts │ ├── modules │ │ ├── ack │ │ │ ├── ack.controller.ts │ │ │ ├── ack.dto.ts │ │ │ └── ack.module.ts │ │ ├── activity │ │ │ ├── activity.constant.ts │ │ │ ├── activity.controller.ts │ │ │ ├── activity.interface.ts │ │ │ ├── activity.model.ts │ │ │ ├── activity.module.ts │ │ │ ├── activity.service.ts │ │ │ ├── activity.util.ts │ │ │ └── dtos │ │ │ │ ├── activity.dto.ts │ │ │ │ ├── like.dto.ts │ │ │ │ └── presence.dto.ts │ │ ├── aggregate │ │ │ ├── aggregate.controller.ts │ │ │ ├── aggregate.dto.ts │ │ │ ├── aggregate.interface.ts │ │ │ ├── aggregate.module.ts │ │ │ └── aggregate.service.ts │ │ ├── ai │ │ │ ├── ai-agent │ │ │ │ └── ai-agent.service.ts │ │ │ ├── ai-deep-reading │ │ │ │ ├── ai-deep-reading.controller.ts │ │ │ │ ├── ai-deep-reading.dto.ts │ │ │ │ ├── ai-deep-reading.model.ts │ │ │ │ └── ai-deep-reading.service.ts │ │ │ ├── ai-summary │ │ │ │ ├── ai-summary.controller.ts │ │ │ │ ├── ai-summary.dto.ts │ │ │ │ ├── ai-summary.model.ts │ │ │ │ └── ai-summary.service.ts │ │ │ ├── ai-writer │ │ │ │ ├── ai-writer.controller.ts │ │ │ │ ├── ai-writer.dto.ts │ │ │ │ └── ai-writer.service.ts │ │ │ ├── ai.constants.ts │ │ │ ├── ai.module.ts │ │ │ └── ai.service.ts │ │ ├── analyze │ │ │ ├── analyze.controller.ts │ │ │ ├── analyze.dto.ts │ │ │ ├── analyze.model.ts │ │ │ ├── analyze.module.ts │ │ │ └── analyze.service.ts │ │ ├── auth │ │ │ ├── auth.constant.ts │ │ │ ├── auth.controller.ts │ │ │ ├── auth.implement.ts │ │ │ ├── auth.interface.ts │ │ │ ├── auth.middleware.ts │ │ │ ├── auth.module.ts │ │ │ └── auth.service.ts │ │ ├── authn │ │ │ ├── auth.module.ts │ │ │ ├── authn.controller.ts │ │ │ ├── authn.model.ts │ │ │ └── authn.service.ts │ │ ├── backup │ │ │ ├── backup.controller.ts │ │ │ ├── backup.module.ts │ │ │ └── backup.service.ts │ │ ├── category │ │ │ ├── category.controller.ts │ │ │ ├── category.dto.ts │ │ │ ├── category.model.ts │ │ │ ├── category.module.ts │ │ │ └── category.service.ts │ │ ├── comment │ │ │ ├── block-keywords.json │ │ │ ├── comment.controller.ts │ │ │ ├── comment.dto.ts │ │ │ ├── comment.email.default.ts │ │ │ ├── comment.enum.ts │ │ │ ├── comment.interceptor.ts │ │ │ ├── comment.model.ts │ │ │ ├── comment.module.ts │ │ │ └── comment.service.ts │ │ ├── configs │ │ │ ├── configs.default.ts │ │ │ ├── configs.dto.ts │ │ │ ├── configs.encrypt.util.ts │ │ │ ├── configs.interface.ts │ │ │ ├── configs.jsonschema.decorator.ts │ │ │ ├── configs.model.ts │ │ │ ├── configs.module.ts │ │ │ └── configs.service.ts │ │ ├── debug │ │ │ ├── debug.controller.ts │ │ │ ├── debug.module.ts │ │ │ └── debug.service.ts │ │ ├── demo │ │ │ └── demo.module.ts │ │ ├── dependency │ │ │ ├── dependency.controller.ts │ │ │ └── dependency.module.ts │ │ ├── feed │ │ │ ├── feed.controller.ts │ │ │ └── feed.module.ts │ │ ├── file │ │ │ ├── file.controller.ts │ │ │ ├── file.dto.ts │ │ │ ├── file.module.ts │ │ │ ├── file.service.ts │ │ │ └── file.type.ts │ │ ├── health │ │ │ ├── health.controller.ts │ │ │ ├── health.dto.ts │ │ │ ├── health.module.ts │ │ │ └── sub-controller │ │ │ │ ├── cron.controller.ts │ │ │ │ └── log.controller.ts │ │ ├── helper │ │ │ ├── helper.controller.ts │ │ │ ├── helper.module.ts │ │ │ └── helper.service.ts │ │ ├── init │ │ │ ├── init.controller.ts │ │ │ ├── init.guard.ts │ │ │ ├── init.module.ts │ │ │ └── init.service.ts │ │ ├── link │ │ │ ├── link-mail.enum.ts │ │ │ ├── link.controller.ts │ │ │ ├── link.dto.ts │ │ │ ├── link.model.ts │ │ │ ├── link.module.ts │ │ │ └── link.service.ts │ │ ├── markdown │ │ │ ├── markdown.controller.ts │ │ │ ├── markdown.dto.ts │ │ │ ├── markdown.interface.ts │ │ │ ├── markdown.module.ts │ │ │ ├── markdown.service.ts │ │ │ └── markdown.util.ts │ │ ├── mcp │ │ │ ├── mcp.module.ts │ │ │ └── mcp.service.ts │ │ ├── note │ │ │ ├── models │ │ │ │ └── coordinate.model.ts │ │ │ ├── note.controller.ts │ │ │ ├── note.dto.ts │ │ │ ├── note.model.ts │ │ │ ├── note.module.ts │ │ │ ├── note.service.ts │ │ │ └── note.type.ts │ │ ├── option │ │ │ ├── controllers │ │ │ │ ├── base.option.controller.ts │ │ │ │ └── email.option.controller.ts │ │ │ ├── dtoes │ │ │ │ ├── config.dto.ts │ │ │ │ └── email.dto.ts │ │ │ ├── option.decorator.ts │ │ │ ├── option.model.ts │ │ │ └── option.module.ts │ │ ├── page │ │ │ ├── page.controller.ts │ │ │ ├── page.dto.ts │ │ │ ├── page.model.ts │ │ │ ├── page.module.ts │ │ │ └── page.service.ts │ │ ├── pageproxy │ │ │ ├── pageproxy.controller.ts │ │ │ ├── pageproxy.dto.ts │ │ │ ├── pageproxy.module.ts │ │ │ └── pageproxy.service.ts │ │ ├── post │ │ │ ├── post.controller.ts │ │ │ ├── post.dto.ts │ │ │ ├── post.model.ts │ │ │ ├── post.module.ts │ │ │ ├── post.service.ts │ │ │ └── post.type.ts │ │ ├── project │ │ │ ├── project.controller.ts │ │ │ ├── project.model.ts │ │ │ └── project.module.ts │ │ ├── reader │ │ │ ├── reader.controller.ts │ │ │ ├── reader.model.ts │ │ │ ├── reader.module.ts │ │ │ └── reader.service.ts │ │ ├── recently │ │ │ ├── recently.controller.ts │ │ │ ├── recently.dto.ts │ │ │ ├── recently.model.ts │ │ │ ├── recently.module.ts │ │ │ └── recently.service.ts │ │ ├── render │ │ │ ├── render.controller.ts │ │ │ └── render.module.ts │ │ ├── say │ │ │ ├── say.controller.ts │ │ │ ├── say.model.ts │ │ │ ├── say.module.ts │ │ │ └── say.service.ts │ │ ├── search │ │ │ ├── search.controller.ts │ │ │ ├── search.dto.ts │ │ │ ├── search.module.ts │ │ │ └── search.service.ts │ │ ├── server-time │ │ │ ├── server-time.controller.ts │ │ │ ├── server-time.middleware.ts │ │ │ └── server-time.module.ts │ │ ├── serverless │ │ │ ├── function.types.ts │ │ │ ├── mock-response.util.ts │ │ │ ├── pack │ │ │ │ ├── built-in │ │ │ │ │ ├── geocode_location.ts │ │ │ │ │ ├── geocode_search.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── ip-query.ts │ │ │ │ └── index.ts │ │ │ ├── serverless.controller.ts │ │ │ ├── serverless.dto.ts │ │ │ ├── serverless.model.ts │ │ │ ├── serverless.module.ts │ │ │ ├── serverless.readme.md │ │ │ ├── serverless.service.ts │ │ │ └── serverless.util.ts │ │ ├── sitemap │ │ │ ├── sitemap.controller.ts │ │ │ └── sitemap.module.ts │ │ ├── slug-tracker │ │ │ ├── slug-tracker.model.ts │ │ │ ├── slug-tracker.module.ts │ │ │ └── slug-tracker.service.ts │ │ ├── snippet │ │ │ ├── snippet.controller.ts │ │ │ ├── snippet.dto.ts │ │ │ ├── snippet.model.ts │ │ │ ├── snippet.module.ts │ │ │ └── snippet.service.ts │ │ ├── subscribe │ │ │ ├── subscribe-mail.enum.ts │ │ │ ├── subscribe.constant.ts │ │ │ ├── subscribe.controller.ts │ │ │ ├── subscribe.dto.ts │ │ │ ├── subscribe.email.default.ts │ │ │ ├── subscribe.model.ts │ │ │ ├── subscribe.module.ts │ │ │ └── subscribe.service.ts │ │ ├── sync-update │ │ │ ├── sync-update.model.ts │ │ │ ├── sync-update.module.ts │ │ │ ├── sync-update.service.ts │ │ │ └── sync-update.type.ts │ │ ├── sync │ │ │ ├── sync.constant.ts │ │ │ ├── sync.controller.ts │ │ │ ├── sync.dto.ts │ │ │ ├── sync.module.ts │ │ │ └── sync.service.ts │ │ ├── topic │ │ │ ├── topic.controller.ts │ │ │ ├── topic.model.ts │ │ │ ├── topic.module.ts │ │ │ └── topic.service.ts │ │ ├── update │ │ │ ├── update.controller.ts │ │ │ ├── update.dto.ts │ │ │ ├── update.module.ts │ │ │ └── update.service.ts │ │ ├── user │ │ │ ├── user.controller.ts │ │ │ ├── user.dto.ts │ │ │ ├── user.model.ts │ │ │ ├── user.module.ts │ │ │ └── user.service.ts │ │ └── webhook │ │ │ ├── webhook-event.model.ts │ │ │ ├── webhook.controller.ts │ │ │ ├── webhook.model.ts │ │ │ ├── webhook.module.ts │ │ │ └── webhook.service.ts │ ├── processors │ │ ├── database │ │ │ ├── database.models.ts │ │ │ ├── database.module.ts │ │ │ ├── database.provider.ts │ │ │ └── database.service.ts │ │ ├── gateway │ │ │ ├── admin │ │ │ │ └── events.gateway.ts │ │ │ ├── base.gateway.ts │ │ │ ├── gateway.module.ts │ │ │ ├── gateway.service.ts │ │ │ ├── shared │ │ │ │ ├── auth.gateway.ts │ │ │ │ └── events.gateway.ts │ │ │ └── web │ │ │ │ ├── dtos │ │ │ │ └── message.ts │ │ │ │ ├── events.gateway.ts │ │ │ │ └── hook.interface.ts │ │ ├── helper │ │ │ ├── helper.asset.service.ts │ │ │ ├── helper.bark.service.ts │ │ │ ├── helper.counting.service.ts │ │ │ ├── helper.cron.service.ts │ │ │ ├── helper.email.service.ts │ │ │ ├── helper.event.service.ts │ │ │ ├── helper.http.service.ts │ │ │ ├── helper.image.service.ts │ │ │ ├── helper.jwt.service.ts │ │ │ ├── helper.macro.service.ts │ │ │ ├── helper.module.ts │ │ │ ├── helper.tq.service.ts │ │ │ ├── helper.upload.service.ts │ │ │ └── helper.url-builder.service.ts │ │ └── redis │ │ │ ├── cache.service.ts │ │ │ ├── redis.config.service.ts │ │ │ ├── redis.module.ts │ │ │ ├── redis.service.ts │ │ │ └── subpub.service.ts │ ├── repl.ts │ ├── shared │ │ ├── dto │ │ │ ├── file.dto.ts │ │ │ ├── id.dto.ts │ │ │ └── pager.dto.ts │ │ ├── interface │ │ │ └── paginator.interface.ts │ │ └── model │ │ │ ├── base-comment.model.ts │ │ │ ├── base.model.ts │ │ │ ├── count.model.ts │ │ │ ├── image.model.ts │ │ │ ├── plugins │ │ │ └── lean-id.ts │ │ │ └── write-base.model.ts │ ├── transformers │ │ ├── crud-factor.transformer.ts │ │ ├── db-query.transformer.ts │ │ ├── get-req.transformer.ts │ │ ├── model.transformer.ts │ │ └── paginate.transformer.ts │ ├── types │ │ ├── request.ts │ │ ├── socket-meta.ts │ │ └── unique.ts │ └── utils │ │ ├── biz.util.ts │ │ ├── check-init.util.ts │ │ ├── cos.util.ts │ │ ├── database.util.ts │ │ ├── encrypt.util.ts │ │ ├── esm-import.util.ts │ │ ├── ip.util.ts │ │ ├── jsonschema.util.ts │ │ ├── mine.util.ts │ │ ├── path.util.ts │ │ ├── pic.util.ts │ │ ├── queue.util.ts │ │ ├── redis-subpub.util.ts │ │ ├── redis.util.ts │ │ ├── s3.util.spec.ts │ │ ├── s3.util.ts │ │ ├── safe-eval.util.ts │ │ ├── schedule.util.ts │ │ ├── system.util.ts │ │ ├── time.util.ts │ │ └── tool.util.ts │ ├── test │ ├── global.d.ts │ ├── helper │ │ ├── create-e2e-app.ts │ │ ├── create-mock-global-module.ts │ │ ├── db-mock.helper.ts │ │ ├── defineProvider.ts │ │ ├── redis-mock.helper.ts │ │ ├── setup-e2e.ts │ │ └── utils.helper.ts │ ├── mock │ │ ├── constants │ │ │ └── token.ts │ │ ├── decorators │ │ │ └── auth.decorator.ts │ │ ├── guard │ │ │ └── auth.guard.ts │ │ ├── interceptors │ │ │ └── counting.interceptor.ts │ │ ├── modules │ │ │ ├── auth.mock.ts │ │ │ ├── comment.mock.ts │ │ │ ├── config.mock.ts │ │ │ ├── gateway.mock.ts │ │ │ ├── redis.mock.ts │ │ │ └── user.mock.ts │ │ └── processors │ │ │ ├── counting.mock.ts │ │ │ ├── email.mock.ts │ │ │ ├── event.mock.ts │ │ │ └── text-macro.mock.ts │ ├── setup-global.ts │ ├── setup.ts │ ├── setupFiles │ │ └── lifecycle.ts │ ├── src │ │ ├── app.controller.e2e-spec.ts │ │ ├── modules │ │ │ ├── auth │ │ │ │ └── auth.service.spec.ts │ │ │ ├── configs │ │ │ │ ├── configs.service.spec.ts │ │ │ │ └── configs.util.spec.ts │ │ │ ├── link │ │ │ │ └── link.controller.e2e-spec.ts │ │ │ ├── markdown │ │ │ │ └── markdown.service.spec.ts │ │ │ ├── note │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── note.controller.e2e-spec.ts.snap │ │ │ │ ├── note.controller.e2e-spec.ts │ │ │ │ └── note.e2e-mock.db.ts │ │ │ ├── options │ │ │ │ └── options.controller.e2e-spec.ts │ │ │ ├── post │ │ │ │ ├── post.controller.e2e-spec.ts │ │ │ │ └── post.e2e-mock.db.ts │ │ │ ├── serverless │ │ │ │ └── serverless.service.spec.ts │ │ │ ├── snippet │ │ │ │ ├── snippet.controller.e2e-spec.ts │ │ │ │ └── snippet.service.spec.ts │ │ │ └── user │ │ │ │ ├── user.controller.e2e-spec.ts │ │ │ │ ├── user.controller.spec.ts │ │ │ │ └── user.service.spec.ts │ │ ├── processors │ │ │ └── helper │ │ │ │ ├── helper.jwt.service.spec.ts │ │ │ │ └── helper.macro.service.spec.ts │ │ ├── transformers │ │ │ ├── __snapshots__ │ │ │ │ └── curd-factor.e2e-spec.ts.snap │ │ │ └── curd-factor.e2e-spec.ts │ │ └── utils │ │ │ ├── case.util.spec.ts │ │ │ ├── encrypt.util.spec.ts │ │ │ ├── pic.util.spec.ts │ │ │ ├── safe-eval.spec.ts │ │ │ └── tool.util.spec.ts │ └── tsconfig.json │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── vitest.config.mts │ └── zip-asset.sh ├── bin └── patch.js ├── codemod └── module-resolution-node-next.ts ├── configs ├── nest-cli.webpack.json ├── nginx.conf └── webpack.config.js ├── debug ├── index.html ├── socket-admin.html └── socket.io.html ├── docker-compose.yml ├── docker-entrypoint.sh ├── dockerfile ├── eslint.config.mjs ├── external ├── pino │ ├── index.js │ └── package.json ├── readme.md └── request │ ├── index.js │ └── package.json ├── package.json ├── packages ├── api-client │ ├── .gitignore │ ├── .npmignore │ ├── .npmrc │ ├── __tests__ │ │ ├── adaptors │ │ │ ├── axios.spec.ts │ │ │ ├── fetch.spec.ts │ │ │ └── umi-request.spec.ts │ │ ├── controllers │ │ │ ├── act.test.ts │ │ │ ├── activity.test.ts │ │ │ ├── aggregate.test.ts │ │ │ ├── ai.test.ts │ │ │ ├── category.test.ts │ │ │ ├── comment.test.ts │ │ │ ├── link.test.ts │ │ │ ├── note.test.ts │ │ │ ├── page.test.ts │ │ │ ├── post.test.ts │ │ │ ├── recently.test.ts │ │ │ ├── say.test.ts │ │ │ ├── search.test.ts │ │ │ ├── serverless.test.ts │ │ │ ├── snippet.test.ts │ │ │ ├── subscribe.test.ts │ │ │ ├── topic.test.ts │ │ │ └── user.test.ts │ │ ├── core │ │ │ └── client.test.ts │ │ ├── helpers │ │ │ ├── adaptor-test.ts │ │ │ ├── e2e-mock-server.ts │ │ │ ├── global-fetch.ts │ │ │ ├── instance.ts │ │ │ └── response.ts │ │ ├── mock │ │ │ └── algolia.json │ │ └── utils │ │ │ ├── auto-bind.spec.ts │ │ │ ├── camelcase-keys.spec.ts │ │ │ ├── index.test.ts │ │ │ └── path.spec.ts │ ├── adaptors │ │ ├── axios.ts │ │ ├── fetch.ts │ │ └── umi-request.ts │ ├── controllers │ │ ├── ack.ts │ │ ├── activity.ts │ │ ├── aggregate.ts │ │ ├── ai.ts │ │ ├── base.ts │ │ ├── category.ts │ │ ├── comment.ts │ │ ├── index.ts │ │ ├── link.ts │ │ ├── note.ts │ │ ├── page.ts │ │ ├── post.ts │ │ ├── project.ts │ │ ├── recently.ts │ │ ├── say.ts │ │ ├── search.ts │ │ ├── severless.ts │ │ ├── snippet.ts │ │ ├── subscribe.ts │ │ ├── topic.ts │ │ └── user.ts │ ├── core │ │ ├── attach-request.ts │ │ ├── client.ts │ │ ├── error.ts │ │ └── index.ts │ ├── dtos │ │ ├── comment.ts │ │ └── index.ts │ ├── index.ts │ ├── interfaces │ │ ├── adapter.ts │ │ ├── client.ts │ │ ├── controller.ts │ │ ├── instance.ts │ │ ├── options.ts │ │ ├── params.ts │ │ ├── request.ts │ │ └── types.ts │ ├── mod-dts.mjs │ ├── models │ │ ├── activity.ts │ │ ├── aggregate.ts │ │ ├── ai.ts │ │ ├── auth.ts │ │ ├── base.ts │ │ ├── category.ts │ │ ├── comment.ts │ │ ├── index.ts │ │ ├── link.ts │ │ ├── note.ts │ │ ├── page.ts │ │ ├── post.ts │ │ ├── project.ts │ │ ├── reader.ts │ │ ├── recently.ts │ │ ├── say.ts │ │ ├── setting.ts │ │ ├── snippet.ts │ │ ├── subscribe.ts │ │ ├── topic.ts │ │ └── user.ts │ ├── package.json │ ├── readme.md │ ├── test.d.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── utils │ │ ├── auto-bind.ts │ │ ├── camelcase-keys.ts │ │ ├── index.ts │ │ └── path.ts │ └── vitest.config.ts ├── compiled │ ├── .gitignore │ ├── auth.ts │ ├── index.ts │ ├── install-pkg.ts │ ├── package.json │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── zod.ts │ ├── zx-global.cjs │ └── zx-global.d.ts └── webhook │ ├── globals.d.ts │ ├── index.cjs │ ├── package.json │ ├── readme.md │ ├── scripts │ ├── generate.js │ └── post-build.cjs │ ├── src │ ├── error.ts │ ├── event.enum.ts │ ├── handler.ts │ ├── index.ts │ └── types.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── patches └── tinyexec@1.0.1.patch ├── paw.paw ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json └── scripts ├── deploy.js ├── download-latest-asset.js ├── init-project.mjs ├── run-pm2.mjs ├── server-deploy.js └── workflow ├── test-docker.sh └── test-server.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | patch/dist 37 | tmp 38 | out 39 | release.zip 40 | 41 | run 42 | 43 | data 44 | assets 45 | .env 46 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # THIS ENV FILE EXAMPLE ONLY FOR DOCKER COMPOSE 2 | # SEE https://docs.docker.com/compose/environment-variables/#the-env-file 3 | JWT_SECRET=asffasgvxczfqreqw213 4 | ALLOWED_ORIGINS=innei.ren,www.innei.ren 5 | 6 | # must be 64bit 7 | ENCRYPT_KEY=593f62860255feb0a914534a43814b9809cc7534da7f5485cd2e3d3c8609acab 8 | ENCRYPT_ENABLE=false 9 | 10 | # CDN Cache header 11 | CDN_CACHE_HEADER=true 12 | FORCE_CACHE_HEADER=false 13 | 14 | # CUSTOM MONGO CONNECTION 15 | MONGO_CONNECTION= 16 | 17 | # Throttle 18 | THROTTLE_TTL=10 19 | THROTTLE_LIMIT=20 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.paw filter=lfs diff=lfs merge=lfs -text 2 | demo-data.zip filter=lfs diff=lfs merge=lfs -text 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [innei] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://afdian.com/@Innei'] 14 | -------------------------------------------------------------------------------- /.github/actions/setup-databases/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Setup MongoDB and Redis' 2 | description: 'Sets up MongoDB and Redis for testing' 3 | 4 | inputs: 5 | mongodb-version: 6 | description: 'MongoDB version to use' 7 | required: false 8 | default: '4.4' 9 | redis-version: 10 | description: 'Redis version to use' 11 | required: false 12 | default: '6' 13 | 14 | runs: 15 | using: 'composite' 16 | steps: 17 | - name: Start MongoDB 18 | uses: supercharge/mongodb-github-action@1.12.0 19 | with: 20 | mongodb-version: ${{ inputs.mongodb-version }} 21 | 22 | - name: Start Redis 23 | uses: supercharge/redis-github-action@1.8.0 24 | with: 25 | redis-version: ${{ inputs.redis-version }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | patch/dist 37 | tmp 38 | out 39 | release.zip 40 | 41 | run 42 | 43 | data 44 | assets 45 | .env 46 | 47 | scripts/workflow/docker-compose.yml 48 | scripts/workflow/data 49 | 50 | bin/process-reporter 51 | 52 | dist 53 | dev/ 54 | 55 | .eslintcache -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*fastify* 2 | public-hoist-pattern[]=mongodb 3 | public-hoist-pattern[]=*eslint* 4 | public-hoist-pattern[]=*webpack* 5 | 6 | registry=https://registry.npmjs.org 7 | 8 | strict-peer-dependencies=false 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | packages/*/node_modules 5 | packages/*/dist 6 | packages/*/out 7 | packages/*/lib 8 | packages/*/build 9 | packages/*/coverage 10 | packages/*/test 11 | packages/*/tests 12 | packages/*/esm 13 | packages/*/types 14 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | import { factory } from '@innei/prettier' 2 | 3 | export default { 4 | ...factory({ 5 | tailwindcss: false, 6 | importSort: true, 7 | }), 8 | importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'], 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[javascript]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "[javascriptreact]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[typescript]": { 10 | "editor.defaultFormatter": "esbenp.prettier-vscode" 11 | }, 12 | "[typescriptreact]": { 13 | "editor.defaultFormatter": "esbenp.prettier-vscode" 14 | }, 15 | "material-icon-theme.activeIconPack": "nest", 16 | "cSpell.words": ["qaqdmin"], 17 | "typescript.tsdk": "node_modules/typescript/lib", 18 | "yaml.schemas": { 19 | "https://json.schemastore.org/github-workflow.json": "file:///Users/xiaoxun/github/innei-repo/mx-space/mx-server/.github/workflows/build.yml" 20 | }, 21 | "typescript.preferences.preferTypeOnlyAutoImports": false 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mix Space Core 2 | 3 | 本项目使用 Monorepo 进行管理。 4 | 5 | - [core](./apps/core): Server Core 主程序 6 | - [api-client](./packages/api-client):适用于前端的 API client 7 | - [webhook](./packages/webhook): Webhook SDK 8 | 9 | # 许可 10 | 11 | 此项目在 `apps/` 目录下的所有文件均使用 GNU Affero General Public License v3.0 (AGPLv3) with Additional Terms (ADDITIONAL_TERMS) 许可。 12 | 13 | 其他部分使用 MIT License 许可。 14 | 15 | 详情请查看 [LICENSE](./LICENSE) 和 [ADDITIONAL_TERMS](./ADDITIONAL_TERMS.md)。 -------------------------------------------------------------------------------- /apps/core/.gitignore: -------------------------------------------------------------------------------- 1 | schema.gql 2 | 3 | app.config.js 4 | 5 | webpack-dist 6 | 7 | ./admin 8 | dist 9 | 10 | tmp 11 | 12 | temp.ts 13 | dist 14 | -------------------------------------------------------------------------------- /apps/core/assets-push.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | cd ../../assets 3 | # rm -rf .git 4 | # git init 5 | git add . || true 6 | git commit -m 'update assets' || true 7 | git pull --rebase 8 | git remote add origin git@github.com:mx-space/assets.git || true 9 | # git branch -M master 10 | git push -u origin master -f 11 | # rm -rf .git 12 | -------------------------------------------------------------------------------- /apps/core/download-latest-admin-assets-dev.js: -------------------------------------------------------------------------------- 1 | #!env node 2 | const { appendFileSync } = require('node:fs') 3 | const { join } = require('node:path') 4 | const { fetch, $ } = require('zx-cjs') 5 | const { 6 | dashboard: { repo, version }, 7 | } = require('./package.json') 8 | 9 | const endpoint = `https://api.github.com/repos/${repo}/releases/tags/v${version}` 10 | !(async () => { 11 | const json = await fetch(endpoint).then((res) => res.json()) 12 | const downloadUrl = json.assets.find( 13 | (asset) => asset.name === 'release.zip', 14 | ).browser_download_url 15 | const buffer = await fetch(downloadUrl).then((res) => res.arrayBuffer()) 16 | appendFileSync(join(process.cwd(), 'admin-release.zip'), Buffer.from(buffer)) 17 | 18 | await $`ls -lh` 19 | 20 | await $`unzip admin-release.zip -d tmp/admin` 21 | 22 | await $`rm -f admin-release.zip` 23 | // release.zip > dist > index.html 24 | })() 25 | -------------------------------------------------------------------------------- /apps/core/download-latest-admin-assets.js: -------------------------------------------------------------------------------- 1 | #!env node 2 | const { appendFileSync } = require('node:fs') 3 | const { join } = require('node:path') 4 | const { fetch, $ } = require('zx-cjs') 5 | const { 6 | dashboard: { repo, version }, 7 | } = require('./package.json') 8 | 9 | const endpoint = `https://api.github.com/repos/${repo}/releases/tags/v${version}` 10 | !(async () => { 11 | const json = await fetch(endpoint).then((res) => res.json()) 12 | const downloadUrl = json.assets.find( 13 | (asset) => asset.name === 'release.zip', 14 | ).browser_download_url 15 | const buffer = await fetch(downloadUrl).then((res) => res.arrayBuffer()) 16 | appendFileSync(join(process.cwd(), 'admin-release.zip'), Buffer.from(buffer)) 17 | 18 | await $`ls -lh` 19 | 20 | await $`unzip admin-release.zip -d out` 21 | await $`mv out/dist out/admin` 22 | await $`rm -f admin-release.zip` 23 | // release.zip > dist > index.html 24 | })() 25 | -------------------------------------------------------------------------------- /apps/core/ecosystem.config.js: -------------------------------------------------------------------------------- 1 | const { cpus } = require('node:os') 2 | const { execSync } = require('node:child_process') 3 | const nodePath = execSync(`npm root --quiet -g`, { encoding: 'utf-8' }).split( 4 | '\n', 5 | )[0] 6 | 7 | const cpuLen = cpus().length 8 | module.exports = { 9 | apps: [ 10 | { 11 | name: 'mx-server', 12 | script: 'index.js', 13 | autorestart: true, 14 | exec_mode: 'cluster', 15 | watch: false, 16 | instances: cpuLen, 17 | max_memory_restart: '520M', 18 | args: '--color --encrypt_enable', 19 | env: { 20 | NODE_ENV: 'production', 21 | NODE_PATH: nodePath, 22 | MX_ENCRYPT_KEY: process.env.MX_ENCRYPT_KEY, 23 | PORT: process.env.PORT, 24 | // NOTE: if OOM happens, try to use jemalloc 25 | // https://blog.csdn.net/qq_21567385/article/details/135322697 26 | // LD_PRELOAD: '/usr/lib/x86_64-linux-gnu/libjemalloc.so', 27 | }, 28 | }, 29 | ], 30 | } 31 | -------------------------------------------------------------------------------- /apps/core/ecosystem.dev.config.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('node:child_process') 2 | const nodePath = execSync(`npm root --quiet -g`, { encoding: 'utf-8' }).split( 3 | '\n', 4 | )[0] 5 | 6 | module.exports = { 7 | apps: [ 8 | { 9 | name: 'mx-server', 10 | script: 'dist/src/main.js', 11 | autorestart: true, 12 | exec_mode: 'cluster', 13 | watch: false, 14 | instances: 2, 15 | max_memory_restart: '220M', 16 | args: '--color --encrypt_enable', 17 | env: { 18 | NODE_ENV: 'development', 19 | NODE_PATH: nodePath, 20 | MX_ENCRYPT_KEY: process.env.MX_ENCRYPT_KEY, 21 | PORT: process.env.PORT, 22 | }, 23 | }, 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /apps/core/external/pino/index.js: -------------------------------------------------------------------------------- 1 | // why this, because we dont need pino logger, and this logger can not bundle whole package into only one file with ncc. 2 | // only work with fastify v4+ with pino v8+ 3 | 4 | module.exports = { 5 | symbols: { 6 | // https://github.com/pinojs/pino/blob/master/lib/symbols.js 7 | serializersSym: Symbol.for('pino.serializers'), 8 | }, 9 | stdSerializers: { 10 | error: function asErrValue(err) { 11 | const obj = { 12 | type: err.constructor.name, 13 | msg: err.message, 14 | stack: err.stack, 15 | } 16 | for (const key in err) { 17 | if (obj[key] === undefined) { 18 | obj[key] = err[key] 19 | } 20 | } 21 | return obj 22 | }, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /apps/core/external/pino/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pino", 3 | "main": "./index.js" 4 | } 5 | -------------------------------------------------------------------------------- /apps/core/external/readme.md: -------------------------------------------------------------------------------- 1 | This is folder used to override dependencies. -------------------------------------------------------------------------------- /apps/core/external/request/index.js: -------------------------------------------------------------------------------- 1 | // why do this. 2 | // we dont need request, because we get image color only by buffer, and we dont need send request. 3 | module.exports = {} 4 | -------------------------------------------------------------------------------- /apps/core/external/request/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "request", 3 | "main": "index.js" 4 | } 5 | -------------------------------------------------------------------------------- /apps/core/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { ModelType } from '@typegoose/typegoose/lib/types' 2 | import type { Document, PaginateModel } from 'mongoose' 3 | 4 | declare global { 5 | export type KV = Record 6 | 7 | // @ts-ignore 8 | export type MongooseModel = ModelType & PaginateModel 9 | 10 | export const isDev: boolean 11 | 12 | export const cwd: string 13 | } 14 | 15 | export {} 16 | -------------------------------------------------------------------------------- /apps/core/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "plugins": [] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/core/scripts/bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | # Add node_modules/.bin to PATH 5 | export PATH="$(pwd)/node_modules/.bin:$(pwd)/../../node_modules/.bin:$PATH" 6 | 7 | rimraf out 8 | npm run build 9 | 10 | # Check if RELEASE environment variable is set to true 11 | if [ "$RELEASE" = "true" ]; then 12 | ncc build dist/src/main.js -o $(pwd)/out --minify -s 13 | else 14 | ncc build dist/src/main.js -o $(pwd)/out -s 15 | fi 16 | 17 | chmod +x out/index.js 18 | node scripts/after-bundle.js 19 | -------------------------------------------------------------------------------- /apps/core/src/common/adapters/socket.adapter.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from 'socket.io' 2 | 3 | import { IoAdapter } from '@nestjs/platform-socket.io' 4 | import { createAdapter } from '@socket.io/redis-adapter' 5 | 6 | import { redisSubPub } from '~/utils/redis-subpub.util' 7 | 8 | export const RedisIoAdapterKey = 'mx-core-socket' 9 | 10 | export class RedisIoAdapter extends IoAdapter { 11 | createIOServer(port: number, options?: any) { 12 | const server = super.createIOServer(port, options) as Server 13 | 14 | const { pubClient, subClient } = redisSubPub 15 | 16 | const redisAdapter = createAdapter(pubClient, subClient, { 17 | key: RedisIoAdapterKey, 18 | requestsTimeout: 10000, 19 | }) 20 | server.adapter(redisAdapter) 21 | return server 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/core/src/common/decorators/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, UseGuards } from '@nestjs/common' 2 | 3 | import { AuthGuard } from '../guards/auth.guard' 4 | 5 | export function Auth() { 6 | const decorators: (ClassDecorator | PropertyDecorator | MethodDecorator)[] = [ 7 | UseGuards(AuthGuard), 8 | ] 9 | 10 | return applyDecorators(...decorators) 11 | } 12 | 13 | export const AuthButProd = () => { 14 | if (isDev) return () => {} 15 | return Auth() 16 | } 17 | -------------------------------------------------------------------------------- /apps/core/src/common/decorators/cookie.decorator.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionContext } from '@nestjs/common' 2 | import type { FastifyRequest } from 'fastify' 3 | 4 | import { createParamDecorator } from '@nestjs/common' 5 | 6 | export const Cookies = createParamDecorator( 7 | (data: string, ctx: ExecutionContext) => { 8 | const request = ctx.switchToHttp().getRequest() 9 | return data ? request.cookies?.[data] : request.cookies 10 | }, 11 | ) 12 | -------------------------------------------------------------------------------- /apps/core/src/common/decorators/cron-description.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common' 2 | 3 | import { CRON_DESCRIPTION } from '~/constants/meta.constant' 4 | 5 | export const CronDescription = (description: string) => 6 | SetMetadata(CRON_DESCRIPTION, description) 7 | -------------------------------------------------------------------------------- /apps/core/src/common/decorators/cron-once.decorator.ts: -------------------------------------------------------------------------------- 1 | import cluster from 'node:cluster' 2 | 3 | import { Cron } from '@nestjs/schedule' 4 | 5 | import { isMainProcess } from '~/global/env.global' 6 | 7 | export const CronOnce: typeof Cron = (...rest): MethodDecorator => { 8 | // If not in cluster mode, and PM2 main worker 9 | if (isMainProcess) { 10 | return Cron.call(null, ...rest) 11 | } 12 | 13 | if (cluster.isWorker && cluster.worker?.id === 1) { 14 | return Cron.call(null, ...rest) 15 | } 16 | 17 | const returnNothing: MethodDecorator = () => {} 18 | return returnNothing 19 | } 20 | -------------------------------------------------------------------------------- /apps/core/src/common/decorators/current-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionContext } from '@nestjs/common' 2 | 3 | import { createParamDecorator } from '@nestjs/common' 4 | 5 | import { getNestExecutionContextRequest } from '~/transformers/get-req.transformer' 6 | 7 | export const CurrentUser = createParamDecorator( 8 | (data: unknown, ctx: ExecutionContext) => { 9 | return getNestExecutionContextRequest(ctx).user 10 | }, 11 | ) 12 | 13 | export const CurrentUserToken = createParamDecorator( 14 | (data: unknown, ctx: ExecutionContext) => { 15 | const token = getNestExecutionContextRequest(ctx).token 16 | 17 | return token ? token.replace(/[Bb]earer /, '') : '' 18 | }, 19 | ) 20 | 21 | export const CurrentReaderId = createParamDecorator( 22 | (data: unknown, ctx: ExecutionContext) => { 23 | return getNestExecutionContextRequest(ctx).readerId 24 | }, 25 | ) 26 | -------------------------------------------------------------------------------- /apps/core/src/common/decorators/demo.decorator.ts: -------------------------------------------------------------------------------- 1 | import type { CanActivate } from '@nestjs/common' 2 | import type { Observable } from 'rxjs' 3 | 4 | import { applyDecorators, UseGuards } from '@nestjs/common' 5 | 6 | import { banInDemo } from '~/utils/biz.util' 7 | 8 | class DemoGuard implements CanActivate { 9 | canActivate(): boolean | Promise | Observable { 10 | banInDemo() 11 | return true 12 | } 13 | } 14 | export const BanInDemo = applyDecorators(UseGuards(DemoGuard)) 15 | -------------------------------------------------------------------------------- /apps/core/src/common/decorators/ip.decorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Innei 3 | * @Date: 2020-04-30 12:21:51 4 | * @LastEditTime: 2020-07-08 21:34:18 5 | * @LastEditors: Innei 6 | * @FilePath: /mx-server/src/core/decorators/ip.decorator.ts 7 | * @Coding with Love 8 | */ 9 | import type { ExecutionContext } from '@nestjs/common' 10 | import type { FastifyRequest } from 'fastify' 11 | 12 | import { createParamDecorator } from '@nestjs/common' 13 | 14 | import { getIp } from '~/utils/ip.util' 15 | 16 | export type IpRecord = { 17 | ip: string 18 | agent: string 19 | } 20 | export const IpLocation = createParamDecorator( 21 | (data: unknown, ctx: ExecutionContext) => { 22 | const request = ctx.switchToHttp().getRequest() 23 | const ip = getIp(request) 24 | const agent = request.headers['user-agent'] 25 | return { 26 | ip, 27 | agent, 28 | } 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /apps/core/src/common/decorators/role.decorator.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionContext } from '@nestjs/common' 2 | 3 | import { createParamDecorator } from '@nestjs/common' 4 | 5 | import { isTest } from '~/global/env.global' 6 | import { getNestExecutionContextRequest } from '~/transformers/get-req.transformer' 7 | 8 | export const IsGuest = createParamDecorator( 9 | (data: unknown, ctx: ExecutionContext) => { 10 | const request = getNestExecutionContextRequest(ctx) 11 | return request.isGuest 12 | }, 13 | ) 14 | 15 | export const IsAuthenticated = createParamDecorator( 16 | (data: unknown, ctx: ExecutionContext) => { 17 | const request = getNestExecutionContextRequest(ctx) 18 | // FIXME Why can't access `isAuthenticated` in vitest test? request instance is not the same? 19 | return ( 20 | request.isAuthenticated || 21 | (isTest ? request.headers['test-token'] : false) 22 | ) 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /apps/core/src/common/decorators/transform-boolean.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer' 2 | 3 | export const TransformBoolean = () => 4 | Transform(({ value }) => value === '1' || value === 'true') 5 | -------------------------------------------------------------------------------- /apps/core/src/common/exceptions/ban-in-demo.exception.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCodeEnum } from '~/constants/error-code.constant' 2 | 3 | import { BusinessException } from './biz.exception' 4 | 5 | export class BanInDemoExcpetion extends BusinessException { 6 | constructor() { 7 | super(ErrorCodeEnum.BanInDemo) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/core/src/common/exceptions/cant-find.exception.ts: -------------------------------------------------------------------------------- 1 | import { sample } from 'lodash' 2 | 3 | import { NotFoundException } from '@nestjs/common' 4 | 5 | export const NotFoundMessage = [ 6 | '真不巧,内容走丢了 o(╥﹏╥)o', 7 | '电波无法到达 ωω', 8 | '数据..不小心丢失了啦 π_π', 9 | '404, 这也不是我的错啦 (๐•̆ ·̭ •̆๐)', 10 | '嘿,这里空空如也,不如别处走走?', 11 | ] 12 | 13 | export class CannotFindException extends NotFoundException { 14 | constructor() { 15 | super(sample(NotFoundMessage)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/core/src/common/exceptions/no-content-canbe-modified.exception.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCodeEnum } from '~/constants/error-code.constant' 2 | 3 | import { BizException } from './biz.exception' 4 | 5 | export class NoContentCanBeModifiedException extends BizException { 6 | constructor() { 7 | super(ErrorCodeEnum.NoContentCanBeModified) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/core/src/common/middlewares/request-context.middleware.ts: -------------------------------------------------------------------------------- 1 | // https://github.dev/ever-co/ever-gauzy/packages/core/src/core/context/request-context.middleware.ts 2 | 3 | import * as cls from 'cls-hooked' 4 | import type { NestMiddleware } from '@nestjs/common' 5 | import type { ServerResponse } from 'node:http' 6 | 7 | import { Injectable } from '@nestjs/common' 8 | 9 | import { BizIncomingMessage } from '~/transformers/get-req.transformer' 10 | 11 | import { RequestContext } from '../contexts/request.context' 12 | 13 | @Injectable() 14 | export class RequestContextMiddleware implements NestMiddleware { 15 | use(req: BizIncomingMessage, res: ServerResponse, next: () => any) { 16 | const requestContext = new RequestContext(req, res) 17 | 18 | const session = 19 | cls.getNamespace(RequestContext.name) || 20 | cls.createNamespace(RequestContext.name) 21 | 22 | session.run(async () => { 23 | session.set(RequestContext.name, requestContext) 24 | next() 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/core/src/common/pipes/validation.pipe.ts: -------------------------------------------------------------------------------- 1 | import type { ValidationPipeOptions } from '@nestjs/common' 2 | 3 | import { Injectable, ValidationPipe } from '@nestjs/common' 4 | 5 | import { isDev } from '~/global/env.global' 6 | 7 | @Injectable() 8 | export class ExtendedValidationPipe extends ValidationPipe { 9 | public static readonly options: ValidationPipeOptions = { 10 | transform: true, 11 | whitelist: true, 12 | errorHttpStatusCode: 422, 13 | forbidUnknownValues: true, 14 | enableDebugMessages: isDev, 15 | stopAtFirstError: true, 16 | } 17 | 18 | public static readonly shared = new ExtendedValidationPipe( 19 | ExtendedValidationPipe.options, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /apps/core/src/constants/article.constant.ts: -------------------------------------------------------------------------------- 1 | export const ArticleType = Object.freeze({ 2 | Post: 'post', 3 | Note: 'note', 4 | Page: 'page', 5 | } as const) 6 | 7 | export enum ArticleTypeEnum { 8 | Post = 'post', 9 | Note = 'note', 10 | Page = 'page', 11 | } 12 | -------------------------------------------------------------------------------- /apps/core/src/constants/db.constant.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/prefer-literal-enum-member */ 2 | export const MIGRATE_COLLECTION_NAME = 'migrations' 3 | export const CHECKSUM_COLLECTION_NAME = 'checksum' 4 | 5 | /// biz 6 | export const POST_COLLECTION_NAME = 'posts' 7 | export const NOTE_COLLECTION_NAME = 'notes' 8 | export const PAGE_COLLECTION_NAME = 'pages' 9 | 10 | export const TOPIC_COLLECTION_NAME = 'topics' 11 | export const CATEGORY_COLLECTION_NAME = 'categories' 12 | 13 | export const COMMENT_COLLECTION_NAME = 'comments' 14 | export const RECENTLY_COLLECTION_NAME = 'recentlies' 15 | 16 | export const ANALYZE_COLLECTION_NAME = 'analyzes' 17 | export const WEBHOOK_EVENT_COLLECTION_NAME = 'webhook_events' 18 | export const AI_SUMMARY_COLLECTION_NAME = 'ai_summaries' 19 | export const AI_DEEP_READING_COLLECTION_NAME = 'ai_deep_readings' 20 | 21 | export const USER_COLLECTION_NAME = 'users' 22 | export enum CollectionRefTypes { 23 | Post = POST_COLLECTION_NAME, 24 | Note = NOTE_COLLECTION_NAME, 25 | Page = PAGE_COLLECTION_NAME, 26 | Recently = RECENTLY_COLLECTION_NAME, 27 | } 28 | -------------------------------------------------------------------------------- /apps/core/src/constants/event-bus.constant.ts: -------------------------------------------------------------------------------- 1 | export enum EventBusEvents { 2 | EmailInit = 'email.init', 3 | PushSearch = 'search.push', 4 | TokenExpired = 'token.expired', 5 | CleanAggregateCache = 'cache.aggregate', 6 | SystemException = 'system.exception', 7 | ConfigChanged = 'config.changed', 8 | OauthChanged = 'oauth.changed', 9 | AppUrlChanged = 'app.url.changed', 10 | } 11 | -------------------------------------------------------------------------------- /apps/core/src/constants/meta.constant.ts: -------------------------------------------------------------------------------- 1 | export { 2 | CACHE_KEY_METADATA as HTTP_CACHE_KEY_METADATA, 3 | CACHE_TTL_METADATA as HTTP_CACHE_TTL_METADATA, 4 | } from '@nestjs/common/cache/cache.constants' 5 | 6 | export const HTTP_CACHE_DISABLE = 'cache_module:cache_disable' 7 | export const HTTP_CACHE_META_OPTIONS = 'cache_module:cache_meta_options' 8 | export const HTTP_REQUEST_TIME = 'http:req_time' 9 | export const HTTP_RES_TRANSFORM_PAGINATE = '__customHttpResTransformPagenate__' 10 | export const HTTP_RES_UPDATE_DOC_COUNT_TYPE = '__updateDocCount__' 11 | 12 | export const CRON_DESCRIPTION = '__cron:description__' 13 | 14 | export const HTTP_IDEMPOTENCE_OPTIONS = '__idempotence_options__' 15 | export const HTTP_IDEMPOTENCE_KEY = '__idempotence_key__' 16 | -------------------------------------------------------------------------------- /apps/core/src/constants/other.constant.ts: -------------------------------------------------------------------------------- 1 | export const alphabet = `1234567890abcdefghijklmnopqrstuvwxyz` 2 | -------------------------------------------------------------------------------- /apps/core/src/constants/path.constant.ts: -------------------------------------------------------------------------------- 1 | import { homedir } from 'node:os' 2 | import { join } from 'node:path' 3 | 4 | import { cwd, isDev } from '~/global/env.global' 5 | 6 | export const HOME = homedir() 7 | 8 | export const TEMP_DIR = isDev ? join(cwd, './tmp') : '/tmp/mx-space' 9 | 10 | export const DATA_DIR = isDev ? join(cwd, './tmp') : join(HOME, '.mx-space') 11 | 12 | export const THEME_DIR = isDev 13 | ? join(cwd, './tmp/theme') 14 | : join(DATA_DIR, 'theme') 15 | 16 | export const USER_ASSET_DIR = join(DATA_DIR, 'assets') 17 | export const LOG_DIR = join(DATA_DIR, 'log') 18 | 19 | export const STATIC_FILE_DIR = join(DATA_DIR, 'static') 20 | export const STATIC_FILE_TRASH_DIR = join(TEMP_DIR, 'trash') 21 | 22 | export const BACKUP_DIR = !isDev 23 | ? join(DATA_DIR, 'backup') 24 | : join(TEMP_DIR, 'backup') 25 | 26 | // 生产环境直接打包到 目录的 admin 下 27 | export const LOCAL_ADMIN_ASSET_PATH = isDev 28 | ? join(DATA_DIR, 'admin') 29 | : join(cwd, './admin') 30 | 31 | export const NODE_REQUIRE_PATH = join(DATA_DIR, 'node_modules') 32 | -------------------------------------------------------------------------------- /apps/core/src/constants/system.constant.ts: -------------------------------------------------------------------------------- 1 | export const REFLECTOR = 'Reflector' 2 | 3 | export const RESPONSE_PASSTHROUGH_METADATA = '__responsePassthrough__' 4 | // @nestjs/schedule 5 | 6 | export { SCHEDULE_CRON_OPTIONS } from '@nestjs/schedule/dist/schedule.constants' 7 | 8 | export const DB_CONNECTION_TOKEN = '__db_connection_token__' 9 | 10 | export const DB_MODEL_TOKEN_SUFFIX = '__db_model_token_suffix__' 11 | 12 | export const SKIP_LOGGING_METADATA = '__skipLogging__' 13 | 14 | export const VALIDATION_PIPE_INJECTION = '__VALIDATION_PIPE__' 15 | -------------------------------------------------------------------------------- /apps/core/src/decorators/dto/isAllowedUrl.ts: -------------------------------------------------------------------------------- 1 | import { isURL } from 'class-validator' 2 | import type { ValidationOptions } from 'class-validator' 3 | 4 | import { validatorFactory } from '../simpleValidatorFactory' 5 | 6 | export const IsAllowedUrl = (validationOptions?: ValidationOptions) => { 7 | return validatorFactory((val) => 8 | isURL(val, { require_protocol: true, require_tld: false }), 9 | )({ 10 | message: '请更正为正确的网址', 11 | ...validationOptions, 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /apps/core/src/decorators/dto/isBooleanOrString.ts: -------------------------------------------------------------------------------- 1 | import { isString } from 'class-validator' 2 | import { isBoolean, merge } from 'lodash' 3 | import type { ValidationOptions } from 'class-validator' 4 | 5 | import { validatorFactory } from '../simpleValidatorFactory' 6 | 7 | export function IsBooleanOrString(validationOptions?: ValidationOptions) { 8 | return validatorFactory((value) => isBoolean(value) || isString(value))( 9 | merge(validationOptions || {}, { 10 | message: '类型必须为 String or Boolean', 11 | }), 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /apps/core/src/decorators/dto/isMongoIdOrInt.ts: -------------------------------------------------------------------------------- 1 | import { isInt, isMongoId } from 'class-validator' 2 | import { merge } from 'lodash' 3 | import type { ValidationOptions } from 'class-validator' 4 | 5 | import { validatorFactory } from '../simpleValidatorFactory' 6 | 7 | export function IsBooleanOrString(validationOptions?: ValidationOptions) { 8 | return validatorFactory((value) => isInt(value) || isMongoId(value))( 9 | merge(validationOptions || {}, { 10 | message: '类型必须为 MongoId or Int', 11 | }), 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /apps/core/src/decorators/dto/isNilOrString.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isString, 3 | registerDecorator, 4 | ValidatorConstraint, 5 | } from 'class-validator' 6 | import { isNil } from 'lodash' 7 | import type { 8 | ValidationArguments, 9 | ValidationOptions, 10 | ValidatorConstraintInterface, 11 | } from 'class-validator' 12 | 13 | @ValidatorConstraint({ async: true }) 14 | class IsNilOrStringConstraint implements ValidatorConstraintInterface { 15 | validate(value: any, _args: ValidationArguments) { 16 | return isNil(value) || isString(value) 17 | } 18 | } 19 | 20 | export function IsNilOrString(validationOptions?: ValidationOptions) { 21 | return function (object: object, propertyName: string) { 22 | registerDecorator({ 23 | target: object.constructor, 24 | propertyName, 25 | options: validationOptions, 26 | constraints: [], 27 | validator: IsNilOrStringConstraint, 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/core/src/decorators/dto/transformEmptyNull.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer' 2 | import { IsOptional } from 'class-validator' 3 | 4 | import { applyDecorators } from '@nestjs/common' 5 | 6 | export const TransformEmptyNull = () => { 7 | return applyDecorators( 8 | Transform(({ value: val }) => (String(val).length === 0 ? null : val)), 9 | IsOptional(), 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /apps/core/src/dump.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import v8 from 'node:v8' 3 | 4 | import { TEMP_DIR } from './constants/path.constant' 5 | 6 | export function registerForMemoryDump() { 7 | function createHeapSnapshot() { 8 | const snapshotStream = v8.getHeapSnapshot() 9 | const localeDate = new Date().toLocaleString() 10 | const fileName = `${TEMP_DIR}/HeapSnapshot-${localeDate}.heapsnapshot` 11 | const fileStream = fs.createWriteStream(fileName) 12 | snapshotStream.pipe(fileStream).on('finish', () => { 13 | console.log('Heap snapshot saved to', fileName) 14 | }) 15 | } 16 | 17 | process.on('SIGUSR2', () => { 18 | console.log('SIGUSR2 received, creating heap snapshot...') 19 | createHeapSnapshot() 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /apps/core/src/global/consola.global.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, Logger } from '@innei/pretty-logger-nestjs' 2 | 3 | import { LOG_DIR } from '~/constants/path.constant' 4 | 5 | import { isTest } from './env.global' 6 | 7 | const logger = createLogger({ 8 | writeToFile: !isTest 9 | ? { 10 | loggerDir: LOG_DIR, 11 | errWriteToStdout: true, 12 | } 13 | : undefined, 14 | }) 15 | Logger.setLoggerInstance(logger) 16 | if (!isTest) { 17 | try { 18 | logger.wrapAll() 19 | } catch { 20 | logger.warn('wrap console failed') 21 | } 22 | logger.onData((data) => { 23 | const { redisSubPub } = require('../utils/redis-subpub.util') 24 | redisSubPub.publish('log', data) 25 | }) 26 | } 27 | 28 | // HACK: forhidden pm2 to override this method 29 | Object.defineProperty(process.stdout, 'write', { 30 | value: process.stdout.write, 31 | writable: false, 32 | configurable: false, 33 | }) 34 | Object.defineProperty(process.stderr, 'write', { 35 | value: process.stdout.write, 36 | writable: false, 37 | configurable: false, 38 | }) 39 | 40 | export { logger as consola, logger } 41 | -------------------------------------------------------------------------------- /apps/core/src/global/dayjs.global.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import duration from 'dayjs/plugin/duration' 3 | import localizedFormat from 'dayjs/plugin/localizedFormat' 4 | import relativeTime from 'dayjs/plugin/relativeTime' 5 | 6 | import 'dayjs/locale/zh-cn' 7 | 8 | dayjs.locale('zh-cn') 9 | dayjs.extend(localizedFormat) 10 | dayjs.extend(relativeTime) 11 | dayjs.extend(duration) 12 | -------------------------------------------------------------------------------- /apps/core/src/global/env.global.ts: -------------------------------------------------------------------------------- 1 | import cluster from 'node:cluster' 2 | 3 | export const isMainCluster = 4 | process.env.NODE_APP_INSTANCE && 5 | Number.parseInt(process.env.NODE_APP_INSTANCE) === 0 6 | export const isMainProcess = cluster.isPrimary || isMainCluster 7 | 8 | export const isDev = process.env.NODE_ENV == 'development' 9 | 10 | export const isTest = !!process.env.TEST 11 | export const isDebugMode = process.env.DEBUG_MODE === '1' 12 | export const cwd = process.cwd() 13 | -------------------------------------------------------------------------------- /apps/core/src/global/json.global.ts: -------------------------------------------------------------------------------- 1 | import JSON5 from 'json5' 2 | 3 | declare global { 4 | interface JSON { 5 | safeParse: typeof JSON.parse 6 | 7 | JSON5: typeof JSON5 8 | } 9 | } 10 | 11 | export const registerJSONGlobal = () => { 12 | JSON.safeParse = (...rest) => { 13 | try { 14 | return JSON5.parse(...rest) 15 | } catch { 16 | return null 17 | } 18 | } 19 | 20 | JSON.JSON5 = JSON5 21 | } 22 | -------------------------------------------------------------------------------- /apps/core/src/migration/helper.ts: -------------------------------------------------------------------------------- 1 | import type { Db } from 'mongodb' 2 | import type { Connection } from 'mongoose' 3 | 4 | export const defineMigration = ( 5 | name: string, 6 | migrate: (db: Db, connection: Connection) => Promise, 7 | ) => { 8 | return { 9 | name, 10 | run: migrate, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/core/src/migration/history.ts: -------------------------------------------------------------------------------- 1 | import v200Alpha1 from './version/v2.0.0-alpha.1' 2 | import v3330 from './version/v3.30.0' 3 | import v3360 from './version/v3.36.0' 4 | import v3393 from './version/v3.39.3' 5 | import v460 from './version/v4.6.0' 6 | import v4_6_0__1 from './version/v4.6.0-1' 7 | import v4_6_1 from './version/v4.6.2' 8 | import v5_0_0__1 from './version/v5.0.0-1' 9 | import v5_1_1 from './version/v5.1.1' 10 | import v5_6_0 from './version/v5.6.0' 11 | import v7_2_1 from './version/v7.2.1' 12 | 13 | export default [ 14 | v200Alpha1, 15 | v3330, 16 | v3360, 17 | v3393, 18 | v460, 19 | v4_6_0__1, 20 | v4_6_1, 21 | v5_0_0__1, 22 | v5_1_1, 23 | v5_6_0, 24 | v7_2_1, 25 | ] 26 | -------------------------------------------------------------------------------- /apps/core/src/migration/version/v2.0.0-alpha.1.ts: -------------------------------------------------------------------------------- 1 | // patch for version lower than v2.0.0-alpha.1 2 | import type { Db } from 'mongodb' 3 | 4 | export default (async function v200Alpha1(db: Db) { 5 | return await Promise.all([ 6 | ['notes', 'posts'].map(async (collectionName) => { 7 | return db 8 | .collection(collectionName) 9 | .updateMany({}, { $unset: { options: 1 } }) 10 | }), 11 | db.collection('categories').updateMany({}, { $unset: { count: '' } }), 12 | ]) 13 | }) 14 | -------------------------------------------------------------------------------- /apps/core/src/migration/version/v3.30.0.ts: -------------------------------------------------------------------------------- 1 | // patch for version lower than v3.30.0 2 | import type { Db } from 'mongodb' 3 | 4 | export default (async function v3330(db: Db) { 5 | await db.collection('users').updateMany({}, { $unset: { authCode: 1 } }) 6 | }) 7 | -------------------------------------------------------------------------------- /apps/core/src/migration/version/v3.36.0.ts: -------------------------------------------------------------------------------- 1 | // patch for version lower than v3.36.0 2 | import type { Db } from 'mongodb' 3 | 4 | export default (async function v3360(db: Db) { 5 | await db.collection('snippets').updateMany( 6 | { 7 | type: 'function', 8 | method: undefined, 9 | enable: undefined, 10 | }, 11 | { 12 | $set: { 13 | method: 'GET', 14 | enable: true, 15 | }, 16 | }, 17 | ) 18 | }) 19 | -------------------------------------------------------------------------------- /apps/core/src/migration/version/v3.39.3.ts: -------------------------------------------------------------------------------- 1 | // patch for version lower than v3.39.0 2 | import type { Db } from 'mongodb' 3 | 4 | export default (async function v3390(db: Db) { 5 | await db.collection('recentlies').updateMany( 6 | { 7 | up: { $exists: false }, 8 | }, 9 | { 10 | $set: { 11 | up: 0, 12 | down: 0, 13 | commentsIndex: 0, 14 | allowComment: true, 15 | }, 16 | }, 17 | ) 18 | }) 19 | -------------------------------------------------------------------------------- /apps/core/src/migration/version/v5.0.0-1.ts: -------------------------------------------------------------------------------- 1 | import { NOTE_COLLECTION_NAME } from '~/constants/db.constant' 2 | 3 | import { defineMigration } from '../helper' 4 | 5 | export default defineMigration('v5.0.0-1', async (db) => { 6 | try { 7 | await Promise.all([ 8 | db.collection(NOTE_COLLECTION_NAME).updateMany( 9 | { 10 | secret: { $exists: true }, 11 | }, 12 | { $rename: { secret: 'publicAt' } }, 13 | ), 14 | db.collection(NOTE_COLLECTION_NAME).updateMany( 15 | { 16 | hasMemory: { $exists: true }, 17 | }, 18 | { $rename: { hasMemory: 'bookmark' } }, 19 | ), 20 | ]) 21 | 22 | await db.collection(NOTE_COLLECTION_NAME).updateMany( 23 | { 24 | bookmark: { $exists: false }, 25 | }, 26 | { 27 | $set: { 28 | bookmark: false, 29 | }, 30 | }, 31 | ) 32 | } catch (error) { 33 | console.error('v5.0.0-1 migration failed') 34 | 35 | throw error 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /apps/core/src/migration/version/v5.1.1.ts: -------------------------------------------------------------------------------- 1 | import { 2 | COMMENT_COLLECTION_NAME, 3 | RECENTLY_COLLECTION_NAME, 4 | } from '~/constants/db.constant' 5 | 6 | import { defineMigration } from '../helper' 7 | 8 | export default defineMigration('v5.1.1', async (db) => { 9 | await db 10 | .collection(COMMENT_COLLECTION_NAME) 11 | .updateMany( 12 | { refType: 'Recently' }, 13 | { $set: { refType: RECENTLY_COLLECTION_NAME } }, 14 | ) 15 | }) 16 | -------------------------------------------------------------------------------- /apps/core/src/migration/version/v5.6.0.ts: -------------------------------------------------------------------------------- 1 | // patch for version lower than v2.0.0-alpha.1 2 | import type { Db } from 'mongodb' 3 | 4 | export default (async function v0560lpha1(db: Db) { 5 | const backupOptions = await db.collection('options').findOne({ 6 | name: 'backupOptions', 7 | }) 8 | 9 | if (!backupOptions) { 10 | return 11 | } 12 | 13 | if (!backupOptions.value) { 14 | return 15 | } 16 | 17 | if (backupOptions.value.endpoint) { 18 | return 19 | } 20 | 21 | const region = backupOptions.value.region 22 | backupOptions.value.endpoint = `https://cos.${region}.myqcloud.com` 23 | backupOptions.value.region = 'auto' 24 | await db 25 | .collection('options') 26 | .updateOne( 27 | { name: 'backupOptions' }, 28 | { $set: { value: backupOptions.value } }, 29 | ) 30 | }) 31 | -------------------------------------------------------------------------------- /apps/core/src/migration/version/v7.2.1.ts: -------------------------------------------------------------------------------- 1 | // patch for version lower than v7.2.1 2 | import type { Db } from 'mongodb' 3 | 4 | export default (async function v0721(db: Db) { 5 | try { 6 | await db.collection('session').drop() 7 | } catch {} 8 | }) 9 | -------------------------------------------------------------------------------- /apps/core/src/modules/ack/ack.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum, IsMongoId, IsObject } from 'class-validator' 2 | 3 | import { ArticleTypeEnum } from '~/constants/article.constant' 4 | 5 | export enum AckEventType { 6 | READ = 'read', 7 | } 8 | 9 | export class AckDto { 10 | @IsEnum(AckEventType) 11 | type: AckEventType 12 | 13 | @IsObject() 14 | payload: any 15 | } 16 | 17 | export class AckReadPayloadDto { 18 | @IsEnum(ArticleTypeEnum) 19 | type: ArticleTypeEnum 20 | 21 | @IsMongoId() 22 | id: string 23 | } 24 | -------------------------------------------------------------------------------- /apps/core/src/modules/ack/ack.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { ExtendedValidationPipe } from '~/common/pipes/validation.pipe' 4 | import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant' 5 | 6 | import { AckController } from './ack.controller' 7 | 8 | @Module({ 9 | controllers: [AckController], 10 | 11 | providers: [ 12 | { 13 | provide: VALIDATION_PIPE_INJECTION, 14 | useValue: ExtendedValidationPipe.shared, 15 | }, 16 | ], 17 | }) 18 | export class AckModule {} 19 | -------------------------------------------------------------------------------- /apps/core/src/modules/activity/activity.constant.ts: -------------------------------------------------------------------------------- 1 | export enum Activity { 2 | Like, 3 | ReadDuration, 4 | } 5 | -------------------------------------------------------------------------------- /apps/core/src/modules/activity/activity.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ActivityLikePayload { 2 | id: string 3 | ip: string 4 | type: ActivityLikeSupportType 5 | readerId?: string 6 | } 7 | 8 | export type ActivityLikeSupportType = 'post' | 'note' 9 | 10 | export interface ActivityPresence { 11 | operationTime: number 12 | updatedAt: number 13 | connectedAt: number 14 | identity: string 15 | roomName: string 16 | position: number 17 | sid: string 18 | displayName?: string 19 | 20 | ip?: string 21 | readerId?: string 22 | } 23 | -------------------------------------------------------------------------------- /apps/core/src/modules/activity/activity.model.ts: -------------------------------------------------------------------------------- 1 | import { modelOptions, prop } from '@typegoose/typegoose' 2 | 3 | import { BaseModel } from '~/shared/model/base.model' 4 | 5 | import { Activity } from './activity.constant' 6 | 7 | @modelOptions({ 8 | options: { 9 | customName: 'activities', 10 | }, 11 | schemaOptions: { 12 | timestamps: { 13 | updatedAt: false, 14 | createdAt: 'created', 15 | }, 16 | }, 17 | }) 18 | export class ActivityModel extends BaseModel { 19 | @prop() 20 | type: Activity 21 | 22 | @prop({ 23 | get(val) { 24 | return JSON.safeParse(val) 25 | }, 26 | set(val) { 27 | return JSON.stringify(val) 28 | }, 29 | type: String, 30 | }) 31 | payload: any 32 | } 33 | -------------------------------------------------------------------------------- /apps/core/src/modules/activity/activity.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common' 2 | 3 | import { GatewayModule } from '~/processors/gateway/gateway.module' 4 | 5 | import { CommentModule } from '../comment/comment.module' 6 | import { NoteModule } from '../note/note.module' 7 | import { PostModule } from '../post/post.module' 8 | import { ReaderModule } from '../reader/reader.module' 9 | import { ActivityController } from './activity.controller' 10 | import { ActivityService } from './activity.service' 11 | 12 | @Module({ 13 | providers: [ActivityService], 14 | controllers: [ActivityController], 15 | exports: [ActivityService], 16 | imports: [ 17 | GatewayModule, 18 | CommentModule, 19 | 20 | forwardRef(() => PostModule), 21 | forwardRef(() => NoteModule), 22 | ReaderModule, 23 | ], 24 | }) 25 | export class ActivityModule {} 26 | -------------------------------------------------------------------------------- /apps/core/src/modules/activity/activity.util.ts: -------------------------------------------------------------------------------- 1 | const prefix = 'article-' 2 | export const isValidRoomName = (roomName: string) => { 3 | return roomName.startsWith(prefix) 4 | } 5 | 6 | export const getArticleIdFromRoomName = (roomName: string) => 7 | roomName.slice(prefix.length) 8 | 9 | export const extractArticleIdFromRoomName = (roomName: string) => { 10 | return roomName.slice(prefix.length) 11 | } 12 | 13 | export const parseRoomName = (roomName: string) => { 14 | const prefix = roomName.split('-')[0] 15 | switch (prefix) { 16 | case 'article': 17 | return { 18 | type: 'article', 19 | refId: extractArticleIdFromRoomName(roomName), 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/core/src/modules/activity/dtos/activity.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform, Type } from 'class-transformer' 2 | import { IsEnum, IsInt, IsNumber, IsOptional } from 'class-validator' 3 | 4 | import { PagerDto } from '~/shared/dto/pager.dto' 5 | 6 | import { Activity } from '../activity.constant' 7 | 8 | const TransformEnum = () => 9 | Transform(({ value }) => (typeof value === 'undefined' ? value : +value)) 10 | 11 | export class ActivityTypeParamsDto { 12 | @IsEnum(Activity) 13 | @TransformEnum() 14 | type: Activity 15 | } 16 | 17 | export class ActivityDeleteDto { 18 | @IsNumber() 19 | @IsOptional() 20 | before?: number 21 | } 22 | 23 | export class ActivityQueryDto extends PagerDto { 24 | @IsEnum(Activity) 25 | @IsOptional() 26 | @TransformEnum() 27 | type: Activity 28 | } 29 | 30 | export class ActivityRangeDto { 31 | @IsInt() 32 | @IsOptional() 33 | @Type(() => Number) 34 | start: number 35 | 36 | @IsInt() 37 | @IsOptional() 38 | @Type(() => Number) 39 | end: number 40 | } 41 | 42 | export class ActivityNotificationDto { 43 | @IsInt() 44 | @Type(() => Number) 45 | from: number 46 | } 47 | -------------------------------------------------------------------------------- /apps/core/src/modules/activity/dtos/like.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum } from 'class-validator' 2 | 3 | import { MongoIdDto } from '~/shared/dto/id.dto' 4 | 5 | import { ActivityLikeSupportType } from '../activity.interface' 6 | 7 | export class LikeBodyDto extends MongoIdDto { 8 | @IsEnum(['Post', 'Note', 'note', 'post']) 9 | type: ActivityLikeSupportType 10 | } 11 | -------------------------------------------------------------------------------- /apps/core/src/modules/activity/dtos/presence.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsMongoId, 3 | IsNumber, 4 | IsOptional, 5 | IsString, 6 | MaxLength, 7 | Min, 8 | } from 'class-validator' 9 | 10 | export class UpdatePresenceDto { 11 | @IsString() 12 | @MaxLength(200) 13 | identity: string 14 | 15 | @IsString() 16 | @MaxLength(50) 17 | roomName: string 18 | 19 | @IsNumber() 20 | ts: number 21 | 22 | @IsNumber() 23 | @Min(0) 24 | position: number 25 | 26 | @IsOptional() 27 | @IsString() 28 | @MaxLength(50) 29 | displayName?: string 30 | 31 | @IsString() 32 | @MaxLength(30) 33 | sid: string 34 | 35 | @IsMongoId() 36 | @IsOptional() 37 | readerId?: string 38 | } 39 | 40 | export class GetPresenceQueryDto { 41 | @IsString() 42 | @MaxLength(50) 43 | room_name: string 44 | } 45 | -------------------------------------------------------------------------------- /apps/core/src/modules/aggregate/aggregate.interface.ts: -------------------------------------------------------------------------------- 1 | import type { ImageModel } from '~/shared/model/image.model' 2 | 3 | export interface RSSProps { 4 | title: string 5 | url: string 6 | author: string 7 | description: string 8 | data: { 9 | created: Date | null 10 | modified: Date | null 11 | link: string 12 | title: string 13 | text: string 14 | id: string 15 | images: ImageModel[] 16 | }[] 17 | } 18 | -------------------------------------------------------------------------------- /apps/core/src/modules/ai/ai-deep-reading/ai-deep-reading.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsOptional, IsString } from 'class-validator' 2 | 3 | import { TransformBoolean } from '~/common/decorators/transform-boolean.decorator' 4 | 5 | class BaseLangQueryDto { 6 | @IsString() 7 | @IsOptional() 8 | lang: string 9 | } 10 | 11 | export class GenerateAiDeepReadingDto extends BaseLangQueryDto { 12 | @IsString() 13 | refId: string 14 | } 15 | 16 | export class GetDeepReadingQueryDto extends BaseLangQueryDto { 17 | @IsOptional() 18 | @IsBoolean() 19 | @TransformBoolean() 20 | onlyDb?: boolean 21 | } 22 | 23 | export class UpdateDeepReadingDto { 24 | @IsString() 25 | deepReading: string 26 | 27 | @IsString() 28 | @IsOptional() 29 | criticalAnalysis?: string 30 | 31 | @IsString({ each: true }) 32 | @IsOptional() 33 | keyPoints?: string[] 34 | 35 | @IsString() 36 | @IsOptional() 37 | content?: string 38 | } 39 | -------------------------------------------------------------------------------- /apps/core/src/modules/ai/ai-deep-reading/ai-deep-reading.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | import { modelOptions, prop } from '@typegoose/typegoose' 4 | 5 | import { AI_DEEP_READING_COLLECTION_NAME } from '~/constants/db.constant' 6 | import { BaseModel } from '~/shared/model/base.model' 7 | 8 | @modelOptions({ 9 | options: { 10 | customName: AI_DEEP_READING_COLLECTION_NAME, 11 | }, 12 | }) 13 | export class AIDeepReadingModel extends BaseModel { 14 | @prop({ 15 | required: true, 16 | }) 17 | hash: string 18 | 19 | @prop({ 20 | required: true, 21 | }) 22 | refId: string 23 | 24 | @prop({ type: [String] }) 25 | keyPoints?: mongoose.Types.Array 26 | 27 | @prop() 28 | criticalAnalysis?: string 29 | 30 | @prop() 31 | content?: string 32 | } 33 | -------------------------------------------------------------------------------- /apps/core/src/modules/ai/ai-summary/ai-summary.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsOptional, IsString } from 'class-validator' 2 | 3 | import { TransformBoolean } from '~/common/decorators/transform-boolean.decorator' 4 | 5 | class BaseLangQueryDto { 6 | @IsString() 7 | @IsOptional() 8 | lang: string 9 | } 10 | 11 | export class GenerateAiSummaryDto extends BaseLangQueryDto { 12 | @IsString() 13 | refId: string 14 | } 15 | 16 | export class GetSummaryQueryDto extends BaseLangQueryDto { 17 | @IsOptional() 18 | @IsBoolean() 19 | @TransformBoolean() 20 | onlyDb?: boolean 21 | } 22 | 23 | export class UpdateSummaryDto { 24 | @IsString() 25 | summary: string 26 | } 27 | -------------------------------------------------------------------------------- /apps/core/src/modules/ai/ai-summary/ai-summary.model.ts: -------------------------------------------------------------------------------- 1 | import { modelOptions, prop } from '@typegoose/typegoose' 2 | 3 | import { AI_SUMMARY_COLLECTION_NAME } from '~/constants/db.constant' 4 | import { BaseModel } from '~/shared/model/base.model' 5 | 6 | @modelOptions({ 7 | options: { 8 | customName: AI_SUMMARY_COLLECTION_NAME, 9 | }, 10 | }) 11 | export class AISummaryModel extends BaseModel { 12 | @prop({ 13 | required: true, 14 | }) 15 | hash: string 16 | 17 | @prop({ 18 | required: true, 19 | }) 20 | summary: string 21 | 22 | @prop({ 23 | required: true, 24 | }) 25 | refId: string 26 | 27 | @prop() 28 | lang?: string 29 | } 30 | -------------------------------------------------------------------------------- /apps/core/src/modules/ai/ai-writer/ai-writer.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Post } from '@nestjs/common' 2 | 3 | import { ApiController } from '~/common/decorators/api-controller.decorator' 4 | import { Auth } from '~/common/decorators/auth.decorator' 5 | 6 | import { AiQueryType, GenerateAiDto } from './ai-writer.dto' 7 | import { AiWriterService } from './ai-writer.service' 8 | 9 | @ApiController('ai/writer') 10 | export class AiWriterController { 11 | constructor(private readonly aiWriterService: AiWriterService) {} 12 | 13 | @Post('generate') 14 | @Auth() 15 | async generate(@Body() body: GenerateAiDto) { 16 | switch (body.type) { 17 | case AiQueryType.TitleSlug: 18 | return this.aiWriterService.generateTitleAndSlugByOpenAI(body.text) 19 | case AiQueryType.Title: 20 | case AiQueryType.Slug: 21 | return this.aiWriterService.generateSlugByTitleViaOpenAI(body.title) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/core/src/modules/ai/ai-writer/ai-writer.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum, IsString, ValidateIf } from 'class-validator' 2 | 3 | export enum AiQueryType { 4 | TitleSlug = 'title-slug', 5 | Title = 'title', 6 | Slug = 'slug', 7 | } 8 | 9 | export class GenerateAiDto { 10 | @IsEnum(AiQueryType) 11 | type: AiQueryType 12 | 13 | @ValidateIf((o: GenerateAiDto) => o.type === AiQueryType.TitleSlug) 14 | @IsString() 15 | text: string 16 | @ValidateIf((o: GenerateAiDto) => o.type === AiQueryType.Title) 17 | @IsString() 18 | title: string 19 | } 20 | -------------------------------------------------------------------------------- /apps/core/src/modules/ai/ai.constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SUMMARY_LANG = 'zh' 2 | 3 | export const LANGUAGE_CODE_TO_NAME = { 4 | ar: 'Arabic', 5 | bg: 'Bulgarian', 6 | cs: 'Czech', 7 | da: 'Danish', 8 | de: 'German', 9 | el: 'Greek', 10 | en: 'English', 11 | es: 'Spanish', 12 | et: 'Estonian', 13 | fa: 'Persian', 14 | fi: 'Finnish', 15 | fr: 'French', 16 | he: 'Hebrew', 17 | hi: 'Hindi', 18 | hr: 'Croatian', 19 | hu: 'Hungarian', 20 | id: 'Indonesian', 21 | is: 'Icelandic', 22 | it: 'Italian', 23 | ja: 'Japanese', 24 | ko: 'Korean', 25 | lt: 'Lithuanian', 26 | lv: 'Latvian', 27 | ms: 'Malay', 28 | nl: 'Dutch', 29 | no: 'Norwegian', 30 | pl: 'Polish', 31 | pt: 'Portuguese', 32 | ro: 'Romanian', 33 | ru: 'Russian', 34 | sk: 'Slovak', 35 | sl: 'Slovenian', 36 | sr: 'Serbian', 37 | sv: 'Swedish', 38 | sw: 'Swahili', 39 | th: 'Thai', 40 | tl: 'Tagalog', 41 | tr: 'Turkish', 42 | uk: 'Ukrainian', 43 | ur: 'Urdu', 44 | vi: 'Vietnamese', 45 | zh: 'Chinese', 46 | } 47 | -------------------------------------------------------------------------------- /apps/core/src/modules/ai/ai.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common' 2 | 3 | import { McpModule } from '../mcp/mcp.module' 4 | import { AIAgentService } from './ai-agent/ai-agent.service' 5 | import { AiDeepReadingController } from './ai-deep-reading/ai-deep-reading.controller' 6 | import { AiDeepReadingService } from './ai-deep-reading/ai-deep-reading.service' 7 | import { AiSummaryController } from './ai-summary/ai-summary.controller' 8 | import { AiSummaryService } from './ai-summary/ai-summary.service' 9 | import { AiWriterController } from './ai-writer/ai-writer.controller' 10 | import { AiWriterService } from './ai-writer/ai-writer.service' 11 | import { AiService } from './ai.service' 12 | 13 | @Module({ 14 | imports: [forwardRef(() => McpModule)], 15 | providers: [ 16 | AiSummaryService, 17 | AiService, 18 | AiWriterService, 19 | AiDeepReadingService, 20 | AIAgentService, 21 | ], 22 | controllers: [ 23 | AiSummaryController, 24 | AiWriterController, 25 | AiDeepReadingController, 26 | ], 27 | exports: [AiService], 28 | }) 29 | export class AiModule {} 30 | -------------------------------------------------------------------------------- /apps/core/src/modules/analyze/analyze.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer' 2 | import { IsDate, IsOptional } from 'class-validator' 3 | 4 | export class AnalyzeDto { 5 | @Transform(({ value: v }) => new Date(Number.parseInt(v))) 6 | @IsOptional() 7 | @IsDate() 8 | from?: Date 9 | 10 | @Transform(({ value: v }) => new Date(Number.parseInt(v))) 11 | @IsOptional() 12 | @IsDate() 13 | to?: Date 14 | } 15 | -------------------------------------------------------------------------------- /apps/core/src/modules/analyze/analyze.model.ts: -------------------------------------------------------------------------------- 1 | import { SchemaTypes } from 'mongoose' 2 | import { UAParser } from 'ua-parser-js' 3 | 4 | import { index, modelOptions, prop, Severity } from '@typegoose/typegoose' 5 | 6 | import { ANALYZE_COLLECTION_NAME } from '~/constants/db.constant' 7 | import { BaseModel } from '~/shared/model/base.model' 8 | 9 | @modelOptions({ 10 | schemaOptions: { 11 | timestamps: { 12 | createdAt: 'timestamp', 13 | updatedAt: false, 14 | }, 15 | }, 16 | options: { 17 | customName: ANALYZE_COLLECTION_NAME, 18 | allowMixed: Severity.ALLOW, 19 | }, 20 | }) 21 | @index({ timestamp: -1 }) 22 | export class AnalyzeModel extends BaseModel { 23 | @prop() 24 | ip?: string 25 | 26 | @prop({ type: SchemaTypes.Mixed }) 27 | ua: UAParser 28 | 29 | @prop() 30 | country?: string 31 | 32 | @prop() 33 | path?: string 34 | 35 | timestamp: Date 36 | } 37 | -------------------------------------------------------------------------------- /apps/core/src/modules/analyze/analyze.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { AnalyzeController } from './analyze.controller' 4 | import { AnalyzeService } from './analyze.service' 5 | 6 | @Module({ 7 | controllers: [AnalyzeController], 8 | exports: [AnalyzeService], 9 | providers: [AnalyzeService], 10 | }) 11 | export class AnalyzeModule {} 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/auth/auth.constant.ts: -------------------------------------------------------------------------------- 1 | export const AUTH_JS_USER_COLLECTION = 'readers' 2 | export const AUTH_JS_ACCOUNT_COLLECTION = 'accounts' 3 | export const AUTH_JS_SESSION_COLLECTION = 'sessions' 4 | 5 | export const AuthInstanceInjectKey = Symbol('AuthInstance') 6 | -------------------------------------------------------------------------------- /apps/core/src/modules/auth/auth.interface.ts: -------------------------------------------------------------------------------- 1 | import type { CreateAuth } from './auth.implement' 2 | 3 | export type AuthInstance = Awaited>['auth'] 4 | export type InjectAuthInstance = { 5 | get: () => AuthInstance 6 | set: (value: AuthInstance) => void 7 | } 8 | -------------------------------------------------------------------------------- /apps/core/src/modules/authn/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common' 2 | 3 | import { AuthnController } from '../authn/authn.controller' 4 | import { AuthnService } from './authn.service' 5 | 6 | @Module({ 7 | providers: [AuthnService], 8 | controllers: [AuthnController], 9 | exports: [AuthnService], 10 | }) 11 | @Global() 12 | export class AuthnModule {} 13 | -------------------------------------------------------------------------------- /apps/core/src/modules/backup/backup.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { BackupController } from './backup.controller' 4 | import { BackupService } from './backup.service' 5 | 6 | @Module({ 7 | controllers: [BackupController], 8 | providers: [BackupService], 9 | exports: [BackupService], 10 | }) 11 | export class BackupModule {} 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/category/category.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common' 2 | 3 | import { PostModule } from '../post/post.module' 4 | import { SlugTrackerModule } from '../slug-tracker/slug-tracker.module' 5 | import { CategoryController } from './category.controller' 6 | import { CategoryService } from './category.service' 7 | 8 | @Module({ 9 | providers: [CategoryService], 10 | exports: [CategoryService], 11 | controllers: [CategoryController], 12 | imports: [forwardRef(() => PostModule), SlugTrackerModule], 13 | }) 14 | export class CategoryModule {} 15 | -------------------------------------------------------------------------------- /apps/core/src/modules/comment/comment.enum.ts: -------------------------------------------------------------------------------- 1 | export enum CommentReplyMailType { 2 | Owner = 'owner', 3 | Guest = 'guest', 4 | } 5 | -------------------------------------------------------------------------------- /apps/core/src/modules/comment/comment.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common' 2 | 3 | import { GatewayModule } from '~/processors/gateway/gateway.module' 4 | 5 | import { AiModule } from '../ai/ai.module' 6 | import { ReaderModule } from '../reader/reader.module' 7 | import { ServerlessModule } from '../serverless/serverless.module' 8 | import { UserModule } from '../user/user.module' 9 | import { CommentController } from './comment.controller' 10 | import { CommentService } from './comment.service' 11 | 12 | @Module({ 13 | controllers: [CommentController], 14 | providers: [CommentService], 15 | exports: [CommentService], 16 | imports: [ 17 | UserModule, 18 | GatewayModule, 19 | forwardRef(() => ServerlessModule), 20 | forwardRef(() => ReaderModule), 21 | forwardRef(() => AiModule), 22 | ], 23 | }) 24 | export class CommentModule {} 25 | -------------------------------------------------------------------------------- /apps/core/src/modules/configs/configs.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from 'mongoose' 2 | 3 | import { modelOptions, prop, Severity } from '@typegoose/typegoose' 4 | 5 | @modelOptions({ 6 | options: { allowMixed: Severity.ALLOW, customName: 'Option' }, 7 | schemaOptions: { 8 | timestamps: { 9 | createdAt: false, 10 | updatedAt: false, 11 | }, 12 | }, 13 | }) 14 | export class OptionModel { 15 | @prop({ unique: true, required: true }) 16 | name: string 17 | 18 | @prop({ type: Schema.Types.Mixed }) 19 | value: any 20 | } 21 | -------------------------------------------------------------------------------- /apps/core/src/modules/configs/configs.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common' 2 | 3 | import { ExtendedValidationPipe } from '~/common/pipes/validation.pipe' 4 | import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant' 5 | 6 | import { UserModule } from '../user/user.module' 7 | import { ConfigsService } from './configs.service' 8 | 9 | @Global() 10 | @Module({ 11 | providers: [ 12 | ConfigsService, 13 | { 14 | provide: VALIDATION_PIPE_INJECTION, 15 | useValue: ExtendedValidationPipe.shared, 16 | }, 17 | ], 18 | imports: [UserModule], 19 | exports: [ConfigsService], 20 | }) 21 | export class ConfigsModule {} 22 | -------------------------------------------------------------------------------- /apps/core/src/modules/debug/debug.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { ServerlessModule } from '../serverless/serverless.module' 4 | import { DebugController } from './debug.controller' 5 | import { DebugService } from './debug.service' 6 | 7 | @Module({ 8 | controllers: [DebugController], 9 | imports: [ServerlessModule], 10 | providers: [DebugService], 11 | }) 12 | export class DebugModule {} 13 | -------------------------------------------------------------------------------- /apps/core/src/modules/debug/debug.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Scope } from '@nestjs/common' 2 | import { REQUEST } from '@nestjs/core' 3 | 4 | @Injectable({ scope: Scope.REQUEST }) 5 | export class DebugService { 6 | constructor(@Inject(REQUEST) private req) { 7 | console.log('DebugService created') 8 | } 9 | 10 | test() { 11 | console.log('this.req', this.req.method) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/core/src/modules/demo/demo.module.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | 3 | import { Module } from '@nestjs/common' 4 | import { CronExpression } from '@nestjs/schedule' 5 | 6 | import { CronOnce } from '~/common/decorators/cron-once.decorator' 7 | import { AssetService } from '~/processors/helper/helper.asset.service' 8 | 9 | import { BackupModule } from '../backup/backup.module' 10 | import { BackupService } from '../backup/backup.service' 11 | 12 | @Module({ 13 | imports: [BackupModule], 14 | }) 15 | export class DemoModule { 16 | constructor( 17 | private readonly backupService: BackupService, 18 | private readonly assetService: AssetService, 19 | ) { 20 | this.reset() 21 | } 22 | 23 | @CronOnce(CronExpression.EVERY_DAY_AT_1AM) 24 | reset() { 25 | this.backupService.restore( 26 | resolve(this.assetService.embedAssetPath, 'demo-data.zip'), 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/core/src/modules/dependency/dependency.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { ServerlessModule } from '../serverless/serverless.module' 4 | import { DependencyController } from './dependency.controller' 5 | 6 | @Module({ 7 | controllers: [DependencyController], 8 | providers: [], 9 | imports: [ServerlessModule], 10 | }) 11 | export class DependencyModule {} 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/feed/feed.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { AggregateModule } from '../aggregate/aggregate.module' 4 | import { MarkdownModule } from '../markdown/markdown.module' 5 | import { FeedController } from './feed.controller' 6 | 7 | @Module({ 8 | controllers: [FeedController], 9 | providers: [], 10 | imports: [AggregateModule, MarkdownModule], 11 | }) 12 | export class FeedModule {} 13 | -------------------------------------------------------------------------------- /apps/core/src/modules/file/file.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum, IsOptional, IsString } from 'class-validator' 2 | 3 | import { FileType, FileTypeEnum } from './file.type' 4 | 5 | export class FileQueryDto { 6 | @IsEnum(FileTypeEnum) 7 | type: FileType 8 | @IsString() 9 | name: string 10 | } 11 | 12 | export class FileUploadDto { 13 | @IsEnum(FileTypeEnum) 14 | @IsOptional() 15 | type?: FileType 16 | } 17 | 18 | export class RenameFileQueryDto { 19 | @IsString() 20 | new_name: string 21 | } 22 | -------------------------------------------------------------------------------- /apps/core/src/modules/file/file.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { FileController } from './file.controller' 4 | import { FileService } from './file.service' 5 | 6 | @Module({ 7 | controllers: [FileController], 8 | providers: [FileService], 9 | exports: [FileService], 10 | }) 11 | export class FileModule {} 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/file/file.type.ts: -------------------------------------------------------------------------------- 1 | export enum FileTypeEnum { 2 | icon = 'icon', 3 | photo = 'photo', 4 | file = 'file', 5 | avatar = 'avatar', 6 | } 7 | export type FileType = keyof typeof FileTypeEnum 8 | -------------------------------------------------------------------------------- /apps/core/src/modules/health/health.controller.ts: -------------------------------------------------------------------------------- 1 | import { Get } from '@nestjs/common' 2 | 3 | import { ApiController } from '~/common/decorators/api-controller.decorator' 4 | import { Auth } from '~/common/decorators/auth.decorator' 5 | import { HttpCache } from '~/common/decorators/cache.decorator' 6 | import { HTTPDecorators } from '~/common/decorators/http.decorator' 7 | import { EmailService } from '~/processors/helper/helper.email.service' 8 | 9 | @ApiController('health') 10 | export class HealthController { 11 | constructor(private readonly emailService: EmailService) {} 12 | 13 | @Get('/') 14 | @HTTPDecorators.Bypass 15 | @HttpCache({ 16 | disable: true, 17 | }) 18 | async check() { 19 | // TODO 20 | return 'OK' 21 | } 22 | 23 | @Get('/email/test') 24 | @Auth() 25 | async testEmail() { 26 | return this.emailService.sendTestEmail().catch((error) => { 27 | return { 28 | message: error.message, 29 | trace: error.stack, 30 | } 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/core/src/modules/health/health.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer' 2 | import { IsIn, IsInt, IsString, Min, ValidateIf } from 'class-validator' 3 | 4 | export class LogQueryDto { 5 | @IsIn(['out', 'error']) 6 | @ValidateIf((o: LogQueryDto) => typeof o.filename === 'undefined') 7 | type?: 'out' | 'error' 8 | @IsInt() 9 | @Min(0) 10 | @Transform(({ value }) => +value) 11 | @ValidateIf((o: LogQueryDto) => typeof o.filename === 'undefined') 12 | index: number 13 | 14 | @IsString() 15 | filename: string 16 | } 17 | 18 | export class LogTypeDto { 19 | @IsIn(['pm2', 'native']) 20 | type: 'pm2' | 'native' 21 | } 22 | -------------------------------------------------------------------------------- /apps/core/src/modules/health/health.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { HealthController } from './health.controller' 4 | import { HealthCronController } from './sub-controller/cron.controller' 5 | import { HealthLogController } from './sub-controller/log.controller' 6 | 7 | @Module({ 8 | controllers: [HealthController, HealthCronController, HealthLogController], 9 | }) 10 | export class HealthModule {} 11 | -------------------------------------------------------------------------------- /apps/core/src/modules/helper/helper.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { HelperController } from './helper.controller' 4 | import { HelperService } from './helper.service' 5 | 6 | @Module({ 7 | controllers: [HelperController], 8 | providers: [HelperService], 9 | }) 10 | export class HelperModule {} 11 | -------------------------------------------------------------------------------- /apps/core/src/modules/helper/helper.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | @Injectable() 4 | export class HelperService {} 5 | -------------------------------------------------------------------------------- /apps/core/src/modules/init/init.guard.ts: -------------------------------------------------------------------------------- 1 | import type { CanActivate } from '@nestjs/common' 2 | 3 | import { checkInit } from '~/utils/check-init.util' 4 | 5 | export class InitGuard implements CanActivate { 6 | async canActivate() { 7 | return !(await checkInit()) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/core/src/modules/init/init.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { BackupModule } from '../backup/backup.module' 4 | import { OptionModule } from '../option/option.module' 5 | import { UserModule } from '../user/user.module' 6 | import { InitController } from './init.controller' 7 | import { InitService } from './init.service' 8 | 9 | @Module({ 10 | providers: [InitService], 11 | exports: [InitService], 12 | controllers: [InitController], 13 | imports: [UserModule, OptionModule, BackupModule], 14 | }) 15 | export class InitModule {} 16 | -------------------------------------------------------------------------------- /apps/core/src/modules/init/init.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | import { DATA_DIR, TEMP_DIR } from '~/constants/path.constant' 4 | 5 | import { UserService } from '../user/user.service' 6 | 7 | @Injectable() 8 | export class InitService { 9 | constructor(private readonly userService: UserService) {} 10 | 11 | getTempdir() { 12 | return TEMP_DIR 13 | } 14 | 15 | getDatadir() { 16 | return DATA_DIR 17 | } 18 | 19 | isInit(): Promise { 20 | return this.userService.hasMaster() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/core/src/modules/link/link-mail.enum.ts: -------------------------------------------------------------------------------- 1 | export enum LinkApplyEmailType { 2 | ToMaster, 3 | ToCandidate, 4 | } 5 | -------------------------------------------------------------------------------- /apps/core/src/modules/link/link.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum, IsString, MaxLength } from 'class-validator' 2 | 3 | import { LinkModel, LinkState } from './link.model' 4 | 5 | export class LinkDto extends LinkModel { 6 | @IsString({ message: '输入你的大名吧' }) 7 | @MaxLength(20, { message: '乃的名字太长了' }) 8 | author: string 9 | } 10 | 11 | export class AuditReasonDto { 12 | @IsString({ message: '请输入审核理由' }) 13 | reason: string 14 | 15 | @IsEnum(LinkState) 16 | state: LinkState 17 | } 18 | -------------------------------------------------------------------------------- /apps/core/src/modules/link/link.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { GatewayModule } from '~/processors/gateway/gateway.module' 4 | 5 | import { LinkController, LinkControllerCrud } from './link.controller' 6 | import { LinkService } from './link.service' 7 | 8 | @Module({ 9 | controllers: [LinkController, LinkControllerCrud], 10 | providers: [LinkService], 11 | exports: [LinkService], 12 | imports: [GatewayModule], 13 | }) 14 | export class LinkModule {} 15 | -------------------------------------------------------------------------------- /apps/core/src/modules/markdown/markdown.interface.ts: -------------------------------------------------------------------------------- 1 | export type MetaType = { 2 | created?: Date | null | undefined 3 | modified?: Date | null | undefined 4 | title: string 5 | slug: string 6 | } & Record 7 | 8 | export interface MarkdownYAMLProperty { 9 | meta: MetaType 10 | text: string 11 | } 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/markdown/markdown.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { MarkdownController } from './markdown.controller' 4 | import { MarkdownService } from './markdown.service' 5 | 6 | @Module({ 7 | controllers: [MarkdownController], 8 | providers: [MarkdownService], 9 | exports: [MarkdownService], 10 | }) 11 | export class MarkdownModule {} 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/mcp/mcp.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common' 2 | 3 | import { CategoryModule } from '../category/category.module' 4 | import { CommentModule } from '../comment/comment.module' 5 | import { NoteModule } from '../note/note.module' 6 | import { PageModule } from '../page/page.module' 7 | import { PostModule } from '../post/post.module' 8 | import { RecentlyModule } from '../recently/recently.module' 9 | import { SayModule } from '../say/say.module' 10 | import { McpService } from './mcp.service' 11 | 12 | @Module({ 13 | imports: [ 14 | forwardRef(() => NoteModule), 15 | forwardRef(() => PostModule), 16 | forwardRef(() => CategoryModule), 17 | forwardRef(() => PageModule), 18 | forwardRef(() => SayModule), 19 | forwardRef(() => RecentlyModule), 20 | forwardRef(() => CommentModule), 21 | ], 22 | 23 | providers: [McpService], 24 | exports: [McpService], 25 | }) 26 | export class McpModule {} 27 | -------------------------------------------------------------------------------- /apps/core/src/modules/note/models/coordinate.model.ts: -------------------------------------------------------------------------------- 1 | import { IsNumber } from 'class-validator' 2 | 3 | import { modelOptions, prop } from '@typegoose/typegoose' 4 | 5 | @modelOptions({ schemaOptions: { id: false, _id: false } }) 6 | export class Coordinate { 7 | @IsNumber() 8 | @prop() 9 | latitude: number 10 | @prop() 11 | @IsNumber() 12 | longitude: number 13 | } 14 | -------------------------------------------------------------------------------- /apps/core/src/modules/note/note.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common' 2 | 3 | import { GatewayModule } from '~/processors/gateway/gateway.module' 4 | 5 | import { CommentModule } from '../comment/comment.module' 6 | import { TopicModule } from '../topic/topic.module' 7 | import { NoteController } from './note.controller' 8 | import { NoteService } from './note.service' 9 | 10 | @Module({ 11 | controllers: [NoteController], 12 | providers: [NoteService], 13 | exports: [NoteService], 14 | imports: [ 15 | GatewayModule, 16 | forwardRef(() => CommentModule), 17 | 18 | forwardRef(() => TopicModule), 19 | ], 20 | }) 21 | export class NoteModule {} 22 | -------------------------------------------------------------------------------- /apps/core/src/modules/note/note.type.ts: -------------------------------------------------------------------------------- 1 | import type { TopicModel } from '../topic/topic.model' 2 | import type { NoteModel } from './note.model' 3 | 4 | export type NormalizedNote = Omit & { 5 | topic: TopicModel 6 | } 7 | -------------------------------------------------------------------------------- /apps/core/src/modules/option/dtoes/config.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator' 2 | import type { IConfig } from '~/modules/configs/configs.interface' 3 | 4 | export class ConfigKeyDto { 5 | @IsString() 6 | @IsNotEmpty() 7 | key: keyof IConfig 8 | } 9 | -------------------------------------------------------------------------------- /apps/core/src/modules/option/dtoes/email.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator' 2 | 3 | export class EmailTemplateTypeDto { 4 | @IsString() 5 | type: string 6 | } 7 | 8 | export class EmailTemplateBodyDto { 9 | @IsString() 10 | source: string 11 | } 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/option/option.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators } from '@nestjs/common' 2 | 3 | import { ApiController } from '~/common/decorators/api-controller.decorator' 4 | import { Auth } from '~/common/decorators/auth.decorator' 5 | 6 | export function OptionController(name?: string, postfixRoute?: string) { 7 | const routes = ['options', 'config'] 8 | return applyDecorators( 9 | Auth(), 10 | ApiController( 11 | postfixRoute 12 | ? routes.map((route) => `/${route}/${postfixRoute}`) 13 | : routes, 14 | ), 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /apps/core/src/modules/option/option.model.ts: -------------------------------------------------------------------------------- 1 | export { OptionModel as ConfigModel } from '../configs/configs.model' 2 | -------------------------------------------------------------------------------- /apps/core/src/modules/option/option.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { GatewayModule } from '~/processors/gateway/gateway.module' 4 | 5 | import { BaseOptionController } from './controllers/base.option.controller' 6 | import { EmailOptionController } from './controllers/email.option.controller' 7 | 8 | @Module({ 9 | imports: [GatewayModule], 10 | controllers: [BaseOptionController, EmailOptionController], 11 | }) 12 | export class OptionModule {} 13 | -------------------------------------------------------------------------------- /apps/core/src/modules/page/page.dto.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer' 2 | import { IsInt, IsMongoId, Min, ValidateNested } from 'class-validator' 3 | 4 | class Seq { 5 | @IsMongoId() 6 | id: string 7 | @IsInt() 8 | @Min(1) 9 | order: number 10 | } 11 | export class PageReorderDto { 12 | @Type(() => Seq) 13 | @ValidateNested() 14 | seq: Seq[] 15 | } 16 | -------------------------------------------------------------------------------- /apps/core/src/modules/page/page.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { GatewayModule } from '~/processors/gateway/gateway.module' 4 | 5 | import { PageController } from './page.controller' 6 | import { PageService } from './page.service' 7 | 8 | @Module({ 9 | imports: [GatewayModule], 10 | controllers: [PageController], 11 | providers: [PageService], 12 | exports: [PageService], 13 | }) 14 | export class PageModule {} 15 | -------------------------------------------------------------------------------- /apps/core/src/modules/pageproxy/pageproxy.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { UpdateModule } from '../update/update.module' 4 | import { UserModule } from '../user/user.module' 5 | import { PageProxyController } from './pageproxy.controller' 6 | import { PageProxyService } from './pageproxy.service' 7 | 8 | @Module({ 9 | controllers: [PageProxyController], 10 | providers: [PageProxyService], 11 | imports: [UpdateModule, UserModule], 12 | }) 13 | export class PageProxyModule {} 14 | -------------------------------------------------------------------------------- /apps/core/src/modules/post/post.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform, Type } from 'class-transformer' 2 | import { IsInt, IsOptional, IsString } from 'class-validator' 3 | 4 | import { PagerDto } from '~/shared/dto/pager.dto' 5 | 6 | export class CategoryAndSlugDto { 7 | @IsString() 8 | readonly category: string 9 | 10 | @IsString() 11 | @Transform(({ value: v }) => decodeURI(v)) 12 | readonly slug: string 13 | } 14 | 15 | export class PostPagerDto extends PagerDto { 16 | @IsOptional() 17 | @IsInt() 18 | @Type(() => Number) 19 | truncate?: number 20 | } 21 | -------------------------------------------------------------------------------- /apps/core/src/modules/post/post.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common' 2 | 3 | import { CategoryModule } from '../category/category.module' 4 | import { SlugTrackerModule } from '../slug-tracker/slug-tracker.module' 5 | import { PostController } from './post.controller' 6 | import { PostService } from './post.service' 7 | 8 | @Module({ 9 | imports: [forwardRef(() => CategoryModule), SlugTrackerModule], 10 | controllers: [PostController], 11 | providers: [PostService], 12 | exports: [PostService], 13 | }) 14 | export class PostModule {} 15 | -------------------------------------------------------------------------------- /apps/core/src/modules/post/post.type.ts: -------------------------------------------------------------------------------- 1 | import type { CategoryModel } from '../category/category.model' 2 | import type { PostModel } from './post.model' 3 | 4 | export type NormalizedPost = Omit & { 5 | category: CategoryModel 6 | } 7 | -------------------------------------------------------------------------------- /apps/core/src/modules/project/project.controller.ts: -------------------------------------------------------------------------------- 1 | import { BaseCrudFactory } from '~/transformers/crud-factor.transformer' 2 | 3 | import { ProjectModel } from './project.model' 4 | 5 | export class ProjectController extends BaseCrudFactory({ 6 | model: ProjectModel, 7 | }) {} 8 | -------------------------------------------------------------------------------- /apps/core/src/modules/project/project.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { ProjectController } from './project.controller' 4 | 5 | @Module({ controllers: [ProjectController] }) 6 | export class ProjectModule {} 7 | -------------------------------------------------------------------------------- /apps/core/src/modules/reader/reader.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Get, Patch } from '@nestjs/common' 2 | 3 | import { ApiController } from '~/common/decorators/api-controller.decorator' 4 | import { Auth } from '~/common/decorators/auth.decorator' 5 | import { MongoIdDto } from '~/shared/dto/id.dto' 6 | 7 | import { ReaderService } from './reader.service' 8 | 9 | @ApiController('readers') 10 | @Auth() 11 | export class ReaderAuthController { 12 | constructor(private readonly readerService: ReaderService) {} 13 | @Get('/') 14 | async find() { 15 | return this.readerService.find() 16 | } 17 | 18 | @Patch('/as-owner') 19 | async updateAsOwner(@Body() body: MongoIdDto) { 20 | return this.readerService.updateAsOwner(body.id) 21 | } 22 | 23 | @Patch('/revoke-owner') 24 | async revokeOwner(@Body() body: MongoIdDto) { 25 | return this.readerService.revokeOwner(body.id) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/core/src/modules/reader/reader.model.ts: -------------------------------------------------------------------------------- 1 | import { modelOptions, prop } from '@typegoose/typegoose' 2 | 3 | import { BaseModel } from '~/shared/model/base.model' 4 | 5 | @modelOptions({ 6 | options: { 7 | customName: 'readers', 8 | }, 9 | }) 10 | export class ReaderModel extends BaseModel { 11 | @prop() 12 | email: string 13 | @prop() 14 | name: string 15 | 16 | @prop() 17 | handle: string 18 | @prop() 19 | image: string 20 | 21 | @prop() 22 | isOwner: boolean 23 | } 24 | -------------------------------------------------------------------------------- /apps/core/src/modules/reader/reader.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { ReaderAuthController } from './reader.controller' 4 | import { ReaderService } from './reader.service' 5 | 6 | @Module({ 7 | controllers: [ReaderAuthController], 8 | providers: [ReaderService], 9 | exports: [ReaderService], 10 | }) 11 | export class ReaderModule {} 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/recently/recently.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer' 2 | import { IsEnum } from 'class-validator' 3 | 4 | export enum RecentlyAttitudeEnum { 5 | Up, 6 | Down, 7 | } 8 | 9 | export class RecentlyAttitudeDto { 10 | @IsEnum(RecentlyAttitudeEnum) 11 | @Transform(({ value }) => +value) 12 | attitude: RecentlyAttitudeEnum 13 | } 14 | -------------------------------------------------------------------------------- /apps/core/src/modules/recently/recently.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common' 2 | 3 | import { CommentModule } from '../comment/comment.module' 4 | import { RecentlyController } from './recently.controller' 5 | import { RecentlyService } from './recently.service' 6 | 7 | @Module({ 8 | controllers: [RecentlyController], 9 | providers: [RecentlyService], 10 | exports: [RecentlyService], 11 | imports: [forwardRef(() => CommentModule)], 12 | }) 13 | export class RecentlyModule {} 14 | -------------------------------------------------------------------------------- /apps/core/src/modules/render/render.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { MarkdownModule } from '../markdown/markdown.module' 4 | import { RenderEjsController } from './render.controller' 5 | 6 | @Module({ 7 | controllers: [RenderEjsController], 8 | imports: [MarkdownModule], 9 | }) 10 | export class RenderEjsModule {} 11 | -------------------------------------------------------------------------------- /apps/core/src/modules/say/say.controller.ts: -------------------------------------------------------------------------------- 1 | import { sample } from 'lodash' 2 | 3 | import { Get } from '@nestjs/common' 4 | 5 | import { BaseCrudFactory } from '~/transformers/crud-factor.transformer' 6 | 7 | import { SayModel } from './say.model' 8 | 9 | export class SayController extends BaseCrudFactory({ model: SayModel }) { 10 | @Get('/random') 11 | async getRandomOne() { 12 | const res = await this.model.find({}).lean() 13 | if (res.length === 0) { 14 | return { data: null } 15 | } 16 | return { data: sample(res) } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/core/src/modules/say/say.model.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, IsString } from 'class-validator' 2 | 3 | import { modelOptions, prop } from '@typegoose/typegoose' 4 | 5 | import { BaseModel } from '~/shared/model/base.model' 6 | 7 | @modelOptions({ 8 | options: { customName: 'Say' }, 9 | }) 10 | export class SayModel extends BaseModel { 11 | @prop({ required: true }) 12 | @IsString() 13 | text: string 14 | 15 | @prop() 16 | @IsString() 17 | @IsOptional() 18 | source: string 19 | 20 | @prop() 21 | @IsString() 22 | @IsOptional() 23 | author: string 24 | } 25 | -------------------------------------------------------------------------------- /apps/core/src/modules/say/say.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { SayController } from './say.controller' 4 | import { SayService } from './say.service' 5 | 6 | @Module({ 7 | controllers: [SayController], 8 | providers: [SayService], 9 | exports: [SayService], 10 | }) 11 | export class SayModule {} 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/say/say.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | import { InjectModel } from '~/transformers/model.transformer' 4 | 5 | import { SayModel } from './say.model' 6 | 7 | @Injectable() 8 | export class SayService { 9 | constructor( 10 | @InjectModel(SayModel) private readonly sayModel: MongooseModel, 11 | ) {} 12 | 13 | public get model() { 14 | return this.sayModel 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/core/src/modules/search/search.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer' 2 | import { IsEnum, IsIn, IsNotEmpty, IsOptional, IsString } from 'class-validator' 3 | 4 | import { PagerDto } from '../../shared/dto/pager.dto' 5 | 6 | export class SearchDto extends PagerDto { 7 | @IsNotEmpty() 8 | @IsString() 9 | keyword: string 10 | 11 | @IsString() 12 | @IsNotEmpty() 13 | @IsOptional() 14 | orderBy: string 15 | 16 | @Transform(({ value: val }) => Number.parseInt(val)) 17 | @IsEnum([1, -1]) 18 | @IsOptional() 19 | order: number 20 | 21 | @IsOptional() 22 | @IsIn([0, 1]) 23 | // HINT: only string type in query params 24 | @Transform(({ value }) => (value === 'true' || value === '1' ? 1 : 0)) 25 | rawAlgolia?: boolean 26 | } 27 | -------------------------------------------------------------------------------- /apps/core/src/modules/search/search.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common' 2 | 3 | import { NoteModule } from '../note/note.module' 4 | import { PageModule } from '../page/page.module' 5 | import { PostModule } from '../post/post.module' 6 | import { SearchController } from './search.controller' 7 | import { SearchService } from './search.service' 8 | 9 | @Module({ 10 | controllers: [SearchController], 11 | providers: [SearchService], 12 | exports: [SearchService], 13 | imports: [ 14 | forwardRef(() => PostModule), 15 | forwardRef(() => NoteModule), 16 | forwardRef(() => PageModule), 17 | ], 18 | }) 19 | export class SearchModule {} 20 | -------------------------------------------------------------------------------- /apps/core/src/modules/server-time/server-time.controller.ts: -------------------------------------------------------------------------------- 1 | import { Get } from '@nestjs/common' 2 | 3 | import { ApiController } from '~/common/decorators/api-controller.decorator' 4 | import { HttpCache } from '~/common/decorators/cache.decorator' 5 | import { HTTPDecorators } from '~/common/decorators/http.decorator' 6 | 7 | @ApiController('/') 8 | export class ServerTimeController { 9 | @Get('/server-time') 10 | @HttpCache.disable 11 | @HTTPDecorators.Bypass 12 | async serverTime() {} 13 | } 14 | -------------------------------------------------------------------------------- /apps/core/src/modules/server-time/server-time.middleware.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from 'node:http' 2 | 3 | export async function trackResponseTimeMiddleware( 4 | req: IncomingMessage, 5 | res: ServerResponse, 6 | next: Function, 7 | ) { 8 | const requestTimeFromHeader = Number(req.headers['x-request-time']) 9 | const now = !Number.isNaN(requestTimeFromHeader) 10 | ? requestTimeFromHeader 11 | : Date.now() 12 | 13 | res.setHeader('Content-Type', 'application/json') 14 | // cors 15 | res.setHeader( 16 | 'Access-Control-Allow-Origin', 17 | req.headers.origin || req.headers.referer || req.headers.host || '*', 18 | ) 19 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') 20 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type') 21 | res.setHeader('Access-Control-Allow-Credentials', 'true') 22 | res.setHeader('Access-Control-Max-Age', '86400') 23 | await next() 24 | 25 | res.write( 26 | JSON.stringify({ 27 | t2: now, 28 | t3: Date.now(), 29 | }), 30 | ) 31 | 32 | res.end() 33 | } 34 | -------------------------------------------------------------------------------- /apps/core/src/modules/server-time/server-time.module.ts: -------------------------------------------------------------------------------- 1 | import type { MiddlewareConsumer, NestModule } from '@nestjs/common' 2 | 3 | import { Module, RequestMethod } from '@nestjs/common' 4 | 5 | import { ServerTimeController } from './server-time.controller' 6 | import { trackResponseTimeMiddleware } from './server-time.middleware' 7 | 8 | @Module({ 9 | controllers: [ServerTimeController], 10 | }) 11 | export class ServerTimeModule implements NestModule { 12 | configure(consumer: MiddlewareConsumer) { 13 | consumer 14 | .apply(trackResponseTimeMiddleware) 15 | .forRoutes({ path: '/server-time', method: RequestMethod.ALL }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/core/src/modules/serverless/function.types.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyRequest } from 'fastify' 2 | 3 | export interface FunctionContextRequest extends FastifyRequest {} 4 | 5 | export interface FunctionContextResponse { 6 | throws: (code: number, message: any) => void 7 | type: (type: string) => FunctionContextResponse 8 | status: (code: number, statusMessage?: string) => FunctionContextResponse 9 | send: (data: any) => any 10 | } 11 | 12 | export interface BuiltInFunctionObject { 13 | name: string 14 | path: string 15 | method: string 16 | code: string 17 | reference: string 18 | } 19 | 20 | export const defineBuiltInSnippetConfig = (config: BuiltInFunctionObject) => 21 | config 22 | -------------------------------------------------------------------------------- /apps/core/src/modules/serverless/mock-response.util.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyReply } from 'fastify' 2 | import type { FunctionContextResponse } from './function.types' 3 | 4 | import { HttpException } from '@nestjs/common' 5 | 6 | export const createMockedContextResponse = ( 7 | reply: FastifyReply, 8 | ): FunctionContextResponse => { 9 | const response: FunctionContextResponse = { 10 | throws(code, message) { 11 | throw new HttpException(message, code) 12 | }, 13 | type(type: string) { 14 | reply.type(type) 15 | return response 16 | }, 17 | send(data: any) { 18 | return reply.send(data) 19 | }, 20 | status(code: number, message?: string) { 21 | reply.raw.statusCode = code 22 | if (message) { 23 | reply.raw.statusMessage = message 24 | } 25 | return response 26 | }, 27 | } 28 | return response 29 | } 30 | -------------------------------------------------------------------------------- /apps/core/src/modules/serverless/pack/built-in/geocode_location.ts: -------------------------------------------------------------------------------- 1 | import type { BuiltInFunctionObject } from '../../function.types' 2 | 3 | const code = ` 4 | export default async function handler(ctx: Context) { 5 | const { latitude, longitude } = ctx.query 6 | const { axios } = await ctx.getService('http') 7 | const config = await ctx.getService('config') 8 | const adminExtra = await config.get('adminExtra') 9 | const gaodemapKey = adminExtra?.gaodemapKey || secret.gaodemapKey 10 | 11 | if (!gaodemapKey) { 12 | ctx.throws(400, '高德地图 API Key 未配置') 13 | } 14 | const { data } = await axios.get( 15 | \`https://restapi.amap.com/v3/geocode/regeo?key=\${gaodemapKey}&location=\` + 16 | \`\${longitude},\${latitude}\`, 17 | ).catch(() => null) 18 | 19 | if (!data) { 20 | ctx.throws(500, '高德地图 API 调用失败') 21 | } 22 | return data 23 | } 24 | `.trim() 25 | 26 | export default { 27 | name: 'geocode_location', 28 | path: 'geocode_location', 29 | code, 30 | method: 'GET', 31 | } as BuiltInFunctionObject 32 | -------------------------------------------------------------------------------- /apps/core/src/modules/serverless/pack/built-in/index.ts: -------------------------------------------------------------------------------- 1 | import geocode_location from './geocode_location' 2 | import geocode_search from './geocode_search' 3 | import ipQuery from './ip-query' 4 | 5 | export const builtInSnippets = [ipQuery, geocode_location, geocode_search].map( 6 | ($) => (($.reference = 'built-in'), $), 7 | ) 8 | -------------------------------------------------------------------------------- /apps/core/src/modules/serverless/pack/index.ts: -------------------------------------------------------------------------------- 1 | import { builtInSnippets } from './built-in' 2 | 3 | export const allBuiltInSnippetPack = [...builtInSnippets] 4 | -------------------------------------------------------------------------------- /apps/core/src/modules/serverless/serverless.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator' 2 | 3 | export class ServerlessReferenceDto { 4 | @IsString() 5 | @IsNotEmpty() 6 | reference: string 7 | 8 | @IsString() 9 | @IsNotEmpty() 10 | name: string 11 | } 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/serverless/serverless.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | index, 3 | modelOptions, 4 | mongoose, 5 | prop, 6 | Severity, 7 | } from '@typegoose/typegoose' 8 | 9 | export const ServerlessStorageCollectionName = `serverlessstorages` 10 | 11 | @modelOptions({ 12 | schemaOptions: {}, 13 | options: { 14 | customName: ServerlessStorageCollectionName, 15 | allowMixed: Severity.ALLOW, 16 | }, 17 | }) 18 | @index({ namespace: 1, key: 1 }) 19 | export class ServerlessStorageModel { 20 | @prop({ index: 1, required: true }) 21 | namespace: string 22 | 23 | @prop({ required: true }) 24 | key: string 25 | 26 | @prop({ type: mongoose.Schema.Types.Mixed, required: true }) 27 | value: any 28 | 29 | get uniqueKey(): string { 30 | return `${this.namespace}/${this.key}` 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/core/src/modules/serverless/serverless.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { ServerlessController } from './serverless.controller' 4 | import { ServerlessService } from './serverless.service' 5 | 6 | @Module({ 7 | controllers: [ServerlessController], 8 | providers: [ServerlessService], 9 | exports: [ServerlessService], 10 | }) 11 | export class ServerlessModule {} 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/sitemap/sitemap.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { AggregateModule } from '../aggregate/aggregate.module' 4 | import { SitemapController } from './sitemap.controller' 5 | 6 | @Module({ controllers: [SitemapController], imports: [AggregateModule] }) 7 | export class SitemapModule {} 8 | -------------------------------------------------------------------------------- /apps/core/src/modules/slug-tracker/slug-tracker.model.ts: -------------------------------------------------------------------------------- 1 | import { modelOptions, prop } from '@typegoose/typegoose' 2 | 3 | @modelOptions({ 4 | schemaOptions: { 5 | timestamps: false, 6 | }, 7 | options: { 8 | customName: 'slug_tracker', 9 | }, 10 | }) 11 | export class SlugTrackerModel { 12 | @prop({ required: true }) 13 | slug: string 14 | 15 | @prop({ required: true }) 16 | type: string 17 | 18 | @prop({ required: true }) 19 | targetId: string 20 | } 21 | -------------------------------------------------------------------------------- /apps/core/src/modules/slug-tracker/slug-tracker.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { SlugTrackerService } from './slug-tracker.service' 4 | 5 | @Module({ 6 | providers: [SlugTrackerService], 7 | exports: [SlugTrackerService], 8 | }) 9 | export class SlugTrackerModule {} 10 | -------------------------------------------------------------------------------- /apps/core/src/modules/slug-tracker/slug-tracker.service.ts: -------------------------------------------------------------------------------- 1 | import type { ArticleTypeEnum } from '~/constants/article.constant' 2 | 3 | import { Injectable } from '@nestjs/common' 4 | import { ReturnModelType } from '@typegoose/typegoose' 5 | 6 | import { InjectModel } from '~/transformers/model.transformer' 7 | 8 | import { SlugTrackerModel } from './slug-tracker.model' 9 | 10 | @Injectable() 11 | export class SlugTrackerService { 12 | constructor( 13 | @InjectModel(SlugTrackerModel) 14 | private readonly slugTrackerModel: ReturnModelType, 15 | ) {} 16 | 17 | createTracker(slug: string, type: ArticleTypeEnum, targetId: string) { 18 | return this.slugTrackerModel.create({ slug, type, targetId }) 19 | } 20 | 21 | findTrackerBySlug(slug: string, type: ArticleTypeEnum) { 22 | return this.slugTrackerModel.findOne({ slug, type }).lean() 23 | } 24 | deleteAllTracker(targetId: string) { 25 | return this.slugTrackerModel.deleteMany({ targetId }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/core/src/modules/snippet/snippet.dto.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer' 2 | import { IsOptional, IsString, ValidateNested } from 'class-validator' 3 | 4 | import { SnippetModel } from './snippet.model' 5 | 6 | export class SnippetMoreDto { 7 | @ValidateNested({ each: true }) 8 | @Type(() => SnippetModel) 9 | snippets: SnippetModel[] 10 | 11 | @IsString({ each: true }) 12 | @IsOptional() 13 | packages?: string[] 14 | } 15 | -------------------------------------------------------------------------------- /apps/core/src/modules/snippet/snippet.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 数据配置区块 3 | */ 4 | import { forwardRef, Module } from '@nestjs/common' 5 | 6 | import { ServerlessModule } from '../serverless/serverless.module' 7 | import { SnippetController } from './snippet.controller' 8 | import { SnippetService } from './snippet.service' 9 | 10 | @Module({ 11 | controllers: [SnippetController], 12 | exports: [SnippetService], 13 | providers: [SnippetService], 14 | imports: [forwardRef(() => ServerlessModule)], 15 | }) 16 | export class SnippetModule {} 17 | -------------------------------------------------------------------------------- /apps/core/src/modules/subscribe/subscribe-mail.enum.ts: -------------------------------------------------------------------------------- 1 | export enum SubscribeMailType { 2 | Newsletter = 'newsletter', 3 | } 4 | -------------------------------------------------------------------------------- /apps/core/src/modules/subscribe/subscribe.constant.ts: -------------------------------------------------------------------------------- 1 | export const SubscribePostCreateBit = Math.trunc(1) 2 | export const SubscribeNoteCreateBit = 1 << 1 3 | export const SubscribeSayCreateBit = 1 << 2 4 | export const SubscribeRecentCreateBit = 1 << 3 5 | export const SubscribeAllBit = 6 | SubscribePostCreateBit | 7 | SubscribeNoteCreateBit | 8 | SubscribeSayCreateBit | 9 | SubscribeRecentCreateBit 10 | 11 | export const SubscribeTypeToBitMap = { 12 | post_c: SubscribePostCreateBit, 13 | note_c: SubscribeNoteCreateBit, 14 | say_c: SubscribeSayCreateBit, 15 | recently_c: SubscribeRecentCreateBit, 16 | all: SubscribeAllBit, 17 | } 18 | -------------------------------------------------------------------------------- /apps/core/src/modules/subscribe/subscribe.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsIn, IsString } from 'class-validator' 2 | 3 | import { SubscribeTypeToBitMap } from './subscribe.constant' 4 | 5 | export class SubscribeDto { 6 | @IsEmail() 7 | email: string 8 | 9 | @IsIn(Object.keys(SubscribeTypeToBitMap), { each: true }) 10 | types: string[] 11 | } 12 | 13 | export class CancelSubscribeDto { 14 | @IsEmail() 15 | email: string 16 | 17 | @IsString() 18 | cancelToken: string 19 | } 20 | -------------------------------------------------------------------------------- /apps/core/src/modules/subscribe/subscribe.email.default.ts: -------------------------------------------------------------------------------- 1 | import type { UserModel, UserModelSecurityKeys } from '../user/user.model' 2 | 3 | import { SubscribeAllBit } from './subscribe.constant' 4 | 5 | const defaultPostProps = { 6 | text: '年纪在四十以上,二十以下的,恐怕就不易在前两派里有个地位了。他们的车破,又不敢“拉晚儿”,所以只能早早的出车,希望能从清晨转到午后三四点钟,拉出“车份儿”和自己的嚼谷①。他们的车破,跑得慢,所以得多走路,少要钱。到瓜市,果市,菜市,去拉货物,都是他们;钱少,可是无须快跑呢。', 7 | title: '骆驼祥子', 8 | } 9 | 10 | export const defaultSubscribeForRenderProps = { 11 | ...defaultPostProps, 12 | 13 | author: '', 14 | detail_link: '#detail_link', 15 | unsubscribe_link: '#unsubscribe_link', 16 | master: '', 17 | 18 | aggregate: { 19 | owner: {} as Omit, 20 | subscriber: { 21 | email: 'subscriber@mail.com', 22 | subscribe: SubscribeAllBit, 23 | }, 24 | post: { 25 | ...defaultPostProps, 26 | id: 'cdab54a19f3f03f7f5159df7', 27 | created: '2023-06-04T15:02:09.179Z', 28 | }, 29 | }, 30 | } 31 | 32 | export type SubscribeTemplateRenderProps = typeof defaultSubscribeForRenderProps 33 | -------------------------------------------------------------------------------- /apps/core/src/modules/subscribe/subscribe.model.ts: -------------------------------------------------------------------------------- 1 | import { modelOptions, prop } from '@typegoose/typegoose' 2 | 3 | import { BaseModel } from '~/shared/model/base.model' 4 | 5 | @modelOptions({ 6 | options: { 7 | customName: 'Subscribe', 8 | }, 9 | schemaOptions: { 10 | timestamps: { 11 | updatedAt: false, 12 | }, 13 | }, 14 | }) 15 | export class SubscribeModel extends BaseModel { 16 | @prop({ 17 | required: true, 18 | }) 19 | email: string 20 | 21 | @prop({ 22 | required: true, 23 | }) 24 | cancelToken: string 25 | 26 | @prop({ 27 | required: true, 28 | }) 29 | subscribe: number 30 | 31 | @prop({ 32 | default: false, 33 | }) 34 | verified: boolean 35 | } 36 | -------------------------------------------------------------------------------- /apps/core/src/modules/subscribe/subscribe.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { UserModule } from '../user/user.module' 4 | import { SubscribeController } from './subscribe.controller' 5 | import { SubscribeService } from './subscribe.service' 6 | 7 | @Module({ 8 | controllers: [SubscribeController], 9 | providers: [SubscribeService], 10 | exports: [SubscribeService], 11 | imports: [UserModule], 12 | }) 13 | export class SubscribeModule {} 14 | -------------------------------------------------------------------------------- /apps/core/src/modules/sync-update/sync-update.model.ts: -------------------------------------------------------------------------------- 1 | import { modelOptions, prop } from '@typegoose/typegoose' 2 | 3 | import { 4 | SyncableCollectionName, 5 | SyncableCollectionNames, 6 | } from '../sync/sync.constant' 7 | import { 8 | SyncableDataInteraction, 9 | SyncableDataInteractions, 10 | } from './sync-update.type' 11 | 12 | @modelOptions({ 13 | schemaOptions: { 14 | timestamps: { 15 | createdAt: false, 16 | updatedAt: false, 17 | }, 18 | }, 19 | options: { 20 | customName: 'sync_update', 21 | }, 22 | }) 23 | export class SyncUpdateModel { 24 | @prop({ 25 | required: true, 26 | }) 27 | updateId: string 28 | 29 | @prop({ 30 | required: true, 31 | index: true, 32 | }) 33 | updateAt: Date 34 | 35 | @prop({ 36 | enum: SyncableCollectionNames, 37 | required: true, 38 | }) 39 | type: SyncableCollectionName 40 | 41 | @prop({ 42 | enum: SyncableDataInteractions, 43 | required: true, 44 | }) 45 | interection: SyncableDataInteraction 46 | } 47 | -------------------------------------------------------------------------------- /apps/core/src/modules/sync-update/sync-update.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { SyncUpdateService } from './sync-update.service' 4 | 5 | @Module({ 6 | providers: [SyncUpdateService], 7 | exports: [SyncUpdateService], 8 | }) 9 | export class SyncUpdateModule {} 10 | -------------------------------------------------------------------------------- /apps/core/src/modules/sync-update/sync-update.type.ts: -------------------------------------------------------------------------------- 1 | export type SyncableDataInteraction = 'create' | 'update' | 'delete' 2 | 3 | export const SyncableDataInteractions = ['create', 'update', 'delete'] as const 4 | -------------------------------------------------------------------------------- /apps/core/src/modules/sync/sync.constant.ts: -------------------------------------------------------------------------------- 1 | export type SyncableCollectionName = 2 | | 'post' 3 | | 'page' 4 | | 'note' 5 | | 'category' 6 | | 'topic' 7 | export const SyncableCollectionNames = [ 8 | 'post', 9 | 'page', 10 | 'note', 11 | 'category', 12 | 'topic', 13 | ] 14 | -------------------------------------------------------------------------------- /apps/core/src/modules/sync/sync.dto.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsDateString, 3 | IsEnum, 4 | IsMongoId, 5 | IsOptional, 6 | IsString, 7 | } from 'class-validator' 8 | 9 | import { 10 | SyncableCollectionName, 11 | SyncableCollectionNames, 12 | } from './sync.constant' 13 | 14 | export class SyncByLastSyncedAtDto { 15 | @IsDateString() 16 | lastSyncedAt: string 17 | } 18 | 19 | export class SyncDataChecksumDto { 20 | @IsOptional() 21 | @IsString() 22 | checksum?: string 23 | 24 | @IsEnum(SyncableCollectionNames) 25 | type: SyncableCollectionName 26 | 27 | @IsMongoId() 28 | id: string 29 | } 30 | -------------------------------------------------------------------------------- /apps/core/src/modules/sync/sync.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { CategoryModule } from '../category/category.module' 4 | import { NoteModule } from '../note/note.module' 5 | import { PageModule } from '../page/page.module' 6 | import { PostModule } from '../post/post.module' 7 | import { TopicModule } from '../topic/topic.module' 8 | import { SyncController } from './sync.controller' 9 | import { SyncService } from './sync.service' 10 | 11 | @Module({ 12 | controllers: [SyncController], 13 | providers: [SyncService], 14 | imports: [PostModule, NoteModule, PageModule, CategoryModule, TopicModule], 15 | }) 16 | export class SyncModule {} 17 | -------------------------------------------------------------------------------- /apps/core/src/modules/topic/topic.controller.ts: -------------------------------------------------------------------------------- 1 | import slugify from 'slugify' 2 | 3 | import { Get, Param } from '@nestjs/common' 4 | 5 | import { CannotFindException } from '~/common/exceptions/cant-find.exception' 6 | import { BaseCrudFactory } from '~/transformers/crud-factor.transformer' 7 | 8 | import { TopicModel } from './topic.model' 9 | 10 | class Upper { 11 | constructor(private readonly _model: MongooseModel) {} 12 | 13 | @Get('/slug/:slug') 14 | async getTopicByTopic(@Param('slug') slug: string) { 15 | slug = slugify(slug) 16 | const topic = await this._model.findOne({ slug }).lean() 17 | if (!topic) { 18 | throw new CannotFindException() 19 | } 20 | 21 | return topic 22 | } 23 | } 24 | 25 | export const TopicBaseController = BaseCrudFactory({ 26 | model: TopicModel, 27 | 28 | classUpper: Upper, 29 | }) 30 | -------------------------------------------------------------------------------- /apps/core/src/modules/topic/topic.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { TopicBaseController } from './topic.controller' 4 | import { TopicService } from './topic.service' 5 | 6 | @Module({ 7 | controllers: [TopicBaseController], 8 | exports: [TopicService], 9 | providers: [TopicService], 10 | }) 11 | export class TopicModule {} 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/topic/topic.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { ReturnModelType } from '@typegoose/typegoose' 3 | 4 | import { InjectModel } from '~/transformers/model.transformer' 5 | 6 | import { TopicModel } from './topic.model' 7 | 8 | @Injectable() 9 | export class TopicService { 10 | constructor( 11 | @InjectModel(TopicModel) 12 | private readonly topicModel: ReturnModelType, 13 | ) {} 14 | 15 | public get model() { 16 | return this.topicModel 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/core/src/modules/update/update.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsOptional } from 'class-validator' 2 | 3 | export class UpdateAdminDto { 4 | @IsBoolean() 5 | @IsOptional() 6 | force?: boolean 7 | } 8 | -------------------------------------------------------------------------------- /apps/core/src/modules/update/update.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { UpdateController } from './update.controller' 4 | import { UpdateService } from './update.service' 5 | 6 | @Module({ 7 | controllers: [UpdateController], 8 | providers: [UpdateService], 9 | exports: [UpdateService], 10 | }) 11 | export class UpdateModule {} 12 | -------------------------------------------------------------------------------- /apps/core/src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Global, Module } from '@nestjs/common' 2 | 3 | import { AuthnModule } from '../authn/auth.module' 4 | import { UserController } from './user.controller' 5 | import { UserService } from './user.service' 6 | 7 | @Global() 8 | @Module({ 9 | controllers: [UserController], 10 | providers: [UserService], 11 | imports: [forwardRef(() => AuthnModule)], 12 | exports: [UserService], 13 | }) 14 | export class UserModule {} 15 | -------------------------------------------------------------------------------- /apps/core/src/modules/webhook/webhook.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | 3 | import { WebhookController } from './webhook.controller' 4 | import { WebhookService } from './webhook.service' 5 | 6 | @Module({ 7 | controllers: [WebhookController], 8 | providers: [WebhookService], 9 | }) 10 | export class WebhookModule {} 11 | -------------------------------------------------------------------------------- /apps/core/src/processors/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common' 2 | 3 | import { databaseModels } from './database.models' 4 | import { databaseProvider } from './database.provider' 5 | import { DatabaseService } from './database.service' 6 | 7 | @Module({ 8 | providers: [DatabaseService, databaseProvider, ...databaseModels], 9 | exports: [DatabaseService, databaseProvider, ...databaseModels], 10 | }) 11 | @Global() 12 | export class DatabaseModule {} 13 | -------------------------------------------------------------------------------- /apps/core/src/processors/database/database.provider.ts: -------------------------------------------------------------------------------- 1 | import { DB_CONNECTION_TOKEN } from '~/constants/system.constant' 2 | import { getDatabaseConnection } from '~/utils/database.util' 3 | 4 | export const databaseProvider = { 5 | provide: DB_CONNECTION_TOKEN, 6 | useFactory: getDatabaseConnection, 7 | } 8 | -------------------------------------------------------------------------------- /apps/core/src/processors/gateway/base.gateway.ts: -------------------------------------------------------------------------------- 1 | import type { Socket } from 'socket.io' 2 | 3 | import { BusinessEvents } from '~/constants/business-event.constant' 4 | 5 | export abstract class BaseGateway { 6 | public gatewayMessageFormat( 7 | type: BusinessEvents, 8 | message: any, 9 | code?: number, 10 | ) { 11 | return { 12 | type, 13 | data: JSON.parse(JSON.stringify(message)), 14 | code, 15 | } 16 | } 17 | 18 | handleDisconnect(client: Socket) { 19 | client.send( 20 | this.gatewayMessageFormat( 21 | BusinessEvents.GATEWAY_CONNECT, 22 | 'WebSocket 断开', 23 | ), 24 | ) 25 | } 26 | handleConnect(client: Socket) { 27 | client.send( 28 | this.gatewayMessageFormat( 29 | BusinessEvents.GATEWAY_CONNECT, 30 | 'WebSocket 已连接', 31 | ), 32 | ) 33 | } 34 | 35 | // eslint-disable-next-line unused-imports/no-unused-vars 36 | broadcast(event: BusinessEvents, data: any) {} 37 | } 38 | 39 | export abstract class BroadcastBaseGateway extends BaseGateway {} 40 | -------------------------------------------------------------------------------- /apps/core/src/processors/gateway/gateway.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Innei 3 | * @Date: 2020-04-30 12:21:51 4 | * @LastEditTime: 2020-05-31 19:07:17 5 | * @LastEditors: Innei 6 | * @FilePath: /mx-server/src/gateway/gateway.module.ts 7 | * @Coding with Love 8 | */ 9 | import { Global, Module } from '@nestjs/common' 10 | 11 | import { AuthService } from '~/modules/auth/auth.service' 12 | 13 | import { AdminEventsGateway } from './admin/events.gateway' 14 | import { GatewayService } from './gateway.service' 15 | import { SharedGateway } from './shared/events.gateway' 16 | import { WebEventsGateway } from './web/events.gateway' 17 | 18 | @Global() 19 | @Module({ 20 | imports: [], 21 | providers: [ 22 | AdminEventsGateway, 23 | WebEventsGateway, 24 | SharedGateway, 25 | 26 | AuthService, 27 | 28 | GatewayService, 29 | ], 30 | exports: [ 31 | AdminEventsGateway, 32 | WebEventsGateway, 33 | SharedGateway, 34 | 35 | GatewayService, 36 | ], 37 | }) 38 | export class GatewayModule {} 39 | -------------------------------------------------------------------------------- /apps/core/src/processors/gateway/shared/events.gateway.ts: -------------------------------------------------------------------------------- 1 | import type { BusinessEvents } from '~/constants/business-event.constant' 2 | 3 | import { Injectable } from '@nestjs/common' 4 | 5 | import { AdminEventsGateway } from '../admin/events.gateway' 6 | import { WebEventsGateway } from '../web/events.gateway' 7 | 8 | @Injectable() 9 | export class SharedGateway { 10 | constructor( 11 | private readonly admin: AdminEventsGateway, 12 | private readonly web: WebEventsGateway, 13 | ) {} 14 | 15 | broadcast(event: BusinessEvents, data: any) { 16 | this.admin.broadcast(event, data) 17 | this.web.broadcast(event, data) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/core/src/processors/gateway/web/dtos/message.ts: -------------------------------------------------------------------------------- 1 | import { IsEnum, IsObject } from 'class-validator' 2 | 3 | export enum SupportedMessageEvent { 4 | Join = 'join', 5 | Leave = 'leave', 6 | UpdateSid = 'updateSid', 7 | } 8 | export class MessageEventDto { 9 | @IsEnum(SupportedMessageEvent) 10 | type: SupportedMessageEvent 11 | @IsObject() 12 | payload: unknown 13 | } 14 | -------------------------------------------------------------------------------- /apps/core/src/processors/gateway/web/hook.interface.ts: -------------------------------------------------------------------------------- 1 | import type { Socket } from 'socket.io' 2 | 3 | export type HookFunction = (socket: Socket) => any 4 | export type HookWithDataFunction = (socket: Socket, data: unknown) => any 5 | export type RoomHookFunction = (socket: Socket, roomName: string) => any 6 | 7 | export type EventGatewayHooks = { 8 | onConnected: HookFunction[] 9 | onDisconnected: HookFunction[] 10 | onMessage: HookWithDataFunction[] 11 | onJoinRoom: RoomHookFunction[] 12 | onLeaveRoom: RoomHookFunction[] 13 | } 14 | -------------------------------------------------------------------------------- /apps/core/src/processors/helper/helper.upload.service.ts: -------------------------------------------------------------------------------- 1 | import type { MultipartFile } from '@fastify/multipart' 2 | import type { FastifyRequest } from 'fastify' 3 | 4 | import { BadRequestException, Injectable } from '@nestjs/common' 5 | 6 | @Injectable() 7 | export class UploadService { 8 | public async getAndValidMultipartField( 9 | req: FastifyRequest, 10 | 11 | options?: { 12 | maxFileSize?: number 13 | }, 14 | ): Promise { 15 | const data = await req.file({ 16 | limits: { 17 | fileSize: options?.maxFileSize, 18 | }, 19 | }) 20 | 21 | if (!data) { 22 | throw new BadRequestException('仅供上传文件!') 23 | } 24 | if (data.fieldname != 'file') { 25 | throw new BadRequestException('字段必须为 file') 26 | } 27 | 28 | return data 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/core/src/processors/redis/cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from 'cache-manager' 2 | 3 | import { CACHE_MANAGER } from '@nestjs/cache-manager' 4 | import { Inject, Injectable } from '@nestjs/common' 5 | 6 | // Cache 客户端管理器 7 | 8 | // 获取器 9 | export type TCacheKey = string 10 | export type TCacheResult = Promise 11 | 12 | /** 13 | * @class CacheService 14 | * @classdesc 承载缓存服务 15 | * @example CacheService.get(CacheKey).then() 16 | * @example CacheService.set(CacheKey).then() 17 | */ 18 | @Injectable() 19 | /** 20 | * @deprecated 21 | */ 22 | export class CacheService { 23 | private cache!: Cache 24 | constructor(@Inject(CACHE_MANAGER) cache: Cache) { 25 | this.cache = cache 26 | } 27 | 28 | public get(key: TCacheKey): TCacheResult { 29 | return this.cache.get(key) 30 | } 31 | 32 | public set(key: TCacheKey, value: any, milliseconds: number) { 33 | return this.cache.set(key, value, milliseconds) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/core/src/processors/redis/redis.config.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Cache config service. 3 | * @file Cache 配置器 4 | * @module processor/redis/redis.config.service 5 | * @author Innei 6 | */ 7 | 8 | import type { 9 | CacheModuleOptions, 10 | CacheOptionsFactory, 11 | } from '@nestjs/cache-manager' 12 | 13 | import Keyv from '@keyv/redis' 14 | import { Injectable } from '@nestjs/common' 15 | 16 | import { REDIS } from '~/app.config' 17 | 18 | @Injectable() 19 | export class RedisConfigService implements CacheOptionsFactory { 20 | // 缓存配置 21 | public createCacheOptions(): CacheModuleOptions { 22 | return { 23 | ttl: REDIS.ttl ?? undefined, 24 | max: REDIS.max, 25 | 26 | stores: [ 27 | new Keyv({ 28 | url: `redis://${REDIS.host}:${REDIS.port}`, 29 | password: REDIS.password as any, 30 | }), 31 | ], 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/core/src/processors/redis/redis.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Cache module. 3 | * @file Cache 全局模块 4 | * @module processor/cache/module 5 | */ 6 | import { CacheModule as NestCacheModule } from '@nestjs/cache-manager' 7 | import { Global, Module } from '@nestjs/common' 8 | 9 | import { CacheService } from './cache.service' 10 | import { RedisConfigService } from './redis.config.service' 11 | import { RedisService } from './redis.service' 12 | import { SubPubBridgeService } from './subpub.service' 13 | 14 | @Global() 15 | @Module({ 16 | imports: [ 17 | NestCacheModule.registerAsync({ 18 | useClass: RedisConfigService, 19 | inject: [RedisConfigService], 20 | }), 21 | ], 22 | providers: [ 23 | RedisConfigService, 24 | CacheService, 25 | SubPubBridgeService, 26 | RedisService, 27 | ], 28 | exports: [CacheService, SubPubBridgeService, RedisService], 29 | }) 30 | export class RedisModule {} 31 | -------------------------------------------------------------------------------- /apps/core/src/processors/redis/subpub.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | import { redisSubPub } from '~/utils/redis-subpub.util' 4 | 5 | @Injectable() 6 | export class SubPubBridgeService { 7 | public publish(event: string, data: any) { 8 | return redisSubPub.publish(event, data) 9 | } 10 | 11 | public subscribe(event: string, callback: (data: any) => void) { 12 | return redisSubPub.subscribe(event, callback) 13 | } 14 | 15 | public unsubscribe(event: string, callback: (data: any) => void) { 16 | return redisSubPub.unsubscribe(event, callback) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/core/src/repl.ts: -------------------------------------------------------------------------------- 1 | import { repl } from '@nestjs/core' 2 | 3 | import { initializeApp } from './global/index.global' 4 | 5 | async function bootstrap() { 6 | initializeApp() 7 | const { AppModule } = await import('./app.module') 8 | await repl(AppModule) 9 | } 10 | 11 | bootstrap() 12 | -------------------------------------------------------------------------------- /apps/core/src/shared/dto/file.dto.ts: -------------------------------------------------------------------------------- 1 | export class FileUploadDto { 2 | file: any 3 | } 4 | -------------------------------------------------------------------------------- /apps/core/src/shared/dto/id.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer' 2 | import { IsDefined, IsMongoId, isMongoId } from 'class-validator' 3 | 4 | import { UnprocessableEntityException } from '@nestjs/common' 5 | 6 | export class MongoIdDto { 7 | @IsMongoId() 8 | id: string 9 | } 10 | 11 | export class IntIdOrMongoIdDto { 12 | @IsDefined() 13 | @Transform(({ value }) => { 14 | if (isMongoId(value)) { 15 | return value 16 | } 17 | const nid = +value 18 | if (!Number.isNaN(nid)) { 19 | return nid 20 | } 21 | throw new UnprocessableEntityException('Invalid id') 22 | }) 23 | id: string | number 24 | } 25 | -------------------------------------------------------------------------------- /apps/core/src/shared/interface/paginator.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Pagination { 2 | data: T[] 3 | pagination: Paginator 4 | } 5 | export class Paginator { 6 | /** 7 | * 总条数 8 | */ 9 | readonly total: number 10 | /** 11 | * 一页多少条 12 | */ 13 | readonly size: number 14 | /** 15 | * 当前页 16 | */ 17 | readonly currentPage: number 18 | /** 19 | * 总页数 20 | */ 21 | readonly totalPage: number 22 | readonly hasNextPage: boolean 23 | readonly hasPrevPage: boolean 24 | } 25 | -------------------------------------------------------------------------------- /apps/core/src/shared/model/base-comment.model.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsOptional } from 'class-validator' 2 | 3 | import { prop } from '@typegoose/typegoose' 4 | 5 | import { BaseModel } from './base.model' 6 | 7 | export abstract class BaseCommentIndexModel extends BaseModel { 8 | @prop({ default: 0 }) 9 | commentsIndex?: number 10 | 11 | @prop({ default: true }) 12 | @IsBoolean() 13 | @IsOptional() 14 | allowComment: boolean 15 | 16 | static get protectedKeys() { 17 | return ['commentsIndex'].concat(super.protectedKeys) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/core/src/shared/model/base.model.ts: -------------------------------------------------------------------------------- 1 | import mongooseLeanGetters from 'mongoose-lean-getters' 2 | import mongooseLeanVirtuals from 'mongoose-lean-virtuals' 3 | import Paginate from 'mongoose-paginate-v2' 4 | 5 | import { index, modelOptions, plugin } from '@typegoose/typegoose' 6 | 7 | import { mongooseLeanId } from './plugins/lean-id' 8 | 9 | @plugin(mongooseLeanVirtuals) 10 | @plugin(Paginate) 11 | @plugin(mongooseLeanGetters) 12 | @plugin(mongooseLeanId) 13 | @modelOptions({ 14 | schemaOptions: { 15 | toJSON: { virtuals: true, getters: true }, 16 | toObject: { virtuals: true, getters: true }, 17 | timestamps: { 18 | createdAt: 'created', 19 | updatedAt: false, 20 | }, 21 | versionKey: false, 22 | }, 23 | }) 24 | @index({ created: -1 }) 25 | @index({ created: 1 }) 26 | export class BaseModel { 27 | created?: Date 28 | 29 | id: string 30 | 31 | static get protectedKeys() { 32 | return ['created', 'id', '_id'] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/core/src/shared/model/count.model.ts: -------------------------------------------------------------------------------- 1 | import { modelOptions, prop } from '@typegoose/typegoose' 2 | 3 | @modelOptions({ 4 | schemaOptions: { id: false, _id: false }, 5 | options: { customName: 'count' }, 6 | }) 7 | export class CountModel { 8 | @prop({ default: 0 }) 9 | read?: number 10 | 11 | @prop({ default: 0 }) 12 | like?: number 13 | } 14 | -------------------------------------------------------------------------------- /apps/core/src/shared/model/image.model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IsHexColor, 3 | IsNumber, 4 | IsOptional, 5 | IsString, 6 | IsUrl, 7 | } from 'class-validator' 8 | 9 | import { modelOptions, prop } from '@typegoose/typegoose' 10 | 11 | @modelOptions({ 12 | schemaOptions: { _id: false }, 13 | }) 14 | export abstract class ImageModel { 15 | @prop() 16 | @IsOptional() 17 | @IsNumber() 18 | width?: number 19 | 20 | @prop() 21 | @IsOptional() 22 | @IsNumber() 23 | height?: number 24 | 25 | @prop() 26 | @IsOptional() 27 | @IsHexColor() 28 | accent?: string 29 | 30 | @prop() 31 | @IsString() 32 | @IsOptional() 33 | type?: string 34 | 35 | @prop() 36 | @IsOptional() 37 | @IsUrl() 38 | src?: string 39 | 40 | @prop() 41 | @IsOptional() 42 | @IsString() 43 | blurHash?: string 44 | } 45 | -------------------------------------------------------------------------------- /apps/core/src/transformers/db-query.transformer.ts: -------------------------------------------------------------------------------- 1 | export const addYearCondition = (year?: number) => { 2 | if (!year) { 3 | return {} 4 | } 5 | return { 6 | created: { 7 | $gte: new Date(year, 1, 1), 8 | $lte: new Date(year + 1, 1, 1), 9 | }, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/core/src/transformers/get-req.transformer.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionContext } from '@nestjs/common' 2 | import type { UserModel } from '~/modules/user/user.model' 3 | import type { FastifyRequest } from 'fastify' 4 | import type { IncomingMessage } from 'node:http' 5 | 6 | type BizRequest = { 7 | user?: UserModel 8 | isGuest: boolean 9 | 10 | isAuthenticated: boolean 11 | token?: string 12 | readerId?: string 13 | } 14 | 15 | export type FastifyBizRequest = FastifyRequest & BizRequest 16 | 17 | export type BizIncomingMessage = IncomingMessage & BizRequest 18 | export function getNestExecutionContextRequest( 19 | context: ExecutionContext, 20 | ): FastifyBizRequest { 21 | return context.switchToHttp().getRequest() as any 22 | } 23 | -------------------------------------------------------------------------------- /apps/core/src/transformers/paginate.transformer.ts: -------------------------------------------------------------------------------- 1 | import type { mongoose } from '@typegoose/typegoose' 2 | import type { Pagination } from '~/shared/interface/paginator.interface' 3 | 4 | export function transformDataToPaginate( 5 | data: mongoose.PaginateResult, 6 | ): Pagination { 7 | return { 8 | data: data.docs, 9 | pagination: { 10 | total: data.totalDocs, 11 | currentPage: data.page as number, 12 | totalPage: data.totalPages as number, 13 | size: data.limit, 14 | hasNextPage: data.hasNextPage, 15 | hasPrevPage: data.hasPrevPage, 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/core/src/types/request.ts: -------------------------------------------------------------------------------- 1 | import type { UserModel } from '~/modules/user/user.model' 2 | import type { FastifyReply, FastifyRequest } from 'fastify' 3 | 4 | export type AdapterRequest = FastifyRequest & 5 | ( 6 | | { 7 | isGuest: true 8 | isAuthenticated: false 9 | } 10 | | { 11 | user: UserModel 12 | token: string 13 | isGuest: false 14 | isAuthenticated: true 15 | } 16 | ) & 17 | Record 18 | export type AdapterResponse = FastifyReply & Record 19 | -------------------------------------------------------------------------------- /apps/core/src/types/socket-meta.ts: -------------------------------------------------------------------------------- 1 | export interface SocketMetadata {} 2 | 3 | declare module 'socket.io' { 4 | interface Socket { 5 | // @ts-expect-error 6 | data: SocketMetadata 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/core/src/types/unique.ts: -------------------------------------------------------------------------------- 1 | export type UniqueArray = T extends readonly [infer X, ...infer Rest] 2 | ? InArray extends true 3 | ? ['Encountered value with duplicates:', X] 4 | : readonly [X, ...UniqueArray] 5 | : T 6 | 7 | type InArray = T extends readonly [X, ...infer _Rest] 8 | ? true 9 | : T extends readonly [X] 10 | ? true 11 | : T extends readonly [infer _, ...infer Rest] 12 | ? InArray 13 | : false 14 | -------------------------------------------------------------------------------- /apps/core/src/utils/biz.util.ts: -------------------------------------------------------------------------------- 1 | import { DEMO_MODE } from '~/app.config' 2 | import { BanInDemoExcpetion } from '~/common/exceptions/ban-in-demo.exception' 3 | import { CollectionRefTypes } from '~/constants/db.constant' 4 | 5 | /** 6 | * 检查是否在 demo 模式下,禁用此功能 7 | */ 8 | export const banInDemo = () => { 9 | if (DEMO_MODE) { 10 | throw new BanInDemoExcpetion() 11 | } 12 | } 13 | 14 | export const checkRefModelCollectionType = (ref: any) => { 15 | if (!ref && typeof ref !== 'object') 16 | throw new TypeError('ref must be an object') 17 | 18 | if ('nid' in ref) { 19 | return CollectionRefTypes.Note 20 | } 21 | if ('title' in ref && 'categoryId' in ref) { 22 | return CollectionRefTypes.Post 23 | } 24 | if ('title' in ref && 'subtitle' in ref) { 25 | return CollectionRefTypes.Page 26 | } 27 | 28 | if ('content' in ref) { 29 | return CollectionRefTypes.Recently 30 | } 31 | throw new ReferenceError('ref is not a valid model collection type') 32 | } 33 | -------------------------------------------------------------------------------- /apps/core/src/utils/check-init.util.ts: -------------------------------------------------------------------------------- 1 | import { USER_COLLECTION_NAME } from '~/constants/db.constant' 2 | 3 | import { getDatabaseConnection } from './database.util' 4 | 5 | export const checkInit = async () => { 6 | const connection = await getDatabaseConnection() 7 | const db = connection.db! 8 | const isUserExist = 9 | (await db.collection(USER_COLLECTION_NAME).countDocuments()) > 0 10 | 11 | return isUserExist 12 | } 13 | -------------------------------------------------------------------------------- /apps/core/src/utils/esm-import.util.ts: -------------------------------------------------------------------------------- 1 | export const importESM = (module: string) => { 2 | return new Function(module, 'return import(modulePath)') 3 | } 4 | -------------------------------------------------------------------------------- /apps/core/src/utils/mine.util.ts: -------------------------------------------------------------------------------- 1 | export const isZipMinetype = (mine: string) => { 2 | const zipMineType = ['application/x-zip-compressed', 'application/zip'] 3 | 4 | return zipMineType.includes(mine) 5 | } 6 | -------------------------------------------------------------------------------- /apps/core/src/utils/path.util.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | 3 | import { LOG_DIR } from '~/constants/path.constant' 4 | 5 | import { getShortDate } from './time.util' 6 | 7 | export const getTodayLogFilePath = () => 8 | resolve(LOG_DIR, `stdout_${getShortDate(new Date())}.log`) 9 | -------------------------------------------------------------------------------- /apps/core/src/utils/pic.util.ts: -------------------------------------------------------------------------------- 1 | import { marked } from 'marked' 2 | 3 | const isVideoExts = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.flv', '.mkv'] 4 | export const pickImagesFromMarkdown = (text: string) => { 5 | const ast = marked.lexer(text) 6 | const images = [] as string[] 7 | function pickImage(node: any) { 8 | if (node.type === 'image') { 9 | if (isVideoExts.some((ext) => node.href.endsWith(ext))) { 10 | return 11 | } 12 | images.push(node.href) 13 | return 14 | } 15 | if (node.tokens && Array.isArray(node.tokens)) { 16 | return node.tokens.forEach((element) => { 17 | pickImage(element) 18 | }) 19 | } 20 | } 21 | ast.forEach((element) => { 22 | pickImage(element) 23 | }) 24 | return images 25 | } 26 | 27 | function componentToHex(c: number) { 28 | const hex = c.toString(16) 29 | return hex.length == 1 ? `0${hex}` : hex 30 | } 31 | 32 | export function rgbToHex({ r, g, b }: { r: number; g: number; b: number }) { 33 | return `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}` 34 | } 35 | -------------------------------------------------------------------------------- /apps/core/src/utils/queue.util.ts: -------------------------------------------------------------------------------- 1 | export class AsyncQueue { 2 | private maxConcurrent: number 3 | private queue: (() => Promise)[] 4 | private activeCount: number 5 | 6 | constructor(maxConcurrent: number) { 7 | this.maxConcurrent = maxConcurrent 8 | this.queue = [] 9 | this.activeCount = 0 10 | } 11 | 12 | private async runNext() { 13 | if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) { 14 | return 15 | } 16 | 17 | this.activeCount++ 18 | const request = this.queue.shift()! 19 | 20 | try { 21 | return await request() 22 | } catch (error) { 23 | console.error('Request failed', error) 24 | } finally { 25 | this.activeCount-- 26 | this.runNext() // Start the next request after this one finishes 27 | } 28 | } 29 | 30 | add(request: () => Promise) { 31 | this.queue.push(request) 32 | return this.runNext() 33 | } 34 | 35 | addMultiple(requests: (() => Promise)[]) { 36 | this.queue.push(...requests) 37 | const wait = this.runNext() 38 | return async () => await wait 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/core/src/utils/redis.util.ts: -------------------------------------------------------------------------------- 1 | import type { RedisKeys } from '~/constants/cache.constant' 2 | 3 | import { DEMO_MODE } from '~/app.config' 4 | 5 | type Prefix = 'mx' | 'mx-demo' 6 | const prefix = DEMO_MODE ? 'mx-demo' : 'mx' 7 | 8 | export const getRedisKey = ( 9 | key: T, 10 | ...concatKeys: string[] 11 | ): `${Prefix}:${T}${string | ''}` => { 12 | return `${prefix}:${key}${ 13 | concatKeys && concatKeys.length > 0 ? `:${concatKeys.join('_')}` : '' 14 | }` 15 | } 16 | -------------------------------------------------------------------------------- /apps/core/src/utils/safe-eval.util.ts: -------------------------------------------------------------------------------- 1 | import vm2 from 'vm2' 2 | 3 | export function safeEval(code: string, context = {}, options?: vm2.VMOptions) { 4 | const sandbox = { 5 | global: {}, 6 | } 7 | 8 | code = `((() => { ${code} })())` 9 | if (context) { 10 | Object.keys(context).forEach((key) => { 11 | sandbox[key] = context[key] 12 | }) 13 | } 14 | 15 | const VM = new vm2.VM({ 16 | timeout: 60_0000, 17 | sandbox, 18 | 19 | eval: false, 20 | ...options, 21 | }) 22 | 23 | return VM.run(code) 24 | } 25 | -------------------------------------------------------------------------------- /apps/core/test/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { WrappedConsola } from '@innei/pretty-logger-nestjs/lib/consola' 2 | import type { Document, PaginateModel } from 'mongoose' 3 | 4 | import 'vitest/globals' 5 | 6 | import type { ModelType } from '@typegoose/typegoose/lib/types' 7 | 8 | declare global { 9 | export type KV = Record 10 | 11 | // @ts-ignore 12 | export type MongooseModel = ModelType & PaginateModel 13 | 14 | export const isDev: boolean 15 | 16 | export const consola: WrappedConsola 17 | export const cwd: string 18 | 19 | interface JSON { 20 | safeParse: typeof JSON.parse 21 | } 22 | } 23 | 24 | export {} 25 | -------------------------------------------------------------------------------- /apps/core/test/helper/create-mock-global-module.ts: -------------------------------------------------------------------------------- 1 | import type { Provider } from '@nestjs/common' 2 | 3 | import { Global, Module } from '@nestjs/common' 4 | 5 | export const createMockGlobalModule = (providers: Provider[]) => { 6 | @Global() 7 | @Module({ 8 | providers, 9 | exports: providers, 10 | }) 11 | class MockGlobalModule {} 12 | 13 | return MockGlobalModule 14 | } 15 | -------------------------------------------------------------------------------- /apps/core/test/helper/defineProvider.ts: -------------------------------------------------------------------------------- 1 | export interface Provider { 2 | provide: new (...args: any[]) => T 3 | useValue: Partial 4 | } 5 | 6 | export const defineProvider = (provider: Provider) => { 7 | return provider 8 | } 9 | 10 | export function defineProviders(providers: [Provider]): [Provider] 11 | export function defineProviders( 12 | providers: [Provider, Provider], 13 | ): [Provider, Provider] 14 | export function defineProviders( 15 | providers: [Provider, Provider, Provider], 16 | ): [Provider, Provider, Provider] 17 | 18 | export function defineProviders(providers: Provider[]) { 19 | return providers 20 | } 21 | -------------------------------------------------------------------------------- /apps/core/test/helper/setup-e2e.ts: -------------------------------------------------------------------------------- 1 | import type { ModuleMetadata } from '@nestjs/common' 2 | import type { NestFastifyApplication } from '@nestjs/platform-fastify' 3 | 4 | import { ValidationPipe } from '@nestjs/common' 5 | import { Test, TestingModule } from '@nestjs/testing' 6 | 7 | import { fastifyApp } from '~/common/adapters/fastify.adapter' 8 | 9 | export const setupE2EApp = async (module: TestingModule | ModuleMetadata) => { 10 | let nextModule: TestingModule 11 | if (module instanceof TestingModule) { 12 | nextModule = module 13 | } else { 14 | nextModule = await Test.createTestingModule(module).compile() 15 | } 16 | 17 | const app = 18 | nextModule.createNestApplication(fastifyApp) 19 | app.useGlobalPipes( 20 | new ValidationPipe({ 21 | transform: true, 22 | whitelist: true, 23 | errorHttpStatusCode: 422, 24 | forbidUnknownValues: true, 25 | enableDebugMessages: isDev, 26 | stopAtFirstError: true, 27 | }), 28 | ) 29 | 30 | await app.init() 31 | await app.getHttpAdapter().getInstance().ready() 32 | return app 33 | } 34 | -------------------------------------------------------------------------------- /apps/core/test/helper/utils.helper.ts: -------------------------------------------------------------------------------- 1 | export const firstOfMap = (map: Map) => [...map.entries()]?.[0] 2 | export const firstKeyOfMap = (map: Map) => 3 | [...map.entries()]?.[0][0] 4 | export const firstValueOfMap = (map: Map) => 5 | [...map.entries()]?.[0][1] 6 | -------------------------------------------------------------------------------- /apps/core/test/mock/constants/token.ts: -------------------------------------------------------------------------------- 1 | export const authJWTToken = '__token__' 2 | -------------------------------------------------------------------------------- /apps/core/test/mock/decorators/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, UseGuards } from '@nestjs/common' 2 | 3 | import { AuthTestingGuard } from '../guard/auth.guard' 4 | 5 | export function Auth() { 6 | const decorators: (ClassDecorator | PropertyDecorator | MethodDecorator)[] = 7 | [] 8 | 9 | decorators.push(UseGuards(AuthTestingGuard)) 10 | 11 | return applyDecorators(...decorators) 12 | } 13 | -------------------------------------------------------------------------------- /apps/core/test/mock/guard/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionContext } from '@nestjs/common' 2 | import type { UserModel } from '~/modules/user/user.model' 3 | 4 | import { UnauthorizedException } from '@nestjs/common' 5 | 6 | import { authJWTToken } from '../constants/token' 7 | 8 | export const mockUser1: UserModel = { 9 | id: '1', 10 | name: 'John Doe', 11 | mail: 'example@ee.com', 12 | password: '**********', 13 | 14 | username: 'johndoe', 15 | created: new Date('2021/1/1 10:00:11'), 16 | } 17 | 18 | export class AuthTestingGuard { 19 | async canActivate(context: ExecutionContext): Promise { 20 | const req = context.switchToHttp().getRequest() 21 | 22 | if (req.headers['test-token']) { 23 | req.user = { 24 | ...mockUser1, 25 | } 26 | req.token = authJWTToken 27 | req.isAuthenticated = true 28 | 29 | return true 30 | } 31 | 32 | throw new UnauthorizedException() 33 | } 34 | } 35 | 36 | export const authPassHeader = { 37 | 'test-token': 1, 38 | } 39 | -------------------------------------------------------------------------------- /apps/core/test/mock/modules/auth.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineProvider } from 'test/helper/defineProvider' 2 | 3 | import { AuthService } from '~/modules/auth/auth.service' 4 | 5 | export const authProvider = defineProvider({ 6 | useValue: {}, 7 | provide: AuthService, 8 | }) 9 | -------------------------------------------------------------------------------- /apps/core/test/mock/modules/comment.mock.ts: -------------------------------------------------------------------------------- 1 | import { dbHelper } from 'test/helper/db-mock.helper' 2 | import { defineProvider } from 'test/helper/defineProvider' 3 | 4 | import { CommentModel } from '~/modules/comment/comment.model' 5 | import { CommentService } from '~/modules/comment/comment.service' 6 | 7 | export const commentProvider = defineProvider({ 8 | provide: CommentService, 9 | useValue: { 10 | model: dbHelper.getModel(CommentModel) as any, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /apps/core/test/mock/modules/config.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineProvider } from 'test/helper/defineProvider' 2 | 3 | import { generateDefaultConfig } from '~/modules/configs/configs.default' 4 | import { ConfigsService } from '~/modules/configs/configs.service' 5 | 6 | export const configProvider = defineProvider({ 7 | provide: ConfigsService, 8 | useValue: { 9 | defaultConfig: generateDefaultConfig(), 10 | async get(key) { 11 | return this.defaultConfig[key] 12 | }, 13 | async getConfig() { 14 | return this.defaultConfig 15 | }, 16 | async waitForConfigReady() { 17 | return this.defaultConfig 18 | }, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /apps/core/test/mock/modules/gateway.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineProviders } from 'test/helper/defineProvider' 2 | 3 | import { AdminEventsGateway } from '~/processors/gateway/admin/events.gateway' 4 | import { WebEventsGateway } from '~/processors/gateway/web/events.gateway' 5 | 6 | export const gatewayProviders = defineProviders([ 7 | { 8 | provide: WebEventsGateway, 9 | useValue: {}, 10 | }, 11 | { 12 | provide: AdminEventsGateway, 13 | useValue: {}, 14 | }, 15 | ]) 16 | -------------------------------------------------------------------------------- /apps/core/test/mock/modules/redis.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineProvider } from 'test/helper/defineProvider' 2 | 3 | import { redisHelper } from '@/helper/redis-mock.helper' 4 | 5 | import { RedisService } from '~/processors/redis/redis.service' 6 | 7 | export const createRedisProvider = async () => 8 | defineProvider({ 9 | provide: RedisService, 10 | useValue: (await redisHelper).RedisService, 11 | }) 12 | -------------------------------------------------------------------------------- /apps/core/test/mock/modules/user.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineProvider } from 'test/helper/defineProvider' 2 | 3 | import { UserService } from '~/modules/user/user.service' 4 | 5 | export const userProvider = defineProvider({ 6 | provide: UserService, 7 | useValue: {}, 8 | }) 9 | -------------------------------------------------------------------------------- /apps/core/test/mock/processors/counting.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineProvider } from 'test/helper/defineProvider' 2 | 3 | import { CountingService } from '~/processors/helper/helper.counting.service' 4 | 5 | const isLikeBeforeMap = {} as Record 6 | 7 | export const countingServiceProvider = defineProvider({ 8 | useValue: { 9 | async updateLikeCount(_, id) { 10 | const isLiked = isLikeBeforeMap[id] 11 | if (isLiked) { 12 | return false 13 | } 14 | isLikeBeforeMap[id] = true 15 | return true 16 | }, 17 | async getThisRecordIsLiked() { 18 | return true 19 | }, 20 | updateReadCount: vi.fn().mockImplementation(async () => { 21 | return 22 | }), 23 | }, 24 | provide: CountingService, 25 | }) 26 | -------------------------------------------------------------------------------- /apps/core/test/mock/processors/email.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineProvider } from 'test/helper/defineProvider' 2 | 3 | import { EmailService } from '~/processors/helper/helper.email.service' 4 | 5 | export const emailProvider = defineProvider({ 6 | provide: EmailService, 7 | useValue: { 8 | async send(options) {}, 9 | render(template, source) { 10 | return '' 11 | }, 12 | sendTestEmail() { 13 | return Promise.resolve() 14 | }, 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /apps/core/test/mock/processors/event.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineProviders } from 'test/helper/defineProvider' 2 | 3 | import { EventEmitter2 } from '@nestjs/event-emitter' 4 | 5 | import { EventManagerService } from '~/processors/helper/helper.event.service' 6 | import { SubPubBridgeService } from '~/processors/redis/subpub.service' 7 | 8 | export const eventEmitterProvider = defineProviders([ 9 | { 10 | provide: EventEmitter2, 11 | useValue: { 12 | emit(event, data) { 13 | return true 14 | }, 15 | }, 16 | }, 17 | { 18 | provide: SubPubBridgeService, 19 | useValue: { 20 | async publish(event, data) {}, 21 | async subscribe(event, callback) {}, 22 | async unsubscribe(event, callback) {}, 23 | }, 24 | }, 25 | { 26 | provide: EventManagerService, 27 | useValue: { 28 | async broadcast(event, data) {}, 29 | async emit() {}, 30 | on() { 31 | return noop 32 | }, 33 | registerHandler() { 34 | return noop 35 | }, 36 | }, 37 | }, 38 | ]) 39 | 40 | const noop = () => {} 41 | -------------------------------------------------------------------------------- /apps/core/test/mock/processors/text-macro.mock.ts: -------------------------------------------------------------------------------- 1 | import { defineProvider } from 'test/helper/defineProvider' 2 | 3 | import { TextMacroService } from '~/processors/helper/helper.macro.service' 4 | 5 | export const textMacroProvider = defineProvider({ 6 | provide: TextMacroService, 7 | useValue: { 8 | async replaceTextMacro(text) { 9 | return text 10 | }, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /apps/core/test/setup-global.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /apps/core/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { mkdirSync } from 'node:fs' 2 | import { MongoMemoryServer } from 'mongodb-memory-server' 3 | import { RedisMemoryServer } from 'redis-memory-server' 4 | 5 | import { 6 | DATA_DIR, 7 | LOG_DIR, 8 | STATIC_FILE_DIR, 9 | TEMP_DIR, 10 | THEME_DIR, 11 | USER_ASSET_DIR, 12 | } from '~/constants/path.constant' 13 | 14 | export async function setup() { 15 | mkdirSync(DATA_DIR, { recursive: true }) 16 | mkdirSync(TEMP_DIR, { recursive: true }) 17 | mkdirSync(LOG_DIR, { recursive: true }) 18 | mkdirSync(USER_ASSET_DIR, { recursive: true }) 19 | mkdirSync(STATIC_FILE_DIR, { recursive: true }) 20 | mkdirSync(THEME_DIR, { recursive: true }) 21 | 22 | // Initialize Redis and MongoDB mock server 23 | await Promise.all([ 24 | RedisMemoryServer.create(), 25 | MongoMemoryServer.create(), 26 | ]).then(async ([redis, db]) => { 27 | await redis.stop() 28 | await db.stop() 29 | }) 30 | } 31 | export async function teardown() {} 32 | -------------------------------------------------------------------------------- /apps/core/test/setupFiles/lifecycle.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { dbHelper } from 'test/helper/db-mock.helper' 3 | import { redisHelper } from 'test/helper/redis-mock.helper' 4 | import { beforeAll } from 'vitest' 5 | 6 | import { registerJSONGlobal } from '~/global/json.global' 7 | 8 | beforeAll(async () => { 9 | global.isDev = true 10 | global.cwd = process.cwd() 11 | global.consola = console 12 | 13 | registerJSONGlobal() 14 | }) 15 | 16 | afterAll(async () => { 17 | await dbHelper.clear() 18 | await dbHelper.close() 19 | await (await redisHelper).close() 20 | }) 21 | 22 | beforeAll(async () => { 23 | await dbHelper.connect() 24 | await redisHelper 25 | }) 26 | 27 | beforeEach(() => { 28 | global.isDev = true 29 | global.cwd = process.cwd() 30 | global.consola = consola 31 | }) 32 | -------------------------------------------------------------------------------- /apps/core/test/src/modules/note/note.e2e-mock.db.ts: -------------------------------------------------------------------------------- 1 | import type { NoteModel } from '~/modules/note/note.model' 2 | 3 | export default Array.from({ length: 20 }).map((_, _i) => { 4 | const i = _i + 1 5 | return { 6 | title: `Note ${i}`, 7 | text: `Content ${i}`, 8 | created: new Date(`2021-03-${i.toFixed().padStart(2, '0')}T00:00:00.000Z`), 9 | modified: null, 10 | allowComment: true, 11 | 12 | hide: false, 13 | commentsIndex: 0, 14 | } 15 | }) as NoteModel[] 16 | -------------------------------------------------------------------------------- /apps/core/test/src/modules/options/options.controller.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { createE2EApp } from 'test/helper/create-e2e-app' 2 | import { authPassHeader } from 'test/mock/guard/auth.guard' 3 | import { configProvider } from 'test/mock/modules/config.mock' 4 | 5 | import { BaseOptionController } from '~/modules/option/controllers/base.option.controller' 6 | 7 | describe('OptionController (e2e)', () => { 8 | const proxy = createE2EApp({ 9 | controllers: [BaseOptionController], 10 | providers: [configProvider], 11 | }) 12 | test('GET /config/jsonschema', () => { 13 | return proxy.app 14 | .inject({ 15 | method: 'GET', 16 | url: '/config/jsonschema', 17 | headers: { 18 | ...authPassHeader, 19 | }, 20 | }) 21 | .then((res) => { 22 | expect(res.statusCode).toBe(200) 23 | const json = res.json() 24 | 25 | expect( 26 | typeof json.properties === 'object' && json.properties, 27 | ).toBeTruthy() 28 | expect(typeof json.default === 'object' && json.default).toBeTruthy() 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /apps/core/test/src/modules/post/post.e2e-mock.db.ts: -------------------------------------------------------------------------------- 1 | import type { CategoryModel } from '~/modules/category/category.model' 2 | import type { PostModel } from '~/modules/post/post.model' 3 | 4 | // @ts-expect-error 5 | export default Array.from({ length: 20 }).map((_, _i) => { 6 | const i = _i + 1 7 | return { 8 | title: `Post ${i}`, 9 | text: `Content ${i}`, 10 | created: new Date(`2021-03-${i.toFixed().padStart(2, '0')}T00:00:00.000Z`), 11 | modified: null, 12 | allowComment: true, 13 | slug: `post-${i}`, 14 | categoryId: '5d367eceaceeed0cabcee4b1', 15 | 16 | commentsIndex: 0, 17 | } 18 | }) as PostModel[] 19 | 20 | export const categoryModels = [ 21 | { 22 | _id: '5d367eceaceeed0cabcee4b1', 23 | id: '5d367eceaceeed0cabcee4b1', 24 | name: 'Category 1', 25 | slug: 'category-1', 26 | }, 27 | ] as (CategoryModel & { _id: string })[] 28 | -------------------------------------------------------------------------------- /apps/core/test/src/transformers/__snapshots__/curd-factor.e2e-spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`BaseCrudFactory > GET /tests 1`] = ` 4 | { 5 | "data": [ 6 | { 7 | "foo": "bar", 8 | "number": 1, 9 | }, 10 | ], 11 | "pagination": { 12 | "current_page": 1, 13 | "has_next_page": false, 14 | "has_prev_page": false, 15 | "size": 10, 16 | "total": 1, 17 | "total_page": 1, 18 | }, 19 | } 20 | `; 21 | 22 | exports[`BaseCrudFactory > POST /tests 1`] = ` 23 | { 24 | "foo": "bar", 25 | "number": 2, 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /apps/core/test/src/utils/case.util.spec.ts: -------------------------------------------------------------------------------- 1 | import snakecaseKeys from 'snakecase-keys' 2 | 3 | test('snakecase', () => { 4 | class A { 5 | a = 1 6 | bA = 2 7 | } 8 | expect(snakecaseKeys(new A() as any)).toEqual({ a: 1, b_a: 2 }) 9 | }) 10 | -------------------------------------------------------------------------------- /apps/core/test/src/utils/pic.util.spec.ts: -------------------------------------------------------------------------------- 1 | import { pickImagesFromMarkdown } from '~/utils/pic.util' 2 | import { sleep } from '~/utils/tool.util' 3 | 4 | describe('src/utils/pic.util', () => { 5 | test('marked ast', async () => { 6 | const res = pickImagesFromMarkdown(` 7 | ![](https://cdn.innei.ren/bed/2021/0813211729.jpeg) 8 | 9 | ![](https://cdn.innei.ren/bed/2021/0813212633.jpg) 10 | `) 11 | // FIXME: ReferenceError: You are trying to import a file after the Jest environment has been torn down 12 | // gifwrap@0.9.2 13 | await sleep(1) 14 | expect(res).toEqual([ 15 | 'https://cdn.innei.ren/bed/2021/0813211729.jpeg', 16 | 'https://cdn.innei.ren/bed/2021/0813212633.jpg', 17 | ]) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /apps/core/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2022", 5 | "lib": [ 6 | "ES2021", 7 | "es2020" 8 | ], 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "baseUrl": "..", 12 | "module": "CommonJS", 13 | "paths": { 14 | "~": [ 15 | "./src" 16 | ], 17 | "~/*": [ 18 | "./src/*" 19 | ], 20 | "@/": [ 21 | "./test" 22 | ], 23 | "@/*": [ 24 | "./test/*" 25 | ] 26 | }, 27 | "resolveJsonModule": true, 28 | "allowJs": true, 29 | "strictNullChecks": false, 30 | "noImplicitAny": false, 31 | "declaration": true, 32 | "noEmit": true, 33 | "outDir": "./dist", 34 | "removeComments": true, 35 | "sourceMap": true, 36 | "allowSyntheticDefaultImports": true, 37 | "esModuleInterop": true, 38 | "skipLibCheck": true 39 | }, 40 | "include": [ 41 | "./src/**/*.ts", 42 | "./src/**/*.tsx", 43 | "./src/**/*.js", 44 | "./src/**/*.jsx", 45 | "./**/*.ts", 46 | ], 47 | } -------------------------------------------------------------------------------- /apps/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "sourceMap": false 6 | }, 7 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2020", 5 | "lib": [ 6 | "ES2021", 7 | "es2020", 8 | "DOM", 9 | "DOM.Iterable" 10 | ], 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "baseUrl": ".", 14 | "module": "CommonJS", 15 | "paths": { 16 | "~": [ 17 | "./src" 18 | ], 19 | "~/*": [ 20 | "./src/*" 21 | ] 22 | }, 23 | "resolveJsonModule": true, 24 | "allowJs": true, 25 | "strictNullChecks": true, 26 | "noImplicitAny": false, 27 | "declaration": false, 28 | "disableSizeLimit": true, 29 | "outDir": "./dist", 30 | "removeComments": true, 31 | "sourceMap": true, 32 | "allowSyntheticDefaultImports": true, 33 | "esModuleInterop": true, 34 | "skipLibCheck": true 35 | }, 36 | "include": [ 37 | "src/**/*", 38 | "*.d.ts" 39 | ], 40 | "exclude": [ 41 | "dist", 42 | "tmp", 43 | "assets/types/type.declare.ts" 44 | ] 45 | } -------------------------------------------------------------------------------- /apps/core/zip-asset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | rm -rf assets/.git 4 | # Copy core/out to $root/out 5 | cp -r ./apps/core/out ./out 6 | 7 | cp -R assets out 8 | # Copy core ecosystem.config.js to $root/out 9 | cp ./apps/core/ecosystem.config.js out 10 | node ./apps/core/download-latest-admin-assets.js 11 | cd out 12 | zip -r ../release.zip ./* 13 | 14 | rm -rf out 15 | -------------------------------------------------------------------------------- /bin/patch.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { resolve } = require('path') 3 | const { readdirSync } = require('fs') 4 | const inquirer = require('inquirer') 5 | 6 | const prompt = inquirer.createPromptModule() 7 | const { $, chalk } = require('zx-cjs') 8 | const package = require('../package.json') 9 | const PATCH_DIR = resolve(process.cwd(), './patch') 10 | 11 | async function bootstarp() { 12 | console.log(chalk.yellowBright('mx-space server patch center')) 13 | 14 | console.log(chalk.yellow(`current version: ${package.version}`)) 15 | 16 | const patchFiles = readdirSync(PATCH_DIR).filter( 17 | (file) => file.startsWith('v') && file.endsWith('.js'), 18 | ) 19 | 20 | prompt({ 21 | type: 'list', 22 | name: 'version', 23 | message: 'Select version you want to patch.', 24 | choices: patchFiles.map((f) => f.replace(/\.js$/, '')), 25 | }).then(async ({ version }) => { 26 | const patchPath = resolve(PATCH_DIR, `./${version}.js`) 27 | console.log(chalk.green(`starting patch... ${patchPath}`)) 28 | await $`node ${patchPath}` 29 | }) 30 | } 31 | 32 | bootstarp() 33 | -------------------------------------------------------------------------------- /configs/nest-cli.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": {} 5 | } 6 | -------------------------------------------------------------------------------- /configs/nginx.conf: -------------------------------------------------------------------------------- 1 | #PROXY-START/ 2 | 3 | location /v2 { 4 | proxy_pass http://127.0.0.1:2333/api/v2; 5 | proxy_set_header Host $host; 6 | proxy_set_header X-Real-IP $remote_addr; 7 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 8 | proxy_set_header REMOTE-HOST $remote_addr; 9 | proxy_set_header Upgrade $http_upgrade; 10 | proxy_set_header Connection "upgrade"; 11 | proxy_set_header Host $host; 12 | 13 | add_header X-Cache $upstream_cache_status; 14 | #Set Nginx Cache 15 | 16 | error_page 444 = @close_connection; 17 | 18 | } 19 | 20 | location @close_connection { 21 | 22 | return 444; 23 | } 24 | 25 | 26 | #PROXY-END/ -------------------------------------------------------------------------------- /configs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | const { resolve } = require('node:path') 4 | module.exports = function (options) { 5 | options.plugins = (options.plugins || []).concat( 6 | new webpack.DefinePlugin({ 7 | isDev: false, 8 | isTest: false, 9 | }), 10 | ) 11 | 12 | options.output = { 13 | filename: 'index.js', 14 | path: resolve(process.cwd(), 'webpack-dist'), 15 | } 16 | return { 17 | ...options, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /debug/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /debug/socket-admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

11 |   
12 |   
17 |   
39 | 
40 | 


--------------------------------------------------------------------------------
/debug/socket.io.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |     
 6 |     
 7 |     Document
 8 |   
 9 |   
10 |     

11 |   
12 |   
17 | 
18 |   
36 | 
37 | 


--------------------------------------------------------------------------------
/external/pino/index.js:
--------------------------------------------------------------------------------
 1 | // why this, because we dont need pino logger, and this logger can not bundle whole package into only one file with ncc.
 2 | // only work with fastify v4+ with pino v8+
 3 | 
 4 | module.exports = {
 5 |   symbols: {
 6 |     // https://github.com/pinojs/pino/blob/master/lib/symbols.js
 7 |     serializersSym: Symbol.for('pino.serializers'),
 8 |   },
 9 |   stdSerializers: {
10 |     error: function asErrValue(err) {
11 |       const obj = {
12 |         type: err.constructor.name,
13 |         msg: err.message,
14 |         stack: err.stack,
15 |       }
16 |       for (const key in err) {
17 |         if (obj[key] === undefined) {
18 |           obj[key] = err[key]
19 |         }
20 |       }
21 |       return obj
22 |     },
23 |   },
24 | }
25 | 


--------------------------------------------------------------------------------
/external/pino/package.json:
--------------------------------------------------------------------------------
1 | {
2 |   "name": "pino",
3 |   "main": "./index.js"
4 | }
5 | 


--------------------------------------------------------------------------------
/external/readme.md:
--------------------------------------------------------------------------------
1 | This is folder used to override dependencies.


--------------------------------------------------------------------------------
/external/request/index.js:
--------------------------------------------------------------------------------
1 | // why do this.
2 | // we dont need request, because we get image color only by buffer, and we dont need send request.
3 | module.exports = {}
4 | 


--------------------------------------------------------------------------------
/external/request/package.json:
--------------------------------------------------------------------------------
1 | {
2 |   "name": "request",
3 |   "main": "index.js"
4 | }
5 | 


--------------------------------------------------------------------------------
/packages/api-client/.gitignore:
--------------------------------------------------------------------------------
1 | esm
2 | lib
3 | dist
4 | build
5 | 
6 | types
7 | 


--------------------------------------------------------------------------------
/packages/api-client/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | __tests__
3 | rollup.config.js
4 | adaptors/*
5 | core/*
6 | utils/*
7 | 


--------------------------------------------------------------------------------
/packages/api-client/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org
2 | 
3 | strict-peer-dependencies=false
4 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/adaptors/axios.spec.ts:
--------------------------------------------------------------------------------
1 | import { axiosAdaptor } from '~/adaptors/axios'
2 | 
3 | import { testAdaptor } from '../helpers/adaptor-test'
4 | 
5 | describe('test axios adaptor', () => {
6 |   testAdaptor(axiosAdaptor)
7 | })
8 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/adaptors/fetch.spec.ts:
--------------------------------------------------------------------------------
 1 | import FormData from 'form-data'
 2 | 
 3 | import { fetchAdaptor } from '~/adaptors/fetch'
 4 | 
 5 | import { testAdaptor } from '../helpers/adaptor-test'
 6 | 
 7 | describe('test fetch adaptor', () => {
 8 |   beforeAll(() => {
 9 |     global.FormData = FormData as any
10 |   })
11 |   testAdaptor(fetchAdaptor)
12 | })
13 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/adaptors/umi-request.spec.ts:
--------------------------------------------------------------------------------
1 | import { umiAdaptor } from '~/adaptors/umi-request'
2 | 
3 | import { testAdaptor } from '../helpers/adaptor-test'
4 | 
5 | describe('test umi-request adaptor', () => {
6 |   testAdaptor(umiAdaptor)
7 | })
8 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/controllers/act.test.ts:
--------------------------------------------------------------------------------
 1 | import { mockRequestInstance } from '~/__tests__/helpers/instance'
 2 | import { mockResponse } from '~/__tests__/helpers/response'
 3 | import { AckController } from '~/controllers'
 4 | 
 5 | describe('test ack client', () => {
 6 |   const client = mockRequestInstance(AckController)
 7 | 
 8 |   test('POST /ack', async () => {
 9 |     mockResponse('/ack', {}, 'post', {
10 |       type: 'read',
11 |       payload: {
12 |         type: 'note',
13 |         id: '11',
14 |       },
15 |     })
16 | 
17 |     await expect(client.ack.read('note', '11')).resolves.not.toThrowError()
18 |   })
19 | })
20 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/controllers/ai.test.ts:
--------------------------------------------------------------------------------
 1 | import { mockRequestInstance } from '~/__tests__/helpers/instance'
 2 | import { mockResponse } from '~/__tests__/helpers/response'
 3 | import { AIController } from '~/controllers'
 4 | 
 5 | describe('test ai client', () => {
 6 |   const client = mockRequestInstance(AIController)
 7 | 
 8 |   test('POST /generate-summary', async () => {
 9 |     mockResponse('/ai/summaries/generate', {}, 'post', {
10 |       lang: 'zh-CN',
11 |       refId: '11',
12 |     })
13 | 
14 |     await expect(
15 |       client.ai.generateSummary('11', 'zh-CN'),
16 |     ).resolves.not.toThrowError()
17 |   })
18 | 
19 |   test('GET /summary/:id', async () => {
20 |     mockResponse(
21 |       '/ai/summaries/article/11?articleId=11&lang=zh-CN&onlyDb=true',
22 |       {},
23 |       'get',
24 |     )
25 | 
26 |     await expect(
27 |       client.ai.getSummary({
28 |         articleId: '11',
29 |         lang: 'zh-CN',
30 |         onlyDb: true,
31 |       }),
32 |     ).resolves.not.toThrowError()
33 |   })
34 | })
35 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/controllers/serverless.test.ts:
--------------------------------------------------------------------------------
 1 | import { mockRequestInstance } from '~/__tests__/helpers/instance'
 2 | import { mockResponse } from '~/__tests__/helpers/response'
 3 | import { ServerlessController } from '~/controllers'
 4 | 
 5 | describe('test Snippet client', () => {
 6 |   const client = mockRequestInstance(ServerlessController)
 7 | 
 8 |   test('GET /:reference/:name', async () => {
 9 |     const mocked = mockResponse('/serverless/api/ping', { message: 'pong' })
10 | 
11 |     const data = await client.serverless.getByReferenceAndName<{}>(
12 |       'api',
13 |       'ping',
14 |     )
15 | 
16 |     expect(data).toEqual(mocked)
17 |     expect(data.$raw.data).toEqual(mocked)
18 |   })
19 | })
20 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/controllers/topic.test.ts:
--------------------------------------------------------------------------------
 1 | import camelcaseKeys from 'camelcase-keys'
 2 | 
 3 | import { mockRequestInstance } from '~/__tests__/helpers/instance'
 4 | import { mockResponse } from '~/__tests__/helpers/response'
 5 | import { TopicController } from '~/controllers/topic'
 6 | 
 7 | describe('test topic client', () => {
 8 |   const client = mockRequestInstance(TopicController)
 9 | 
10 |   test('GET /topics/slug/:slug', async () => {
11 |     const mocked = mockResponse('/topics/slug/111', {
12 |       name: 'name-topic',
13 |     })
14 |     const data = await client.topic.getTopicBySlug('111')
15 |     expect(data).toEqual(camelcaseKeys(mocked))
16 |   })
17 | })
18 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/helpers/e2e-mock-server.ts:
--------------------------------------------------------------------------------
 1 | import cors from 'cors'
 2 | import express from 'express'
 3 | import type { AddressInfo } from 'node:net'
 4 | 
 5 | type Express = ReturnType
 6 | export const createMockServer = (options: { port?: number } = {}) => {
 7 |   const { port = 0 } = options
 8 | 
 9 |   const app: Express = express()
10 |   app.use(express.json())
11 |   app.use(cors())
12 |   const server = app.listen(port)
13 | 
14 |   return {
15 |     app,
16 |     port: (server.address() as AddressInfo).port,
17 |     server,
18 |     close() {
19 |       server.close()
20 |     },
21 |   }
22 | }
23 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/helpers/global-fetch.ts:
--------------------------------------------------------------------------------
 1 | // @ts-nocheck
 2 | 
 3 | import AbortController from 'abort-controller'
 4 | 
 5 | const TEN_MEGABYTES = 1000 * 1000 * 10
 6 | 
 7 | if (!globalThis.fetch) {
 8 |   globalThis.fetch = (url, options) =>
 9 |     fetch(url, { highWaterMark: TEN_MEGABYTES, ...options })
10 | }
11 | 
12 | if (!globalThis.Headers) {
13 |   globalThis.Headers = Headers
14 | }
15 | 
16 | if (!globalThis.Request) {
17 |   globalThis.Request = Request
18 | }
19 | 
20 | if (!globalThis.Response) {
21 |   globalThis.Response = Response
22 | }
23 | 
24 | if (!globalThis.AbortController) {
25 |   globalThis.AbortController = AbortController
26 | }
27 | 
28 | if (!globalThis.ReadableStream) {
29 |   try {
30 |     globalThis.ReadableStream = await import(
31 |       'web-streams-polyfill/ponyfill/es2018'
32 |     )
33 |   } catch {}
34 | }
35 | 
36 | export {}
37 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/helpers/instance.ts:
--------------------------------------------------------------------------------
 1 | import type { HTTPClient } from '~/core'
 2 | import type { IController } from '~/interfaces/controller'
 3 | 
 4 | import { axiosAdaptor } from '~/adaptors/axios'
 5 | import { createClient } from '~/core'
 6 | 
 7 | export const mockRequestInstance = (
 8 |   injectController: new (client: HTTPClient) => IController,
 9 | ) => {
10 |   const client = createClient(axiosAdaptor)('https://api.innei.ren/v2')
11 |   client.injectControllers(injectController)
12 |   return client
13 | }
14 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/utils/auto-bind.spec.ts:
--------------------------------------------------------------------------------
 1 | import { autoBind } from '~/utils/auto-bind'
 2 | 
 3 | describe('test auto bind', () => {
 4 |   it('should bind in class', () => {
 5 |     class A {
 6 |       constructor() {
 7 |         autoBind(this)
 8 |       }
 9 |       name = 'A'
10 |       foo() {
11 |         return this?.name
12 |       }
13 |     }
14 | 
15 |     expect(new A().foo()).toBe('A')
16 | 
17 |     function tester(caller: any) {
18 |       return caller()
19 |     }
20 |     expect(tester(new A().foo)).toBe('A')
21 | 
22 |     function tester2 any>(caller: T) {
23 |       return caller.call({})
24 |     }
25 |     expect(tester2(new A().foo)).toBe('A')
26 |   })
27 | })
28 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/utils/index.test.ts:
--------------------------------------------------------------------------------
 1 | import { destructureData } from '~/utils'
 2 | 
 3 | describe('test utils', () => {
 4 |   test('destructureData', () => {
 5 |     const d = destructureData({ data: { a: 1, b: 2 } })
 6 |     expect(d).toEqual({ a: 1, b: 2 })
 7 | 
 8 |     const d2 = destructureData({ data: { a: 1, b: 2 }, c: 3 })
 9 |     expect(d2).toEqual({ data: { a: 1, b: 2 }, c: 3 })
10 | 
11 |     const d3 = destructureData({ data: [{ a: 1 }] })
12 |     expect(d3).toEqual({ data: [{ a: 1 }] })
13 | 
14 |     const d4 = destructureData({ a: 1 })
15 |     expect(d4).toEqual({ a: 1 })
16 | 
17 |     const d5 = destructureData([])
18 |     expect(d5).toEqual([])
19 | 
20 |     const d6 = destructureData({ data: [] })
21 |     expect(d6).toEqual({ data: [] })
22 | 
23 |     const d7 = destructureData(
24 |       (() => {
25 |         const d = { data: { a: 1 } }
26 |         Object.defineProperty(d, '$raw', { value: { a: 1 }, enumerable: false })
27 |         return d
28 |       })(),
29 |     )
30 |     expect(d7).toEqual({ a: 1 })
31 |     expect(d7.$raw).toBeTruthy()
32 |   })
33 | })
34 | 


--------------------------------------------------------------------------------
/packages/api-client/__tests__/utils/path.spec.ts:
--------------------------------------------------------------------------------
1 | import { resolveFullPath } from '~/utils/path'
2 | 
3 | describe('TEST path.utils', () => {
4 |   it('should resolve full path', () => {
5 |     const path = resolveFullPath('http://localhost:3000', '/api/v1/users')
6 |     expect(path).toBe('http://localhost:3000/api/v1/users')
7 |   })
8 | })
9 | 


--------------------------------------------------------------------------------
/packages/api-client/adaptors/umi-request.ts:
--------------------------------------------------------------------------------
 1 | import { extend } from 'umi-request'
 2 | import type { IRequestAdapter } from '~/interfaces/adapter'
 3 | import type { RequestMethod, RequestResponse } from 'umi-request'
 4 | 
 5 | const $http = /*#__PURE__*/ extend({
 6 |   getResponse: true,
 7 |   requestType: 'json',
 8 |   responseType: 'json',
 9 | })
10 | 
11 | export const umiAdaptor: IRequestAdapter<
12 |   RequestMethod,
13 |   RequestResponse
14 | > = Object.preventExtensions({
15 |   get default() {
16 |     return $http
17 |   },
18 |   responseWrapper: {} as any as RequestResponse,
19 |   get(url, options) {
20 |     return $http.get(url, options)
21 |   },
22 |   post(url, options) {
23 |     return $http.post(url, options)
24 |   },
25 |   put(url, options) {
26 |     return $http.put(url, options)
27 |   },
28 |   delete(url, options) {
29 |     return $http.delete(url, options)
30 |   },
31 |   patch(url, options) {
32 |     return $http.patch(url, options)
33 |   },
34 | })
35 | 
36 | // eslint-disable-next-line import/no-default-export
37 | export default umiAdaptor
38 | 


--------------------------------------------------------------------------------
/packages/api-client/controllers/project.ts:
--------------------------------------------------------------------------------
 1 | import type { IRequestAdapter } from '~/interfaces/adapter'
 2 | import type { ProjectModel } from '~/models/project'
 3 | import type { HTTPClient } from '../core'
 4 | 
 5 | import { autoBind } from '~/utils/auto-bind'
 6 | 
 7 | import { BaseCrudController } from './base'
 8 | 
 9 | declare module '../core/client' {
10 |   interface HTTPClient<
11 |     T extends IRequestAdapter = IRequestAdapter,
12 |     ResponseWrapper = unknown,
13 |   > {
14 |     project: ProjectController
15 |   }
16 | }
17 | 
18 | export class ProjectController extends BaseCrudController<
19 |   ProjectModel,
20 |   ResponseWrapper
21 | > {
22 |   constructor(protected readonly client: HTTPClient) {
23 |     super(client)
24 |     autoBind(this)
25 |   }
26 | 
27 |   base = 'projects'
28 |   name = 'project'
29 | }
30 | 


--------------------------------------------------------------------------------
/packages/api-client/controllers/severless.ts:
--------------------------------------------------------------------------------
 1 | import type { IRequestAdapter } from '~/interfaces/adapter'
 2 | import type { IController } from '~/interfaces/controller'
 3 | import type { IRequestHandler } from '~/interfaces/request'
 4 | import type { HTTPClient } from '../core'
 5 | 
 6 | import { autoBind } from '~/utils/auto-bind'
 7 | 
 8 | declare module '../core/client' {
 9 |   interface HTTPClient<
10 |     T extends IRequestAdapter = IRequestAdapter,
11 |     ResponseWrapper = unknown,
12 |   > {
13 |     serverless: ServerlessController
14 |   }
15 | }
16 | 
17 | export class ServerlessController implements IController {
18 |   base = 'serverless'
19 |   name = 'serverless'
20 | 
21 |   constructor(protected client: HTTPClient) {
22 |     autoBind(this)
23 |   }
24 | 
25 |   get proxy(): IRequestHandler {
26 |     return this.client.proxy(this.base)
27 |   }
28 | 
29 |   getByReferenceAndName(reference: string, name: string) {
30 |     return this.proxy(reference)(name).get()
31 |   }
32 | }
33 | 


--------------------------------------------------------------------------------
/packages/api-client/controllers/snippet.ts:
--------------------------------------------------------------------------------
 1 | import type { IRequestAdapter } from '~/interfaces/adapter'
 2 | import type { IController } from '~/interfaces/controller'
 3 | import type { IRequestHandler } from '~/interfaces/request'
 4 | import type { HTTPClient } from '../core'
 5 | 
 6 | import { autoBind } from '~/utils/auto-bind'
 7 | 
 8 | declare module '../core/client' {
 9 |   interface HTTPClient<
10 |     T extends IRequestAdapter = IRequestAdapter,
11 |     ResponseWrapper = unknown,
12 |   > {
13 |     snippet: SnippetController
14 |   }
15 | }
16 | 
17 | export class SnippetController implements IController {
18 |   base = 'snippets'
19 |   name = 'snippet'
20 | 
21 |   constructor(protected client: HTTPClient) {
22 |     autoBind(this)
23 |   }
24 | 
25 |   get proxy(): IRequestHandler {
26 |     return this.client.proxy(this.base)
27 |   }
28 | 
29 |   // getById(id: string) {
30 |   //   return this.proxy(id).get>()
31 |   // }
32 | 
33 |   getByReferenceAndName(reference: string, name: string) {
34 |     return this.proxy(reference)(name).get()
35 |   }
36 | }
37 | 


--------------------------------------------------------------------------------
/packages/api-client/core/error.ts:
--------------------------------------------------------------------------------
 1 | /* eslint-disable unicorn/custom-error-definition */
 2 | export class RequestError extends Error {
 3 |   constructor(
 4 |     message: string,
 5 |     public status: number,
 6 |     public path: string,
 7 |     public raw: any,
 8 |   ) {
 9 |     super(message)
10 |   }
11 | }
12 | 


--------------------------------------------------------------------------------
/packages/api-client/core/index.ts:
--------------------------------------------------------------------------------
1 | export * from './client'
2 | export * from './error'
3 | 


--------------------------------------------------------------------------------
/packages/api-client/dtos/comment.ts:
--------------------------------------------------------------------------------
 1 | export interface CommentDto {
 2 |   author: string
 3 | 
 4 |   text: string
 5 | 
 6 |   mail: string
 7 | 
 8 |   url?: string
 9 | 
10 |   source?: 'github' | 'google'
11 |   avatar?: string
12 | }
13 | 


--------------------------------------------------------------------------------
/packages/api-client/dtos/index.ts:
--------------------------------------------------------------------------------
1 | export * from './comment'
2 | 


--------------------------------------------------------------------------------
/packages/api-client/index.ts:
--------------------------------------------------------------------------------
 1 | import { createClient } from './core'
 2 | 
 3 | export * from './controllers'
 4 | export * from './models'
 5 | export * from './dtos'
 6 | 
 7 | export { createClient, RequestError } from './core'
 8 | export type { HTTPClient } from './core'
 9 | export { camelcaseKeys as simpleCamelcaseKeys } from './utils/camelcase-keys'
10 | 
11 | export default createClient
12 | export type { IRequestAdapter } from './interfaces/adapter'
13 | 


--------------------------------------------------------------------------------
/packages/api-client/interfaces/adapter.ts:
--------------------------------------------------------------------------------
 1 | import type { RequestOptions } from './instance'
 2 | 
 3 | export type IAdaptorRequestResponseType

= Promise< 4 | Record & { data: P } 5 | > 6 | 7 | export type IRequestAdapter = Readonly< 8 | (Response extends undefined ? {} : { responseWrapper: Response }) & { 9 | default: T 10 | 11 | get:

( 12 | url: string, 13 | options?: Omit, 14 | ) => IAdaptorRequestResponseType

15 | 16 | post:

( 17 | url: string, 18 | options: Partial, 19 | ) => IAdaptorRequestResponseType

20 | 21 | patch:

( 22 | url: string, 23 | options: Partial, 24 | ) => IAdaptorRequestResponseType

25 | 26 | delete:

( 27 | url: string, 28 | options?: Omit, 29 | ) => IAdaptorRequestResponseType

30 | 31 | put:

( 32 | url: string, 33 | options: Partial, 34 | ) => IAdaptorRequestResponseType

35 | } 36 | > 37 | -------------------------------------------------------------------------------- /packages/api-client/interfaces/client.ts: -------------------------------------------------------------------------------- 1 | import type { IController } from './controller' 2 | import type { Class } from './types' 3 | 4 | interface IClientOptions { 5 | controllers: Class[] 6 | getCodeMessageFromException: ( 7 | error: T, 8 | ) => { 9 | message?: string | undefined | null 10 | code?: number | undefined | null 11 | } 12 | customThrowResponseError: (err: any) => T 13 | transformResponse: (data: any) => T 14 | /** 15 | * 16 | * @default (res) => res.data 17 | */ 18 | getDataFromResponse: (response: unknown) => T 19 | } 20 | export type ClientOptions = Partial 21 | -------------------------------------------------------------------------------- /packages/api-client/interfaces/controller.ts: -------------------------------------------------------------------------------- 1 | export interface IController { 2 | base: string 3 | 4 | name: string | string[] 5 | } 6 | -------------------------------------------------------------------------------- /packages/api-client/interfaces/instance.ts: -------------------------------------------------------------------------------- 1 | export interface RequestOptions { 2 | method?: string 3 | data?: Record 4 | params?: Record | URLSearchParams 5 | headers?: Record 6 | 7 | [key: string]: any 8 | } 9 | -------------------------------------------------------------------------------- /packages/api-client/interfaces/options.ts: -------------------------------------------------------------------------------- 1 | export type SortOrder = 'asc' | 'desc' 2 | -------------------------------------------------------------------------------- /packages/api-client/interfaces/params.ts: -------------------------------------------------------------------------------- 1 | export interface PaginationParams { 2 | size?: number 3 | page?: number 4 | } 5 | -------------------------------------------------------------------------------- /packages/api-client/interfaces/types.ts: -------------------------------------------------------------------------------- 1 | export type Class = new (...args: any[]) => T 2 | 3 | export type SelectFields = `${'+' | '-' | ''}${T}`[] 4 | -------------------------------------------------------------------------------- /packages/api-client/mod-dts.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'node:fs' 2 | import path from 'node:path' 3 | 4 | const __dirname = new URL(import.meta.url).pathname.replace(/\/[^/]*$/, '') 5 | const PKG = JSON.parse(readFileSync(path.resolve(__dirname, './package.json'))) 6 | 7 | const dts = path.resolve(__dirname, './dist/index.d.ts') 8 | const content = readFileSync(dts, 'utf-8') 9 | 10 | // replace declare module '../core/client' 11 | // with declare module '@mx-space/api-client' 12 | writeFileSync( 13 | dts, 14 | content.replaceAll( 15 | /declare module '..\/core\/client'/g, 16 | 'declare module ' + `'${PKG.name}'`, 17 | ), 18 | ) 19 | -------------------------------------------------------------------------------- /packages/api-client/models/ai.ts: -------------------------------------------------------------------------------- 1 | export interface AISummaryModel { 2 | id: string 3 | created: string 4 | summary: string 5 | hash: string 6 | refId: string 7 | lang: string 8 | } 9 | 10 | export interface AIDeepReadingModel { 11 | id: string 12 | hash: string 13 | refId: string 14 | keyPoints: string[] 15 | criticalAnalysis: string 16 | content: string 17 | } 18 | -------------------------------------------------------------------------------- /packages/api-client/models/auth.ts: -------------------------------------------------------------------------------- 1 | export interface AuthUser { 2 | id: string 3 | email: string 4 | isOwner: boolean 5 | image: string 6 | name: string 7 | provider: string 8 | handle: string 9 | } 10 | -------------------------------------------------------------------------------- /packages/api-client/models/base.ts: -------------------------------------------------------------------------------- 1 | export interface Count { 2 | read: number 3 | like: number 4 | } 5 | 6 | export interface Image { 7 | height: number 8 | width: number 9 | type: string 10 | accent?: string 11 | src: string 12 | blurHash?: string 13 | } 14 | 15 | export interface Pager { 16 | total: number 17 | size: number 18 | currentPage: number 19 | totalPage: number 20 | hasPrevPage: boolean 21 | hasNextPage: boolean 22 | } 23 | 24 | export interface PaginateResult { 25 | data: T[] 26 | pagination: Pager 27 | } 28 | 29 | export interface BaseModel { 30 | created: string 31 | id: string 32 | } 33 | 34 | export interface BaseCommentIndexModel extends BaseModel { 35 | commentsIndex?: number 36 | 37 | allowComment: boolean 38 | } 39 | export interface TextBaseModel extends BaseCommentIndexModel { 40 | title: string 41 | text: string 42 | images?: Image[] 43 | modified: string | null 44 | } 45 | 46 | export type ModelWithLiked = T & { 47 | liked: boolean 48 | } 49 | -------------------------------------------------------------------------------- /packages/api-client/models/category.ts: -------------------------------------------------------------------------------- 1 | import type { BaseModel } from './base' 2 | import type { PostModel } from './post' 3 | 4 | export enum CategoryType { 5 | Category, 6 | Tag, 7 | } 8 | 9 | export interface CategoryModel extends BaseModel { 10 | type: CategoryType 11 | count: number 12 | slug: string 13 | name: string 14 | } 15 | export type CategoryWithChildrenModel = CategoryModel & { 16 | children: Pick[] 17 | } 18 | 19 | export type CategoryEntries = { 20 | entries: Record 21 | } 22 | export interface TagModel { 23 | count: number 24 | name: string 25 | } 26 | -------------------------------------------------------------------------------- /packages/api-client/models/comment.ts: -------------------------------------------------------------------------------- 1 | import type { BaseModel } from './base' 2 | import type { CategoryModel } from './category' 3 | 4 | import { CollectionRefTypes } from '@core/constants/db.constant' 5 | 6 | export { CollectionRefTypes } 7 | export interface CommentModel extends BaseModel { 8 | refType: CollectionRefTypes 9 | ref: string 10 | state: number 11 | commentsIndex: number 12 | author: string 13 | text: string 14 | mail?: string 15 | url?: string 16 | ip?: string 17 | agent?: string 18 | key: string 19 | pin?: boolean 20 | 21 | avatar: string 22 | 23 | parent?: CommentModel | string 24 | children: CommentModel[] 25 | 26 | isWhispers?: boolean 27 | location?: string 28 | 29 | source?: string 30 | readerId?: string 31 | editedAt?: string 32 | } 33 | export interface CommentRef { 34 | id: string 35 | categoryId?: string 36 | slug: string 37 | title: string 38 | category?: CategoryModel 39 | } 40 | 41 | export enum CommentState { 42 | Unread, 43 | Read, 44 | Junk, 45 | } 46 | -------------------------------------------------------------------------------- /packages/api-client/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './activity' 2 | export * from './aggregate' 3 | export * from './ai' 4 | export * from './auth' 5 | export * from './base' 6 | export * from './category' 7 | export * from './comment' 8 | export * from './link' 9 | export * from './note' 10 | export * from './page' 11 | export * from './post' 12 | export * from './project' 13 | export * from './reader' 14 | export * from './recently' 15 | export * from './say' 16 | export * from './setting' 17 | export * from './snippet' 18 | export * from './subscribe' 19 | export * from './topic' 20 | export * from './user' 21 | -------------------------------------------------------------------------------- /packages/api-client/models/link.ts: -------------------------------------------------------------------------------- 1 | import type { BaseModel } from './base' 2 | 3 | export enum LinkType { 4 | Friend, 5 | Collection, 6 | } 7 | 8 | export enum LinkState { 9 | Pass, 10 | Audit, 11 | Outdate, 12 | Banned, 13 | Reject, 14 | } 15 | 16 | export interface LinkModel extends BaseModel { 17 | name: string 18 | url: string 19 | avatar: string 20 | description?: string 21 | type: LinkType 22 | state: LinkState 23 | hide: boolean 24 | email: string 25 | } 26 | -------------------------------------------------------------------------------- /packages/api-client/models/note.ts: -------------------------------------------------------------------------------- 1 | import type { ModelWithLiked, TextBaseModel } from './base' 2 | import type { TopicModel } from './topic' 3 | 4 | export interface NoteModel extends TextBaseModel { 5 | hide: boolean 6 | count: { 7 | read: number 8 | like: number 9 | } 10 | 11 | mood?: string 12 | weather?: string 13 | bookmark?: boolean 14 | 15 | publicAt?: Date 16 | password?: string | null 17 | nid: number 18 | 19 | location?: string 20 | 21 | coordinates?: Coordinate 22 | topic?: TopicModel 23 | topicId?: string 24 | } 25 | 26 | export interface Coordinate { 27 | latitude: number 28 | longitude: number 29 | } 30 | 31 | export interface NoteWrappedPayload { 32 | data: NoteModel 33 | next?: Partial 34 | prev?: Partial 35 | } 36 | 37 | export interface NoteWrappedWithLikedPayload { 38 | data: ModelWithLiked 39 | next?: Partial 40 | prev?: Partial 41 | } 42 | -------------------------------------------------------------------------------- /packages/api-client/models/page.ts: -------------------------------------------------------------------------------- 1 | import type { TextBaseModel } from './base' 2 | 3 | export enum EnumPageType { 4 | 'md' = 'md', 5 | 'html' = 'html', 6 | 'frame' = 'frame', 7 | } 8 | export interface PageModel extends TextBaseModel { 9 | created: string 10 | 11 | slug: string 12 | 13 | subtitle?: string 14 | 15 | order?: number 16 | 17 | type?: EnumPageType 18 | 19 | options?: object 20 | } 21 | -------------------------------------------------------------------------------- /packages/api-client/models/post.ts: -------------------------------------------------------------------------------- 1 | import type { Count, Image, TextBaseModel } from './base' 2 | import type { CategoryModel } from './category' 3 | 4 | export interface PostModel extends TextBaseModel { 5 | summary?: string | null 6 | copyright: boolean 7 | tags: string[] 8 | count: Count 9 | text: string 10 | title: string 11 | slug: string 12 | categoryId: string 13 | images: Image[] 14 | category: CategoryModel 15 | pin?: string | null 16 | pinOrder?: number 17 | related?: Pick< 18 | PostModel, 19 | | 'id' 20 | | 'category' 21 | | 'categoryId' 22 | | 'created' 23 | | 'modified' 24 | | 'title' 25 | | 'slug' 26 | | 'summary' 27 | >[] 28 | } 29 | -------------------------------------------------------------------------------- /packages/api-client/models/project.ts: -------------------------------------------------------------------------------- 1 | import type { BaseModel } from './base' 2 | 3 | export interface ProjectModel extends BaseModel { 4 | name: string 5 | previewUrl?: string 6 | docUrl?: string 7 | projectUrl?: string 8 | images?: string[] 9 | description: string 10 | avatar?: string 11 | text: string 12 | } 13 | -------------------------------------------------------------------------------- /packages/api-client/models/reader.ts: -------------------------------------------------------------------------------- 1 | export interface ReaderModel { 2 | email: string 3 | name: string 4 | handle: string 5 | 6 | image: string 7 | 8 | isOwner: boolean 9 | } 10 | -------------------------------------------------------------------------------- /packages/api-client/models/recently.ts: -------------------------------------------------------------------------------- 1 | import type { BaseCommentIndexModel } from './base' 2 | 3 | export enum RecentlyRefTypes { 4 | Post = 'Post', 5 | Note = 'Note', 6 | Page = 'Page', 7 | } 8 | 9 | export type RecentlyRefType = { 10 | title: string 11 | url: string 12 | } 13 | export interface RecentlyModel extends BaseCommentIndexModel { 14 | content: string 15 | 16 | ref?: RecentlyRefType & { [key: string]: any } 17 | refId?: string 18 | refType?: RecentlyRefTypes 19 | 20 | up: number 21 | down: number 22 | } 23 | -------------------------------------------------------------------------------- /packages/api-client/models/say.ts: -------------------------------------------------------------------------------- 1 | import type { BaseModel } from './base' 2 | 3 | export interface SayModel extends BaseModel { 4 | text: string 5 | source?: string 6 | author?: string 7 | } 8 | -------------------------------------------------------------------------------- /packages/api-client/models/snippet.ts: -------------------------------------------------------------------------------- 1 | import type { BaseModel } from './base' 2 | 3 | export enum SnippetType { 4 | JSON = 'json', 5 | Function = 'function', 6 | Text = 'text', 7 | YAML = 'yaml', 8 | } 9 | export interface SnippetModel extends BaseModel { 10 | type: SnippetType 11 | private: boolean 12 | raw: string 13 | name: string 14 | reference: string 15 | comment?: string 16 | metatype?: string 17 | schema?: string 18 | data: T 19 | } 20 | -------------------------------------------------------------------------------- /packages/api-client/models/subscribe.ts: -------------------------------------------------------------------------------- 1 | import type { SubscribeTypeToBitMap } from '@core/modules/subscribe/subscribe.constant' 2 | 3 | export * from '@core/modules/subscribe/subscribe.constant' 4 | 5 | export type SubscribeType = keyof typeof SubscribeTypeToBitMap 6 | -------------------------------------------------------------------------------- /packages/api-client/models/topic.ts: -------------------------------------------------------------------------------- 1 | import type { BaseModel } from './base' 2 | 3 | export interface TopicModel extends BaseModel { 4 | description?: string 5 | introduce: string 6 | name: string 7 | slug: string 8 | icon?: string 9 | } 10 | -------------------------------------------------------------------------------- /packages/api-client/models/user.ts: -------------------------------------------------------------------------------- 1 | import type { BaseModel } from './base' 2 | 3 | export interface UserModel extends BaseModel { 4 | introduce: string 5 | mail: string 6 | url: string 7 | name: string 8 | socialIds: Record 9 | username: string 10 | modified: string 11 | v: number 12 | lastLoginTime: string 13 | lastLoginIp?: string 14 | avatar: string 15 | postID: string 16 | } 17 | 18 | export type TLogin = { 19 | token: string 20 | expiresIn: number 21 | // 登陆足迹 22 | lastLoginTime: null | string 23 | lastLoginIp?: null | string 24 | } & Pick< 25 | UserModel, 26 | 'name' | 'username' | 'created' | 'url' | 'mail' | 'avatar' | 'id' 27 | > 28 | -------------------------------------------------------------------------------- /packages/api-client/test.d.ts: -------------------------------------------------------------------------------- 1 | import 'vitest/globals' 2 | 3 | export {} 4 | -------------------------------------------------------------------------------- /packages/api-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "jsx": "react", 5 | "lib": [ 6 | "ESNext", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "baseUrl": ".", 11 | "module": "ESNext", 12 | "moduleResolution": "node", 13 | "paths": { 14 | "~/*": [ 15 | "*" 16 | ], 17 | "@core/*": [ 18 | "../../apps/core/src/*" 19 | ], 20 | }, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "declaration": true, 24 | "outDir": "./esm", 25 | "sourceMap": true, 26 | "esModuleInterop": true, 27 | "forceConsistentCasingInFileNames": true, 28 | "skipLibCheck": true 29 | }, 30 | "exclude": [ 31 | "esm/*", 32 | "build/*", 33 | "node_modules/*", 34 | "lib/*", 35 | "dist/**" 36 | ] 37 | } -------------------------------------------------------------------------------- /packages/api-client/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from 'node:fs' 2 | import path from 'node:path' 3 | import { defineConfig } from 'tsup' 4 | 5 | const __dirname = new URL(import.meta.url).pathname.replace(/\/[^/]*$/, '') 6 | 7 | const adaptorNames = readdirSync(path.resolve(__dirname, './adaptors')).map( 8 | (i) => path.parse(i).name, 9 | ) 10 | 11 | export default defineConfig({ 12 | clean: true, 13 | target: 'es2020', 14 | entry: ['index.ts', ...adaptorNames.map((name) => `adaptors/${name}.ts`)], 15 | external: adaptorNames, 16 | dts: true, 17 | format: ['cjs', 'esm', 'iife'], 18 | }) 19 | -------------------------------------------------------------------------------- /packages/api-client/utils/camelcase-keys.ts: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from '.' 2 | 3 | /** 4 | * A simple camelCase function that only handles strings, but not handling symbol, date, or other complex case. 5 | * If you need to handle more complex cases, please use camelcase-keys package. 6 | */ 7 | export const camelcaseKeys = (obj: any): T => { 8 | if (Array.isArray(obj)) { 9 | return obj.map((x) => camelcaseKeys(x)) as any 10 | } 11 | 12 | if (isPlainObject(obj)) { 13 | return Object.keys(obj).reduce((result: any, key) => { 14 | const nextKey = isMongoId(key) ? key : camelcase(key) 15 | result[nextKey] = camelcaseKeys(obj[key]) 16 | return result 17 | }, {}) as any 18 | } 19 | 20 | return obj 21 | } 22 | 23 | export function camelcase(str: string) { 24 | return str.replace(/^_+/, '').replaceAll(/([_-][a-z])/gi, ($1) => { 25 | return $1.toUpperCase().replace('-', '').replace('_', '') 26 | }) 27 | } 28 | const isMongoId = (id: string) => id.length === 24 && /^[\dA-F]{24}$/i.test(id) 29 | -------------------------------------------------------------------------------- /packages/api-client/utils/path.ts: -------------------------------------------------------------------------------- 1 | export const resolveFullPath = (endpoint: string, path: string) => { 2 | if (!path.startsWith('/')) { 3 | path = `/${path}` 4 | } 5 | return `${endpoint}${path}` 6 | } 7 | -------------------------------------------------------------------------------- /packages/api-client/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsPath from 'vite-tsconfig-paths' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | 8 | include: ['__tests__/**/*.(spec|test).ts'], 9 | }, 10 | 11 | // @ts-ignore 12 | plugins: [tsPath()], 13 | optimizeDeps: { 14 | needsInterop: ['lodash'], 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /packages/compiled/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/compiled/auth.ts: -------------------------------------------------------------------------------- 1 | export { betterAuth, getCookieKey } from 'better-auth' 2 | export { getSessionCookie, setSessionCookie } from 'better-auth/cookies' 3 | 4 | export { 5 | APIError, 6 | createAuthMiddleware, 7 | getSessionFromCtx, 8 | } from 'better-auth/api' 9 | 10 | export { mongodbAdapter } from 'better-auth/adapters/mongodb' 11 | export { fromNodeHeaders, toNodeHandler } from 'better-auth/node' 12 | export { bearer, jwt } from 'better-auth/plugins' 13 | 14 | export type * from 'better-auth' 15 | -------------------------------------------------------------------------------- /packages/compiled/index.ts: -------------------------------------------------------------------------------- 1 | export * as nanoid from 'nanoid' 2 | 3 | export * as zx from 'zx' 4 | export * from 'zx' 5 | -------------------------------------------------------------------------------- /packages/compiled/install-pkg.ts: -------------------------------------------------------------------------------- 1 | export * from './node_modules/@antfu/install-pkg' 2 | -------------------------------------------------------------------------------- /packages/compiled/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mx-space/compiled", 3 | "private": true, 4 | "type": "module", 5 | "main": "dist/index.cjs", 6 | "exports": { 7 | ".": "./dist/index.cjs", 8 | "./auth": "./dist/auth.cjs", 9 | "./zod": "./dist/zod.cjs", 10 | "./install-pkg": "./dist/install-pkg.cjs" 11 | }, 12 | "scripts": { 13 | "build": "tsup" 14 | }, 15 | "devDependencies": { 16 | "@antfu/install-pkg": "1.1.0", 17 | "better-auth": "1.2.5", 18 | "nanoid": "5.1.5", 19 | "zod": "3.24.3", 20 | "zx": "7.2.3" 21 | } 22 | } -------------------------------------------------------------------------------- /packages/compiled/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "jsx": "react", 5 | "lib": [ 6 | "ESNext", 7 | "DOM", 8 | "DOM.Iterable" 9 | ], 10 | "baseUrl": ".", 11 | "module": "ESNext", 12 | "moduleResolution": "node", 13 | "paths": { 14 | "~/*": [ 15 | "*" 16 | ], 17 | "@core/*": [ 18 | "../../apps/core/src/*" 19 | ], 20 | }, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "declaration": true, 24 | "outDir": "./esm", 25 | "sourceMap": true, 26 | "esModuleInterop": true, 27 | "forceConsistentCasingInFileNames": true, 28 | "skipLibCheck": true 29 | }, 30 | "exclude": [ 31 | "esm/*", 32 | "build/*", 33 | "node_modules/*", 34 | "lib/*", 35 | "dist/**" 36 | ] 37 | } -------------------------------------------------------------------------------- /packages/compiled/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | clean: true, 5 | target: 'es2020', 6 | entry: ['index.ts', 'auth.ts', 'zod.ts', 'install-pkg.ts'], 7 | dts: true, 8 | external: ['mongodb'], 9 | format: ['cjs'], 10 | }) 11 | -------------------------------------------------------------------------------- /packages/compiled/zod.ts: -------------------------------------------------------------------------------- 1 | export * from './node_modules/zod' 2 | -------------------------------------------------------------------------------- /packages/compiled/zx-global.cjs: -------------------------------------------------------------------------------- 1 | // import zx from './dist/index.cjs' 2 | const zx = require('./dist/index.cjs').zx 3 | 4 | Object.assign(global, zx) 5 | -------------------------------------------------------------------------------- /packages/compiled/zx-global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/webhook/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface JSON { 3 | safeParse: typeof JSON.parse 4 | } 5 | } 6 | export {} 7 | -------------------------------------------------------------------------------- /packages/webhook/index.cjs: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const { createHandler, BusinessEvents } = require('./dist/index.cjs') 3 | const app = express() 4 | 5 | app.use(express.json()) 6 | 7 | const handler = createHandler({ secret: 'test' }) 8 | app.post('/webhook', (req, res) => { 9 | handler(req, res) 10 | }) 11 | handler.emitter.on(BusinessEvents.POST_UPDATE, (event) => { 12 | console.log(event) 13 | }) 14 | 15 | app.listen('13333') 16 | -------------------------------------------------------------------------------- /packages/webhook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mx-space/webhook", 3 | "version": "0.5.0", 4 | "type": "module", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "types": "./dist/index.d.ts", 11 | "require": "./dist/index.cjs", 12 | "import": "./dist/index.js" 13 | }, 14 | "./dist/*": { 15 | "require": "./dist/*.cjs", 16 | "import": "./dist/*.js" 17 | }, 18 | "./package.json": "./package.json" 19 | }, 20 | "scripts": { 21 | "build": "node scripts/generate.js && tsup && node scripts/post-build.cjs" 22 | }, 23 | "devDependencies": { 24 | "express": "4.21.2" 25 | }, 26 | "bump": { 27 | "before": [ 28 | "git pull --rebase", 29 | "pnpm i", 30 | "npm run build" 31 | ], 32 | "after": [ 33 | "npm publish --access=public" 34 | ], 35 | "tag": false, 36 | "commit_message": "chore(release): bump @mx-space/webhook to v${NEW_VERSION}" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/webhook/readme.md: -------------------------------------------------------------------------------- 1 | # Mix Space Core Webhook SDK 2 | 3 | ```bash 4 | pnpm install @mx-space/webhook 5 | ``` 6 | 7 | ## Usage 8 | 9 | ```ts 10 | const handler = createHandler({ 11 | secret: 'your_secret', 12 | }) 13 | 14 | ctx.server.post('/mx/webhook', (req, res) => { 15 | handler(req.raw, res.raw) 16 | }) 17 | 18 | handler.emitter.on(event, callback) 19 | ``` 20 | 21 | ## MIT -------------------------------------------------------------------------------- /packages/webhook/src/error.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable unicorn/custom-error-definition */ 2 | export class InvalidSignatureError extends Error { 3 | constructor() { 4 | super('Invalid Signature') 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/webhook/src/event.enum.ts: -------------------------------------------------------------------------------- 1 | export { 2 | BusinessEvents, 3 | EventScope, 4 | } from '@core/constants/business-event.constant' 5 | -------------------------------------------------------------------------------- /packages/webhook/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event.enum' 2 | export * from './handler' 3 | export * from './types' 4 | export * from './error' 5 | -------------------------------------------------------------------------------- /packages/webhook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "ESNext", 6 | "DOM", 7 | "DOM.Iterable" 8 | ], 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "baseUrl": ".", 12 | "module": "ESNext", 13 | "moduleResolution": "node", 14 | "paths": { 15 | "~/*": [ 16 | "../../apps/core/src/*" 17 | ], 18 | "@core/*": [ 19 | "../../apps/core/src/*" 20 | ], 21 | }, 22 | "resolveJsonModule": true, 23 | "strict": false, 24 | "declaration": true, 25 | "sourceMap": true, 26 | "esModuleInterop": true, 27 | "forceConsistentCasingInFileNames": true, 28 | "skipLibCheck": true 29 | }, 30 | } -------------------------------------------------------------------------------- /packages/webhook/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | clean: true, 5 | target: 'es2020', 6 | entry: ['src/index.ts'], 7 | dts: true, 8 | format: ['cjs', 'esm'], 9 | }) 10 | -------------------------------------------------------------------------------- /paw.paw: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:5284b91c27da0d584797128c6f504c9f251d3e808f13f7a45bd2fd2345a1a0de 3 | size 117205 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | - apps/* 4 | onlyBuiltDependencies: 5 | - '@nestjs/core' 6 | - '@swc/core' 7 | - esbuild 8 | - mongodb-memory-server 9 | - redis-memory-server 10 | - sharp 11 | - simple-git-hooks 12 | - unrs-resolver 13 | patchedDependencies: 14 | tinyexec@1.0.1: patches/tinyexec@1.0.1.patch 15 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":automergePatch", 5 | ":automergeTypes", 6 | ":automergeTesters", 7 | ":automergeLinters", 8 | ":rebaseStalePrs" 9 | ], 10 | "packageRules": [ 11 | { 12 | "updateTypes": ["major"], 13 | "labels": ["UPDATE-MAJOR"] 14 | } 15 | ], 16 | "ignoreDeps": [ 17 | "class-validator", 18 | "class-transformer", 19 | "vitest", 20 | "vite", 21 | "snakecase-keys", 22 | "zx", 23 | "algoliasearch", 24 | "@algolia/client-search" 25 | ], 26 | "enabled": true 27 | } 28 | -------------------------------------------------------------------------------- /scripts/download-latest-asset.js: -------------------------------------------------------------------------------- 1 | function getOsBuildAssetName() { 2 | const platform = process.platform 3 | const kernelMap = { 4 | darwin: 'macos', 5 | linux: 'linux', 6 | win32: 'windows', 7 | } 8 | const os = kernelMap[platform] 9 | if (!os) { 10 | throw new Error('No current platform build. Please build manually') 11 | } 12 | return `release-${os}.zip` 13 | } 14 | 15 | const { appendFileSync } = require('node:fs') 16 | 17 | async function main() { 18 | const res = await fetch( 19 | `https://api.github.com/repos/mx-space/core/releases/latest`, 20 | ) 21 | const data = await res.json() 22 | const downloadUrl = data.assets.find((asset) => 23 | [getOsBuildAssetName(), 'release.zip'].includes(asset.name), 24 | )?.browser_download_url 25 | if (!downloadUrl) { 26 | throw new Error('no download url found') 27 | } 28 | 29 | const buffer = await fetch(downloadUrl).then((res) => res.arrayBuffer()) 30 | appendFileSync(`release-downloaded.zip`, Buffer.from(buffer)) 31 | await $`unzip release-downloaded.zip -d mx-server` 32 | } 33 | 34 | main() 35 | -------------------------------------------------------------------------------- /scripts/run-pm2.mjs: -------------------------------------------------------------------------------- 1 | import { $ } from 'zx' 2 | 3 | const argv = process.argv.slice(2) 4 | 5 | console.log(argv) 6 | $`pm2 reload ecosystem.dev.config.js -- ${argv}` 7 | --------------------------------------------------------------------------------