forked from opentiny/tiny-engine
Compare commits
1 Commits
develop
...
atom-2024/
| Author | SHA1 | Date |
|---|---|---|
|
|
dbb8365f35 |
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
*.conf
|
||||
pnpm-lock.yaml
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
SQL_HOST=localhost
|
||||
SQL_PORT=3306
|
||||
SQL_USER=root
|
||||
SQL_PASSWORD=admin
|
||||
SQL_DATABASE=tiny_engine
|
||||
|
|
@ -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
|
||||
|
|
@ -1,29 +1,2 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
dist/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
lerna-debug.log
|
||||
packages/design-core/bundle-deps
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
tmp
|
||||
temp
|
||||
node_modules
|
||||
**/node_modules
|
||||
379
README.md
379
README.md
|
|
@ -1,108 +1,271 @@
|
|||
<p align="center">
|
||||
<a href="https://opentiny.design/tiny-engine" target="_blank" rel="noopener noreferrer">
|
||||
<img alt="OpenTiny Logo" src="logo.svg" height="100" style="max-width:100%;">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">TinyEngine enables developers to customize low-code platforms, build low-bit platforms online in real time, and support secondary development or integration of low-bit platform capabilities.</p>
|
||||
|
||||
English | [简体中文](README.zh-CN.md)
|
||||
|
||||
🌈 Features:
|
||||
|
||||
- Cross-end cross-frame front-end components
|
||||
- Supports online real-time construction, secondary development, or being integrated.
|
||||
- Directly generate deployable source code without engine support.
|
||||
- Allows access to third-party components and customized extension plug-ins.
|
||||
- Supports high-code and low-code, and hybrid development and deployment of applications.
|
||||
- The platform accesses AI big model capabilities to help developers build applications.
|
||||
|
||||
## Documentation
|
||||
|
||||
- intro:https://opentiny.design/tiny-engine#/home
|
||||
- tutorial:https://opentiny.design/tiny-engine#/help-center/course/engine
|
||||
- playground:https://opentiny.design/tiny-engine#/tiny-engine-editor
|
||||
|
||||
## Development
|
||||
|
||||
### Dependencies required for installation
|
||||
|
||||
```sh
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
### Local development: Start the local mock server and use the mock data of the local mock server.
|
||||
|
||||
```sh
|
||||
$ pnpm dev
|
||||
```
|
||||
|
||||
## Local development, directly connected to the local tiny-engine-webservice server
|
||||
|
||||
1. Start <a href="https://github.com/opentiny/tiny-engine-data-center/blob/main/README.md" target="_blank">tiny-engine-data-center</a>
|
||||
|
||||
2. Start <a href="https://github.com/opentiny/tiny-engine-webservice/blob/main/README.md" target="_blank">tiny-engine-webservice</a>
|
||||
|
||||
3. Modify the origin value in `vite.config.js` in the `packages/design-core/` directory of the tiny-engine project to be the address port of your local webService project (the webService port defaults to 7011), such as:
|
||||
|
||||
<img alt="Modify port" src="https://res.hc-cdn.com/lowcode-portal/1.1.55/img/docimg/backend_deploy_5.png">
|
||||
|
||||
|
||||
### Materials Synchronization [Solution](https://opentiny.design/tiny-engine#/help-center/course/engine/56)
|
||||
|
||||
```sh
|
||||
$ pnpm splitMaterials
|
||||
```
|
||||
|
||||
```sh
|
||||
$ pnpm buildMaterials
|
||||
```
|
||||
|
||||
Open a browser: `http://localhost:8080/?type=app&id=918&tenant=1&pageid=NTJ4MjvqoVj8OVsc`
|
||||
`url search` Parameters:
|
||||
|
||||
- `type=app` Application type
|
||||
- `id=xxx` Application ID
|
||||
- `tenant=xxx` Organization ID
|
||||
- `pageid=xxx` Page ID
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
# Build all plug-ins first
|
||||
pnpm build:plugin
|
||||
|
||||
# Build Designer
|
||||
pnpm build:alpha or build:prod
|
||||
|
||||
```
|
||||
The folder where the product is located after building
|
||||
```
|
||||
tiny-engine/packages/design-core/dist/
|
||||
```
|
||||
|
||||
## Milestones
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %Y-%m-%d
|
||||
|
||||
1.0.0-beta.x version :active,2023-09-25, 2024-03-31
|
||||
1.0.0-rc version : 2024-04-01, 2024-06-30
|
||||
1.0.0 version : 2024-07-01, 2024-07-31
|
||||
|
||||
```
|
||||
|
||||
## 🤝 Participation and Contribution
|
||||
|
||||
If you are interested in our open source project, please join us! 🎉
|
||||
|
||||
Please read the [Contribution Guide](CONTRIBUTING.md) before participating in the contribution.
|
||||
|
||||
- Add official assistant WeChat opentiny-official and join the technical exchange group
|
||||
- Join the mailing list opentiny@googlegroups.com
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
# 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布局
|
||||
|
||||

|
||||
|
||||
绿色区域: 网络与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)
|
||||
|
||||
|
||||
## 前端流程图
|
||||
|
||||

|
||||
|
||||
## 源码阅读顺序
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
108
README.zh-CN.md
108
README.zh-CN.md
|
|
@ -1,108 +0,0 @@
|
|||
<p align="center">
|
||||
<a href="https://opentiny.design/tiny-engine" target="_blank" rel="noopener noreferrer">
|
||||
<img alt="OpenTiny Logo" src="logo.svg" height="100" style="max-width:100%;">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">TinyEngine低代码引擎使能开发者定制低代码平台,支持在线实时构建低码平台,支持二次开发或集成低码平台能力</p>
|
||||
|
||||
[English](README.md) | 简体中文
|
||||
|
||||
🌈 特性:
|
||||
|
||||
- 跨端跨框架前端组件
|
||||
- 支持在线实时构建、支持二次开发或被集成
|
||||
- 直接生成可部署的源码,运行时无需引擎支撑
|
||||
- 允许接入第三方组件、允许定制扩展插件
|
||||
- 支持高代码与低代码,混合开发部署应用
|
||||
- 平台接入 AI 大模型能力,辅助开发者构建应用
|
||||
|
||||
## 文档
|
||||
|
||||
- 介绍:https://opentiny.design/tiny-engine#/home
|
||||
- 使用文档:https://opentiny.design/tiny-engine#/help-center/course/engine
|
||||
- 演示应用:https://opentiny.design/tiny-engine#/tiny-engine-editor
|
||||
|
||||
## 开发
|
||||
|
||||
### 安装所需的依赖
|
||||
|
||||
```sh
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
### 本地开发,启动本地 mock 服务器,使用本地 mock 服务器的 mock 数据
|
||||
|
||||
```sh
|
||||
$ pnpm dev
|
||||
```
|
||||
|
||||
### 本地开发,直连本地的tiny-engine-webservice服务端
|
||||
|
||||
1. 启动 <a href="https://github.com/opentiny/tiny-engine-data-center/blob/main/README.md" target="_blank">tiny-engine-data-center</a>
|
||||
|
||||
2. 启动 <a href="https://github.com/opentiny/tiny-engine-webservice/blob/main/README.md" target="_blank">tiny-engine-webservice</a>
|
||||
|
||||
3. 修改 tiny-engine 项目 `packages/design-core/` 目录下 `vite.config.js` 中origin的值为自己本地webService项目的地址端口(webService端口默认为7011),如:
|
||||
|
||||
<img alt="修改端口" src="https://res.hc-cdn.com/lowcode-portal/1.1.55/img/docimg/backend_deploy_5.png">
|
||||
|
||||
|
||||
### 物料同步[方案](https://opentiny.design/tiny-engine#/help-center/course/engine/56)
|
||||
|
||||
```sh
|
||||
$ pnpm splitMaterials
|
||||
```
|
||||
|
||||
```sh
|
||||
$ pnpm buildMaterials
|
||||
```
|
||||
|
||||
浏览器打开:`http://localhost:8080/?type=app&id=918&tenant=1&pageid=NTJ4MjvqoVj8OVsc`
|
||||
`url search`参数:
|
||||
|
||||
- `type=app` 应用类型
|
||||
- `id=xxx` 应用 ID
|
||||
- `tenant=xxx` 组织 ID
|
||||
- `pageid=xxx` 页面 ID
|
||||
|
||||
## 构建
|
||||
|
||||
```sh
|
||||
# 先构建所有插件
|
||||
pnpm run build:plugin
|
||||
|
||||
# 构建设计器
|
||||
pnpm run build:alpha 或 build:prod
|
||||
|
||||
```
|
||||
构建后产物所在文件夹
|
||||
```
|
||||
tiny-engine/packages/design-core/dist/
|
||||
```
|
||||
|
||||
## 里程碑
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %Y-%m-%d
|
||||
|
||||
1.0.0-beta.x version :active,2023-09-25, 2024-03-31
|
||||
1.0.0-rc version : 2024-04-01, 2024-06-30
|
||||
1.0.0 version : 2024-07-01, 2024-07-31
|
||||
|
||||
```
|
||||
|
||||
## 🤝 参与贡献
|
||||
|
||||
如果你对我们的开源项目感兴趣,欢迎加入我们!🎉
|
||||
|
||||
参与贡献之前请先阅读[贡献指南](CONTRIBUTING.zh-CN.md)。
|
||||
|
||||
- 添加官方小助手微信 opentiny-official,加入技术交流群
|
||||
- 加入邮件列表 opentiny@googlegroups.com
|
||||
|
||||
## 开源协议
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
|
@ -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
|
||||
|
||||
## 流程图
|
||||
|
||||

|
||||
|
||||
## 附件
|
||||
|
||||
[函数组合的可交换性讨论](../../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";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -8,4 +8,4 @@ tool_params:
|
|||
secsolar:
|
||||
source_dir: ./
|
||||
cmetrics:
|
||||
exclude: vite.config.js|package.json|index.js|mockServer/assets|packages/vue-generator/test
|
||||
exclude: vite.config.js|package.json|index.js|mockServer/assets
|
||||
|
|
@ -26,6 +26,7 @@ module.exports = {
|
|||
'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-unused-vars': ['error', { ignoreRestSiblings: true, varsIgnorePattern: '^_', argsIgnorePattern: '^_' }],
|
||||
'no-undef': 'off' // e.g. defineEmits
|
||||
}
|
||||
}
|
||||
|
|
@ -64,14 +64,6 @@ body:
|
|||
attributes:
|
||||
label: What is actually happening
|
||||
placeholder: Please Input
|
||||
- type: input
|
||||
id: project-name
|
||||
attributes:
|
||||
label: What is your project name
|
||||
description: We also welcome you to fill in more detailed project information in the following issue [#334](https://github.com/opentiny/tiny-engine/issues/334).
|
||||
placeholder: Please Input
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-comments
|
||||
attributes:
|
||||
|
|
@ -2,4 +2,4 @@ blank_issues_enabled: true
|
|||
contact_links:
|
||||
- name: Questions or need help
|
||||
url: https://github.com/opentiny/tiny-engine/discussions
|
||||
about: Add this WeChat(opentiny-official), we will invite you to the WeChat discussion group later.
|
||||
about: Add this WeChat(opentiny), we will invite you to the WeChat discussion group later.
|
||||
|
|
@ -21,11 +21,3 @@ body:
|
|||
placeholder: Please Input
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: project-name
|
||||
attributes:
|
||||
label: What is your project name
|
||||
description: We also welcome you to fill in more detailed project information in the following issue [#334](https://github.com/opentiny/tiny-engine/issues/334).
|
||||
placeholder: Please Input
|
||||
validations:
|
||||
required: true
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue