Compare commits

..

1 Commits

Author SHA1 Message Date
GaoNeng dbb8365f35
feat: 花岗岩小队作品提交 (#414)
* fix:修复资源管理工具类型切换 (#239)

* fix:修复资源管理工具类型切换

* fix: 左右两侧面板宽度固定为280px (#230)

* fix(styles-spacing): 增大物料设置内外边距时点击区域 #134 (#184)

* fix(styles-spacing): 增大物料设置内外边距时点击区域 #134

* fix:预览引入builtin组件依赖,built组件样式文件注入js (#251)

* add config (#262)

* add config

* update config

* fix: 修复由于物料边框引起的左侧面板样式问题 (#247)

* fix: 设计器构建后使用vue runtime prod build(本地开发保持development, preview预览保持development) (#254)

* fix(canvas-renderer): 修复 isNative component 判断失效的 bug (#241)

* feat: js panel add jsx support  (#253)

* feat: JS面板支持JSX语法

* fix: 画布解析JS表达式时支持JSX语法

* fix: 修复画布解析JSX表达式时会返回undefined

* feat: 添加Tree和Tooltip的自定义渲染函数属性

* fix: 修改变量名

* feat: algo node

* fix: 修复报错

* fix: fix overflow

* feat: add node

* feat(setting): property

* feat: property setting

* feat(layer): bundle schema

* feat: useMonaco hook

* feat(code): monaco editor

* feat: code editor

* refactor: monaco editor

* feat: useVisitor

* feat(front-end): save layer

* init: backend project init

* refactor: move dl-flow-front repository to dl-flow

* chore: move dl-flow-backend to dl-flow

* chore: 重构仓库结构

* ci: fix ci

* feat(backend): unit test

* chore: update ci

* ci: 手动触发

* fix: ci error

* feat: hot load & minifiy

* fix: ci error

* feat(back): refactor to nest.js

* feat: expose port in docker-compose.yaml

* release: v0.0.2

* release: v0.0.3 release

* release: v0.0.4

* fix: ci fix

* fix: ci

* fix: ci

* fix: ci

* fix: ci

* fix: ci

* fix: ci

* fix: fix import

* fix: fix import

* feat(backend): layer controller

* feat(bakend): material

* fix: fix layer unit test error

* release: version 0.0.7 release

* fix(front-end): fix warning

* feat: save layer

* fix(backend): if property is undefined will no longer throw server error

* feat: remove memory server in dev env

* fix(front): delete tab item lazy load

fix(front): label i18n

* feat: remove cache when install dependencies

* fix: ci

* styled(front): delete console.log

* feat(frontend): context menu

* feat(front): group

* feat(front): group layout

* feat(front): group auto size & auto position

* feat(front): update schema

* feat(front): schema plugin

* feat(front): schema editor

* feat(front): schema field change

* feat(front): schema dynamic change

* feat(frontend): export button

* feat(backend): should add layer identifier when save

* feat(backend): code-generate service

* feat(backend): code generate

* test(backend): base unit test

* test(ast): ast base unit test

* feat(frontend): emit createCodeGenerate event

* feat(bakend): generate python code

* feat(backend): file download

* feat(front): download file

* test(backend): remove code-generate gateway unit test

* feat(frontend): add layer

You can use your existing network to build a lenet now

* feat(backend): remove generate res

* fix(frontend): retry

* fix: group generate code fail

* fix: build error

* 🐳 chore: frontend image

* chore: add packaging strategy

* fix: expose 9000-9900 ports

* chore: ignore all conf file when docker build

* feat: deploy example

* feat: expose bundle.json

* feat(backend): get material

* feat(frontend): unexpose bundle.json

* test: skip ast

* feat(frontend): get material

* feat: README.md

* feat(bundle): bundle data

* feat: update css

* feat: ref schema

* feat(font): list data sturct support

* feat(backend): support list data-struct

* feat(bundle): upsample, SpectralNorm Unflatten

* test(back): ast unit test

* feat(back): user api

* feat: use secret at test

* feat: Add environment variables to dockerfile

* fix: ci error

* feat(back): auth guard

* feat(user): register

* feat(front): login & register

* feat(backend): auth at code generate

* feat: remove dashboard

* refactor: group generate

* styled: clean comment code

* feat: build sequential

* feat(backend): Sequential

* feat(backend): auto load bundle.json

* feat: update example bundle.json

* doc: update readme

* doc: update readme & docker-compose

* fix(backend): jwt token expire

* fix: data will never is empty

* fix: force get schema

* feat: Improve link logic

* fix(backend): stack overflow at sequencingNode

* feat: Fix the issue of untraceable child nodes

* feat: Nested groups are not allowed

* feat: completion bundle.json

* fix(backend): bcrypt error

* fix(backend): restore auth in gateway

* feat(backend): ws exception

* feat(frontend): error retry

* feat: salt len

* fix: ci error

* fix: when process.env.PWD_SALT is undefined

* fix(backend): salt error

* refactor(addons): clean toolbars

* refactor(canvas): remove Unused components

* doc: perfect document

* docs: Discussion on the Commutability of Function Combinations

* docs: complete the images in the document

* doc: refinish documents

* 更新 README.md

* doc: improve documentation and annotations

* docs: improve documentation

* refactor: repo sturct

* fix: dry run

* fix: build error

* fix: build err

* fix(example): docker compose up -d can run

* feat: all in one start

* feat(dashboard): backend

* fix(backend): project:counter loss

* feat(frontend): infinity scroll load project

* feat(frontend): navigator

* feat(frontend): save schema

* feat(backend): add data field when return project info

* feat(backend): should take graphData when save

* feat(frontend): save and import data

* feat: welcome will take nick name

* fix: return data at create

* fix(front): edge loss

* fix: build error

* fix: build error

* fix: build error

* feat(front): rename

* feat(front): update state

* fix(rename): import style

* feat(frontend): menu

* feat(frontend): menu

* fix: add loss entry

* fix: add loss dep

* fix: prettier error

* fix: collapse error

* fix: do not use jsx

* feat: menu take style

* fix(backend):when paramattr name is empty, use uuid

* feat(backend): expose example volume

* fix: add padding property

*  feat(example): add example

* docs: update README

* fix: mirror error

---------

Co-authored-by: yaoyun8 <142570291+yaoyun8@users.noreply.github.com>
Co-authored-by: lizhijie429 <632163606@qq.com>
Co-authored-by: wenmine <wwmmail@foxmail.com>
Co-authored-by: Lu17301156525 <128358973+Lu17301156525@users.noreply.github.com>
Co-authored-by: rhlin <r.h.lin@163.com>
Co-authored-by: chilingling <26962197+chilingling@users.noreply.github.com>
Co-authored-by: Gene <Pacify.98@gmail.com>
2024-05-09 15:27:21 +08:00
1729 changed files with 63112 additions and 93480 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
*.conf
pnpm-lock.yaml

View File

@ -1,5 +0,0 @@
SQL_HOST=localhost
SQL_PORT=3306
SQL_USER=root
SQL_PASSWORD=admin
SQL_DATABASE=tiny_engine

79
.github/workflows/docker-dry-run.yaml vendored Normal file
View File

@ -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

92
.github/workflows/docker.yml vendored Normal file
View File

@ -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

31
.gitignore vendored
View File

@ -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
View File

@ -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
- introhttps://opentiny.design/tiny-engine#/home
- tutorialhttps://opentiny.design/tiny-engine#/help-center/course/engine
- playgroundhttps://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布局
![](public/Snipaste_2024-03-24_21-37-53.png)
绿色区域: 网络与layer选择区域
红色区域: 布局区域, 在绿色区域 单击需要的网络后会显示在红色布局区内
蓝色区域: 网络区域, 可以配置网络的一些属性(Covn2D 举例)
- 输入特征大小
- 输出特征大小
- 卷积核大小等
注意, 您应当自行校验网络的配置项, 例如: 理论上卷积核大小为1是可以存在的它也的确有一些作用比如
- 升维/降维
- 增加非线性
但是**可以**这么做并不意味着它**适合**你的需求更不等同于**不存在运行时候错误**(比如维度错误)
## 结构
### 流程图
### Web UI
下图展示了项目与TinyEngine的差异文件
```
├── packages
   ├── canvas
   │   ├── src
   │   │   ├── components
   │   │   │   ├── container
   │   │   │   │   ├── AlgoNode.vue // 创建的自定义节点
   │   │   │   │   ├── GroupNode.vue // 组节点
   │   │   │   │   └── X6Canvas.vue // x6容器
   ├── controller
   │   ├── src
   │   │   ├── useLayer.js // 自定义Layer的逻辑
   │   │   ├── useResource.js // 数据请求逻辑
   │   │   ├── useSchema.js // schema逻辑
   │   │   ├── useVisitor.js // Python AST解析
   │   │   ├── useWS.js // socket.io的二次封装
   │   │   └── useX6.js // x6的一些逻辑封装
   ├── design-core
   │   ├── authentication.html
   │   ├── src
   │   │   ├── App.vue
   │   │   └── authentication // 登陆页面
   │   │      └── src
   │   │      ├── App.vue
   │   │      ├── components
   │   │      │   ├── login.vue
   │   │      │   └── register.vue
   │   │      └── main.js
   ├── plugins
   │   ├── materials // 物料 (paddlepaddle的网络物料)
   │   │   └── src
   │   │      ├── Main.vue
   │   │      ├── layer
   │   │      │   └── main.vue
   │   │      └── networks
   │   │      └── main.vue
   │   └── schema // 传输给后端的schema的预览窗
   │   └── src
   │       └── Main.vue
   └── settings // 物料的Props设计页面
      ├── code
      │   └── src
      │      └── Main.vue
      └── props
      ├── index.js
      ├── package.json
      └── src
         ├── Main.vue
         └── components
         ├── Empty.vue
         ├── ParamAttr.vue
         ├── enums.vue
         ├── list.vue
└── property-setting.vue
```
### 后端
[参考](./dl-flow-backend/README.md)
### 为什么结束节点和开始节点必须只有一个
因为目前生成的是`Sequential`而不是`Layer`.
`Layer`的确更加的灵活。但是问题也非常的显而易见。
我们设计的是又向无环图, 又向表明 A->B 是正确的,但是 B->A 是不一定的。假设有一幅图
```
Start
|
v
Node-1
|
-----+-----
| | |
V V V
END1 END2 END3
```
那么不管如何遍历最终节点,其实都是正确的。但在神经网络中,不同网络的运算顺序会有不同的结果。比如`先池化后卷积`和`先卷积后池化`的运算结果是不同的。进入训练阶段,训练结果也可能不同。这主要是因为函数的组合在某些条件下是不可交换的,我们也使用数学语言证明了这一点。详细可以参考[函数组合的交换性讨论](./dl-flow-backend/proof/函数组合的可交换性讨论.pdf)
## 前端流程图
![](./public/sequenceDiagram.png)
## 源码阅读顺序
```
dl-flow-back >> README.md >> code-generate/README.md >> code-generate.gateway.ts >> code-generate.service.ts >> ast.service.ts
dl-flow-frontend >> X6Canvas >> useX6 >> useSchma >> export
```

View File

@ -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)

View File

@ -0,0 +1,4 @@
.env
node_modules
*.md
proof

View File

@ -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

7
dl-flow-backend/.env Normal file
View File

@ -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

View File

@ -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',
},
};

42
dl-flow-backend/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

88
dl-flow-backend/README.md Normal file
View File

@ -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

View File

@ -0,0 +1 @@
1

View File

@ -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 存放位置

View File

@ -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

26
dl-flow-backend/global.d.ts vendored Normal file
View File

@ -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

View File

@ -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();
});
});

View File

@ -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 {}

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class DatabaseService {}

View File

@ -0,0 +1,2 @@
export * from './database.module';
export * from './database.service';

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "../../dist/libs/database"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

View File

@ -0,0 +1,2 @@
export * from './redis.module';
export * from './redis.service';

View File

@ -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 {}

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class RedisService {}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "../../dist/libs/redis"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

View File

@ -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));
},
},
});
};
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "../../dist/libs/shared"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,9 @@
# code generate
## 流程图
![](../../../public/3bb36ea8-b6cc-48ae-b227-1118f3b0a4c6.png)
## 附件
[函数组合的可交换性讨论](../../proof/函数组合的可交换性讨论.pdf)

View File

@ -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,
'',
),
);
});
});

View File

@ -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();
});
});

View File

@ -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[];
}

View File

@ -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));
}
}

View File

@ -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;
}
}
}

View File

@ -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'),
);
}
}

View File

@ -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[];
};
}

View File

@ -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;
}
}

View File

@ -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('');
};

View File

@ -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();
});
});

View File

@ -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[];
}

View File

@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class DeleteLayer {
@IsString()
id: string;
}

View File

@ -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 });
}
}

View File

@ -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 {}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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();
});
});

View File

@ -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[];
}

View File

@ -0,0 +1,6 @@
import { Prop } from '@nestjs/mongoose';
export class DeleteMaterial {
@Prop()
id: string;
}

View File

@ -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(),
};
}
}

View File

@ -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 {}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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();
});
});

View File

@ -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>;
}

View File

@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class DeleteProjectDto {
@IsString()
project_id: string;
}

View File

@ -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);

View File

@ -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));
}
}

View File

@ -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 {}

View File

@ -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,
},
);
}
}

View File

@ -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',
});
});
});

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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 {}

View File

@ -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);

View File

@ -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');
}
}

View File

@ -0,0 +1,7 @@
import { WsExceptionFilter } from './ws-exception.filter';
describe('WsExceptionFilter', () => {
it('should be defined', () => {
expect(new WsExceptionFilter()).toBeDefined();
});
});

View File

@ -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;
}
}

View File

@ -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!');
});
});

View File

@ -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"
}
}

View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@ -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/*"
]
}
}
}

View File

@ -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,
}),
],
};
};

5459
dl-flow-example/bundle.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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-----

View File

@ -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-----

View File

@ -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";
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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:

View File

@ -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.

View File

@ -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