Compare commits
1 Commits
develop
...
atom-2024/
Author | SHA1 | Date |
---|---|---|
GaoNeng | dbb8365f35 |
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
*.conf
|
||||
pnpm-lock.yaml
|
|
@ -0,0 +1,79 @@
|
|||
name: Build And Publish (Dry run)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
front:
|
||||
name: Build front-end docker image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
gaonengwww/dl-flow-frontend
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
working-directory: ./
|
||||
context: ./
|
||||
build-context: ./
|
||||
file: ./dockerfile
|
||||
push: false
|
||||
back:
|
||||
name: Build back-end docker image
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./dl-flow-backend
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
gaonengwww/dl-flow-backend
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
working-directory: dl-flow-backend/
|
||||
context: dl-flow-backend/
|
||||
build-context: dl-flow-backend/
|
||||
file: dl-flow-backend/dockerfile
|
||||
push: false
|
|
@ -0,0 +1,92 @@
|
|||
name: Build And Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*.*.*"
|
||||
|
||||
jobs:
|
||||
front:
|
||||
name: Build Front docker image
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: .
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
gaonengwww/dl-flow-frontend
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
working-directory: ./
|
||||
context: ./
|
||||
build-context: ./
|
||||
file: ./dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
back:
|
||||
name: Build docker image
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./dl-flow-backend
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
gaonengwww/dl-flow-backend
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest,enable=${{ github.event_name != 'pull_request' }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
working-directory: dl-flow-backend/
|
||||
context: dl-flow-backend/
|
||||
build-context: dl-flow-backend/
|
||||
file: dl-flow-backend/dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
**/node_modules
|
273
README.md
273
README.md
|
@ -1,2 +1,271 @@
|
|||
# lowcode-design-core
|
||||
|
||||
# dl-flow
|
||||
|
||||
dl-Flow 是一种拖拽式的线性网络搭建的 Web 应用程序。你可以使用该程序直观的搭建一个paddlepaddle的神经网络.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Docker 搭建
|
||||
|
||||
我们非常推荐您使用Docker进行部署. 这不仅可以让你快速的进行上手, 也可以让您再后续对接其他程序更加的方便快捷(例如 K8s).
|
||||
|
||||
```yaml
|
||||
# docker-compose.yaml
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo
|
||||
ports:
|
||||
- 27018:27017
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
front:
|
||||
image: gaonengwww/dl-flow-frontend
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
server:
|
||||
image: gaonengwww/dl-flow-backend
|
||||
ports:
|
||||
- 9000:9000
|
||||
environment:
|
||||
- DB_URL=mongodb://mongodb:27017/dl-flow # 数据库地址
|
||||
- REDIS_HOST=redis # redis地址 (必填)
|
||||
- REDIS_PORT=6379 # redis端口 (必填)
|
||||
- REDIS_DB=0 # redis数据库 (必填)
|
||||
- REDIS_PASSWORD="" # redis密码
|
||||
- JWT_EXPIRE_IN=1d # JWT 过期时间 (必填)
|
||||
- JWT_SIGN_ALGORITHM=RS256 # JWT签名算法, 要与密钥对符合, 例如密钥对是RSA 2048bit, 那么此处应该是 RS256 (必填)
|
||||
- JWT_PUB_KEY=./keys/pub.key # JWT 公钥 (必填)
|
||||
- JWT_PRI_KEY=./keys/pri.key # JWT 私钥 (必填)
|
||||
- PWD_SALT=salt # bcrypt 盐(必填)
|
||||
- PWD_SALT_LEN=12 # bcrypt 盐(必填)
|
||||
volumes: # 强烈将下述卷挂载到本地, 以避免数据丢失
|
||||
- ./public:/public # 代码生成暂存位置
|
||||
- ./keys:/keys # 密钥对存放位置
|
||||
- ./data:/data # bundle.json与install.lock 存放位置
|
||||
- ./examples:/examples # 示例文件夹
|
||||
```
|
||||
|
||||
`Web-Ui` 使用nginx驱动, 接下来我们需要编写 `nginx.conf`
|
||||
|
||||
```conf
|
||||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
gzip on;
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
location ~ /endpoint/ {
|
||||
rewrite ^/endpoint/(.*)$ /$1 break; # 主要负责反代的rewrite, 否则发的是 http://server:9000/endpoint/
|
||||
proxy_pass http://server:9000;
|
||||
}
|
||||
location ~ /socket.io {
|
||||
proxy_pass http://server:9001;
|
||||
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
接下来,您便可以访问 `http://localhost` 开始搭建
|
||||
|
||||
|
||||
## 源码构建
|
||||
|
||||
```bash
|
||||
git clone https://atomgit.com/opentiny/000003.git
|
||||
# git clone git@atomgit.com:opentiny/000003.git
|
||||
ls -al
|
||||
# .github
|
||||
# dl-flow-backend // 后端
|
||||
# dl-flow-frontend // WebUi
|
||||
# dl-flow-example // 示例
|
||||
# nginx.conf // 预设好的nginx文件
|
||||
```
|
||||
|
||||
### 后端构建
|
||||
|
||||
后端采用的是`Nest.js`
|
||||
|
||||
```bash
|
||||
cd dl-flow-backend
|
||||
pnpm install
|
||||
pnpm build
|
||||
node dist/main.js
|
||||
```
|
||||
|
||||
### 前端构建
|
||||
|
||||
前端魔改自`TinyEngine`, 部署方式与`TinyEngine`大同小异.
|
||||
|
||||
```bash
|
||||
cd dl-flow-frontend
|
||||
pnpm install
|
||||
pnpm build:plugin
|
||||
pnpm build:prod
|
||||
cd packages/design-core/dist
|
||||
```
|
||||
|
||||
### 环境变量与含义
|
||||
|
||||
- DB_URL: 数据库链接地址 (必填)
|
||||
- REDIS_HOST: redis地址 (必填)
|
||||
- REDIS_PORT: redis端口 (必填)
|
||||
- REDIS_DB: redis数据库 (必填)
|
||||
- REDIS_PASSWORD: redis密码 (必填)
|
||||
- JWT_EXPIRE_IN: JWT过期时间, 规则可参考[vercel/ms](https://github.com/vercel/ms) (必填)
|
||||
- JWT_SIGN_ALGORITHM: JWT签名算法, 要与密钥对符合, 例如密钥对是RSA 2048bit, 那么此处应该是 RS256 (必填)
|
||||
- JWT_PUB_KEY: JWT 公钥 (必填)
|
||||
- JWT_PRI_KEY: JWT 私钥 (必填)
|
||||
- ~~PWD_SALT: bcrypt 盐 (必填)~~
|
||||
- PWD_SALT_LEN: bcrypt 盐长度
|
||||
|
||||
### Bug 反馈
|
||||
|
||||
如有bug与其他方面的疑问, 欢迎提交[issue](https://atomgit.com/opentiny/000003/issues)
|
||||
|
||||
## WebUi布局
|
||||
|
||||
![](public/Snipaste_2024-03-24_21-37-53.png)
|
||||
|
||||
绿色区域: 网络与layer选择区域
|
||||
红色区域: 布局区域, 在绿色区域 单击需要的网络后会显示在红色布局区内
|
||||
蓝色区域: 网络区域, 可以配置网络的一些属性(Covn2D 举例)
|
||||
|
||||
- 输入特征大小
|
||||
- 输出特征大小
|
||||
- 卷积核大小等
|
||||
|
||||
注意, 您应当自行校验网络的配置项, 例如: 理论上卷积核大小为1是可以存在的,它也的确有一些作用,比如
|
||||
|
||||
- 升维/降维
|
||||
- 增加非线性
|
||||
|
||||
但是**可以**这么做并不意味着它**适合**你的需求更不等同于**不存在运行时候错误**(比如维度错误)
|
||||
|
||||
## 结构
|
||||
|
||||
### 流程图
|
||||
|
||||
### Web UI
|
||||
|
||||
下图展示了项目与TinyEngine的差异文件
|
||||
|
||||
```
|
||||
├── packages
|
||||
├── canvas
|
||||
│ ├── src
|
||||
│ │ ├── components
|
||||
│ │ │ ├── container
|
||||
│ │ │ │ ├── AlgoNode.vue // 创建的自定义节点
|
||||
│ │ │ │ ├── GroupNode.vue // 组节点
|
||||
│ │ │ │ └── X6Canvas.vue // x6容器
|
||||
├── controller
|
||||
│ ├── src
|
||||
│ │ ├── useLayer.js // 自定义Layer的逻辑
|
||||
│ │ ├── useResource.js // 数据请求逻辑
|
||||
│ │ ├── useSchema.js // schema逻辑
|
||||
│ │ ├── useVisitor.js // Python AST解析
|
||||
│ │ ├── useWS.js // socket.io的二次封装
|
||||
│ │ └── useX6.js // x6的一些逻辑封装
|
||||
├── design-core
|
||||
│ ├── authentication.html
|
||||
│ ├── src
|
||||
│ │ ├── App.vue
|
||||
│ │ └── authentication // 登陆页面
|
||||
│ │ └── src
|
||||
│ │ ├── App.vue
|
||||
│ │ ├── components
|
||||
│ │ │ ├── login.vue
|
||||
│ │ │ └── register.vue
|
||||
│ │ └── main.js
|
||||
├── plugins
|
||||
│ ├── materials // 物料 (paddlepaddle的网络物料)
|
||||
│ │ └── src
|
||||
│ │ ├── Main.vue
|
||||
│ │ ├── layer
|
||||
│ │ │ └── main.vue
|
||||
│ │ └── networks
|
||||
│ │ └── main.vue
|
||||
│ └── schema // 传输给后端的schema的预览窗
|
||||
│ └── src
|
||||
│ └── Main.vue
|
||||
└── settings // 物料的Props设计页面
|
||||
├── code
|
||||
│ └── src
|
||||
│ └── Main.vue
|
||||
└── props
|
||||
├── index.js
|
||||
├── package.json
|
||||
└── src
|
||||
├── Main.vue
|
||||
└── components
|
||||
├── Empty.vue
|
||||
├── ParamAttr.vue
|
||||
├── enums.vue
|
||||
├── list.vue
|
||||
└── property-setting.vue
|
||||
```
|
||||
|
||||
### 后端
|
||||
|
||||
[参考](./dl-flow-backend/README.md)
|
||||
|
||||
|
||||
### 为什么结束节点和开始节点必须只有一个
|
||||
|
||||
因为目前生成的是`Sequential`而不是`Layer`.
|
||||
|
||||
`Layer`的确更加的灵活。但是问题也非常的显而易见。
|
||||
|
||||
我们设计的是又向无环图, 又向表明 A->B 是正确的,但是 B->A 是不一定的。假设有一幅图
|
||||
|
||||
```
|
||||
Start
|
||||
|
|
||||
v
|
||||
Node-1
|
||||
|
|
||||
-----+-----
|
||||
| | |
|
||||
V V V
|
||||
END1 END2 END3
|
||||
```
|
||||
|
||||
那么不管如何遍历最终节点,其实都是正确的。但在神经网络中,不同网络的运算顺序会有不同的结果。比如`先池化后卷积`和`先卷积后池化`的运算结果是不同的。进入训练阶段,训练结果也可能不同。这主要是因为函数的组合在某些条件下是不可交换的,我们也使用数学语言证明了这一点。详细可以参考[函数组合的交换性讨论](./dl-flow-backend/proof/函数组合的可交换性讨论.pdf)
|
||||
|
||||
|
||||
## 前端流程图
|
||||
|
||||
![](./public/sequenceDiagram.png)
|
||||
|
||||
## 源码阅读顺序
|
||||
|
||||
```
|
||||
dl-flow-back >> README.md >> code-generate/README.md >> code-generate.gateway.ts >> code-generate.service.ts >> ast.service.ts
|
||||
|
||||
dl-flow-frontend >> X6Canvas >> useX6 >> useSchma >> export
|
||||
```
|
|
@ -0,0 +1,4 @@
|
|||
.env
|
||||
node_modules
|
||||
*.md
|
||||
proof
|
|
@ -0,0 +1,12 @@
|
|||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
|
@ -0,0 +1,7 @@
|
|||
DB_URL=mongodb://localhost:27017:/dl-flow
|
||||
JWT_EXPIRE_IN="1d"
|
||||
JWT_SIGN_ALGORITHM="RS256"
|
||||
JWT_PUB_KEY="./keys/pub.key"
|
||||
JWT_PRI_KEY="./keys/pri.key"
|
||||
PWD_SALT=”salt“
|
||||
PWD_SALT_LEN=2
|
|
@ -0,0 +1,25 @@
|
|||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# do not upload public fold
|
||||
public
|
||||
# do not upload keys
|
||||
keys
|
||||
# do not upload data
|
||||
data
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
# dl-flow backend
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
├── README.md
|
||||
├── docker-compose.yaml // 快速启动 compose示例
|
||||
├── dockerfile // docker构建文件
|
||||
├── global.d.ts
|
||||
├── libs
|
||||
│ ├── database // 数据库模块
|
||||
│ ├── redis // 缓存模块
|
||||
│ └── shared // 共用文件
|
||||
├── src
|
||||
│ ├── app.module.ts // 主文件
|
||||
│ ├── auth-guard // token校验门禁 (可以理解为中间件, 不过nest的分类更加细致)
|
||||
│ ├── code-generate
|
||||
│ │ ├── ast
|
||||
│ │ ├── ast.service.ts // ast生成服务
|
||||
│ │ ├── code-generate.controller.ts // code-generate路由
|
||||
│ │ ├── code-generate.gateway.ts // code-generate的websocket路由, 不过在nest中叫做gateway
|
||||
│ │ ├── code-generate.module.ts // code-generate的module
|
||||
│ │ ├── code-generate.schema.ts // code-generate的schema
|
||||
│ │ └── code-generate.service.ts // code-generate的服务
|
||||
│ ├── layer // layer获取与存储的http接口 (没什么技术含量全是CRUD)
|
||||
│ ├── main.ts
|
||||
│ ├── material // 物料的获取 (没什么技术含量全是CRUD)
|
||||
│ ├── user // 用户的登陆注册
|
||||
│ └── ws-exception // websocket的错误捕获, 主要用于捕获Exception和Runtime Error
|
||||
├── tsconfig.build.json
|
||||
├── tsconfig.json
|
||||
└── webpack.config.js // 打包的配置文件, 主要是定义全局变量
|
||||
```
|
||||
|
||||
### global.d.ts
|
||||
|
||||
```typescript
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
// 为了让process.env.xxx的时候有类型提示
|
||||
interface ProcessEnv {
|
||||
DB_URL: string;
|
||||
REDIS_HOST: string;
|
||||
REDIS_PORT: number;
|
||||
REDIS_DB: number;
|
||||
REDIS_PASSWORD: string;
|
||||
JWT_EXPIRE_IN: string;
|
||||
JWT_SIGN_ALGORITHM: JwtSignOptions['algorithm'];
|
||||
JWT_PUB_KEY: string;
|
||||
JWT_PRI_KEY: string;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
PWD_SALT: string; // used for bcrypt
|
||||
PWD_SALT_LEN: string;
|
||||
}
|
||||
}
|
||||
// 开发环境标志
|
||||
declare const __DEV__: boolean;
|
||||
// 测试环境标志
|
||||
declare const __TEST__: boolean;
|
||||
}
|
||||
export {};
|
||||
```
|
||||
|
||||
|
||||
## QA
|
||||
|
||||
### 为什么使用的是全局变量而不是环境变量?
|
||||
|
||||
`__DEV__`与`__TEST__`主要是用于测试环境与开发环境。如果一段代码只是测试环境需要(例如准备内存数据库),那么我们可以这么写
|
||||
|
||||
```typescript
|
||||
if (__TEST__){
|
||||
// do sth
|
||||
}
|
||||
```
|
||||
|
||||
之后我们只需要在jest的配置文件里的`global`配置项中中,将`__TEST__`定义为`true`就可以了。(详细参考packages.json L94)
|
||||
而在打包时侯,`webpack`会将`__DEV__`与`__TEST__`占位符替换为false, 这样一来所有的测试代码与开发调试代码都将会被标记为`dead code`. 最后会被`webpack`自动剔除 (详细参考 webpack.config.js L12-)
|
||||
|
||||
### 为什么使用内存数据库而不是MOCK?
|
||||
|
||||
mock一组数据是人来mock, 工作量是其次,最主要的问题是难以`靠近实战`. 之所以选择使用内存数据库而不是手动mock, 我给出如下原因
|
||||
|
||||
1. 不需要手动mock数据,避免因为人脑无法达到**完全理性**而造成的**数据结构**问题
|
||||
2. 内存数据库的可维护性很高
|
||||
3. 数据的销毁简单,不需要本地启动复杂的环境
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
1
|
|
@ -0,0 +1,31 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo
|
||||
ports:
|
||||
- 27018:27017
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
server:
|
||||
image: gaonengwww/dl-flow-backend
|
||||
ports:
|
||||
- 9000:9000
|
||||
environment:
|
||||
- DB_URL=mongodb://mongodb:27017/dl-flow # 数据库地址
|
||||
- REDIS_HOST=redis # redis地址 (必填)
|
||||
- REDIS_PORT=6379 # redis端口 (必填)
|
||||
- REDIS_DB=0 # redis数据库 (必填)
|
||||
- REDIS_PASSWORD="" # redis密码
|
||||
- JWT_EXPIRE_IN="1d" # JWT 过期时间 (必填)
|
||||
- JWT_SIGN_ALGORITHM="RS256" # JWT签名算法, 要与密钥对符合, 例如密钥对是RSA 2048bit, 那么此处应该是 RS256 (必填)
|
||||
- JWT_PUB_KEY=./keys/key.pub # JWT 公钥 (必填)
|
||||
- JWT_PRI_KEY=./keys/key.pri # JWT 私钥 (必填)
|
||||
- PWD_SALT=salt # bcrypt 盐(必填)
|
||||
- PWD_SALT_LEN=12 # bcrypt 盐(必填)
|
||||
# volumes: # 强烈将下述卷挂载到本地, 以避免数据丢失
|
||||
# - ./_test/public:/public # 代码生成暂存位置
|
||||
# - ./_test/keys:/keys # 密钥对存放位置
|
||||
# - ./_test/data:/data # bundle.json与install.lock 存放位置
|
|
@ -0,0 +1,25 @@
|
|||
FROM node:18 as builder
|
||||
WORKDIR /CODE
|
||||
ADD . /CODE/
|
||||
|
||||
RUN npm install pnpm -g && pnpm install && pnpm build
|
||||
|
||||
FROM node:18-alpine as runner
|
||||
COPY --from=builder /CODE/dist ./dist
|
||||
COPY --from=builder /CODE/node_modules ./node_modules
|
||||
VOLUME [ "/public", "/keys", "/data", "/examples"]
|
||||
|
||||
ENV DB_URL=""
|
||||
ENV REDIS_HOST=""
|
||||
ENV REDIS_PORT=""
|
||||
ENV REDIS_DB=""
|
||||
ENV REDIS_PASSWORD=""
|
||||
ENV JWT_EXPIRE_IN=""
|
||||
ENV JWT_SIGN_ALGORITHM=""
|
||||
ENV JWT_PUB_KEY=""
|
||||
ENV JWT_PRI_KEY=""
|
||||
ENV PWD_SALT=""
|
||||
ENV PWD_SALT_LEN=""
|
||||
|
||||
EXPOSE 9000-9900
|
||||
CMD [ "node","dist/main.js" ]
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
|||
import { JwtSignOptions } from '@nestjs/jwt';
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
DB_URL: string;
|
||||
REDIS_HOST: string;
|
||||
REDIS_PORT: number;
|
||||
REDIS_DB: number;
|
||||
REDIS_PASSWORD: string;
|
||||
JWT_EXPIRE_IN: string;
|
||||
JWT_SIGN_ALGORITHM: JwtSignOptions['algorithm'];
|
||||
JWT_PUB_KEY: string;
|
||||
JWT_PRI_KEY: string;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
PWD_SALT: string; // used for bcrypt
|
||||
PWD_SALT_LEN: string;
|
||||
}
|
||||
}
|
||||
declare const __DEV__: boolean;
|
||||
declare const __TEST__: boolean;
|
||||
}
|
||||
|
||||
export {};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { DatabaseService } from '../database.service';
|
||||
|
||||
describe('DatabaseService', () => {
|
||||
let service: DatabaseService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [DatabaseService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DatabaseService>(DatabaseService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import { Logger, Module } from '@nestjs/common';
|
||||
import { DatabaseService } from './database.service';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
|
||||
@Module({
|
||||
providers: [DatabaseService],
|
||||
exports: [DatabaseService],
|
||||
imports: [
|
||||
MongooseModule.forRootAsync({
|
||||
useFactory: async () => {
|
||||
let uri = process.env.DB_URL;
|
||||
// 当且仅当为开发环境或测试环境时候,才会去启用内存数据库
|
||||
if (__DEV__ || __TEST__) {
|
||||
const { MongoMemoryReplSet } = await import('mongodb-memory-server');
|
||||
const mongod = await MongoMemoryReplSet.create({
|
||||
replSet: {
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
uri = mongod.getUri();
|
||||
Logger.log(`Memory server url is: ${uri}`, 'DbModule');
|
||||
}
|
||||
return {
|
||||
uri,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class DbModule {}
|
|
@ -0,0 +1,4 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseService {}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './database.module';
|
||||
export * from './database.service';
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "../../dist/libs/database"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './redis.module';
|
||||
export * from './redis.service';
|
|
@ -0,0 +1,50 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { RedisService } from './redis.service';
|
||||
import {
|
||||
RedisModule as Redis,
|
||||
RedisModuleOptions,
|
||||
} from '@liaoliaots/nestjs-redis';
|
||||
|
||||
export const memoryRedis = async () => {
|
||||
if (__DEV__ || __TEST__) {
|
||||
const { RedisMemoryServer } = await import('redis-memory-server');
|
||||
const server = new RedisMemoryServer({
|
||||
instance: {
|
||||
ip: '127.0.0.1',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
await server.start();
|
||||
return server;
|
||||
}
|
||||
};
|
||||
|
||||
@Module({
|
||||
providers: [RedisService],
|
||||
exports: [RedisService],
|
||||
imports: [
|
||||
Redis.forRootAsync({
|
||||
useFactory: async (): Promise<RedisModuleOptions> => {
|
||||
if (__DEV__ || __TEST__) {
|
||||
const server = await memoryRedis();
|
||||
return {
|
||||
config: {
|
||||
host: await server.getIp(),
|
||||
port: await server.getPort(),
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
readyLog: true,
|
||||
config: {
|
||||
host: process.env.REDIS_HOST,
|
||||
port: process.env.REDIS_PORT,
|
||||
db: process.env.REDIS_DB,
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class RedisModule {}
|
|
@ -0,0 +1,4 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class RedisService {}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "../../dist/libs/redis"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { ValidationOptions, registerDecorator } from 'class-validator';
|
||||
|
||||
export type Enum = {
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
default?: boolean;
|
||||
};
|
||||
|
||||
export type Label = {
|
||||
zh_CN: string;
|
||||
en_US: string;
|
||||
};
|
||||
|
||||
export type Property = {
|
||||
id: string;
|
||||
label: Label;
|
||||
type: string;
|
||||
default: string | number | null | boolean;
|
||||
enums: Enum[];
|
||||
data: any;
|
||||
};
|
||||
|
||||
export const isProperty = (object: unknown | unknown[]) => {
|
||||
if (Array.isArray(object)) {
|
||||
return false;
|
||||
}
|
||||
if (object instanceof Object) {
|
||||
const maybeProperty: Partial<Property> = object;
|
||||
const keys = ['id', 'label', 'type', 'default'];
|
||||
const maybePropertyKeys = Object.keys(maybeProperty);
|
||||
if (!maybePropertyKeys.length) {
|
||||
return false;
|
||||
}
|
||||
return maybePropertyKeys.every((key) => keys.includes(key));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export function IsProperty(validationOptions?: ValidationOptions) {
|
||||
return function (object: Partial<Property>, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isProperty',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any) {
|
||||
return isProperty(value);
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function IsProperties(validationOptions?: ValidationOptions) {
|
||||
return function (object: Partial<Property>, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'IsProperties',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: unknown[]) {
|
||||
console.log(value);
|
||||
if (!value?.length) {
|
||||
return false;
|
||||
}
|
||||
return value.every((v) => isProperty(v));
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"outDir": "../../dist/libs/shared"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"webpack": true,
|
||||
"webpackConfigPath": "webpack.config.js"
|
||||
},
|
||||
"projects": {
|
||||
"database": {
|
||||
"type": "library",
|
||||
"root": "libs/database",
|
||||
"entryFile": "index",
|
||||
"sourceRoot": "libs/database/src",
|
||||
"compilerOptions": {
|
||||
"tsConfigPath": "libs/database/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"shared": {
|
||||
"type": "library",
|
||||
"root": "libs/shared",
|
||||
"entryFile": "index",
|
||||
"sourceRoot": "libs/shared/src",
|
||||
"compilerOptions": {
|
||||
"tsConfigPath": "libs/shared/tsconfig.lib.json"
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"type": "library",
|
||||
"root": "libs/redis",
|
||||
"entryFile": "index",
|
||||
"sourceRoot": "libs/redis/src",
|
||||
"compilerOptions": {
|
||||
"tsConfigPath": "libs/redis/tsconfig.lib.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,109 @@
|
|||
{
|
||||
"name": "dl-flow-backend",
|
||||
"version": "0.0.7",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"",
|
||||
"start": "cross-env DB_URL=mongodb://127.0.0.1:27017/dl-flow?directConnection=true nest start --watch",
|
||||
"start:dev": "cross-env NODE_ENV=DEV DB_URL=mongodb://127.0.0.1:27017/?directConnection=true nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest --clear-cache && jest --force-exit --detectOpenHandles",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/jest/bin/jest.js --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||
"@nestjs/common": "^9.0.0",
|
||||
"@nestjs/core": "^9.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/mongoose": "^10.0.2",
|
||||
"@nestjs/platform-express": "^9.0.0",
|
||||
"@nestjs/platform-socket.io": "^10.3.2",
|
||||
"@nestjs/websockets": "^10.3.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"compressing": "^1.10.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"mongoose": "^8.1.1",
|
||||
"ms": "^2.1.3",
|
||||
"ramda": "^0.29.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.2.0",
|
||||
"socket.io": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antv/x6": "^2.18.1",
|
||||
"@golevelup/ts-jest": "^0.4.0",
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.0",
|
||||
"@nestjs/testing": "^9.0.0",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "29.5.1",
|
||||
"@types/ms": "^0.7.34",
|
||||
"@types/node": "18.16.12",
|
||||
"@types/ramda": "^0.29.10",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"express": "^4.18.2",
|
||||
"jest": "29.5.0",
|
||||
"mongodb-memory-server": "^9.1.6",
|
||||
"prettier": "^2.3.2",
|
||||
"redis-memory-server": "^0.10.0",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "29.1.0",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "^5.0.0",
|
||||
"webpack": "^5.90.1"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "./coverage",
|
||||
"globals": {
|
||||
"__TEST__": true,
|
||||
"__DEV__": false
|
||||
},
|
||||
"roots": [
|
||||
"<rootDir>/src/",
|
||||
"<rootDir>/libs/"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^@app/database(|/.*)$": "<rootDir>/libs/database/src/$1",
|
||||
"^@app/shared(|/.*)$": "<rootDir>/libs/shared/src/$1",
|
||||
"^@app/redis(|/.*)$": "<rootDir>/libs/redis/src/$1"
|
||||
},
|
||||
"verbose": true,
|
||||
"maxWorkers": 1
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,170 @@
|
|||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { LayerModule } from './layer/layer.module';
|
||||
import { DbModule } from '@app/database';
|
||||
import { MaterialModule } from './material/material.module';
|
||||
import { CodeGenerateModule } from './code-generate/code-generate.module';
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
} from 'fs';
|
||||
import { basename, join } from 'path';
|
||||
import { UserModule } from './user/user.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { InjectModel, MongooseModule } from '@nestjs/mongoose';
|
||||
import { Material, MaterialSchema } from './material/material.schema';
|
||||
import { Model } from 'mongoose';
|
||||
import { RedisModule } from '@app/redis';
|
||||
import { InjectRedis } from '@liaoliaots/nestjs-redis';
|
||||
import { Redis } from 'ioredis';
|
||||
import { ProjectModule } from './project/project.module';
|
||||
import { User, UserSchema } from './user/user.schema';
|
||||
import { Project, ProjectSchema } from './project/entities/project.entity';
|
||||
import { isEmpty } from 'ramda';
|
||||
import { UserService } from './user/user.service';
|
||||
import { ProjectService } from './project/project.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DbModule,
|
||||
LayerModule,
|
||||
MaterialModule,
|
||||
CodeGenerateModule,
|
||||
UserModule,
|
||||
JwtModule.register({
|
||||
global: true,
|
||||
publicKey: readFileSync(
|
||||
join(process.cwd(), process.env.JWT_PUB_KEY ?? './keys/pub.key'),
|
||||
),
|
||||
privateKey: readFileSync(
|
||||
join(process.cwd(), process.env.JWT_PRI_KEY ?? './keys/pri.key'),
|
||||
),
|
||||
signOptions: {
|
||||
algorithm: process.env.JWT_SIGN_ALGORITHM ?? 'RS256',
|
||||
expiresIn: process.env.JWT_EXPIRE_IN ?? '1 day',
|
||||
},
|
||||
}),
|
||||
MongooseModule.forFeature([
|
||||
{
|
||||
name: Material.name,
|
||||
schema: MaterialSchema,
|
||||
},
|
||||
{
|
||||
name: User.name,
|
||||
schema: UserSchema,
|
||||
},
|
||||
{
|
||||
name: Project.name,
|
||||
schema: ProjectSchema,
|
||||
},
|
||||
]),
|
||||
RedisModule,
|
||||
ProjectModule,
|
||||
],
|
||||
providers: [UserService, ProjectService],
|
||||
})
|
||||
export class AppModule implements OnModuleInit {
|
||||
private readonly Logger: Logger = new Logger('App');
|
||||
constructor(
|
||||
@InjectRedis()
|
||||
private readonly redis: Redis,
|
||||
@InjectModel(Material.name)
|
||||
private readonly MaterialModel: Model<Material>,
|
||||
@InjectModel(User.name)
|
||||
private readonly UserModel: Model<User>,
|
||||
@InjectModel(Project.name)
|
||||
private readonly ProjectModel: Model<Project>,
|
||||
private readonly userService: UserService,
|
||||
private readonly projectService: ProjectService,
|
||||
) {}
|
||||
async onModuleInit() {
|
||||
const root = process.cwd();
|
||||
const publicPath = join(root, 'public');
|
||||
const lock = join(root, 'data', 'install.lock');
|
||||
const bundle = join(root, 'data', 'bundle.json');
|
||||
const examplePath = join(root, 'examples');
|
||||
if (existsSync(lock) && !__DEV__) {
|
||||
this.Logger.log('Lock file exists');
|
||||
return;
|
||||
}
|
||||
if (!existsSync(bundle)) {
|
||||
this.Logger.warn('bundle.json not exists');
|
||||
}
|
||||
if (existsSync(bundle)) {
|
||||
this.Logger.log('bundle.json exists');
|
||||
const { data } = JSON.parse(readFileSync(bundle).toString());
|
||||
const types = data.types;
|
||||
const materials = data.materials;
|
||||
if (types) {
|
||||
this.Logger.log('Insert types');
|
||||
try {
|
||||
for (const [key, value] of Object.entries(types)) {
|
||||
await this.redis.hset('types', { [key]: JSON.stringify(value) });
|
||||
}
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
this.Logger.error('Insert types fail', e.stack);
|
||||
process.exit(-1);
|
||||
}
|
||||
}
|
||||
if (materials) {
|
||||
this.Logger.log('Insert materials');
|
||||
try {
|
||||
await this.MaterialModel.insertMany(materials);
|
||||
} catch (err) {
|
||||
const e = err as Error;
|
||||
this.Logger.error('Insert Materials fail', e.stack);
|
||||
process.exit(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!existsSync(publicPath)) {
|
||||
mkdirSync(publicPath);
|
||||
}
|
||||
const adminUser = await this.UserModel.findOne({
|
||||
email: 'admin@no-reply.com',
|
||||
});
|
||||
let profile;
|
||||
let token;
|
||||
if (!adminUser) {
|
||||
this.Logger.warn('Not find admin user');
|
||||
profile = await this.userService.register({
|
||||
email: 'admin@no-reply.com',
|
||||
nick: 'Admin',
|
||||
password: 'admin',
|
||||
});
|
||||
token = (
|
||||
await this.userService.login({
|
||||
email: 'admin@no-reply.com',
|
||||
password: 'admin',
|
||||
})
|
||||
).jwt;
|
||||
this.Logger.log('Create Admin user success');
|
||||
}
|
||||
if (existsSync(examplePath)) {
|
||||
const examples = readdirSync(examplePath).map((fileName) => {
|
||||
return [basename(fileName, '.json'), join(examplePath, fileName)];
|
||||
});
|
||||
|
||||
for (const [exampleName, examplePath] of examples) {
|
||||
this.Logger.log(`Insert ${exampleName} example`);
|
||||
const content = JSON.parse(readFileSync(examplePath).toString());
|
||||
if (!isEmpty(content['data']) && !isEmpty(content['graphData'])) {
|
||||
const { projectId } = await this.projectService.create(
|
||||
{ name: exampleName },
|
||||
token,
|
||||
);
|
||||
await this.projectService.updateProject(projectId, {
|
||||
...content,
|
||||
});
|
||||
this.Logger.log(`Insert ${exampleName} success`);
|
||||
} else {
|
||||
this.Logger.warn(`Example should contain data and graphdata`);
|
||||
}
|
||||
}
|
||||
}
|
||||
writeFileSync(lock, '1');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { InjectRedis } from '@liaoliaots/nestjs-redis';
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private jwt: JwtService,
|
||||
@InjectRedis()
|
||||
private redis: Redis,
|
||||
) {}
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const token = this.extractTokenFromHeader(req);
|
||||
try {
|
||||
this.jwt.verify(token);
|
||||
} catch {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
// 如果redis中不存在token, 那么也提示未登陆错误
|
||||
if (!Boolean(await this.redis.exists(token))) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
# code generate
|
||||
|
||||
## 流程图
|
||||
|
||||
![](../../../public/3bb36ea8-b6cc-48ae-b227-1118f3b0a4c6.png)
|
||||
|
||||
## 附件
|
||||
|
||||
[函数组合的可交换性讨论](../../proof/函数组合的可交换性讨论.pdf)
|
|
@ -0,0 +1,409 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AST, VarDecl } from '../ast.service';
|
||||
import { Material } from '../../material/material.schema';
|
||||
import { Cell, Edge } from '@antv/x6';
|
||||
import { Layer } from '../code-generate.schema';
|
||||
import { StandardizationNodes } from '../code-generate.service';
|
||||
|
||||
describe('AST', () => {
|
||||
let service: AST;
|
||||
const layer = {
|
||||
code: `
|
||||
class Layer:
|
||||
def __init__(self,x):
|
||||
pass
|
||||
`,
|
||||
clazz: 'Layer',
|
||||
id: 'layer-1',
|
||||
label: {
|
||||
zh_CN: '',
|
||||
en_US: '',
|
||||
},
|
||||
properties: [],
|
||||
del: false,
|
||||
mode: 'layer',
|
||||
};
|
||||
const buildNode = (total: number, startId = 0) => {
|
||||
const arr: {
|
||||
id: string;
|
||||
shape: string;
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
size: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
zIndex: number;
|
||||
data: Material;
|
||||
}[] = [];
|
||||
for (let i = 1; i <= total; i++) {
|
||||
const obj = {
|
||||
id: `node${i + startId}`,
|
||||
shape: 'dag-node',
|
||||
position: { x: 0, y: 0 },
|
||||
size: { width: 0, height: 0 },
|
||||
attrs: {},
|
||||
zIndex: 0,
|
||||
data: {
|
||||
mode: 'nn',
|
||||
id: 'Conv1d',
|
||||
properties: [],
|
||||
} as any,
|
||||
};
|
||||
arr.push(obj);
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
const buildLayer = (total: number): any[] => {
|
||||
const arr = [];
|
||||
for (let i = 1; i <= total; i++) {
|
||||
arr.push({
|
||||
id: `layer-${i}`,
|
||||
shape: 'dag-node',
|
||||
label: {
|
||||
zh_CN: '',
|
||||
en_US: '',
|
||||
},
|
||||
data: {
|
||||
code: `
|
||||
class Layer${i}:
|
||||
def __init__(self,x):
|
||||
pass
|
||||
`,
|
||||
clazz: `Layer${i}`,
|
||||
properties: [{ id: 'x', data: 1 }],
|
||||
del: false,
|
||||
mode: 'layer',
|
||||
},
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AST],
|
||||
}).compile();
|
||||
service = module.get<AST>(AST);
|
||||
});
|
||||
it.skip('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
it('standardization', () => {
|
||||
expect(service.standardization('123', 'string')).toBe(`'123'`);
|
||||
expect(service.standardization('123', 'number')).toBe(`123`);
|
||||
expect(service.standardization('[0,0]', 'array')).toBe('[0,0]');
|
||||
expect(service.standardization('123', 'boolean')).toBe('true');
|
||||
expect(service.standardization('true', 'boolean')).toBe('true');
|
||||
expect(service.standardization('false', 'boolean')).toBe('false');
|
||||
});
|
||||
describe('buildNN', () => {
|
||||
it('non properties', () => {
|
||||
const node = buildNode(1)[0];
|
||||
expect(service.buildNN(node.data, node.id).name).toBe(`node${node.id}`);
|
||||
expect(service.buildNN(node.data, node.id).codeGen()).toBe(
|
||||
`node${node.id} = paddle.nn.${node.data.id}()`,
|
||||
);
|
||||
});
|
||||
it('has properties', () => {
|
||||
const node = buildNode(1)[0];
|
||||
node.data.properties.push(
|
||||
{
|
||||
id: 'in_channel',
|
||||
data: 256,
|
||||
label: {
|
||||
zh_CN: '',
|
||||
en_US: '',
|
||||
},
|
||||
type: 'number',
|
||||
default: '',
|
||||
enums: [],
|
||||
},
|
||||
{
|
||||
id: 'out_channel',
|
||||
data: 128,
|
||||
label: {
|
||||
zh_CN: '',
|
||||
en_US: '',
|
||||
},
|
||||
type: 'number',
|
||||
default: '',
|
||||
enums: [],
|
||||
},
|
||||
{
|
||||
id: 'weight_attr',
|
||||
data: {
|
||||
name: 'weight',
|
||||
learning_rate: 1e-5,
|
||||
},
|
||||
label: {
|
||||
zh_CN: '',
|
||||
en_US: '',
|
||||
},
|
||||
type: 'ParamAttr',
|
||||
default: '',
|
||||
enums: [],
|
||||
},
|
||||
{
|
||||
id: 'test',
|
||||
type: 'list',
|
||||
data: [1, '2', true, {}],
|
||||
label: {
|
||||
zh_CN: '',
|
||||
en_US: '',
|
||||
},
|
||||
default: '',
|
||||
enums: [],
|
||||
},
|
||||
);
|
||||
expect(service.buildNN(node.data, node.id).name).toBe(`node${node.id}`);
|
||||
expect(service.buildNN(node.data, node.id).codeGen()).toBe(
|
||||
`node${node.id} = paddle.nn.${node.data.id}(in_channel = 256,out_channel = 128,weight_attr = ParamAttr(name = 'weight',learning_rate = 0.00001),test = [1,'2',true,{}])`,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('group', () => {
|
||||
it('non nest', () => {
|
||||
const group = {
|
||||
id: 'group-1',
|
||||
shape: 'group-node',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
size: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
attrs: undefined,
|
||||
zIndex: 0,
|
||||
data: new Material(),
|
||||
children: ['node-1', 'node-2'],
|
||||
};
|
||||
const ast = service.buildGroup(group as any, {
|
||||
'group-1': group as any,
|
||||
'node-1': {
|
||||
id: 'node-1',
|
||||
shape: 'dag-node',
|
||||
data: {
|
||||
id: 'Conv1D',
|
||||
mode: 'nn',
|
||||
properties: [],
|
||||
} as Material,
|
||||
position: { x: 0, y: 0 },
|
||||
size: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
attrs: undefined,
|
||||
zIndex: 0,
|
||||
},
|
||||
'node-2': {
|
||||
id: 'node-2',
|
||||
shape: 'dag-node',
|
||||
data: {
|
||||
id: 'Conv1D',
|
||||
mode: 'nn',
|
||||
properties: [],
|
||||
} as Material,
|
||||
position: { x: 0, y: 0 },
|
||||
size: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
attrs: undefined,
|
||||
zIndex: 0,
|
||||
},
|
||||
});
|
||||
// expect(ast.children.length).toBe(3);
|
||||
// expect(ast.children[0]).toBeInstanceOf(VarDecl);
|
||||
// expect(ast.children[1]).toBeInstanceOf(VarDecl);
|
||||
// expect(ast.children[2]).toBeInstanceOf(VarDecl);
|
||||
expect(ast.children[0].codeGen()).toContain('group1');
|
||||
});
|
||||
it('has nest', () => {
|
||||
const nodes = {
|
||||
'node-4': buildNode(1, 3)[0],
|
||||
'node-11': buildNode(1, 10)[0],
|
||||
'group-3': {
|
||||
id: 'group-3',
|
||||
shape: 'group',
|
||||
children: ['node-11'] as any,
|
||||
data: new Material(),
|
||||
position: { x: 0, y: 0 },
|
||||
size: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
attrs: undefined,
|
||||
zIndex: 0,
|
||||
},
|
||||
'group-2': {
|
||||
id: 'group-2',
|
||||
shape: 'group',
|
||||
data: new Material(),
|
||||
position: { x: 0, y: 0 },
|
||||
size: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
attrs: undefined,
|
||||
zIndex: 0,
|
||||
children: ['node-4', 'group-3'] as any,
|
||||
},
|
||||
'node-2': {
|
||||
id: 'node-2',
|
||||
shape: 'dag-node',
|
||||
data: {
|
||||
id: 'Conv1D',
|
||||
mode: 'nn',
|
||||
properties: [],
|
||||
} as Material,
|
||||
position: { x: 0, y: 0 },
|
||||
size: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
attrs: undefined,
|
||||
zIndex: 0,
|
||||
},
|
||||
'node-1': {
|
||||
id: 'node-1',
|
||||
shape: 'dag-node',
|
||||
data: {
|
||||
id: 'Conv1D',
|
||||
mode: 'nn',
|
||||
properties: [],
|
||||
} as Material,
|
||||
position: { x: 0, y: 0 },
|
||||
size: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
attrs: undefined,
|
||||
zIndex: 0,
|
||||
},
|
||||
};
|
||||
const group = {
|
||||
id: 'group-1',
|
||||
shape: 'group-node',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
size: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
attrs: undefined,
|
||||
zIndex: 0,
|
||||
data: new Material(),
|
||||
children: ['node-1', 'node-2', 'group-2'],
|
||||
};
|
||||
const ast = service.buildGroup(group as any, nodes as any);
|
||||
expect(ast.codeGen()).toBe(`group_group3 = paddle.concat(x=[nodenode11])
|
||||
group_group2 = paddle.concat(x=[group_group3,nodenode4])
|
||||
group_group1 = paddle.concat(x=[group_group2,nodenode2,nodenode1])`);
|
||||
});
|
||||
});
|
||||
it('build', () => {
|
||||
const node: StandardizationNodes = {
|
||||
'node-1': {
|
||||
id: 'node-1',
|
||||
shape: 'node',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
size: { width: 0, height: 0 },
|
||||
attrs: {},
|
||||
zIndex: 0,
|
||||
data: {
|
||||
mode: 'nn',
|
||||
id: 'Conv1D',
|
||||
properties: [],
|
||||
} as Material,
|
||||
},
|
||||
'node-2': {
|
||||
id: 'node-2',
|
||||
shape: 'node',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
size: { width: 0, height: 0 },
|
||||
attrs: {},
|
||||
zIndex: 0,
|
||||
data: {
|
||||
mode: 'nn',
|
||||
id: 'Conv1D',
|
||||
properties: [],
|
||||
} as Material,
|
||||
},
|
||||
layer: {
|
||||
...(buildLayer(1)[0] as Layer),
|
||||
} as any,
|
||||
'node-3': {
|
||||
id: 'node-3',
|
||||
shape: 'node',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
size: { width: 0, height: 0 },
|
||||
attrs: {},
|
||||
zIndex: 0,
|
||||
data: {
|
||||
mode: 'nn',
|
||||
id: 'Conv1D',
|
||||
properties: [],
|
||||
} as Material,
|
||||
},
|
||||
'layer-2': {
|
||||
...buildLayer(1)[0],
|
||||
},
|
||||
group: {
|
||||
id: 'group',
|
||||
shape: 'group',
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
size: { width: 0, height: 0 },
|
||||
attrs: {},
|
||||
zIndex: 0,
|
||||
children: ['node-3', 'layer-2'] as any,
|
||||
data: new Material(),
|
||||
},
|
||||
};
|
||||
const ast = service.build(
|
||||
[
|
||||
node['node-1'],
|
||||
node['node-2'],
|
||||
node['layer'],
|
||||
node['node-3'],
|
||||
node['group'],
|
||||
],
|
||||
node,
|
||||
);
|
||||
expect(ast.codeGen().replace(/\n| /gim, '')).toEqual(
|
||||
`true = True
|
||||
false = False
|
||||
nodenode1 = paddle.nn.Conv1D()
|
||||
nodenode2 = paddle.nn.Conv1D()
|
||||
|
||||
class Layer1:
|
||||
def __init__(self,x):
|
||||
pass
|
||||
|
||||
layer1 = Layer1(x=1)
|
||||
nodenode3 = paddle.nn.Conv1D()
|
||||
group_group = paddle.concat(x=[layer1,nodenode3])
|
||||
model=paddle.nn.Sequential(nodenode1,nodenode2,group_group)`.replace(
|
||||
/\n| /gim,
|
||||
'',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { CodeGenerateService } from '../code-generate.service';
|
||||
import { Cell, Edge } from '../code-generate.schema';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
const groupNestTestCase = JSON.parse(
|
||||
readFileSync('./group-nest-test-case.json').toString(),
|
||||
);
|
||||
|
||||
describe('code generate', () => {
|
||||
let service: CodeGenerateService;
|
||||
beforeEach(async () => {
|
||||
const moudle: TestingModule = await Test.createTestingModule({
|
||||
providers: [CodeGenerateService],
|
||||
}).compile();
|
||||
service = moudle.get<CodeGenerateService>(CodeGenerateService);
|
||||
});
|
||||
it('standardizationEdge', () => {
|
||||
expect(
|
||||
service.standardizationEdge(
|
||||
groupNestTestCase.payload.edges as unknown as Edge[],
|
||||
service.standardizationNode(groupNestTestCase.payload.cells),
|
||||
),
|
||||
).toBeDefined();
|
||||
console.log(
|
||||
service.standardizationEdge(
|
||||
groupNestTestCase.payload.edges as unknown as Edge[],
|
||||
service.standardizationNode(groupNestTestCase.payload.cells),
|
||||
),
|
||||
);
|
||||
});
|
||||
it('sequencingNode', () => {
|
||||
const nodes = service.standardizationNode(
|
||||
service.extract(
|
||||
groupNestTestCase.payload.cells as unknown as Cell[],
|
||||
(cell) => cell.shape.includes('node'),
|
||||
),
|
||||
);
|
||||
const edges = service.standardizationEdge(
|
||||
groupNestTestCase.payload.edges as unknown as Edge[],
|
||||
service.standardizationNode(groupNestTestCase.payload.cells),
|
||||
);
|
||||
const { start, end } = groupNestTestCase.meta;
|
||||
expect(service.sequencingNode(nodes, edges, start, end)).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,386 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Material } from '../material/material.schema';
|
||||
import { Layer } from '../layer/layer.schema';
|
||||
import { Cell } from './code-generate.schema';
|
||||
import { Exception, StandardizationNodes } from './code-generate.service';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
@Injectable()
|
||||
export class AST {
|
||||
/**
|
||||
* # Reference material
|
||||
* @see {@link https://llvm.org/docs/tutorial/MyFirstLanguageFrontend/LangImpl03.html|Kaleidoscope}
|
||||
* @see {@link https://github.com/xkbeyer/liquid|liquid}
|
||||
*/
|
||||
build(cells: Cell[], standardizationNodes: StandardizationNodes) {
|
||||
const ast: IAST = {
|
||||
type: 'root',
|
||||
children: [
|
||||
// I HATE PYTHON
|
||||
new VarDecl('true', new Identifier('True')),
|
||||
new VarDecl('false', new Identifier('False')),
|
||||
],
|
||||
codeGen: () => {
|
||||
// 每个 AstItem 必须存在一个 codeGen 函数,只要根节点调用第一层节点的codeGen
|
||||
// 并且在开发时, 确保其他节点的codeGen函数中也遍历了子节点且调用codeGen就可以完成整个语法树的代码生成
|
||||
// 不过考虑到说动频接换行符太繁琐, 这里包装成了数组, 最后join就可以
|
||||
return ast.children.map((child) => child.codeGen()).join('\n');
|
||||
},
|
||||
};
|
||||
for (const cell of cells) {
|
||||
const data = cell.data as Material | Layer;
|
||||
let item;
|
||||
if (this.isGroup(cell)) {
|
||||
item = this.buildGroup(cell, standardizationNodes);
|
||||
ast.children.push(item);
|
||||
continue;
|
||||
}
|
||||
if (this.isLayer(data)) {
|
||||
item = this.buildLayer(data);
|
||||
ast.children.push(item);
|
||||
const callee = new Identifier(data.clazz);
|
||||
const clazzInstance = new CallExpression(
|
||||
callee,
|
||||
data.properties.map((v) => `${v.id}=${v.data}`),
|
||||
);
|
||||
const instance = new VarDecl(cell.id.replace('-', ''), clazzInstance);
|
||||
ast.children.push(instance);
|
||||
continue;
|
||||
}
|
||||
if (this.isNN(data)) {
|
||||
item = this.buildNN(data, cell.id);
|
||||
ast.children.push(item);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ast.children.push(new VarDecl('model', this.buildSequential(ast)));
|
||||
return ast;
|
||||
}
|
||||
/**
|
||||
* Eg.
|
||||
* abcd -> 'abcd'
|
||||
*/
|
||||
standardization(val: unknown, type?: string): string {
|
||||
if (val === 'true' || val === 'false') {
|
||||
return val;
|
||||
}
|
||||
if (type === 'list') {
|
||||
return `[${(val as any[]).map((v) =>
|
||||
typeof v !== 'string' ? JSON.stringify(v) : `'${v}'`,
|
||||
)}]`;
|
||||
}
|
||||
if (type === 'boolean') {
|
||||
return `${Boolean(val)}`;
|
||||
}
|
||||
if (type === 'string') {
|
||||
return `'${val}'`;
|
||||
}
|
||||
return `${val}`;
|
||||
}
|
||||
standardizationParamAttr(key: string, val: string) {
|
||||
switch (key) {
|
||||
case 'name':
|
||||
return this.standardization(
|
||||
val === '' ? randomUUID().replace(/\-/gim, '') : val,
|
||||
'string',
|
||||
);
|
||||
case 'learning_rate':
|
||||
return this.standardization(val, 'number');
|
||||
case 'trainable':
|
||||
return this.standardization(val, 'boolean');
|
||||
case 'do_model_average':
|
||||
return this.standardization(val, 'boolean');
|
||||
case 'need_clip':
|
||||
return this.standardization(val, 'boolean');
|
||||
default:
|
||||
return val;
|
||||
}
|
||||
}
|
||||
buildNN(nn: Material, cellId: string) {
|
||||
/*
|
||||
* 构建 arguments, 是一个string数组, 这样下一步在 new CallExpress或者 new VarDecl时就不需要再次递归调用`codeGen`了
|
||||
*/
|
||||
const args = nn.properties
|
||||
.map((v) => {
|
||||
const id = v.id;
|
||||
if (typeof v.data === 'object' && !Array.isArray(v.data)) {
|
||||
const args: string[] = [];
|
||||
for (const key of Object.keys(v.data)) {
|
||||
if (v.data[key] !== undefined) {
|
||||
const val =
|
||||
v.type === 'ParamAttr'
|
||||
? this.standardizationParamAttr(key, v.data[key])
|
||||
: this.standardization(
|
||||
v.data[key] ?? v.default ?? 'None',
|
||||
v.type,
|
||||
);
|
||||
args.push(new VarDecl(key, new Identifier(val)).codeGen());
|
||||
}
|
||||
}
|
||||
const callExpression = new CallExpression(
|
||||
new Identifier(v.type),
|
||||
args,
|
||||
);
|
||||
const varDecl = new VarDecl(id, callExpression);
|
||||
return varDecl.codeGen();
|
||||
}
|
||||
if (v.data) {
|
||||
return new VarDecl(
|
||||
id,
|
||||
// 标准化参数
|
||||
// 比如字符串 abcd 会被标准化为 'abcd'
|
||||
new Identifier(this.standardization(v.data, v.type)),
|
||||
).codeGen();
|
||||
}
|
||||
})
|
||||
.filter((v) => v !== undefined);
|
||||
const callee = new Identifier(`paddle.nn.${nn.id}`);
|
||||
const fnCall = new CallExpression(callee, args);
|
||||
const varDecl = new VarDecl(`node${cellId.replace(/-/gim, '')}`, fnCall);
|
||||
return varDecl;
|
||||
}
|
||||
extractGroup(group: Cell, nodeTable: StandardizationNodes) {
|
||||
const stack: Cell[] = [];
|
||||
if (this.isGroup(group)) {
|
||||
stack.push(group);
|
||||
}
|
||||
for (const child of (group.children as unknown as string[]) ?? []) {
|
||||
// 考虑group嵌套问题
|
||||
if (this.isGroup(nodeTable[child])) {
|
||||
stack.push(...this.extractGroup(nodeTable[child], nodeTable));
|
||||
}
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
isChild(group: Cell, child: Cell) {
|
||||
return (group.children as unknown as string[]).includes(child.id);
|
||||
}
|
||||
/**
|
||||
* @description
|
||||
*
|
||||
* Layer本质是一组代码, 直接用ClazzDef一下就好.
|
||||
*/
|
||||
buildLayer(layer: Layer) {
|
||||
const clazzDef = new ClazzDef(layer.code);
|
||||
return clazzDef;
|
||||
}
|
||||
buildGroup(group: Cell, standardizationNodes: StandardizationNodes) {
|
||||
const ast = new GroupAst();
|
||||
const stack: [Cell, 'start' | 'node' | 'end'][] = [];
|
||||
const groups = this.extractGroup(group, standardizationNodes);
|
||||
for (const g of groups) {
|
||||
stack.push([g, 'start']);
|
||||
for (const child of (g.children as unknown as string[]) ?? []) {
|
||||
if (standardizationNodes[child]) {
|
||||
stack.push([standardizationNodes[child], 'node']);
|
||||
}
|
||||
}
|
||||
stack.push([g, 'end']);
|
||||
}
|
||||
let activeGroup: Cell | null = null;
|
||||
/**
|
||||
* 如果不做处理可能会出现
|
||||
* node_1 = ...
|
||||
* node_2 = ...
|
||||
* group_1 = paddle.concat(node_1,node_2)
|
||||
* node_3 = ...
|
||||
* group_2 = paddle.concat(node_1,node_2,node_3, group_1)
|
||||
* 本质与LC. 20是同一种思路
|
||||
* @see https://leetcode.cn/problems/valid-parentheses/description/
|
||||
*/
|
||||
while (stack.length) {
|
||||
const [cell, type] = stack.pop();
|
||||
const children = [];
|
||||
if (type === 'end') {
|
||||
activeGroup = cell;
|
||||
}
|
||||
if (type === 'node') {
|
||||
if (this.isGroup(cell)) {
|
||||
children.push(`group_${cell.id.replace(/-/gim, '')}`);
|
||||
} else {
|
||||
if (this.isNN(cell.data)) {
|
||||
children.push(`node${cell.id.replace(/-/gim, '')}`);
|
||||
}
|
||||
if (this.isLayer(cell.data)) {
|
||||
children.push(`${cell.id.replace('-', '')}`);
|
||||
}
|
||||
}
|
||||
while (true) {
|
||||
const [cell, type] = stack.pop();
|
||||
if (type === 'start') {
|
||||
break;
|
||||
}
|
||||
if (this.isGroup(cell)) {
|
||||
children.push(`group_${cell.id.replace(/-/gim, '')}`);
|
||||
} else {
|
||||
if (this.isNN(cell.data)) {
|
||||
children.push(`node${cell.id.replace(/-/gim, '')}`);
|
||||
}
|
||||
if (this.isLayer(cell.data)) {
|
||||
children.push(`${cell.id.replace('-', '')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!activeGroup) {
|
||||
throw new Exception('Schema错误, 请检查Schema格式');
|
||||
}
|
||||
const callee = new Identifier('paddle.concat');
|
||||
/**
|
||||
* @see https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/concat_cn.html
|
||||
*/
|
||||
const call = new CallExpression(callee, [
|
||||
['x=[', children.join(','), ']'].join(''),
|
||||
]);
|
||||
const concatVar = new VarDecl(
|
||||
`group_${activeGroup.id}`.replace(/-/gim, ''),
|
||||
call,
|
||||
);
|
||||
ast.children.push(concatVar);
|
||||
ast.childId.push(...children);
|
||||
}
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
buildSequential(ast: IAST) {
|
||||
const stack = [
|
||||
...ast.children
|
||||
.map((child) => {
|
||||
// 是变量直接拿到变量名
|
||||
if (child instanceof VarDecl) {
|
||||
return child.name;
|
||||
}
|
||||
// 这里不过是为了方便开发时区分, 将Group做了一个新的AST root, 被称作GroupAst
|
||||
//
|
||||
if (child instanceof GroupAst) {
|
||||
return child.children.map((child) =>
|
||||
child instanceof VarDecl ? child.name : null,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.flat()
|
||||
.filter(
|
||||
(child) => child !== null && child !== 'true' && child !== 'false',
|
||||
),
|
||||
];
|
||||
const groups = ast.children
|
||||
.map((child) => (child instanceof GroupAst ? child : null))
|
||||
.filter((child) => child !== null);
|
||||
const childrenId = groups
|
||||
.map((group) => group.childId)
|
||||
.reduce((pre, cur) => [...pre, ...cur], []);
|
||||
return new CallExpression(
|
||||
new Identifier('paddle.nn.Sequential'),
|
||||
stack.filter((item) => !childrenId.includes(item)),
|
||||
);
|
||||
}
|
||||
|
||||
isGroup(cell: Cell) {
|
||||
return cell?.shape && cell.shape.includes('group');
|
||||
}
|
||||
isNN(data: Material | Layer): data is Material {
|
||||
return data.mode === 'nn';
|
||||
}
|
||||
isLayer(data: Material | Layer): data is Layer {
|
||||
return data.mode === 'layer';
|
||||
}
|
||||
}
|
||||
|
||||
// base type
|
||||
export class Node {}
|
||||
|
||||
export class VarDecl implements IVarDecl, Node {
|
||||
public name: string;
|
||||
public val: ASTItem;
|
||||
constructor(name: string, val: ASTItem) {
|
||||
this.name = name;
|
||||
this.val = val;
|
||||
}
|
||||
codeGen() {
|
||||
return `${this.name} = ${this.val.codeGen()}`.replace('\t', '');
|
||||
}
|
||||
}
|
||||
|
||||
export class Identifier implements IIdentifier, Node {
|
||||
name: string;
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
codeGen() {
|
||||
return `${this.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class CallExpression implements ICallExpression, Node {
|
||||
callee: IIdentifier;
|
||||
args: any[];
|
||||
constructor(callee: IIdentifier, args: any[]) {
|
||||
this.callee = callee;
|
||||
this.args = args;
|
||||
}
|
||||
codeGen() {
|
||||
return `${this.callee.codeGen()}(${this.args.join(',')})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class ClazzDef implements IClazzDefine {
|
||||
code: string;
|
||||
constructor(code: string) {
|
||||
this.code = code;
|
||||
}
|
||||
codeGen() {
|
||||
return `${this.code}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class Statement implements IStmt, Node {
|
||||
children: Node[] = [];
|
||||
}
|
||||
|
||||
export class GroupAst implements IGroupAst {
|
||||
type = 'root' as const;
|
||||
children: ASTItem[] = [];
|
||||
childId: string[] = [];
|
||||
codeGen() {
|
||||
return this.children.map((child) => child.codeGen()).join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
type IVarDecl = {
|
||||
name: string;
|
||||
val: ASTItem;
|
||||
codeGen: () => string;
|
||||
};
|
||||
type IIdentifier = {
|
||||
name: string;
|
||||
codeGen: () => string;
|
||||
};
|
||||
type ICallExpression = {
|
||||
callee: IIdentifier;
|
||||
args: any[];
|
||||
codeGen: () => string;
|
||||
};
|
||||
type IClazzDefine = {
|
||||
code: string;
|
||||
codeGen: () => string;
|
||||
};
|
||||
type IStmt = {
|
||||
children: Node[];
|
||||
};
|
||||
|
||||
type ASTItem =
|
||||
| IVarDecl
|
||||
| IIdentifier
|
||||
| ICallExpression
|
||||
| IClazzDefine
|
||||
| IGroupAst;
|
||||
|
||||
type IAST = {
|
||||
type: 'root';
|
||||
children: ASTItem[];
|
||||
codeGen: () => string;
|
||||
};
|
||||
|
||||
interface IGroupAst extends IAST {
|
||||
childId: string[];
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Res,
|
||||
StreamableFile,
|
||||
} from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
@Controller('code-generate')
|
||||
export class CodeGenerateController {
|
||||
@Get(':filename')
|
||||
download(
|
||||
@Param('filename') fileName: string,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): StreamableFile {
|
||||
if (!fileName) {
|
||||
throw new HttpException(
|
||||
'file name can not be undefined',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
const filePath = join(process.cwd(), 'public', fileName + '.py');
|
||||
if (!existsSync(filePath)) {
|
||||
throw new HttpException(`${fileName} not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
res.set({
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Disposition': `attachment; filename="${fileName}.py"`,
|
||||
});
|
||||
return new StreamableFile(createReadStream(filePath));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
import {
|
||||
WebSocketGateway,
|
||||
SubscribeMessage,
|
||||
MessageBody,
|
||||
ConnectedSocket,
|
||||
} from '@nestjs/websockets';
|
||||
import { CodeGenerateService, Exception } from './code-generate.service';
|
||||
import { Cell, GenerateCodeDto } from './code-generate.schema';
|
||||
import { Socket } from 'socket.io';
|
||||
import { AST } from './ast.service';
|
||||
import { UseFilters, ValidationPipe } from '@nestjs/common';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { createHash } from 'crypto';
|
||||
import { join } from 'path';
|
||||
import { cwd } from 'process';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { InjectRedis } from '@liaoliaots/nestjs-redis';
|
||||
import { Redis } from 'ioredis';
|
||||
import { WsExceptionFilter } from '../ws-exception/ws-exception.filter';
|
||||
|
||||
export enum State {
|
||||
err = 'err',
|
||||
finfish = 'finish',
|
||||
progress = 'progress',
|
||||
done = 'done',
|
||||
}
|
||||
|
||||
@WebSocketGateway(9001, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
},
|
||||
})
|
||||
export class CodeGenerateGateway {
|
||||
constructor(
|
||||
private readonly codeGenerateService: CodeGenerateService,
|
||||
private readonly ast: AST,
|
||||
private readonly jwt: JwtService,
|
||||
@InjectRedis() private readonly redis: Redis,
|
||||
) {}
|
||||
@UseFilters(WsExceptionFilter)
|
||||
@SubscribeMessage('createCodeGenerate')
|
||||
async create(
|
||||
@MessageBody(new ValidationPipe()) schema: GenerateCodeDto,
|
||||
@ConnectedSocket() client: Socket,
|
||||
) {
|
||||
const token = client.handshake.headers.authorization;
|
||||
if (!token || !(await this.redis.exists(token))) {
|
||||
client.emit('unauth', '');
|
||||
client.disconnect();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const {
|
||||
handshake: {
|
||||
headers: { authorization },
|
||||
},
|
||||
} = client;
|
||||
this.jwt.verifyAsync(authorization ?? '');
|
||||
} catch {
|
||||
client.emit('unauth', '');
|
||||
client.disconnect();
|
||||
return;
|
||||
}
|
||||
const {
|
||||
meta: { start, end },
|
||||
payload: { cells: cell, edges },
|
||||
} = schema;
|
||||
if (!cell) {
|
||||
client.emitWithAck(State.err, '创建结束, 因为没有节点');
|
||||
return;
|
||||
}
|
||||
if (!cell.length) {
|
||||
client.emitWithAck(State.finfish, '创建结束, 因为图为空');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
client.emitWithAck(State.progress, '检查起始节点');
|
||||
this.codeGenerateService.checkStartNodes(cell, start);
|
||||
client.emitWithAck(State.progress, '检查起始节点通过');
|
||||
client.emitWithAck(State.progress, '检查结束节点');
|
||||
this.codeGenerateService.checkEndNode(cell, end);
|
||||
client.emitWithAck(State.progress, '检查结束节点通过');
|
||||
} catch (e) {
|
||||
const { message } = e as Error;
|
||||
client.emitWithAck(State.err, message);
|
||||
return;
|
||||
}
|
||||
client.emitWithAck(State.progress, '提取节点...');
|
||||
const nodes = this.codeGenerateService.extract<Cell>(cell, (cell: any) =>
|
||||
cell.shape.includes('node'),
|
||||
);
|
||||
client.emitWithAck(State.progress, `节点数量为: ${nodes.length}`);
|
||||
if (!nodes.length) {
|
||||
client.emitWithAck(State.finfish, '生成结束');
|
||||
return;
|
||||
}
|
||||
client.emitWithAck(State.progress, `边数量为: ${edges.length}`);
|
||||
if (!edges.length) {
|
||||
client.emitWithAck(State.finfish, `生成结束, 边节点数量为0`);
|
||||
return;
|
||||
}
|
||||
if (!nodes.length) {
|
||||
client.emitWithAck(State.finfish, '生成结束, 节点数量为0');
|
||||
return;
|
||||
}
|
||||
client.emitWithAck(State.progress, '标准化节点');
|
||||
const standardizationNodes =
|
||||
this.codeGenerateService.standardizationNode(nodes);
|
||||
client.emitWithAck(State.progress, '标准化边');
|
||||
const standardizationEdges = this.codeGenerateService.standardizationEdge(
|
||||
edges,
|
||||
standardizationNodes,
|
||||
);
|
||||
client.emitWithAck(State.progress, '顺序化节点');
|
||||
const sequence = this.codeGenerateService.sequencingNode(
|
||||
standardizationNodes,
|
||||
standardizationEdges,
|
||||
end,
|
||||
start,
|
||||
);
|
||||
client.emitWithAck(State.progress, 'AST构建');
|
||||
const ast = this.ast.build(sequence, standardizationNodes);
|
||||
if (!ast.children) {
|
||||
// transform success
|
||||
client.emitWithAck(State.finfish, 'AST转码成功');
|
||||
return;
|
||||
}
|
||||
client.emitWithAck(State.progress, 'AST转码');
|
||||
const code = ast.codeGen();
|
||||
client.emitWithAck(State.progress, 'AST转码成功');
|
||||
const hash = createHash('sha512').update(code).digest('hex').toString();
|
||||
const fileName = `${hash}-${new Date().getTime()}`;
|
||||
const content = ['import paddle', 'from paddle import *', code].join('\n');
|
||||
const publicPath = join(cwd(), 'public');
|
||||
writeFileSync(join(publicPath, fileName + '.py'), content);
|
||||
return client.emitWithAck(State.done, fileName);
|
||||
}
|
||||
async handleConnection(@ConnectedSocket() socket: Socket) {
|
||||
const token = socket.handshake.headers.authorization;
|
||||
if (!token || !(await this.redis.exists(token))) {
|
||||
socket.emit('unauth', '');
|
||||
socket.disconnect();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.jwt.verifyAsync(socket.handshake.headers.authorization ?? '');
|
||||
} catch {
|
||||
socket.emit('unauth', '');
|
||||
socket.disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { Module, OnModuleInit } from '@nestjs/common';
|
||||
import { CodeGenerateService } from './code-generate.service';
|
||||
import { CodeGenerateGateway } from './code-generate.gateway';
|
||||
import { AST } from './ast.service';
|
||||
import { CodeGenerateController } from './code-generate.controller';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { join } from 'path';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.register({
|
||||
global: true,
|
||||
publicKey: readFileSync(
|
||||
join(process.cwd(), process.env.JWT_PUB_KEY ?? './keys/pub.key'),
|
||||
).toString(),
|
||||
privateKey: readFileSync(
|
||||
join(process.cwd(), process.env.JWT_PRI_KEY ?? './keys/pri.key'),
|
||||
).toString(),
|
||||
signOptions: {
|
||||
algorithm: process.env.JWT_SIGN_ALGORITHM ?? 'RS256',
|
||||
expiresIn: process.env.JWT_EXPIRE_IN ?? '1 day',
|
||||
},
|
||||
}),
|
||||
],
|
||||
controllers: [CodeGenerateController],
|
||||
providers: [CodeGenerateGateway, CodeGenerateService, AST],
|
||||
})
|
||||
export class CodeGenerateModule implements OnModuleInit {
|
||||
onModuleInit() {
|
||||
console.log(
|
||||
join(process.cwd(), process.env.JWT_PUB_KEY ?? './keys/pub.key'),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
import { Enum, Label, Property as TProperty } from '@app/shared';
|
||||
import { Layer as LayerSchema } from '../layer/layer.schema';
|
||||
|
||||
class Meta {
|
||||
@IsString()
|
||||
start: string;
|
||||
@IsString()
|
||||
end: string;
|
||||
}
|
||||
|
||||
class Property implements TProperty {
|
||||
id: string;
|
||||
label: Label;
|
||||
type: string;
|
||||
default: string | number | null | boolean;
|
||||
enums: Enum[];
|
||||
data: any;
|
||||
}
|
||||
|
||||
class Material {
|
||||
@IsObject()
|
||||
label: Label;
|
||||
@IsString()
|
||||
id: string;
|
||||
@IsString()
|
||||
desc: string;
|
||||
@IsBoolean()
|
||||
nn: boolean;
|
||||
@IsArray()
|
||||
properties: Property[];
|
||||
@IsString()
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export class Layer extends LayerSchema {
|
||||
@IsString()
|
||||
id: string;
|
||||
@IsObject()
|
||||
lable: Label;
|
||||
@IsString()
|
||||
code: string;
|
||||
@IsString()
|
||||
clazz: string;
|
||||
@IsArray()
|
||||
properties: Property[];
|
||||
@IsString()
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export class Cell {
|
||||
@IsString()
|
||||
id: string;
|
||||
@IsString()
|
||||
shape: string;
|
||||
@IsObject()
|
||||
position: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
@IsObject()
|
||||
size: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
@IsObject()
|
||||
attrs: object;
|
||||
@IsNumber()
|
||||
zIndex: number;
|
||||
@IsObject()
|
||||
data: Material | Layer;
|
||||
@IsObject()
|
||||
children?: string[];
|
||||
}
|
||||
export class Edge {
|
||||
@IsString()
|
||||
id: string;
|
||||
@IsString()
|
||||
shape: string;
|
||||
@IsObject()
|
||||
source: {
|
||||
cell: string;
|
||||
port: string;
|
||||
};
|
||||
@IsObject()
|
||||
target: {
|
||||
cell: string;
|
||||
port: string;
|
||||
};
|
||||
@IsObject()
|
||||
attr: object;
|
||||
@IsNumber()
|
||||
zIndex: number;
|
||||
@IsString()
|
||||
parent: string;
|
||||
}
|
||||
|
||||
export class GenerateCodeDto {
|
||||
@IsObject()
|
||||
meta: Meta;
|
||||
@IsObject()
|
||||
payload: {
|
||||
cells: (Cell | Edge)[];
|
||||
edges: Edge[];
|
||||
};
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Cell, Edge } from './code-generate.schema';
|
||||
|
||||
export type StandardizationEdges = {
|
||||
[source: string]: Set<string>;
|
||||
};
|
||||
|
||||
export type StandardizationNodes = {
|
||||
[id: string]: Cell;
|
||||
};
|
||||
|
||||
export class Exception extends Error {}
|
||||
|
||||
@Injectable()
|
||||
export class CodeGenerateService {
|
||||
checkStartNodes(cells: (Cell | Edge)[], startId: string) {
|
||||
const nodes = cells.filter(({ id }) => id === startId);
|
||||
if (!nodes.length) {
|
||||
throw new Error('找不到起始节点');
|
||||
}
|
||||
}
|
||||
checkEndNode(cells: (Cell | Edge)[], endId: string) {
|
||||
const nodes = cells.filter(({ id }) => id === endId);
|
||||
if (!nodes.length) {
|
||||
throw new Exception('找不到结束节点');
|
||||
}
|
||||
if (nodes.length > 1) {
|
||||
throw new Exception('结束节点数量大于1, 请考虑合并或修改网络结构');
|
||||
}
|
||||
}
|
||||
extract<R>(cell: (Cell | Edge)[], fn: (cell: Cell | Edge) => boolean) {
|
||||
return cell.filter(fn) as unknown as R[];
|
||||
}
|
||||
standardizationEdge(edges: Edge[], nodes: StandardizationNodes) {
|
||||
const obj: StandardizationEdges = {};
|
||||
for (const edge of edges) {
|
||||
const { source, target } = edge;
|
||||
if (obj[target.cell]) {
|
||||
obj[target.cell].add(source.cell);
|
||||
} else {
|
||||
obj[target.cell] = new Set([source.cell]);
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
standardizationNode(cells: Cell[]) {
|
||||
const obj: StandardizationNodes = {};
|
||||
for (const cell of cells) {
|
||||
obj[cell.id] = cell;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
getChildren(
|
||||
id: string,
|
||||
edge: StandardizationEdges,
|
||||
nodes: StandardizationNodes,
|
||||
) {
|
||||
const childIds = edge[id];
|
||||
if (!childIds || !childIds.size) {
|
||||
return [];
|
||||
}
|
||||
const children = [];
|
||||
for (const childId of childIds) {
|
||||
const child = nodes[childId];
|
||||
children.push(child);
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* 业务排序,start - end可能不一定是拓补上的有序。例如可能是
|
||||
* ```
|
||||
* --------
|
||||
* v |
|
||||
* --------- |
|
||||
* | end | |
|
||||
* --------- |
|
||||
* |
|
||||
* |
|
||||
* |
|
||||
* |
|
||||
* ----------- |
|
||||
* | start | |
|
||||
* ----------- |
|
||||
* | |
|
||||
* -------
|
||||
* ```
|
||||
*
|
||||
* 上图所示, 所以需要对节点进行排序。变为一个栈,即
|
||||
* ```
|
||||
* | |
|
||||
* |---------------|
|
||||
* | start | <----- Stack Top
|
||||
* |---------------|
|
||||
* | end | <----- Stack Bottom
|
||||
* +---------------+
|
||||
```
|
||||
*/
|
||||
sequencingNode(
|
||||
nodes: StandardizationNodes,
|
||||
edges: StandardizationEdges,
|
||||
endId: string,
|
||||
startId: string,
|
||||
) {
|
||||
debugger;
|
||||
const endNode = nodes[endId];
|
||||
const sequence: Cell[] = [endNode];
|
||||
const visitor = (id: string) => {
|
||||
if (!id || id === startId) {
|
||||
return;
|
||||
}
|
||||
const edgesArr = Array.from(edges[id] ?? []);
|
||||
for (const edge of edgesArr) {
|
||||
const connectedNode = nodes[edge];
|
||||
if (connectedNode) {
|
||||
sequence.unshift(connectedNode);
|
||||
if (connectedNode.shape.includes('group')) {
|
||||
for (const child of connectedNode.children) {
|
||||
visitor(child);
|
||||
}
|
||||
}
|
||||
visitor(connectedNode.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
visitor(endId);
|
||||
return sequence;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { Cell, Layer } from './code-generate.schema';
|
||||
|
||||
export const layerCodeGen = (layer: Layer) => {
|
||||
const { code, id, clazz, properties } = layer;
|
||||
const args = properties.map(({ data }) => data);
|
||||
const call = [clazz, '(', args.join(','), ')'].join('');
|
||||
return [id, '=', call].join('');
|
||||
};
|
||||
|
||||
export const nnCodeGen = (cell: Cell) => {
|
||||
const {
|
||||
data: { id, properties },
|
||||
} = cell;
|
||||
const args = properties.map(({ data }) => data);
|
||||
const stack = [id, '(', args, ')'];
|
||||
return stack.join('');
|
||||
};
|
|
@ -0,0 +1,59 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LayerService } from '../layer.service';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Layer, LayerSchema } from '../layer.schema';
|
||||
import { DbModule } from '@app/database';
|
||||
|
||||
describe('LayerService', () => {
|
||||
let service: LayerService;
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
DbModule,
|
||||
MongooseModule.forFeature([{ name: Layer.name, schema: LayerSchema }]),
|
||||
],
|
||||
providers: [LayerService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LayerService>(LayerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('Get All Layer', async () => {
|
||||
const id = (
|
||||
await service.saveLayer({
|
||||
label: {
|
||||
en_US: '',
|
||||
zh_CN: '',
|
||||
},
|
||||
properties: [],
|
||||
code: '',
|
||||
clazz: '',
|
||||
})
|
||||
).id;
|
||||
expect(service.findAll()).resolves.toHaveLength(1);
|
||||
});
|
||||
it('Create Layer', () => {
|
||||
expect(service.saveLayer({} as any)).resolves.not.toThrow();
|
||||
});
|
||||
it('Delete Layer', async () => {
|
||||
const id = (
|
||||
await service.saveLayer({
|
||||
label: {
|
||||
en_US: '',
|
||||
zh_CN: '',
|
||||
},
|
||||
properties: [],
|
||||
code: '',
|
||||
clazz: '',
|
||||
})
|
||||
).id;
|
||||
expect(service.deleteLayer({ id })).resolves.toBeUndefined();
|
||||
});
|
||||
it('Delete Layer But not exists', () => {
|
||||
expect(service.deleteLayer({ id: 'not exists record' })).rejects.toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
import { IsProperties, Label, Property } from '@app/shared';
|
||||
import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateLayerDto {
|
||||
@IsOptional()
|
||||
@IsString({ message: 'Id should be string' })
|
||||
id?: string;
|
||||
@IsObject()
|
||||
label: Label;
|
||||
@IsString()
|
||||
code: string;
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
clazz: string;
|
||||
@IsProperties({
|
||||
message: 'properties should be array, please check field',
|
||||
})
|
||||
properties: Property[];
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IsString } from 'class-validator';
|
||||
|
||||
export class DeleteLayer {
|
||||
@IsString()
|
||||
id: string;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Delete,
|
||||
Param,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { LayerService } from './layer.service';
|
||||
import { CreateLayerDto } from './dto/create-layer.dto';
|
||||
import { AuthGuard } from '../auth-guard/auth-guard.guard';
|
||||
|
||||
@Controller('layer')
|
||||
@UseGuards(AuthGuard)
|
||||
export class LayerController {
|
||||
constructor(private readonly layerService: LayerService) {}
|
||||
@Get('/')
|
||||
async getLayerList() {
|
||||
return await this.layerService.findAll();
|
||||
}
|
||||
@Post('/')
|
||||
createLayer(@Body() dto: CreateLayerDto) {
|
||||
return this.layerService.saveLayer(dto);
|
||||
}
|
||||
@Delete('/:id')
|
||||
deleteLayer(@Param('id') id: string) {
|
||||
return this.layerService.deleteLayer({ id });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { LayerService } from './layer.service';
|
||||
import { LayerController } from './layer.controller';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Layer, LayerSchema } from './layer.schema';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([{ name: Layer.name, schema: LayerSchema }]),
|
||||
],
|
||||
controllers: [LayerController],
|
||||
providers: [LayerService],
|
||||
})
|
||||
export class LayerModule {}
|
|
@ -0,0 +1,22 @@
|
|||
import { Label, Property } from '@app/shared';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
|
||||
@Schema({ autoCreate: true })
|
||||
export class Layer {
|
||||
@Prop()
|
||||
id: string;
|
||||
@Prop({ type: () => Object })
|
||||
label: Label;
|
||||
@Prop()
|
||||
code: string;
|
||||
@Prop()
|
||||
clazz: string;
|
||||
@Prop({ type: () => Array })
|
||||
properties: Property[];
|
||||
@Prop({ type: () => Boolean })
|
||||
del: boolean;
|
||||
@Prop({ type: () => String })
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export const LayerSchema = SchemaFactory.createForClass(Layer);
|
|
@ -0,0 +1,34 @@
|
|||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Layer } from './layer.schema';
|
||||
import { Model, Types } from 'mongoose';
|
||||
import { CreateLayerDto } from './dto/create-layer.dto';
|
||||
import { DeleteLayer } from './dto/delete-layer.dto';
|
||||
|
||||
@Injectable()
|
||||
export class LayerService {
|
||||
constructor(@InjectModel(Layer.name) private LayerModel: Model<Layer>) {}
|
||||
async findAll() {
|
||||
return await this.LayerModel.find({});
|
||||
}
|
||||
async saveLayer(dto: CreateLayerDto) {
|
||||
const data = {
|
||||
id: new Types.ObjectId(),
|
||||
...dto,
|
||||
mode: 'layer',
|
||||
};
|
||||
return await this.LayerModel.create(data);
|
||||
}
|
||||
async deleteLayer(dto: DeleteLayer) {
|
||||
const { id } = dto;
|
||||
const layerExists = await this.isExists(id);
|
||||
if (!layerExists) {
|
||||
throw new HttpException('Layer not exists', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
await this.LayerModel.deleteOne({ id });
|
||||
}
|
||||
private async isExists(id: string) {
|
||||
const data = await this.LayerModel.find({ id });
|
||||
return data.length >= 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
async function bootstrap() {
|
||||
if (__DEV__) {
|
||||
config({
|
||||
path: '.env',
|
||||
debug: true,
|
||||
});
|
||||
}
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
await app.listen(9000);
|
||||
}
|
||||
bootstrap();
|
|
@ -0,0 +1,69 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { MaterialService } from '../material.service';
|
||||
import { Material, MaterialSchema } from '../material.schema';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { DbModule } from '@app/database';
|
||||
import { RedisModule } from '@app/redis';
|
||||
|
||||
describe('MaterialService', () => {
|
||||
let service: MaterialService;
|
||||
let id = '';
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
DbModule,
|
||||
MongooseModule.forFeature([
|
||||
{
|
||||
name: Material.name,
|
||||
schema: MaterialSchema,
|
||||
},
|
||||
]),
|
||||
RedisModule,
|
||||
],
|
||||
providers: [MaterialService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<MaterialService>(MaterialService);
|
||||
id = (
|
||||
await service.createMaterial({
|
||||
id: 'TestLayer',
|
||||
label: {
|
||||
en_US: 'Test',
|
||||
zh_CN: '测试',
|
||||
},
|
||||
desc: '',
|
||||
properties: [],
|
||||
nn: true,
|
||||
mode: 'nn',
|
||||
})
|
||||
).id;
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
it('getAll', () => {
|
||||
expect(service.findAll()).resolves.toHaveLength(1);
|
||||
});
|
||||
it('delete', () => {
|
||||
expect(service.DeleteMaterial({ id })).resolves.toBe(true);
|
||||
});
|
||||
it('delete but not exiets', () => {
|
||||
expect(service.DeleteMaterial({ id: 'not exists' })).rejects.toThrow();
|
||||
});
|
||||
it('create but exists', () => {
|
||||
expect(
|
||||
service.createMaterial({
|
||||
id,
|
||||
label: {
|
||||
en_US: 'Test',
|
||||
zh_CN: '测试',
|
||||
},
|
||||
desc: '',
|
||||
properties: [],
|
||||
nn: true,
|
||||
mode: 'nn',
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import { IsProperties, Label, Property } from '@app/shared';
|
||||
import { IsBoolean, IsObject, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CretaeMaterial {
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
desc: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
nn: boolean;
|
||||
|
||||
@IsString()
|
||||
mode: string;
|
||||
|
||||
@IsObject()
|
||||
label: Label;
|
||||
|
||||
@IsProperties()
|
||||
properties: Property[];
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { Prop } from '@nestjs/mongoose';
|
||||
|
||||
export class DeleteMaterial {
|
||||
@Prop()
|
||||
id: string;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { MaterialService } from './material.service';
|
||||
import { AuthGuard } from '../auth-guard/auth-guard.guard';
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@Controller('material')
|
||||
export class MaterialController {
|
||||
constructor(private readonly materialService: MaterialService) {}
|
||||
@Get()
|
||||
async getAll() {
|
||||
return {
|
||||
data: await this.materialService.findAll(),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MaterialService } from './material.service';
|
||||
import { MaterialController } from './material.controller';
|
||||
import { DbModule } from '@app/database';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Material, MaterialSchema } from './material.schema';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DbModule,
|
||||
MongooseModule.forFeature([
|
||||
{
|
||||
name: Material.name,
|
||||
schema: MaterialSchema,
|
||||
},
|
||||
]),
|
||||
],
|
||||
controllers: [MaterialController],
|
||||
providers: [MaterialService],
|
||||
})
|
||||
export class MaterialModule {}
|
|
@ -0,0 +1,20 @@
|
|||
import { Label, Property } from '@app/shared';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
|
||||
@Schema({ autoCreate: true })
|
||||
export class Material {
|
||||
@Prop({ type: () => Object })
|
||||
label: Label;
|
||||
@Prop({ type: () => String })
|
||||
id: string;
|
||||
@Prop({ type: () => String })
|
||||
desc: string;
|
||||
@Prop({ type: () => Boolean })
|
||||
nn: boolean;
|
||||
@Prop({ type: () => Array })
|
||||
properties: Property[];
|
||||
@Prop({ type: () => String })
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export const MaterialSchema = SchemaFactory.createForClass(Material);
|
|
@ -0,0 +1,45 @@
|
|||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Material } from './material.schema';
|
||||
import { Model } from 'mongoose';
|
||||
import { CretaeMaterial } from './dto/cretae-material.dto';
|
||||
import { DeleteMaterial } from './dto/delete-material.dto';
|
||||
import { InjectRedis } from '@liaoliaots/nestjs-redis';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
@Injectable()
|
||||
export class MaterialService {
|
||||
constructor(
|
||||
@InjectRedis()
|
||||
private readonly redis: Redis,
|
||||
@InjectModel(Material.name)
|
||||
private readonly MaterialMode: Model<Material>,
|
||||
) {}
|
||||
async findAll() {
|
||||
const typesRaw = await this.redis.hgetall('types');
|
||||
const types = [];
|
||||
for (const [key, value] of Object.entries(typesRaw)) {
|
||||
types.push([key, JSON.parse(value)]);
|
||||
}
|
||||
return {
|
||||
types: Object.fromEntries(types),
|
||||
materials: await this.MaterialMode.find(),
|
||||
};
|
||||
}
|
||||
async createMaterial(data: CretaeMaterial) {
|
||||
const material = await this.MaterialMode.find({ id: data.id });
|
||||
if (material.length) {
|
||||
throw new HttpException(`${data.id} exists`, HttpStatus.CONFLICT);
|
||||
}
|
||||
return this.MaterialMode.create({ ...data });
|
||||
}
|
||||
async DeleteMaterial(data: DeleteMaterial) {
|
||||
const { id } = data;
|
||||
const material = await this.MaterialMode.find({ id });
|
||||
if (!material.length) {
|
||||
throw new HttpException(`Material ${id} not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
await this.MaterialMode.deleteOne({ id });
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ProjectService } from '../project.service';
|
||||
import { DbModule } from '@app/database';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Project, ProjectSchema } from '../entities/project.entity';
|
||||
import { RedisModule } from '@app/redis';
|
||||
import { UserService } from '../../user/user.service';
|
||||
import { User, UserSchema } from '../../user/user.schema';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { configDotenv } from 'dotenv';
|
||||
|
||||
describe('ProjectService', () => {
|
||||
let service: ProjectService;
|
||||
let userService: UserService;
|
||||
let token = '';
|
||||
|
||||
beforeAll(async () => {
|
||||
configDotenv({
|
||||
path: '.env',
|
||||
});
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
imports: [
|
||||
DbModule,
|
||||
MongooseModule.forFeature([
|
||||
{
|
||||
name: Project.name,
|
||||
schema: ProjectSchema,
|
||||
},
|
||||
{
|
||||
name: User.name,
|
||||
schema: UserSchema,
|
||||
},
|
||||
]),
|
||||
RedisModule,
|
||||
JwtModule.register({
|
||||
secret: 'test',
|
||||
signOptions: {
|
||||
algorithm: 'none',
|
||||
expiresIn: process.env.JWT_EXPIRE_IN ?? '24h',
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [ProjectService, UserService],
|
||||
}).compile();
|
||||
service = module.get<ProjectService>(ProjectService);
|
||||
userService = module.get<UserService>(UserService);
|
||||
await userService.register({
|
||||
nick: 'test-1',
|
||||
password: '123456789Sd!',
|
||||
email: 'test@no-reply.com',
|
||||
});
|
||||
token = await userService.login({
|
||||
email: 'test@no-reply.com',
|
||||
password: '123456789Sd!',
|
||||
});
|
||||
expect(token).toBeDefined();
|
||||
expect(token.length).toBeGreaterThan(0);
|
||||
expect(await service.create({ name: 'test' }, token)).toBeDefined();
|
||||
}, 60 * 1000);
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
it('create(exists)', () => {
|
||||
expect(service.create({ name: 'test' }, token)).rejects.toThrow();
|
||||
});
|
||||
it('getProject', (done) => {
|
||||
const p1 = service.getProject(0, 10).then(({ projects }) => {
|
||||
expect(projects).toHaveLength(1);
|
||||
});
|
||||
const p2 = service.getProject(1, 10).then(({ projects }) => {
|
||||
expect(projects).toHaveLength(0);
|
||||
});
|
||||
const p3 = service.getProject(-1, 10).then(({ projects }) => {
|
||||
expect(projects).toHaveLength(1);
|
||||
});
|
||||
const p4 = service.getProject(0, 100).then(({ projects }) => {
|
||||
expect(projects).toHaveLength(1);
|
||||
});
|
||||
Promise.all([p1, p2, p3, p4]).then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('updateProject', () => {
|
||||
expect(service.updateProject(1, { name: 'test-1' })).resolves.toBeDefined();
|
||||
});
|
||||
it('delete project', () => {
|
||||
expect(service.deleteProject(1)).resolves.toBeTruthy();
|
||||
});
|
||||
it('restore project', () => {
|
||||
expect(service.restoreProject(1)).resolves.toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import { IsObject, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateProjectDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
}
|
||||
export class UpdateProjectDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
data?: Record<string, any>;
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
graphData?: Record<string, any>;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { IsString } from 'class-validator';
|
||||
|
||||
export class DeleteProjectDto {
|
||||
@IsString()
|
||||
project_id: string;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { User } from '../../user/user.schema';
|
||||
|
||||
@Schema({ autoCreate: true })
|
||||
export class Project {
|
||||
@Prop({ type: () => Number })
|
||||
projectId: number;
|
||||
@Prop({ type: () => String })
|
||||
name: string;
|
||||
@Prop({ type: () => String })
|
||||
author: string;
|
||||
@Prop({ type: () => Number })
|
||||
createAt: number;
|
||||
@Prop({ type: () => Boolean })
|
||||
removed: boolean;
|
||||
@Prop({ type: () => Object })
|
||||
data: Record<string, any>;
|
||||
@Prop({ type: () => Object })
|
||||
graphData: Record<string, any>;
|
||||
}
|
||||
|
||||
export const ProjectSchema = SchemaFactory.createForClass(Project);
|
|
@ -0,0 +1,78 @@
|
|||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { ProjectService } from './project.service';
|
||||
import { CreateProjectDto, UpdateProjectDto } from './dto/create-project.dto';
|
||||
import { Request } from 'express';
|
||||
|
||||
@Controller('project')
|
||||
export class ProjectController {
|
||||
constructor(private readonly projectService: ProjectService) {}
|
||||
@Get('/')
|
||||
async getProjects(@Query('page') page: number) {
|
||||
return this.projectService.getProject(page);
|
||||
}
|
||||
@Get('/:id')
|
||||
async getProjectInfo(@Param('id') id?: string) {
|
||||
if (!id) {
|
||||
throw new BadRequestException('id 不能为空');
|
||||
}
|
||||
if (Number.isNaN(Number(id))) {
|
||||
throw new BadRequestException('id 应该为数字');
|
||||
}
|
||||
return {
|
||||
data: await this.projectService.getProjectInfo(Number(id)),
|
||||
};
|
||||
}
|
||||
@Post('/')
|
||||
async create(@Body() body: CreateProjectDto, @Req() req: Request) {
|
||||
return {
|
||||
data: await this.projectService.create(
|
||||
body,
|
||||
req.headers.authorization.replace('Bearer', '').trim(),
|
||||
),
|
||||
};
|
||||
}
|
||||
@Patch('/:id')
|
||||
async patchProjectInfo(
|
||||
@Body() body: UpdateProjectDto,
|
||||
@Param('id') id?: string,
|
||||
) {
|
||||
if (!id) {
|
||||
throw new BadRequestException('id 不能为空');
|
||||
}
|
||||
if (Number.isNaN(Number(id))) {
|
||||
throw new BadRequestException('id 应该为数字');
|
||||
}
|
||||
return this.projectService.updateProject(Number(id), body);
|
||||
}
|
||||
@Delete('/:id')
|
||||
async deleteProject(@Param('id') id?: string) {
|
||||
if (!id) {
|
||||
throw new BadRequestException('id 不能为空');
|
||||
}
|
||||
if (Number.isNaN(Number(id))) {
|
||||
throw new BadRequestException('id 应该为数字');
|
||||
}
|
||||
return this.projectService.deleteProject(Number(id));
|
||||
}
|
||||
@Post('/restore/:id')
|
||||
async restoreProject(@Param('id') id?: string) {
|
||||
if (!id) {
|
||||
throw new BadRequestException('id 不能为空');
|
||||
}
|
||||
if (Number.isNaN(Number(id))) {
|
||||
throw new BadRequestException('id 应该为数字');
|
||||
}
|
||||
return this.projectService.restoreProject(Number(id));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ProjectService } from './project.service';
|
||||
import { ProjectController } from './project.controller';
|
||||
import { DbModule } from '@app/database';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Project, ProjectSchema } from './entities/project.entity';
|
||||
import { RedisModule } from '@app/redis';
|
||||
|
||||
@Module({
|
||||
controllers: [ProjectController],
|
||||
providers: [ProjectService],
|
||||
imports: [
|
||||
DbModule,
|
||||
MongooseModule.forFeature([
|
||||
{
|
||||
name: Project.name,
|
||||
schema: ProjectSchema,
|
||||
},
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class ProjectModule {}
|
|
@ -0,0 +1,106 @@
|
|||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { CreateProjectDto, UpdateProjectDto } from './dto/create-project.dto';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Project } from './entities/project.entity';
|
||||
import { Model } from 'mongoose';
|
||||
import { InjectRedis } from '@liaoliaots/nestjs-redis';
|
||||
import { Redis } from 'ioredis';
|
||||
import { User } from '../user/user.schema';
|
||||
|
||||
enum ProjectStatus {
|
||||
busy,
|
||||
empty,
|
||||
}
|
||||
|
||||
interface ProjectItem {
|
||||
// project name
|
||||
name: string;
|
||||
status: ProjectStatus;
|
||||
author: {
|
||||
nick: string;
|
||||
};
|
||||
createAt: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ProjectService {
|
||||
constructor(
|
||||
@InjectModel(Project.name)
|
||||
private ProjectModel: Model<Project>,
|
||||
@InjectRedis()
|
||||
private redis: Redis,
|
||||
) {}
|
||||
private async projectExists(name: string) {
|
||||
const res = await this.ProjectModel.findOne({ name });
|
||||
return Boolean(res);
|
||||
}
|
||||
async create({ name }: CreateProjectDto, token: string) {
|
||||
if (await this.projectExists(name)) {
|
||||
throw new HttpException(`${name} 存在`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const email = await this.redis.get(`${token}:id`);
|
||||
if (!email) {
|
||||
throw new HttpException(`未登录`, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
const project = new this.ProjectModel();
|
||||
const id = Number((await this.redis.get(`project:counter`)) ?? '1');
|
||||
project.projectId = id;
|
||||
project.name = name;
|
||||
project.createAt = new Date().getTime();
|
||||
project.author = email;
|
||||
project.removed = false;
|
||||
project.data = {
|
||||
meta: {
|
||||
start: '',
|
||||
end: '',
|
||||
},
|
||||
payload: {
|
||||
cells: [],
|
||||
edges: [],
|
||||
},
|
||||
};
|
||||
project.graphData = {
|
||||
cells: [],
|
||||
};
|
||||
const res = await project.save();
|
||||
if (!(await this.redis.get('project:counter'))) {
|
||||
await this.redis.set('project:counter', 2);
|
||||
return res;
|
||||
}
|
||||
await this.redis.incr('project:counter');
|
||||
return res;
|
||||
}
|
||||
async getProject(page = 0, size = 10) {
|
||||
const totalPages = Math.round(
|
||||
Number(await this.redis.get(`project:counter`)) / size,
|
||||
);
|
||||
const projects = await this.ProjectModel.find({ removed: false })
|
||||
.limit(size)
|
||||
.skip(Math.max(page, 0) * size)
|
||||
.populate('author', 'nick', User.name);
|
||||
return {
|
||||
projects,
|
||||
totalPages,
|
||||
};
|
||||
}
|
||||
async getProjectInfo(id: number) {
|
||||
return this.ProjectModel.findOne({ projectId: id });
|
||||
}
|
||||
async deleteProject(id: number) {
|
||||
await this.ProjectModel.updateOne({ projectId: id }, { removed: true });
|
||||
return true;
|
||||
}
|
||||
async restoreProject(id: number) {
|
||||
await this.ProjectModel.updateOne({ projectId: id }, { removed: false });
|
||||
return true;
|
||||
}
|
||||
async updateProject(id: number, newProjectInfo: UpdateProjectDto) {
|
||||
return await this.ProjectModel.updateOne(
|
||||
{ projectId: id },
|
||||
{
|
||||
...newProjectInfo,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserService } from '../user.service';
|
||||
import { DbModule } from '@app/database';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { User, UserSchema } from '../user.schema';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { RedisModule } from '@app/redis';
|
||||
import { configDotenv } from 'dotenv';
|
||||
import { readFileSync } from 'fs';
|
||||
import { HttpException } from '@nestjs/common';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeAll(async () => {
|
||||
configDotenv({
|
||||
path: '.env',
|
||||
});
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UserService],
|
||||
imports: [
|
||||
DbModule,
|
||||
MongooseModule.forFeature([
|
||||
{
|
||||
name: User.name,
|
||||
schema: UserSchema,
|
||||
},
|
||||
]),
|
||||
JwtModule.register({
|
||||
// publicKey: readFileSync(
|
||||
// process.env.JWT_PUB_KEY ?? './static/pub.key',
|
||||
// ),
|
||||
// privateKey: readFileSync(
|
||||
// process.env.JWT_PRI_KEY ?? './static/pri.key',
|
||||
// ),
|
||||
secret: 'test',
|
||||
signOptions: {
|
||||
algorithm: 'none',
|
||||
expiresIn: process.env.JWT_EXPIRE_IN ?? '24h',
|
||||
},
|
||||
}),
|
||||
RedisModule,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UserService>(UserService);
|
||||
await service.register({
|
||||
email: 'test@no-reply.com',
|
||||
password: '123456789Sd!',
|
||||
nick: 'tester-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
it('login', () => {
|
||||
expect(
|
||||
service.login({ email: 'test@no-reply.com', password: '123456789Sd!' }),
|
||||
).resolves.not.toBe('');
|
||||
expect(
|
||||
service.login({ email: 'test_@no-reply.com', password: '123456789Sd!' }),
|
||||
).rejects.toThrowError(HttpException);
|
||||
expect(
|
||||
service.login({ email: 'test@no-reply.com', password: '123456789Sd' }),
|
||||
).rejects.toThrowError(HttpException);
|
||||
expect(
|
||||
service.login({ email: 'empty@no-reply.com', password: '123456789Sd' }),
|
||||
).rejects.toThrowError(HttpException);
|
||||
});
|
||||
it('register', () => {
|
||||
expect(
|
||||
service.register({
|
||||
email: '_test@no-reply.com',
|
||||
password: '123456789Sd!',
|
||||
nick: 'nick-2',
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
it('profile', () => {
|
||||
expect(service.getProfile('test@no-reply.com')).resolves.toMatchObject({
|
||||
email: 'test@no-reply.com',
|
||||
nick: 'tester-1',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { LoginDTO, RegisterDTO } from './user.dto';
|
||||
import { AuthGuard } from '../auth-guard/auth-guard.guard';
|
||||
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
@Post('/login')
|
||||
async login(@Body() body: LoginDTO) {
|
||||
return this.userService.login(body);
|
||||
}
|
||||
@Post('/reg')
|
||||
async register(@Body() body: RegisterDTO) {
|
||||
return this.userService.register(body);
|
||||
}
|
||||
@UseGuards(AuthGuard)
|
||||
@Get(':email')
|
||||
async getProfile(@Param('email') email: string) {
|
||||
if (!email) {
|
||||
throw new HttpException(`${email} not found`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return this.userService.getProfile(email);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { IsEmail, IsString } from 'class-validator';
|
||||
|
||||
export class LoginDTO {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class RegisterDTO {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
@IsString()
|
||||
password: string;
|
||||
@IsString()
|
||||
nick: string;
|
||||
}
|
||||
|
||||
export class GetProfileDTO {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { UserController } from './user.controller';
|
||||
import { DbModule } from '@app/database';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { User, UserSchema } from './user.schema';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { RedisModule } from '@app/redis';
|
||||
|
||||
@Module({
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
imports: [
|
||||
DbModule,
|
||||
MongooseModule.forFeature([
|
||||
{
|
||||
name: User.name,
|
||||
schema: UserSchema,
|
||||
},
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class UserModule {}
|
|
@ -0,0 +1,17 @@
|
|||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
|
||||
@Schema({ autoCreate: true, autoIndex: true })
|
||||
export class User {
|
||||
@Prop({ type: () => String })
|
||||
email: string;
|
||||
@Prop({ type: () => String })
|
||||
password: string;
|
||||
@Prop({ type: () => String })
|
||||
nick: string;
|
||||
@Prop({ type: () => Number })
|
||||
create_at: number;
|
||||
@Prop({ type: () => Number })
|
||||
update_at: number;
|
||||
}
|
||||
|
||||
export const UserSchema = SchemaFactory.createForClass(User);
|
|
@ -0,0 +1,72 @@
|
|||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { LoginDTO, RegisterDTO } from './user.dto';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { User } from './user.schema';
|
||||
import { Model } from 'mongoose';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { InjectRedis } from '@liaoliaots/nestjs-redis';
|
||||
import { Redis } from 'ioredis';
|
||||
import { compareSync, hashSync } from 'bcryptjs';
|
||||
import { isEmpty } from 'ramda';
|
||||
import ms from 'ms';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@InjectModel(User.name)
|
||||
private readonly UserModel: Model<User>,
|
||||
private readonly jwt: JwtService,
|
||||
@InjectRedis()
|
||||
private redis: Redis,
|
||||
) {}
|
||||
async login(data: LoginDTO) {
|
||||
const { email, password } = data;
|
||||
const userProfile = await this.UserModel.findOne({
|
||||
email,
|
||||
});
|
||||
if (
|
||||
!userProfile ||
|
||||
isEmpty(userProfile) ||
|
||||
!compareSync(password, userProfile.password)
|
||||
) {
|
||||
throw new HttpException(`${email} is not found`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
const jwt = this.jwt.sign({ email });
|
||||
await this.redis.set(`token:${email}`, jwt);
|
||||
await this.redis.hmset(`profile:${email}`, userProfile.toJSON());
|
||||
await this.redis.set(`${jwt}`, email);
|
||||
await this.redis.set(`${jwt}:id`, userProfile._id.toString());
|
||||
await this.redis.setnx(jwt, ms(process.env.JWT_EXPIRE_IN));
|
||||
await this.redis.setnx(`${jwt}:id`, ms(process.env.JWT_EXPIRE_IN));
|
||||
return { jwt, nick: userProfile.nick };
|
||||
}
|
||||
async register(data: RegisterDTO) {
|
||||
const { email, password, nick } = data;
|
||||
const profile = this.UserModel.findOne({
|
||||
email,
|
||||
});
|
||||
if (!profile) {
|
||||
throw new HttpException('user exists', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const date = new Date();
|
||||
const _password = hashSync(
|
||||
password,
|
||||
Number(process.env.PWD_SALT_LEN ?? process.env.PWD_SALT?.length ?? 10),
|
||||
);
|
||||
const userModel = new this.UserModel();
|
||||
userModel.email = email;
|
||||
userModel.password = _password;
|
||||
userModel.nick = nick;
|
||||
userModel.create_at = date.getTime();
|
||||
userModel.update_at = date.getTime();
|
||||
return userModel
|
||||
.save()
|
||||
.then((user) => user.toJSON())
|
||||
.catch((reason) => {
|
||||
throw new HttpException(reason, HttpStatus.BAD_REQUEST);
|
||||
});
|
||||
}
|
||||
async getProfile(email: string) {
|
||||
return this.UserModel.findOne({ email }).select('nick email');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { WsExceptionFilter } from './ws-exception.filter';
|
||||
|
||||
describe('WsExceptionFilter', () => {
|
||||
it('should be defined', () => {
|
||||
expect(new WsExceptionFilter()).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
|
||||
import { Exception } from '../code-generate/code-generate.service';
|
||||
import { Socket } from 'socket.io';
|
||||
import { State } from '../code-generate/code-generate.gateway';
|
||||
|
||||
@Catch()
|
||||
export class WsExceptionFilter<T> implements ExceptionFilter {
|
||||
catch(exception: T, host: ArgumentsHost) {
|
||||
const ws = host.switchToWs();
|
||||
const client = ws.getClient<Socket>();
|
||||
if (exception instanceof Exception) {
|
||||
client.emit(State.err, exception.message);
|
||||
} else {
|
||||
client.emit(State.err, 'Schema 有误, 请检查');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { AppModule } from './../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect('Hello World!');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"@app/database/(.*)": "<rootDir>/../libs/database/src/$1",
|
||||
"@app/database": "<rootDir>/../libs/database/src",
|
||||
"@app/shared/(.*)": "<rootDir>/../libs/shared/src/$1",
|
||||
"@app/shared": "<rootDir>/../libs/shared/src",
|
||||
"@app/redis/(.*)": "<rootDir>/../libs/redis/src/$1",
|
||||
"@app/redis": "<rootDir>/../libs/redis/src",
|
||||
"@app/auth-guard/(.*)": "<rootDir>/../libs/auth-guard/src/$1",
|
||||
"@app/auth-guard": "<rootDir>/../libs/auth-guard/src"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"esModuleInterop": true,
|
||||
"paths": {
|
||||
"@app/database": [
|
||||
"libs/database/src"
|
||||
],
|
||||
"@app/database/*": [
|
||||
"libs/database/src/*"
|
||||
],
|
||||
"@app/shared": [
|
||||
"libs/shared/src"
|
||||
],
|
||||
"@app/shared/*": [
|
||||
"libs/shared/src/*"
|
||||
],
|
||||
"@app/redis": [
|
||||
"libs/redis/src"
|
||||
],
|
||||
"@app/redis/*": [
|
||||
"libs/redis/src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { DefinePlugin } = require('webpack');
|
||||
/**
|
||||
*
|
||||
* @param {import('webpack').Configuration} option
|
||||
* @returns {import('webpack').Configuration}
|
||||
*/
|
||||
module.exports = (option) => {
|
||||
return {
|
||||
...option,
|
||||
plugins: [
|
||||
new DefinePlugin({
|
||||
__DEV__: process.env.NODE_ENV === 'DEV' ?? false,
|
||||
__TEST__: process.env.NODE_ENV === 'TEST' ?? false,
|
||||
}),
|
||||
],
|
||||
};
|
||||
};
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,36 @@
|
|||
services:
|
||||
mongodb:
|
||||
image: mongo
|
||||
ports:
|
||||
- 27018:27017
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
front:
|
||||
image: gaonengwww/dl-flow-frontend
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
server:
|
||||
image: gaonengwww/dl-flow-backend
|
||||
ports:
|
||||
- 9000:9000
|
||||
environment:
|
||||
- DB_URL=mongodb://mongodb:27017/dl-flow # 数据库地址
|
||||
- REDIS_HOST=redis # redis地址 (必填)
|
||||
- REDIS_PORT=6379 # redis端口 (必填)
|
||||
- REDIS_DB=0 # redis数据库 (必填)
|
||||
- REDIS_PASSWORD="" # redis密码
|
||||
- JWT_EXPIRE_IN=1d # JWT 过期时间 (必填)
|
||||
- JWT_SIGN_ALGORITHM=RS256 # JWT签名算法, 要与密钥对符合, 例如密钥对是RSA 2048bit, 那么此处应该是 RS256 (必填)
|
||||
- JWT_PUB_KEY=./keys/pub.key # JWT 公钥 (必填)
|
||||
- JWT_PRI_KEY=./keys/pri.key # JWT 私钥 (必填)
|
||||
- PWD_SALT=salt # bcrypt 盐(必填)
|
||||
- PWD_SALT_LEN=12 # bcrypt 盐(必填)
|
||||
volumes: # 强烈将下述卷挂载到本地, 以避免数据丢失
|
||||
- ./public:/public # 代码生成暂存位置
|
||||
- ./keys:/keys # 密钥对存放位置
|
||||
- ./data:/data # bundle.json与install.lock 存放位置
|
||||
- ./examples:/examples
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAr2epjP5hwAOfq7pM/9yy3LdkWTap+bYS1HUuqQRlddB10iPU
|
||||
RwGE+KwadSeXCXLNk+j8nXoeXYfBx9wQUqj0yP0voSVfa6NgKa9FSnyLLmD+ac1c
|
||||
uCfNfa21pgS9Te1NbVEMyV+ihDWXTpFwAZJo925VGVE/xaR2EcSC1lQGFAce+zRk
|
||||
sIoFp3iwcRA92a8RWVJAhjNrj8MSJBzcNHPc26rnAMVwaeQS4GHNqJsOYZUJNr9q
|
||||
wHWlcw6eyH6qjvX2VF1y1kkobK0/hpe8abQ447UWh19Tpo/xd5e5KWjuFqe8CV6z
|
||||
vOoAWIy33HZ0IJwjbTPTYv9iV0vNDz/Cn5xHfwIDAQABAoIBAQCFbjf+d2xootkN
|
||||
y5TTdlHhsOh3LSw0Nxsv30x1alC8RK28A2Fx+mfquxENWeZ9W4WjJLM2IDWFNMZm
|
||||
gLMyDjDXzDI1RbbGrOt0Ck6NkRxXVZVzarNDq5OYLVJnTmerJf+mTueJMGTyacoG
|
||||
DIDF0VhkENxPfA0lDix666AT5qsRA+8kDdgH6le1Bht2KgdsOkBj+pNZ0uJ63Xnt
|
||||
hUcwWDMQ9jXGbdCRTBP/gX6ncsoyZXGiAyWL1nTThxZFoaCdbJJr8Cp2k9Fu93TJ
|
||||
8Hg7DqylKeQ4bDiXWi+l8MpG2J2Coz5rIw7qa8sByQhHZtqtqbCbUc/JWaLI8804
|
||||
RnIGYoPhAoGBANuxByRUZ1dCxUxvB2CEJhujIwOy1EwCY59qJy8tza2pc2Dg/AnR
|
||||
DUmaVpbwVqhX3ejR8sUopVbEFx6AC6c1eenp/V46Wpvrz+WQpN7HvBrlT/Qmmroa
|
||||
BE2R6NcPIn/i2LFMfosqJyOzLn4kz1eEcLQnO7q0ruy0rvo0Vto+oDVXAoGBAMxk
|
||||
54xuUltAM8C0Fok7W8TIxKtcCHlK7R5ac+51P/McWxXJqiokZQI2al1HXJ32U9Ns
|
||||
SD+Ywi7GUjoqheX4094yrZtJMgMoprIfFXRGesDA2sJ3B/H3LL2Ka/kemr7I18v8
|
||||
eFbClbpttFwJl1jPX7piAQkwFBzEFvlYvUof1D4ZAoGAKWA6/x2iZO3faNjUY9of
|
||||
rz7XXl/02eftSV3dyWXwAdATOeDFtzeXMBCQVFcpiwUZdzrrZTSVhzThQc5N440P
|
||||
y/1UycVlwU31VsAaPRcTF2Gu4EXRCFHUE6PyXWatUbawpxvIDX+/5Vpe3EUkg9Ae
|
||||
xd9JwY08ELqq5darsOjwlXsCgYBpxQW1vBlOM0kUvZyz40235ZUwf+26prVR8cjw
|
||||
ayhurAvOmI9AQ5kprfMY1ibtb10tmWlBz9Ec13ARvZOQ0FUDNQJ1y0jgLZclscQu
|
||||
aZJ4UNRjsakg46H5a7o4Lkgx1kklvD6h1wwYb1DYF/aD9Lw6/SBAGustf6PL4MoD
|
||||
o7j3cQKBgQC9pR15ir4kcqpfz3vdZzGz48F6oH4s7TahQCzdA3UpTgH64QmedY2l
|
||||
S0EeHQxOIFGO9IITRAL5a0y8S0quU/JKeg13WGqQ7pL1P/4Oy2nmZOnG1j1elvgw
|
||||
SY9tSfoO0maE2gUFJ7NwMSWi7spiBezQ6Zx2CH4Dc4Qyq26zabnV+A==
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr2epjP5hwAOfq7pM/9yy
|
||||
3LdkWTap+bYS1HUuqQRlddB10iPURwGE+KwadSeXCXLNk+j8nXoeXYfBx9wQUqj0
|
||||
yP0voSVfa6NgKa9FSnyLLmD+ac1cuCfNfa21pgS9Te1NbVEMyV+ihDWXTpFwAZJo
|
||||
925VGVE/xaR2EcSC1lQGFAce+zRksIoFp3iwcRA92a8RWVJAhjNrj8MSJBzcNHPc
|
||||
26rnAMVwaeQS4GHNqJsOYZUJNr9qwHWlcw6eyH6qjvX2VF1y1kkobK0/hpe8abQ4
|
||||
47UWh19Tpo/xd5e5KWjuFqe8CV6zvOoAWIy33HZ0IJwjbTPTYv9iV0vNDz/Cn5xH
|
||||
fwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
|
@ -0,0 +1,33 @@
|
|||
worker_processes auto;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
gzip on;
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
location ~ /endpoint/ {
|
||||
rewrite ^/endpoint/(.*)$ /$1 break;
|
||||
proxy_pass http://server:9000;
|
||||
}
|
||||
location ~ /socket.io {
|
||||
proxy_pass http://server:9001;
|
||||
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
if [ ! $version ];
|
||||
then npm version 0.1.0-`date "+%Y%m%d%H%M%S"`;
|
||||
else npm version $version;
|
||||
fi
|
||||
|
||||
npm install
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
echo "[ERROR] build falid!"
|
||||
exit 1
|
||||
fi
|
||||
echo '[INFO] build completed'
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
version: 1.0
|
||||
name: tiny-engine
|
||||
language: nodejs
|
||||
|
||||
# 构建工具
|
||||
dependencies:
|
||||
base:
|
||||
nodejs: best
|
||||
|
||||
# 构建机器
|
||||
machine:
|
||||
standard:
|
||||
euler:
|
||||
- default
|
||||
|
||||
# 构建脚本
|
||||
scripts:
|
||||
- sh ./.build_config/build.sh
|
||||
|
||||
# 构建产物
|
||||
artifacts:
|
||||
npm_deploy:
|
||||
- config_path: ./package.json
|
|
@ -0,0 +1,11 @@
|
|||
version: 2.0
|
||||
|
||||
steps:
|
||||
pre_codecheck:
|
||||
- checkout
|
||||
|
||||
tool_params:
|
||||
secsolar:
|
||||
source_dir: ./
|
||||
cmetrics:
|
||||
exclude: vite.config.js|package.json|index.js|mockServer/assets
|
|
@ -0,0 +1,9 @@
|
|||
.vscode
|
||||
dist
|
||||
public
|
||||
package-lock.json
|
||||
**/node_modules/**
|
||||
tmp
|
||||
temp
|
||||
mockServer
|
||||
packages/vue-generator/**/output/**
|
|
@ -0,0 +1,32 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2015: true,
|
||||
worker: true,
|
||||
node: true,
|
||||
jest: true
|
||||
},
|
||||
extends: ['eslint:recommended', 'plugin:vue/vue3-essential'],
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@babel/eslint-parser',
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
requireConfigFile: false,
|
||||
babelOptions: {
|
||||
parserOpts: {
|
||||
plugins: ['jsx']
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: ['vue'],
|
||||
rules: {
|
||||
'no-console': 'error',
|
||||
'no-debugger': 'error',
|
||||
'space-before-function-paren': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'no-use-before-define': 'error',
|
||||
'no-unused-vars': ['error', { ignoreRestSiblings: true, varsIgnorePattern: '^_', argsIgnorePattern: '^_' }],
|
||||
'no-undef': 'off' // e.g. defineEmits
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
name: '🐛 Bug report'
|
||||
description: Create a report to help us improve Tiny Engine
|
||||
title: '🐛 [Bug]: '
|
||||
labels: ['🐛 bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please fill out the following carefully in order to better fix the problem.
|
||||
- type: input
|
||||
id: Environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
**Depending on your browser and operating system, websites may behave differently from one environment to another. Make sure your developers know your technical environment.**
|
||||
placeholder: Please browser information.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: node-version
|
||||
attributes:
|
||||
label: Version
|
||||
description: |
|
||||
### **Check if the issue is reproducible with the latest stable version.**
|
||||
You can use the command `node -v` to view it
|
||||
placeholder: latest
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: tiny-vue-version
|
||||
attributes:
|
||||
label: Version
|
||||
description: |
|
||||
### **Check if the issue is reproducible with the latest stable version.**
|
||||
You can use the command `npm ls @opentiny/vue` to view it
|
||||
placeholder: latest
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: minimal-repo
|
||||
attributes:
|
||||
label: Link to minimal reproduction
|
||||
description: |
|
||||
**Provide a streamlined CodePen / CodeSandbox or GitHub repository link as much as possible. Please don't fill in a link randomly, it will only close your issue directly.**
|
||||
placeholder: Please Input
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Step to reproduce
|
||||
description: |
|
||||
**After the replay is turned on, what actions do we need to perform to make the bug appear? Simple and clear steps can help us locate the problem more quickly. Please clearly describe the steps of reproducing the issue. Issues without clear reproducing steps will not be repaired. If the issue marked with 'need reproduction' does not provide relevant steps within 7 days, it will be closed directly.**
|
||||
placeholder: Please Input
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What is expected
|
||||
placeholder: Please Input
|
||||
- type: textarea
|
||||
id: actually
|
||||
attributes:
|
||||
label: What is actually happening
|
||||
placeholder: Please Input
|
||||
- type: textarea
|
||||
id: additional-comments
|
||||
attributes:
|
||||
label: Any additional comments (optional)
|
||||
description: |
|
||||
**Some background / context of how you ran into this bug.**
|
||||
placeholder: Please Input
|
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Questions or need help
|
||||
url: https://github.com/opentiny/tiny-engine/discussions
|
||||
about: Add this WeChat(opentiny), we will invite you to the WeChat discussion group later.
|
|
@ -0,0 +1,23 @@
|
|||
name: ✨ Feature Request
|
||||
description: Propose new features to @opentiny/tiny-engine to improve it.
|
||||
title: '✨ [Feature]: '
|
||||
labels: ['✨ feature']
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-solve
|
||||
attributes:
|
||||
label: What problem does this feature solve
|
||||
description: |
|
||||
Explain your use case, context, and rationale behind this feature request. More importantly, what is the end user experience you are trying to build that led to the need for this feature?
|
||||
placeholder: Please Input
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature-api
|
||||
attributes:
|
||||
label: What does the proposed API look like
|
||||
description: |
|
||||
Describe how you propose to solve the problem and provide code samples of how the API would work once implemented. Note that you can use Markdown to format your code blocks.
|
||||
placeholder: Please Input
|
||||
validations:
|
||||
required: true
|
|
@ -0,0 +1,52 @@
|
|||
English | [简体中文](https://github.com/opentiny/tiny-engine/blob/develop/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.zh-CN.md)
|
||||
|
||||
# PR
|
||||
|
||||
## PR Checklist
|
||||
|
||||
Please check if your PR fulfills the following requirements:
|
||||
|
||||
- [ ] The commit message follows our [Commit Message Guidelines](https://github.com/opentiny/tiny-engine/blob/develop/CONTRIBUTING.md)
|
||||
- [ ] Tests for the changes have been added (for bug fixes / features)
|
||||
- [ ] Docs have been added / updated (for bug fixes / features)
|
||||
- [ ] Built its own designer, fully self-validated
|
||||
|
||||
## PR Type
|
||||
|
||||
What kind of change does this PR introduce?
|
||||
|
||||
<!-- Please check the one that applies to this PR using "x". -->
|
||||
|
||||
- [ ] Bugfix
|
||||
- [ ] Feature
|
||||
- [ ] Code style update (formatting, local variables)
|
||||
- [ ] Refactoring (no functional changes, no api changes)
|
||||
- [ ] Build related changes
|
||||
- [ ] CI related changes
|
||||
- [ ] Documentation content changes
|
||||
- [ ] Other... Please describe:
|
||||
|
||||
## Background and solution
|
||||
<!--
|
||||
1. Describe the problem and the scenario.
|
||||
2. New features need to be described and attached with renderings.
|
||||
3. Screenshots or GIFs involving UI/Interaction changes/Bugfix before and after modification are required.
|
||||
-->
|
||||
|
||||
### What is the current behavior?
|
||||
|
||||
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
|
||||
|
||||
Issue Number: N/A
|
||||
|
||||
### What is the new behavior?
|
||||
|
||||
|
||||
## Does this PR introduce a breaking change?
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
|
||||
|
||||
## Other information
|
52
dl-flow-frontend/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.zh-CN.md
vendored
Normal file
52
dl-flow-frontend/.github/PULL_REQUEST_TEMPLATE/PULL_REQUEST_TEMPLATE.zh-CN.md
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
[English](https://github.com/opentiny/tiny-engine/blob/develop/.github/PULL_REQUEST_TEMPLATE.md) | 简体中文
|
||||
|
||||
# PR
|
||||
|
||||
## PR Checklist
|
||||
|
||||
请检查您的 PR 是否满足以下要求:
|
||||
|
||||
- [ ] commit message遵循我们的[提交贡献指南](https://github.com/opentiny/tiny-engine/blob/develop/CONTRIBUTING.md)
|
||||
- [ ] 添加了更改内容的测试用例(用于bugfix/功能)
|
||||
- [ ] 文档已添加/更新(用于bugfix/功能)
|
||||
- [ ] 是否构建了自己的设计器,经过了充分的自验证
|
||||
|
||||
## PR 类型
|
||||
|
||||
这个PR的类型是?
|
||||
|
||||
- [ ] 日常 bug 修复
|
||||
- [ ] 新特性支持
|
||||
- [ ] 代码风格优化
|
||||
- [ ] 重构
|
||||
- [ ] 构建优化
|
||||
- [ ] 测试用例
|
||||
- [ ] 文档更新
|
||||
- [ ] 分支合并
|
||||
- [ ] 其他改动(请补充)
|
||||
|
||||
|
||||
## 需求背景和解决方案
|
||||
|
||||
<!--
|
||||
1. 要解决的具体问题。
|
||||
2. 新增特性,需要进行功能描述,并附上效果图。
|
||||
3. 涉及UI/交互变动/Bugfix需要有修改前&修改后截图或 GIF。
|
||||
-->
|
||||
|
||||
|
||||
Issue Number: N/A
|
||||
|
||||
### 修改前
|
||||
|
||||
|
||||
### 修改后
|
||||
|
||||
## 此PR是否含有 breaking change?
|
||||
|
||||
- [ ] 是
|
||||
- [ ] 否
|
||||
|
||||
<!-- 如果此 PR 包含breaking change,请在下面从用户角度描述具体变化和其他风险。-->
|
||||
|
||||
## Other information
|
|
@ -0,0 +1,26 @@
|
|||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- ignore-for-release
|
||||
authors:
|
||||
- allcontributors[bot]
|
||||
categories:
|
||||
- title: Breaking Changes 🛠
|
||||
labels:
|
||||
- Semver-Major
|
||||
- breaking-change
|
||||
- title: Exciting New Features 🎉
|
||||
labels:
|
||||
- Semver-Minor
|
||||
- feature
|
||||
- enhancement
|
||||
- title: Bug Fixes 🐛
|
||||
labels:
|
||||
- Semver-Patch
|
||||
- bug
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- documentation
|
||||
- refactoring
|
||||
- unit-test
|
||||
- ci
|
|
@ -0,0 +1,23 @@
|
|||
name: AI Code Review
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: anc95/ChatGPT-CodeReview@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LANGUAGE: Chinese
|
||||
OPENAI_API_ENDPOINT: https://api.openai.com/v1
|
||||
MODEL: gpt-3.5-turbo
|
||||
MAX_TOKENS: 4096
|
||||
MAX_PATCH_LENGTH: 10000
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue