mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-09 15:39:24 +08:00
Merge branch 'next' into T-4910
This commit is contained in:
commit
db5f254bd2
@ -19,6 +19,8 @@ DB_STORAGE=storage/db/nocobase-test.sqlite
|
|||||||
# DB_USER=nocobase
|
# DB_USER=nocobase
|
||||||
# DB_PASSWORD=nocobase
|
# DB_PASSWORD=nocobase
|
||||||
# DB_LOGGING=on
|
# DB_LOGGING=on
|
||||||
|
TZ=UTC
|
||||||
|
ENCRYPTION_FIELD_KEY="2DKJ)P+u(9bP5eF#MTdhy8ZJdfa(xT)K"
|
||||||
|
|
||||||
################# CACHE #################
|
################# CACHE #################
|
||||||
# default is memory cache, when develop mode,code's change will be clear memory cache, so can use 'cache-manager-fs-hash'
|
# default is memory cache, when develop mode,code's change will be clear memory cache, so can use 'cache-manager-fs-hash'
|
||||||
|
49
.github/workflows/build-pro-image.yml
vendored
49
.github/workflows/build-pro-image.yml
vendored
@ -16,8 +16,12 @@ on:
|
|||||||
- '.github/workflows/build-pro-image.yml'
|
- '.github/workflows/build-pro-image.yml'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
app-token:
|
||||||
if: github.event.pull_request.head.repo.fork != true
|
if: github.event.pull_request.head.repo.fork != true
|
||||||
|
uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main
|
||||||
|
secrets: inherit
|
||||||
|
build-and-push:
|
||||||
|
needs: app-token
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
verdaccio:
|
verdaccio:
|
||||||
@ -25,10 +29,18 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 4873:4873
|
- 4873:4873
|
||||||
steps:
|
steps:
|
||||||
|
- name: Decrypt app token
|
||||||
|
id: app-token
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
|
||||||
|
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
|
||||||
|
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
|
||||||
|
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Checkout pro-plugins
|
- name: Checkout pro-plugins
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@ -37,7 +49,14 @@ jobs:
|
|||||||
ref: main
|
ref: main
|
||||||
path: packages/pro-plugins
|
path: packages/pro-plugins
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
- name: Clone pro repos
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
|
||||||
|
do
|
||||||
|
git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
|
||||||
|
done
|
||||||
- run: |
|
- run: |
|
||||||
cd packages/pro-plugins &&
|
cd packages/pro-plugins &&
|
||||||
if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then
|
if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then
|
||||||
@ -49,8 +68,30 @@ jobs:
|
|||||||
git checkout main
|
git checkout main
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
- run: |
|
||||||
|
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
|
||||||
|
do
|
||||||
|
cd ./packages/pro-plugins/@nocobase/$repo
|
||||||
|
if git show-ref --quiet refs/remotes/origin/${{ github.head_ref || github.ref_name }}; then
|
||||||
|
git checkout ${{ github.head_ref || github.ref_name }}
|
||||||
|
else
|
||||||
|
if git show-ref --quiet refs/remotes/origin/${{ github.event.pull_request.base.ref }}; then
|
||||||
|
git checkout ${{ github.event.pull_request.base.ref }}
|
||||||
|
else
|
||||||
|
git checkout main
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
cd ../../../../
|
||||||
|
done
|
||||||
- name: rm .git
|
- name: rm .git
|
||||||
run: rm -rf packages/pro-plugins/.git && git config --global user.email "you@example.com" && git config --global user.name "Your Name" && git add -A && git commit -m "tmp commit"
|
run: |
|
||||||
|
rm -rf packages/pro-plugins/.git
|
||||||
|
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
|
||||||
|
do
|
||||||
|
rm -rf packages/pro-plugins/@nocobase/$repo/.git
|
||||||
|
done
|
||||||
|
git config --global user.email "you@example.com"
|
||||||
|
git config --global user.name "Your Name" && git add -A && git commit -m "tmp commit"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
40
.github/workflows/get-nocobase-app-token.yml
vendored
Normal file
40
.github/workflows/get-nocobase-app-token.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
name: Get nocobase app github token
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
outputs:
|
||||||
|
token:
|
||||||
|
value: ${{ jobs.get-app-token.outputs.token }}
|
||||||
|
user-id:
|
||||||
|
value: ${{ jobs.get-app-token.outputs.user-id }}
|
||||||
|
app-slug:
|
||||||
|
value: ${{ jobs.get-app-token.outputs.app-slug }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
get-app-token:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
token: ${{ steps.encrypt-token.outputs.token }}
|
||||||
|
app-slug: ${{ steps.app-token.outputs.app-slug }}
|
||||||
|
user-id: ${{ steps.get-user-id.outputs.user-id }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/create-github-app-token@v1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.NOCOBASE_APP_ID }}
|
||||||
|
private-key: ${{ secrets.NOCOBASE_APP_PRIVATE_KEY }}
|
||||||
|
repositories: nocobase,pro-plugins,${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ',') }}
|
||||||
|
skip-token-revoke: true
|
||||||
|
- name: Encrypt token
|
||||||
|
id: encrypt-token
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
APP_TOKEN=${{ steps.app-token.outputs.token }};
|
||||||
|
BINARY_ENCRYPTED_SECRET=$(echo -n "$APP_TOKEN" | openssl enc -aes-256-cbc -pbkdf2 -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
|
||||||
|
ENCRYPTED_SECRET=$(echo -n "$BINARY_ENCRYPTED_SECRET" | base64 -w 0);
|
||||||
|
echo "token=$ENCRYPTED_SECRET" >> $GITHUB_OUTPUT
|
||||||
|
- name: Get GitHub App User ID
|
||||||
|
id: get-user-id
|
||||||
|
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
34
.github/workflows/manual-build-pro-image.yml
vendored
34
.github/workflows/manual-build-pro-image.yml
vendored
@ -16,8 +16,12 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
app-token:
|
||||||
if: github.event.pull_request.head.repo.fork != true
|
if: github.event.pull_request.head.repo.fork != true
|
||||||
|
uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main
|
||||||
|
secrets: inherit
|
||||||
|
build-and-push:
|
||||||
|
needs: app-token
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
verdaccio:
|
verdaccio:
|
||||||
@ -25,11 +29,19 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 4873:4873
|
- 4873:4873
|
||||||
steps:
|
steps:
|
||||||
|
- name: Decrypt app token
|
||||||
|
id: app-token
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
|
||||||
|
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
|
||||||
|
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
|
||||||
|
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.base_branch }}
|
ref: ${{ github.event.inputs.base_branch }}
|
||||||
ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
submodules: true
|
submodules: true
|
||||||
- name: Set PR branch
|
- name: Set PR branch
|
||||||
id: set_pro_pr_branch
|
id: set_pro_pr_branch
|
||||||
@ -43,9 +55,23 @@ jobs:
|
|||||||
repository: nocobase/pro-plugins
|
repository: nocobase/pro-plugins
|
||||||
path: packages/pro-plugins
|
path: packages/pro-plugins
|
||||||
ref: ${{ steps.set_pro_pr_branch.outputs.pr_branch || 'main' }}
|
ref: ${{ steps.set_pro_pr_branch.outputs.pr_branch || 'main' }}
|
||||||
ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
- name: Clone pro repos
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
|
||||||
|
do
|
||||||
|
git clone -b ${{ steps.set_pro_pr_branch.outputs.pr_branch || 'main' }} https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
|
||||||
|
done
|
||||||
- name: rm .git
|
- name: rm .git
|
||||||
run: rm -rf packages/pro-plugins/.git && git config --global user.email "you@example.com" && git config --global user.name "Your Name" && git add -A && git commit -m "tmp commit"
|
run: |
|
||||||
|
rm -rf packages/pro-plugins/.git
|
||||||
|
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
|
||||||
|
do
|
||||||
|
rm -rf packages/pro-plugins/@nocobase/$repo/.git
|
||||||
|
done
|
||||||
|
git config --global user.email "you@example.com"
|
||||||
|
git config --global user.name "Your Name" && git add -A && git commit -m "tmp commit"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
173
.github/workflows/manual-release.yml
vendored
173
.github/workflows/manual-release.yml
vendored
@ -12,14 +12,62 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push-commit:
|
app-token:
|
||||||
|
uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main
|
||||||
|
secrets: inherit
|
||||||
|
pre-merge-main-into-next:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: app-token
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
repo:
|
||||||
|
- 'nocobase'
|
||||||
|
- 'pro-plugins'
|
||||||
|
- ${{ fromJSON(vars.PRO_PLUGIN_REPOS) }}
|
||||||
|
steps:
|
||||||
|
- name: Decrypt app token
|
||||||
|
id: app-token
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
|
||||||
|
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
|
||||||
|
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
|
||||||
|
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# ref: 'main'
|
||||||
|
repository: nocobase/${{ matrix.repo }}
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
- name: main -> next (nocobase/${{ matrix.repo }})
|
||||||
|
run: |
|
||||||
|
git config --global user.name '${{ needs.app-token.outputs.app-slug }}[bot]'
|
||||||
|
git config --global user.email '${{ needs.app-token.outputs.user-id }}+${{ needs.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
git checkout next
|
||||||
|
git merge main
|
||||||
|
git push origin next --tags --atomic
|
||||||
|
update-version:
|
||||||
|
needs:
|
||||||
|
- app-token
|
||||||
|
- pre-merge-main-into-next
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Decrypt app token
|
||||||
|
id: app-token
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
|
||||||
|
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
|
||||||
|
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
|
||||||
|
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: nocobase/nocobase
|
repository: nocobase/nocobase
|
||||||
ssh-key: ${{ secrets.NOCOBASE_DEPLOY_KEY }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Checkout pro-plugins
|
- name: Checkout pro-plugins
|
||||||
@ -28,43 +76,15 @@ jobs:
|
|||||||
repository: nocobase/pro-plugins
|
repository: nocobase/pro-plugins
|
||||||
path: packages/pro-plugins
|
path: packages/pro-plugins
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ssh-key: ${{ secrets.PRO_PLUGINS_DEPLOY_KEY }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
- name: main -> next(nocobase)
|
- name: Clone pro repos
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
git config --global user.email "actions@github.com"
|
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
|
||||||
git config --global user.name "GitHub Actions Bot"
|
do
|
||||||
git checkout main
|
git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
|
||||||
git pull origin main
|
done
|
||||||
git checkout next
|
|
||||||
git merge main
|
|
||||||
git push origin next
|
|
||||||
- name: main -> next(pro-plugins)
|
|
||||||
run: |
|
|
||||||
cd ./packages/pro-plugins
|
|
||||||
git checkout main
|
|
||||||
git pull origin main
|
|
||||||
git checkout next
|
|
||||||
git merge main
|
|
||||||
git push origin next
|
|
||||||
- name: push pro plugins(next)
|
|
||||||
continue-on-error: true
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
ssh: true
|
|
||||||
branch: next
|
|
||||||
directory: packages/pro-plugins
|
|
||||||
repository: nocobase/pro-plugins
|
|
||||||
tags: true
|
|
||||||
atomic: true
|
|
||||||
- name: push nocobase(next)
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
branch: next
|
|
||||||
ssh: true
|
|
||||||
repository: nocobase/nocobase
|
|
||||||
tags: true
|
|
||||||
atomic: true
|
|
||||||
- name: Set Node.js 18
|
- name: Set Node.js 18
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
@ -72,67 +92,42 @@ jobs:
|
|||||||
- name: Install Lerna
|
- name: Install Lerna
|
||||||
run: npm install -g lerna@4 auto-changelog@2
|
run: npm install -g lerna@4 auto-changelog@2
|
||||||
- name: Run release.sh
|
- name: Run release.sh
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd ./packages/pro-plugins
|
cd ./packages/pro-plugins
|
||||||
git checkout main
|
git checkout main
|
||||||
|
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
|
||||||
|
do
|
||||||
|
echo "@nocobase/$repo" >> .git/info/exclude
|
||||||
|
done
|
||||||
|
echo "$(<.git/info/exclude )"
|
||||||
cd ./../..
|
cd ./../..
|
||||||
git checkout main
|
git checkout main
|
||||||
git config --global user.email "actions@github.com"
|
git config --global user.name '${{ needs.app-token.outputs.app-slug }}[bot]'
|
||||||
git config --global user.name "GitHub Actions Bot"
|
git config --global user.email '${{ needs.app-token.outputs.user-id }}+${{ needs.app-token.outputs.app-slug }}[bot]@users.noreply.github.com>'
|
||||||
echo "packages/pro-plugins/" >> .git/info/exclude
|
echo "packages/pro-plugins" >> .git/info/exclude
|
||||||
bash release.sh $IS_FEAT
|
bash release.sh $IS_FEAT
|
||||||
env:
|
env:
|
||||||
IS_FEAT: ${{ inputs.is_feat && '--is-feat' || '' }}
|
IS_FEAT: ${{ inputs.is_feat && '--is-feat' || '' }}
|
||||||
- name: push pro plugins
|
PRO_PLUGIN_REPOS: ${{ vars.PRO_PLUGIN_REPOS }}
|
||||||
continue-on-error: true
|
- name: Push and merge into next
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
ssh: true
|
|
||||||
branch: main
|
|
||||||
directory: packages/pro-plugins
|
|
||||||
repository: nocobase/pro-plugins
|
|
||||||
tags: true
|
|
||||||
atomic: true
|
|
||||||
- name: push nocobase
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
branch: main
|
|
||||||
ssh: true
|
|
||||||
repository: nocobase/nocobase
|
|
||||||
tags: true
|
|
||||||
atomic: true
|
|
||||||
- name: main -> next
|
|
||||||
run: |
|
|
||||||
git config --global user.email "actions@github.com"
|
|
||||||
git config --global user.name "GitHub Actions Bot"
|
|
||||||
git checkout main
|
|
||||||
git pull origin main
|
|
||||||
git checkout next
|
|
||||||
git merge -X ours main --no-edit
|
|
||||||
git push origin next
|
|
||||||
- name: main -> next
|
|
||||||
run: |
|
run: |
|
||||||
|
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
|
||||||
|
do
|
||||||
|
cd ./packages/pro-plugins/@nocobase/$repo
|
||||||
|
git push origin main --atomic --tags
|
||||||
|
git checkout next
|
||||||
|
git merge -X ours main --no-edit
|
||||||
|
git push origin next --tags --atomic
|
||||||
|
cd ../../../../
|
||||||
|
done
|
||||||
cd ./packages/pro-plugins
|
cd ./packages/pro-plugins
|
||||||
git checkout main
|
git push origin main --atomic --tags
|
||||||
git pull origin main
|
|
||||||
git checkout next
|
git checkout next
|
||||||
git merge -X ours main --no-edit
|
git merge -X ours main --no-edit
|
||||||
git push origin next
|
git push origin next --tags --atomic
|
||||||
- name: push pro plugins
|
cd ../../
|
||||||
continue-on-error: true
|
git push origin main --atomic --tags
|
||||||
uses: ad-m/github-push-action@master
|
git checkout next
|
||||||
with:
|
git merge -X ours main --no-edit
|
||||||
ssh: true
|
git push origin next --tags --atomic
|
||||||
branch: next
|
|
||||||
directory: packages/pro-plugins
|
|
||||||
repository: nocobase/pro-plugins
|
|
||||||
tags: true
|
|
||||||
atomic: true
|
|
||||||
- name: push nocobase
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
branch: next
|
|
||||||
ssh: true
|
|
||||||
repository: nocobase/nocobase
|
|
||||||
tags: true
|
|
||||||
atomic: true
|
|
||||||
|
22
.github/workflows/release-next.yml
vendored
22
.github/workflows/release-next.yml
vendored
@ -8,10 +8,22 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
app-token:
|
||||||
|
uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main
|
||||||
|
secrets: inherit
|
||||||
publish-npm:
|
publish-npm:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:18
|
container: node:18
|
||||||
|
needs: app-token
|
||||||
steps:
|
steps:
|
||||||
|
- name: Decrypt app token
|
||||||
|
id: app-token
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
|
||||||
|
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
|
||||||
|
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
|
||||||
|
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@ -87,9 +99,15 @@ jobs:
|
|||||||
repository: nocobase/pro-plugins
|
repository: nocobase/pro-plugins
|
||||||
path: packages/pro-plugins
|
path: packages/pro-plugins
|
||||||
ref: next
|
ref: next
|
||||||
ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
- name: Clone pro repos
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
|
||||||
|
do
|
||||||
|
git clone -b next https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
|
||||||
|
done
|
||||||
- name: Build Pro plugins
|
- name: Build Pro plugins
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
yarn config set registry https://registry.npmjs.org/
|
yarn config set registry https://registry.npmjs.org/
|
||||||
yarn install
|
yarn install
|
||||||
|
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@ -10,10 +10,22 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
app-token:
|
||||||
|
uses: nocobase/nocobase/.github/workflows/get-nocobase-app-token.yml@main
|
||||||
|
secrets: inherit
|
||||||
publish-npm:
|
publish-npm:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:18
|
container: node:18
|
||||||
|
needs: app-token
|
||||||
steps:
|
steps:
|
||||||
|
- name: Decrypt app token
|
||||||
|
id: app-token
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
ENCRYPTED_SECRET=${{ needs.app-token.outputs.token }};
|
||||||
|
BINARY_ENCRYPTED_SECRET=$(echo -n "$ENCRYPTED_SECRET" | base64 --decode);
|
||||||
|
APP_TOKEN=$(echo -n "$BINARY_ENCRYPTED_SECRET" | openssl enc -aes-256-cbc -pbkdf2 -d -salt -k "${{ secrets.APP_TOKEN_ENCRYPTION_PASSWORD }}");
|
||||||
|
echo "token=$APP_TOKEN" >> $GITHUB_OUTPUT
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Send curl request and parse response
|
- name: Send curl request and parse response
|
||||||
@ -60,9 +72,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
repository: nocobase/pro-plugins
|
repository: nocobase/pro-plugins
|
||||||
path: packages/pro-plugins
|
path: packages/pro-plugins
|
||||||
ssh-key: ${{ secrets.SUBMODULE_SSH_KEY }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
- name: Clone pro repos
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
for repo in ${{ join(fromJSON(vars.PRO_PLUGIN_REPOS), ' ') }}
|
||||||
|
do
|
||||||
|
git clone -b main https://x-access-token:${{ steps.app-token.outputs.token }}@github.com/nocobase/$repo.git packages/pro-plugins/@nocobase/$repo
|
||||||
|
done
|
||||||
- name: Build Pro plugins
|
- name: Build Pro plugins
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
yarn config set registry https://registry.npmjs.org/
|
yarn config set registry https://registry.npmjs.org/
|
||||||
yarn install
|
yarn install
|
||||||
|
37
CHANGELOG.md
37
CHANGELOG.md
@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
## [v1.3.3-beta](https://github.com/nocobase/nocobase/compare/v1.3.2-beta...v1.3.3-beta) - 2024-08-27
|
||||||
|
|
||||||
|
### Merged
|
||||||
|
|
||||||
|
- fix: use the built-in logo file [`#5032`](https://github.com/nocobase/nocobase/pull/5032)
|
||||||
|
- chore: optimize pro image build ci [`#5140`](https://github.com/nocobase/nocobase/pull/5140)
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
- chore(versions): 😊 publish v1.3.3-beta [`9dffefb`](https://github.com/nocobase/nocobase/commit/9dffefb90a662789f9c4e12d2a088a73363c89db)
|
||||||
|
- chore: update changelog [`7c28f4d`](https://github.com/nocobase/nocobase/commit/7c28f4d06690d6b36701f773a933287c0a395a6d)
|
||||||
|
- fix(release): remove continue-on-error for build step [`5a41ab0`](https://github.com/nocobase/nocobase/commit/5a41ab063c8eea8bb0240cc6baf5d485b4fe9f84)
|
||||||
|
|
||||||
|
## [v1.3.2-beta](https://github.com/nocobase/nocobase/compare/v1.3.1-beta...v1.3.2-beta) - 2024-08-26
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
- chore(versions): 😊 publish v1.3.2-beta [`dcadaa6`](https://github.com/nocobase/nocobase/commit/dcadaa666583b3fdc8e7caa6befd37ad442f56e6)
|
||||||
|
- chore(release): optimize release workflow [`6987d46`](https://github.com/nocobase/nocobase/commit/6987d46b3eb5d928f7fc3e1d3226578913b68820)
|
||||||
|
- chore: update changelog [`388b0e2`](https://github.com/nocobase/nocobase/commit/388b0e2a8869862c86cc365ae5f347b74a372e7e)
|
||||||
|
|
||||||
|
## [v1.3.1-beta](https://github.com/nocobase/nocobase/compare/v1.3.0-beta...v1.3.1-beta) - 2024-08-26
|
||||||
|
|
||||||
|
### Merged
|
||||||
|
|
||||||
|
- feat(publish): publish pro repos [`#5129`](https://github.com/nocobase/nocobase/pull/5129)
|
||||||
|
- fix(tree): missing collection schema [`#5131`](https://github.com/nocobase/nocobase/pull/5131)
|
||||||
|
- fix(cli): support upgrade to next [`#5130`](https://github.com/nocobase/nocobase/pull/5130)
|
||||||
|
- fix(client): fix field names of variable input [`#5128`](https://github.com/nocobase/nocobase/pull/5128)
|
||||||
|
- fix: cannot access 'ActionPage' before initialization [`#5125`](https://github.com/nocobase/nocobase/pull/5125)
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
|
||||||
|
- chore(versions): 😊 publish v1.3.1-beta [`4aff92a`](https://github.com/nocobase/nocobase/commit/4aff92ad3bf338a8f798b3cc7460b32316f83d65)
|
||||||
|
- chore: update changelog [`4515f02`](https://github.com/nocobase/nocobase/commit/4515f0220f2b5854d5b3abbbdab8d116ba818669)
|
||||||
|
- fix: missing schema [`c4b8195`](https://github.com/nocobase/nocobase/commit/c4b819528a15f3f7294ce4027ea64342742881f3)
|
||||||
|
|
||||||
## [v1.3.0-beta](https://github.com/nocobase/nocobase/compare/v1.2.39-alpha...v1.3.0-beta) - 2024-08-25
|
## [v1.3.0-beta](https://github.com/nocobase/nocobase/compare/v1.2.39-alpha...v1.3.0-beta) - 2024-08-25
|
||||||
|
|
||||||
### Merged
|
### Merged
|
||||||
|
BIN
packages/core/app/client/public/nocobase.png
Normal file
BIN
packages/core/app/client/public/nocobase.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -313,7 +313,7 @@ function areTimeZonesEqual(timeZone1, timeZone2) {
|
|||||||
}
|
}
|
||||||
timeZone1 = getTimezonesByOffset(timeZone1);
|
timeZone1 = getTimezonesByOffset(timeZone1);
|
||||||
timeZone2 = getTimezonesByOffset(timeZone2);
|
timeZone2 = getTimezonesByOffset(timeZone2);
|
||||||
return moment.tz(timeZone1).format() === moment.tz(timeZone2).format();
|
return moment.tz(timeZone1).format('Z') === moment.tz(timeZone2).format('Z');
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.initEnv = function initEnv() {
|
exports.initEnv = function initEnv() {
|
||||||
@ -406,6 +406,10 @@ exports.initEnv = function initEnv() {
|
|||||||
process.env.DB_TIMEZONE = process.env.TZ;
|
process.env.DB_TIMEZONE = process.env.TZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!/^[+-]\d{1,2}:\d{2}$/.test(process.env.DB_TIMEZONE)) {
|
||||||
|
process.env.DB_TIMEZONE = moment.tz(process.env.DB_TIMEZONE).format('Z');
|
||||||
|
}
|
||||||
|
|
||||||
if (!areTimeZonesEqual(process.env.DB_TIMEZONE, process.env.TZ)) {
|
if (!areTimeZonesEqual(process.env.DB_TIMEZONE, process.env.TZ)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`process.env.DB_TIMEZONE="${process.env.DB_TIMEZONE}" and process.env.TZ="${process.env.TZ}" are different`,
|
`process.env.DB_TIMEZONE="${process.env.DB_TIMEZONE}" and process.env.TZ="${process.env.TZ}" are different`,
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Repository } from '@nocobase/database';
|
||||||
import { MockDatabase, MockServer, createMockServer } from '@nocobase/test';
|
import { MockDatabase, MockServer, createMockServer } from '@nocobase/test';
|
||||||
import Migration from '../migrations/20240802141435-collection-tree';
|
import Migration from '../migrations/20240802141435-collection-tree';
|
||||||
import { Repository } from '@nocobase/database';
|
|
||||||
|
|
||||||
describe('tree collection sync', async () => {
|
describe('tree collection sync', async () => {
|
||||||
let app: MockServer;
|
let app: MockServer;
|
||||||
@ -77,38 +77,25 @@ describe('collection tree migrate test', () => {
|
|||||||
{
|
{
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
name: 'parent',
|
name: 'parent',
|
||||||
|
foreignKey: 'parentId',
|
||||||
|
target: 'test_tree',
|
||||||
treeParent: true,
|
treeParent: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
name: 'children',
|
name: 'children',
|
||||||
|
foreignKey: 'parentId',
|
||||||
|
target: 'test_tree',
|
||||||
treeChildren: true,
|
treeChildren: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
context: {},
|
||||||
});
|
});
|
||||||
const collection = db.collection({
|
await app.db.getCollection('test_tree').model.truncate();
|
||||||
name: 'test_tree',
|
await app.db.getCollection('main_test_tree_path').model.truncate();
|
||||||
tree: 'adjacency-list',
|
const repository = app.db.getRepository('test_tree');
|
||||||
fields: [
|
await repository.create({
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'belongsTo',
|
|
||||||
name: 'parent',
|
|
||||||
treeParent: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'hasMany',
|
|
||||||
name: 'children',
|
|
||||||
treeChildren: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
await collection.sync();
|
|
||||||
await collection.repository.create({
|
|
||||||
values: [
|
values: [
|
||||||
{
|
{
|
||||||
name: 'c1',
|
name: 'c1',
|
||||||
@ -131,7 +118,6 @@ describe('collection tree migrate test', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await app.db.clean({ drop: true });
|
|
||||||
await app.destroy();
|
await app.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -164,26 +150,7 @@ describe('collection tree migrate test', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(p.name).toBe('collection-tree');
|
expect(p.name).toBe('collection-tree');
|
||||||
const collection1 = db.collection({
|
const collection1 = db.getCollection('test_tree');
|
||||||
name: 'test_tree',
|
|
||||||
tree: 'adjacency-list',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
type: 'string',
|
|
||||||
name: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'belongsTo',
|
|
||||||
name: 'parent',
|
|
||||||
treeParent: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'hasMany',
|
|
||||||
name: 'children',
|
|
||||||
treeChildren: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
const pathCollection1 = db.getCollection(name);
|
const pathCollection1 = db.getCollection(name);
|
||||||
expect(pathCollection1).toBeTruthy();
|
expect(pathCollection1).toBeTruthy();
|
||||||
expect(await pathCollection1.existsInDb()).toBeTruthy();
|
expect(await pathCollection1.existsInDb()).toBeTruthy();
|
||||||
|
@ -7,10 +7,10 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Migration } from '@nocobase/server';
|
|
||||||
import { Model, SyncOptions } from '@nocobase/database';
|
import { Model, SyncOptions } from '@nocobase/database';
|
||||||
import { Transaction } from 'sequelize';
|
import { Migration } from '@nocobase/server';
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
|
import { Transaction } from 'sequelize';
|
||||||
|
|
||||||
export default class extends Migration {
|
export default class extends Migration {
|
||||||
on = 'afterLoad'; // 'beforeLoad' or 'afterLoad'
|
on = 'afterLoad'; // 'beforeLoad' or 'afterLoad'
|
||||||
@ -28,7 +28,7 @@ export default class extends Migration {
|
|||||||
|
|
||||||
for (const treeCollection of treeCollections) {
|
for (const treeCollection of treeCollections) {
|
||||||
const name = `main_${treeCollection.name}_path`;
|
const name = `main_${treeCollection.name}_path`;
|
||||||
this.app.db.collection({
|
const collectionOptions = {
|
||||||
name,
|
name,
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
@ -42,11 +42,15 @@ export default class extends Migration {
|
|||||||
fields: [{ name: 'path', length: 191 }],
|
fields: [{ name: 'path', length: 191 }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
};
|
||||||
|
if (treeCollection.options.schema) {
|
||||||
|
collectionOptions['schema'] = treeCollection.options.schema;
|
||||||
|
}
|
||||||
|
this.app.db.collection(collectionOptions);
|
||||||
const treeExistsInDb = await this.app.db.getCollection(name).existsInDb({ transaction });
|
const treeExistsInDb = await this.app.db.getCollection(name).existsInDb({ transaction });
|
||||||
if (!treeExistsInDb) {
|
if (!treeExistsInDb) {
|
||||||
await this.app.db.getCollection(name).sync({ transaction } as SyncOptions);
|
await this.app.db.getCollection(name).sync({ transaction } as SyncOptions);
|
||||||
this.app.db.collection({
|
const opts = {
|
||||||
name: treeCollection.name,
|
name: treeCollection.name,
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
timestamps: false,
|
timestamps: false,
|
||||||
@ -54,7 +58,11 @@ export default class extends Migration {
|
|||||||
{ type: 'integer', name: 'id' },
|
{ type: 'integer', name: 'id' },
|
||||||
{ type: 'integer', name: 'parentId' },
|
{ type: 'integer', name: 'parentId' },
|
||||||
],
|
],
|
||||||
});
|
};
|
||||||
|
if (treeCollection.options.schema) {
|
||||||
|
opts['schema'] = treeCollection.options.schema;
|
||||||
|
}
|
||||||
|
this.app.db.collection(opts);
|
||||||
const chunkSize = 1000;
|
const chunkSize = 1000;
|
||||||
await this.app.db.getRepository(treeCollection.name).chunk({
|
await this.app.db.getRepository(treeCollection.name).chunk({
|
||||||
chunkSize: chunkSize,
|
chunkSize: chunkSize,
|
||||||
|
@ -7,11 +7,11 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Plugin } from '@nocobase/server';
|
|
||||||
import { Collection, Model, SyncOptions, DestroyOptions } from '@nocobase/database';
|
|
||||||
import { DataSource, SequelizeCollectionManager } from '@nocobase/data-source-manager';
|
import { DataSource, SequelizeCollectionManager } from '@nocobase/data-source-manager';
|
||||||
import { Transaction } from 'sequelize';
|
import { Collection, DestroyOptions, Model, SyncOptions } from '@nocobase/database';
|
||||||
|
import { Plugin } from '@nocobase/server';
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
|
import { Transaction } from 'sequelize';
|
||||||
import { TreeCollection } from './tree-collection';
|
import { TreeCollection } from './tree-collection';
|
||||||
|
|
||||||
const getFilterTargetKey = (model: Model) => {
|
const getFilterTargetKey = (model: Model) => {
|
||||||
@ -40,7 +40,11 @@ class PluginCollectionTreeServer extends Plugin {
|
|||||||
const parentForeignKey = collection.treeParentField?.foreignKey || 'parentId';
|
const parentForeignKey = collection.treeParentField?.foreignKey || 'parentId';
|
||||||
|
|
||||||
//always define tree path collection
|
//always define tree path collection
|
||||||
this.defineTreePathCollection(name);
|
const options = {};
|
||||||
|
if (collection.options.schema) {
|
||||||
|
options['schema'] = collection.options.schema;
|
||||||
|
}
|
||||||
|
this.defineTreePathCollection(name, options);
|
||||||
|
|
||||||
//afterSync
|
//afterSync
|
||||||
collectionManager.db.on(`${collection.name}.afterSync`, async ({ transaction }) => {
|
collectionManager.db.on(`${collection.name}.afterSync`, async ({ transaction }) => {
|
||||||
@ -134,7 +138,7 @@ class PluginCollectionTreeServer extends Plugin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async defineTreePathCollection(name: string) {
|
private async defineTreePathCollection(name: string, options: { schema?: string }) {
|
||||||
this.db.collection({
|
this.db.collection({
|
||||||
name,
|
name,
|
||||||
autoGenId: false,
|
autoGenId: false,
|
||||||
@ -149,6 +153,7 @@ class PluginCollectionTreeServer extends Plugin {
|
|||||||
fields: [{ name: 'path', length: 191 }],
|
fields: [{ name: 'path', length: 191 }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -7,6 +7,7 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import PluginFileManagerServer from '@nocobase/plugin-file-manager';
|
||||||
import { InstallOptions, Plugin } from '@nocobase/server';
|
import { InstallOptions, Plugin } from '@nocobase/server';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
|
||||||
@ -16,18 +17,30 @@ export class PluginSystemSettingsServer extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async install(options?: InstallOptions) {
|
async install(options?: InstallOptions) {
|
||||||
|
const plugin = this.pm.get('file-manager') as PluginFileManagerServer;
|
||||||
|
const logo = plugin
|
||||||
|
? await plugin.createFileRecord({
|
||||||
|
filePath: resolve(__dirname, './logo.png'),
|
||||||
|
collectionName: 'attachments',
|
||||||
|
values: {
|
||||||
|
title: 'nocobase-logo',
|
||||||
|
extname: '.png',
|
||||||
|
mimetype: 'image/png',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: {
|
||||||
|
title: 'nocobase-logo',
|
||||||
|
filename: '682e5ad037dd02a0fe4800a3e91c283b.png',
|
||||||
|
extname: '.png',
|
||||||
|
mimetype: 'image/png',
|
||||||
|
url: '/nocobase.png',
|
||||||
|
};
|
||||||
await this.db.getRepository('systemSettings').create({
|
await this.db.getRepository('systemSettings').create({
|
||||||
values: {
|
values: {
|
||||||
title: 'NocoBase',
|
title: 'NocoBase',
|
||||||
appLang: this.getInitAppLang(options),
|
appLang: this.getInitAppLang(options),
|
||||||
enabledLanguages: [this.getInitAppLang(options)],
|
enabledLanguages: [this.getInitAppLang(options)],
|
||||||
logo: {
|
logo,
|
||||||
title: 'nocobase-logo',
|
|
||||||
filename: '682e5ad037dd02a0fe4800a3e91c283b.png',
|
|
||||||
extname: '.png',
|
|
||||||
mimetype: 'image/png',
|
|
||||||
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/682e5ad037dd02a0fe4800a3e91c283b.png',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/src
|
@ -0,0 +1 @@
|
|||||||
|
# @nocobase/plugin-user-data-sync
|
2
packages/plugins/@nocobase/plugin-user-data-sync/client.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-user-data-sync/client.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/client';
|
||||||
|
export { default } from './dist/client';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/client/index.js');
|
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "@nocobase/plugin-user-data-sync",
|
||||||
|
"displayName": "User data synchronization",
|
||||||
|
"displayName.zh-CN": "用户数据同步",
|
||||||
|
"description": "Provide user data source management and user data synchronization interface. The data source can be DingTalk, WeCom, etc., and can be expanded.",
|
||||||
|
"description.zh-CN": "提供用户数据源管理,用户数据同步接口,数据源可为钉钉、企业微信等,可扩展。",
|
||||||
|
"version": "1.4.0-alpha",
|
||||||
|
"main": "dist/server/index.js",
|
||||||
|
"dependencies": {},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nocobase/client": "1.x",
|
||||||
|
"@nocobase/server": "1.x",
|
||||||
|
"@nocobase/test": "1.x"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Users & permissions"
|
||||||
|
]
|
||||||
|
}
|
2
packages/plugins/@nocobase/plugin-user-data-sync/server.d.ts
vendored
Normal file
2
packages/plugins/@nocobase/plugin-user-data-sync/server.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './dist/server';
|
||||||
|
export { default } from './dist/server';
|
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./dist/server/index.js');
|
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { observer, useForm } from '@formily/react';
|
||||||
|
import { useActionContext, useCollectionRecordData, usePlugin, useRequest } from '@nocobase/client';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import SourcePlugin from '.';
|
||||||
|
|
||||||
|
export const useValuesFromOptions = (options) => {
|
||||||
|
const record = useCollectionRecordData();
|
||||||
|
const result = useRequest(
|
||||||
|
() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: {
|
||||||
|
...record.options,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
manual: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { run } = result;
|
||||||
|
const ctx = useActionContext();
|
||||||
|
useEffect(() => {
|
||||||
|
if (ctx.visible) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
}, [ctx.visible, run]);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAdminSettingsForm = (sourceType: string) => {
|
||||||
|
const plugin = usePlugin(SourcePlugin);
|
||||||
|
const source = plugin.sourceTypes.get(sourceType);
|
||||||
|
return source?.components?.AdminSettingsForm;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Options = observer(
|
||||||
|
() => {
|
||||||
|
const form = useForm();
|
||||||
|
const record = useCollectionRecordData();
|
||||||
|
const Component = useAdminSettingsForm(form.values.sourceType || record.sourceType);
|
||||||
|
return Component ? <Component /> : null;
|
||||||
|
},
|
||||||
|
{ displayName: 'Options' },
|
||||||
|
);
|
@ -0,0 +1,284 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
ActionContextProvider,
|
||||||
|
SchemaComponent,
|
||||||
|
useAPIClient,
|
||||||
|
useActionContext,
|
||||||
|
useRequest,
|
||||||
|
ExtendCollectionsProvider,
|
||||||
|
useDataBlockRequest,
|
||||||
|
useDataBlockResource,
|
||||||
|
useCollection,
|
||||||
|
useCollectionRecordData,
|
||||||
|
ActionProps,
|
||||||
|
} from '@nocobase/client';
|
||||||
|
import { App as AntdApp } from 'antd';
|
||||||
|
import React, { useContext, useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
userDataSyncSourcesSchema,
|
||||||
|
createFormSchema,
|
||||||
|
sourceCollection,
|
||||||
|
tasksTableBlockSchema,
|
||||||
|
} from './schemas/user-data-sync-sources';
|
||||||
|
import { Button, Dropdown, Empty } from 'antd';
|
||||||
|
import { PlusOutlined, DownOutlined } from '@ant-design/icons';
|
||||||
|
import { SourceTypeContext, SourceTypesContext, useSourceTypes } from './sourceType';
|
||||||
|
import { useValuesFromOptions, Options } from './Options';
|
||||||
|
import { NAMESPACE, useUserDataSyncSourceTranslation } from './locale';
|
||||||
|
import { Schema, useForm } from '@formily/react';
|
||||||
|
import { taskCollection } from './schemas/user-data-sync-sources';
|
||||||
|
import { createForm } from '@formily/core';
|
||||||
|
|
||||||
|
const useEditFormProps = () => {
|
||||||
|
const recordData = useCollectionRecordData();
|
||||||
|
const form = useMemo(
|
||||||
|
() =>
|
||||||
|
createForm({
|
||||||
|
values: recordData,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSubmitActionProps = () => {
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
const { message } = AntdApp.useApp();
|
||||||
|
const form = useForm();
|
||||||
|
const resource = useDataBlockResource();
|
||||||
|
const { runAsync } = useDataBlockRequest();
|
||||||
|
const collection = useCollection();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'primary',
|
||||||
|
async onClick() {
|
||||||
|
await form.submit();
|
||||||
|
const values = form.values;
|
||||||
|
if (values[collection.filterTargetKey]) {
|
||||||
|
await resource.update({
|
||||||
|
values,
|
||||||
|
filterByTk: values[collection.filterTargetKey],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await resource.create({ values });
|
||||||
|
}
|
||||||
|
await runAsync();
|
||||||
|
message.success('Saved successfully');
|
||||||
|
setVisible(false);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function useDeleteActionProps(): ActionProps {
|
||||||
|
const { message } = AntdApp.useApp();
|
||||||
|
const record = useCollectionRecordData();
|
||||||
|
const resource = useDataBlockResource();
|
||||||
|
const collection = useCollection();
|
||||||
|
const { runAsync } = useDataBlockRequest();
|
||||||
|
return {
|
||||||
|
confirm: {
|
||||||
|
title: 'Delete',
|
||||||
|
content: 'Are you sure you want to delete it?',
|
||||||
|
},
|
||||||
|
async onClick() {
|
||||||
|
await resource.destroy({
|
||||||
|
filterByTk: record[collection.filterTargetKey],
|
||||||
|
});
|
||||||
|
await runAsync();
|
||||||
|
message.success('Deleted!');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useSyncActionProps(): ActionProps {
|
||||||
|
const { message } = AntdApp.useApp();
|
||||||
|
const record = useCollectionRecordData();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { runAsync } = useDataBlockRequest();
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
await api.resource('userData').pull({ name: record['name'] });
|
||||||
|
await runAsync();
|
||||||
|
message.success('Synced!');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const useCustomFormProps = () => {
|
||||||
|
const { type: sourceType } = useContext(SourceTypeContext);
|
||||||
|
const form = useMemo(
|
||||||
|
() =>
|
||||||
|
createForm({
|
||||||
|
initialValues: {
|
||||||
|
sourceType: sourceType,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
form,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useTasksTableBlockProps = () => {
|
||||||
|
const record = useCollectionRecordData();
|
||||||
|
const collection = useCollection();
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
pageSize: 20,
|
||||||
|
filter: {
|
||||||
|
sourceId: record[collection.filterTargetKey],
|
||||||
|
},
|
||||||
|
sort: ['-sort'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function useRetryActionProps(): ActionProps {
|
||||||
|
const { message } = AntdApp.useApp();
|
||||||
|
const record = useCollectionRecordData();
|
||||||
|
const resource = useDataBlockResource();
|
||||||
|
const collection = useCollection();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const { runAsync } = useDataBlockRequest();
|
||||||
|
return {
|
||||||
|
async onClick() {
|
||||||
|
await api.resource('userData').retry({ id: record[collection.filterTargetKey], sourceId: record['sourceId'] });
|
||||||
|
await runAsync();
|
||||||
|
message.success('Successfully');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddNew = () => {
|
||||||
|
const { t } = useUserDataSyncSourceTranslation();
|
||||||
|
const api = useAPIClient();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [type, setType] = useState('');
|
||||||
|
const types = useSourceTypes();
|
||||||
|
const items = types.map((item) => ({
|
||||||
|
...item,
|
||||||
|
onClick: () => {
|
||||||
|
setVisible(true);
|
||||||
|
setType(item.value);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const emptyItem = [
|
||||||
|
{
|
||||||
|
key: '__empty__',
|
||||||
|
label: (
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
{t('No user data source plugin installed', { ns: NAMESPACE })}
|
||||||
|
<br />{' '}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href={
|
||||||
|
api.auth.locale === 'zh-CN'
|
||||||
|
? 'https://docs-cn.nocobase.com/handbook/user-data-sync'
|
||||||
|
: 'https://docs.nocobase.com/handbook/user-data-sync'
|
||||||
|
}
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{t('View documentation', { ns: NAMESPACE })}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
onClick: () => {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionContextProvider value={{ visible, setVisible }}>
|
||||||
|
<SourceTypeContext.Provider value={{ type }}>
|
||||||
|
<Dropdown menu={{ items: items && items.length > 0 ? items : emptyItem }}>
|
||||||
|
<Button icon={<PlusOutlined />} type={'primary'}>
|
||||||
|
{t('Add new')} <DownOutlined />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<SchemaComponent scope={{ types, setType, useCustomFormProps }} schema={createFormSchema} />
|
||||||
|
</SourceTypeContext.Provider>
|
||||||
|
</ActionContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tasks = () => {
|
||||||
|
const { t } = useUserDataSyncSourceTranslation();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
return (
|
||||||
|
<ActionContextProvider value={{ visible, setVisible }}>
|
||||||
|
<Button
|
||||||
|
type={'link'}
|
||||||
|
onClick={() => {
|
||||||
|
setVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Tasks')}
|
||||||
|
</Button>
|
||||||
|
<ExtendCollectionsProvider collections={[taskCollection]}>
|
||||||
|
<SchemaComponent scope={{ useRetryActionProps, useTasksTableBlockProps }} schema={tasksTableBlockSchema} />
|
||||||
|
</ExtendCollectionsProvider>
|
||||||
|
</ActionContextProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserDataSyncSource: React.FC = () => {
|
||||||
|
const { t } = useUserDataSyncSourceTranslation();
|
||||||
|
const [types, setTypes] = useState([]);
|
||||||
|
const api = useAPIClient();
|
||||||
|
useRequest(
|
||||||
|
() =>
|
||||||
|
api
|
||||||
|
.resource('userData')
|
||||||
|
.listSyncTypes()
|
||||||
|
.then((res) => {
|
||||||
|
const types = res?.data?.data || [];
|
||||||
|
return types.map((type: { name: string; title?: string }) => ({
|
||||||
|
key: type.name,
|
||||||
|
label: Schema.compile(type.title || type.name, { t }),
|
||||||
|
value: type.name,
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
onSuccess: (types) => {
|
||||||
|
setTypes(types);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SourceTypesContext.Provider value={{ types }}>
|
||||||
|
<ExtendCollectionsProvider collections={[sourceCollection]}>
|
||||||
|
<SchemaComponent
|
||||||
|
schema={userDataSyncSourcesSchema}
|
||||||
|
components={{ AddNew, Options, Tasks }}
|
||||||
|
scope={{
|
||||||
|
types,
|
||||||
|
t,
|
||||||
|
useEditFormProps,
|
||||||
|
useSubmitActionProps,
|
||||||
|
useDeleteActionProps,
|
||||||
|
useSyncActionProps,
|
||||||
|
useValuesFromOptions,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ExtendCollectionsProvider>
|
||||||
|
</SourceTypesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
249
packages/plugins/@nocobase/plugin-user-data-sync/src/client/client.d.ts
vendored
Normal file
249
packages/plugins/@nocobase/plugin-user-data-sync/src/client/client.d.ts
vendored
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// CSS modules
|
||||||
|
type CSSModuleClasses = { readonly [key: string]: string };
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sass' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.less' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.styl' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.stylus' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.pcss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS
|
||||||
|
declare module '*.css' { }
|
||||||
|
declare module '*.scss' { }
|
||||||
|
declare module '*.sass' { }
|
||||||
|
declare module '*.less' { }
|
||||||
|
declare module '*.styl' { }
|
||||||
|
declare module '*.stylus' { }
|
||||||
|
declare module '*.pcss' { }
|
||||||
|
declare module '*.sss' { }
|
||||||
|
|
||||||
|
// Built-in asset types
|
||||||
|
// see `src/node/constants.ts`
|
||||||
|
|
||||||
|
// images
|
||||||
|
declare module '*.apng' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.png' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.jfif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pjpeg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pjp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.gif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.svg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ico' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.webp' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.avif' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// media
|
||||||
|
declare module '*.mp4' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.webm' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ogg' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.mp3' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.wav' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.flac' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.aac' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.opus' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.mov' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.m4a' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.vtt' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fonts
|
||||||
|
declare module '*.woff' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.woff2' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.eot' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.ttf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.otf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// other
|
||||||
|
declare module '*.webmanifest' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.pdf' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
declare module '*.txt' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wasm?init
|
||||||
|
declare module '*.wasm?init' {
|
||||||
|
const initWasm: (options?: WebAssembly.Imports) => Promise<WebAssembly.Instance>;
|
||||||
|
export default initWasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// web worker
|
||||||
|
declare module '*?worker' {
|
||||||
|
const workerConstructor: {
|
||||||
|
new(options?: { name?: string }): Worker;
|
||||||
|
};
|
||||||
|
export default workerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?worker&inline' {
|
||||||
|
const workerConstructor: {
|
||||||
|
new(options?: { name?: string }): Worker;
|
||||||
|
};
|
||||||
|
export default workerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?worker&url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker' {
|
||||||
|
const sharedWorkerConstructor: {
|
||||||
|
new(options?: { name?: string }): SharedWorker;
|
||||||
|
};
|
||||||
|
export default sharedWorkerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker&inline' {
|
||||||
|
const sharedWorkerConstructor: {
|
||||||
|
new(options?: { name?: string }): SharedWorker;
|
||||||
|
};
|
||||||
|
export default sharedWorkerConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?sharedworker&url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?raw' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?url' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*?inline' {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Plugin } from '@nocobase/client';
|
||||||
|
import { Registry, tval } from '@nocobase/utils/client';
|
||||||
|
import { ComponentType } from 'react';
|
||||||
|
import { NAMESPACE } from './locale';
|
||||||
|
import { UserDataSyncSource } from './UserDataSyncSource';
|
||||||
|
|
||||||
|
export type SourceOptions = {
|
||||||
|
components: Partial<{
|
||||||
|
AdminSettingsForm: ComponentType;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PluginUserDataSyncClient extends Plugin {
|
||||||
|
sourceTypes = new Registry<SourceOptions>();
|
||||||
|
|
||||||
|
registerType(sourceType: string, options: SourceOptions) {
|
||||||
|
this.sourceTypes.register(sourceType, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can get and modify the app instance here
|
||||||
|
async load() {
|
||||||
|
this.app.pluginSettingsManager.add('users-permissions.sync', {
|
||||||
|
title: tval('Synchronize', { ns: NAMESPACE }),
|
||||||
|
icon: 'SyncOutlined',
|
||||||
|
Component: UserDataSyncSource,
|
||||||
|
sort: 99,
|
||||||
|
aclSnippet: 'pm.user-data-sync',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginUserDataSyncClient;
|
@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export const NAMESPACE = 'user-data-sync';
|
||||||
|
|
||||||
|
export function useUserDataSyncSourceTranslation() {
|
||||||
|
return useTranslation([NAMESPACE, 'client'], { nsMode: 'fallback' });
|
||||||
|
}
|
@ -0,0 +1,523 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ISchema } from '@nocobase/client';
|
||||||
|
|
||||||
|
export const sourceCollection = {
|
||||||
|
name: 'userDataSyncSources',
|
||||||
|
sortable: true,
|
||||||
|
filterTargetKey: 'id',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
interface: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'input',
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Source name")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'input',
|
||||||
|
type: 'string',
|
||||||
|
name: 'sourceType',
|
||||||
|
allowNull: false,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Type")}}',
|
||||||
|
'x-component': 'Select',
|
||||||
|
required: true,
|
||||||
|
dataSource: '{{ types }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// interface: 'input',
|
||||||
|
// type: 'string',
|
||||||
|
// name: 'displayName',
|
||||||
|
// uiSchema: {
|
||||||
|
// type: 'string',
|
||||||
|
// title: '{{t("Source display name")}}',
|
||||||
|
// 'x-component': 'Input',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'enabled',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: '{{t("Enabled")}}',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const taskCollection = {
|
||||||
|
name: 'userDataSyncTasks',
|
||||||
|
filterTargetKey: 'id',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
interface: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'batch',
|
||||||
|
interface: 'input',
|
||||||
|
type: 'string',
|
||||||
|
allowNull: false,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Batch")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'source',
|
||||||
|
interface: 'input',
|
||||||
|
type: 'belongsTo',
|
||||||
|
target: 'userDataSyncSources',
|
||||||
|
targetKey: 'id',
|
||||||
|
foreignKey: 'sourceId',
|
||||||
|
allowNull: false,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'object',
|
||||||
|
title: '{{t("Source")}}',
|
||||||
|
'x-component': 'AssociationField',
|
||||||
|
'x-component-props': {
|
||||||
|
fieldNames: {
|
||||||
|
value: 'id',
|
||||||
|
label: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
interface: 'input',
|
||||||
|
type: 'string',
|
||||||
|
allowNull: false,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Status")}}',
|
||||||
|
'x-component': 'Select',
|
||||||
|
required: true,
|
||||||
|
enum: [
|
||||||
|
{ label: '{{t("Init")}}', value: 'init', color: 'default' },
|
||||||
|
{ label: '{{t("Processing")}}', value: 'processing', color: 'processing' },
|
||||||
|
{ label: '{{t("Success")}}', value: 'success', color: 'success' },
|
||||||
|
{ label: '{{t("Failed")}}', value: 'failed', color: 'error' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'message',
|
||||||
|
interface: 'input',
|
||||||
|
type: 'string',
|
||||||
|
allowNull: true,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Message")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cost',
|
||||||
|
interface: 'input',
|
||||||
|
type: 'integer',
|
||||||
|
allowNull: true,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'integer',
|
||||||
|
title: '{{t("Cost")}}',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
'x-component-props': {
|
||||||
|
precision: 0,
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createFormSchema: ISchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
title: '{{t("Add new")}}',
|
||||||
|
properties: {
|
||||||
|
form: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'FormV2',
|
||||||
|
'x-use-component-props': 'useCustomFormProps',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
sourceType: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component-props': {
|
||||||
|
options: '{{ types }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// displayName: {
|
||||||
|
// 'x-component': 'CollectionField',
|
||||||
|
// 'x-decorator': 'FormItem',
|
||||||
|
// },
|
||||||
|
enabled: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: 'object',
|
||||||
|
'x-component': 'Options',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
submit: {
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-use-component-props': 'useSubmitActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tasksTableBlockSchema: ISchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
title: '{{ t("Tasks") }}',
|
||||||
|
properties: {
|
||||||
|
table: {
|
||||||
|
type: 'void',
|
||||||
|
'x-decorator': 'TableBlockProvider',
|
||||||
|
'x-use-decorator-props': 'useTasksTableBlockProps',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: taskCollection.name,
|
||||||
|
dragSort: false,
|
||||||
|
action: 'list',
|
||||||
|
showIndex: true,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
table: {
|
||||||
|
type: 'array',
|
||||||
|
'x-component': 'TableV2',
|
||||||
|
'x-use-component-props': 'useTableBlockProps',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: 'id',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
batch: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Batch") }}',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
batch: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-pattern': 'readPretty',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Status") }}',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-pattern': 'readPretty',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Message") }}',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-pattern': 'readPretty',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Actions")}}',
|
||||||
|
'x-decorator': 'TableV2.Column.ActionBar',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Space',
|
||||||
|
'x-component-props': {
|
||||||
|
split: '|',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
sync: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Retry") }}',
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-use-component-props': 'useRetryActionProps',
|
||||||
|
'x-display': '{{ $record.status === "failed" ? "visible" : "hidden" }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userDataSyncSourcesSchema: ISchema = {
|
||||||
|
type: 'void',
|
||||||
|
name: 'userDataSyncSources',
|
||||||
|
'x-component': 'CardItem',
|
||||||
|
'x-decorator': 'TableBlockProvider',
|
||||||
|
'x-decorator-props': {
|
||||||
|
collection: sourceCollection.name,
|
||||||
|
dragSort: false,
|
||||||
|
action: 'list',
|
||||||
|
params: {
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
|
showIndex: true,
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'ActionBar',
|
||||||
|
'x-component-props': {
|
||||||
|
style: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
delete: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Delete")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-use-component-props': 'useBulkDestroyActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
icon: 'DeleteOutlined',
|
||||||
|
confirm: {
|
||||||
|
title: "{{t('Delete')}}",
|
||||||
|
content: "{{t('Are you sure you want to delete it?')}}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Add new")}}',
|
||||||
|
'x-component': 'AddNew',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
type: 'array',
|
||||||
|
'x-component': 'TableV2',
|
||||||
|
'x-use-component-props': 'useTableBlockProps',
|
||||||
|
'x-component-props': {
|
||||||
|
rowKey: 'id',
|
||||||
|
rowSelection: {
|
||||||
|
type: 'checkbox',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Source name")}}',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-pattern': 'readPretty',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// displayName: {
|
||||||
|
// type: 'void',
|
||||||
|
// title: '{{t("Source display name")}}',
|
||||||
|
// 'x-component': 'TableV2.Column',
|
||||||
|
// properties: {
|
||||||
|
// displayName: {
|
||||||
|
// type: 'string',
|
||||||
|
// 'x-component': 'CollectionField',
|
||||||
|
// 'x-pattern': 'readPretty',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
sourceType: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Type")}}',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
sourceType: {
|
||||||
|
type: 'string',
|
||||||
|
'x-component': 'Select',
|
||||||
|
'x-pattern': 'readPretty',
|
||||||
|
enum: '{{ types }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enabled: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Enabled")}}',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
enabled: {
|
||||||
|
type: 'boolean',
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-pattern': 'readPretty',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Actions")}}',
|
||||||
|
'x-decorator': 'TableV2.Column.ActionBar',
|
||||||
|
'x-component': 'TableV2.Column',
|
||||||
|
properties: {
|
||||||
|
actions: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Space',
|
||||||
|
'x-component-props': {
|
||||||
|
split: '|',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
sync: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Sync") }}',
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-use-component-props': 'useSyncActionProps',
|
||||||
|
'x-display': '{{ $record.enabled ? "visible" : "hidden" }}',
|
||||||
|
},
|
||||||
|
tasks: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Tasks") }}',
|
||||||
|
'x-component': 'Tasks',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
'x-display': '{{ $record.enabled ? "visible" : "hidden" }}',
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{t("Configure")}}',
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
openMode: 'drawer',
|
||||||
|
},
|
||||||
|
properties: {
|
||||||
|
drawer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer',
|
||||||
|
title: '{{t("Configure")}}',
|
||||||
|
properties: {
|
||||||
|
form: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'FormV2',
|
||||||
|
'x-use-component-props': 'useEditFormProps',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
sourceType: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component-props': {
|
||||||
|
options: '{{ types }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// displayName: {
|
||||||
|
// 'x-component': 'CollectionField',
|
||||||
|
// 'x-decorator': 'FormItem',
|
||||||
|
// },
|
||||||
|
enabled: {
|
||||||
|
'x-component': 'CollectionField',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: 'object',
|
||||||
|
'x-component': 'Options',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Action.Drawer.Footer',
|
||||||
|
properties: {
|
||||||
|
submit: {
|
||||||
|
title: '{{t("Submit")}}',
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-use-component-props': 'useSubmitActionProps',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
type: 'void',
|
||||||
|
title: '{{ t("Delete") }}',
|
||||||
|
'x-component': 'Action.Link',
|
||||||
|
'x-use-component-props': 'useDeleteActionProps',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
export const SourceTypeContext = createContext<{ type: string }>({ type: '' });
|
||||||
|
SourceTypeContext.displayName = 'SourceTypeContext';
|
||||||
|
|
||||||
|
export const SourceTypesContext = createContext<{
|
||||||
|
types: {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}[];
|
||||||
|
}>({ types: [] });
|
||||||
|
SourceTypesContext.displayName = 'SourceTypesContext';
|
||||||
|
|
||||||
|
export const useSourceTypes = () => {
|
||||||
|
const { types } = useContext(SourceTypesContext);
|
||||||
|
return types;
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './server';
|
||||||
|
export { default, UserDataResource, FormatUser, SyncAccept, OriginRecord } from './server';
|
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"Synchronize": "Synchronize",
|
||||||
|
"Source name": "Source name",
|
||||||
|
"Source display name": "Source display name",
|
||||||
|
"Type": "Type",
|
||||||
|
"Sync": "Sync",
|
||||||
|
"Tasks": "Tasks",
|
||||||
|
"Batch": "Batch",
|
||||||
|
"Status": "Status",
|
||||||
|
"Message": "Message",
|
||||||
|
"Init": "Init",
|
||||||
|
"Processing": "Processing",
|
||||||
|
"Success": "Success",
|
||||||
|
"Failed": "Failed",
|
||||||
|
"Authenticator": "Authenticator",
|
||||||
|
"Retry": "Retry",
|
||||||
|
"No user data source plugin installed": "No user data source plugin installed",
|
||||||
|
"View documentation": "View documentation"
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"Synchronize": "同步",
|
||||||
|
"Source name": "数据源名称",
|
||||||
|
"Source display name": "数据源展示名称",
|
||||||
|
"Type": "类型",
|
||||||
|
"Sync": "同步",
|
||||||
|
"Tasks": "任务",
|
||||||
|
"Batch": "批次",
|
||||||
|
"Status": "状态",
|
||||||
|
"Message": "信息",
|
||||||
|
"Init": "初始化",
|
||||||
|
"Processing": "进行中",
|
||||||
|
"Success": "成功",
|
||||||
|
"Failed": "失败",
|
||||||
|
"Authenticator": "用户认证",
|
||||||
|
"dingtalk": "钉钉",
|
||||||
|
"wecom": "企业微信",
|
||||||
|
"default": "默认",
|
||||||
|
"Retry": "重试",
|
||||||
|
"No user data source plugin installed": "未安装同步数据源",
|
||||||
|
"View documentation": "查看文档"
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
import { UserDataResourceManager } from '../user-data-resource-manager';
|
||||||
|
import { MockUsersResource } from './mock-resource';
|
||||||
|
import PluginUserDataSyncServer from '../plugin';
|
||||||
|
|
||||||
|
describe('api', async () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let agent: any;
|
||||||
|
let resourceManager: UserDataResourceManager;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['user-data-sync'],
|
||||||
|
});
|
||||||
|
agent = app.agent();
|
||||||
|
const plugin = app.pm.get('user-data-sync') as PluginUserDataSyncServer;
|
||||||
|
resourceManager = plugin.resourceManager;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('push data', async () => {
|
||||||
|
const usersResource = new MockUsersResource(app.db, app.logger);
|
||||||
|
resourceManager.registerResource(usersResource);
|
||||||
|
const res = await agent.resource('userData').push({
|
||||||
|
values: {
|
||||||
|
dataType: 'user',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(usersResource.data.length).toBe(1);
|
||||||
|
expect(usersResource.data[0]).toMatchObject({
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
OriginRecord,
|
||||||
|
PrimaryKey,
|
||||||
|
RecordResourceChanged,
|
||||||
|
SyncAccept,
|
||||||
|
UserDataResource,
|
||||||
|
} from '../user-data-resource-manager';
|
||||||
|
|
||||||
|
export class MockUsersResource extends UserDataResource {
|
||||||
|
name = 'mock-users';
|
||||||
|
accepts: SyncAccept[] = ['user'];
|
||||||
|
data = [];
|
||||||
|
|
||||||
|
async update(record: OriginRecord, resourcePks: PrimaryKey[]): Promise<RecordResourceChanged[]> {
|
||||||
|
this.data[resourcePks[0]] = record.metaData;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(record: OriginRecord, matchKey: string): Promise<RecordResourceChanged[]> {
|
||||||
|
this.data.push(record.metaData);
|
||||||
|
return [{ resourcesPk: this.data.length - 1, isDeleted: false }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ErrorResource extends UserDataResource {
|
||||||
|
async update(record: OriginRecord, resourcePks: PrimaryKey[]): Promise<RecordResourceChanged[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
async create(record: OriginRecord, matchKey: string): Promise<RecordResourceChanged[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,153 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MockDatabase, MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
import { UserDataResourceManager } from '../user-data-resource-manager';
|
||||||
|
import { ErrorResource, MockUsersResource } from './mock-resource';
|
||||||
|
|
||||||
|
describe('user-data-resource-manager', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: MockDatabase;
|
||||||
|
let resourceManager: UserDataResourceManager;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['user-data-sync'],
|
||||||
|
});
|
||||||
|
db = app.db;
|
||||||
|
resourceManager = new UserDataResourceManager();
|
||||||
|
resourceManager.db = db;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.clean({ drop: true });
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('register resource error', async () => {
|
||||||
|
try {
|
||||||
|
const errResource = new ErrorResource(db, app.logger);
|
||||||
|
expect(resourceManager.registerResource(errResource)).toThrowError(
|
||||||
|
'"name" for user data synchronize resource is required',
|
||||||
|
);
|
||||||
|
const errResource2 = new ErrorResource(db, app.logger);
|
||||||
|
errResource2.name = 'error';
|
||||||
|
expect(resourceManager.registerResource(errResource2)).toThrowError(
|
||||||
|
'"accepts" for user data synchronize resource is required',
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('register resource in order', async () => {
|
||||||
|
const usersResource = new MockUsersResource(db, app.logger);
|
||||||
|
resourceManager.registerResource(usersResource, { after: 'mock-users2' });
|
||||||
|
const usersResource2 = new MockUsersResource(db, app.logger);
|
||||||
|
usersResource2.name = 'mock-users2';
|
||||||
|
resourceManager.registerResource(usersResource2);
|
||||||
|
const nodes = resourceManager.resources.nodes;
|
||||||
|
expect(nodes.length).toBe(2);
|
||||||
|
expect(nodes).toEqual([usersResource2, usersResource]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create for a resource', async () => {
|
||||||
|
const mockUsersResource = new MockUsersResource(db, app.logger);
|
||||||
|
resourceManager.registerResource(mockUsersResource);
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
matchKey: 'uid',
|
||||||
|
});
|
||||||
|
expect(mockUsersResource.data.length).toBe(1);
|
||||||
|
expect(mockUsersResource.data[0]).toMatchObject({
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
});
|
||||||
|
const originRecords = await resourceManager.findOriginRecords({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
sourceUks: ['1'],
|
||||||
|
});
|
||||||
|
expect(originRecords.length).toBe(1);
|
||||||
|
expect(originRecords[0]).toMatchObject({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
sourceUk: '1',
|
||||||
|
metaData: {
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
},
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
resource: 'mock-users',
|
||||||
|
resourcePk: '0',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('update for a resource', async () => {
|
||||||
|
const mockUsersResource = new MockUsersResource(db, app.logger);
|
||||||
|
resourceManager.registerResource(mockUsersResource);
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(mockUsersResource.data.length).toBe(1);
|
||||||
|
expect(mockUsersResource.data[0]).toMatchObject({
|
||||||
|
nickname: 'test',
|
||||||
|
});
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(mockUsersResource.data.length).toBe(1);
|
||||||
|
expect(mockUsersResource.data[0]).toMatchObject({
|
||||||
|
nickname: 'test2',
|
||||||
|
});
|
||||||
|
const originRecords = await resourceManager.findOriginRecords({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
sourceUks: ['1'],
|
||||||
|
});
|
||||||
|
expect(originRecords.length).toBe(1);
|
||||||
|
expect(originRecords[0]).toMatchObject({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
sourceUk: '1',
|
||||||
|
metaData: {
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test2',
|
||||||
|
},
|
||||||
|
lastMetaData: {
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Context, Next } from '@nocobase/actions';
|
||||||
|
import { PluginUserDataSyncServer } from '../plugin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
listSyncTypes: async (ctx: Context, next: Next) => {
|
||||||
|
const plugin = ctx.app.pm.get(PluginUserDataSyncServer) as PluginUserDataSyncServer;
|
||||||
|
ctx.body = plugin.sourceManager.listTypes();
|
||||||
|
await next();
|
||||||
|
},
|
||||||
|
pull: async (ctx: Context, next: Next) => {
|
||||||
|
const { name } = ctx.action.params;
|
||||||
|
const plugin = ctx.app.pm.get(PluginUserDataSyncServer) as PluginUserDataSyncServer;
|
||||||
|
await plugin.syncService.pull(name, ctx);
|
||||||
|
await next();
|
||||||
|
},
|
||||||
|
push: async (ctx: Context, next: Next) => {
|
||||||
|
const data = ctx.action.params.values || {};
|
||||||
|
const plugin = ctx.app.pm.get(PluginUserDataSyncServer) as PluginUserDataSyncServer;
|
||||||
|
try {
|
||||||
|
const result = await plugin.syncService.push(data);
|
||||||
|
ctx.body = { code: 0, message: 'success', result };
|
||||||
|
} catch (error) {
|
||||||
|
ctx.status = 500;
|
||||||
|
ctx.body = { code: 500, message: error.message };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
},
|
||||||
|
retry: async (ctx: Context, next: Next) => {
|
||||||
|
const { sourceId, id } = ctx.action.params;
|
||||||
|
const plugin = ctx.app.pm.get(PluginUserDataSyncServer) as PluginUserDataSyncServer;
|
||||||
|
await plugin.syncService.retry(sourceId, id, ctx);
|
||||||
|
await next();
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineCollection } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default defineCollection({
|
||||||
|
name: 'userDataSyncRecordsResources',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'recordId',
|
||||||
|
type: 'bigInt',
|
||||||
|
interface: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resource',
|
||||||
|
interface: 'Select',
|
||||||
|
type: 'string',
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resourcePk',
|
||||||
|
interface: 'Input',
|
||||||
|
type: 'string',
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineCollection } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default defineCollection({
|
||||||
|
dumpRules: {
|
||||||
|
group: 'third-party',
|
||||||
|
},
|
||||||
|
shared: true,
|
||||||
|
name: 'userDataSyncRecords',
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
logging: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
interface: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sourceName',
|
||||||
|
interface: 'Input',
|
||||||
|
type: 'string',
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sourceUk',
|
||||||
|
interface: 'Input',
|
||||||
|
type: 'string',
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dataType',
|
||||||
|
interface: 'Select',
|
||||||
|
type: 'string',
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'resources',
|
||||||
|
type: 'hasMany',
|
||||||
|
target: 'userDataSyncRecordsResources',
|
||||||
|
sourceKey: 'id',
|
||||||
|
foreignKey: 'recordId',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'json',
|
||||||
|
name: 'metaData',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'json',
|
||||||
|
name: 'lastMetaData',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineCollection } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default defineCollection({
|
||||||
|
dumpRules: {
|
||||||
|
group: 'third-party',
|
||||||
|
},
|
||||||
|
shared: true,
|
||||||
|
name: 'userDataSyncSources',
|
||||||
|
title: '{{t("Sync Sources")}}',
|
||||||
|
sortable: true,
|
||||||
|
model: 'SyncSourceModel',
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
logging: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
interface: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'input',
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Source name")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'input',
|
||||||
|
type: 'string',
|
||||||
|
name: 'sourceType',
|
||||||
|
allowNull: false,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Source Type")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
interface: 'input',
|
||||||
|
type: 'string',
|
||||||
|
name: 'displayName',
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Source display name")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
},
|
||||||
|
translation: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'boolean',
|
||||||
|
name: 'enabled',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'json',
|
||||||
|
name: 'options',
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'tasks',
|
||||||
|
target: 'userDataSyncTasks',
|
||||||
|
sourceKey: 'id',
|
||||||
|
foreignKey: 'sourceId',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineCollection } from '@nocobase/database';
|
||||||
|
|
||||||
|
export default defineCollection({
|
||||||
|
dumpRules: {
|
||||||
|
group: 'third-party',
|
||||||
|
},
|
||||||
|
name: 'userDataSyncTasks',
|
||||||
|
title: '{{t("Sync Tasks")}}',
|
||||||
|
sortable: 'sort',
|
||||||
|
model: 'SyncTaskModel',
|
||||||
|
createdBy: true,
|
||||||
|
updatedBy: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
logging: true,
|
||||||
|
shared: true,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'bigInt',
|
||||||
|
autoIncrement: true,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false,
|
||||||
|
interface: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'batch',
|
||||||
|
interface: 'input',
|
||||||
|
type: 'string',
|
||||||
|
allowNull: false,
|
||||||
|
unique: true,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Batch")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'source',
|
||||||
|
interface: 'input',
|
||||||
|
type: 'belongsTo',
|
||||||
|
target: 'userDataSyncSources',
|
||||||
|
targetKey: 'id',
|
||||||
|
foreignKey: 'sourceId',
|
||||||
|
allowNull: false,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'object',
|
||||||
|
title: '{{t("Source")}}',
|
||||||
|
'x-component': 'AssociationField',
|
||||||
|
'x-component-props': {
|
||||||
|
fieldNames: {
|
||||||
|
value: 'id',
|
||||||
|
label: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
'x-read-pretty': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
interface: 'Select',
|
||||||
|
type: 'string',
|
||||||
|
allowNull: false,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Status")}}',
|
||||||
|
'x-component': 'Select',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'message',
|
||||||
|
interface: 'input',
|
||||||
|
type: 'string',
|
||||||
|
allowNull: true,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'string',
|
||||||
|
title: '{{t("Message")}}',
|
||||||
|
'x-component': 'Input',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cost',
|
||||||
|
interface: 'input',
|
||||||
|
type: 'integer',
|
||||||
|
allowNull: true,
|
||||||
|
uiSchema: {
|
||||||
|
type: 'integer',
|
||||||
|
title: '{{t("Cost")}}',
|
||||||
|
'x-component': 'InputNumber',
|
||||||
|
'x-component-props': {
|
||||||
|
precision: 0,
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { SyncSource } from './sync-source';
|
||||||
|
export * from './user-data-resource-manager';
|
||||||
|
export { default } from './plugin';
|
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Model } from '@nocobase/database';
|
||||||
|
|
||||||
|
export class SyncSourceModel extends Model {
|
||||||
|
declare id: number;
|
||||||
|
declare name: string;
|
||||||
|
declare sourceType: string;
|
||||||
|
declare options: any;
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Plugin } from '@nocobase/server';
|
||||||
|
import { UserDataResourceManager } from './user-data-resource-manager';
|
||||||
|
import { UserDataSyncService } from './user-data-sync-service';
|
||||||
|
import userDataActions from './actions/user-data';
|
||||||
|
import { SyncSourceManager } from './sync-source-manager';
|
||||||
|
import { SyncSourceModel } from './models/sync-source';
|
||||||
|
import { LoggerOptions, Logger } from '@nocobase/logger';
|
||||||
|
|
||||||
|
export class PluginUserDataSyncServer extends Plugin {
|
||||||
|
sourceManager: SyncSourceManager;
|
||||||
|
resourceManager: UserDataResourceManager;
|
||||||
|
syncService: UserDataSyncService;
|
||||||
|
|
||||||
|
async afterAdd() {}
|
||||||
|
|
||||||
|
async beforeLoad() {
|
||||||
|
this.app.db.registerModels({ SyncSourceModel });
|
||||||
|
this.sourceManager = new SyncSourceManager();
|
||||||
|
this.resourceManager = new UserDataResourceManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
getLogger(): Logger {
|
||||||
|
const logger = this.createLogger({
|
||||||
|
dirname: 'user-data-sync',
|
||||||
|
filename: '%DATE%.log',
|
||||||
|
format: 'json',
|
||||||
|
} as LoggerOptions);
|
||||||
|
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
const logger = this.getLogger();
|
||||||
|
this.resourceManager.db = this.app.db;
|
||||||
|
this.resourceManager.logger = this.app.logger;
|
||||||
|
this.syncService = new UserDataSyncService(this.resourceManager, this.sourceManager, logger);
|
||||||
|
this.app.resourceManager.define({
|
||||||
|
name: 'userData',
|
||||||
|
actions: {
|
||||||
|
listSyncTypes: userDataActions.listSyncTypes,
|
||||||
|
pull: userDataActions.pull,
|
||||||
|
push: userDataActions.push,
|
||||||
|
retry: userDataActions.retry,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app.acl.registerSnippet({
|
||||||
|
name: `pm.${this.name}`,
|
||||||
|
actions: ['userData:*', 'userDataSyncSources:*'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async install() {}
|
||||||
|
|
||||||
|
async afterEnable() {}
|
||||||
|
|
||||||
|
async afterDisable() {}
|
||||||
|
|
||||||
|
async remove() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PluginUserDataSyncServer;
|
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Registry } from '@nocobase/utils';
|
||||||
|
import { SyncSource, SyncSourceExtend } from './sync-source';
|
||||||
|
import { Context } from '@nocobase/actions';
|
||||||
|
import { SyncSourceModel } from './models/sync-source';
|
||||||
|
|
||||||
|
type SyncSourceConfig = {
|
||||||
|
syncSource: SyncSourceExtend<SyncSource>;
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SyncSourceManager {
|
||||||
|
protected syncSourceTypes: Registry<SyncSourceConfig> = new Registry();
|
||||||
|
registerType(syncSourceType: string, syncSourceConfig: SyncSourceConfig) {
|
||||||
|
this.syncSourceTypes.register(syncSourceType, syncSourceConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
listTypes() {
|
||||||
|
return Array.from(this.syncSourceTypes.getEntities()).map(([syncSourceType, source]) => ({
|
||||||
|
name: syncSourceType,
|
||||||
|
title: source.title,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByName(name: string, ctx: Context) {
|
||||||
|
const repo = ctx.db.getRepository('userDataSyncSources');
|
||||||
|
const sourceInstance: SyncSourceModel = await repo.findOne({ filter: { enabled: true, name: name } });
|
||||||
|
if (!sourceInstance) {
|
||||||
|
throw new Error(`SyncSource [${name}] is not found.`);
|
||||||
|
}
|
||||||
|
return this.create(sourceInstance, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(id: number, ctx: Context) {
|
||||||
|
const repo = ctx.db.getRepository('userDataSyncSources');
|
||||||
|
const sourceInstance: SyncSourceModel = await repo.findOne({ filter: { enabled: true }, filterByTk: id });
|
||||||
|
if (!sourceInstance) {
|
||||||
|
throw new Error(`SyncSource [${id}] is not found.`);
|
||||||
|
}
|
||||||
|
return this.create(sourceInstance, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
create(sourceInstance: SyncSourceModel, ctx: Context) {
|
||||||
|
const { syncSource } = this.syncSourceTypes.get(sourceInstance.sourceType) || {};
|
||||||
|
if (!syncSource) {
|
||||||
|
throw new Error(`SyncSourceType [${sourceInstance.sourceType}] is not found.`);
|
||||||
|
}
|
||||||
|
return new syncSource({ sourceInstance: sourceInstance, options: sourceInstance.options, ctx });
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,106 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Context } from '@nocobase/actions';
|
||||||
|
import { SyncSourceModel } from './models/sync-source';
|
||||||
|
import { UserData } from './user-data-resource-manager';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
export type SyncSourceConfig = {
|
||||||
|
sourceInstance: SyncSourceModel;
|
||||||
|
options: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
ctx: Context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ISyncSource {
|
||||||
|
pull(): Promise<UserData[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class SyncSource implements ISyncSource {
|
||||||
|
instance: SyncSourceModel;
|
||||||
|
protected options: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
protected ctx: Context;
|
||||||
|
|
||||||
|
constructor(config: SyncSourceConfig) {
|
||||||
|
const { options, ctx, sourceInstance } = config;
|
||||||
|
this.instance = sourceInstance;
|
||||||
|
this.options = options;
|
||||||
|
this.ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract pull(): Promise<UserData[]>;
|
||||||
|
|
||||||
|
async newTask() {
|
||||||
|
const batch = generateUniqueNumber();
|
||||||
|
return await this.instance.createTask({ batch, status: 'init' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async beginTask(taskId: number) {
|
||||||
|
const tasks = await this.instance.getTasks({ where: { id: taskId } });
|
||||||
|
if (!tasks && !tasks.length) {
|
||||||
|
throw new Error(`Task [${taskId}] is not found.`);
|
||||||
|
}
|
||||||
|
const task = tasks[0];
|
||||||
|
if (task.status !== 'init') {
|
||||||
|
throw new Error(`Task [${taskId}] is not init.`);
|
||||||
|
}
|
||||||
|
task.status = 'processing';
|
||||||
|
await task.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
async endTask(params: EndTaskParams) {
|
||||||
|
const { taskId, success, cost, message } = params;
|
||||||
|
const tasks = await this.instance.getTasks({ where: { id: taskId } });
|
||||||
|
if (!tasks && !tasks.length) {
|
||||||
|
throw new Error(`Task [${taskId}] is not found.`);
|
||||||
|
}
|
||||||
|
const task = tasks[0];
|
||||||
|
if (task.status !== 'processing') {
|
||||||
|
throw new Error(`Task [${taskId}] is not processing.`);
|
||||||
|
}
|
||||||
|
task.status = success ? 'success' : 'failed';
|
||||||
|
task.cost = cost;
|
||||||
|
task.message = message;
|
||||||
|
await task.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
async retryTask(taskId: number) {
|
||||||
|
const tasks = await this.instance.getTasks({ where: { id: taskId } });
|
||||||
|
if (!tasks && !tasks.length) {
|
||||||
|
throw new Error(`Task [${taskId}] is not found.`);
|
||||||
|
}
|
||||||
|
const task = tasks[0];
|
||||||
|
if (task.status !== 'failed') {
|
||||||
|
throw new Error(`Task [${taskId}] is not failed.`);
|
||||||
|
}
|
||||||
|
task.status = 'processing';
|
||||||
|
task.message = '';
|
||||||
|
await task.save();
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SyncSourceExtend<T extends SyncSource> = new (config: SyncSourceConfig) => T;
|
||||||
|
|
||||||
|
type EndTaskParams = {
|
||||||
|
taskId: number;
|
||||||
|
success: boolean;
|
||||||
|
cost?: number;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function generateUniqueNumber() {
|
||||||
|
const formattedDate = dayjs().format('YYYYMMDDHHmmss');
|
||||||
|
const randomDigits = Math.floor(100000 + Math.random() * 900000);
|
||||||
|
return formattedDate + randomDigits;
|
||||||
|
}
|
@ -0,0 +1,268 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Toposort, ToposortOptions } from '@nocobase/utils';
|
||||||
|
import Database, { Repository } from '@nocobase/database';
|
||||||
|
import { SystemLogger } from '@nocobase/logger';
|
||||||
|
|
||||||
|
export type FormatUser = {
|
||||||
|
uid: string;
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
nickname?: string;
|
||||||
|
phone?: string;
|
||||||
|
departments?: string[];
|
||||||
|
isDeleted?: boolean;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FormatDepartment = {
|
||||||
|
uid: string;
|
||||||
|
title?: string;
|
||||||
|
parentUid?: string;
|
||||||
|
isDeleted?: boolean;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserDataRecord = FormatUser | FormatDepartment;
|
||||||
|
|
||||||
|
export type SyncDataType = 'user' | 'department';
|
||||||
|
|
||||||
|
export type SyncAccept = SyncDataType;
|
||||||
|
|
||||||
|
export type OriginRecord = {
|
||||||
|
id: number;
|
||||||
|
sourceName: string;
|
||||||
|
sourceUk: string;
|
||||||
|
dataType: SyncDataType;
|
||||||
|
metaData: UserDataRecord;
|
||||||
|
resources: {
|
||||||
|
resource: string;
|
||||||
|
resourcePk: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserData = {
|
||||||
|
dataType: SyncDataType;
|
||||||
|
matchKey?: string;
|
||||||
|
records: UserDataRecord[];
|
||||||
|
sourceName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PrimaryKey = number | string;
|
||||||
|
|
||||||
|
export type RecordResourceChanged = {
|
||||||
|
resourcesPk: PrimaryKey;
|
||||||
|
isDeleted: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export abstract class UserDataResource {
|
||||||
|
name: string;
|
||||||
|
accepts: SyncAccept[];
|
||||||
|
db: Database;
|
||||||
|
logger: SystemLogger;
|
||||||
|
|
||||||
|
constructor(db: Database, logger: SystemLogger) {
|
||||||
|
this.db = db;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract update(record: OriginRecord, resourcePks: PrimaryKey[], matchKey?: string): Promise<RecordResourceChanged[]>;
|
||||||
|
abstract create(record: OriginRecord, matchKey: string): Promise<RecordResourceChanged[]>;
|
||||||
|
|
||||||
|
get syncRecordRepo() {
|
||||||
|
return this.db.getRepository('userDataSyncRecords');
|
||||||
|
}
|
||||||
|
|
||||||
|
get syncRecordResourceRepo() {
|
||||||
|
return this.db.getRepository('userDataSyncRecordsResources');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SyncResult = {
|
||||||
|
resource: string;
|
||||||
|
detail: {
|
||||||
|
count: {
|
||||||
|
all: number;
|
||||||
|
success: number;
|
||||||
|
failed: number;
|
||||||
|
};
|
||||||
|
failedRecords: {
|
||||||
|
record: UserDataRecord;
|
||||||
|
message: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UserDataResourceManager {
|
||||||
|
resources = new Toposort<UserDataResource>();
|
||||||
|
syncRecordRepo: Repository;
|
||||||
|
syncRecordResourceRepo: Repository;
|
||||||
|
logger: SystemLogger;
|
||||||
|
|
||||||
|
registerResource(resource: UserDataResource, options?: ToposortOptions) {
|
||||||
|
if (!resource.name) {
|
||||||
|
throw new Error('"name" for user data synchronize resource is required');
|
||||||
|
}
|
||||||
|
if (!resource.accepts) {
|
||||||
|
throw new Error('"accepts" for user data synchronize resource is required');
|
||||||
|
}
|
||||||
|
this.resources.add(resource, { tag: resource.name, ...options });
|
||||||
|
}
|
||||||
|
|
||||||
|
set db(value: Database) {
|
||||||
|
this.syncRecordRepo = value.getRepository('userDataSyncRecords');
|
||||||
|
this.syncRecordResourceRepo = value.getRepository('userDataSyncRecordsResources');
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveOriginRecords(data: UserData): Promise<void> {
|
||||||
|
for (const record of data.records) {
|
||||||
|
if (record.uid === undefined) {
|
||||||
|
throw new Error(`record must has uid, error record: ${JSON.stringify(record)}`);
|
||||||
|
}
|
||||||
|
const syncRecord = await this.syncRecordRepo.findOne({
|
||||||
|
where: {
|
||||||
|
sourceName: data.sourceName,
|
||||||
|
sourceUk: record.uid,
|
||||||
|
dataType: data.dataType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (syncRecord) {
|
||||||
|
syncRecord.lastMetaData = syncRecord.metaData;
|
||||||
|
syncRecord.metaData = record;
|
||||||
|
await syncRecord.save();
|
||||||
|
} else {
|
||||||
|
await this.syncRecordRepo.create({
|
||||||
|
values: {
|
||||||
|
sourceName: data.sourceName,
|
||||||
|
sourceUk: record.uid,
|
||||||
|
dataType: data.dataType,
|
||||||
|
metaData: record,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOriginRecords({ sourceName, dataType, sourceUks }): Promise<OriginRecord[]> {
|
||||||
|
return await this.syncRecordRepo.find({
|
||||||
|
appends: ['resources'],
|
||||||
|
filter: { sourceName, dataType, sourceUk: { $in: sourceUks } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async addResourceToOriginRecord({ recordId, resource, resourcePk }): Promise<void> {
|
||||||
|
const syncRecord = await this.syncRecordRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
id: recordId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (syncRecord) {
|
||||||
|
await syncRecord.createResource({
|
||||||
|
resource,
|
||||||
|
resourcePk,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeResourceFromOriginRecord({ recordId, resource, resourcePk }): Promise<void> {
|
||||||
|
const recordResource = await this.syncRecordResourceRepo.findOne({
|
||||||
|
where: {
|
||||||
|
recordId,
|
||||||
|
resource,
|
||||||
|
resourcePk,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (recordResource) {
|
||||||
|
await recordResource.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOrCreate(data: UserData): Promise<SyncResult[]> {
|
||||||
|
await this.saveOriginRecords(data);
|
||||||
|
const { dataType, sourceName, records, matchKey } = data;
|
||||||
|
const sourceUks = records.map((record) => record.uid);
|
||||||
|
let processed = false;
|
||||||
|
const syncResults: SyncResult[] = [];
|
||||||
|
for (const resource of this.resources.nodes) {
|
||||||
|
if (!resource.accepts.includes(dataType)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const associateResource = resource.name;
|
||||||
|
processed = true;
|
||||||
|
const originRecords = await this.findOriginRecords({ sourceName, sourceUks, dataType });
|
||||||
|
if (!(originRecords && originRecords.length)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const successRecords = [];
|
||||||
|
const failedRecords = [];
|
||||||
|
for (const originRecord of originRecords) {
|
||||||
|
const resourceRecords = originRecord.resources?.filter(
|
||||||
|
(r: { resource: string }) => r.resource === associateResource,
|
||||||
|
);
|
||||||
|
let recordResourceChangeds: RecordResourceChanged[];
|
||||||
|
if (resourceRecords && resourceRecords.length > 0) {
|
||||||
|
const resourcePks = resourceRecords.map((r: { resourcePk: string }) => r.resourcePk);
|
||||||
|
try {
|
||||||
|
recordResourceChangeds = await resource.update(originRecord, resourcePks, matchKey);
|
||||||
|
this.logger?.debug(`update record success. Data changed: ${JSON.stringify(recordResourceChangeds)}`);
|
||||||
|
successRecords.push(originRecord.metaData);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger?.warn(`update record error: ${error.message}`, { originRecord });
|
||||||
|
failedRecords.push({ record: originRecord.metaData, message: error.message });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
recordResourceChangeds = await resource.create(originRecord, matchKey);
|
||||||
|
this.logger?.debug(`create record success. Data changed: ${JSON.stringify(recordResourceChangeds)}`);
|
||||||
|
successRecords.push(originRecord.metaData);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger?.warn(`create record error: ${error.message}`, { originRecord });
|
||||||
|
failedRecords.push({ record: originRecord.metaData, message: error.message });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!recordResourceChangeds || recordResourceChangeds.length === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const { resourcesPk, isDeleted } of recordResourceChangeds) {
|
||||||
|
if (isDeleted) {
|
||||||
|
await this.removeResourceFromOriginRecord({
|
||||||
|
recordId: originRecord.id,
|
||||||
|
resource: associateResource,
|
||||||
|
resourcePk: resourcesPk,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.addResourceToOriginRecord({
|
||||||
|
recordId: originRecord.id,
|
||||||
|
resource: associateResource,
|
||||||
|
resourcePk: resourcesPk,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syncResults.push({
|
||||||
|
resource: associateResource,
|
||||||
|
detail: {
|
||||||
|
count: {
|
||||||
|
all: originRecords.length,
|
||||||
|
success: successRecords.length,
|
||||||
|
failed: failedRecords.length,
|
||||||
|
},
|
||||||
|
failedRecords,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!processed) {
|
||||||
|
throw new Error(`dataType "${dataType}" is not support`);
|
||||||
|
}
|
||||||
|
return syncResults;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SyncResult, UserData, UserDataResourceManager } from './user-data-resource-manager';
|
||||||
|
import { SyncSourceManager } from './sync-source-manager';
|
||||||
|
import { Context } from '@nocobase/actions';
|
||||||
|
import { SyncSource } from './sync-source';
|
||||||
|
import { Logger } from '@nocobase/logger';
|
||||||
|
|
||||||
|
export class UserDataSyncService {
|
||||||
|
resourceManager: UserDataResourceManager;
|
||||||
|
sourceManager: SyncSourceManager;
|
||||||
|
logger: Logger;
|
||||||
|
|
||||||
|
constructor(resourceManager: UserDataResourceManager, sourceManager: SyncSourceManager, logger: Logger) {
|
||||||
|
this.resourceManager = resourceManager;
|
||||||
|
this.sourceManager = sourceManager;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
async pull(sourceName: string, ctx: Context) {
|
||||||
|
const source = await this.sourceManager.getByName(sourceName, ctx);
|
||||||
|
const task = await source.newTask();
|
||||||
|
await source.beginTask(task.id);
|
||||||
|
ctx.log.info('begin sync task of source', { source: sourceName, sourceType: source.instance.sourceType });
|
||||||
|
this.runSync(source, task, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async push(data: any): Promise<SyncResult[]> {
|
||||||
|
const { dataType, records } = data;
|
||||||
|
if (dataType === undefined) {
|
||||||
|
throw new Error('dataType for user data synchronize is required');
|
||||||
|
}
|
||||||
|
if (dataType !== 'user' && dataType !== 'department') {
|
||||||
|
throw new Error('dataType must be user or department');
|
||||||
|
}
|
||||||
|
if (records === undefined) {
|
||||||
|
throw new Error('records for user data synchronize is required');
|
||||||
|
}
|
||||||
|
if (records.length === 0) {
|
||||||
|
throw new Error('records must have at least one piece of data');
|
||||||
|
}
|
||||||
|
const userData: UserData = {
|
||||||
|
dataType: data.dataType,
|
||||||
|
matchKey: data.matchKey,
|
||||||
|
records: data.records,
|
||||||
|
sourceName: data.sourceName ? data.sourceName : 'api',
|
||||||
|
};
|
||||||
|
this.logger.info({
|
||||||
|
source: data.sourceName ? data.sourceName : 'api',
|
||||||
|
sourceType: 'api',
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
return await this.resourceManager.updateOrCreate(userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async retry(sourceId: number, taskId: number, ctx: Context) {
|
||||||
|
const source = await this.sourceManager.getById(sourceId, ctx);
|
||||||
|
const task = await source.retryTask(taskId);
|
||||||
|
ctx.log.info('retry sync task of source', {
|
||||||
|
source: source.instance.name,
|
||||||
|
sourceType: source.instance.name,
|
||||||
|
task: task.id,
|
||||||
|
});
|
||||||
|
this.runSync(source, task, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async runSync(source: SyncSource, task: any, ctx: Context) {
|
||||||
|
const currentTimeMillis = new Date().getTime();
|
||||||
|
try {
|
||||||
|
ctx.log.info('begin pull data of source', {
|
||||||
|
source: source.instance.name,
|
||||||
|
sourceType: source.instance.sourceType,
|
||||||
|
});
|
||||||
|
const data = await source.pull();
|
||||||
|
// 输出拉取的数据
|
||||||
|
this.logger.info({
|
||||||
|
source: source.instance.name,
|
||||||
|
sourceType: source.instance.sourceType,
|
||||||
|
batch: task.batch,
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
ctx.log.info('end pull data of source', { source: source.instance.name, sourceType: source.instance.sourceType });
|
||||||
|
ctx.log.info('begin update data of source', {
|
||||||
|
source: source.instance.name,
|
||||||
|
sourceType: source.instance.sourceType,
|
||||||
|
});
|
||||||
|
for (const item of data) {
|
||||||
|
await this.resourceManager.updateOrCreate(item);
|
||||||
|
}
|
||||||
|
ctx.log.info('end update data of source', {
|
||||||
|
source: source.instance.name,
|
||||||
|
sourceType: source.instance.sourceType,
|
||||||
|
});
|
||||||
|
const costTime = new Date().getTime() - currentTimeMillis;
|
||||||
|
await source.endTask({ taskId: task.id, success: true, cost: costTime });
|
||||||
|
} catch (err) {
|
||||||
|
ctx.log.error(
|
||||||
|
`sync task of source: ${source.instance.name} sourceType: ${source.instance.sourceType} error: ${err.message}`,
|
||||||
|
{ method: 'runSync', err: err.stack, cause: err.cause },
|
||||||
|
);
|
||||||
|
await source.endTask({ taskId: task.id, success: false, message: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
info: {
|
||||||
|
title: 'NocoBase API - User data synchronization plugin',
|
||||||
|
},
|
||||||
|
paths: {
|
||||||
|
'/userData:push': {
|
||||||
|
post: {
|
||||||
|
description: 'Push user data',
|
||||||
|
tags: ['Push'],
|
||||||
|
security: [],
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
$ref: '#/components/schemas/userData',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'ok',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
schemas: {
|
||||||
|
userData: {
|
||||||
|
type: 'object',
|
||||||
|
description: '用户数据',
|
||||||
|
properties: {
|
||||||
|
dataType: {
|
||||||
|
type: 'string',
|
||||||
|
description: '数据类型, 目前可选值为: user, department',
|
||||||
|
},
|
||||||
|
uniqueKey: {
|
||||||
|
type: 'string',
|
||||||
|
description: '唯一键',
|
||||||
|
},
|
||||||
|
records: {
|
||||||
|
type: 'array',
|
||||||
|
description:
|
||||||
|
'数据, 若 dataType 为 user, 则为用户数据字段见schemas/user, 若 dataType 为 department, 则为部门数据字段见schemas/department',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourceName: {
|
||||||
|
type: 'string',
|
||||||
|
description: '数据源名称',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
description: '用户',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'ID',
|
||||||
|
},
|
||||||
|
nickname: {
|
||||||
|
type: 'string',
|
||||||
|
description: '昵称',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
description: '邮箱',
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: '手机号',
|
||||||
|
},
|
||||||
|
departments: {
|
||||||
|
type: 'array',
|
||||||
|
description: '所属部门, 部门ID 数组',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
department: {
|
||||||
|
type: 'object',
|
||||||
|
description: '部门',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'ID',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: '名称',
|
||||||
|
},
|
||||||
|
parentId: {
|
||||||
|
type: 'string',
|
||||||
|
description: '父级部门ID',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
/api/userData:push
|
||||||
|
*/
|
@ -19,6 +19,7 @@
|
|||||||
"@nocobase/database": "1.x",
|
"@nocobase/database": "1.x",
|
||||||
"@nocobase/plugin-acl": "1.x",
|
"@nocobase/plugin-acl": "1.x",
|
||||||
"@nocobase/plugin-auth": "1.x",
|
"@nocobase/plugin-auth": "1.x",
|
||||||
|
"@nocobase/plugin-user-data-sync": "1.x",
|
||||||
"@nocobase/resourcer": "1.x",
|
"@nocobase/resourcer": "1.x",
|
||||||
"@nocobase/server": "1.x",
|
"@nocobase/server": "1.x",
|
||||||
"@nocobase/test": "1.x",
|
"@nocobase/test": "1.x",
|
||||||
|
@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { UserDataResourceManager } from '@nocobase/plugin-user-data-sync';
|
||||||
|
import { MockDatabase, MockServer, createMockServer } from '@nocobase/test';
|
||||||
|
import PluginUserDataSyncServer from 'packages/plugins/@nocobase/plugin-user-data-sync/src/server/plugin';
|
||||||
|
|
||||||
|
describe('user data sync', () => {
|
||||||
|
let app: MockServer;
|
||||||
|
let db: MockDatabase;
|
||||||
|
let resourceManager: UserDataResourceManager;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = await createMockServer({
|
||||||
|
plugins: ['user-data-sync', 'users'],
|
||||||
|
});
|
||||||
|
db = app.db;
|
||||||
|
const plugin = app.pm.get('user-data-sync') as PluginUserDataSyncServer;
|
||||||
|
resourceManager = plugin.resourceManager;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.clean({ drop: true });
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create user', async () => {
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
matchKey: 'email',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
email: 'test@nocobase.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const user = await db.getRepository('users').findOne({
|
||||||
|
filter: {
|
||||||
|
email: 'test@nocobase.com',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(user).toBeTruthy();
|
||||||
|
expect(user.nickname).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update existing user when creating', async () => {
|
||||||
|
const user = await db.getRepository('users').create({
|
||||||
|
values: {
|
||||||
|
email: 'test@nocobase.com',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(user.nickname).toBeFalsy();
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
matchKey: 'email',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
email: 'test@nocobase.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const user2 = await db.getRepository('users').findOne({
|
||||||
|
filter: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(user2).toBeTruthy();
|
||||||
|
expect(user2.nickname).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shoud update user', async () => {
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
matchKey: 'email',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test',
|
||||||
|
email: 'test@nocobase.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const user = await db.getRepository('users').findOne({
|
||||||
|
filter: {
|
||||||
|
email: 'test@nocobase.com',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(user).toBeTruthy();
|
||||||
|
await resourceManager.updateOrCreate({
|
||||||
|
sourceName: 'test',
|
||||||
|
dataType: 'user',
|
||||||
|
matchKey: 'email',
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
uid: '1',
|
||||||
|
nickname: 'test2',
|
||||||
|
email: 'test@nocobase.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const user2 = await db.getRepository('users').findOne({
|
||||||
|
filter: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(user2).toBeTruthy();
|
||||||
|
expect(user2.nickname).toBe('test2');
|
||||||
|
});
|
||||||
|
});
|
@ -11,10 +11,11 @@ import { Collection, Op } from '@nocobase/database';
|
|||||||
import { Plugin } from '@nocobase/server';
|
import { Plugin } from '@nocobase/server';
|
||||||
import { parse } from '@nocobase/utils';
|
import { parse } from '@nocobase/utils';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
|
||||||
import { Cache } from '@nocobase/cache';
|
import { Cache } from '@nocobase/cache';
|
||||||
import * as actions from './actions/users';
|
import * as actions from './actions/users';
|
||||||
import { UserModel } from './models/UserModel';
|
import { UserModel } from './models/UserModel';
|
||||||
|
import PluginUserDataSyncServer from '@nocobase/plugin-user-data-sync';
|
||||||
|
import { UserDataSyncResource } from './user-data-sync-resource';
|
||||||
|
|
||||||
export default class PluginUsersServer extends Plugin {
|
export default class PluginUsersServer extends Plugin {
|
||||||
async beforeLoad() {
|
async beforeLoad() {
|
||||||
@ -179,6 +180,11 @@ export default class PluginUsersServer extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userDataSyncPlugin = this.app.pm.get('user-data-sync') as PluginUserDataSyncServer;
|
||||||
|
if (userDataSyncPlugin) {
|
||||||
|
userDataSyncPlugin.resourceManager.registerResource(new UserDataSyncResource(this.db, this.app.logger));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getInstallingData(options: any = {}) {
|
getInstallingData(options: any = {}) {
|
||||||
|
@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* This file is part of the NocoBase (R) project.
|
||||||
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
||||||
|
* Authors: NocoBase Team.
|
||||||
|
*
|
||||||
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
||||||
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Model } from '@nocobase/database';
|
||||||
|
import {
|
||||||
|
FormatUser,
|
||||||
|
OriginRecord,
|
||||||
|
PrimaryKey,
|
||||||
|
RecordResourceChanged,
|
||||||
|
SyncAccept,
|
||||||
|
UserDataResource,
|
||||||
|
} from '@nocobase/plugin-user-data-sync';
|
||||||
|
|
||||||
|
export class UserDataSyncResource extends UserDataResource {
|
||||||
|
name = 'users';
|
||||||
|
accepts: SyncAccept[] = ['user'];
|
||||||
|
|
||||||
|
get userRepo() {
|
||||||
|
return this.db.getRepository('users');
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUser(user: Model, sourceUser: FormatUser) {
|
||||||
|
if (sourceUser.isDeleted) {
|
||||||
|
// 删除用户
|
||||||
|
const roles = await user.getRoles();
|
||||||
|
// 是否有Root角色
|
||||||
|
for (const role of roles) {
|
||||||
|
if (role.name === 'root') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await user.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dataChanged = false;
|
||||||
|
if (sourceUser.username !== undefined && user.username !== sourceUser.username) {
|
||||||
|
user.username = sourceUser.username;
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
if (sourceUser.phone !== undefined && user.phone !== sourceUser.phone) {
|
||||||
|
user.phone = sourceUser.phone;
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
if (sourceUser.email !== undefined && user.email !== sourceUser.email) {
|
||||||
|
user.email = sourceUser.email;
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
if (sourceUser.nickname !== undefined && user.nickname !== sourceUser.nickname) {
|
||||||
|
user.nickname = sourceUser.nickname;
|
||||||
|
dataChanged = true;
|
||||||
|
}
|
||||||
|
if (dataChanged) {
|
||||||
|
await user.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(record: OriginRecord, resourcePks: PrimaryKey[], matchKey: string): Promise<RecordResourceChanged[]> {
|
||||||
|
const { metaData: sourceUser } = record;
|
||||||
|
const resourcePk = resourcePks[0];
|
||||||
|
const user = await this.userRepo.findOne({
|
||||||
|
filterByTk: resourcePk,
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
// 用户不存在, 重新创建用户
|
||||||
|
const result = await this.create(record, matchKey);
|
||||||
|
return [...result, { resourcesPk: resourcePk, isDeleted: true }];
|
||||||
|
}
|
||||||
|
await this.updateUser(user, sourceUser);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(record: OriginRecord, matchKey: string): Promise<RecordResourceChanged[]> {
|
||||||
|
const { metaData: sourceUser } = record;
|
||||||
|
const filter = {};
|
||||||
|
let user: any;
|
||||||
|
if (['phone', 'email', 'username'].includes(matchKey)) {
|
||||||
|
filter[matchKey] = sourceUser[matchKey];
|
||||||
|
user = await this.userRepo.findOne({
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (user) {
|
||||||
|
await this.updateUser(user, sourceUser);
|
||||||
|
} else {
|
||||||
|
user = await this.userRepo.create({
|
||||||
|
values: {
|
||||||
|
nickname: sourceUser.nickname,
|
||||||
|
phone: sourceUser.phone,
|
||||||
|
email: sourceUser.email,
|
||||||
|
username: sourceUser.username,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [{ resourcesPk: user.id, isDeleted: false }];
|
||||||
|
}
|
||||||
|
}
|
@ -53,6 +53,7 @@
|
|||||||
"@nocobase/plugin-theme-editor": "1.4.0-alpha",
|
"@nocobase/plugin-theme-editor": "1.4.0-alpha",
|
||||||
"@nocobase/plugin-ui-schema-storage": "1.4.0-alpha",
|
"@nocobase/plugin-ui-schema-storage": "1.4.0-alpha",
|
||||||
"@nocobase/plugin-users": "1.4.0-alpha",
|
"@nocobase/plugin-users": "1.4.0-alpha",
|
||||||
|
"@nocobase/plugin-user-data-sync": "1.4.0-alpha",
|
||||||
"@nocobase/plugin-verification": "1.4.0-alpha",
|
"@nocobase/plugin-verification": "1.4.0-alpha",
|
||||||
"@nocobase/plugin-workflow": "1.4.0-alpha",
|
"@nocobase/plugin-workflow": "1.4.0-alpha",
|
||||||
"@nocobase/plugin-workflow-action-trigger": "1.4.0-alpha",
|
"@nocobase/plugin-workflow-action-trigger": "1.4.0-alpha",
|
||||||
|
@ -23,6 +23,7 @@ export class PresetNocoBase extends Plugin {
|
|||||||
'field-sort',
|
'field-sort',
|
||||||
'verification',
|
'verification',
|
||||||
'users',
|
'users',
|
||||||
|
'user-data-sync',
|
||||||
'acl',
|
'acl',
|
||||||
'field-china-region',
|
'field-china-region',
|
||||||
'workflow',
|
'workflow',
|
||||||
|
@ -12,6 +12,13 @@ fi
|
|||||||
|
|
||||||
lerna version $new_version --preid alpha --force-publish=* --no-git-tag-version -y
|
lerna version $new_version --preid alpha --force-publish=* --no-git-tag-version -y
|
||||||
|
|
||||||
|
echo $PRO_PLUGIN_REPOS | jq -r '.[]' | while read i; do
|
||||||
|
cd ./packages/pro-plugins/@nocobase/$i
|
||||||
|
git add .
|
||||||
|
git commit -m "chore(versions): 😊 publish v$(jq -r '.version' ../../../../lerna.json)"
|
||||||
|
git tag v$(jq -r '.version' ../../../../lerna.json)
|
||||||
|
cd ../../../../
|
||||||
|
done
|
||||||
cd ./packages/pro-plugins
|
cd ./packages/pro-plugins
|
||||||
git add .
|
git add .
|
||||||
git commit -m "chore(versions): 😊 publish v$(jq -r '.version' ../../lerna.json)"
|
git commit -m "chore(versions): 😊 publish v$(jq -r '.version' ../../lerna.json)"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user