mirror of
https://github.com/penpot/penpot.git
synced 2026-01-06 05:18:52 -05:00
Compare commits
317 Commits
niwinz-dev
...
andy-docs-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e3df1dc73 | ||
|
|
b90aba0f95 | ||
|
|
d4b7f231c7 | ||
|
|
d1607fbe54 | ||
|
|
cce1dd86a2 | ||
|
|
e184a9a8b9 | ||
|
|
58c6c94cb8 | ||
|
|
ecee7ecfc7 | ||
|
|
a4ada6dc8a | ||
|
|
b770145436 | ||
|
|
11b75408fe | ||
|
|
59f7ede4ff | ||
|
|
8954b05d76 | ||
|
|
cb4c155b32 | ||
|
|
0b346e02ff | ||
|
|
946f641917 | ||
|
|
e88039e46a | ||
|
|
3c45a8d0b4 | ||
|
|
c9d71f3b2d | ||
|
|
9f37175775 | ||
|
|
5ed870cc6e | ||
|
|
2a3d7e470d | ||
|
|
f654eb2dcd | ||
|
|
c21d705143 | ||
|
|
85c1750706 | ||
|
|
e2151409bf | ||
|
|
4fe6cfc57a | ||
|
|
2eed7444b7 | ||
|
|
ef376fbb7b | ||
|
|
18d5b84b00 | ||
|
|
c62fadac47 | ||
|
|
a264f84e6c | ||
|
|
9311ee4c87 | ||
|
|
b8c8579ff5 | ||
|
|
82295c79d4 | ||
|
|
5174591058 | ||
|
|
658303fa36 | ||
|
|
84013c21fa | ||
|
|
f3062ade39 | ||
|
|
ca19d4deeb | ||
|
|
dfceccca3d | ||
|
|
9e2d3b1fa1 | ||
|
|
4dc0f3b4eb | ||
|
|
215288b6b4 | ||
|
|
2e2c3e7bac | ||
|
|
7ac44009d5 | ||
|
|
f22eef5bf6 | ||
|
|
7c61049103 | ||
|
|
fe819c6ec4 | ||
|
|
1a4594a615 | ||
|
|
d2bff2853f | ||
|
|
ff96f7be85 | ||
|
|
a403af7ebd | ||
|
|
e3c9588c1c | ||
|
|
25b63e5675 | ||
|
|
6c59d633cd | ||
|
|
bb0a891638 | ||
|
|
c5bd183f73 | ||
|
|
06441063f2 | ||
|
|
9f11a2cb32 | ||
|
|
053b2c6248 | ||
|
|
fad058ee59 | ||
|
|
568c2fd9d7 | ||
|
|
794eb78aca | ||
|
|
e761bcac85 | ||
|
|
1929ee36ed | ||
|
|
84cccd1b79 | ||
|
|
e66d44ca81 | ||
|
|
2f3b464715 | ||
|
|
286e477ad5 | ||
|
|
6e6749f42e | ||
|
|
7b6aa0c12a | ||
|
|
409f95ac17 | ||
|
|
25950be077 | ||
|
|
9eda1d0d78 | ||
|
|
f6c4f800c4 | ||
|
|
f363d6a801 | ||
|
|
e88ce0d52f | ||
|
|
fe5fe7a933 | ||
|
|
699cc147b5 | ||
|
|
b1d792a757 | ||
|
|
18e6842e35 | ||
|
|
0df420d353 | ||
|
|
dac2d31b35 | ||
|
|
1d3a1a094a | ||
|
|
9652996f07 | ||
|
|
2a2735cd67 | ||
|
|
0552b6e713 | ||
|
|
30e655b1da | ||
|
|
0aeecc6268 | ||
|
|
5cec006a76 | ||
|
|
307e06372b | ||
|
|
9f24e76c27 | ||
|
|
7954eaf529 | ||
|
|
fe406b577e | ||
|
|
76c03af024 | ||
|
|
fe5cdcbdc7 | ||
|
|
2479a06f9a | ||
|
|
fc5e4a821b | ||
|
|
3cdbc27de9 | ||
|
|
a8ed1371d4 | ||
|
|
e7bac41c37 | ||
|
|
dede2a8f8e | ||
|
|
e94abad3eb | ||
|
|
f8bc6e12a9 | ||
|
|
a2c3208af9 | ||
|
|
a303df9c34 | ||
|
|
bda24f3829 | ||
|
|
94fd3119e8 | ||
|
|
c8091b42a7 | ||
|
|
41a859b444 | ||
|
|
1cf20f7604 | ||
|
|
629541bc9d | ||
|
|
44245d1b5f | ||
|
|
a8692c72c6 | ||
|
|
7b7da59ca9 | ||
|
|
b78e3159d1 | ||
|
|
2106028350 | ||
|
|
c2e7f9dc42 | ||
|
|
8568098c5e | ||
|
|
c8bc1ef1d2 | ||
|
|
59bd434d05 | ||
|
|
3cf1e53340 | ||
|
|
24f0956630 | ||
|
|
91991d5b32 | ||
|
|
447b6ed1ab | ||
|
|
336486fecd | ||
|
|
bbd417c119 | ||
|
|
e3ffd45a18 | ||
|
|
52a4a46ebd | ||
|
|
025423a75e | ||
|
|
a6f17e35dd | ||
|
|
9a8cac3cac | ||
|
|
884b857d17 | ||
|
|
a20bbeff79 | ||
|
|
2a5f1f870b | ||
|
|
cf5303a39c | ||
|
|
bf1e26c4e6 | ||
|
|
4713d943d1 | ||
|
|
df083cb315 | ||
|
|
6401b25964 | ||
|
|
65f4adc68e | ||
|
|
dfab472522 | ||
|
|
9759adf747 | ||
|
|
9ae1a08573 | ||
|
|
ddab2cab14 | ||
|
|
cf1a8fff65 | ||
|
|
45be4769d7 | ||
|
|
a68c41709a | ||
|
|
4290bce718 | ||
|
|
3dd237002b | ||
|
|
e0fb112bfb | ||
|
|
2629537fd3 | ||
|
|
cb7711f637 | ||
|
|
a114e9adb0 | ||
|
|
5fed5fa158 | ||
|
|
6c8873c7f5 | ||
|
|
37b0c4adc0 | ||
|
|
9106617436 | ||
|
|
68a95cf0d0 | ||
|
|
1438632dde | ||
|
|
112fa46896 | ||
|
|
6da5bbf33a | ||
|
|
95faf340c4 | ||
|
|
723ea508df | ||
|
|
cbe538261c | ||
|
|
1925e6782f | ||
|
|
bcd950c141 | ||
|
|
b215689566 | ||
|
|
dfe0f64c7c | ||
|
|
9de3910526 | ||
|
|
59eb75d8c3 | ||
|
|
6670b76ccc | ||
|
|
09b9383a0b | ||
|
|
31e37f352d | ||
|
|
c5958e4d61 | ||
|
|
f1e7149e88 | ||
|
|
d80ef17623 | ||
|
|
ffe469ce71 | ||
|
|
c35bb6e09a | ||
|
|
8d404d97a1 | ||
|
|
fa2b0bd67c | ||
|
|
9563d1b1f6 | ||
|
|
33fc578f96 | ||
|
|
79786dde16 | ||
|
|
926b2c9cfb | ||
|
|
c1b2aa7628 | ||
|
|
991b26b73f | ||
|
|
254a7461b2 | ||
|
|
1384ed8aba | ||
|
|
c9393c0cfb | ||
|
|
6eeb55fb88 | ||
|
|
c759afc10d | ||
|
|
090cb63568 | ||
|
|
f223831766 | ||
|
|
854f286364 | ||
|
|
2846b80cf7 | ||
|
|
ad0ef82ffc | ||
|
|
3bb547fc45 | ||
|
|
c3b326d95e | ||
|
|
8c1a97dac5 | ||
|
|
4053e8c8db | ||
|
|
ee86a3943d | ||
|
|
46b3e174ed | ||
|
|
c0c2c9489c | ||
|
|
c05c179d67 | ||
|
|
596193d34d | ||
|
|
15eee0d8d8 | ||
|
|
0b7444e8ff | ||
|
|
96a91dc710 | ||
|
|
503d431d8e | ||
|
|
ed5875f29a | ||
|
|
b12d44150b | ||
|
|
66f5eb57b9 | ||
|
|
7023880e67 | ||
|
|
0e4cf23a93 | ||
|
|
3f93b0d44b | ||
|
|
4a6c3d6ad3 | ||
|
|
478439f055 | ||
|
|
9ef2454210 | ||
|
|
a702fee3cd | ||
|
|
cac639d267 | ||
|
|
0daf548773 | ||
|
|
9249a5d4ea | ||
|
|
4c83feaa31 | ||
|
|
4d8ad19eea | ||
|
|
e7e7d576b2 | ||
|
|
7f2af1c355 | ||
|
|
ad38a21053 | ||
|
|
9bb92277e4 | ||
|
|
643621a389 | ||
|
|
ef8d2e7418 | ||
|
|
adffac4eec | ||
|
|
575342b3bb | ||
|
|
b6ecb4368e | ||
|
|
081df7fc03 | ||
|
|
358343b4df | ||
|
|
d7c19325cc | ||
|
|
b472a8ab19 | ||
|
|
2b83d0d0e9 | ||
|
|
911ac263fa | ||
|
|
545c78eb74 | ||
|
|
124b098c92 | ||
|
|
43ed430475 | ||
|
|
edd3b1512e | ||
|
|
d9623c3c88 | ||
|
|
f052e31ff0 | ||
|
|
73dfe12ec9 | ||
|
|
0c3d73745e | ||
|
|
a6ecc4fb3c | ||
|
|
625d9ab188 | ||
|
|
e6cc15e19b | ||
|
|
0af2cd6413 | ||
|
|
443e0b0206 | ||
|
|
a4fa5e9304 | ||
|
|
d94c311a1e | ||
|
|
c0c8390a7d | ||
|
|
f4be117219 | ||
|
|
7eb590e9fd | ||
|
|
6588913141 | ||
|
|
5c4a60aee7 | ||
|
|
af02e12685 | ||
|
|
675864ce0b | ||
|
|
c55f3182d8 | ||
|
|
0d6eac7656 | ||
|
|
7acfd199aa | ||
|
|
33d6f543a1 | ||
|
|
4237ef572e | ||
|
|
6babea8b12 | ||
|
|
6b7f91c671 | ||
|
|
b3b183c151 | ||
|
|
59f2ee87e6 | ||
|
|
6af8940a46 | ||
|
|
e1a1110f06 | ||
|
|
1dcf1e0b0f | ||
|
|
4c3e345c9c | ||
|
|
a3b9a9f07b | ||
|
|
17ec360720 | ||
|
|
89b67d59d5 | ||
|
|
77be00014e | ||
|
|
e336f287b6 | ||
|
|
50aa6ff306 | ||
|
|
56f162f219 | ||
|
|
f7a0c4139a | ||
|
|
7c39e321c4 | ||
|
|
ce6a863599 | ||
|
|
832690e71e | ||
|
|
7526cb0d71 | ||
|
|
3292109ab0 | ||
|
|
be376d2030 | ||
|
|
4d455b5e9f | ||
|
|
50ce28e378 | ||
|
|
1eee8e2ce8 | ||
|
|
5e6ce9172f | ||
|
|
03adbc2ae8 | ||
|
|
e63a3f76f7 | ||
|
|
6d42d456fb | ||
|
|
c818b6f88f | ||
|
|
3f3c7905b4 | ||
|
|
e8dd13beb2 | ||
|
|
fc6b64aa68 | ||
|
|
5e0a2f66e3 | ||
|
|
108d4fabba | ||
|
|
2e277a39ca | ||
|
|
814ec43714 | ||
|
|
54bb9ea755 | ||
|
|
374e921672 | ||
|
|
64e5ea93a0 | ||
|
|
2562d70880 | ||
|
|
d99ef29152 | ||
|
|
d5a2cd9cd2 | ||
|
|
82d2889e96 | ||
|
|
ff2e845f2c | ||
|
|
aa94671002 | ||
|
|
52cf136f84 | ||
|
|
808427795c | ||
|
|
172c6ad4b8 |
@@ -1,18 +1,45 @@
|
||||
name: Build and Upload Penpot Bundles
|
||||
name: Build and Upload Penpot Bundle
|
||||
|
||||
on:
|
||||
# Create bundle from manual action
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
build_wasm:
|
||||
description: 'BUILD_WASM. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
build_storybook:
|
||||
description: 'BUILD_STORYBOOK. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
workflow_call:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
build_wasm:
|
||||
description: 'BUILD_WASM. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
build_storybook:
|
||||
description: 'BUILD_STORYBOOK. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
|
||||
jobs:
|
||||
build-bundles:
|
||||
name: Build and Upload Penpot Bundles
|
||||
build-bundle:
|
||||
name: Build and Upload Penpot Bundle
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
@@ -30,12 +57,12 @@ jobs:
|
||||
id: vars
|
||||
run: |
|
||||
echo "commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||
echo "gh_branch=${{ github.base_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Docker Buildx for multi-arch build
|
||||
uses: docker/setup-buildx-action@v3
|
||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run manage.sh build-bundle from host
|
||||
env:
|
||||
BUILD_WASM: ${{ inputs.build_wasm }}
|
||||
BUILD_STORYBOOK: ${{ inputs.build_storybook }}
|
||||
run: ./manage.sh build-bundle
|
||||
|
||||
- name: Prepare directories for zipping
|
||||
@@ -43,16 +70,22 @@ jobs:
|
||||
mkdir zips
|
||||
mv bundles penpot
|
||||
|
||||
- name: Create zip bundles
|
||||
- name: Create zip bundle
|
||||
run: |
|
||||
echo "📦 Packaging Penpot bundles..."
|
||||
echo "📦 Packaging Penpot bundle..."
|
||||
zip -r zips/penpot.zip penpot
|
||||
|
||||
- name: Upload Penpot bundle to S3
|
||||
if: github.ref_type == 'branch'
|
||||
run: |
|
||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_branch}}-latest.zip
|
||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}-latest.zip
|
||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.commit_hash }}.zip
|
||||
|
||||
- name: Upload Penpot bundle to S3
|
||||
if: github.ref_type == 'tag'
|
||||
run: |
|
||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
@@ -60,5 +93,5 @@ jobs:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
TEXT: |
|
||||
❌ *[PENPOT] Error during the execution of the job*
|
||||
📄 Triggered from ref: `${{ steps.vars.outputs.gh_branch}}`
|
||||
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
6
.github/workflows/build-develop.yml
vendored
6
.github/workflows/build-develop.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build and Upload Penpot DEVELOP Bundles
|
||||
name: DEVELOP - Build and Upload Penpot Bundle
|
||||
|
||||
on:
|
||||
schedule:
|
||||
@@ -6,7 +6,9 @@ on:
|
||||
|
||||
jobs:
|
||||
build-develop-bundle:
|
||||
uses: ./.github/workflows/build-bundles.yml
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "develop"
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
8
.github/workflows/build-staging.yml
vendored
8
.github/workflows/build-staging.yml
vendored
@@ -1,12 +1,14 @@
|
||||
name: Build and Upload Penpot STAGING Bundles
|
||||
name: STAGING - Build and Upload Penpot Bundle
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * 1-5'
|
||||
- cron: '36 5-20 * * 1-5'
|
||||
|
||||
jobs:
|
||||
build-staging-bundle:
|
||||
uses: ./.github/workflows/build-bundles.yml
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "staging"
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
15
.github/workflows/build-tag.yml
vendored
Normal file
15
.github/workflows/build-tag.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: TAG - Build and Upload Penpot Bundle
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build-tag-bundle:
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: ${{ github.ref_name }}
|
||||
build_wasm: "no"
|
||||
build_storybook: "yes"
|
||||
2
.github/workflows/commit-checker.yml
vendored
2
.github/workflows/commit-checker.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Check Commit Type
|
||||
uses: gsactions/commit-message-checker@v2
|
||||
with:
|
||||
pattern: '^(Merge|Revert|:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle):)\s[A-Z].*[^.]$'
|
||||
pattern: '^(Merge|Revert|:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind):)\s[A-Z].*[^.]$'
|
||||
flags: 'gm'
|
||||
error: 'Commit should match CONTRIBUTING.md guideline'
|
||||
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,6 +31,7 @@
|
||||
/.clj-kondo/.cache
|
||||
/_dump
|
||||
/notes
|
||||
/playground/
|
||||
/backend/*.md
|
||||
/backend/*.sql
|
||||
/backend/*.txt
|
||||
|
||||
45
CHANGES.md
45
CHANGES.md
@@ -4,13 +4,37 @@
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- Variants
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Add efficiency enhancements to right sidebar [Github #7182](https://github.com/penpot/penpot/pull/7182)
|
||||
- Add defaults for artboard drawing [Taiga #494](https://tree.taiga.io/project/penpot/us/494?milestone=465047)
|
||||
- Continuous display of distances between elements when moving a layer with the keyboard [Taiga #1780](https://tree.taiga.io/project/penpot/us/1780)
|
||||
- New Number token - unitless values [Taiga #10936](https://tree.taiga.io/project/penpot/us/10936)
|
||||
- New font-family token [Taiga #10937](https://tree.taiga.io/project/penpot/us/10937)
|
||||
- New text case token [Taiga #10942](https://tree.taiga.io/project/penpot/us/10942)
|
||||
- New text-decoration token [Taiga #10941](https://tree.taiga.io/project/penpot/us/10941)
|
||||
- New letter spacing token [Taiga #10940](https://tree.taiga.io/project/penpot/us/10940)
|
||||
- New font weight token [Taiga #10939](https://tree.taiga.io/project/penpot/us/10939)
|
||||
- Upgrade Node to v22.18.0 [Github #7283](https://github.com/penpot/penpot/pull/7283)
|
||||
- Upgrade the base docker image for penpot frontend to v1.29.1 [Github #7283](https://github.com/penpot/penpot/pull/7283)
|
||||
- Create variant from an existing component [Taiga #2088](https://tree.taiga.io/project/penpot/us/2088)
|
||||
- Create variant from an existing variant [Taiga #8282](https://tree.taiga.io/project/penpot/us/8282)
|
||||
- Actions over a component with variants [Taiga #10503](https://tree.taiga.io/project/penpot/us/10503)
|
||||
- Create a variant by dragging a component into a component with variants [Taiga #8134](https://tree.taiga.io/project/penpot/us/8134)
|
||||
- Transform a variant into an individual component [Taiga #8141](https://tree.taiga.io/project/penpot/us/8141)
|
||||
- Delete variant [Taiga #6890](https://tree.taiga.io/project/penpot/us/6890)
|
||||
- Restore an orphaned copy of a variant [Taiga #10446](https://tree.taiga.io/project/penpot/us/10446)
|
||||
- Add, Edit & Delete variant properties name and value [Taiga #6892](https://tree.taiga.io/project/penpot/us/6892)
|
||||
- Retrieve variants [Taiga #6888](https://tree.taiga.io/project/penpot/us/6888)
|
||||
- Retrieve variants with nested components [Taiga #10277](https://tree.taiga.io/project/penpot/us/10277)
|
||||
- Create variants in bulk from existing components [Taiga #7926](https://tree.taiga.io/project/penpot/us/7926)
|
||||
- Alternative ways of creating variants - Button Design Tab [Taiga #10316](https://tree.taiga.io/project/penpot/us/10316)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -23,8 +47,25 @@
|
||||
- Fix font size/variant not updated when editing a text [Taiga #11552](https://tree.taiga.io/project/penpot/issue/11552)
|
||||
- Fix issue where Alt + arrow keys shortcut interferes with letter-spacing when moving text layers [Taiga #11552](https://tree.taiga.io/project/penpot/issue/11771)
|
||||
- Fix consistency issues on how font variants are visualized [Taiga #11499](https://tree.taiga.io/project/penpot/us/11499)
|
||||
- Fix parsing rx and ry SVG values for rect radius [Taiga #11861](https://tree.taiga.io/project/penpot/issue/11861)
|
||||
- Misleading affordance in saved versions [Taiga #11887](https://tree.taiga.io/project/penpot/issue/11887)
|
||||
- Fix pasting RTF text crashes penpot [Taiga #11717](https://tree.taiga.io/project/penpot/issue/11717)
|
||||
- Fix navigation arrows in Libraries & Templates carousel [Taiga #10609](https://tree.taiga.io/project/penpot/issue/10609)
|
||||
- Fix applying tokens with zero value to size [Taiga #11618](https://tree.taiga.io/project/penpot/issue/11618)
|
||||
- Fix typo [Taiga #11969](https://tree.taiga.io/project/penpot/issue/11969)
|
||||
- Fix typo [Taiga #11970](https://tree.taiga.io/project/penpot/issue/11970)
|
||||
- Fix typos [Taiga #11971](https://tree.taiga.io/project/penpot/issue/11971)
|
||||
- Fix inconsistent naming for "Flatten" [Taiga #8371](https://tree.taiga.io/project/penpot/issue/8371)
|
||||
- Layout item tokens should be unapplied when moving out of a layout [Taiga #11012](https://tree.taiga.io/project/penpot/issue/11012)
|
||||
- Fix incorrect date displayed for support plan [Taiga #11986](https://tree.taiga.io/project/penpot/issue/11986)
|
||||
- Fix can't import 'borderWidth' type token [#132](https://github.com/tokens-studio/penpot/issues/132)
|
||||
- Fix moving elements up or down while pressing alt [Taiga Issue #11992](https://tree.taiga.io/project/penpot/issue/11992)
|
||||
- Fix conflicting shortcuts (remove dec/inc line height and letter spacing) [Taiga #12102](https://tree.taiga.io/project/penpot/issue/12102)
|
||||
- Fix conflicting shortcuts (remove text-align shortcuts) [Taiga #12047](https://tree.taiga.io/project/penpot/issue/12047)
|
||||
- Fix export file with empty tokens library [Taiga #12137](https://tree.taiga.io/project/penpot/issue/12137)
|
||||
- Fix context menu on spacing tokens [Taiga #12141](https://tree.taiga.io/project/penpot/issue/12141)
|
||||
|
||||
## 2.9.0 (Unreleased)
|
||||
## 2.9.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
@@ -52,6 +93,8 @@
|
||||
- Add the ability to show login dialog on profile settings [Github #6871](https://github.com/penpot/penpot/pull/6871)
|
||||
- Improve the application of tokens with object specific tokens [Taiga #10209](https://tree.taiga.io/project/penpot/us/10209)
|
||||
- Add info to apply-token event [Taiga #11710](https://tree.taiga.io/project/penpot/task/11710)
|
||||
- Fix double click on set name input [Taiga #11747](https://tree.taiga.io/project/penpot/issue/11747)
|
||||
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
:deps
|
||||
{penpot/common {:local/root "../common"}
|
||||
org.clojure/clojure {:mvn/version "1.12.1"}
|
||||
org.clojure/clojure {:mvn/version "1.12.2"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-3"}
|
||||
@@ -38,7 +38,7 @@
|
||||
metosin/reitit-core {:mvn/version "0.9.1"}
|
||||
nrepl/nrepl {:mvn/version "1.3.1"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.7.6"}
|
||||
org.postgresql/postgresql {:mvn/version "42.7.7"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.49.1.0"}
|
||||
|
||||
com.zaxxer/HikariCP {:mvn/version "6.3.0"}
|
||||
@@ -65,7 +65,7 @@
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.31.55"}}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.33.8"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
|
||||
(ns user
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.data :as d]
|
||||
[app.common.debug :as debug]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.fressian :as fres]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.perf :as perf]
|
||||
[app.common.pprint :as pp]
|
||||
@@ -19,8 +21,9 @@
|
||||
[app.common.schema.desc-js-like :as smdj]
|
||||
[app.common.schema.desc-native :as smdn]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.openapi :as oapi]
|
||||
[app.common.spec :as us]
|
||||
[app.common.json :as json]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
@@ -30,7 +33,6 @@
|
||||
[app.srepl.helpers :as srepl.helpers]
|
||||
[app.srepl.main :as srepl]
|
||||
[app.util.blob :as blob]
|
||||
[app.common.time :as ct]
|
||||
[clj-async-profiler.core :as prof]
|
||||
[clojure.contrib.humanize :as hum]
|
||||
[clojure.java.io :as io]
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.weak :as weak]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
@@ -141,11 +142,13 @@
|
||||
([index coll attr]
|
||||
(reduce #(index-object %1 %2 attr) index coll)))
|
||||
|
||||
(defn- decode-row-features
|
||||
[{:keys [features] :as row}]
|
||||
(defn decode-row
|
||||
[{:keys [data changes features] :as row}]
|
||||
(when row
|
||||
(cond-> row
|
||||
(db/pgarray? features) (assoc :features (db/decode-pgarray features #{})))))
|
||||
features (assoc :features (db/decode-pgarray features #{}))
|
||||
changes (assoc :changes (blob/decode changes))
|
||||
data (assoc :data (blob/decode data)))))
|
||||
|
||||
(def sql:get-minimal-file
|
||||
"SELECT f.id,
|
||||
@@ -159,7 +162,6 @@
|
||||
[cfg id & {:as opts}]
|
||||
(db/get-with-sql cfg [sql:get-minimal-file id] opts))
|
||||
|
||||
;; DEPRECATED
|
||||
(defn decode-file
|
||||
"A general purpose file decoding function that resolves all external
|
||||
pointers, run migrations and return plain vanilla file map"
|
||||
@@ -167,8 +169,7 @@
|
||||
(binding [pmap/*load-fn* (partial fdata/load-pointer cfg id)]
|
||||
(let [file (->> file
|
||||
(fmigr/resolve-applied-migrations cfg)
|
||||
(fdata/resolve-file-data cfg)
|
||||
(fdata/decode-file-data cfg))
|
||||
(fdata/resolve-file-data cfg))
|
||||
libs (delay (get-resolved-file-libraries cfg file))]
|
||||
|
||||
(-> file
|
||||
@@ -179,119 +180,6 @@
|
||||
(update :data assoc :id id)
|
||||
(cond-> migrate? (fmg/migrate-file libs))))))
|
||||
|
||||
(def sql:get-file
|
||||
"SELECT f.id,
|
||||
f.project_id,
|
||||
f.created_at,
|
||||
f.modified_at,
|
||||
f.deleted_at,
|
||||
f.name,
|
||||
f.is_shared,
|
||||
f.has_media_trimmed,
|
||||
f.revn,
|
||||
f.data AS legacy_data,
|
||||
f.ignore_sync_until,
|
||||
f.comment_thread_seqn,
|
||||
f.features,
|
||||
f.version,
|
||||
f.vern,
|
||||
p.team_id,
|
||||
coalesce(fd.backend, 'db') AS backend,
|
||||
fd.metadata AS metadata,
|
||||
fd.data AS data
|
||||
FROM file AS f
|
||||
LEFT JOIN file_data AS fd ON (fd.file_id = f.id AND fd.id = f.id)
|
||||
INNER JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE f.id = ?")
|
||||
|
||||
(defn- migrate-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [read-only?]} {:keys [id] :as file}]
|
||||
(binding [pmap/*load-fn* (partial fdata/load-pointer cfg id)
|
||||
pmap/*tracked* (pmap/create-tracked)]
|
||||
(let [libs (delay (get-resolved-file-libraries cfg file))
|
||||
;; For avoid unnecesary overhead of creating multiple
|
||||
;; pointers and handly internally with objects map in their
|
||||
;; worst case (when probably all shapes and all pointers
|
||||
;; will be readed in any case), we just realize/resolve them
|
||||
;; before applying the migration to the file.
|
||||
file (-> (fdata/realize cfg file)
|
||||
(fmg/migrate-file libs))]
|
||||
|
||||
(if (or read-only? (db/read-only? conn))
|
||||
file
|
||||
(do ;; When file is migrated, we break the rule of no
|
||||
;; perform mutations on get operations and update the
|
||||
;; file with all migrations applied
|
||||
(update-file! cfg file)
|
||||
(fmigr/resolve-applied-migrations cfg file))))))
|
||||
|
||||
;; FIXME: filter by project-id
|
||||
(defn- get-file*
|
||||
[{:keys [::db/conn] :as cfg} id
|
||||
{:keys [#_project-id
|
||||
migrate?
|
||||
realize?
|
||||
decode?
|
||||
skip-locked?
|
||||
include-deleted?
|
||||
throw-if-not-exists?
|
||||
lock-for-update?]
|
||||
:or {lock-for-update? false
|
||||
migrate? true
|
||||
decode? true
|
||||
include-deleted? false
|
||||
throw-if-not-exists? true
|
||||
realize? false}
|
||||
:as options}]
|
||||
|
||||
(assert (db/connection? conn) "expected cfg with valid connection")
|
||||
|
||||
(let [sql
|
||||
(if lock-for-update?
|
||||
(str sql:get-file " FOR UPDATE of f")
|
||||
sql:get-file)
|
||||
|
||||
sql
|
||||
(if skip-locked?
|
||||
(str sql " SKIP LOCKED")
|
||||
sql)
|
||||
|
||||
file
|
||||
(db/get-with-sql conn [sql id]
|
||||
{::db/throw-if-not-exists false
|
||||
::db/remove-deleted (not include-deleted?)})
|
||||
|
||||
file
|
||||
(-> file
|
||||
(d/update-when :features db/decode-pgarray #{})
|
||||
(d/update-when :metadata fdata/decode-metadata))]
|
||||
|
||||
(if file
|
||||
(let [file
|
||||
(->> file
|
||||
(fmigr/resolve-applied-migrations cfg)
|
||||
(fdata/resolve-file-data cfg))
|
||||
|
||||
will-migrate?
|
||||
(and migrate? (fmg/need-migration? file))]
|
||||
|
||||
(if decode?
|
||||
(cond->> (fdata/decode-file-data cfg file)
|
||||
(and realize? (not will-migrate?))
|
||||
(fdata/realize cfg)
|
||||
|
||||
will-migrate?
|
||||
(migrate-file cfg options))
|
||||
|
||||
file))
|
||||
|
||||
(when-not (or skip-locked? (not throw-if-not-exists?))
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "database object not found"
|
||||
:table :file
|
||||
:file-id id)))))
|
||||
|
||||
(defn get-file
|
||||
"Get file, resolve all features and apply migrations.
|
||||
|
||||
@@ -299,7 +187,10 @@
|
||||
operations on file, because it removes the ovehead of lazy fetching
|
||||
and decoding."
|
||||
[cfg file-id & {:as opts}]
|
||||
(db/run! cfg get-file* file-id opts))
|
||||
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(when-let [row (db/get* conn :file {:id file-id}
|
||||
(assoc opts ::db/remove-deleted false))]
|
||||
(decode-file cfg row opts)))))
|
||||
|
||||
(defn clean-file-features
|
||||
[file]
|
||||
@@ -323,12 +214,12 @@
|
||||
(let [conn (db/get-connection cfg)
|
||||
ids (db/create-array conn "uuid" ids)]
|
||||
(->> (db/exec! conn [sql:get-teams ids])
|
||||
(map decode-row-features))))
|
||||
(map decode-row))))
|
||||
|
||||
(defn get-team
|
||||
[cfg team-id]
|
||||
(-> (db/get cfg :team {:id team-id})
|
||||
(decode-row-features)))
|
||||
(decode-row)))
|
||||
|
||||
(defn get-fonts
|
||||
[cfg team-id]
|
||||
@@ -607,43 +498,21 @@
|
||||
(defn- file->params
|
||||
[file]
|
||||
(-> (select-keys file file-attrs)
|
||||
(assoc :data nil)
|
||||
(dissoc :team-id)
|
||||
(dissoc :migrations)))
|
||||
|
||||
(defn file->file-data-params
|
||||
[{:keys [id backend] :as file} & {:as opts}]
|
||||
(let [created-at (or (:created-at file) (ct/now))
|
||||
modified-at (or (:modified-at file) created-at)
|
||||
backend (if (and (::overwrite-storage-backend opts) backend)
|
||||
backend
|
||||
(cf/get :file-storage-backend))]
|
||||
|
||||
(d/without-nils
|
||||
{:id id
|
||||
:type "main"
|
||||
:file-id id
|
||||
:data (:data file)
|
||||
:metadata (:metadata file)
|
||||
:backend backend
|
||||
:created-at created-at
|
||||
:modified-at modified-at})))
|
||||
|
||||
(defn insert-file!
|
||||
"Insert a new file into the database table. Expectes a not-encoded file.
|
||||
Returns nil."
|
||||
[{:keys [::db/conn] :as cfg} file & {:as opts}]
|
||||
|
||||
(when (:migrations file)
|
||||
(fmigr/upsert-migrations! conn file))
|
||||
|
||||
(let [file (encode-file cfg file)]
|
||||
(db/insert! conn :file
|
||||
(file->params file)
|
||||
(assoc opts ::db/return-keys false))
|
||||
|
||||
(->> (file->file-data-params file)
|
||||
(fdata/update! cfg))
|
||||
|
||||
{::db/return-keys false})
|
||||
nil))
|
||||
|
||||
(defn update-file!
|
||||
@@ -657,25 +526,21 @@
|
||||
(let [file
|
||||
(encode-file cfg file)
|
||||
|
||||
file-params
|
||||
(file->params (dissoc file :id))
|
||||
params
|
||||
(file->params (dissoc file :id))]
|
||||
|
||||
file-data-params
|
||||
(file->file-data-params file)]
|
||||
|
||||
(db/update! conn :file file-params
|
||||
(db/update! conn :file params
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(fdata/update! cfg file-data-params)
|
||||
nil))
|
||||
|
||||
(defn save-file!
|
||||
"Applies all the final validations and perist the file, binfile
|
||||
specific, should not be used outside of binfile domain.
|
||||
|
||||
Returns nil"
|
||||
[{:keys [::timestamp] :as cfg} file & {:as opts}]
|
||||
|
||||
(assert (ct/inst? timestamp) "expected valid timestamp")
|
||||
|
||||
(let [file (-> file
|
||||
@@ -739,14 +604,22 @@
|
||||
;; FIXME: :is-indirect set to false to all rows looks
|
||||
;; completly useless
|
||||
(map #(assoc % :is-indirect false))
|
||||
(map decode-row-features))
|
||||
(map decode-row))
|
||||
(db/exec! conn [sql:get-file-libraries file-id])))
|
||||
|
||||
;; FIXME: this will use a lot of memory if file uses too many big
|
||||
;; libraries, we should load required libraries on demand
|
||||
(defn get-resolved-file-libraries
|
||||
"A helper for preload file libraries"
|
||||
[{:keys [::db/conn] :as cfg} file]
|
||||
(->> (get-file-libraries conn (:id file))
|
||||
(into [file] (map #(get-file cfg (:id %))))
|
||||
(d/index-by :id)))
|
||||
"Get all file libraries including itself. Returns an instance of
|
||||
LoadableWeakValueMap that allows do not have strong references to
|
||||
the loaded libraries and reduce possible memory pressure on having
|
||||
all this libraries loaded at same time on processing file validation
|
||||
or file migration.
|
||||
|
||||
This still requires at least one library at time to be loaded while
|
||||
access to it is performed, but it improves considerable not having
|
||||
the need of loading all the libraries at the same time."
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
(let [library-ids (->> (get-file-libraries conn (:id file))
|
||||
(map :id)
|
||||
(cons (:id file)))
|
||||
load-fn #(get-file cfg % :migrate? false)]
|
||||
(weak/loadable-weak-value-map library-ids load-fn {id file})))
|
||||
|
||||
@@ -36,11 +36,6 @@
|
||||
"fdata/shape-data-type"
|
||||
nil
|
||||
|
||||
;; There is no migration needed, but we don't want to allow
|
||||
;; copy paste nor import of variant files into no-variant teams
|
||||
"variants/v1"
|
||||
nil
|
||||
|
||||
(ex/raise :type :internal
|
||||
:code :no-migration-defined
|
||||
:hint (str/ffmt "no migation for feature '%' on file importation" feature)
|
||||
|
||||
@@ -346,7 +346,7 @@
|
||||
thumbnails (->> (bfc/get-file-object-thumbnails cfg file-id)
|
||||
(mapv #(dissoc % :file-id)))
|
||||
|
||||
file (cond-> (bfc/get-file cfg file-id :realize? true)
|
||||
file (cond-> (bfc/get-file cfg file-id)
|
||||
detach?
|
||||
(-> (ctf/detach-external-references file-id)
|
||||
(dissoc :libraries))
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
|
||||
(defn- write-file!
|
||||
[cfg file-id]
|
||||
(let [file (bfc/get-file cfg file-id :realize? true)
|
||||
(let [file (bfc/get-file cfg file-id)
|
||||
thumbs (bfc/get-file-object-thumbnails cfg file-id)
|
||||
media (bfc/get-file-media cfg file)
|
||||
rels (bfc/get-files-rels cfg #{file-id})]
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
[app.common.types.page :as ctp]
|
||||
[app.common.types.plugins :as ctpg]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.tokens-lib :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.common.types.typography :as cty]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
@@ -41,8 +41,10 @@
|
||||
[datoteka.fs :as fs]
|
||||
[datoteka.io :as io])
|
||||
(:import
|
||||
java.io.File
|
||||
java.io.InputStream
|
||||
java.io.OutputStreamWriter
|
||||
java.lang.AutoCloseable
|
||||
java.util.zip.ZipEntry
|
||||
java.util.zip.ZipFile
|
||||
java.util.zip.ZipOutputStream))
|
||||
@@ -103,25 +105,25 @@
|
||||
(sm/encoder ctp/schema:page sm/json-transformer))
|
||||
|
||||
(def encode-shape
|
||||
(sm/encoder ::cts/shape sm/json-transformer))
|
||||
(sm/encoder cts/schema:shape sm/json-transformer))
|
||||
|
||||
(def encode-media
|
||||
(sm/encoder ::ctf/media sm/json-transformer))
|
||||
(sm/encoder ctf/schema:media sm/json-transformer))
|
||||
|
||||
(def encode-component
|
||||
(sm/encoder ::ctc/component sm/json-transformer))
|
||||
(sm/encoder ctc/schema:component sm/json-transformer))
|
||||
|
||||
(def encode-color
|
||||
(sm/encoder ctcl/schema:library-color sm/json-transformer))
|
||||
|
||||
(def encode-typography
|
||||
(sm/encoder ::cty/typography sm/json-transformer))
|
||||
(sm/encoder cty/schema:typography sm/json-transformer))
|
||||
|
||||
(def encode-tokens-lib
|
||||
(sm/encoder ::cto/tokens-lib sm/json-transformer))
|
||||
(sm/encoder ctob/schema:tokens-lib sm/json-transformer))
|
||||
|
||||
(def encode-plugin-data
|
||||
(sm/encoder ::ctpg/plugin-data sm/json-transformer))
|
||||
(sm/encoder ctpg/schema:plugin-data sm/json-transformer))
|
||||
|
||||
(def encode-storage-object
|
||||
(sm/encoder schema:storage-object sm/json-transformer))
|
||||
@@ -138,7 +140,7 @@
|
||||
(sm/decoder ctf/schema:media sm/json-transformer))
|
||||
|
||||
(def decode-component
|
||||
(sm/decoder ::ctc/component sm/json-transformer))
|
||||
(sm/decoder ctc/schema:component sm/json-transformer))
|
||||
|
||||
(def decode-color
|
||||
(sm/decoder ctcl/schema:library-color sm/json-transformer))
|
||||
@@ -147,19 +149,19 @@
|
||||
(sm/decoder schema:file sm/json-transformer))
|
||||
|
||||
(def decode-page
|
||||
(sm/decoder ::ctp/page sm/json-transformer))
|
||||
(sm/decoder ctp/schema:page sm/json-transformer))
|
||||
|
||||
(def decode-shape
|
||||
(sm/decoder ::cts/shape sm/json-transformer))
|
||||
(sm/decoder cts/schema:shape sm/json-transformer))
|
||||
|
||||
(def decode-typography
|
||||
(sm/decoder ::cty/typography sm/json-transformer))
|
||||
(sm/decoder cty/schema:typography sm/json-transformer))
|
||||
|
||||
(def decode-tokens-lib
|
||||
(sm/decoder cto/schema:tokens-lib sm/json-transformer))
|
||||
(sm/decoder ctob/schema:tokens-lib sm/json-transformer))
|
||||
|
||||
(def decode-plugin-data
|
||||
(sm/decoder ::ctpg/plugin-data sm/json-transformer))
|
||||
(sm/decoder ctpg/schema:plugin-data sm/json-transformer))
|
||||
|
||||
(def decode-storage-object
|
||||
(sm/decoder schema:storage-object sm/json-transformer))
|
||||
@@ -173,31 +175,31 @@
|
||||
(sm/check-fn schema:manifest))
|
||||
|
||||
(def validate-file
|
||||
(sm/check-fn ::ctf/file))
|
||||
(sm/check-fn ctf/schema:file))
|
||||
|
||||
(def validate-page
|
||||
(sm/check-fn ::ctp/page))
|
||||
(sm/check-fn ctp/schema:page))
|
||||
|
||||
(def validate-shape
|
||||
(sm/check-fn ::cts/shape))
|
||||
(sm/check-fn cts/schema:shape))
|
||||
|
||||
(def validate-media
|
||||
(sm/check-fn ::ctf/media))
|
||||
(sm/check-fn ctf/schema:media))
|
||||
|
||||
(def validate-color
|
||||
(sm/check-fn ctcl/schema:library-color))
|
||||
|
||||
(def validate-component
|
||||
(sm/check-fn ::ctc/component))
|
||||
(sm/check-fn ctc/schema:component))
|
||||
|
||||
(def validate-typography
|
||||
(sm/check-fn ::cty/typography))
|
||||
(sm/check-fn cty/schema:typography))
|
||||
|
||||
(def validate-tokens-lib
|
||||
(sm/check-fn ::cto/tokens-lib))
|
||||
(sm/check-fn ctob/schema:tokens-lib))
|
||||
|
||||
(def validate-plugin-data
|
||||
(sm/check-fn ::ctpg/plugin-data))
|
||||
(sm/check-fn ctpg/schema:plugin-data))
|
||||
|
||||
(def validate-storage-object
|
||||
(sm/check-fn schema:storage-object))
|
||||
@@ -222,11 +224,9 @@
|
||||
(throw (IllegalArgumentException.
|
||||
"the `include-libraries` and `embed-assets` are mutally excluding options")))
|
||||
|
||||
(let [detach? (and (not embed-assets) (not include-libraries))]
|
||||
(let [detach? (and (not embed-assets) (not include-libraries))]
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(cond-> (bfc/get-file cfg file-id
|
||||
{:realize? true
|
||||
:lock-for-update? true})
|
||||
(cond-> (bfc/get-file cfg file-id {::sql/for-update true})
|
||||
detach?
|
||||
(-> (ctf/detach-external-references file-id)
|
||||
(dissoc :libraries))
|
||||
@@ -253,9 +253,9 @@
|
||||
(write-entry! output path params)
|
||||
|
||||
(with-open [input (sto/get-object-data storage sobject)]
|
||||
(.putNextEntry output (ZipEntry. (str "objects/" id ext)))
|
||||
(.putNextEntry ^ZipOutputStream output (ZipEntry. (str "objects/" id ext)))
|
||||
(io/copy input output :size (:size sobject))
|
||||
(.closeEntry output))))))
|
||||
(.closeEntry ^ZipOutputStream output))))))
|
||||
|
||||
(defn- export-file
|
||||
[{:keys [::file-id ::output] :as cfg}]
|
||||
@@ -349,7 +349,8 @@
|
||||
typography (encode-typography object)]
|
||||
(write-entry! output path typography)))
|
||||
|
||||
(when tokens-lib
|
||||
(when (and tokens-lib
|
||||
(not (ctob/empty-lib? tokens-lib)))
|
||||
(let [path (str "files/" file-id "/tokens.json")
|
||||
encoded-tokens (encode-tokens-lib tokens-lib)]
|
||||
(write-entry! output path encoded-tokens)))))
|
||||
@@ -449,7 +450,7 @@
|
||||
(defn- read-manifest
|
||||
[^ZipFile input]
|
||||
(let [entry (get-zip-entry input "manifest.json")]
|
||||
(with-open [reader (zip-entry-reader input entry)]
|
||||
(with-open [^AutoCloseable reader (zip-entry-reader input entry)]
|
||||
(let [manifest (json/read reader :key-fn json/read-kebab-key)]
|
||||
(decode-manifest manifest)))))
|
||||
|
||||
@@ -539,12 +540,12 @@
|
||||
|
||||
(defn- read-entry
|
||||
[^ZipFile input entry]
|
||||
(with-open [reader (zip-entry-reader input entry)]
|
||||
(with-open [^AutoCloseable reader (zip-entry-reader input entry)]
|
||||
(json/read reader :key-fn json/read-kebab-key)))
|
||||
|
||||
(defn- read-plain-entry
|
||||
[^ZipFile input entry]
|
||||
(with-open [reader (zip-entry-reader input entry)]
|
||||
(with-open [^AutoCloseable reader (zip-entry-reader input entry)]
|
||||
(json/read reader)))
|
||||
|
||||
(defn- read-file
|
||||
@@ -1008,8 +1009,8 @@
|
||||
(try
|
||||
(l/info :hint "start exportation" :export-id (str id))
|
||||
(binding [bfc/*state* (volatile! (bfc/initial-state))]
|
||||
(with-open [output (io/output-stream output)]
|
||||
(with-open [output (ZipOutputStream. output)]
|
||||
(with-open [^AutoCloseable output (io/output-stream output)]
|
||||
(with-open [^AutoCloseable output (ZipOutputStream. output)]
|
||||
(let [cfg (assoc cfg ::output output)]
|
||||
(export-files cfg)
|
||||
(export-storage-objects cfg)))))
|
||||
@@ -1053,7 +1054,7 @@
|
||||
|
||||
(l/info :hint "import: started" :id (str id))
|
||||
(try
|
||||
(with-open [input (ZipFile. (fs/file input))]
|
||||
(with-open [input (ZipFile. ^File (fs/file input))]
|
||||
(import-files (assoc cfg ::bfc/input input)))
|
||||
|
||||
(catch Throwable cause
|
||||
@@ -1068,6 +1069,6 @@
|
||||
|
||||
(defn get-manifest
|
||||
[path]
|
||||
(with-open [input (ZipFile. (fs/file path))]
|
||||
(with-open [^AutoCloseable input (ZipFile. ^File (fs/file path))]
|
||||
(-> (read-manifest input)
|
||||
(validate-manifest))))
|
||||
|
||||
@@ -52,8 +52,6 @@
|
||||
|
||||
:redis-uri "redis://redis/0"
|
||||
|
||||
:file-storage-backend "db"
|
||||
|
||||
:objects-storage-backend "fs"
|
||||
:objects-storage-fs-directory "assets"
|
||||
|
||||
@@ -107,8 +105,7 @@
|
||||
[:auto-file-snapshot-timeout {:optional true} ::ct/duration]
|
||||
|
||||
[:media-max-file-size {:optional true} ::sm/int]
|
||||
[:deletion-delay {:optional true} ::ct/duration]
|
||||
[:file-clean-delay {:optional true} ::ct/duration]
|
||||
[:deletion-delay {:optional true} ::ct/duration] ;; REVIEW
|
||||
[:telemetry-enabled {:optional true} ::sm/boolean]
|
||||
[:default-blob-version {:optional true} ::sm/int]
|
||||
[:allow-demo-users {:optional true} ::sm/boolean]
|
||||
@@ -213,8 +210,6 @@
|
||||
[:prepl-host {:optional true} :string]
|
||||
[:prepl-port {:optional true} ::sm/int]
|
||||
|
||||
[:file-storage-backend :string]
|
||||
|
||||
[:media-directory {:optional true} :string] ;; REVIEW
|
||||
[:media-uri {:optional true} :string]
|
||||
[:assets-path {:optional true} :string]
|
||||
@@ -305,11 +300,6 @@
|
||||
(or (c/get config :deletion-delay)
|
||||
(ct/duration {:days 7})))
|
||||
|
||||
(defn get-file-clean-delay
|
||||
[]
|
||||
(or (c/get config :file-clean-delay)
|
||||
(ct/duration {:days 2})))
|
||||
|
||||
(defn get
|
||||
"A configuration getter. Helps code be more testable."
|
||||
([key]
|
||||
|
||||
@@ -53,8 +53,15 @@
|
||||
opts (cond-> opts
|
||||
(::order-by opts) (assoc :order-by (::order-by opts))
|
||||
(::columns opts) (assoc :columns (::columns opts))
|
||||
(::for-update opts) (assoc :suffix "FOR UPDATE")
|
||||
(::for-share opts) (assoc :suffix "FOR SHARE"))]
|
||||
|
||||
(or (::db/for-update opts)
|
||||
(::for-update opts))
|
||||
(assoc :suffix "FOR UPDATE")
|
||||
|
||||
(or (::db/for-share opts)
|
||||
(::for-share opts))
|
||||
(assoc :suffix "FOR SHARE"))]
|
||||
|
||||
(sql/for-query table where-params opts))))
|
||||
|
||||
(defn update
|
||||
|
||||
@@ -12,10 +12,7 @@
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.path :as path]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.storage :as sto]
|
||||
@@ -25,6 +22,14 @@
|
||||
[app.worker :as wrk]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; OFFLOAD
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn offloaded?
|
||||
[file]
|
||||
(= "objects-storage" (:data-backend file)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; OBJECTS-MAP
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -60,25 +65,36 @@
|
||||
objects)))))
|
||||
fdata))
|
||||
|
||||
|
||||
(defn realize-objects
|
||||
"Process a file and remove all instances of objects mao realizing them
|
||||
to a plain data. Used in operation where is more efficient have the
|
||||
whole file loaded in memory or we going to persist it in an
|
||||
alterantive storage."
|
||||
[_cfg file]
|
||||
(update file :data process-objects (partial into {})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; POINTER-MAP
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn get-file-data
|
||||
"Get file data given a file instance."
|
||||
[system file]
|
||||
(if (offloaded? file)
|
||||
(let [storage (sto/resolve system ::db/reuse-conn true)]
|
||||
(->> (sto/get-object storage (:data-ref-id file))
|
||||
(sto/get-object-bytes storage)))
|
||||
(:data file)))
|
||||
|
||||
(defn resolve-file-data
|
||||
[system file]
|
||||
(let [data (get-file-data system file)]
|
||||
(assoc file :data data)))
|
||||
|
||||
(defn decode-file-data
|
||||
[{:keys [::wrk/executor]} {:keys [data] :as file}]
|
||||
(cond-> file
|
||||
(bytes? data)
|
||||
(assoc :data (px/invoke! executor #(blob/decode data)))))
|
||||
|
||||
(defn load-pointer
|
||||
"A database loader pointer helper"
|
||||
[cfg file-id id]
|
||||
(let [fragment (db/get* cfg :file-data
|
||||
{:id id :file-id file-id :type "fragment"}
|
||||
{::sql/columns [:content :backend :id]})]
|
||||
[system file-id id]
|
||||
(let [fragment (db/get* system :file-data-fragment
|
||||
{:id id :file-id file-id}
|
||||
{::sql/columns [:data :data-backend :data-ref-id :id]})]
|
||||
|
||||
(l/trc :hint "load pointer"
|
||||
:file-id (str file-id)
|
||||
@@ -92,22 +108,22 @@
|
||||
:file-id file-id
|
||||
:fragment-id id))
|
||||
|
||||
;; FIXME: conditional thread scheduling for decoding big objects
|
||||
(blob/decode (:data fragment))))
|
||||
(let [data (get-file-data system fragment)]
|
||||
;; FIXME: conditional thread scheduling for decoding big objects
|
||||
(blob/decode data))))
|
||||
|
||||
(defn persist-pointers!
|
||||
"Persist all currently tracked pointer objects"
|
||||
[cfg file-id]
|
||||
(let [conn (db/get-connection cfg)]
|
||||
[system file-id]
|
||||
(let [conn (db/get-connection system)]
|
||||
(doseq [[id item] @pmap/*tracked*]
|
||||
(when (pmap/modified? item)
|
||||
(l/trc :hint "persist pointer" :file-id (str file-id) :id (str id))
|
||||
(let [content (-> item deref blob/encode)]
|
||||
(db/insert! conn :file-data
|
||||
(db/insert! conn :file-data-fragment
|
||||
{:id id
|
||||
:file-id file-id
|
||||
:type "fragment"
|
||||
:content content}))))))
|
||||
:data content}))))))
|
||||
|
||||
(defn process-pointers
|
||||
"Apply a function to all pointers on the file. Usuly used for
|
||||
@@ -121,14 +137,6 @@
|
||||
(d/update-vals update-fn')
|
||||
(update :pages-index d/update-vals update-fn'))))
|
||||
|
||||
(defn realize-pointers
|
||||
"Process a file and remove all instances of pointers realizing them to
|
||||
a plain data. Used in operation where is more efficient have the
|
||||
whole file loaded in memory."
|
||||
[cfg {:keys [id] :as file}]
|
||||
(binding [pmap/*load-fn* (partial load-pointer cfg id)]
|
||||
(update file :data process-pointers deref)))
|
||||
|
||||
(defn get-used-pointer-ids
|
||||
"Given a file, return all pointer ids used in the data."
|
||||
[fdata]
|
||||
@@ -192,314 +200,3 @@
|
||||
(update :features disj "fdata/path-data")
|
||||
(update :migrations disj "0003-convert-path-content")
|
||||
(vary-meta update ::fmg/migrated disj "0003-convert-path-content"))))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; GENERAL PURPOSE HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn realize
|
||||
"A helper that combines realize-pointers and realize-objects"
|
||||
[cfg file]
|
||||
(->> file
|
||||
(realize-pointers cfg)
|
||||
(realize-objects cfg)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; STORAGE
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defmulti resolve-file-data
|
||||
(fn [_cfg file] (or (get file :backend) "db")))
|
||||
|
||||
(defmethod resolve-file-data "db"
|
||||
[_cfg {:keys [legacy-data data] :as file}]
|
||||
(if (and (some? legacy-data) (not data))
|
||||
(-> file
|
||||
(assoc :data legacy-data)
|
||||
(dissoc :legacy-data))
|
||||
(dissoc file :legacy-data)))
|
||||
|
||||
(defmethod resolve-file-data "storage"
|
||||
[cfg object]
|
||||
(let [storage (sto/resolve cfg ::db/reuse-conn true)
|
||||
ref-id (-> object :metadata :storage-ref-id)
|
||||
data (->> (sto/get-object storage ref-id)
|
||||
(sto/get-object-bytes storage))]
|
||||
(-> object
|
||||
(assoc :data data)
|
||||
(dissoc :legacy-data))))
|
||||
|
||||
(defn decode-file-data
|
||||
[{:keys [::wrk/executor]} {:keys [data] :as file}]
|
||||
(cond-> file
|
||||
(bytes? data)
|
||||
(assoc :data (px/invoke! executor #(blob/decode data)))))
|
||||
|
||||
(def ^:private sql:insert-file-data
|
||||
"INSERT INTO file_data (file_id, id, created_at, modified_at,
|
||||
type, backend, metadata, data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
|
||||
(def ^:private sql:upsert-file-data
|
||||
(str sql:insert-file-data
|
||||
" ON CONFLICT (file_id, id)
|
||||
DO UPDATE SET modified_at=?,
|
||||
backend=?,
|
||||
metadata=?,
|
||||
data=?;"))
|
||||
|
||||
(defn- create-in-database
|
||||
[cfg {:keys [id file-id created-at modified-at type backend data metadata]}]
|
||||
(let [metadata (some-> metadata db/json)
|
||||
created-at (or created-at (ct/now))
|
||||
modified-at (or modified-at created-at)]
|
||||
(db/exec-one! cfg [sql:insert-file-data
|
||||
file-id id
|
||||
created-at
|
||||
modified-at
|
||||
type
|
||||
backend
|
||||
metadata
|
||||
data])))
|
||||
|
||||
(defn- upsert-in-database
|
||||
[cfg {:keys [id file-id created-at modified-at type backend data metadata]}]
|
||||
(let [metadata (some-> metadata db/json)
|
||||
created-at (or created-at (ct/now))
|
||||
modified-at (or modified-at created-at)]
|
||||
|
||||
(db/exec-one! cfg [sql:upsert-file-data
|
||||
file-id id
|
||||
created-at
|
||||
modified-at
|
||||
type
|
||||
backend
|
||||
metadata
|
||||
data
|
||||
modified-at
|
||||
backend
|
||||
metadata
|
||||
data])))
|
||||
|
||||
(defmulti ^:private handle-persistence
|
||||
(fn [_cfg params] (:backend params)))
|
||||
|
||||
(defmethod handle-persistence "db"
|
||||
[_ params]
|
||||
(dissoc params :metadata))
|
||||
|
||||
(defmethod handle-persistence "storage"
|
||||
[{:keys [::sto/storage] :as cfg}
|
||||
{:keys [id file-id data] :as params}]
|
||||
|
||||
(let [content (sto/content data)
|
||||
sobject (sto/put-object! storage
|
||||
{::sto/content content
|
||||
::sto/touch true
|
||||
:bucket "file-data"
|
||||
:content-type "application/octet-stream"
|
||||
:file-id file-id
|
||||
:id id})
|
||||
metadata {:storage-ref-id (:id sobject)}]
|
||||
(-> params
|
||||
(assoc :metadata metadata)
|
||||
(assoc :data nil))))
|
||||
|
||||
(defn- process-metadata
|
||||
[cfg metadata]
|
||||
(when-let [storage-id (:storage-ref-id metadata)]
|
||||
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
|
||||
(sto/touch-object! storage storage-id))))
|
||||
|
||||
(defn- default-backend
|
||||
[backend]
|
||||
(or backend (cf/get :file-storage-backend "db")))
|
||||
|
||||
(def ^:private schema:metadata
|
||||
[:map {:title "Metadata"}
|
||||
[:storage-ref-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(def decode-metadata-with-schema
|
||||
(sm/decoder schema:metadata sm/json-transformer))
|
||||
|
||||
(defn decode-metadata
|
||||
[metadata]
|
||||
(some-> metadata
|
||||
(db/decode-json-pgobject)
|
||||
(decode-metadata-with-schema)))
|
||||
|
||||
(def ^:private schema:update-params
|
||||
[:map {:closed true}
|
||||
[:id ::sm/uuid]
|
||||
[:type [:enum "main" "snapshot"]]
|
||||
[:file-id ::sm/uuid]
|
||||
[:backend {:optional true} [:enum "db" "storage"]]
|
||||
[:metadata {:optional true} [:maybe schema:metadata]]
|
||||
[:data {:optional true} bytes?]
|
||||
[:created-at {:optional true} ::ct/inst]
|
||||
[:modified-at {:optional true} ::ct/inst]])
|
||||
|
||||
(def ^:private check-update-params
|
||||
(sm/check-fn schema:update-params :hint "invalid params received for update"))
|
||||
|
||||
(defn update!
|
||||
[cfg params & {:keys [throw-if-not-exists?]}]
|
||||
(let [params (-> (check-update-params params)
|
||||
(update :backend default-backend))]
|
||||
|
||||
(some->> (:metadata params) (process-metadata cfg))
|
||||
(let [result (handle-persistence cfg params)
|
||||
result (if throw-if-not-exists?
|
||||
(create-in-database cfg result)
|
||||
(upsert-in-database cfg result))]
|
||||
(-> result db/get-update-count pos?))))
|
||||
|
||||
(defn create!
|
||||
[cfg params]
|
||||
(update! cfg params :throw-on-conflict? true))
|
||||
|
||||
(def ^:private schema:delete-params
|
||||
[:map {:closed true}
|
||||
[:id ::sm/uuid]
|
||||
[:type [:enum "main" "snapshot"]]
|
||||
[:file-id ::sm/uuid]])
|
||||
|
||||
(def check-delete-params
|
||||
(sm/check-fn schema:delete-params :hint "invalid params received for delete"))
|
||||
|
||||
(defn delete!
|
||||
[cfg params]
|
||||
(when-let [fdata (db/get* cfg :file-data
|
||||
(check-delete-params params))]
|
||||
|
||||
(some->> (get fdata :metadata)
|
||||
(decode-metadata)
|
||||
(process-metadata cfg))
|
||||
|
||||
(-> (db/delete! cfg :file-data params)
|
||||
(db/get-update-count)
|
||||
(pos?))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCRIPTS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:get-unmigrated-files
|
||||
"SELECT f.id, f.data, f.created_at, f.modified_at
|
||||
FROM file AS f
|
||||
WHERE f.data IS NOT NULL
|
||||
ORDER BY f.modified_at ASC
|
||||
LIMIT ?
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn migrate-files-to-storage
|
||||
"Migrate the current existing files to store data in new storage
|
||||
tables."
|
||||
[system & {:keys [chunk-size] :or {chunk-size 100}}]
|
||||
(db/tx-run! system
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(reduce (fn [total {:keys [id data index created-at modified-at]}]
|
||||
(l/dbg :hint "migrating file" :file-id (str id))
|
||||
(db/update! conn :file {:data nil} {:id id} ::db/return-keys false)
|
||||
(db/insert! conn :file-data
|
||||
{:backend "db"
|
||||
:metadata nil
|
||||
:type "main"
|
||||
:data data
|
||||
:created-at created-at
|
||||
:modified-at modified-at
|
||||
:file-id id
|
||||
:id id}
|
||||
{::db/return-keys false})
|
||||
(inc total))
|
||||
0
|
||||
(db/plan conn [sql:get-unmigrated-files chunk-size]
|
||||
{:fetch-size 1})))))
|
||||
|
||||
(def ^:private sql:get-migrated-files
|
||||
"SELECT f.id, f.data
|
||||
FROM file_data AS f
|
||||
WHERE f.data IS NOT NULL
|
||||
AND f.id = f.file_id
|
||||
ORDER BY f.id ASC
|
||||
LIMIT ?
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn rollback-files-from-storage
|
||||
"Migrate back to the file table storage."
|
||||
[system & {:keys [chunk-size] :or {chunk-size 100}}]
|
||||
(db/tx-run! system
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(reduce (fn [total {:keys [id data]}]
|
||||
(l/dbg :hint "rollback file" :file-id (str id))
|
||||
(db/update! conn :file {:data data} {:id id} ::db/return-keys false)
|
||||
(db/delete! conn :file-data {:id id} ::db/return-keys false)
|
||||
(inc total))
|
||||
0
|
||||
(db/plan conn [sql:get-migrated-files chunk-size]
|
||||
{:fetch-size 1})))))
|
||||
|
||||
(def ^:private sql:get-unmigrated-snapshots
|
||||
"SELECT fc.id, fc.data, fc.file_id, fc.created_at, fc.updated_at AS modified_at
|
||||
FROM file_change AS fc
|
||||
WHERE fc.data IS NOT NULL
|
||||
AND f.label IS NOT NULL
|
||||
ORDER BY f.id ASC
|
||||
LIMIT ?
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn migrate-snapshots-to-storage
|
||||
"Migrate the current existing files to store data in new storage
|
||||
tables."
|
||||
[system & {:keys [chunk-size] :or {chunk-size 100}}]
|
||||
(db/tx-run! system
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(reduce (fn [total {:keys [id file-id data created-at modified-at]}]
|
||||
(l/dbg :hint "migrating snapshot" :file-id (str file-id) :id (str id))
|
||||
(db/update! conn :file-change {:data nil} {:id id :file-id file-id} ::db/return-keys false)
|
||||
(db/insert! conn :file-data
|
||||
{:backend "db"
|
||||
:metadata nil
|
||||
:type "snapshot"
|
||||
:data data
|
||||
:created-at created-at
|
||||
:modified-at modified-at
|
||||
:file-id file-id
|
||||
:id id}
|
||||
{::db/return-keys false})
|
||||
(inc total))
|
||||
0
|
||||
(db/plan conn [sql:get-unmigrated-snapshots chunk-size]
|
||||
{:fetch-size 1})))))
|
||||
|
||||
(def ^:private sql:get-migrated-snapshots
|
||||
"SELECT f.id, f.data, f.file_id
|
||||
FROM file_data AS f
|
||||
WHERE f.data IS NOT NULL
|
||||
AND f.type = 'snapshot'
|
||||
AND f.id != f.file_id
|
||||
ORDER BY f.id ASC
|
||||
LIMIT ?
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn rollback-snapshots-from-storage
|
||||
"Migrate back to the file table storage."
|
||||
[system & {:keys [chunk-size] :or {chunk-size 100}}]
|
||||
(db/tx-run! system
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(db/exec! conn ["SET statement_timeout = 0"])
|
||||
(db/exec! conn ["SET idle_in_transaction_session_timeout = 0"])
|
||||
|
||||
(reduce (fn [total {:keys [id file-id data]}]
|
||||
(l/dbg :hint "rollback snapshot" :file-id (str id) :id (str id))
|
||||
(db/update! conn :file-change {:data data} {:id id :file-id file-id} ::db/return-keys false)
|
||||
(db/delete! conn :file-data {:id id :file-id file-id} ::db/return-keys false)
|
||||
(inc total))
|
||||
0
|
||||
(db/plan conn [sql:get-migrated-snapshots chunk-size]
|
||||
{:fetch-size 1})))))
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.features.file-snapshots
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as-alias cfeat]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
(def sql:snapshots
|
||||
"SELECT c.id,
|
||||
c.label,
|
||||
c.created_at,
|
||||
c.updated_at AS modified_at,
|
||||
c.deleted_at,
|
||||
c.profile_id,
|
||||
c.created_by,
|
||||
c.locked_by,
|
||||
c.revn,
|
||||
c.features,
|
||||
c.migrations,
|
||||
c.version,
|
||||
c.file_id,
|
||||
c.data AS legacy_data,
|
||||
fd.data AS data,
|
||||
coalesce(fd.backend, 'db') AS backend,
|
||||
fd.metadata AS metadata
|
||||
FROM file_change AS c
|
||||
LEFT JOIN file_data AS fd ON (fd.file_id = c.file_id
|
||||
AND fd.id = c.id
|
||||
AND fd.type = 'snapshot')
|
||||
WHERE c.label IS NOT NULL")
|
||||
|
||||
(def ^:private sql:get-snapshot
|
||||
(str sql:snapshots " AND c.file_id = ? AND c.id = ?"))
|
||||
|
||||
(def ^:private sql:get-snapshots
|
||||
(str sql:snapshots " AND c.file_id = ?"))
|
||||
|
||||
(def ^:private sql:get-snapshot-without-data
|
||||
(str "WITH snapshots AS (" sql:snapshots ")"
|
||||
"SELECT c.id,
|
||||
c.label,
|
||||
c.revn,
|
||||
c.created_at,
|
||||
c.modified_at,
|
||||
c.deleted_at,
|
||||
c.profile_id,
|
||||
c.created_by,
|
||||
c.features,
|
||||
c.metadata,
|
||||
c.migrations,
|
||||
c.version,
|
||||
c.file_id
|
||||
FROM snapshots AS c
|
||||
WHERE c.id = ?"))
|
||||
|
||||
(defn- decode-snapshot
|
||||
[snapshot]
|
||||
(some-> snapshot (-> (d/update-when :metadata fdata/decode-metadata)
|
||||
(d/update-when :migrations db/decode-pgarray [])
|
||||
(d/update-when :features db/decode-pgarray #{}))))
|
||||
|
||||
(def sql:get-minimal-file
|
||||
"SELECT f.id,
|
||||
f.revn,
|
||||
f.modified_at,
|
||||
f.deleted_at,
|
||||
fd.backend AS backend,
|
||||
fd.metadata AS metadata
|
||||
FROM file AS f
|
||||
LEFT JOIN file_data AS fd ON (fd.file_id = f.id AND fd.id = f.id)
|
||||
WHERE f.id = ?")
|
||||
|
||||
(defn get-minimal-file
|
||||
[cfg id & {:as opts}]
|
||||
(-> (db/get-with-sql cfg [sql:get-minimal-file id] opts)
|
||||
(d/update-when :metadata fdata/decode-metadata)))
|
||||
|
||||
(defn get-minimal-snapshot
|
||||
[cfg snapshot-id]
|
||||
(-> (db/get-with-sql cfg [sql:get-snapshot-without-data snapshot-id])
|
||||
(decode-snapshot)))
|
||||
|
||||
(defn get-snapshot
|
||||
"Get snapshot with decoded data"
|
||||
[cfg file-id snapshot-id]
|
||||
(->> (db/get-with-sql cfg [sql:get-snapshot file-id snapshot-id])
|
||||
(decode-snapshot)
|
||||
(fdata/resolve-file-data cfg)
|
||||
(fdata/decode-file-data cfg)))
|
||||
|
||||
(def ^:private sql:get-visible-snapshots
|
||||
(str "WITH "
|
||||
"snapshots1 AS ( " sql:snapshots "),"
|
||||
"snapshots2 AS (
|
||||
SELECT c.id,
|
||||
c.label,
|
||||
c.version,
|
||||
c.created_at,
|
||||
c.modified_at,
|
||||
c.created_by,
|
||||
c.locked_by,
|
||||
c.profile_id
|
||||
FROM snapshots1 AS c
|
||||
WHERE c.file_id = ?
|
||||
AND (c.deleted_at IS NULL OR deleted_at > now())
|
||||
), snapshots3 AS (
|
||||
(SELECT * FROM snapshots2 WHERE created_by = 'system' LIMIT 1000)
|
||||
UNION ALL
|
||||
(SELECT * FROM snapshots2 WHERE created_by != 'system' LIMIT 1000)
|
||||
)
|
||||
SELECT * FROM snapshots3
|
||||
ORDER BY created_at DESC;"))
|
||||
|
||||
(defn get-visible-snapshots
|
||||
"Return a list of snapshots fecheable from the API, it has a limited
|
||||
set of fields and applies big but safe limits over all available
|
||||
snapshots. It return a ordered vector by the snapshot date of
|
||||
creation."
|
||||
[cfg file-id]
|
||||
(->> (db/exec! cfg [sql:get-visible-snapshots file-id])
|
||||
(mapv decode-snapshot)))
|
||||
|
||||
(def ^:private schema:decoded-file
|
||||
[:map {:title "DecodedFile"}
|
||||
[:id ::sm/uuid]
|
||||
[:revn :int]
|
||||
[:vern :int]
|
||||
[:data :map]
|
||||
[:version :int]
|
||||
[:features ::cfeat/features]
|
||||
[:migrations [::sm/set :string]]])
|
||||
|
||||
(def ^:private schema:snapshot
|
||||
[:map {:title "Snapshot"}
|
||||
[:id ::sm/uuid]
|
||||
[:revn [::sm/int {:min 0}]]
|
||||
[:version [::sm/int {:min 0}]]
|
||||
[:features ::cfeat/features]
|
||||
[:migrations [::sm/set ::sm/text]]
|
||||
[:profile-id {:optional true} ::sm/uuid]
|
||||
[:label ::sm/text]
|
||||
[:file-id ::sm/uuid]
|
||||
[:created-by [:enum "system" "user" "admin"]]
|
||||
[:deleted-at {:optional true} ::ct/inst]
|
||||
[:modified-at ::ct/inst]
|
||||
[:created-at ::ct/inst]])
|
||||
|
||||
(def ^:private schema:snapshot-params
|
||||
[:map {:title "SnapshotParams"}
|
||||
[:id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:label ::sm/text]
|
||||
[:modified-at {:optional true} ::ct/inst]])
|
||||
|
||||
(def ^:private check-snapshot
|
||||
(sm/check-fn schema:snapshot))
|
||||
|
||||
(def ^:private check-snapshot-params
|
||||
(sm/check-fn schema:snapshot-params))
|
||||
|
||||
(def ^:private check-decoded-file
|
||||
(sm/check-fn schema:decoded-file))
|
||||
|
||||
(defn- generate-snapshot-label
|
||||
[]
|
||||
(let [ts (-> (ct/now)
|
||||
(ct/format-inst)
|
||||
(str/replace #"[T:\.]" "-")
|
||||
(str/rtrim "Z"))]
|
||||
(str "snapshot-" ts)))
|
||||
|
||||
(defn create!
|
||||
"Create a file snapshot; expects a non-encoded file."
|
||||
[cfg file & {:keys [label created-by deleted-at profile-id session-id]
|
||||
:or {deleted-at :default
|
||||
created-by "system"}}]
|
||||
|
||||
(let [file (check-decoded-file file)
|
||||
|
||||
|
||||
snapshot-id (uuid/next)
|
||||
created-at (ct/now)
|
||||
deleted-at (cond
|
||||
(= deleted-at :default)
|
||||
(ct/plus (ct/now) (cf/get-deletion-delay))
|
||||
|
||||
(ct/inst? deleted-at)
|
||||
deleted-at
|
||||
|
||||
:else
|
||||
nil)
|
||||
|
||||
label (or label (generate-snapshot-label))
|
||||
data (px/invoke! (::wrk/executor cfg) #(blob/encode (:data file)))
|
||||
features (:features file)
|
||||
migrations (:migrations file)
|
||||
|
||||
snapshot {:id snapshot-id
|
||||
:revn (:revn file)
|
||||
:version (:version file)
|
||||
:file-id (:id file)
|
||||
:features features
|
||||
:migrations migrations
|
||||
:label label
|
||||
:created-at created-at
|
||||
:modified-at created-at
|
||||
:created-by created-by}
|
||||
|
||||
snapshot (cond-> snapshot
|
||||
deleted-at
|
||||
(assoc :deleted-at deleted-at)
|
||||
|
||||
:always
|
||||
(check-snapshot))]
|
||||
|
||||
(db/insert! cfg :file-change
|
||||
(-> snapshot
|
||||
(update :features into-array)
|
||||
(update :migrations into-array)
|
||||
(assoc :updated-at created-at)
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :session-id session-id)
|
||||
(dissoc :modified-at))
|
||||
{::db/return-keys false})
|
||||
|
||||
(fdata/create! cfg
|
||||
{:id snapshot-id
|
||||
:file-id (:id file)
|
||||
:type "snapshot"
|
||||
:data data
|
||||
:created-at created-at
|
||||
:modified-at created-at})
|
||||
|
||||
snapshot))
|
||||
|
||||
(defn update!
|
||||
[cfg params]
|
||||
|
||||
(let [{:keys [id file-id label modified-at]}
|
||||
(check-snapshot-params params)
|
||||
|
||||
modified-at
|
||||
(or modified-at (ct/now))]
|
||||
|
||||
(-> (db/update! cfg :file-change
|
||||
{:label label
|
||||
:created-by "user"
|
||||
:updated-at modified-at
|
||||
:deleted-at nil}
|
||||
{:file-id file-id
|
||||
:id id}
|
||||
{::db/return-keys false})
|
||||
(db/get-update-count)
|
||||
(pos?))))
|
||||
|
||||
(defn restore!
|
||||
[{:keys [::db/conn] :as cfg} file-id snapshot-id]
|
||||
(let [file (get-minimal-file conn file-id {::db/for-update true})
|
||||
vern (rand-int Integer/MAX_VALUE)
|
||||
|
||||
storage
|
||||
(sto/resolve cfg {::db/reuse-conn true})
|
||||
|
||||
snapshot
|
||||
(get-snapshot cfg file-id snapshot-id)]
|
||||
|
||||
(when-not snapshot
|
||||
(ex/raise :type :not-found
|
||||
:code :snapshot-not-found
|
||||
:hint "unable to find snapshot with the provided label"
|
||||
:snapshot-id snapshot-id
|
||||
:file-id file-id))
|
||||
|
||||
(when-not (:data snapshot)
|
||||
(ex/raise :type :internal
|
||||
:code :snapshot-without-data
|
||||
:hint "snapshot has no data"
|
||||
:label (:label snapshot)
|
||||
:file-id file-id))
|
||||
|
||||
(let [;; If the snapshot has applied migrations stored, we reuse
|
||||
;; them, if not, we take a safest set of migrations as
|
||||
;; starting point. This is because, at the time of
|
||||
;; implementing snapshots, migrations were not taken into
|
||||
;; account so we need to make this backward compatible in
|
||||
;; some way.
|
||||
migrations
|
||||
(or (:migrations snapshot)
|
||||
(fmg/generate-migrations-from-version 67))
|
||||
|
||||
file
|
||||
(-> file
|
||||
(update :revn inc)
|
||||
(assoc :migrations migrations)
|
||||
(assoc :data (:data snapshot))
|
||||
(assoc :vern vern)
|
||||
(assoc :version (:version snapshot))
|
||||
(assoc :has-media-trimmed false)
|
||||
(assoc :modified-at (:modified-at snapshot))
|
||||
(assoc :features (:features snapshot)))]
|
||||
|
||||
(l/dbg :hint "restoring snapshot"
|
||||
:file-id (str file-id)
|
||||
:label (:label snapshot)
|
||||
:snapshot-id (str (:id snapshot)))
|
||||
|
||||
;; In the same way, on reseting the file data, we need to restore
|
||||
;; the applied migrations on the moment of taking the snapshot
|
||||
(bfc/update-file! cfg file ::bfc/reset-migrations true)
|
||||
|
||||
;; FIXME: this should be separated functions, we should not have
|
||||
;; inline sql here.
|
||||
|
||||
;; clean object thumbnails
|
||||
(let [sql (str "update file_tagged_object_thumbnail "
|
||||
" set deleted_at = now() "
|
||||
" where file_id=? returning media_id")
|
||||
res (db/exec! conn [sql file-id])]
|
||||
(doseq [media-id (into #{} (keep :media-id) res)]
|
||||
(sto/touch-object! storage media-id)))
|
||||
|
||||
;; clean file thumbnails
|
||||
(let [sql (str "update file_thumbnail "
|
||||
" set deleted_at = now() "
|
||||
" where file_id=? returning media_id")
|
||||
res (db/exec! conn [sql file-id])]
|
||||
(doseq [media-id (into #{} (keep :media-id) res)]
|
||||
(sto/touch-object! storage media-id)))
|
||||
|
||||
vern)))
|
||||
|
||||
(defn delete!
|
||||
[cfg {:keys [id file-id]}]
|
||||
(let [deleted-at (ct/now)]
|
||||
(db/update! cfg :file-change
|
||||
{:deleted-at deleted-at}
|
||||
{:id id :file-id file-id}
|
||||
{::db/return-keys false})
|
||||
true))
|
||||
|
||||
|
||||
(defn reduce-snapshots
|
||||
"Process the file snapshots using efficient reduction."
|
||||
[cfg file-id xform f init]
|
||||
(let [conn (db/get-connection cfg)
|
||||
xform (comp
|
||||
(map (partial fdata/resolve-file-data cfg))
|
||||
(map (partial fdata/decode-file-data cfg))
|
||||
xform)]
|
||||
|
||||
(->> (db/plan conn [sql:get-snapshots file-id] {:fetch-size 1})
|
||||
(transduce xform f init))))
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
[app.http.awsns :as-alias awsns]
|
||||
[app.http.debug :as-alias debug]
|
||||
[app.http.errors :as errors]
|
||||
[app.http.management :as mgmt]
|
||||
[app.http.middleware :as mw]
|
||||
[app.http.session :as session]
|
||||
[app.http.websocket :as-alias ws]
|
||||
@@ -143,6 +144,7 @@
|
||||
[::debug/routes schema:routes]
|
||||
[::mtx/routes schema:routes]
|
||||
[::awsns/routes schema:routes]
|
||||
[::mgmt/routes schema:routes]
|
||||
::session/manager
|
||||
::setup/props
|
||||
::db/pool])
|
||||
@@ -170,6 +172,9 @@
|
||||
["/webhooks"
|
||||
(::awsns/routes cfg)]
|
||||
|
||||
["/management"
|
||||
(::mgmt/routes cfg)]
|
||||
|
||||
(::ws/routes cfg)
|
||||
|
||||
["/api" {:middleware [[mw/cors]]}
|
||||
|
||||
234
backend/src/app/http/management.clj
Normal file
234
backend/src/app/http/management.clj
Normal file
@@ -0,0 +1,234 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.http.management
|
||||
"Internal mangement HTTP API"
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc.commands.profile :as cmd.profile]
|
||||
[app.setup :as-alias setup]
|
||||
[app.tokens :as tokens]
|
||||
[app.worker :as-alias wrk]
|
||||
[integrant.core :as ig]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
;; ---- ROUTES
|
||||
|
||||
(declare ^:private authenticate)
|
||||
(declare ^:private get-customer)
|
||||
(declare ^:private update-customer)
|
||||
|
||||
(defmethod ig/assert-key ::routes
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expect valid database pool"))
|
||||
|
||||
(def ^:private default-system
|
||||
{:name ::default-system
|
||||
:compile
|
||||
(fn [_ _]
|
||||
(fn [handler cfg]
|
||||
(fn [request]
|
||||
(handler cfg request))))})
|
||||
|
||||
(def ^:private transaction
|
||||
{:name ::transaction
|
||||
:compile
|
||||
(fn [data _]
|
||||
(when (:transaction data)
|
||||
(fn [handler]
|
||||
(fn [cfg request]
|
||||
(db/tx-run! cfg handler request)))))})
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
["" {:middleware [[default-system cfg]
|
||||
[transaction]]}
|
||||
["/authenticate"
|
||||
{:handler authenticate
|
||||
:allowed-methods #{:post}}]
|
||||
|
||||
["/get-customer"
|
||||
{:handler get-customer
|
||||
:transaction true
|
||||
:allowed-methods #{:post}}]
|
||||
|
||||
["/update-customer"
|
||||
{:handler update-customer
|
||||
:allowed-methods #{:post}
|
||||
:transaction true}]])
|
||||
|
||||
;; ---- HELPERS
|
||||
|
||||
(defn- coercer
|
||||
[schema & {:as opts}]
|
||||
(let [decode-fn (sm/decoder schema sm/json-transformer)
|
||||
check-fn (sm/check-fn schema opts)]
|
||||
(fn [data]
|
||||
(-> data decode-fn check-fn))))
|
||||
|
||||
;; ---- API: AUTHENTICATE
|
||||
|
||||
(defn- authenticate
|
||||
[cfg request]
|
||||
(let [token (-> request :params :token)
|
||||
props (get cfg ::setup/props)
|
||||
result (tokens/verify props {:token token :iss "authentication"})]
|
||||
{::yres/status 200
|
||||
::yres/body result}))
|
||||
|
||||
;; ---- API: GET-CUSTOMER
|
||||
|
||||
(def ^:private schema:get-customer
|
||||
[:map [:id ::sm/uuid]])
|
||||
|
||||
(def ^:private coerce-get-customer-params
|
||||
(coercer schema:get-customer
|
||||
:type :validation
|
||||
:hint "invalid data provided for `get-customer` rpc call"))
|
||||
|
||||
(def ^:private sql:get-customer-slots
|
||||
"WITH teams AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
tpr.profile_id AS profile_id
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.is_owner IS true
|
||||
AND tpr.profile_id = ?
|
||||
), teams_with_slots AS (
|
||||
SELECT tpr.team_id AS id,
|
||||
count(*) AS total
|
||||
FROM team_profile_rel AS tpr
|
||||
WHERE tpr.team_id IN (SELECT id FROM teams)
|
||||
AND tpr.can_edit IS true
|
||||
GROUP BY 1
|
||||
ORDER BY 2
|
||||
)
|
||||
SELECT max(total) AS total FROM teams_with_slots;")
|
||||
|
||||
(defn- get-customer-slots
|
||||
[cfg profile-id]
|
||||
(let [result (db/exec-one! cfg [sql:get-customer-slots profile-id])]
|
||||
(:total result)))
|
||||
|
||||
(defn- get-customer
|
||||
[cfg request]
|
||||
(let [profile-id (-> request :params coerce-get-customer-params :id)
|
||||
profile (cmd.profile/get-profile cfg profile-id)
|
||||
result {:id (get profile :id)
|
||||
:name (get profile :fullname)
|
||||
:email (get profile :email)
|
||||
:num-editors (get-customer-slots cfg profile-id)
|
||||
:subscription (-> profile :props :subscription)}]
|
||||
{::yres/status 200
|
||||
::yres/body result}))
|
||||
|
||||
|
||||
;; ---- API: UPDATE-CUSTOMER
|
||||
|
||||
(def ^:private schema:timestamp
|
||||
(sm/type-schema
|
||||
{:type ::timestamp
|
||||
:pred ct/inst?
|
||||
:type-properties
|
||||
{:title "inst"
|
||||
:description "The same as :app.common.time/inst but encodes to epoch"
|
||||
:error/message "should be an instant"
|
||||
:gen/gen (->> (sg/small-int)
|
||||
(sg/fmap (fn [v] (ct/inst v))))
|
||||
:decode/string ct/inst
|
||||
:encode/string inst-ms
|
||||
:decode/json ct/inst
|
||||
:encode/json inst-ms}}))
|
||||
|
||||
(def ^:private schema:subscription
|
||||
[:map {:title "Subscription"}
|
||||
[:id ::sm/text]
|
||||
[:customer-id ::sm/text]
|
||||
[:type [:enum
|
||||
"unlimited"
|
||||
"professional"
|
||||
"enterprise"]]
|
||||
[:status [:enum
|
||||
"active"
|
||||
"canceled"
|
||||
"incomplete"
|
||||
"incomplete_expired"
|
||||
"past_due"
|
||||
"paused"
|
||||
"trialing"
|
||||
"unpaid"]]
|
||||
|
||||
[:billing-period [:enum
|
||||
"month"
|
||||
"day"
|
||||
"week"
|
||||
"year"]]
|
||||
[:quantity :int]
|
||||
[:description [:maybe ::sm/text]]
|
||||
[:created-at schema:timestamp]
|
||||
[:start-date [:maybe schema:timestamp]]
|
||||
[:ended-at [:maybe schema:timestamp]]
|
||||
[:trial-end [:maybe schema:timestamp]]
|
||||
[:trial-start [:maybe schema:timestamp]]
|
||||
[:cancel-at [:maybe schema:timestamp]]
|
||||
[:canceled-at [:maybe schema:timestamp]]
|
||||
[:current-period-end [:maybe schema:timestamp]]
|
||||
[:current-period-start [:maybe schema:timestamp]]
|
||||
[:cancel-at-period-end :boolean]
|
||||
|
||||
[:cancellation-details
|
||||
[:map {:title "CancellationDetails"}
|
||||
[:comment [:maybe ::sm/text]]
|
||||
[:reason [:maybe ::sm/text]]
|
||||
[:feedback [:maybe
|
||||
[:enum
|
||||
"customer_service"
|
||||
"low_quality"
|
||||
"missing_feature"
|
||||
"other"
|
||||
"switched_service"
|
||||
"too_complex"
|
||||
"too_expensive"
|
||||
"unused"]]]]]])
|
||||
|
||||
(def ^:private schema:update-customer
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:subscription [:maybe schema:subscription]]])
|
||||
|
||||
(def ^:private coerce-update-customer-params
|
||||
(coercer schema:update-customer
|
||||
:type :validation
|
||||
:hint "invalid data provided for `update-customer` rpc call"))
|
||||
|
||||
(defn- update-customer
|
||||
[cfg request]
|
||||
(let [{:keys [id subscription]}
|
||||
(-> request :params coerce-update-customer-params)
|
||||
|
||||
{:keys [props] :as profile}
|
||||
(cmd.profile/get-profile cfg id ::db/for-update true)
|
||||
|
||||
props
|
||||
(assoc props :subscription subscription)]
|
||||
|
||||
(l/dbg :hint "update customer"
|
||||
:profile-id (str id)
|
||||
:subscription-type (get subscription :type)
|
||||
:subscription-status (get subscription :status)
|
||||
:subscription-quantity (get subscription :quantity))
|
||||
|
||||
(db/update! cfg :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
|
||||
{::yres/status 201
|
||||
::yres/body nil}))
|
||||
@@ -33,7 +33,7 @@
|
||||
(println "event:" (d/name name))
|
||||
(println "data:" (t/encode-str data {:type :json-verbose}))
|
||||
(println))]
|
||||
(.getBytes data "UTF-8"))
|
||||
(.getBytes ^String data "UTF-8"))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "unexpected error on encoding value on sse stream"
|
||||
:cause cause)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
[app.http.awsns :as http.awsns]
|
||||
[app.http.client :as-alias http.client]
|
||||
[app.http.debug :as-alias http.debug]
|
||||
[app.http.management :as mgmt]
|
||||
[app.http.session :as-alias session]
|
||||
[app.http.session.tasks :as-alias session.tasks]
|
||||
[app.http.websocket :as http.ws]
|
||||
@@ -273,6 +274,10 @@
|
||||
::email/blacklist (ig/ref ::email/blacklist)
|
||||
::email/whitelist (ig/ref ::email/whitelist)}
|
||||
|
||||
::mgmt/routes
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::setup/props (ig/ref ::setup/props)}
|
||||
|
||||
:app.http/router
|
||||
{::session/manager (ig/ref ::session/manager)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
@@ -281,6 +286,7 @@
|
||||
::setup/props (ig/ref ::setup/props)
|
||||
::mtx/routes (ig/ref ::mtx/routes)
|
||||
::oidc/routes (ig/ref ::oidc/routes)
|
||||
::mgmt/routes (ig/ref ::mgmt/routes)
|
||||
::http.debug/routes (ig/ref ::http.debug/routes)
|
||||
::http.assets/routes (ig/ref ::http.assets/routes)
|
||||
::http.ws/routes (ig/ref ::http.ws/routes)
|
||||
|
||||
@@ -38,15 +38,13 @@
|
||||
org.im4java.core.Info))
|
||||
|
||||
(def schema:upload
|
||||
(sm/register!
|
||||
^{::sm/type ::upload}
|
||||
[:map {:title "Upload"}
|
||||
[:filename :string]
|
||||
[:size ::sm/int]
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} :string]
|
||||
[:headers {:optional true}
|
||||
[:map-of :string :string]]]))
|
||||
[:map {:title "Upload"}
|
||||
[:filename :string]
|
||||
[:size ::sm/int]
|
||||
[:path ::fs/path]
|
||||
[:mtype {:optional true} :string]
|
||||
[:headers {:optional true}
|
||||
[:map-of :string :string]]])
|
||||
|
||||
(def ^:private schema:input
|
||||
[:map {:title "Input"}
|
||||
@@ -118,7 +116,7 @@
|
||||
(defn- parse-svg
|
||||
[text]
|
||||
(let [text (strip-doctype text)]
|
||||
(dm/with-open [istream (IOUtils/toInputStream text "UTF-8")]
|
||||
(dm/with-open [istream (IOUtils/toInputStream ^String text "UTF-8")]
|
||||
(xml/parse istream secure-parser-factory))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -446,8 +446,8 @@
|
||||
{:name "0140-add-locked-by-column-to-file-change-table"
|
||||
:fn (mg/resource "app/migrations/sql/0140-add-locked-by-column-to-file-change-table.sql")}
|
||||
|
||||
{:name "0141-add-file-data-table.sql"
|
||||
:fn (mg/resource "app/migrations/sql/0141-add-file-data-table.sql")}])
|
||||
{:name "0141-add-idx-to-file-library-rel"
|
||||
:fn (mg/resource "app/migrations/sql/0141-add-idx-to-file-library-rel.sql")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
CREATE TABLE file_data (
|
||||
file_id uuid NOT NULL REFERENCES file(id) DEFERRABLE,
|
||||
id uuid NOT NULL,
|
||||
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
modified_at timestamptz NOT NULL DEFAULT now(),
|
||||
|
||||
type text NULL,
|
||||
backend text NULL,
|
||||
|
||||
metadata jsonb NULL,
|
||||
data bytea NULL,
|
||||
|
||||
PRIMARY KEY (file_id, id)
|
||||
|
||||
) PARTITION BY HASH (file_id, id);
|
||||
|
||||
CREATE TABLE file_data_00 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 0);
|
||||
CREATE TABLE file_data_01 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 1);
|
||||
CREATE TABLE file_data_02 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 2);
|
||||
CREATE TABLE file_data_03 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 3);
|
||||
CREATE TABLE file_data_04 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 4);
|
||||
CREATE TABLE file_data_05 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 5);
|
||||
CREATE TABLE file_data_06 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 6);
|
||||
CREATE TABLE file_data_07 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 7);
|
||||
CREATE TABLE file_data_08 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 8);
|
||||
CREATE TABLE file_data_09 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 9);
|
||||
CREATE TABLE file_data_10 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 10);
|
||||
CREATE TABLE file_data_11 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 11);
|
||||
CREATE TABLE file_data_12 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 12);
|
||||
CREATE TABLE file_data_13 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 13);
|
||||
CREATE TABLE file_data_14 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 14);
|
||||
CREATE TABLE file_data_15 PARTITION OF file_data FOR VALUES WITH (MODULUS 16, REMAINDER 15);
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX IF NOT EXISTS file_library_rel__library_file_id__idx
|
||||
ON file_library_rel (library_file_id);
|
||||
@@ -239,6 +239,7 @@
|
||||
'app.rpc.commands.files
|
||||
'app.rpc.commands.files-create
|
||||
'app.rpc.commands.files-share
|
||||
'app.rpc.commands.files-temp
|
||||
'app.rpc.commands.files-update
|
||||
'app.rpc.commands.files-snapshot
|
||||
'app.rpc.commands.files-thumbnails
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
[:project-id ::sm/uuid]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:version {:optional true} ::sm/int]
|
||||
[:file ::media/upload]])
|
||||
[:file media/schema:upload]])
|
||||
|
||||
(sv/defmethod ::import-binfile
|
||||
"Import a penpot file in a binary format. If `file-id` is provided,
|
||||
|
||||
@@ -184,8 +184,8 @@
|
||||
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
||||
(let [file (->> file
|
||||
(feat.fdata/resolve-file-data cfg)
|
||||
(feat.fdata/decode-file-data cfg))
|
||||
(files/decode-row)
|
||||
(feat.fdata/resolve-file-data cfg))
|
||||
data (get file :data)]
|
||||
(-> file
|
||||
(assoc :page-name (dm/get-in data [:pages-index page-id :name]))
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.features.file-migrations :as feat.fmigr]
|
||||
[app.features.logical-deletion :as ldel]
|
||||
[app.loggers.audit :as-alias audit]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
@@ -38,7 +39,8 @@
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[app.worker :as wrk]
|
||||
[cuerdas.core :as str]))
|
||||
[cuerdas.core :as str]
|
||||
[promesa.exec :as px]))
|
||||
|
||||
;; --- FEATURES
|
||||
|
||||
@@ -53,10 +55,12 @@
|
||||
(ct/duration {:days 7}))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [features] :as row}]
|
||||
[{:keys [data changes features] :as row}]
|
||||
(when row
|
||||
(cond-> row
|
||||
(db/pgarray? features) (assoc :features (db/decode-pgarray features #{})))))
|
||||
features (assoc :features (db/decode-pgarray features #{}))
|
||||
changes (assoc :changes (blob/decode changes))
|
||||
data (assoc :data (blob/decode data)))))
|
||||
|
||||
(defn check-version!
|
||||
[file]
|
||||
@@ -74,6 +78,7 @@
|
||||
|
||||
;; --- FILE PERMISSIONS
|
||||
|
||||
|
||||
(def ^:private sql:file-permissions
|
||||
"select fpr.is_owner,
|
||||
fpr.is_admin,
|
||||
@@ -191,7 +196,7 @@
|
||||
|
||||
(def schema:permissions-mixin
|
||||
[:map {:title "PermissionsMixin"}
|
||||
[:permissions ::perms/permissions]])
|
||||
[:permissions perms/schema:permissions]])
|
||||
|
||||
(def schema:file-with-permissions
|
||||
[:merge {:title "FileWithPermissions"}
|
||||
@@ -205,9 +210,90 @@
|
||||
[:id ::sm/uuid]
|
||||
[:project-id {:optional true} ::sm/uuid]])
|
||||
|
||||
(defn- migrate-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file} {:keys [read-only?]}]
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)
|
||||
pmap/*tracked* (pmap/create-tracked)]
|
||||
(let [libs (delay (bfc/get-resolved-file-libraries cfg file))
|
||||
;; For avoid unnecesary overhead of creating multiple pointers and
|
||||
;; handly internally with objects map in their worst case (when
|
||||
;; probably all shapes and all pointers will be readed in any
|
||||
;; case), we just realize/resolve them before applying the
|
||||
;; migration to the file
|
||||
file (-> file
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file libs))]
|
||||
|
||||
(if (or read-only? (db/read-only? conn))
|
||||
file
|
||||
(let [;; When file is migrated, we break the rule of no perform
|
||||
;; mutations on get operations and update the file with all
|
||||
;; migrations applied
|
||||
file (if (contains? (:features file) "fdata/objects-map")
|
||||
(feat.fdata/enable-objects-map file)
|
||||
file)
|
||||
file (if (contains? (:features file) "fdata/pointer-map")
|
||||
(feat.fdata/enable-pointer-map file)
|
||||
file)]
|
||||
|
||||
(db/update! conn :file
|
||||
{:data (blob/encode (:data file))
|
||||
:version (:version file)
|
||||
:features (db/create-array conn "text" (:features file))}
|
||||
{:id id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(when (contains? (:features file) "fdata/pointer-map")
|
||||
(feat.fdata/persist-pointers! cfg id))
|
||||
|
||||
(feat.fmigr/upsert-migrations! conn file)
|
||||
(feat.fmigr/resolve-applied-migrations cfg file))))))
|
||||
|
||||
(defn get-file
|
||||
[{:keys [::db/conn ::wrk/executor] :as cfg} id
|
||||
& {:keys [project-id
|
||||
migrate?
|
||||
include-deleted?
|
||||
lock-for-update?
|
||||
preload-pointers?]
|
||||
:or {include-deleted? false
|
||||
lock-for-update? false
|
||||
migrate? true
|
||||
preload-pointers? false}
|
||||
:as options}]
|
||||
|
||||
(assert (db/connection? conn) "expected cfg with valid connection")
|
||||
|
||||
(let [params (merge {:id id}
|
||||
(when (some? project-id)
|
||||
{:project-id project-id}))
|
||||
file (->> (db/get conn :file params
|
||||
{::db/check-deleted (not include-deleted?)
|
||||
::db/remove-deleted (not include-deleted?)
|
||||
::sql/for-update lock-for-update?})
|
||||
(feat.fmigr/resolve-applied-migrations cfg)
|
||||
(feat.fdata/resolve-file-data cfg))
|
||||
|
||||
;; NOTE: we perform the file decoding in a separate thread
|
||||
;; because it has heavy and synchronous operations for
|
||||
;; decoding file body that are not very friendly with virtual
|
||||
;; threads.
|
||||
file (px/invoke! executor #(decode-row file))
|
||||
|
||||
file (if (and migrate? (fmg/need-migration? file))
|
||||
(migrate-file cfg file options)
|
||||
file)]
|
||||
|
||||
(if preload-pointers?
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(update file :data feat.fdata/process-pointers deref))
|
||||
|
||||
file)))
|
||||
|
||||
(defn get-minimal-file
|
||||
[cfg id & {:as opts}]
|
||||
(let [opts (assoc opts ::sql/columns [:id :modified-at :deleted-at :revn :vern])]
|
||||
(let [opts (assoc opts ::sql/columns [:id :modified-at :deleted-at :revn :vern :data-ref-id :data-backend])]
|
||||
(db/get cfg :file {:id id} opts)))
|
||||
|
||||
(defn- get-minimal-file-with-perms
|
||||
@@ -247,9 +333,9 @@
|
||||
:project-id project-id
|
||||
:file-id id)
|
||||
|
||||
file (-> (bfc/get-file cfg id
|
||||
:project-id project-id)
|
||||
file (-> (get-file cfg id :project-id project-id)
|
||||
(assoc :permissions perms)
|
||||
(assoc :team-id (:id team))
|
||||
(check-version!))]
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
@@ -261,7 +347,8 @@
|
||||
;; pointers on backend and return a complete file.
|
||||
(if (and (contains? (:features file) "fdata/pointer-map")
|
||||
(not (contains? (:features params) "fdata/pointer-map")))
|
||||
(feat.fdata/realize-pointers cfg file)
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(update file :data feat.fdata/process-pointers deref))
|
||||
file))))
|
||||
|
||||
;; --- COMMAND QUERY: get-file-fragment (by id)
|
||||
@@ -271,7 +358,7 @@
|
||||
[:id ::sm/uuid]
|
||||
[:file-id ::sm/uuid]
|
||||
[:created-at ::ct/inst]
|
||||
[:data any?]])
|
||||
[:content ::sm/any]])
|
||||
|
||||
(def schema:get-file-fragment
|
||||
[:map {:title "get-file-fragment"}
|
||||
@@ -281,8 +368,10 @@
|
||||
|
||||
(defn- get-file-fragment
|
||||
[cfg file-id fragment-id]
|
||||
(some-> (db/get cfg :file-data {:file-id file-id :id fragment-id :type "fragment"})
|
||||
(update :data blob/decode)))
|
||||
(let [resolve-file-data (partial feat.fdata/resolve-file-data cfg)]
|
||||
(some-> (db/get cfg :file-data-fragment {:file-id file-id :id fragment-id})
|
||||
(resolve-file-data)
|
||||
(update :data blob/decode))))
|
||||
|
||||
(sv/defmethod ::get-file-fragment
|
||||
"Retrieve a file fragment by its ID. Only authenticated users."
|
||||
@@ -372,8 +461,42 @@
|
||||
(:has-libraries row)))
|
||||
|
||||
|
||||
;; --- COMMAND QUERY: get-library-usage
|
||||
|
||||
|
||||
(declare get-library-usage)
|
||||
|
||||
(def schema:get-library-usage
|
||||
[:map {:title "get-library-usage"}
|
||||
[:file-id ::sm/uuid]])
|
||||
:sample
|
||||
(sv/defmethod ::get-library-usage
|
||||
"Gets the number of files that use the specified library."
|
||||
{::doc/added "2.10.0"
|
||||
::sm/params schema:get-library-usage
|
||||
::sm/result ::sm/int}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(check-read-permissions! pool profile-id file-id)
|
||||
(get-library-usage conn file-id)))
|
||||
|
||||
(def ^:private sql:get-library-usage
|
||||
"SELECT COUNT(*) AS used
|
||||
FROM file_library_rel AS flr
|
||||
JOIN file AS fl ON (flr.library_file_id = fl.id)
|
||||
WHERE flr.library_file_id = ?::uuid
|
||||
AND (fl.deleted_at IS NULL OR
|
||||
fl.deleted_at > now())")
|
||||
|
||||
(defn- get-library-usage
|
||||
[conn file-id]
|
||||
(let [row (db/exec-one! conn [sql:get-library-usage file-id])]
|
||||
{:used-in (:used row)}))
|
||||
|
||||
|
||||
;; --- QUERY COMMAND: get-page
|
||||
|
||||
|
||||
(defn- prune-objects
|
||||
"Given the page data and the object-id returns the page data with all
|
||||
other not needed objects removed from the `:objects` data
|
||||
@@ -407,7 +530,7 @@
|
||||
|
||||
(let [perms (get-permissions conn profile-id file-id share-id)
|
||||
|
||||
file (bfc/get-file cfg file-id :read-only? true)
|
||||
file (get-file cfg file-id :read-only? true)
|
||||
|
||||
proj (db/get conn :project {:id (:project-id file)})
|
||||
|
||||
@@ -463,6 +586,24 @@
|
||||
|
||||
;; --- COMMAND QUERY: get-team-shared-files
|
||||
|
||||
(defn- components-and-variants
|
||||
"Return a set with all the variant-ids, and a list of components, but with
|
||||
only one component by variant"
|
||||
[components]
|
||||
(let [{:keys [variant-ids components]}
|
||||
(reduce (fn [{:keys [variant-ids components] :as acc} {:keys [variant-id] :as component}]
|
||||
(cond
|
||||
(nil? variant-id)
|
||||
{:variant-ids variant-ids :components (conj components component)}
|
||||
(contains? variant-ids variant-id)
|
||||
acc
|
||||
:else
|
||||
{:variant-ids (conj variant-ids variant-id) :components (conj components component)}))
|
||||
{:variant-ids #{} :components []}
|
||||
components)]
|
||||
{:components components
|
||||
:variant-ids variant-ids}))
|
||||
|
||||
(def ^:private sql:team-shared-files
|
||||
"select f.id,
|
||||
f.revn,
|
||||
@@ -496,10 +637,13 @@
|
||||
:sample (into [] (take limit sorted-assets))}))]
|
||||
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(let [load-objects (fn [component]
|
||||
(ctf/load-component-objects data component))
|
||||
components-sample (-> (assets-sample (ctkl/components data) 4)
|
||||
(update :sample #(mapv load-objects %)))]
|
||||
(let [load-objects (fn [component]
|
||||
(ctf/load-component-objects data component))
|
||||
comps-and-variants (components-and-variants (ctkl/components-seq data))
|
||||
components (into {} (map (juxt :id identity) (:components comps-and-variants)))
|
||||
components-sample (-> (assets-sample components 4)
|
||||
(update :sample #(mapv load-objects %))
|
||||
(assoc :variants-count (-> comps-and-variants :variant-ids count)))]
|
||||
{:components components-sample
|
||||
:media (assets-sample (:media data) 3)
|
||||
:colors (assets-sample (:colors data) 3)
|
||||
@@ -553,6 +697,7 @@
|
||||
|
||||
;; --- COMMAND QUERY: Files that use this File library
|
||||
|
||||
|
||||
(def ^:private sql:library-using-files
|
||||
"SELECT f.id,
|
||||
f.name
|
||||
@@ -625,6 +770,7 @@
|
||||
|
||||
;; --- COMMAND QUERY: get-file-summary
|
||||
|
||||
|
||||
(defn- get-file-summary
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id id project-id] :as params}]
|
||||
(check-read-permissions! conn profile-id id)
|
||||
@@ -633,20 +779,22 @@
|
||||
:project-id project-id
|
||||
:file-id id)
|
||||
|
||||
file (bfc/get-file cfg id
|
||||
:project-id project-id
|
||||
:read-only? true)]
|
||||
file (get-file cfg id
|
||||
:project-id project-id
|
||||
:read-only? true)]
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
{:name (:name file)
|
||||
:components-count (count (ctkl/components-seq (:data file)))
|
||||
:graphics-count (count (get-in file [:data :media] []))
|
||||
:colors-count (count (get-in file [:data :colors] []))
|
||||
:typography-count (count (get-in file [:data :typographies] []))})))
|
||||
(let [components-and-variants (components-and-variants (ctkl/components-seq (:data file)))]
|
||||
{:name (:name file)
|
||||
:components-count (-> components-and-variants :components count)
|
||||
:variants-count (-> components-and-variants :variant-ids count)
|
||||
:graphics-count (count (get-in file [:data :media] []))
|
||||
:colors-count (count (get-in file [:data :colors] []))
|
||||
:typography-count (count (get-in file [:data :typographies] []))}))))
|
||||
|
||||
(sv/defmethod ::get-file-summary
|
||||
"Retrieve a file summary by its ID. Only authenticated users."
|
||||
@@ -658,6 +806,7 @@
|
||||
|
||||
;; --- COMMAND QUERY: get-file-info
|
||||
|
||||
|
||||
(defn- get-file-info
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
|
||||
(db/get* conn :file
|
||||
@@ -722,7 +871,7 @@
|
||||
|
||||
;; --- MUTATION COMMAND: set-file-shared
|
||||
|
||||
(def ^:private sql:get-referenced-files
|
||||
(def sql:get-referenced-files
|
||||
"SELECT f.id
|
||||
FROM file_library_rel AS flr
|
||||
INNER JOIN file AS f ON (f.id = flr.file_id)
|
||||
@@ -733,51 +882,56 @@
|
||||
(defn- absorb-library-by-file!
|
||||
[cfg ldata file-id]
|
||||
|
||||
(assert (db/connection-map? cfg)
|
||||
"expected cfg with valid connection")
|
||||
(dm/assert!
|
||||
"expected cfg with valid connection"
|
||||
(db/connection-map? cfg))
|
||||
|
||||
(binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)
|
||||
pmap/*tracked* (pmap/create-tracked)]
|
||||
(let [file (-> (bfc/get-file cfg file-id
|
||||
:include-deleted? true
|
||||
:lock-for-update? true)
|
||||
(let [file (-> (get-file cfg file-id
|
||||
:include-deleted? true
|
||||
:lock-for-update? true)
|
||||
(update :data ctf/absorb-assets ldata))]
|
||||
|
||||
(l/trc :hint "library absorbed"
|
||||
:library-id (str (:id ldata))
|
||||
:file-id (str file-id))
|
||||
|
||||
(bfc/update-file! cfg {:id file-id
|
||||
:migrations (:migrations file)
|
||||
:revn (inc (:revn file))
|
||||
:data (:data file)
|
||||
:modified-at (ct/now)
|
||||
:has-media-trimmed false}))))
|
||||
(db/update! cfg :file
|
||||
{:revn (inc (:revn file))
|
||||
:data (blob/encode (:data file))
|
||||
:modified-at (ct/now)
|
||||
:has-media-trimmed false}
|
||||
{:id file-id})
|
||||
|
||||
(feat.fdata/persist-pointers! cfg file-id))))
|
||||
|
||||
(defn- absorb-library
|
||||
"Find all files using a shared library, and absorb all library assets
|
||||
into the file local libraries"
|
||||
[cfg {:keys [id data] :as library}]
|
||||
[cfg {:keys [id] :as library}]
|
||||
|
||||
(assert (db/connection-map? cfg)
|
||||
"expected cfg with valid connection")
|
||||
(dm/assert!
|
||||
"expected cfg with valid connection"
|
||||
(db/connection-map? cfg))
|
||||
|
||||
(let [ids (->> (db/exec! cfg [sql:get-referenced-files id])
|
||||
(sequence bfc/xf-map-id))]
|
||||
(let [ldata (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(-> library :data (feat.fdata/process-pointers deref)))
|
||||
ids (->> (db/exec! cfg [sql:get-referenced-files id])
|
||||
(map :id))]
|
||||
|
||||
(l/trc :hint "absorbing library"
|
||||
:library-id (str id)
|
||||
:files (str/join "," (map str ids)))
|
||||
|
||||
(run! (partial absorb-library-by-file! cfg data) ids)
|
||||
(run! (partial absorb-library-by-file! cfg ldata) ids)
|
||||
library))
|
||||
|
||||
(defn absorb-library!
|
||||
[{:keys [::db/conn] :as cfg} id]
|
||||
(let [file (-> (bfc/get-file cfg id
|
||||
:realize? true
|
||||
:lock-for-update? true
|
||||
:include-deleted? true)
|
||||
(let [file (-> (get-file cfg id
|
||||
:lock-for-update? true
|
||||
:include-deleted? true)
|
||||
(check-version!))
|
||||
|
||||
proj (db/get* conn :project {:id (:project-id file)}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.file :as ctf]
|
||||
@@ -52,7 +51,6 @@
|
||||
:revn revn
|
||||
:is-shared is-shared
|
||||
:features features
|
||||
:migrations fmg/available-migrations
|
||||
:ignore-sync-until ignore-sync-until
|
||||
:created-at modified-at
|
||||
:deleted-at deleted-at}
|
||||
@@ -68,7 +66,7 @@
|
||||
{:modified-at (ct/now)}
|
||||
{:id project-id})
|
||||
|
||||
(bfc/get-file cfg (:id file)))))
|
||||
file)))
|
||||
|
||||
(def ^:private schema:create-file
|
||||
[:map {:title "create-file"}
|
||||
|
||||
@@ -8,17 +8,52 @@
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.migrations :as fmg]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.file-snapshots :as fsnap]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.features.file-migrations :refer [reset-migrations!]]
|
||||
[app.main :as-alias main]
|
||||
[app.msgbus :as mbus]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.util.services :as sv]))
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.services :as sv]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn decode-row
|
||||
[{:keys [migrations] :as row}]
|
||||
(when row
|
||||
(cond-> row
|
||||
(some? migrations)
|
||||
(assoc :migrations (db/decode-pgarray migrations)))))
|
||||
|
||||
(def sql:get-file-snapshots
|
||||
"WITH changes AS (
|
||||
SELECT id, label, revn, created_at, created_by, profile_id, locked_by
|
||||
FROM file_change
|
||||
WHERE file_id = ?
|
||||
AND data IS NOT NULL
|
||||
AND (deleted_at IS NULL OR deleted_at > now())
|
||||
), versions AS (
|
||||
(SELECT * FROM changes WHERE created_by = 'system' LIMIT 1000)
|
||||
UNION ALL
|
||||
(SELECT * FROM changes WHERE created_by != 'system' LIMIT 1000)
|
||||
)
|
||||
SELECT * FROM versions
|
||||
ORDER BY created_at DESC;")
|
||||
|
||||
(defn get-file-snapshots
|
||||
[conn file-id]
|
||||
(db/exec! conn [sql:get-file-snapshots file-id]))
|
||||
|
||||
(def ^:private schema:get-file-snapshots
|
||||
[:map {:title "get-file-snapshots"}
|
||||
@@ -30,7 +65,73 @@
|
||||
[cfg {:keys [::rpc/profile-id file-id] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(files/check-read-permissions! conn profile-id file-id)
|
||||
(fsnap/get-visible-snapshots conn file-id))))
|
||||
(get-file-snapshots conn file-id))))
|
||||
|
||||
(defn- generate-snapshot-label
|
||||
[]
|
||||
(let [ts (-> (ct/now)
|
||||
(ct/format-inst)
|
||||
(str/replace #"[T:\.]" "-")
|
||||
(str/rtrim "Z"))]
|
||||
(str "snapshot-" ts)))
|
||||
|
||||
(defn create-file-snapshot!
|
||||
[cfg file & {:keys [label created-by deleted-at profile-id]
|
||||
:or {deleted-at :default
|
||||
created-by :system}}]
|
||||
|
||||
(assert (#{:system :user :admin} created-by)
|
||||
"expected valid keyword for created-by")
|
||||
|
||||
(let [created-by
|
||||
(name created-by)
|
||||
|
||||
deleted-at
|
||||
(cond
|
||||
(= deleted-at :default)
|
||||
(ct/plus (ct/now) (cf/get-deletion-delay))
|
||||
|
||||
(ct/inst? deleted-at)
|
||||
deleted-at
|
||||
|
||||
:else
|
||||
nil)
|
||||
|
||||
label
|
||||
(or label (generate-snapshot-label))
|
||||
|
||||
snapshot-id
|
||||
(uuid/next)
|
||||
|
||||
data
|
||||
(blob/encode (:data file))
|
||||
|
||||
features
|
||||
(into-array (:features file))
|
||||
|
||||
migrations
|
||||
(into-array (:migrations file))]
|
||||
|
||||
(l/dbg :hint "creating file snapshot"
|
||||
:file-id (str (:id file))
|
||||
:id (str snapshot-id)
|
||||
:label label)
|
||||
|
||||
(db/insert! cfg :file-change
|
||||
{:id snapshot-id
|
||||
:revn (:revn file)
|
||||
:data data
|
||||
:version (:version file)
|
||||
:features features
|
||||
:migrations migrations
|
||||
:profile-id profile-id
|
||||
:file-id (:id file)
|
||||
:label label
|
||||
:deleted-at deleted-at
|
||||
:created-by created-by}
|
||||
{::db/return-keys false})
|
||||
|
||||
{:id snapshot-id :label label}))
|
||||
|
||||
(def ^:private schema:create-file-snapshot
|
||||
[:map
|
||||
@@ -43,7 +144,7 @@
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id label]}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(let [file (bfc/get-file cfg file-id :realize? true)
|
||||
(let [file (bfc/get-file cfg file-id)
|
||||
project (db/get-by-id cfg :project (:project-id file))]
|
||||
|
||||
(-> cfg
|
||||
@@ -54,10 +155,96 @@
|
||||
(quotes/check! {::quotes/id ::quotes/snapshots-per-file}
|
||||
{::quotes/id ::quotes/snapshots-per-team}))
|
||||
|
||||
(fsnap/create! cfg file
|
||||
{:label label
|
||||
:profile-id profile-id
|
||||
:created-by "user"})))
|
||||
(create-file-snapshot! cfg file
|
||||
{:label label
|
||||
:profile-id profile-id
|
||||
:created-by :user})))
|
||||
|
||||
(defn restore-file-snapshot!
|
||||
[{:keys [::db/conn ::mbus/msgbus] :as cfg} file-id snapshot-id]
|
||||
(let [storage (sto/resolve cfg {::db/reuse-conn true})
|
||||
file (files/get-minimal-file conn file-id {::db/for-update true})
|
||||
vern (rand-int Integer/MAX_VALUE)
|
||||
snapshot (some->> (db/get* conn :file-change
|
||||
{:file-id file-id
|
||||
:id snapshot-id}
|
||||
{::db/for-share true})
|
||||
(feat.fdata/resolve-file-data cfg)
|
||||
(decode-row))
|
||||
|
||||
;; If snapshot has tracked applied migrations, we reuse them,
|
||||
;; if not we take a safest set of migrations as starting
|
||||
;; point. This is because, at the time of implementing
|
||||
;; snapshots, migrations were not taken into account so we
|
||||
;; need to make this backward compatible in some way.
|
||||
file (assoc file :migrations
|
||||
(or (:migrations snapshot)
|
||||
(fmg/generate-migrations-from-version 67)))]
|
||||
|
||||
(when-not snapshot
|
||||
(ex/raise :type :not-found
|
||||
:code :snapshot-not-found
|
||||
:hint "unable to find snapshot with the provided label"
|
||||
:snapshot-id snapshot-id
|
||||
:file-id file-id))
|
||||
|
||||
(when-not (:data snapshot)
|
||||
(ex/raise :type :validation
|
||||
:code :snapshot-without-data
|
||||
:hint "snapshot has no data"
|
||||
:label (:label snapshot)
|
||||
:file-id file-id))
|
||||
|
||||
(l/dbg :hint "restoring snapshot"
|
||||
:file-id (str file-id)
|
||||
:label (:label snapshot)
|
||||
:snapshot-id (str (:id snapshot)))
|
||||
|
||||
;; If the file was already offloaded, on restoring the snapshot we
|
||||
;; are going to replace the file data, so we need to touch the old
|
||||
;; referenced storage object and avoid possible leaks
|
||||
(when (feat.fdata/offloaded? file)
|
||||
(sto/touch-object! storage (:data-ref-id file)))
|
||||
|
||||
;; In the same way, on reseting the file data, we need to restore
|
||||
;; the applied migrations on the moment of taking the snapshot
|
||||
(reset-migrations! conn file)
|
||||
|
||||
(db/update! conn :file
|
||||
{:data (:data snapshot)
|
||||
:revn (inc (:revn file))
|
||||
:vern vern
|
||||
:version (:version snapshot)
|
||||
:data-backend nil
|
||||
:data-ref-id nil
|
||||
:has-media-trimmed false
|
||||
:features (:features snapshot)}
|
||||
{:id file-id})
|
||||
|
||||
;; clean object thumbnails
|
||||
(let [sql (str "update file_tagged_object_thumbnail "
|
||||
" set deleted_at = now() "
|
||||
" where file_id=? returning media_id")
|
||||
res (db/exec! conn [sql file-id])]
|
||||
(doseq [media-id (into #{} (keep :media-id) res)]
|
||||
(sto/touch-object! storage media-id)))
|
||||
|
||||
;; clean file thumbnails
|
||||
(let [sql (str "update file_thumbnail "
|
||||
" set deleted_at = now() "
|
||||
" where file_id=? returning media_id")
|
||||
res (db/exec! conn [sql file-id])]
|
||||
(doseq [media-id (into #{} (keep :media-id) res)]
|
||||
(sto/touch-object! storage media-id)))
|
||||
|
||||
;; Send to the clients a notification to reload the file
|
||||
(mbus/pub! msgbus
|
||||
:topic (:id file)
|
||||
:message {:type :file-restore
|
||||
:file-id (:id file)
|
||||
:vern vern})
|
||||
{:id (:id snapshot)
|
||||
:label (:label snapshot)}))
|
||||
|
||||
(def ^:private schema:restore-file-snapshot
|
||||
[:map {:title "restore-file-snapshot"}
|
||||
@@ -66,56 +253,88 @@
|
||||
|
||||
(sv/defmethod ::restore-file-snapshot
|
||||
{::doc/added "1.20"
|
||||
::sm/params schema:restore-file-snapshot
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(let [file (bfc/get-file cfg file-id)]
|
||||
(fsnap/create! cfg file
|
||||
{:profile-id profile-id
|
||||
:created-by "system"})
|
||||
(let [vern (fsnap/restore! cfg file-id id)]
|
||||
;; Send to the clients a notification to reload the file
|
||||
(mbus/pub! msgbus
|
||||
:topic (:id file)
|
||||
:message {:type :file-restore
|
||||
:file-id (:id file)
|
||||
:vern vern})
|
||||
nil)))
|
||||
::sm/params schema:restore-file-snapshot}
|
||||
[cfg {:keys [::rpc/profile-id file-id id] :as params}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
(let [file (bfc/get-file cfg file-id)]
|
||||
(create-file-snapshot! cfg file
|
||||
{:profile-id profile-id
|
||||
:created-by :system})
|
||||
(restore-file-snapshot! cfg file-id id)))))
|
||||
|
||||
(def ^:private schema:update-file-snapshot
|
||||
[:map {:title "update-file-snapshot"}
|
||||
[:id ::sm/uuid]
|
||||
[:label ::sm/text]])
|
||||
|
||||
(defn- update-file-snapshot!
|
||||
[conn snapshot-id label]
|
||||
(-> (db/update! conn :file-change
|
||||
{:label label
|
||||
:created-by "user"
|
||||
:deleted-at nil}
|
||||
{:id snapshot-id}
|
||||
{::db/return-keys true})
|
||||
(dissoc :data :features :migrations)))
|
||||
|
||||
(defn- get-snapshot
|
||||
"Get a minimal snapshot from database and lock for update"
|
||||
[conn id]
|
||||
(db/get conn :file-change
|
||||
{:id id}
|
||||
{::sql/columns [:id :file-id :created-by :deleted-at :profile-id :locked-by]
|
||||
::db/for-update true}))
|
||||
|
||||
(sv/defmethod ::update-file-snapshot
|
||||
{::doc/added "1.20"
|
||||
::sm/params schema:update-file-snapshot
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id label]}]
|
||||
(let [snapshot (fsnap/get-minimal-snapshot conn id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
|
||||
(fsnap/update! conn (assoc snapshot :label label))))
|
||||
::sm/params schema:update-file-snapshot}
|
||||
[cfg {:keys [::rpc/profile-id id label]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(let [snapshot (get-snapshot conn id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
|
||||
(update-file-snapshot! conn id label)))))
|
||||
|
||||
(def ^:private schema:remove-file-snapshot
|
||||
[:map {:title "remove-file-snapshot"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(defn- delete-file-snapshot!
|
||||
[conn snapshot-id]
|
||||
(db/update! conn :file-change
|
||||
{:deleted-at (ct/now)}
|
||||
{:id snapshot-id}
|
||||
{::db/return-keys false})
|
||||
nil)
|
||||
|
||||
(sv/defmethod ::delete-file-snapshot
|
||||
{::doc/added "1.20"
|
||||
::sm/params schema:remove-file-snapshot
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id]}]
|
||||
(let [snapshot (fsnap/get-minimal-snapshot conn id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
|
||||
::sm/params schema:remove-file-snapshot}
|
||||
[cfg {:keys [::rpc/profile-id id]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(let [snapshot (get-snapshot conn id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
|
||||
|
||||
(when (not= (:created-by snapshot) "user")
|
||||
(ex/raise :type :validation
|
||||
:code :system-snapshots-cant-be-deleted
|
||||
:file-id (:file-id snapshot)
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
(fsnap/delete! conn snapshot)))
|
||||
(when (not= (:created-by snapshot) "user")
|
||||
(ex/raise :type :validation
|
||||
:code :system-snapshots-cant-be-deleted
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
|
||||
;; Check if version is locked by someone else
|
||||
(when (and (:locked-by snapshot)
|
||||
(not= (:locked-by snapshot) profile-id))
|
||||
(ex/raise :type :validation
|
||||
:code :snapshot-is-locked
|
||||
:hint "Cannot delete a locked version"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id
|
||||
:locked-by (:locked-by snapshot)))
|
||||
|
||||
(delete-file-snapshot! conn id)))))
|
||||
|
||||
;;; Lock/unlock version endpoints
|
||||
|
||||
@@ -123,7 +342,6 @@
|
||||
[:map {:title "lock-file-snapshot"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
;; MOVE to fsnap
|
||||
(defn- lock-file-snapshot!
|
||||
[conn snapshot-id profile-id]
|
||||
(db/update! conn :file-change
|
||||
@@ -134,45 +352,44 @@
|
||||
|
||||
(sv/defmethod ::lock-file-snapshot
|
||||
{::doc/added "1.20"
|
||||
::sm/params schema:lock-file-snapshot
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id]}]
|
||||
(let [snapshot (fsnap/get-minimal-snapshot conn id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
|
||||
::sm/params schema:lock-file-snapshot}
|
||||
[cfg {:keys [::rpc/profile-id id]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(let [snapshot (get-snapshot conn id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
|
||||
|
||||
(when (not= (:created-by snapshot) "user")
|
||||
(ex/raise :type :validation
|
||||
:code :system-snapshots-cant-be-locked
|
||||
:hint "Only user-created versions can be locked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
(when (not= (:created-by snapshot) "user")
|
||||
(ex/raise :type :validation
|
||||
:code :system-snapshots-cant-be-locked
|
||||
:hint "Only user-created versions can be locked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
|
||||
;; Only the creator can lock their own version
|
||||
(when (not= (:profile-id snapshot) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :only-creator-can-lock
|
||||
:hint "Only the version creator can lock it"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id
|
||||
:creator-id (:profile-id snapshot)))
|
||||
;; Only the creator can lock their own version
|
||||
(when (not= (:profile-id snapshot) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :only-creator-can-lock
|
||||
:hint "Only the version creator can lock it"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id
|
||||
:creator-id (:profile-id snapshot)))
|
||||
|
||||
;; Check if already locked
|
||||
(when (:locked-by snapshot)
|
||||
(ex/raise :type :validation
|
||||
:code :snapshot-already-locked
|
||||
:hint "Version is already locked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id
|
||||
:locked-by (:locked-by snapshot)))
|
||||
;; Check if already locked
|
||||
(when (:locked-by snapshot)
|
||||
(ex/raise :type :validation
|
||||
:code :snapshot-already-locked
|
||||
:hint "Version is already locked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id
|
||||
:locked-by (:locked-by snapshot)))
|
||||
|
||||
(lock-file-snapshot! conn id profile-id)))
|
||||
(lock-file-snapshot! conn id profile-id)))))
|
||||
|
||||
(def ^:private schema:unlock-file-snapshot
|
||||
[:map {:title "unlock-file-snapshot"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
|
||||
;; MOVE to fsnap
|
||||
(defn- unlock-file-snapshot!
|
||||
[conn snapshot-id]
|
||||
(db/update! conn :file-change
|
||||
@@ -183,34 +400,35 @@
|
||||
|
||||
(sv/defmethod ::unlock-file-snapshot
|
||||
{::doc/added "1.20"
|
||||
::sm/params schema:unlock-file-snapshot
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id id]}]
|
||||
(let [snapshot (fsnap/get-minimal-snapshot conn id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
|
||||
::sm/params schema:unlock-file-snapshot}
|
||||
[cfg {:keys [::rpc/profile-id id]}]
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(let [snapshot (get-snapshot conn id)]
|
||||
(files/check-edition-permissions! conn profile-id (:file-id snapshot))
|
||||
|
||||
(when (not= (:created-by snapshot) "user")
|
||||
(ex/raise :type :validation
|
||||
:code :system-snapshots-cant-be-unlocked
|
||||
:hint "Only user-created versions can be unlocked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
(when (not= (:created-by snapshot) "user")
|
||||
(ex/raise :type :validation
|
||||
:code :system-snapshots-cant-be-unlocked
|
||||
:hint "Only user-created versions can be unlocked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
|
||||
;; Only the creator can unlock their own version
|
||||
(when (not= (:profile-id snapshot) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :only-creator-can-unlock
|
||||
:hint "Only the version creator can unlock it"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id
|
||||
:creator-id (:profile-id snapshot)))
|
||||
;; Only the creator can unlock their own version
|
||||
(when (not= (:profile-id snapshot) profile-id)
|
||||
(ex/raise :type :validation
|
||||
:code :only-creator-can-unlock
|
||||
:hint "Only the version creator can unlock it"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id
|
||||
:creator-id (:profile-id snapshot)))
|
||||
|
||||
;; Check if not locked
|
||||
(when (not (:locked-by snapshot))
|
||||
(ex/raise :type :validation
|
||||
:code :snapshot-not-locked
|
||||
:hint "Version is not locked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
;; Check if not locked
|
||||
(when (not (:locked-by snapshot))
|
||||
(ex/raise :type :validation
|
||||
:code :snapshot-not-locked
|
||||
:hint "Version is not locked"
|
||||
:snapshot-id id
|
||||
:profile-id profile-id))
|
||||
|
||||
(unlock-file-snapshot! conn id)))
|
||||
(unlock-file-snapshot! conn id)))))
|
||||
|
||||
160
backend/src/app/rpc/commands/files_temp.clj
Normal file
160
backend/src/app/rpc/commands/files_temp.clj
Normal file
@@ -0,0 +1,160 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.rpc.commands.files-temp
|
||||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.files.changes :as cpc]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as sql]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.files-create :as files.create]
|
||||
[app.rpc.commands.files-update :as-alias files.update]
|
||||
[app.rpc.commands.projects :as projects]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
[clojure.set :as set]))
|
||||
|
||||
;; --- MUTATION COMMAND: create-temp-file
|
||||
|
||||
(def ^:private schema:create-temp-file
|
||||
[:map {:title "create-temp-file"}
|
||||
[:name [:string {:max 250}]]
|
||||
[:project-id ::sm/uuid]
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:is-shared ::sm/boolean]
|
||||
[:features ::cfeat/features]
|
||||
[:create-page ::sm/boolean]])
|
||||
|
||||
(sv/defmethod ::create-temp-file
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::sm/params schema:create-temp-file
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id project-id] :as params}]
|
||||
(projects/check-edition-permissions! conn profile-id project-id)
|
||||
(let [team (teams/get-team conn :profile-id profile-id :project-id project-id)
|
||||
;; When we create files, we only need to respect the team
|
||||
;; features, because some features can be enabled
|
||||
;; globally, but the team is still not migrated properly.
|
||||
input-features
|
||||
(:features params #{})
|
||||
|
||||
;; If the imported project doesn't contain v2 we need to remove it
|
||||
team-features
|
||||
(cond-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(not (contains? input-features "components/v2"))
|
||||
(disj "components/v2"))
|
||||
|
||||
;; We also include all no migration features declared by
|
||||
;; client; that enables the ability to enable a runtime
|
||||
;; feature on frontend and make it permanent on file
|
||||
features
|
||||
(-> input-features
|
||||
(set/intersection cfeat/no-migration-features)
|
||||
(set/union team-features))
|
||||
|
||||
params
|
||||
(-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :deleted-at (ct/in-future {:days 1}))
|
||||
(assoc :features features))]
|
||||
|
||||
(files.create/create-file cfg params)))
|
||||
|
||||
;; --- MUTATION COMMAND: update-temp-file
|
||||
|
||||
(def ^:private schema:update-temp-file
|
||||
[:map {:title "update-temp-file"}
|
||||
[:changes [:vector cpc/schema:change]]
|
||||
[:revn [::sm/int {:min 0}]]
|
||||
[:session-id ::sm/uuid]
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::update-temp-file
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::sm/params schema:update-temp-file}
|
||||
[cfg {:keys [::rpc/profile-id session-id id revn changes] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(db/insert! conn :file-change
|
||||
{:id (uuid/next)
|
||||
:session-id session-id
|
||||
:profile-id profile-id
|
||||
:created-at (ct/now)
|
||||
:file-id id
|
||||
:revn revn
|
||||
:data nil
|
||||
:changes (blob/encode changes)})
|
||||
(rph/with-meta (rph/wrap nil)
|
||||
{::audit/replace-props {:file-id id
|
||||
:revn revn}}))))
|
||||
|
||||
;; --- MUTATION COMMAND: persist-temp-file
|
||||
|
||||
(defn persist-temp-file
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as params}]
|
||||
(let [file (files/get-file cfg id
|
||||
:migrate? false
|
||||
:lock-for-update? true)]
|
||||
|
||||
(when (nil? (:deleted-at file))
|
||||
(ex/raise :type :validation
|
||||
:code :cant-persist-already-persisted-file))
|
||||
|
||||
(let [changes (->> (db/cursor conn
|
||||
(sql/select :file-change {:file-id id}
|
||||
{:order-by [[:revn :asc]]})
|
||||
{:chunk-size 10})
|
||||
(sequence (mapcat (comp blob/decode :changes))))
|
||||
|
||||
file (update file :data cpc/process-changes changes)
|
||||
|
||||
file (if (contains? (:features file) "fdata/objects-map")
|
||||
(fdata/enable-objects-map file)
|
||||
file)
|
||||
|
||||
file (if (contains? (:features file) "fdata/pointer-map")
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)]
|
||||
(let [file (fdata/enable-pointer-map file)]
|
||||
(fdata/persist-pointers! cfg id)
|
||||
file))
|
||||
file)]
|
||||
|
||||
;; Delete changes from the changes history
|
||||
(db/delete! conn :file-change {:file-id id})
|
||||
|
||||
(db/update! conn :file
|
||||
{:deleted-at nil
|
||||
:revn 1
|
||||
:data (blob/encode (:data file))}
|
||||
{:id id})
|
||||
nil)))
|
||||
|
||||
(def ^:private schema:persist-temp-file
|
||||
[:map {:title "persist-temp-file"}
|
||||
[:id ::sm/uuid]])
|
||||
|
||||
(sv/defmethod ::persist-temp-file
|
||||
{::doc/added "1.17"
|
||||
::doc/module :files
|
||||
::sm/params schema:persist-temp-file}
|
||||
[cfg {:keys [::rpc/profile-id id] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
(persist-temp-file cfg params))))
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.rpc.commands.files-thumbnails
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.features :as cfeat]
|
||||
@@ -203,9 +202,9 @@
|
||||
:profile-id profile-id
|
||||
:file-id file-id)
|
||||
|
||||
file (bfc/get-file cfg file-id
|
||||
:realize? true
|
||||
:read-only? true)]
|
||||
file (files/get-file cfg file-id
|
||||
:preload-pointers? true
|
||||
:read-only? true)]
|
||||
|
||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
@@ -272,7 +271,7 @@
|
||||
[:map {:title "create-file-object-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:object-id [:string {:max 250}]]
|
||||
[:media ::media/upload]
|
||||
[:media media/schema:upload]
|
||||
[:tag {:optional true} [:string {:max 50}]]])
|
||||
|
||||
(sv/defmethod ::create-file-object-thumbnail
|
||||
@@ -340,7 +339,6 @@
|
||||
data (-> (sto/content path)
|
||||
(sto/wrap-with-hash hash))
|
||||
tnow (ct/now)
|
||||
|
||||
media (sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/deduplicate? true
|
||||
@@ -383,7 +381,7 @@
|
||||
[:map {:title "create-file-thumbnail"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:revn ::sm/int]
|
||||
[:media ::media/upload]])
|
||||
[:media media/schema:upload]])
|
||||
|
||||
(sv/defmethod ::create-file-thumbnail
|
||||
"Creates or updates the file thumbnail. Mainly used for paint the
|
||||
|
||||
@@ -19,9 +19,8 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.features.file-migrations :as feat.fmigr]
|
||||
[app.features.file-snapshots :as fsnap]
|
||||
[app.features.logical-deletion :as ldel]
|
||||
[app.http.errors :as errors]
|
||||
[app.loggers.audit :as audit]
|
||||
@@ -34,6 +33,7 @@
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[app.util.pointer-map :as pmap]
|
||||
[app.util.services :as sv]
|
||||
@@ -64,10 +64,10 @@
|
||||
[:revn {:min 0} ::sm/int]
|
||||
[:vern {:min 0} ::sm/int]
|
||||
[:features {:optional true} ::cfeat/features]
|
||||
[:changes {:optional true} [:vector ::cpc/change]]
|
||||
[:changes {:optional true} [:vector cpc/schema:change]]
|
||||
[:changes-with-metadata {:optional true}
|
||||
[:vector [:map
|
||||
[:changes [:vector ::cpc/change]]
|
||||
[:changes [:vector cpc/schema:change]]
|
||||
[:hint-origin {:optional true} :keyword]
|
||||
[:hint-events {:optional true} [:vector [:string {:max 250}]]]]]]
|
||||
[:skip-validate {:optional true} ::sm/boolean]])
|
||||
@@ -76,7 +76,7 @@
|
||||
schema:update-file-result
|
||||
[:vector {:title "update-file-result"}
|
||||
[:map
|
||||
[:changes [:vector ::cpc/change]]
|
||||
[:changes [:vector cpc/schema:change]]
|
||||
[:file-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:revn {:min 0} ::sm/int]
|
||||
@@ -129,78 +129,77 @@
|
||||
::sm/params schema:update-file
|
||||
::sm/result schema:update-file-result
|
||||
::doc/module :files
|
||||
::doc/added "1.17"
|
||||
::db/transaction true}
|
||||
[{:keys [::mtx/metrics ::db/conn] :as cfg}
|
||||
::doc/added "1.17"}
|
||||
[{:keys [::mtx/metrics] :as cfg}
|
||||
{:keys [::rpc/profile-id id changes changes-with-metadata] :as params}]
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
(db/xact-lock! conn id)
|
||||
|
||||
(files/check-edition-permissions! conn profile-id id)
|
||||
(db/xact-lock! conn id)
|
||||
(let [file (get-file conn id)
|
||||
team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:team-id (:team-id file))
|
||||
|
||||
(let [file (get-file cfg id)
|
||||
team (teams/get-team conn
|
||||
:profile-id profile-id
|
||||
:team-id (:team-id file))
|
||||
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
|
||||
features (-> (cfeat/get-team-enabled-features cf/flags team)
|
||||
(cfeat/check-client-features! (:features params))
|
||||
(cfeat/check-file-features! (:features file)))
|
||||
changes (if changes-with-metadata
|
||||
(->> changes-with-metadata (mapcat :changes) vec)
|
||||
(vec changes))
|
||||
|
||||
changes (if changes-with-metadata
|
||||
(->> changes-with-metadata (mapcat :changes) vec)
|
||||
(vec changes))
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features (set/difference features cfeat/frontend-only-features))
|
||||
(assoc :team team)
|
||||
(assoc :file file)
|
||||
(assoc :changes changes))
|
||||
|
||||
params (-> params
|
||||
(assoc :profile-id profile-id)
|
||||
(assoc :features (set/difference features cfeat/frontend-only-features))
|
||||
(assoc :team team)
|
||||
(assoc :file file)
|
||||
(assoc :changes changes))
|
||||
cfg (assoc cfg ::timestamp (ct/now))
|
||||
|
||||
cfg (assoc cfg ::timestamp (ct/now))
|
||||
|
||||
tpoint (ct/tpoint)]
|
||||
tpoint (ct/tpoint)]
|
||||
|
||||
|
||||
(when (not= (:vern params)
|
||||
(:vern file))
|
||||
(ex/raise :type :validation
|
||||
:code :vern-conflict
|
||||
:hint "A different version has been restored for the file."
|
||||
:context {:incoming-revn (:revn params)
|
||||
:stored-revn (:revn file)}))
|
||||
(when (not= (:vern params)
|
||||
(:vern file))
|
||||
(ex/raise :type :validation
|
||||
:code :vern-conflict
|
||||
:hint "A different version has been restored for the file."
|
||||
:context {:incoming-revn (:revn params)
|
||||
:stored-revn (:revn file)}))
|
||||
|
||||
(when (> (:revn params)
|
||||
(:revn file))
|
||||
(ex/raise :type :validation
|
||||
:code :revn-conflict
|
||||
:hint "The incoming revision number is greater that stored version."
|
||||
:context {:incoming-revn (:revn params)
|
||||
:stored-revn (:revn file)}))
|
||||
(when (> (:revn params)
|
||||
(:revn file))
|
||||
(ex/raise :type :validation
|
||||
:code :revn-conflict
|
||||
:hint "The incoming revision number is greater that stored version."
|
||||
:context {:incoming-revn (:revn params)
|
||||
:stored-revn (:revn file)}))
|
||||
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it
|
||||
(when-let [features (-> features
|
||||
(set/difference (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(not-empty))]
|
||||
(let [features (->> features
|
||||
(set/union (:features team))
|
||||
(db/create-array conn "text"))]
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id (:id team)}
|
||||
{::db/return-keys false})))
|
||||
;; When newly computed features does not match exactly with
|
||||
;; the features defined on team row, we update it
|
||||
(when-let [features (-> features
|
||||
(set/difference (:features team))
|
||||
(set/difference cfeat/no-team-inheritable-features)
|
||||
(not-empty))]
|
||||
(let [features (->> features
|
||||
(set/union (:features team))
|
||||
(db/create-array conn "text"))]
|
||||
(db/update! conn :team
|
||||
{:features features}
|
||||
{:id (:id team)}
|
||||
{::db/return-keys false})))
|
||||
|
||||
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
(mtx/run! metrics {:id :update-file-changes :inc (count changes)})
|
||||
|
||||
(binding [l/*context* (some-> (meta params)
|
||||
(get :app.http/request)
|
||||
(errors/request->context))]
|
||||
(-> (update-file* cfg params)
|
||||
(rph/with-defer #(let [elapsed (tpoint)]
|
||||
(l/trace :hint "update-file" :time (ct/format-duration elapsed))))))))
|
||||
(binding [l/*context* (some-> (meta params)
|
||||
(get :app.http/request)
|
||||
(errors/request->context))]
|
||||
(-> (update-file* cfg params)
|
||||
(rph/with-defer #(let [elapsed (tpoint)]
|
||||
(l/trace :hint "update-file" :time (ct/format-duration elapsed))))))))))
|
||||
|
||||
(defn- update-file*
|
||||
"Internal function, part of the update-file process, that encapsulates
|
||||
@@ -213,44 +212,28 @@
|
||||
[{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg}
|
||||
{:keys [profile-id file team features changes session-id skip-validate] :as params}]
|
||||
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)
|
||||
pmap/*load-fn* (partial fdata/load-pointer cfg (:id file))]
|
||||
(let [;; Retrieve the file data
|
||||
file (feat.fmigr/resolve-applied-migrations cfg file)
|
||||
file (feat.fdata/resolve-file-data cfg file)
|
||||
file (assoc file :features
|
||||
(-> features
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/union (:features file))))]
|
||||
|
||||
(let [file (assoc file :features
|
||||
(-> features
|
||||
(set/difference cfeat/frontend-only-features)
|
||||
(set/union (:features file))))
|
||||
;; We create a new lexycal scope for clearly delimit the result of
|
||||
;; executing this update file operation and all its side effects
|
||||
(let [file (px/invoke! executor
|
||||
(fn []
|
||||
;; Process the file data on separated thread for avoid to do
|
||||
;; the CPU intensive operation on vthread.
|
||||
(binding [cfeat/*current* features
|
||||
cfeat/*previous* (:features file)]
|
||||
(update-file-data! cfg file
|
||||
process-changes-and-validate
|
||||
changes skip-validate))))]
|
||||
|
||||
;; We need to preserve the original revn for the response
|
||||
revn
|
||||
(get file :revn)
|
||||
|
||||
;; We create a new lexical scope for clearly delimit the result of
|
||||
;; executing this update file operation and all its side effects
|
||||
file
|
||||
(px/invoke! executor
|
||||
(fn []
|
||||
;; Process the file data on separated thread
|
||||
;; for avoid to do the CPU intensive operation
|
||||
;; on vthread.
|
||||
(binding [cfeat/*current* features
|
||||
cfeat/*previous* (:features file)]
|
||||
(update-file-data! cfg file
|
||||
process-changes-and-validate
|
||||
changes skip-validate))))
|
||||
|
||||
deleted-at
|
||||
(ct/plus timestamp (ct/duration {:hours 1}))]
|
||||
|
||||
(when-let [file (::snapshot file)]
|
||||
(let [deleted-at (ct/plus timestamp (ldel/get-deletion-delay team))
|
||||
label (str "internal/snapshot/" revn)]
|
||||
|
||||
(fsnap/create! cfg file
|
||||
{:label label
|
||||
:deleted-at deleted-at
|
||||
:profile-id profile-id
|
||||
:session-id session-id})))
|
||||
(feat.fmigr/upsert-migrations! conn file)
|
||||
(persist-file! cfg file)
|
||||
|
||||
;; Insert change (xlog) with deleted_at in a future data for
|
||||
;; make them automatically eleggible for GC once they expires
|
||||
@@ -260,28 +243,34 @@
|
||||
:profile-id profile-id
|
||||
:created-at timestamp
|
||||
:updated-at timestamp
|
||||
:deleted-at deleted-at
|
||||
:deleted-at (if (::snapshot-data file)
|
||||
(ct/plus timestamp (ldel/get-deletion-delay team))
|
||||
(ct/plus timestamp (ct/duration {:hours 1})))
|
||||
:file-id (:id file)
|
||||
:revn (:revn file)
|
||||
:version (:version file)
|
||||
:features (into-array (:features file))
|
||||
:features (:features file)
|
||||
:label (::snapshot-label file)
|
||||
:data (::snapshot-data file)
|
||||
:changes (blob/encode changes)}
|
||||
{::db/return-keys false})
|
||||
|
||||
(persist-file! cfg file)
|
||||
|
||||
;; Send asynchronous notifications
|
||||
(send-notifications! cfg params file)
|
||||
(send-notifications! cfg params file))
|
||||
|
||||
(with-meta {:revn revn :lagged (get-lagged-changes conn params)}
|
||||
{::audit/replace-props
|
||||
{:id (:id file)
|
||||
:name (:name file)
|
||||
:features (:features file)
|
||||
:project-id (:project-id file)
|
||||
:team-id (:team-id file)}}))))
|
||||
(when (feat.fdata/offloaded? file)
|
||||
(let [storage (sto/resolve cfg ::db/reuse-conn true)]
|
||||
(some->> (:data-ref-id file) (sto/touch-object! storage))))
|
||||
|
||||
(let [response {:revn (:revn file)
|
||||
:lagged (get-lagged-changes conn params)}]
|
||||
(vary-meta response assoc ::audit/replace-props
|
||||
{:id (:id file)
|
||||
:name (:name file)
|
||||
:features (:features file)
|
||||
:project-id (:project-id file)
|
||||
:team-id (:team-id file)}))))
|
||||
|
||||
;: FIXME: DEPRECATED
|
||||
(defn update-file!
|
||||
"A public api that allows apply a transformation to a file with all context setup."
|
||||
[{:keys [::db/conn] :as cfg} file-id update-fn & args]
|
||||
@@ -290,42 +279,51 @@
|
||||
(feat.fmigr/upsert-migrations! conn file)
|
||||
(persist-file! cfg file)))
|
||||
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.*, p.team_id
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE f.id = ?
|
||||
AND (f.deleted_at IS NULL OR
|
||||
f.deleted_at > now())
|
||||
FOR KEY SHARE")
|
||||
|
||||
(defn get-file
|
||||
"Get not-decoded file, only decodes the features set."
|
||||
[cfg id]
|
||||
;; FIXME: lock for share
|
||||
(bfc/get-file cfg id :decode? false :lock-for-update? true))
|
||||
[conn id]
|
||||
(let [file (db/exec-one! conn [sql:get-file id])]
|
||||
(when-not file
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint (format "file with id '%s' does not exists" id)))
|
||||
(update file :features db/decode-pgarray #{})))
|
||||
|
||||
(defn persist-file!
|
||||
"Function responsible of persisting already encoded file. Should be
|
||||
used together with `get-file` and `update-file-data!`.
|
||||
|
||||
It also updates the project modified-at attr."
|
||||
[{:keys [::db/conn ::timestamp] :as cfg} file]
|
||||
[{:keys [::db/conn ::timestamp]} file]
|
||||
(let [;; The timestamp can be nil because this function is also
|
||||
;; intended to be used outside of this module
|
||||
modified-at
|
||||
(or timestamp (ct/now))
|
||||
|
||||
file
|
||||
(-> file
|
||||
(dissoc ::snapshot)
|
||||
(assoc :modified-at modified-at)
|
||||
(assoc :has-media-trimmed false))]
|
||||
modified-at (or timestamp (ct/now))]
|
||||
|
||||
(db/update! conn :project
|
||||
{:modified-at modified-at}
|
||||
{:id (:project-id file)}
|
||||
{::db/return-keys false})
|
||||
|
||||
(bfc/update-file! cfg file)))
|
||||
|
||||
(defn- attach-snapshot
|
||||
"Attach snapshot data to the file. This should be called before the
|
||||
upcoming file operations are applied to the file."
|
||||
[file migrated? cfg]
|
||||
(let [snapshot (if migrated? file (update file :data (partial fdata/realize cfg)))]
|
||||
(assoc file ::snapshot snapshot)))
|
||||
(db/update! conn :file
|
||||
{:revn (:revn file)
|
||||
:data (:data file)
|
||||
:version (:version file)
|
||||
:features (:features file)
|
||||
:data-backend nil
|
||||
:data-ref-id nil
|
||||
:modified-at modified-at
|
||||
:has-media-trimmed false}
|
||||
{:id (:id file)}
|
||||
{::db/return-keys false})))
|
||||
|
||||
(defn- update-file-data!
|
||||
"Perform a file data transformation in with all update context setup.
|
||||
@@ -337,35 +335,52 @@
|
||||
fdata/pointer-map modified fragments."
|
||||
|
||||
[cfg {:keys [id] :as file} update-fn & args]
|
||||
(let [file (update file :data (fn [data]
|
||||
(-> data
|
||||
(blob/decode)
|
||||
(assoc :id id))))
|
||||
libs (delay (bfc/get-resolved-file-libraries cfg file))
|
||||
(binding [pmap/*tracked* (pmap/create-tracked)
|
||||
pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)]
|
||||
(let [file (update file :data (fn [data]
|
||||
(-> data
|
||||
(blob/decode)
|
||||
(assoc :id (:id file)))))
|
||||
libs (delay (bfc/get-resolved-file-libraries cfg file))
|
||||
|
||||
need-migration?
|
||||
(fmg/need-migration? file)
|
||||
;; For avoid unnecesary overhead of creating multiple pointers
|
||||
;; and handly internally with objects map in their worst
|
||||
;; case (when probably all shapes and all pointers will be
|
||||
;; readed in any case), we just realize/resolve them before
|
||||
;; applying the migration to the file
|
||||
file (if (fmg/need-migration? file)
|
||||
(-> file
|
||||
(update :data feat.fdata/process-pointers deref)
|
||||
(update :data feat.fdata/process-objects (partial into {}))
|
||||
(fmg/migrate-file libs))
|
||||
file)
|
||||
|
||||
take-snapshot?
|
||||
(take-snapshot? file)
|
||||
file (apply update-fn cfg file args)
|
||||
|
||||
;; For avoid unnecesary overhead of creating multiple
|
||||
;; pointers and handly internally with objects map in their
|
||||
;; worst case (when probably all shapes and all pointers
|
||||
;; will be readed in any case), we just realize/resolve them
|
||||
;; before applying the migration to the file
|
||||
file
|
||||
(cond-> file
|
||||
need-migration?
|
||||
(->> (fdata/realize cfg))
|
||||
;; TODO: reuse operations if file is migrated
|
||||
;; TODO: move encoding to a separated thread
|
||||
file (if (take-snapshot? file)
|
||||
(let [tpoint (ct/tpoint)
|
||||
snapshot (-> (:data file)
|
||||
(feat.fdata/process-pointers deref)
|
||||
(feat.fdata/process-objects (partial into {}))
|
||||
(blob/encode))
|
||||
elapsed (tpoint)
|
||||
label (str "internal/snapshot/" (:revn file))]
|
||||
|
||||
need-migration?
|
||||
(fmg/migrate-file libs)
|
||||
(l/trc :hint "take snapshot"
|
||||
:file-id (str (:id file))
|
||||
:revn (:revn file)
|
||||
:label label
|
||||
:elapsed (ct/format-duration elapsed))
|
||||
|
||||
take-snapshot?
|
||||
(attach-snapshot need-migration? cfg))]
|
||||
(-> file
|
||||
(assoc ::snapshot-data snapshot)
|
||||
(assoc ::snapshot-label label)))
|
||||
file)]
|
||||
|
||||
(bfc/encode-file cfg file))))
|
||||
|
||||
(apply update-fn cfg file args)))
|
||||
|
||||
(defn- soft-validate-file-schema!
|
||||
[file]
|
||||
@@ -393,7 +408,6 @@
|
||||
(not skip-validate))
|
||||
(bfc/get-resolved-file-libraries cfg file))
|
||||
|
||||
|
||||
;; The main purpose of this atom is provide a contextual state
|
||||
;; for the changes subsystem where optionally some hints can
|
||||
;; be provided for the changes processing. Right now we are
|
||||
@@ -455,9 +469,8 @@
|
||||
(defn- get-lagged-changes
|
||||
[conn {:keys [id revn] :as params}]
|
||||
(->> (db/exec! conn [sql:lagged-changes id revn])
|
||||
(filter :changes)
|
||||
(mapv (fn [row]
|
||||
(update row :changes blob/decode)))))
|
||||
(map files/decode-row)
|
||||
(vec)))
|
||||
|
||||
(defn- send-notifications!
|
||||
[cfg {:keys [team changes session-id] :as params} file]
|
||||
|
||||
@@ -37,14 +37,13 @@
|
||||
|
||||
(def ^:private
|
||||
schema:get-font-variants
|
||||
[:schema {:title "get-font-variants"}
|
||||
[:and
|
||||
[:map
|
||||
[:team-id {:optional true} ::sm/uuid]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:project-id {:optional true} ::sm/uuid]
|
||||
[:share-id {:optional true} ::sm/uuid]]
|
||||
[::sm/contains-any #{:team-id :file-id :project-id}]]])
|
||||
[:and
|
||||
[:map {:title "get-font-variants"}
|
||||
[:team-id {:optional true} ::sm/uuid]
|
||||
[:file-id {:optional true} ::sm/uuid]
|
||||
[:project-id {:optional true} ::sm/uuid]
|
||||
[:share-id {:optional true} ::sm/uuid]]
|
||||
[::sm/contains-any #{:team-id :file-id :project-id}]])
|
||||
|
||||
(sv/defmethod ::get-font-variants
|
||||
{::doc/added "1.18"
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
[:file-id ::sm/uuid]
|
||||
[:is-local ::sm/boolean]
|
||||
[:name [:string {:max 250}]]
|
||||
[:content ::media/upload]])
|
||||
[:content media/schema:upload]])
|
||||
|
||||
(sv/defmethod ::upload-file-media-object
|
||||
{::doc/added "1.17"
|
||||
|
||||
@@ -131,9 +131,7 @@
|
||||
;; NOTE: we need to retrieve the profile independently if we use
|
||||
;; it or not for explicit locking and avoid concurrent updates of
|
||||
;; the same row/object.
|
||||
(let [profile (-> (db/get-by-id conn :profile profile-id ::sql/for-update true)
|
||||
(decode-row))
|
||||
|
||||
(let [profile (get-profile conn profile-id ::db/for-update true)
|
||||
;; Update the profile map with direct params
|
||||
profile (-> profile
|
||||
(assoc :fullname fullname)
|
||||
@@ -143,9 +141,9 @@
|
||||
(db/update! conn :profile
|
||||
{:fullname fullname
|
||||
:lang lang
|
||||
:theme theme
|
||||
:props (db/tjson (:props profile))}
|
||||
{:id profile-id})
|
||||
:theme theme}
|
||||
{:id profile-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(-> profile
|
||||
(strip-private-attrs)
|
||||
@@ -228,21 +226,22 @@
|
||||
|
||||
(defn- update-notifications!
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id dashboard-comments email-comments email-invites]}]
|
||||
(let [profile (get-profile conn profile-id)
|
||||
(let [profile
|
||||
(get-profile conn profile-id ::db/for-update true)
|
||||
|
||||
notifications
|
||||
{:dashboard-comments dashboard-comments
|
||||
:email-comments email-comments
|
||||
:email-invites email-invites}]
|
||||
:email-invites email-invites}
|
||||
|
||||
(db/update!
|
||||
conn :profile
|
||||
{:props
|
||||
(-> (:props profile)
|
||||
(assoc :notifications notifications)
|
||||
(db/tjson))}
|
||||
{:id (:id profile)})
|
||||
props
|
||||
(-> (get profile :props)
|
||||
(assoc :notifications notifications))]
|
||||
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id}
|
||||
{::db/return-keys false})
|
||||
nil))
|
||||
|
||||
;; --- MUTATION: Update Photo
|
||||
@@ -253,7 +252,7 @@
|
||||
(def ^:private
|
||||
schema:update-profile-photo
|
||||
[:map {:title "update-profile-photo"}
|
||||
[:file ::media/upload]])
|
||||
[:file media/schema:upload]])
|
||||
|
||||
(sv/defmethod ::update-profile-photo
|
||||
{:doc/added "1.1"
|
||||
@@ -411,7 +410,7 @@
|
||||
|
||||
(defn update-profile-props
|
||||
[{:keys [::db/conn] :as cfg} profile-id props]
|
||||
(let [profile (get-profile conn profile-id ::sql/for-update true)
|
||||
(let [profile (get-profile conn profile-id ::db/for-update true)
|
||||
props (reduce-kv (fn [props k v]
|
||||
;; We don't accept namespaced keys
|
||||
(if (simple-ident? k)
|
||||
@@ -424,16 +423,17 @@
|
||||
|
||||
(db/update! conn :profile
|
||||
{:props (db/tjson props)}
|
||||
{:id profile-id})
|
||||
{:id profile-id}
|
||||
{::db/return-keys false})
|
||||
|
||||
(filter-props props)))
|
||||
|
||||
(sv/defmethod ::update-profile-props
|
||||
{::doc/added "1.0"
|
||||
::sm/params schema:update-profile-props}
|
||||
::sm/params schema:update-profile-props
|
||||
::db/transaction true}
|
||||
[cfg {:keys [::rpc/profile-id props]}]
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(update-profile-props cfg profile-id props))))
|
||||
(update-profile-props cfg profile-id props))
|
||||
|
||||
;; --- MUTATION: Delete Profile
|
||||
|
||||
@@ -471,6 +471,26 @@
|
||||
(-> (rph/wrap nil)
|
||||
(rph/with-transform (session/delete-fn cfg)))))
|
||||
|
||||
(def sql:get-subscription-editors
|
||||
"SELECT DISTINCT
|
||||
p.id,
|
||||
p.fullname AS name,
|
||||
p.email AS email
|
||||
FROM team_profile_rel AS tpr1
|
||||
JOIN team_profile_rel AS tpr2
|
||||
ON (tpr1.team_id = tpr2.team_id)
|
||||
JOIN profile AS p
|
||||
ON (tpr2.profile_id = p.id)
|
||||
WHERE tpr1.profile_id = ?
|
||||
AND tpr1.is_owner IS true
|
||||
AND tpr2.can_edit IS true")
|
||||
|
||||
(sv/defmethod ::get-subscription-usage
|
||||
{::doc/added "2.9"}
|
||||
[cfg {:keys [::rpc/profile-id]}]
|
||||
(let [editors (db/exec! cfg [sql:get-subscription-editors profile-id])]
|
||||
{:editors editors}))
|
||||
|
||||
;; --- HELPERS
|
||||
|
||||
(def sql:owned-teams
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
[app.common.features :as cfeat]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.team :as tt]
|
||||
[app.common.types.team :as types.team]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -629,7 +629,7 @@
|
||||
|
||||
;; assign owner role to new profile
|
||||
(db/update! conn :team-profile-rel
|
||||
(get tt/permissions-for-role :owner)
|
||||
(get types.team/permissions-for-role :owner)
|
||||
{:team-id id :profile-id reassign-to}))
|
||||
|
||||
;; and finally, if all other conditions does not match and the
|
||||
@@ -742,7 +742,7 @@
|
||||
:team-id team-id
|
||||
:role role})
|
||||
|
||||
(let [params (get tt/permissions-for-role role)]
|
||||
(let [params (get types.team/permissions-for-role role)]
|
||||
;; Only allow single owner on team
|
||||
(when (= role :owner)
|
||||
(db/update! conn :team-profile-rel
|
||||
@@ -760,7 +760,7 @@
|
||||
[:map {:title "update-team-member-role"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:member-id ::sm/uuid]
|
||||
[:role ::tt/role]])
|
||||
[:role types.team/schema:role]])
|
||||
|
||||
(sv/defmethod ::update-team-member-role
|
||||
{::doc/added "1.17"
|
||||
@@ -810,7 +810,7 @@
|
||||
(def ^:private schema:update-team-photo
|
||||
[:map {:title "update-team-photo"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:file ::media/upload]])
|
||||
[:file media/schema:upload]])
|
||||
|
||||
(sv/defmethod ::update-team-photo
|
||||
{::doc/added "1.17"
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.rpc.commands.teams-invitations
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
@@ -22,6 +21,7 @@
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
@@ -75,7 +75,7 @@
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:fullname :string]]]
|
||||
[:role ::types.team/role]
|
||||
[:role types.team/schema:role]
|
||||
[:email ::sm/email]])
|
||||
|
||||
(def ^:private check-create-invitation-params
|
||||
@@ -257,7 +257,7 @@
|
||||
(def ^:private schema:create-team-invitations
|
||||
[:map {:title "create-team-invitations"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:role ::types.team/role]
|
||||
[:role types.team/schema:role]
|
||||
[:emails [::sm/set ::sm/email]]])
|
||||
|
||||
(def ^:private max-invitations-by-request-threshold
|
||||
@@ -318,7 +318,7 @@
|
||||
[:features {:optional true} ::cfeat/features]
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:emails [::sm/set ::sm/email]]
|
||||
[:role ::types.team/role]])
|
||||
[:role types.team/schema:role]])
|
||||
|
||||
(sv/defmethod ::create-team-with-invitations
|
||||
{::doc/added "1.17"
|
||||
@@ -403,7 +403,7 @@
|
||||
[:map {:title "update-team-invitation-role"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:email ::sm/email]
|
||||
[:role ::types.team/role]])
|
||||
[:role types.team/schema:role]])
|
||||
|
||||
(sv/defmethod ::update-team-invitation-role
|
||||
{::doc/added "1.17"
|
||||
@@ -499,7 +499,7 @@
|
||||
"A specific method for obtain a file with name and page-id used for
|
||||
team request access procediment"
|
||||
[cfg file-id]
|
||||
(let [file (bfc/get-file cfg file-id :migrate? false)]
|
||||
(let [file (files/get-file cfg file-id :migrate? false)]
|
||||
(-> file
|
||||
(dissoc :data)
|
||||
(dissoc :deleted-at)
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
[:iss :keyword]
|
||||
[:exp ::ct/inst]
|
||||
[:profile-id ::sm/uuid]
|
||||
[:role ::types.team/role]
|
||||
[:role types.team/schema:role]
|
||||
[:team-id ::sm/uuid]
|
||||
[:member-email ::sm/email]
|
||||
[:member-id {:optional true} ::sm/uuid]])
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
(defn- get-view-only-bundle
|
||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id file-id ::perms] :as params}]
|
||||
(let [file (bfc/get-file cfg file-id)
|
||||
(let [file (files/get-file cfg file-id)
|
||||
|
||||
project (db/get conn :project
|
||||
{:id (:project-id file)}
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
libs (->> (bfc/get-file-libraries conn file-id)
|
||||
(mapv (fn [{:keys [id] :as lib}]
|
||||
(merge lib (bfc/get-file cfg id)))))
|
||||
(merge lib (files/get-file cfg id)))))
|
||||
|
||||
links (->> (db/query conn :share-link {:file-id file-id})
|
||||
(mapv (fn [row]
|
||||
|
||||
@@ -166,9 +166,6 @@
|
||||
:servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri))
|
||||
;; :description "penpot backend"
|
||||
}]
|
||||
:security
|
||||
{:api_key []}
|
||||
|
||||
:paths paths
|
||||
:components {:schemas @definitions}}))
|
||||
|
||||
|
||||
@@ -10,15 +10,14 @@
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.schema :as sm]))
|
||||
|
||||
(sm/register!
|
||||
^{::sm/type ::permissions}
|
||||
[:map {:title "Permissions"}
|
||||
[:type {:gen/elements [:membership :share-link]} :keyword]
|
||||
[:is-owner ::sm/boolean]
|
||||
[:is-admin ::sm/boolean]
|
||||
[:can-edit ::sm/boolean]
|
||||
[:can-read ::sm/boolean]
|
||||
[:is-logged ::sm/boolean]])
|
||||
(def schema:permissions
|
||||
[:map {:title "Permissions"}
|
||||
[:type {:gen/elements [:membership :share-link]} :keyword]
|
||||
[:is-owner ::sm/boolean]
|
||||
[:is-admin ::sm/boolean]
|
||||
[:can-edit ::sm/boolean]
|
||||
[:can-read ::sm/boolean]
|
||||
[:is-logged ::sm/boolean]])
|
||||
|
||||
(def valid-roles
|
||||
#{:admin :owner :editor :viewer})
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
[app.common.files.validate :as cfv]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.features.file-snapshots :as fsnap]
|
||||
[app.main :as main]))
|
||||
[app.main :as main]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.files-snapshot :as fsnap]))
|
||||
|
||||
(def ^:dynamic *system* nil)
|
||||
|
||||
@@ -47,7 +48,7 @@
|
||||
([system id]
|
||||
(db/run! system
|
||||
(fn [system]
|
||||
(bfc/get-file system id :decode? false)))))
|
||||
(files/get-file system id :migrate? false)))))
|
||||
|
||||
(defn update-team!
|
||||
[system {:keys [id] :as team}]
|
||||
@@ -117,10 +118,10 @@
|
||||
(let [conn (db/get-connection system)]
|
||||
(->> (get-and-lock-team-files conn team-id)
|
||||
(reduce (fn [result file-id]
|
||||
(let [file (bfc/get-file system file-id :realize? true :lock-for-update? true)]
|
||||
(fsnap/create! system file
|
||||
{:label label
|
||||
:created-by "admin"})
|
||||
(let [file (fsnap/get-file-snapshots system file-id)]
|
||||
(fsnap/create-file-snapshot! system file
|
||||
{:label label
|
||||
:created-by :admin})
|
||||
(inc result)))
|
||||
0))))
|
||||
|
||||
@@ -131,23 +132,21 @@
|
||||
(into #{}))
|
||||
|
||||
snap (search-file-snapshots conn ids label)
|
||||
|
||||
ids' (into #{} (map :file-id) snap)]
|
||||
|
||||
(when (not= ids ids')
|
||||
(throw (RuntimeException. "no uniform snapshot available")))
|
||||
|
||||
(reduce (fn [result {:keys [file-id id]}]
|
||||
(fsnap/restore! system file-id id)
|
||||
(fsnap/restore-file-snapshot! system file-id id)
|
||||
(inc result))
|
||||
0
|
||||
snap)))
|
||||
|
||||
(defn process-file!
|
||||
[system file-id update-fn & {:keys [label validate? with-libraries?] :or {validate? true} :as opts}]
|
||||
(let [file (bfc/get-file system file-id
|
||||
:lock-for-update? true
|
||||
:realize? true)
|
||||
|
||||
(let [file (bfc/get-file system file-id ::db/for-update true)
|
||||
libs (when with-libraries?
|
||||
(bfc/get-resolved-file-libraries system file))
|
||||
|
||||
@@ -164,10 +163,10 @@
|
||||
(cfv/validate-file-schema! file'))
|
||||
|
||||
(when (string? label)
|
||||
(fsnap/create! system file
|
||||
{:label label
|
||||
:deleted-at (ct/in-future {:days 30})
|
||||
:created-by "admin"}))
|
||||
(fsnap/create-file-snapshot! system file
|
||||
{:label label
|
||||
:deleted-at (ct/in-future {:days 30})
|
||||
:created-by :admin}))
|
||||
|
||||
(let [file' (update file' :revn inc)]
|
||||
(bfc/update-file! system file')
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.features.file-snapshots :as fsnap]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.main :as main]
|
||||
[app.msgbus :as mbus]
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.files-snapshot :as fsnap]
|
||||
[app.rpc.commands.management :as mgmt]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.commands.projects :as projects]
|
||||
@@ -150,15 +150,15 @@
|
||||
|
||||
(defn enable-objects-map-feature-on-file!
|
||||
[file-id & {:as opts}]
|
||||
(process-file! file-id fdata/enable-objects-map opts))
|
||||
(process-file! file-id feat.fdata/enable-objects-map opts))
|
||||
|
||||
(defn enable-pointer-map-feature-on-file!
|
||||
[file-id & {:as opts}]
|
||||
(process-file! file-id fdata/enable-pointer-map opts))
|
||||
(process-file! file-id feat.fdata/enable-pointer-map opts))
|
||||
|
||||
(defn enable-path-data-feature-on-file!
|
||||
[file-id & {:as opts}]
|
||||
(process-file! file-id fdata/enable-path-data opts))
|
||||
(process-file! file-id feat.fdata/enable-path-data opts))
|
||||
|
||||
(defn enable-storage-features-on-file!
|
||||
[file-id & {:as opts}]
|
||||
@@ -338,10 +338,7 @@
|
||||
collectable file-changes entry."
|
||||
[& {:keys [file-id label]}]
|
||||
(let [file-id (h/parse-uuid file-id)]
|
||||
(db/tx-run! main/system
|
||||
(fn [cfg]
|
||||
(let [file (bfc/get-file cfg file-id :realize? true)]
|
||||
(fsnap/create! cfg file {:label label :created-by "admin"}))))))
|
||||
(db/tx-run! main/system fsnap/create-file-snapshot! {:file-id file-id :label label})))
|
||||
|
||||
(defn restore-file-snapshot!
|
||||
[file-id & {:keys [label id]}]
|
||||
@@ -351,13 +348,13 @@
|
||||
(fn [{:keys [::db/conn] :as system}]
|
||||
(cond
|
||||
(uuid? snapshot-id)
|
||||
(fsnap/restore! system file-id snapshot-id)
|
||||
(fsnap/restore-file-snapshot! system file-id snapshot-id)
|
||||
|
||||
(string? label)
|
||||
(->> (h/search-file-snapshots conn #{file-id} label)
|
||||
(map :id)
|
||||
(first)
|
||||
(fsnap/restore! system file-id))
|
||||
(fsnap/restore-file-snapshot! system file-id))
|
||||
|
||||
:else
|
||||
(throw (ex-info "snapshot id or label should be provided" {})))))))
|
||||
@@ -366,9 +363,9 @@
|
||||
[file-id & {:as _}]
|
||||
(let [file-id (h/parse-uuid file-id)]
|
||||
(db/tx-run! main/system
|
||||
(fn [cfg]
|
||||
(->> (fsnap/get-visible-snapshots cfg file-id)
|
||||
(print-table [:label :id :revn :created-at :created-by]))))))
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(->> (fsnap/get-file-snapshots conn file-id)
|
||||
(print-table [:label :id :revn :created-at]))))))
|
||||
|
||||
(defn take-team-snapshot!
|
||||
[team-id & {:keys [label rollback?] :or {rollback? true}}]
|
||||
@@ -550,68 +547,6 @@
|
||||
:rollback rollback?
|
||||
:elapsed elapsed))))))
|
||||
|
||||
(defn process!
|
||||
"Apply a function to all files in the database"
|
||||
[& {:keys [max-jobs
|
||||
rollback?
|
||||
max-items
|
||||
chunk-size
|
||||
proc-fn]
|
||||
:or {max-items Long/MAX_VALUE
|
||||
chunk-size 100
|
||||
rollback? true}
|
||||
:as opts}]
|
||||
|
||||
(let [tpoint (ct/tpoint)
|
||||
max-jobs (or max-jobs (px/get-available-processors))
|
||||
processed (atom 0)
|
||||
opts (-> opts
|
||||
(assoc :chunk-size chunk-size)
|
||||
(dissoc :rollback?)
|
||||
(dissoc :proc-fn)
|
||||
(dissoc :max-jobs)
|
||||
(dissoc :max-items))
|
||||
|
||||
start-job
|
||||
(fn [jid]
|
||||
(l/dbg :hint "start job thread" :jid jid)
|
||||
(px/sleep 1000)
|
||||
|
||||
(loop []
|
||||
(let [result (-> main/system
|
||||
(assoc ::db/rollback rollback?)
|
||||
(proc-fn opts))]
|
||||
(let [total (swap! processed + result)]
|
||||
(l/dbg :hint "chunk processed" :jid jid :total total :chunk result ::l/sync? true)
|
||||
|
||||
(when (and (pos? result)
|
||||
(< total max-items))
|
||||
(recur))))))]
|
||||
|
||||
(l/dbg :hint "process:start"
|
||||
:rollback rollback?
|
||||
:max-jobs max-jobs
|
||||
:max-items max-items)
|
||||
|
||||
(try
|
||||
(let [jobs (->> (range max-jobs)
|
||||
(map (fn [jid] (px/fn->thread (partial start-job jid))))
|
||||
(doall))]
|
||||
(doseq [job jobs]
|
||||
(.join ^java.lang.Thread job)))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/dbg :hint "process:error" :cause cause))
|
||||
|
||||
(finally
|
||||
(let [elapsed (ct/format-duration (tpoint))]
|
||||
(l/dbg :hint "process:end"
|
||||
:processed @processed
|
||||
:rollback rollback?
|
||||
:elapsed elapsed))))))
|
||||
|
||||
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; DELETE/RESTORE OBJECTS (WITH CASCADE, SOFT)
|
||||
@@ -671,10 +606,11 @@
|
||||
(let [file-id (h/parse-uuid file-id)]
|
||||
(db/tx-run! main/system
|
||||
(fn [system]
|
||||
(when-let [file (db/get* system :file
|
||||
{:id file-id}
|
||||
{::db/remove-deleted false
|
||||
::sql/columns [:id :name]})]
|
||||
(when-let [file (some-> (db/get* system :file
|
||||
{:id file-id}
|
||||
{::db/remove-deleted false
|
||||
::sql/columns [:id :name]})
|
||||
(files/decode-row))]
|
||||
(audit/insert! system
|
||||
{::audit/name "restore-file"
|
||||
::audit/type "action"
|
||||
@@ -895,19 +831,6 @@
|
||||
(with-open [reader (io/reader path)]
|
||||
(process-data! system deleted-at (line-seq reader))))))))
|
||||
|
||||
|
||||
(defn process-chunks
|
||||
"A generic function that executes the specified proc iterativelly
|
||||
until 0 results is returned"
|
||||
[cfg proc-fn & params]
|
||||
(loop [total 0]
|
||||
(let [result (apply proc-fn cfg params)]
|
||||
(if (pos? result)
|
||||
(do
|
||||
(l/trc :hint "chunk processed" :size result :total total)
|
||||
(recur (+ total result)))
|
||||
total))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; CASCADE FIXING
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -113,10 +113,13 @@
|
||||
|
||||
(defn- create-database-object
|
||||
[{:keys [::backend ::db/connectable]} {:keys [::content ::expired-at ::touched-at ::touch] :as params}]
|
||||
(let [id (or (::id params) (uuid/random))
|
||||
(let [id (or (:id params) (uuid/random))
|
||||
mdata (cond-> (get-metadata params)
|
||||
(satisfies? impl/IContentHash content)
|
||||
(assoc :hash (impl/get-hash content)))
|
||||
(assoc :hash (impl/get-hash content))
|
||||
|
||||
:always
|
||||
(dissoc :id))
|
||||
|
||||
touched-at (if touch
|
||||
(or touched-at (ct/now))
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
(SELECT EXISTS (SELECT 1 FROM team_font_variant WHERE ttf_file_id = ?))) AS has_refs")
|
||||
|
||||
(defn- has-team-font-variant-refs?
|
||||
[conn {:keys [id]}]
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:has-team-font-variant-refs id id id id])
|
||||
(get :has-refs)))
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
(SELECT EXISTS (SELECT 1 FROM file_media_object WHERE thumbnail_id = ?))) AS has_refs")
|
||||
|
||||
(defn- has-file-media-object-refs?
|
||||
[conn {:keys [id]}]
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:has-file-media-object-refs id id])
|
||||
(get :has-refs)))
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
(SELECT EXISTS (SELECT 1 FROM team WHERE photo_id = ?))) AS has_refs")
|
||||
|
||||
(defn- has-profile-refs?
|
||||
[conn {:keys [id]}]
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:has-profile-refs id id])
|
||||
(get :has-refs)))
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"SELECT EXISTS (SELECT 1 FROM file_tagged_object_thumbnail WHERE media_id = ?) AS has_refs")
|
||||
|
||||
(defn- has-file-object-thumbnails-refs?
|
||||
[conn {:keys [id]}]
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:has-file-object-thumbnail-refs id])
|
||||
(get :has-refs)))
|
||||
|
||||
@@ -71,23 +71,36 @@
|
||||
"SELECT EXISTS (SELECT 1 FROM file_thumbnail WHERE media_id = ?) AS has_refs")
|
||||
|
||||
(defn- has-file-thumbnails-refs?
|
||||
[conn {:keys [id]}]
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:has-file-thumbnail-refs id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def sql:exists-file-data-refs
|
||||
"SELECT EXISTS (
|
||||
SELECT 1 FROM file_data
|
||||
WHERE file_id = ?
|
||||
AND id = ?
|
||||
AND metadata->>'storage-ref-id' = ?::text
|
||||
) AS has_refs")
|
||||
(def ^:private
|
||||
sql:has-file-data-refs
|
||||
"SELECT EXISTS (SELECT 1 FROM file WHERE data_ref_id = ?) AS has_refs")
|
||||
|
||||
(defn- has-file-data-refs?
|
||||
[conn sobject]
|
||||
(let [{:keys [file-id id]} (:metadata sobject)]
|
||||
(-> (db/exec-one! conn [sql:exists-file-data-refs file-id id (:id sobject)])
|
||||
(get :has-refs))))
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:has-file-data-refs id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def ^:private
|
||||
sql:has-file-data-fragment-refs
|
||||
"SELECT EXISTS (SELECT 1 FROM file_data_fragment WHERE data_ref_id = ?) AS has_refs")
|
||||
|
||||
(defn- has-file-data-fragment-refs?
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:has-file-data-fragment-refs id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def ^:private
|
||||
sql:has-file-change-refs
|
||||
"SELECT EXISTS (SELECT 1 FROM file_change WHERE data_ref_id = ?) AS has_refs")
|
||||
|
||||
(defn- has-file-change-refs?
|
||||
[conn id]
|
||||
(-> (db/exec-one! conn [sql:has-file-change-refs id])
|
||||
(get :has-refs)))
|
||||
|
||||
(def ^:private sql:mark-freeze-in-bulk
|
||||
"UPDATE storage_object
|
||||
@@ -130,50 +143,52 @@
|
||||
"file-media-object"))
|
||||
|
||||
(defn- process-objects!
|
||||
[conn has-refs? bucket objects]
|
||||
[conn has-refs? ids bucket]
|
||||
(loop [to-freeze #{}
|
||||
to-delete #{}
|
||||
objects (seq objects)]
|
||||
(if-let [{:keys [id] :as object} (first objects)]
|
||||
(if (has-refs? conn object)
|
||||
ids (seq ids)]
|
||||
(if-let [id (first ids)]
|
||||
(if (has-refs? conn id)
|
||||
(do
|
||||
(l/debug :hint "processing object"
|
||||
:id (str id)
|
||||
:status "freeze"
|
||||
:bucket bucket)
|
||||
(recur (conj to-freeze id) to-delete (rest objects)))
|
||||
(recur (conj to-freeze id) to-delete (rest ids)))
|
||||
(do
|
||||
(l/debug :hint "processing object"
|
||||
:id (str id)
|
||||
:status "delete"
|
||||
:bucket bucket)
|
||||
(recur to-freeze (conj to-delete id) (rest objects))))
|
||||
(recur to-freeze (conj to-delete id) (rest ids))))
|
||||
(do
|
||||
(some->> (seq to-freeze) (mark-freeze-in-bulk! conn))
|
||||
(some->> (seq to-delete) (mark-delete-in-bulk! conn))
|
||||
[(count to-freeze) (count to-delete)]))))
|
||||
|
||||
(defn- process-bucket!
|
||||
[conn bucket objects]
|
||||
[conn bucket ids]
|
||||
(case bucket
|
||||
"file-media-object" (process-objects! conn has-file-media-object-refs? bucket objects)
|
||||
"team-font-variant" (process-objects! conn has-team-font-variant-refs? bucket objects)
|
||||
"file-object-thumbnail" (process-objects! conn has-file-object-thumbnails-refs? bucket objects)
|
||||
"file-thumbnail" (process-objects! conn has-file-thumbnails-refs? bucket objects)
|
||||
"profile" (process-objects! conn has-profile-refs? bucket objects)
|
||||
"file-data" (process-objects! conn has-file-data-refs? bucket objects)
|
||||
"file-media-object" (process-objects! conn has-file-media-object-refs? ids bucket)
|
||||
"team-font-variant" (process-objects! conn has-team-font-variant-refs? ids bucket)
|
||||
"file-object-thumbnail" (process-objects! conn has-file-object-thumbnails-refs? ids bucket)
|
||||
"file-thumbnail" (process-objects! conn has-file-thumbnails-refs? ids bucket)
|
||||
"profile" (process-objects! conn has-profile-refs? ids bucket)
|
||||
"file-data" (process-objects! conn has-file-data-refs? ids bucket)
|
||||
"file-data-fragment" (process-objects! conn has-file-data-fragment-refs? ids bucket)
|
||||
"file-change" (process-objects! conn has-file-change-refs? ids bucket)
|
||||
(ex/raise :type :internal
|
||||
:code :unexpected-unknown-reference
|
||||
:hint (dm/fmt "unknown reference '%'" bucket))))
|
||||
|
||||
(defn process-chunk!
|
||||
[{:keys [::db/conn]} chunk]
|
||||
(reduce-kv (fn [[nfo ndo] bucket objects]
|
||||
(let [[nfo' ndo'] (process-bucket! conn bucket objects)]
|
||||
(reduce-kv (fn [[nfo ndo] bucket ids]
|
||||
(let [[nfo' ndo'] (process-bucket! conn bucket ids)]
|
||||
[(+ nfo nfo')
|
||||
(+ ndo ndo')]))
|
||||
[0 0]
|
||||
(d/group-by lookup-bucket identity #{} chunk)))
|
||||
(d/group-by lookup-bucket :id #{} chunk)))
|
||||
|
||||
(def ^:private
|
||||
sql:get-touched-storage-objects
|
||||
@@ -197,8 +212,8 @@
|
||||
deleted 0]
|
||||
(if-let [chunk (get-chunk pool)]
|
||||
(let [[nfo ndo] (db/tx-run! cfg process-chunk! chunk)]
|
||||
(recur (+ freezed nfo)
|
||||
(+ deleted ndo)))
|
||||
(recur (long (+ freezed nfo))
|
||||
(long (+ deleted ndo))))
|
||||
(do
|
||||
(l/inf :hint "task finished"
|
||||
:to-freeze freezed
|
||||
|
||||
@@ -45,11 +45,6 @@
|
||||
{:deleted-at deleted-at}
|
||||
{:file-id id})
|
||||
|
||||
;; Mark file data fragment to be deleted
|
||||
(db/update! conn :file-data-fragment
|
||||
{:deleted-at deleted-at}
|
||||
{:file-id id})
|
||||
|
||||
;; Mark file media objects to be deleted
|
||||
(db/update! conn :file-media-object
|
||||
{:deleted-at deleted-at}
|
||||
|
||||
@@ -23,13 +23,26 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.features.fdata :as feat.fdata]
|
||||
[app.features.file-snapshots :as fsnap]
|
||||
[app.storage :as sto]
|
||||
[app.worker :as wrk]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(declare get-file)
|
||||
|
||||
(def sql:get-snapshots
|
||||
"SELECT fc.file_id AS id,
|
||||
fc.id AS snapshot_id,
|
||||
fc.data,
|
||||
fc.revn,
|
||||
fc.version,
|
||||
fc.features,
|
||||
fc.data_backend,
|
||||
fc.data_ref_id
|
||||
FROM file_change AS fc
|
||||
WHERE fc.file_id = ?
|
||||
AND fc.data IS NOT NULL
|
||||
ORDER BY fc.created_at ASC")
|
||||
|
||||
(def ^:private sql:mark-file-media-object-deleted
|
||||
"UPDATE file_media_object
|
||||
SET deleted_at = now()
|
||||
@@ -44,22 +57,21 @@
|
||||
(defn- clean-file-media!
|
||||
"Performs the garbage collection of file media objects."
|
||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as file}]
|
||||
(let [used-media
|
||||
(fsnap/reduce-snapshots cfg id xf:collect-used-media conj #{})
|
||||
(let [xform (comp
|
||||
(map (partial bfc/decode-file cfg))
|
||||
xf:collect-used-media)
|
||||
|
||||
used-media
|
||||
(into used-media xf:collect-used-media [file])
|
||||
used (->> (db/plan conn [sql:get-snapshots id] {:fetch-size 1})
|
||||
(transduce xform conj #{}))
|
||||
used (into used xf:collect-used-media [file])
|
||||
|
||||
used-media
|
||||
(db/create-array conn "uuid" used-media)
|
||||
ids (db/create-array conn "uuid" used)
|
||||
unused (->> (db/exec! conn [sql:mark-file-media-object-deleted id ids])
|
||||
(into #{} (map :id)))]
|
||||
|
||||
unused-media
|
||||
(->> (db/exec! conn [sql:mark-file-media-object-deleted id used-media])
|
||||
(into #{} (map :id)))]
|
||||
(l/dbg :hint "clean" :rel "file-media-object" :file-id (str id) :total (count unused))
|
||||
|
||||
(l/dbg :hint "clean" :rel "file-media-object" :file-id (str id) :total (count unused-media))
|
||||
|
||||
(doseq [id unused-media]
|
||||
(doseq [id unused]
|
||||
(l/trc :hint "mark deleted"
|
||||
:rel "file-media-object"
|
||||
:id (str id)
|
||||
@@ -86,7 +98,7 @@
|
||||
(thc/fmt-object-id file-id page-id id "frame")
|
||||
(thc/fmt-object-id file-id page-id id "component")))))))
|
||||
|
||||
ids (db/create-array conn "uuid" using)
|
||||
ids (db/create-array conn "text" using)
|
||||
unused (->> (db/exec! conn [sql:mark-file-object-thumbnails-deleted file-id ids])
|
||||
(into #{} (map :object-id)))]
|
||||
|
||||
@@ -122,7 +134,13 @@
|
||||
file))
|
||||
|
||||
(def ^:private sql:get-files-for-library
|
||||
"SELECT f.id
|
||||
"SELECT f.id,
|
||||
f.data,
|
||||
f.modified_at,
|
||||
f.features,
|
||||
f.version,
|
||||
f.data_backend,
|
||||
f.data_ref_id
|
||||
FROM file AS f
|
||||
LEFT JOIN file_library_rel AS fl ON (fl.file_id = f.id)
|
||||
WHERE fl.library_file_id = ?
|
||||
@@ -143,21 +161,15 @@
|
||||
deleted-components
|
||||
(ctkl/deleted-components-seq data)
|
||||
|
||||
file-xform
|
||||
xform
|
||||
(mapcat (partial get-used-components deleted-components file-id))
|
||||
|
||||
library-xform
|
||||
(comp
|
||||
(map :id)
|
||||
(map #(bfc/get-file cfg % :realize? true :read-only? true))
|
||||
file-xform)
|
||||
|
||||
used-remote
|
||||
(->> (db/plan conn [sql:get-files-for-library file-id] {:fetch-size 1})
|
||||
(transduce library-xform conj #{}))
|
||||
(transduce (comp (map (partial bfc/decode-file cfg)) xform) conj #{}))
|
||||
|
||||
used-local
|
||||
(into #{} file-xform [file])
|
||||
(into #{} xform [file])
|
||||
|
||||
unused
|
||||
(transduce bfc/xf-map-id disj
|
||||
@@ -217,22 +229,34 @@
|
||||
(cfv/validate-file-schema! file)
|
||||
file))
|
||||
|
||||
(defn get-file
|
||||
[cfg {:keys [file-id revn]}]
|
||||
(let [file (bfc/get-file cfg file-id
|
||||
:realize? true
|
||||
:skip-locked? true
|
||||
:lock-for-update? true)]
|
||||
(def ^:private sql:get-file
|
||||
"SELECT f.id,
|
||||
f.data,
|
||||
f.revn,
|
||||
f.version,
|
||||
f.features,
|
||||
f.modified_at,
|
||||
f.data_backend,
|
||||
f.data_ref_id
|
||||
FROM file AS f
|
||||
WHERE f.has_media_trimmed IS false
|
||||
AND f.modified_at < now() - ?::interval
|
||||
AND f.deleted_at IS NULL
|
||||
AND f.id = ?
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
;; We should ensure that the scheduled file and the procesing file
|
||||
;; has not changed since schedule, for this reason we check the
|
||||
;; revn from props with the revn from retrieved file from database
|
||||
(when (= revn (:revn file))
|
||||
file)))
|
||||
(defn get-file
|
||||
[{:keys [::db/conn ::min-age]} file-id]
|
||||
(let [min-age (if min-age
|
||||
(db/interval min-age)
|
||||
(db/interval 0))]
|
||||
(->> (db/exec! conn [sql:get-file min-age file-id])
|
||||
(first))))
|
||||
|
||||
(defn- process-file!
|
||||
[cfg {:keys [file-id] :as props}]
|
||||
(if-let [file (get-file cfg props)]
|
||||
[cfg file-id]
|
||||
(if-let [file (get-file cfg file-id)]
|
||||
(let [file (->> file
|
||||
(bfc/decode-file cfg)
|
||||
(bfl/clean-file)
|
||||
@@ -243,7 +267,7 @@
|
||||
true)
|
||||
|
||||
(do
|
||||
(l/dbg :hint "skip cleaning, criteria does not match" :file-id (str file-id))
|
||||
(l/dbg :hint "skip" :file-id (str file-id))
|
||||
false)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -258,20 +282,26 @@
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(try
|
||||
(-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(db/tx-run! (fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [cfg (update cfg ::sto/storage sto/configure conn)
|
||||
processed? (process-file! cfg props)]
|
||||
(when (and processed? (contains? cf/flags :tiered-file-data-storage))
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :offload-file-data)
|
||||
(assoc ::wrk/params props)
|
||||
(assoc ::wrk/priority 10)
|
||||
(assoc ::wrk/delay 1000))))
|
||||
processed?))))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "error on cleaning file"
|
||||
:file-id (str (:file-id props))
|
||||
:cause cause)))))
|
||||
(let [min-age (ct/duration (or (:min-age props)
|
||||
(cf/get-deletion-delay)))
|
||||
file-id (get props :file-id)
|
||||
cfg (-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(assoc ::min-age min-age))]
|
||||
|
||||
(try
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||
(let [cfg (update cfg ::sto/storage sto/configure conn)
|
||||
processed? (process-file! cfg file-id)]
|
||||
(when (and processed? (contains? cf/flags :tiered-file-data-storage))
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :offload-file-data)
|
||||
(assoc ::wrk/params props)
|
||||
(assoc ::wrk/priority 10)
|
||||
(assoc ::wrk/delay 1000))))
|
||||
processed?)))
|
||||
|
||||
(catch Throwable cause
|
||||
(l/err :hint "error on cleaning file"
|
||||
:file-id (str (:file-id props))
|
||||
:cause cause))))))
|
||||
|
||||
@@ -17,29 +17,29 @@
|
||||
(def ^:private
|
||||
sql:get-candidates
|
||||
"SELECT f.id,
|
||||
f.revn,
|
||||
f.modified_at
|
||||
FROM file AS f
|
||||
WHERE f.has_media_trimmed IS false
|
||||
AND f.modified_at < now() - ?::interval
|
||||
AND f.deleted_at IS NULL
|
||||
ORDER BY f.modified_at DESC
|
||||
FOR UPDATE OF f
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- get-candidates
|
||||
[{:keys [::db/conn ::min-age] :as cfg}]
|
||||
(let [min-age (db/interval min-age)]
|
||||
(db/plan conn [sql:get-candidates min-age] {:fetch-size 10})))
|
||||
(db/cursor conn [sql:get-candidates min-age] {:chunk-size 10})))
|
||||
|
||||
(defn- schedule!
|
||||
[cfg]
|
||||
(let [total (reduce (fn [total {:keys [id modified-at revn]}]
|
||||
(let [params {:file-id id :modified-at modified-at :revn revn}]
|
||||
[{:keys [::min-age] :as cfg}]
|
||||
(let [total (reduce (fn [total {:keys [id]}]
|
||||
(let [params {:file-id id :min-age min-age}]
|
||||
(wrk/submit! (assoc cfg ::wrk/params params))
|
||||
(inc total)))
|
||||
0
|
||||
(get-candidates cfg))]
|
||||
|
||||
{:processed total}))
|
||||
|
||||
(defmethod ig/assert-key ::handler
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
(defmethod ig/expand-key ::handler
|
||||
[k v]
|
||||
{k (assoc v ::min-age (cf/get-file-clean-delay))})
|
||||
{k (assoc v ::min-age (cf/get-deletion-delay))})
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.storage :as sto]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
@@ -124,19 +123,17 @@
|
||||
0)))
|
||||
|
||||
(def ^:private sql:get-files
|
||||
"SELECT f.id,
|
||||
f.deleted_at,
|
||||
f.project_id
|
||||
FROM file AS f
|
||||
WHERE f.deleted_at IS NOT NULL
|
||||
AND f.deleted_at < now() + ?::interval
|
||||
ORDER BY f.deleted_at ASC
|
||||
"SELECT id, deleted_at, project_id, data_backend, data_ref_id
|
||||
FROM file
|
||||
WHERE deleted_at IS NOT NULL
|
||||
AND deleted_at < now() + ?::interval
|
||||
ORDER BY deleted_at ASC
|
||||
LIMIT ?
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- delete-files!
|
||||
[{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}]
|
||||
[{:keys [::db/conn ::sto/storage ::deletion-threshold ::chunk-size] :as cfg}]
|
||||
(->> (db/plan conn [sql:get-files deletion-threshold chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [id deleted-at project-id] :as file}]
|
||||
(l/trc :hint "permanently delete"
|
||||
@@ -145,8 +142,8 @@
|
||||
:project-id (str project-id)
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
;; Delete associated file data
|
||||
(fdata/delete! cfg {:file-id id :id id :type "main"})
|
||||
(when (= "objects-storage" (:data-backend file))
|
||||
(sto/touch-object! storage (:data-ref-id file)))
|
||||
|
||||
;; And finally, permanently delete the file.
|
||||
(db/delete! conn :file {:id id})
|
||||
@@ -212,6 +209,32 @@
|
||||
(inc total))
|
||||
0)))
|
||||
|
||||
(def ^:private sql:get-file-data-fragments
|
||||
"SELECT file_id, id, deleted_at, data_ref_id
|
||||
FROM file_data_fragment
|
||||
WHERE deleted_at IS NOT NULL
|
||||
AND deleted_at < now() + ?::interval
|
||||
ORDER BY deleted_at ASC
|
||||
LIMIT ?
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- delete-file-data-fragments!
|
||||
[{:keys [::db/conn ::sto/storage ::deletion-threshold ::chunk-size] :as cfg}]
|
||||
(->> (db/plan conn [sql:get-file-data-fragments deletion-threshold chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [file-id id deleted-at data-ref-id]}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "file-data-fragment"
|
||||
:id (str id)
|
||||
:file-id (str file-id)
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
(some->> data-ref-id (sto/touch-object! storage))
|
||||
(db/delete! conn :file-data-fragment {:file-id file-id :id id})
|
||||
|
||||
(inc total))
|
||||
0)))
|
||||
|
||||
(def ^:private sql:get-file-media-objects
|
||||
"SELECT id, file_id, media_id, thumbnail_id, deleted_at
|
||||
FROM file_media_object
|
||||
@@ -241,35 +264,8 @@
|
||||
(inc total))
|
||||
0)))
|
||||
|
||||
(def ^:private sql:get-file-data-fragments
|
||||
"SELECT file_id, id, deleted_at
|
||||
FROM file_data_fragment
|
||||
WHERE deleted_at IS NOT NULL
|
||||
AND deleted_at < now() + ?::interval
|
||||
ORDER BY deleted_at ASC
|
||||
LIMIT ?
|
||||
FOR UPDATE
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- delete-file-data-fragments!
|
||||
[{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}]
|
||||
(->> (db/plan conn [sql:get-file-data-fragments deletion-threshold chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [file-id id deleted-at]}]
|
||||
(l/trc :hint "permanently delete"
|
||||
:rel "file-data-fragment"
|
||||
:id (str id)
|
||||
:file-id (str file-id)
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
;; Delete associated file data
|
||||
(fdata/delete! cfg {:file-id file-id :id id :type "fragment"})
|
||||
(db/delete! conn :file-data-fragment {:file-id file-id :id id})
|
||||
|
||||
(inc total))
|
||||
0)))
|
||||
|
||||
(def ^:private sql:get-file-change
|
||||
"SELECT id, file_id, deleted_at
|
||||
"SELECT id, file_id, deleted_at, data_backend, data_ref_id
|
||||
FROM file_change
|
||||
WHERE deleted_at IS NOT NULL
|
||||
AND deleted_at < now() + ?::interval
|
||||
@@ -279,7 +275,7 @@
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- delete-file-changes!
|
||||
[{:keys [::db/conn ::deletion-threshold ::chunk-size] :as cfg}]
|
||||
[{:keys [::db/conn ::deletion-threshold ::chunk-size ::sto/storage] :as cfg}]
|
||||
(->> (db/plan conn [sql:get-file-change deletion-threshold chunk-size] {:fetch-size 5})
|
||||
(reduce (fn [total {:keys [id file-id deleted-at] :as xlog}]
|
||||
(l/trc :hint "permanently delete"
|
||||
@@ -288,8 +284,8 @@
|
||||
:file-id (str file-id)
|
||||
:deleted-at (ct/format-inst deleted-at))
|
||||
|
||||
;; Delete associated file data, if it exists
|
||||
(fdata/delete! cfg {:file-id file-id :id id :type "snapshot"})
|
||||
(when (= "objects-storage" (:data-backend xlog))
|
||||
(sto/touch-object! storage (:data-ref-id xlog)))
|
||||
|
||||
(db/delete! conn :file-change {:id id})
|
||||
|
||||
@@ -299,10 +295,10 @@
|
||||
(def ^:private deletion-proc-vars
|
||||
[#'delete-profiles!
|
||||
#'delete-file-media-objects!
|
||||
#'delete-file-data-fragments!
|
||||
#'delete-file-object-thumbnails!
|
||||
#'delete-file-thumbnails!
|
||||
#'delete-file-changes!
|
||||
#'delete-file-data-fragments!
|
||||
#'delete-files!
|
||||
#'delete-projects!
|
||||
#'delete-fonts!
|
||||
@@ -317,7 +313,7 @@
|
||||
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
|
||||
(proc-fn cfg)))]
|
||||
(if (pos? result)
|
||||
(recur (+ total result))
|
||||
(recur (long (+ total result)))
|
||||
total))))
|
||||
|
||||
(defmethod ig/assert-key ::handler
|
||||
@@ -339,7 +335,7 @@
|
||||
(if-let [proc-fn (first procs)]
|
||||
(let [result (execute-proc! cfg proc-fn)]
|
||||
(recur (rest procs)
|
||||
(+ total result)))
|
||||
(long (+ total result))))
|
||||
(do
|
||||
(l/inf :hint "task finished" :deleted total)
|
||||
{:processed total}))))))
|
||||
|
||||
@@ -8,73 +8,101 @@
|
||||
"A maintenance task responsible of moving file data from hot
|
||||
storage (the database row) to a cold storage (fs or s3)."
|
||||
(:require
|
||||
[app.binfile.common :as bfc]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.db :as db]
|
||||
[app.features.fdata :as fdata]
|
||||
[app.features.file-snapshots :as fsnap]
|
||||
[app.db.sql :as-alias sql]
|
||||
[app.storage :as sto]
|
||||
[app.util.blob :as blob]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defn- offload-file-data
|
||||
[{:keys [::db/conn ::file-id] :as cfg}]
|
||||
(let [file (bfc/get-file cfg file-id :realize? true :lock-for-update? true)]
|
||||
(cond
|
||||
(not= "db" (:backend file))
|
||||
(l/wrn :hint (str "skiping file offload (file offloaded or incompatible with offloading) for " file-id)
|
||||
:file-id (str file-id))
|
||||
(defn- offload-file-data!
|
||||
[{:keys [::db/conn ::sto/storage ::file-id] :as cfg}]
|
||||
(let [file (db/get conn :file {:id file-id}
|
||||
{::sql/for-update true})]
|
||||
(when (nil? (:data file))
|
||||
(ex/raise :hint "file already offloaded"
|
||||
:type :internal
|
||||
:code :file-already-offloaded
|
||||
:file-id file-id))
|
||||
|
||||
(nil? (:data file))
|
||||
(l/err :hint (str "skiping file offload (missing data) for " file-id)
|
||||
:file-id (str file-id))
|
||||
(let [data (sto/content (:data file))
|
||||
sobj (sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/touch true
|
||||
:bucket "file-data"
|
||||
:content-type "application/octet-stream"
|
||||
:file-id file-id})]
|
||||
|
||||
:else
|
||||
(do
|
||||
(fdata/update! cfg {:id file-id
|
||||
:file-id file-id
|
||||
:type "main"
|
||||
:backend "storage"
|
||||
:data (blob/encode (:data file))})
|
||||
(l/trc :hint "offload file data"
|
||||
:file-id (str file-id)
|
||||
:storage-id (str (:id sobj)))
|
||||
|
||||
(db/update! conn :file
|
||||
{:data nil}
|
||||
{:id file-id}
|
||||
{::db/return-keys false})
|
||||
(db/update! conn :file
|
||||
{:data-backend "objects-storage"
|
||||
:data-ref-id (:id sobj)
|
||||
:data nil}
|
||||
{:id file-id}
|
||||
{::db/return-keys false}))))
|
||||
|
||||
(l/trc :hint "offload file data"
|
||||
:file-id (str file-id))))))
|
||||
(defn- offload-file-data-fragments!
|
||||
[{:keys [::db/conn ::sto/storage ::file-id] :as cfg}]
|
||||
(doseq [fragment (db/query conn :file-data-fragment
|
||||
{:file-id file-id
|
||||
:deleted-at nil
|
||||
:data-backend nil}
|
||||
{::db/for-update true})]
|
||||
(let [data (sto/content (:data fragment))
|
||||
sobj (sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/touch true
|
||||
:bucket "file-data-fragment"
|
||||
:content-type "application/octet-stream"
|
||||
:file-id file-id
|
||||
:file-fragment-id (:id fragment)})]
|
||||
|
||||
(l/trc :hint "offload file data fragment"
|
||||
:file-id (str file-id)
|
||||
:file-fragment-id (str (:id fragment))
|
||||
:storage-id (str (:id sobj)))
|
||||
|
||||
(db/update! conn :file-data-fragment
|
||||
{:data-backend "objects-storage"
|
||||
:data-ref-id (:id sobj)
|
||||
:data nil}
|
||||
{:id (:id fragment)}
|
||||
{::db/return-keys false}))))
|
||||
|
||||
(def sql:get-snapshots
|
||||
(str "WITH snapshots AS (" fsnap/sql:snapshots ")"
|
||||
"SELECT s.*
|
||||
FROM snapshots AS s
|
||||
WHERE s.backend = 'db'
|
||||
AND s.file_id = ?
|
||||
ORDER BY s.created_at"))
|
||||
"SELECT fc.*
|
||||
FROM file_change AS fc
|
||||
WHERE fc.file_id = ?
|
||||
AND fc.label IS NOT NULL
|
||||
AND fc.data IS NOT NULL
|
||||
AND fc.data_backend IS NULL")
|
||||
|
||||
(defn- offload-snapshot-data
|
||||
[{:keys [::db/conn ::file-id] :as cfg} snapshot]
|
||||
(let [{:keys [id data] :as snapshot} (fdata/resolve-file-data cfg snapshot)]
|
||||
(if (nil? (:data snapshot))
|
||||
(l/err :hint (str "skiping snapshot offload (missing data) for " file-id)
|
||||
(defn- offload-file-snapshots!
|
||||
[{:keys [::db/conn ::sto/storage ::file-id] :as cfg}]
|
||||
(doseq [snapshot (db/exec! conn [sql:get-snapshots file-id])]
|
||||
(let [data (sto/content (:data snapshot))
|
||||
sobj (sto/put-object! storage
|
||||
{::sto/content data
|
||||
::sto/touch true
|
||||
:bucket "file-change"
|
||||
:content-type "application/octet-stream"
|
||||
:file-id file-id
|
||||
:file-change-id (:id snapshot)})]
|
||||
|
||||
(l/trc :hint "offload file change"
|
||||
:file-id (str file-id)
|
||||
:snapshot-id id)
|
||||
(do
|
||||
(fsnap/create! cfg {:id id
|
||||
:file-id file-id
|
||||
:type "snapshot"
|
||||
:backend "storage"
|
||||
:data data})
|
||||
:file-change-id (str (:id snapshot))
|
||||
:storage-id (str (:id sobj)))
|
||||
|
||||
(l/trc :hint "offload snapshot data"
|
||||
:file-id (str file-id)
|
||||
:snapshot-id (str id))
|
||||
|
||||
(db/update! conn :file-change
|
||||
{:data nil}
|
||||
{:id id :file-id file-id}
|
||||
{::db/return-keys false})))))
|
||||
(db/update! conn :file-change
|
||||
{:data-backend "objects-storage"
|
||||
:data-ref-id (:id sobj)
|
||||
:data nil}
|
||||
{:id (:id snapshot)}
|
||||
{::db/return-keys false}))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HANDLER
|
||||
@@ -88,12 +116,10 @@
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [file-id (:file-id props)]
|
||||
(-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(assoc ::file-id (:file-id props))
|
||||
(db/tx-run! (fn [{:keys [::db/conn] :as cfg}]
|
||||
(offload-file-data cfg)
|
||||
|
||||
(run! (partial offload-snapshot-data cfg)
|
||||
(db/plan conn [sql:get-snapshots file-id]))))))))
|
||||
(-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(assoc ::file-id (:file-id props))
|
||||
(db/tx-run! (fn [cfg]
|
||||
(offload-file-data! cfg)
|
||||
(offload-file-data-fragments! cfg)
|
||||
(offload-file-snapshots! cfg))))))
|
||||
|
||||
96
backend/test/backend_tests/http_management_test.clj
Normal file
96
backend/test/backend_tests/http_management_test.clj
Normal file
@@ -0,0 +1,96 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns backend-tests.http-management-test
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.http.access-token]
|
||||
[app.http.management :as mgmt]
|
||||
[app.http.session :as sess]
|
||||
[app.main :as-alias main]
|
||||
[app.rpc :as-alias rpc]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[mockery.core :refer [with-mocks]]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
|
||||
(t/deftest authenticate-method
|
||||
(let [profile (th/create-profile* 1)
|
||||
props (get th/*system* :app.setup/props)
|
||||
token (#'sess/gen-token props {:profile-id (:id profile)})
|
||||
request {:params {:token token}}
|
||||
response (#'mgmt/authenticate th/*system* request)]
|
||||
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (= "authentication" (-> response ::yres/body :iss)))
|
||||
(t/is (= (:id profile) (-> response ::yres/body :uid)))))
|
||||
|
||||
(t/deftest get-customer-method
|
||||
(let [profile (th/create-profile* 1)
|
||||
request {:params {:id (:id profile)}}
|
||||
response (#'mgmt/get-customer th/*system* request)]
|
||||
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (= (:id profile) (-> response ::yres/body :id)))
|
||||
(t/is (= (:fullname profile) (-> response ::yres/body :name)))
|
||||
(t/is (= (:email profile) (-> response ::yres/body :email)))
|
||||
(t/is (= 1 (-> response ::yres/body :num-editors)))
|
||||
(t/is (nil? (-> response ::yres/body :subscription)))))
|
||||
|
||||
(t/deftest update-customer-method
|
||||
(let [profile (th/create-profile* 1)
|
||||
|
||||
subs {:type "unlimited"
|
||||
:description nil
|
||||
:id "foobar"
|
||||
:customer-id (str (:id profile))
|
||||
:status "past_due"
|
||||
:billing-period "week"
|
||||
:quantity 1
|
||||
:created-at (ct/truncate (ct/now) :day)
|
||||
:cancel-at-period-end true
|
||||
:start-date nil
|
||||
:ended-at nil
|
||||
:trial-end nil
|
||||
:trial-start nil
|
||||
:cancel-at nil
|
||||
:canceled-at nil
|
||||
:current-period-end nil
|
||||
:current-period-start nil
|
||||
|
||||
:cancellation-details
|
||||
{:comment "other"
|
||||
:reason "other"
|
||||
:feedback "other"}}
|
||||
|
||||
request {:params {:id (:id profile)
|
||||
:subscription subs}}
|
||||
response (#'mgmt/update-customer th/*system* request)]
|
||||
|
||||
(t/is (= 201 (::yres/status response)))
|
||||
(t/is (nil? (::yres/body response)))
|
||||
|
||||
(let [request {:params {:id (:id profile)}}
|
||||
response (#'mgmt/get-customer th/*system* request)]
|
||||
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (= (:id profile) (-> response ::yres/body :id)))
|
||||
(t/is (= (:fullname profile) (-> response ::yres/body :name)))
|
||||
(t/is (= (:email profile) (-> response ::yres/body :email)))
|
||||
(t/is (= 1 (-> response ::yres/body :num-editors)))
|
||||
|
||||
(let [subs' (-> response ::yres/body :subscription)]
|
||||
(t/is (= subs' subs))))))
|
||||
|
||||
|
||||
|
||||
|
||||
36
backend/test/backend_tests/rpc_doc_test.clj
Normal file
36
backend/test/backend_tests/rpc_doc_test.clj
Normal file
@@ -0,0 +1,36 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns backend-tests.rpc-doc-test
|
||||
"Internal binfile test, no RPC involved"
|
||||
(:require
|
||||
[app.common.json :as json]
|
||||
[app.common.pprint :as pp]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.test :as smt]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.doc :as rpc.doc]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
|
||||
(t/deftest openapi-context-json-encode
|
||||
(smt/check!
|
||||
(smt/for [context (->> sg/int
|
||||
(sg/fmap (fn [_]
|
||||
(rpc.doc/prepare-openapi-context (::rpc/methods th/*system*)))))]
|
||||
(try
|
||||
(json/encode context)
|
||||
true
|
||||
(catch Throwable _cause
|
||||
false)))
|
||||
{:num 30}))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
(t/deftest internal-encode-decode
|
||||
(smt/check!
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator cts/schema:shape))
|
||||
(cg/not-empty))]
|
||||
(let [obj1 (omap/wrap data)
|
||||
obj2 (omap/create (deref obj1))
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
(t/deftest fressian-encode-decode
|
||||
(smt/check!
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator cts/schema:shape))
|
||||
(cg/not-empty)
|
||||
(cg/fmap omap/wrap)
|
||||
(cg/fmap (fn [o] {:objects o})))]
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
(t/deftest transit-encode-decode
|
||||
(smt/check!
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator ::cts/shape))
|
||||
(smt/for [data (->> (cg/map cg/uuid (sg/generator cts/schema:shape))
|
||||
(cg/not-empty)
|
||||
(cg/fmap omap/wrap)
|
||||
(cg/fmap (fn [o] {:objects o})))]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{:deps
|
||||
{org.clojure/clojure {:mvn/version "1.12.1"}
|
||||
{org.clojure/clojure {:mvn/version "1.12.2"}
|
||||
org.clojure/data.json {:mvn/version "2.5.1"}
|
||||
org.clojure/tools.cli {:mvn/version "1.1.230"}
|
||||
org.clojure/test.check {:mvn/version "1.1.1"}
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
frankiesardo/linked {:mvn/version "1.3.0"}
|
||||
|
||||
com.sun.mail/jakarta.mail {:mvn/version "2.0.1"}
|
||||
com.sun.mail/jakarta.mail {:mvn/version "2.0.2"}
|
||||
org.la4j/la4j {:mvn/version "0.6.0"}
|
||||
|
||||
;; exception printing
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.buffer
|
||||
"A collection of helpers and macros for work with byte buffers"
|
||||
"A collection of helpers and macros for work with byte
|
||||
buffer (ByteBuffer on JVM and DataView on JS)."
|
||||
(:refer-clojure :exclude [clone])
|
||||
(:require
|
||||
[app.common.uuid :as uuid])
|
||||
@@ -19,42 +20,42 @@
|
||||
(if (:ns &env)
|
||||
`(.getInt8 ~target ~offset true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(long (.get ~target ~offset)))))
|
||||
`(long (.get ~target (unchecked-int ~offset))))))
|
||||
|
||||
(defmacro read-unsigned-byte
|
||||
[target offset]
|
||||
(if (:ns &env)
|
||||
`(.getUint8 ~target ~offset true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(bit-and (long (.get ~target ~offset)) 0xff))))
|
||||
`(bit-and (long (.get ~target (unchecked-int ~offset))) 0xff))))
|
||||
|
||||
(defmacro read-bool
|
||||
[target offset]
|
||||
(if (:ns &env)
|
||||
`(== 1 (.getInt8 ~target ~offset true))
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(== 1 (.get ~target ~offset)))))
|
||||
`(== 1 (.get ~target (unchecked-int ~offset))))))
|
||||
|
||||
(defmacro read-short
|
||||
[target offset]
|
||||
(if (:ns &env)
|
||||
`(.getInt16 ~target ~offset true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(.getShort ~target ~offset))))
|
||||
`(.getShort ~target (unchecked-int ~offset)))))
|
||||
|
||||
(defmacro read-int
|
||||
[target offset]
|
||||
(if (:ns &env)
|
||||
`(.getInt32 ~target ~offset true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(long (.getInt ~target ~offset)))))
|
||||
`(long (.getInt ~target (unchecked-int ~offset))))))
|
||||
|
||||
(defmacro read-float
|
||||
[target offset]
|
||||
(if (:ns &env)
|
||||
`(.getFloat32 ~target ~offset true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(double (.getFloat ~target ~offset)))))
|
||||
`(double (.getFloat ~target (unchecked-int ~offset))))))
|
||||
|
||||
(defmacro read-uuid
|
||||
[target offset]
|
||||
@@ -68,8 +69,8 @@
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(try
|
||||
(.order ~target ByteOrder/BIG_ENDIAN)
|
||||
(let [msb# (.getLong ~target (+ ~offset 0))
|
||||
lsb# (.getLong ~target (+ ~offset 8))]
|
||||
(let [msb# (.getLong ~target (unchecked-int (+ ~offset 0)))
|
||||
lsb# (.getLong ~target (unchecked-int (+ ~offset 8)))]
|
||||
(java.util.UUID. (long msb#) (long lsb#)))
|
||||
(finally
|
||||
(.order ~target ByteOrder/LITTLE_ENDIAN))))))
|
||||
@@ -78,6 +79,13 @@
|
||||
[target offset value]
|
||||
(if (:ns &env)
|
||||
`(.setInt8 ~target ~offset ~value true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(.put ~target (unchecked-int ~offset) (unchecked-byte ~value)))))
|
||||
|
||||
(defmacro write-u8
|
||||
[target offset value]
|
||||
(if (:ns &env)
|
||||
`(.setUint8 ~target ~offset ~value true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(.put ~target ~offset (unchecked-byte ~value)))))
|
||||
|
||||
@@ -86,28 +94,45 @@
|
||||
(if (:ns &env)
|
||||
`(.setInt8 ~target ~offset (if ~value 0x01 0x00) true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(.put ~target ~offset (unchecked-byte (if ~value 0x01 0x00))))))
|
||||
`(.put ~target (unchecked-int ~offset) (unchecked-byte (if ~value 0x01 0x00))))))
|
||||
|
||||
(defmacro write-short
|
||||
[target offset value]
|
||||
(if (:ns &env)
|
||||
`(.setInt16 ~target ~offset ~value true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(.putShort ~target ~offset (unchecked-short ~value)))))
|
||||
`(.putShort ~target (unchecked-int ~offset) (unchecked-short ~value)))))
|
||||
|
||||
(defmacro write-int
|
||||
[target offset value]
|
||||
(if (:ns &env)
|
||||
`(.setInt32 ~target ~offset ~value true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(.putInt ~target (unchecked-int ~offset) (unchecked-int ~value)))))
|
||||
|
||||
(defmacro write-u32
|
||||
[target offset value]
|
||||
(if (:ns &env)
|
||||
`(.setUint32 ~target ~offset ~value true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(.putInt ~target ~offset (unchecked-int ~value)))))
|
||||
|
||||
(defmacro write-i32
|
||||
"Idiomatic alias for `write-int`"
|
||||
[target offset value]
|
||||
`(write-int ~target ~offset ~value))
|
||||
|
||||
(defmacro write-float
|
||||
[target offset value]
|
||||
(if (:ns &env)
|
||||
`(.setFloat32 ~target ~offset ~value true)
|
||||
(let [target (with-meta target {:tag 'java.nio.ByteBuffer})]
|
||||
`(.putFloat ~target ~offset (unchecked-float ~value)))))
|
||||
`(.putFloat ~target (unchecked-int ~offset) (unchecked-float ~value)))))
|
||||
|
||||
(defmacro write-f32
|
||||
"Idiomatic alias for `write-float`."
|
||||
[target offset value]
|
||||
`(write-float ~target ~offset ~value))
|
||||
|
||||
(defmacro write-uuid
|
||||
[target offset value]
|
||||
@@ -122,8 +147,8 @@
|
||||
value (with-meta value {:tag 'java.util.UUID})]
|
||||
`(try
|
||||
(.order ~target ByteOrder/BIG_ENDIAN)
|
||||
(.putLong ~target (+ ~offset 0) (.getMostSignificantBits ~value))
|
||||
(.putLong ~target (+ ~offset 8) (.getLeastSignificantBits ~value))
|
||||
(.putLong ~target (unchecked-int (+ ~offset 0)) (.getMostSignificantBits ~value))
|
||||
(.putLong ~target (unchecked-int (+ ~offset 8)) (.getLeastSignificantBits ~value))
|
||||
(finally
|
||||
(.order ~target ByteOrder/LITTLE_ENDIAN))))))
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
"layout/grid"
|
||||
"components/v2"
|
||||
"plugins/runtime"
|
||||
"design-tokens/v1"})
|
||||
"design-tokens/v1"
|
||||
"variants/v1"})
|
||||
|
||||
;; A set of features that should not be propagated to team on creating
|
||||
;; or modifying a file
|
||||
@@ -95,18 +96,18 @@
|
||||
(-> #{"layout/grid"
|
||||
"design-tokens/v1"
|
||||
"fdata/shape-data-type"
|
||||
"fdata/path-data"}
|
||||
"fdata/path-data"
|
||||
"variants/v1"}
|
||||
(into frontend-only-features)
|
||||
(into backend-only-features)))
|
||||
|
||||
(def schema:features
|
||||
(sm/register!
|
||||
^{::sm/type ::features}
|
||||
[:schema
|
||||
{:title "FileFeatures"
|
||||
::smdj/inline true
|
||||
:gen/gen (smg/subseq supported-features)}
|
||||
[::sm/set :string]]))
|
||||
(sm/register!
|
||||
^{::sm/type ::features}
|
||||
[:schema
|
||||
{:title "FileFeatures"
|
||||
::smdj/inline true
|
||||
:gen/gen (smg/subseq supported-features)}
|
||||
[::sm/set :string]])
|
||||
|
||||
(defn- flag->feature
|
||||
"Translate a flag to a feature name"
|
||||
|
||||
@@ -83,24 +83,25 @@
|
||||
|
||||
[:multi {:decode/json #(update % :grid-type keyword)
|
||||
:gen/gen gen
|
||||
:title "SetDefaultGridChange"
|
||||
:dispatch :grid-type
|
||||
::smd/simplified true}
|
||||
[:square
|
||||
[:map
|
||||
[:map {:title "SetDefautSquareGridAttrs"}
|
||||
[:type [:= :set-default-grid]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:grid-type [:= :square]]
|
||||
[:params [:maybe ctg/schema:square-params]]]]
|
||||
|
||||
[:column
|
||||
[:map
|
||||
[:map {:title "SetDefaultColumnGridAttrs"}
|
||||
[:type [:= :set-default-grid]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:grid-type [:= :column]]
|
||||
[:params [:maybe ctg/schema:column-params]]]]
|
||||
|
||||
[:row
|
||||
[:map
|
||||
[:map {:title "SetDefaultRowGridAttrs"}
|
||||
[:type [:= :set-default-grid]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:grid-type [:= :row]]
|
||||
@@ -111,20 +112,20 @@
|
||||
[:type [:= :set-guide]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:params [:maybe ::ctp/guide]]]
|
||||
[:params [:maybe ctp/schema:guide]]]
|
||||
gen (->> (sg/generator schema)
|
||||
(sg/fmap (fn [change]
|
||||
(if (some? (:params change))
|
||||
(update change :params assoc :id (:id change))
|
||||
change))))]
|
||||
[:schema {:gen/gen gen} schema]))
|
||||
(sm/update-properties schema assoc :gen/gen gen)))
|
||||
|
||||
(def schema:set-flow-change
|
||||
(let [schema [:map {:title "SetFlowChange"}
|
||||
[:type [:= :set-flow]]
|
||||
[:page-id ::sm/uuid]
|
||||
[:id ::sm/uuid]
|
||||
[:params [:maybe ::ctp/flow]]]
|
||||
[:params [:maybe ctp/schema:flow]]]
|
||||
|
||||
gen (->> (sg/generator schema)
|
||||
(sg/fmap (fn [change]
|
||||
@@ -132,7 +133,7 @@
|
||||
(update change :params assoc :id (:id change))
|
||||
change))))]
|
||||
|
||||
[:schema {:gen/gen gen} schema]))
|
||||
(sm/update-properties schema assoc :gen/gen gen)))
|
||||
|
||||
(def schema:set-plugin-data-change
|
||||
(let [types #{:file :page :shape :color :typography :component}
|
||||
@@ -169,287 +170,274 @@
|
||||
|
||||
:else
|
||||
(dissoc change :page-id)))))]
|
||||
|
||||
[:and {:gen/gen gen} schema check1]))
|
||||
[:and (sm/update-properties schema assoc :gen/gen gen) check1]))
|
||||
|
||||
(def schema:change
|
||||
[:schema
|
||||
[:multi {:dispatch :type
|
||||
:title "Change"
|
||||
:decode/json #(update % :type keyword)
|
||||
::smd/simplified true}
|
||||
[:set-option
|
||||
[:multi {:dispatch :type
|
||||
:title "Change"
|
||||
:decode/json #(update % :type keyword)
|
||||
::smd/simplified true}
|
||||
|
||||
;; DEPRECATED: remove before 2.3 release
|
||||
;;
|
||||
;; Is still there for not cause error when event is received
|
||||
[:map {:title "SetOptionChange"}]]
|
||||
[:set-comment-thread-position
|
||||
[:map {:title "SetCommentThreadPositionChange"}
|
||||
[:comment-thread-id ::sm/uuid]
|
||||
[:page-id ::sm/uuid]
|
||||
[:frame-id [:maybe ::sm/uuid]]
|
||||
[:position [:maybe ::gpt/point]]]]
|
||||
|
||||
[:set-comment-thread-position
|
||||
[:map
|
||||
[:comment-thread-id ::sm/uuid]
|
||||
[:page-id ::sm/uuid]
|
||||
[:frame-id [:maybe ::sm/uuid]]
|
||||
[:position [:maybe ::gpt/point]]]]
|
||||
[:add-obj
|
||||
[:map {:title "AddObjChange"}
|
||||
[:type [:= :add-obj]]
|
||||
[:id ::sm/uuid]
|
||||
[:obj cts/schema:shape]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:frame-id ::sm/uuid]
|
||||
[:parent-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:index {:optional true} [:maybe :int]]
|
||||
[:ignore-touched {:optional true} :boolean]]]
|
||||
|
||||
[:add-obj
|
||||
[:map {:title "AddObjChange"}
|
||||
[:type [:= :add-obj]]
|
||||
[:id ::sm/uuid]
|
||||
[:obj :map]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:frame-id ::sm/uuid]
|
||||
[:parent-id {:optional true} [:maybe ::sm/uuid]]
|
||||
[:index {:optional true} [:maybe :int]]
|
||||
[:ignore-touched {:optional true} :boolean]]]
|
||||
[:mod-obj
|
||||
[:map {:title "ModObjChange"}
|
||||
[:type [:= :mod-obj]]
|
||||
[:id ::sm/uuid]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:operations [:vector {:gen/max 5} schema:operation]]]]
|
||||
|
||||
[:mod-obj
|
||||
[:map {:title "ModObjChange"}
|
||||
[:type [:= :mod-obj]]
|
||||
[:id ::sm/uuid]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:operations [:vector {:gen/max 5} schema:operation]]]]
|
||||
[:del-obj
|
||||
[:map {:title "DelObjChange"}
|
||||
[:type [:= :del-obj]]
|
||||
[:id ::sm/uuid]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:ignore-touched {:optional true} :boolean]]]
|
||||
|
||||
[:del-obj
|
||||
[:map {:title "DelObjChange"}
|
||||
[:type [:= :del-obj]]
|
||||
[:id ::sm/uuid]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:ignore-touched {:optional true} :boolean]]]
|
||||
[:set-guide schema:set-guide-change]
|
||||
[:set-flow schema:set-flow-change]
|
||||
[:set-default-grid schema:set-default-grid-change]
|
||||
|
||||
[:set-guide schema:set-guide-change]
|
||||
[:set-flow schema:set-flow-change]
|
||||
[:set-default-grid schema:set-default-grid-change]
|
||||
[:fix-obj
|
||||
[:map {:title "FixObjChange"}
|
||||
[:type [:= :fix-obj]]
|
||||
[:id ::sm/uuid]
|
||||
[:fix {:optional true} :keyword]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]]]
|
||||
|
||||
[:fix-obj
|
||||
[:map {:title "FixObjChange"}
|
||||
[:type [:= :fix-obj]]
|
||||
[:id ::sm/uuid]
|
||||
[:fix {:optional true} :keyword]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]]]
|
||||
[:mov-objects
|
||||
[:map {:title "MovObjectsChange"}
|
||||
[:type [:= :mov-objects]]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:ignore-touched {:optional true} :boolean]
|
||||
[:parent-id ::sm/uuid]
|
||||
[:shapes ::sm/any]
|
||||
[:index {:optional true} [:maybe :int]]
|
||||
[:after-shape {:optional true} ::sm/any]
|
||||
[:allow-altering-copies {:optional true} :boolean]]]
|
||||
|
||||
[:mov-objects
|
||||
[:map {:title "MovObjectsChange"}
|
||||
[:type [:= :mov-objects]]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:ignore-touched {:optional true} :boolean]
|
||||
[:parent-id ::sm/uuid]
|
||||
[:shapes ::sm/any]
|
||||
[:index {:optional true} [:maybe :int]]
|
||||
[:after-shape {:optional true} ::sm/any]
|
||||
[:allow-altering-copies {:optional true} :boolean]]]
|
||||
[:reorder-children
|
||||
[:map {:title "ReorderChildrenChange"}
|
||||
[:type [:= :reorder-children]]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:ignore-touched {:optional true} :boolean]
|
||||
[:parent-id ::sm/uuid]
|
||||
[:shapes ::sm/any]]]
|
||||
|
||||
[:reorder-children
|
||||
[:map {:title "ReorderChildrenChange"}
|
||||
[:type [:= :reorder-children]]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:ignore-touched {:optional true} :boolean]
|
||||
[:parent-id ::sm/uuid]
|
||||
[:shapes ::sm/any]]]
|
||||
[:add-page
|
||||
[:map {:title "AddPageChange"}
|
||||
[:type [:= :add-page]]
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:name {:optional true} :string]
|
||||
[:page {:optional true} ::sm/any]]]
|
||||
|
||||
[:add-page
|
||||
[:map {:title "AddPageChange"}
|
||||
[:type [:= :add-page]]
|
||||
[:id {:optional true} ::sm/uuid]
|
||||
[:name {:optional true} :string]
|
||||
[:page {:optional true} ::sm/any]]]
|
||||
[:mod-page
|
||||
[:map {:title "ModPageChange"}
|
||||
[:type [:= :mod-page]]
|
||||
[:id ::sm/uuid]
|
||||
;; All props are optional, background can be nil because is the
|
||||
;; way to remove already set background
|
||||
[:background {:optional true} [:maybe ctc/schema:hex-color]]
|
||||
[:name {:optional true} :string]]]
|
||||
|
||||
[:mod-page
|
||||
[:map {:title "ModPageChange"}
|
||||
[:type [:= :mod-page]]
|
||||
[:id ::sm/uuid]
|
||||
;; All props are optional, background can be nil because is the
|
||||
;; way to remove already set background
|
||||
[:background {:optional true} [:maybe ctc/schema:hex-color]]
|
||||
[:name {:optional true} :string]]]
|
||||
[:set-plugin-data schema:set-plugin-data-change]
|
||||
|
||||
[:set-plugin-data schema:set-plugin-data-change]
|
||||
[:del-page
|
||||
[:map {:title "DelPageChange"}
|
||||
[:type [:= :del-page]]
|
||||
[:id ::sm/uuid]]]
|
||||
|
||||
[:del-page
|
||||
[:map {:title "DelPageChange"}
|
||||
[:type [:= :del-page]]
|
||||
[:id ::sm/uuid]]]
|
||||
[:mov-page
|
||||
[:map {:title "MovPageChange"}
|
||||
[:type [:= :mov-page]]
|
||||
[:id ::sm/uuid]
|
||||
[:index :int]]]
|
||||
|
||||
[:mov-page
|
||||
[:map {:title "MovPageChange"}
|
||||
[:type [:= :mov-page]]
|
||||
[:id ::sm/uuid]
|
||||
[:index :int]]]
|
||||
[:reg-objects
|
||||
[:map {:title "RegObjectsChange"}
|
||||
[:type [:= :reg-objects]]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:shapes [:vector {:gen/max 5} ::sm/uuid]]]]
|
||||
|
||||
[:reg-objects
|
||||
[:map {:title "RegObjectsChange"}
|
||||
[:type [:= :reg-objects]]
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:shapes [:vector {:gen/max 5} ::sm/uuid]]]]
|
||||
[:add-color
|
||||
[:map {:title "AddColorChange"}
|
||||
[:type [:= :add-color]]
|
||||
[:color ctc/schema:library-color]]]
|
||||
|
||||
[:add-color
|
||||
[:map {:title "AddColorChange"}
|
||||
[:type [:= :add-color]]
|
||||
[:color ctc/schema:library-color]]]
|
||||
[:mod-color
|
||||
[:map {:title "ModColorChange"}
|
||||
[:type [:= :mod-color]]
|
||||
[:color ctc/schema:library-color]]]
|
||||
|
||||
[:mod-color
|
||||
[:map {:title "ModColorChange"}
|
||||
[:type [:= :mod-color]]
|
||||
[:color ctc/schema:library-color]]]
|
||||
[:del-color
|
||||
[:map {:title "DelColorChange"}
|
||||
[:type [:= :del-color]]
|
||||
[:id ::sm/uuid]]]
|
||||
|
||||
[:del-color
|
||||
[:map {:title "DelColorChange"}
|
||||
[:type [:= :del-color]]
|
||||
[:id ::sm/uuid]]]
|
||||
[:add-media
|
||||
[:map {:title "AddMediaChange"}
|
||||
[:type [:= :add-media]]
|
||||
[:object ctf/schema:media]]]
|
||||
|
||||
;; DEPRECATED: remove before 2.3
|
||||
[:add-recent-color
|
||||
[:map {:title "AddRecentColorChange"}]]
|
||||
[:mod-media
|
||||
[:map {:title "ModMediaChange"}
|
||||
[:type [:= :mod-media]]
|
||||
[:object ctf/schema:media]]]
|
||||
|
||||
[:add-media
|
||||
[:map {:title "AddMediaChange"}
|
||||
[:type [:= :add-media]]
|
||||
[:object ctf/schema:media]]]
|
||||
[:del-media
|
||||
[:map {:title "DelMediaChange"}
|
||||
[:type [:= :del-media]]
|
||||
[:id ::sm/uuid]]]
|
||||
|
||||
[:mod-media
|
||||
[:map {:title "ModMediaChange"}
|
||||
[:type [:= :mod-media]]
|
||||
[:object ctf/schema:media]]]
|
||||
[:add-component
|
||||
[:map {:title "AddComponentChange"}
|
||||
[:type [:= :add-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
|
||||
[:path {:optional true} :string]
|
||||
[:main-instance-id ::sm/uuid]
|
||||
[:main-instance-page ::sm/uuid]]]
|
||||
|
||||
[:del-media
|
||||
[:map {:title "DelMediaChange"}
|
||||
[:type [:= :del-media]]
|
||||
[:id ::sm/uuid]]]
|
||||
[:mod-component
|
||||
[:map {:title "ModCompoenentChange"}
|
||||
[:type [:= :mod-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
|
||||
[:name {:optional true} :string]
|
||||
[:variant-id {:optional true} ::sm/uuid]
|
||||
[:variant-properties {:optional true} [:vector ctv/schema:variant-property]]]]
|
||||
|
||||
[:add-component
|
||||
[:map {:title "AddComponentChange"}
|
||||
[:type [:= :add-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
|
||||
[:path {:optional true} :string]]]
|
||||
[:del-component
|
||||
[:map {:title "DelComponentChange"}
|
||||
[:type [:= :del-component]]
|
||||
[:id ::sm/uuid]
|
||||
;; when it is an undo of a cut-paste, we need to undo the movement
|
||||
;; of the shapes so we need to move them delta
|
||||
[:delta {:optional true} ::gpt/point]
|
||||
[:skip-undelete? {:optional true} :boolean]]]
|
||||
|
||||
[:mod-component
|
||||
[:map {:title "ModCompoenentChange"}
|
||||
[:type [:= :mod-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:shapes {:optional true} [:vector {:gen/max 3} ::sm/any]]
|
||||
[:name {:optional true} :string]
|
||||
[:variant-id {:optional true} ::sm/uuid]
|
||||
[:variant-properties {:optional true} [:vector ::ctv/variant-property]]]]
|
||||
[:restore-component
|
||||
[:map {:title "RestoreComponentChange"}
|
||||
[:type [:= :restore-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:page-id ::sm/uuid]]]
|
||||
|
||||
[:del-component
|
||||
[:map {:title "DelComponentChange"}
|
||||
[:type [:= :del-component]]
|
||||
[:id ::sm/uuid]
|
||||
;; when it is an undo of a cut-paste, we need to undo the movement
|
||||
;; of the shapes so we need to move them delta
|
||||
[:delta {:optional true} ::gpt/point]
|
||||
[:skip-undelete? {:optional true} :boolean]]]
|
||||
[:purge-component
|
||||
[:map {:title "PurgeComponentChange"}
|
||||
[:type [:= :purge-component]]
|
||||
[:id ::sm/uuid]]]
|
||||
|
||||
[:restore-component
|
||||
[:map {:title "RestoreComponentChange"}
|
||||
[:type [:= :restore-component]]
|
||||
[:id ::sm/uuid]
|
||||
[:page-id ::sm/uuid]]]
|
||||
[:add-typography
|
||||
[:map {:title "AddTypogrphyChange"}
|
||||
[:type [:= :add-typography]]
|
||||
[:typography ctt/schema:typography]]]
|
||||
|
||||
[:purge-component
|
||||
[:map {:title "PurgeComponentChange"}
|
||||
[:type [:= :purge-component]]
|
||||
[:id ::sm/uuid]]]
|
||||
[:mod-typography
|
||||
[:map {:title "ModTypogrphyChange"}
|
||||
[:type [:= :mod-typography]]
|
||||
[:typography ctt/schema:typography]]]
|
||||
|
||||
[:add-typography
|
||||
[:map {:title "AddTypogrphyChange"}
|
||||
[:type [:= :add-typography]]
|
||||
[:typography ::ctt/typography]]]
|
||||
[:del-typography
|
||||
[:map {:title "DelTypogrphyChange"}
|
||||
[:type [:= :del-typography]]
|
||||
[:id ::sm/uuid]]]
|
||||
|
||||
[:mod-typography
|
||||
[:map {:title "ModTypogrphyChange"}
|
||||
[:type [:= :mod-typography]]
|
||||
[:typography ::ctt/typography]]]
|
||||
[:update-active-token-themes
|
||||
[:map {:title "UpdateActiveTokenThemes"}
|
||||
[:type [:= :update-active-token-themes]]
|
||||
[:theme-paths [:set :string]]]]
|
||||
|
||||
[:del-typography
|
||||
[:map {:title "DelTypogrphyChange"}
|
||||
[:type [:= :del-typography]]
|
||||
[:id ::sm/uuid]]]
|
||||
[:rename-token-set-group
|
||||
[:map {:title "RenameTokenSetGroup"}
|
||||
[:type [:= :rename-token-set-group]]
|
||||
[:set-group-path [:vector :string]]
|
||||
[:set-group-fname :string]]]
|
||||
|
||||
[:update-active-token-themes
|
||||
[:map {:title "UpdateActiveTokenThemes"}
|
||||
[:type [:= :update-active-token-themes]]
|
||||
[:theme-paths [:set :string]]]]
|
||||
[:move-token-set
|
||||
[:map {:title "MoveTokenSet"}
|
||||
[:type [:= :move-token-set]]
|
||||
[:from-path [:vector :string]]
|
||||
[:to-path [:vector :string]]
|
||||
[:before-path [:maybe [:vector :string]]]
|
||||
[:before-group [:maybe :boolean]]]]
|
||||
|
||||
[:rename-token-set-group
|
||||
[:map {:title "RenameTokenSetGroup"}
|
||||
[:type [:= :rename-token-set-group]]
|
||||
[:set-group-path [:vector :string]]
|
||||
[:set-group-fname :string]]]
|
||||
[:move-token-set-group
|
||||
[:map {:title "MoveTokenSetGroup"}
|
||||
[:type [:= :move-token-set-group]]
|
||||
[:from-path [:vector :string]]
|
||||
[:to-path [:vector :string]]
|
||||
[:before-path [:maybe [:vector :string]]]
|
||||
[:before-group [:maybe :boolean]]]]
|
||||
|
||||
[:move-token-set
|
||||
[:map {:title "MoveTokenSet"}
|
||||
[:type [:= :move-token-set]]
|
||||
[:from-path [:vector :string]]
|
||||
[:to-path [:vector :string]]
|
||||
[:before-path [:maybe [:vector :string]]]
|
||||
[:before-group [:maybe :boolean]]]]
|
||||
[:set-token-theme
|
||||
[:map {:title "SetTokenThemeChange"}
|
||||
[:type [:= :set-token-theme]]
|
||||
[:theme-name :string]
|
||||
[:group :string]
|
||||
[:theme [:maybe ctob/schema:token-theme-attrs]]]]
|
||||
|
||||
[:move-token-set-group
|
||||
[:map {:title "MoveTokenSetGroup"}
|
||||
[:type [:= :move-token-set-group]]
|
||||
[:from-path [:vector :string]]
|
||||
[:to-path [:vector :string]]
|
||||
[:before-path [:maybe [:vector :string]]]
|
||||
[:before-group [:maybe :boolean]]]]
|
||||
[:set-tokens-lib
|
||||
[:map {:title "SetTokensLib"}
|
||||
[:type [:= :set-tokens-lib]]
|
||||
[:tokens-lib ::sm/any]]]
|
||||
|
||||
[:set-token-theme
|
||||
[:map {:title "SetTokenThemeChange"}
|
||||
[:type [:= :set-token-theme]]
|
||||
[:theme-name :string]
|
||||
[:group :string]
|
||||
[:theme [:maybe ctob/schema:token-theme-attrs]]]]
|
||||
[:set-token-set
|
||||
[:map {:title "SetTokenSetChange"}
|
||||
[:type [:= :set-token-set]]
|
||||
[:set-name :string]
|
||||
[:group? :boolean]
|
||||
|
||||
[:set-tokens-lib
|
||||
[:map {:title "SetTokensLib"}
|
||||
[:type [:= :set-tokens-lib]]
|
||||
[:tokens-lib ::sm/any]]]
|
||||
;; FIXME: we should not pass private types as part of changes
|
||||
;; protocol, the changes protocol should reflect a
|
||||
;; method/protocol for perform surgical operations on file data,
|
||||
;; this has nothing todo with internal types of a file data
|
||||
;; structure.
|
||||
[:token-set {:gen/gen (sg/generator ctob/schema:token-set)}
|
||||
[:maybe [:fn ctob/token-set?]]]]]
|
||||
|
||||
[:set-token-set
|
||||
[:map {:title "SetTokenSetChange"}
|
||||
[:type [:= :set-token-set]]
|
||||
[:set-name :string]
|
||||
[:group? :boolean]
|
||||
[:set-token
|
||||
[:map {:title "SetTokenChange"}
|
||||
[:type [:= :set-token]]
|
||||
[:set-name :string]
|
||||
[:token-id ::sm/uuid]
|
||||
[:token [:maybe ctob/schema:token-attrs]]]]
|
||||
|
||||
;; FIXME: we should not pass private types as part of changes
|
||||
;; protocol, the changes protocol should reflect a
|
||||
;; method/protocol for perform surgical operations on file data,
|
||||
;; this has nothing todo with internal types of a file data
|
||||
;; structure.
|
||||
[:token-set {:gen/gen (sg/generator ctob/schema:token-set)}
|
||||
[:maybe [:fn ctob/token-set?]]]]]
|
||||
|
||||
[:set-token
|
||||
[:map {:title "SetTokenChange"}
|
||||
[:type [:= :set-token]]
|
||||
[:set-name :string]
|
||||
[:token-id ::sm/uuid]
|
||||
[:token [:maybe ctob/schema:token-attrs]]]]
|
||||
|
||||
[:set-base-font-size
|
||||
[:map {:title "ModBaseFontSize"}
|
||||
[:type [:= :set-base-font-size]]
|
||||
[:base-font-size :string]]]]])
|
||||
[:set-base-font-size
|
||||
[:map {:title "ModBaseFontSize"}
|
||||
[:type [:= :set-base-font-size]]
|
||||
[:base-font-size :string]]]])
|
||||
|
||||
(def schema:changes
|
||||
[:sequential {:gen/max 5 :gen/min 1} schema:change])
|
||||
|
||||
(sm/register! ::change schema:change)
|
||||
(sm/register! ::changes schema:changes)
|
||||
|
||||
(def valid-change?
|
||||
(sm/lazy-validator schema:change))
|
||||
|
||||
(def check-changes!
|
||||
(def check-changes
|
||||
(sm/check-fn schema:changes))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -534,7 +522,7 @@
|
||||
;; When verify? false we spec the schema validation. Currently used
|
||||
;; to make just 1 validation even if the changes are applied twice
|
||||
(when verify?
|
||||
(check-changes! items))
|
||||
(check-changes items))
|
||||
|
||||
(binding [*touched-changes* (volatile! #{})
|
||||
cts/*wasm-sync* true]
|
||||
@@ -547,11 +535,6 @@
|
||||
#?(:clj (validate-shapes! data result items))
|
||||
result))))
|
||||
|
||||
;; DEPRECATED: remove after 2.3 release
|
||||
(defmethod process-change :set-option
|
||||
[data _]
|
||||
data)
|
||||
|
||||
;; --- Comment Threads
|
||||
|
||||
(defmethod process-change :set-comment-thread-position
|
||||
@@ -945,12 +928,6 @@
|
||||
[data {:keys [id]}]
|
||||
(ctl/delete-color data id))
|
||||
|
||||
;; DEPRECATED: remove before 2.3
|
||||
(defmethod process-change :add-recent-color
|
||||
[data _]
|
||||
data)
|
||||
|
||||
|
||||
;; -- Media
|
||||
|
||||
(defmethod process-change :add-media
|
||||
@@ -1091,21 +1068,23 @@
|
||||
|
||||
;; === Operations
|
||||
|
||||
(def ^:private decode-shape
|
||||
(sm/decoder cts/schema:shape sm/json-transformer))
|
||||
(def decode-shape-attrs
|
||||
(sm/decoder cts/schema:shape-attrs sm/json-transformer))
|
||||
|
||||
(defmethod process-operation :assign
|
||||
[{:keys [type] :as shape} {:keys [value] :as op}]
|
||||
(let [modifications (assoc value :type type)
|
||||
modifications (decode-shape modifications)]
|
||||
modifications (decode-shape-attrs modifications)]
|
||||
(reduce-kv (fn [shape k v]
|
||||
(process-operation shape {:type :set
|
||||
:attr k
|
||||
:val v
|
||||
:ignore-touched (:ignore-touched op)
|
||||
:ignore-geometry (:ignore-geometry op)}))
|
||||
(if (not= v (get shape k))
|
||||
(process-operation shape {:type :set
|
||||
:attr k
|
||||
:val v
|
||||
:ignore-touched (:ignore-touched op)
|
||||
:ignore-geometry (:ignore-geometry op)})
|
||||
shape))
|
||||
shape
|
||||
modifications)))
|
||||
(dissoc modifications :type))))
|
||||
|
||||
(defmethod process-operation :set
|
||||
[shape op]
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
;; Auxiliary functions to help create a set of changes (undo + redo)
|
||||
|
||||
;; TODO: this is a duplicate schema
|
||||
(def schema:changes
|
||||
(sm/register!
|
||||
^{::sm/type ::changes}
|
||||
@@ -36,7 +36,7 @@
|
||||
[:stack-undo? {:optional true} boolean?]
|
||||
[:undo-group {:optional true} ::sm/any]]))
|
||||
|
||||
(def check-changes!
|
||||
(def check-changes
|
||||
(sm/check-fn schema:changes))
|
||||
|
||||
(defn empty-changes
|
||||
@@ -168,9 +168,8 @@
|
||||
|
||||
(defn apply-changes-local
|
||||
[changes & {:keys [apply-to-library?]}]
|
||||
(assert
|
||||
(check-changes! changes)
|
||||
"expected valid changes")
|
||||
(assert (check-changes changes)
|
||||
"expected valid changes")
|
||||
|
||||
(if-let [file-data (::file-data (meta changes))]
|
||||
(let [library-data (::library-data (meta changes))
|
||||
|
||||
@@ -426,15 +426,15 @@
|
||||
|
||||
(defn components-nesting-loop?
|
||||
"Check if a nesting loop would be created if the given shape is moved below the given parent"
|
||||
[objects shape-id parent-id]
|
||||
(let [xf-get-component-id (keep :component-id)
|
||||
|
||||
children (get-children-with-self objects shape-id)
|
||||
child-components (into #{} xf-get-component-id children)
|
||||
|
||||
parents (get-parents-with-self objects parent-id)
|
||||
parent-components (into #{} xf-get-component-id parents)]
|
||||
(seq (set/intersection child-components parent-components))))
|
||||
([objects shape-id parent-id]
|
||||
(let [children (get-children-with-self objects shape-id)
|
||||
parents (get-parents-with-self objects parent-id)]
|
||||
(components-nesting-loop? children parents)))
|
||||
([children parents]
|
||||
(let [xf-get-component-id (keep :component-id)
|
||||
child-components (into #{} xf-get-component-id children)
|
||||
parent-components (into #{} xf-get-component-id parents)]
|
||||
(seq (set/intersection child-components parent-components)))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ALGORITHMS & TRANSFORMATIONS FOR SHAPES
|
||||
@@ -798,6 +798,13 @@
|
||||
(let [path-split (split-path path)]
|
||||
(merge-path-item (first path-split) name)))
|
||||
|
||||
(defn inside-path? [child parent]
|
||||
(let [child-path (split-path child)
|
||||
parent-path (split-path parent)]
|
||||
(and (<= (count parent-path) (count child-path))
|
||||
(= parent-path (take (count parent-path) child-path)))))
|
||||
|
||||
|
||||
|
||||
(defn split-by-last-period
|
||||
"Splits a string into two parts:
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.types.shape.shadow :as ctss]
|
||||
[app.common.types.shape.text :as ctst]
|
||||
[app.common.types.text :as types.text]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]
|
||||
@@ -1568,6 +1569,41 @@
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-page))))
|
||||
|
||||
(defmethod migrate-data "0011-fix-invalid-text-touched-flags"
|
||||
[data _]
|
||||
(letfn [(fix-shape [shape]
|
||||
(let [touched-groups (ctk/normal-touched-groups shape)
|
||||
content-touched? (touched-groups :content-group)
|
||||
text-touched? (or (touched-groups :text-content-text)
|
||||
(touched-groups :text-content-attribute)
|
||||
(touched-groups :text-content-structure))]
|
||||
(if (and text-touched? (not content-touched?))
|
||||
(update shape :touched ctk/set-touched-group :content-group)
|
||||
shape)))
|
||||
|
||||
(update-page [page]
|
||||
(d/update-when page :objects d/update-vals fix-shape))]
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-page))))
|
||||
|
||||
(defmethod migrate-data "0012-fix-position-data"
|
||||
[data _]
|
||||
(let [decode-fn
|
||||
(sm/decoder ctst/schema:position-data sm/json-transformer)
|
||||
|
||||
update-object
|
||||
(fn [object]
|
||||
(if (cfh/text-shape? object)
|
||||
(d/update-when object :position-data decode-fn)
|
||||
object))
|
||||
|
||||
update-container
|
||||
(fn [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(def available-migrations
|
||||
(into (d/ordered-set)
|
||||
@@ -1635,4 +1671,6 @@
|
||||
"0008-fix-library-colors-v4"
|
||||
"0009-clean-library-colors"
|
||||
"0009-add-partial-text-touched-flags"
|
||||
"0010-fix-swap-slots-pointing-non-existent-shapes"]))
|
||||
"0010-fix-swap-slots-pointing-non-existent-shapes"
|
||||
"0011-fix-invalid-text-touched-flags"
|
||||
"0012-fix-position-data"]))
|
||||
|
||||
@@ -491,6 +491,19 @@
|
||||
(pcb/with-library-data file-data)
|
||||
(pcb/update-component (:id shape) repair-component))))
|
||||
|
||||
(defmethod repair-error :invalid-text-touched
|
||||
[_ {:keys [shape page-id] :as error} file-data _]
|
||||
(let [repair-shape
|
||||
(fn [shape]
|
||||
;; Add content group
|
||||
(log/debug :hint " -> add :content-group to :touched-groups")
|
||||
(update shape :touched ctk/set-touched-group :content-group))]
|
||||
|
||||
(log/dbg :hint "repairing shape :invalid-text-touched" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||
(-> (pcb/empty-changes nil page-id)
|
||||
(pcb/with-file-data file-data)
|
||||
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||
|
||||
(defmethod repair-error :misplaced-slot
|
||||
[_ {:keys [shape page-id] :as error} file-data _]
|
||||
(let [repair-shape
|
||||
|
||||
@@ -269,6 +269,22 @@
|
||||
(d/parse-double width 1)
|
||||
(d/parse-double height 1)))
|
||||
|
||||
(defn- parse-radius-attrs
|
||||
[attrs]
|
||||
(if (or (contains? attrs :rx) (contains? attrs :ry))
|
||||
(let [rx-val (d/parse-double (:rx attrs) 0)
|
||||
ry-val (d/parse-double (:ry attrs) 0)
|
||||
radius (cond
|
||||
(and (contains? attrs :rx) (contains? attrs :ry))
|
||||
(min rx-val ry-val)
|
||||
(contains? attrs :rx)
|
||||
rx-val
|
||||
(contains? attrs :ry)
|
||||
ry-val
|
||||
:else 0)]
|
||||
{:r1 radius :r2 radius :r3 radius :r4 radius})
|
||||
{}))
|
||||
|
||||
(defn create-rect-shape [name frame-id svg-data {:keys [attrs] :as data}]
|
||||
(let [transform (->> (csvg/parse-transform (:transform attrs))
|
||||
(gmt/transform-in (gpt/point svg-data)))
|
||||
@@ -280,7 +296,9 @@
|
||||
(update :y - (:y origin)))
|
||||
|
||||
props (-> (dissoc attrs :x :y :width :height :rx :ry :transform)
|
||||
(csvg/attrs->props))]
|
||||
(csvg/attrs->props))
|
||||
|
||||
radius-attrs (parse-radius-attrs attrs)]
|
||||
(cts/setup-shape
|
||||
(-> (calculate-rect-metadata rect transform)
|
||||
(assoc :type :rect)
|
||||
@@ -288,13 +306,10 @@
|
||||
(assoc :frame-id frame-id)
|
||||
(assoc :svg-viewbox vbox)
|
||||
(assoc :svg-attrs props)
|
||||
;; We need to ensure fills are empty on import process
|
||||
;; because setup-shape assings one by default.
|
||||
;; We need to ensure fills are empty on import process
|
||||
;; because setup-shape assings one by default.
|
||||
(assoc :fills [])
|
||||
(cond-> (contains? attrs :rx)
|
||||
(assoc :rx (d/parse-double (:rx attrs) 0)))
|
||||
(cond-> (contains? attrs :ry)
|
||||
(assoc :ry (d/parse-double (:ry attrs) 0)))))))
|
||||
(merge radius-attrs)))))
|
||||
|
||||
(defn- parse-circle-attrs
|
||||
[attrs]
|
||||
@@ -508,6 +523,7 @@
|
||||
:else (dm/str tag))]
|
||||
(dm/str "svg-" suffix)))
|
||||
|
||||
|
||||
(defn parse-svg-element
|
||||
[frame-id svg-data {:keys [tag attrs hidden] :as element} unames]
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
:not-component-not-allowed
|
||||
:component-nil-objects-not-allowed
|
||||
:instance-head-not-frame
|
||||
:invalid-text-touched
|
||||
:misplaced-slot
|
||||
:missing-slot
|
||||
:shape-ref-cycle
|
||||
@@ -328,6 +329,20 @@
|
||||
"This shape has children with the same swap slot"
|
||||
shape file page)))
|
||||
|
||||
(defn- check-valid-touched
|
||||
"Validate that the text touched flags are coherent."
|
||||
[shape file page]
|
||||
(let [touched-groups (ctk/normal-touched-groups shape)
|
||||
content-touched? (touched-groups :content-group)
|
||||
text-touched? (or (touched-groups :text-content-text)
|
||||
(touched-groups :text-content-attribute)
|
||||
(touched-groups :text-content-structure))]
|
||||
;; For now we only check this combination, that has been reported in some bugs
|
||||
(when (and text-touched? (not content-touched?))
|
||||
(report-error :invalid-text-touched
|
||||
"This thape has text type touched but not content touched"
|
||||
shape file page))))
|
||||
|
||||
(defn- check-shape-main-root-top
|
||||
"Root shape of a top main instance:
|
||||
|
||||
@@ -369,6 +384,7 @@
|
||||
(check-component-ref shape file page libraries)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(check-duplicate-swap-slot shape file page)
|
||||
(check-valid-touched shape file page)
|
||||
(run! #(check-shape % file page libraries :context :copy-top :library-exists library-exists) (:shapes shape))))
|
||||
|
||||
(defn- check-shape-copy-root-nested
|
||||
@@ -379,6 +395,7 @@
|
||||
[shape file page libraries library-exists]
|
||||
(check-component-not-main-head shape file page libraries)
|
||||
(check-component-not-root shape file page)
|
||||
(check-valid-touched shape file page)
|
||||
;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached
|
||||
;; so we only validate the shape-ref if the ancestor is from a valid library
|
||||
(when library-exists
|
||||
@@ -401,6 +418,7 @@
|
||||
(check-component-not-root shape file page)
|
||||
(check-component-ref shape file page libraries)
|
||||
(check-empty-swap-slot shape file page)
|
||||
(check-valid-touched shape file page)
|
||||
(run! #(check-shape % file page libraries :context :copy-any) (:shapes shape)))
|
||||
|
||||
(defn- check-shape-not-component
|
||||
|
||||
@@ -119,7 +119,9 @@
|
||||
;; Only for developtment.
|
||||
:tiered-file-data-storage
|
||||
:token-units
|
||||
:token-base-font-size
|
||||
:token-typography-types
|
||||
:token-typography-composite
|
||||
:transit-readable-response
|
||||
:user-feedback
|
||||
;; TODO: remove this flag.
|
||||
@@ -131,7 +133,8 @@
|
||||
:hide-release-modal
|
||||
:subscriptions
|
||||
:subscriptions-old
|
||||
:frontend-binary-fills})
|
||||
:frontend-binary-fills
|
||||
:inspect-styles})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
@@ -153,7 +156,9 @@
|
||||
:enable-dashboard-templates-section
|
||||
:enable-google-fonts-provider
|
||||
:enable-component-thumbnails
|
||||
:enable-render-wasm-dpr])
|
||||
:enable-render-wasm-dpr
|
||||
:enable-token-units
|
||||
:enable-token-typography-types])
|
||||
|
||||
(defn parse
|
||||
[& flags]
|
||||
|
||||
@@ -25,16 +25,7 @@
|
||||
|
||||
;; --- Matrix Impl
|
||||
|
||||
(defn format-precision
|
||||
[mtx precision]
|
||||
(when mtx
|
||||
(dm/fmt "matrix(%, %, %, %, %, %)"
|
||||
(mth/to-fixed (.-a mtx) precision)
|
||||
(mth/to-fixed (.-b mtx) precision)
|
||||
(mth/to-fixed (.-c mtx) precision)
|
||||
(mth/to-fixed (.-d mtx) precision)
|
||||
(mth/to-fixed (.-e mtx) precision)
|
||||
(mth/to-fixed (.-f mtx) precision))))
|
||||
(declare format-precision)
|
||||
|
||||
(cr/defrecord Matrix [^double a
|
||||
^double b
|
||||
@@ -46,6 +37,17 @@
|
||||
(toString [this]
|
||||
(format-precision this precision)))
|
||||
|
||||
(defn format-precision
|
||||
[mtx precision]
|
||||
(when mtx
|
||||
(dm/fmt "matrix(%, %, %, %, %, %)"
|
||||
(mth/to-fixed (.-a ^Matrix mtx) precision)
|
||||
(mth/to-fixed (.-b ^Matrix mtx) precision)
|
||||
(mth/to-fixed (.-c ^Matrix mtx) precision)
|
||||
(mth/to-fixed (.-d ^Matrix mtx) precision)
|
||||
(mth/to-fixed (.-e ^Matrix mtx) precision)
|
||||
(mth/to-fixed (.-f ^Matrix mtx) precision))))
|
||||
|
||||
(defn matrix?
|
||||
"Return true if `v` is Matrix instance."
|
||||
[v]
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
(if (and (some? layout-line) (<= from-idx max-idx))
|
||||
(let [to-idx (+ from-idx (:num-children layout-line))
|
||||
children (subvec children from-idx to-idx)
|
||||
|
||||
[_ modif-tree]
|
||||
(reduce set-child-modifiers [layout-line modif-tree] children)]
|
||||
(recur modif-tree (first pending) (rest pending) to-idx))
|
||||
|
||||
@@ -255,10 +255,10 @@
|
||||
(if-let [pt (first pts)]
|
||||
(let [x (dm/get-prop pt :x)
|
||||
y (dm/get-prop pt :y)]
|
||||
(recur (mth/min minx x)
|
||||
(mth/min miny y)
|
||||
(mth/max maxx x)
|
||||
(mth/max maxy y)
|
||||
(recur (double (mth/min minx x))
|
||||
(double (mth/min miny y))
|
||||
(double (mth/max maxx x))
|
||||
(double (mth/max maxy y))
|
||||
(rest pts)))
|
||||
(when (d/num? minx miny maxx maxy)
|
||||
(make-rect minx miny (- maxx minx) (- maxy miny)))))))
|
||||
|
||||
@@ -393,7 +393,7 @@
|
||||
min-fr
|
||||
(let [{:keys [size type value]} (first tracks)
|
||||
min-fr (if (= type :flex) (max min-fr (/ size value)) min-fr)]
|
||||
(recur (rest tracks) min-fr)))))
|
||||
(recur (rest tracks) (double min-fr))))))
|
||||
|
||||
(defn calc-layout-data
|
||||
([parent transformed-parent-bounds children bounds objects]
|
||||
|
||||
@@ -2020,7 +2020,6 @@
|
||||
skip-operations? (or skip-operations?
|
||||
(= attr-val (get current-shape attr)))
|
||||
|
||||
|
||||
;; On a text-change, we want to force a position-data reset
|
||||
;; so it's calculated again
|
||||
[roperations uoperations]
|
||||
@@ -2028,6 +2027,16 @@
|
||||
(add-update-attr-operations :position-data current-shape roperations uoperations nil)
|
||||
[roperations uoperations])
|
||||
|
||||
;; On a rotation operation we need to keep also the transformation matrixes
|
||||
[roperations uoperations]
|
||||
(if (and (not skip-operations?) (= attr :rotation))
|
||||
(let [[roperations uoperations]
|
||||
(add-update-attr-operations
|
||||
:transform current-shape roperations uoperations (:transform previous-shape))]
|
||||
(add-update-attr-operations
|
||||
:transform-inverse current-shape roperations uoperations (:transform-inverse previous-shape)))
|
||||
[roperations uoperations])
|
||||
|
||||
[roperations' uoperations']
|
||||
(if skip-operations?
|
||||
[roperations uoperations]
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
[app.common.types.pages-list :as ctpl]
|
||||
[app.common.types.shape.interactions :as ctsi]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.shape.token :as ctst]
|
||||
[app.common.types.text :as ctt]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]))
|
||||
|
||||
(def text-typography-attrs (set ctt/text-typography-attrs))
|
||||
(def text-typography-style-attrs (set ctt/text-typography-attrs))
|
||||
|
||||
(defn- generate-unapply-tokens
|
||||
"When updating attributes that have a token applied, we must unapply it, because the value
|
||||
@@ -38,10 +39,14 @@
|
||||
(let [new-shape (get new-objects (:id shape))
|
||||
attrs (ctt/get-diff-attrs (:content shape) (:content new-shape))
|
||||
|
||||
;; Unapply token when applying typography asset style
|
||||
attrs (if (seq (set/intersection text-typography-attrs attrs))
|
||||
(into attrs cto/typography-keys)
|
||||
attrs)]
|
||||
attrs (cond-> attrs
|
||||
;; Unapply token when applying typography asset style
|
||||
(seq (set/intersection text-typography-style-attrs attrs))
|
||||
(into cto/typography-keys)
|
||||
|
||||
;; Unapply font-weight when changing the font-family attribute
|
||||
(and (:font-id attrs) (ctst/font-weight-applied? shape))
|
||||
(conj :font-weight))]
|
||||
(apply set/union (map cto/shape-attr->token-attrs attrs))))
|
||||
|
||||
check-attr
|
||||
@@ -400,9 +405,10 @@
|
||||
(remove #(= % parent-id) all-parents))]
|
||||
|
||||
(-> changes
|
||||
;; Remove layout-item properties when moving a shape outside a layout
|
||||
;; Remove layout-item properties and tokens when moving a shape outside a layout
|
||||
(cond-> (not (ctl/any-layout? parent))
|
||||
(pcb/update-shapes ids ctl/remove-layout-item-data))
|
||||
(-> (pcb/update-shapes ids ctl/remove-layout-item-data)
|
||||
(pcb/update-shapes ids cto/unapply-layout-item-tokens)))
|
||||
|
||||
;; Remove the hide in viewer flag
|
||||
(cond-> (and (not= uuid/zero parent-id) (cfh/frame-shape? parent))
|
||||
|
||||
@@ -172,6 +172,10 @@
|
||||
objects (pcb/get-objects changes)
|
||||
variant-id (:id variant-container)
|
||||
|
||||
num-shapes (->> variant-container
|
||||
:shapes
|
||||
count)
|
||||
|
||||
;; If we are cut-pasting a variant-container, this will be null
|
||||
;; because it hasn't any shapes yet
|
||||
first-comp-id (->> variant-container
|
||||
@@ -198,7 +202,7 @@
|
||||
0
|
||||
shapes)
|
||||
|
||||
num-new-props (if (or (zero? num-base-props)
|
||||
num-new-props (if (or (zero? num-shapes)
|
||||
(< total-props num-base-props))
|
||||
0
|
||||
(- total-props num-base-props))
|
||||
@@ -213,7 +217,7 @@
|
||||
(reduce
|
||||
(fn [changes shape]
|
||||
(let [component (ctcl/get-component data (:component-id shape) true)]
|
||||
(if (or (zero? num-base-props) ;; do nothing if there are no base props
|
||||
(if (or (zero? num-shapes) ;; do nothing if there are no shapes
|
||||
(and (= variant-id (:variant-id shape)) ;; or we are only moving the shape inside its parent (it is
|
||||
(not (:deleted component)))) ;; the same parent and the component isn't deleted)
|
||||
changes
|
||||
|
||||
@@ -31,9 +31,11 @@
|
||||
component-id
|
||||
new-component-id
|
||||
{:new-shape-id new-shape-id :apply-changes-local-library? true}))]
|
||||
(-> changes
|
||||
(clvp/generate-update-property-value new-component-id prop-num value)
|
||||
(pcb/change-parent (:parent-id shape) [new-shape] 0))))
|
||||
(cond-> changes
|
||||
(>= prop-num 0)
|
||||
(clvp/generate-update-property-value new-component-id prop-num value)
|
||||
:always
|
||||
(pcb/change-parent (:parent-id shape) [new-shape] 0))))
|
||||
|
||||
(defn- generate-path
|
||||
[path objects base-id shape]
|
||||
|
||||
@@ -131,34 +131,10 @@
|
||||
(->> (entries schema)
|
||||
(into #{} xf:map-key)))
|
||||
|
||||
|
||||
;; (defn key-transformer
|
||||
;; [& {:as opts}]
|
||||
;; (mt/key-transformer opts))
|
||||
|
||||
;; (defn- transform-map-keys
|
||||
;; [f o]
|
||||
;; (cond
|
||||
;; (record? o)
|
||||
;; (reduce-kv (fn [res k v]
|
||||
;; (let [k' (f k)]
|
||||
;; (if (= k k')
|
||||
;; res
|
||||
;; (-> res
|
||||
;; (assoc k' v)
|
||||
;; (dissoc k)))))
|
||||
;; o
|
||||
;; o)
|
||||
|
||||
;; (map? o)
|
||||
;; (persistent!
|
||||
;; (reduce-kv (fn [res k v]
|
||||
;; (assoc! res (f k) v))
|
||||
;; (transient {})
|
||||
;; o))
|
||||
|
||||
;; :else
|
||||
;; o))
|
||||
(defn update-properties
|
||||
[s f & args]
|
||||
(let [s (schema s)]
|
||||
(apply m/-update-properties s f args)))
|
||||
|
||||
(defn -transform-map-keys
|
||||
([f]
|
||||
@@ -679,8 +655,7 @@
|
||||
identity)]
|
||||
{:pred #(contains? options %)
|
||||
:type-properties
|
||||
{:title "one-of"
|
||||
:description "One of the Set"
|
||||
{:title "enum"
|
||||
:gen/gen (sg/elements options)
|
||||
:decode/string decode
|
||||
:decode/json decode
|
||||
@@ -723,15 +698,14 @@
|
||||
|
||||
{:pred pred
|
||||
:type-properties
|
||||
{:title "int"
|
||||
:description "int"
|
||||
{:title "integer"
|
||||
:description "integer"
|
||||
:error/message "expected to be int/long"
|
||||
:error/code "errors.invalid-integer"
|
||||
:gen/gen gen
|
||||
:decode/string parse-long
|
||||
:decode/json parse-long
|
||||
::oapi/type "integer"
|
||||
::oapi/format "int64"}}))})
|
||||
::oapi/type "integer"}}))})
|
||||
|
||||
(defn parse-double
|
||||
[v]
|
||||
@@ -793,8 +767,8 @@
|
||||
|
||||
{:pred pred
|
||||
:type-properties
|
||||
{:title "int"
|
||||
:description "int"
|
||||
{:title "number"
|
||||
:description "number"
|
||||
:error/message "expected to be number"
|
||||
:error/code "errors.invalid-number"
|
||||
:gen/gen gen
|
||||
@@ -844,10 +818,7 @@
|
||||
#(some (fn [prop]
|
||||
(contains? % prop))
|
||||
choices))]
|
||||
{:pred pred
|
||||
:type-properties
|
||||
{:title "contains any"
|
||||
:description "contains predicate"}}))})
|
||||
{:pred pred}))})
|
||||
|
||||
;; (register!
|
||||
;; {:type ::inst
|
||||
@@ -943,8 +914,6 @@
|
||||
:gen/gen (sg/uri)
|
||||
:decode/string decode-uri
|
||||
:decode/json decode-uri
|
||||
:encode/json str
|
||||
:encode/string str
|
||||
::oapi/type "string"
|
||||
::oapi/format "uri"}})
|
||||
|
||||
@@ -970,6 +939,7 @@
|
||||
:type-properties
|
||||
{:title "string"
|
||||
:description "not whitespace string"
|
||||
::oapi/type "string"
|
||||
:gen/gen (sg/word-string)
|
||||
:error/fn
|
||||
(fn [{:keys [value schema]}]
|
||||
|
||||
@@ -91,11 +91,15 @@
|
||||
(defmethod visit :int [_ schema _ _] (str "integer" (-titled schema) (-min-max-suffix-number schema)))
|
||||
(defmethod visit :double [_ schema _ _] (str "double" (-titled schema) (-min-max-suffix-number schema)))
|
||||
(defmethod visit :select-keys [_ schema _ options] (describe* (m/deref schema) options))
|
||||
(defmethod visit :and [_ s children _] (str (str/join " && " children) (-titled s)))
|
||||
(defmethod visit :and [_ s children _]
|
||||
(str (str/join " && " (filter some? children)) (-titled s)))
|
||||
(defmethod visit :enum [_ s children _options] (str "enum" (-titled s) " of " (str/join ", " children)))
|
||||
(defmethod visit :maybe [_ _ children _] (str (first children) " nullable"))
|
||||
(defmethod visit :tuple [_ _ children _] (str "(" (str/join ", " children) ")"))
|
||||
(defmethod visit :re [_ s _ options] (str "regex pattern " (-titled s) "matching " (pr-str (first (m/children s options)))))
|
||||
(defmethod visit :re [_ _ children _]
|
||||
(let [pattern (first children)]
|
||||
(str "string & regex pattern /" (str pattern) "/")))
|
||||
|
||||
(defmethod visit :any [_ s _ _] (str "anything" (-titled s)))
|
||||
(defmethod visit :some [_ _ _ _] "anything but null")
|
||||
(defmethod visit :nil [_ _ _ _] "null")
|
||||
@@ -108,10 +112,11 @@
|
||||
(defmethod visit :uuid [_ _ _ _] "uuid")
|
||||
(defmethod visit :boolean [_ _ _ _] "boolean")
|
||||
(defmethod visit :keyword [_ _ _ _] "string")
|
||||
(defmethod visit :fn [_ _ _ _] "FN")
|
||||
(defmethod visit :fn [_ _ _ _]
|
||||
nil)
|
||||
|
||||
(defmethod visit :vector [_ _ children _]
|
||||
(str "[" (last children) "]"))
|
||||
(str "[" (str/trim (last children)) "]"))
|
||||
|
||||
(defn -tagged [children] (map (fn [[tag _ c]] (str c " (tag: " tag ")")) children))
|
||||
|
||||
@@ -137,8 +142,15 @@
|
||||
(some? suffix)
|
||||
(str suffix))))
|
||||
|
||||
(defmethod visit :map-of [_ _ children _]
|
||||
(str "map[" (first children) "," (second children) "]"))
|
||||
(defmethod visit :map-of
|
||||
[_ schema children _]
|
||||
(let [props (m/properties schema)
|
||||
title (some->> (:title props) str/camel str/capital)]
|
||||
|
||||
(str (if title
|
||||
(str "type " title ": ")
|
||||
"")
|
||||
"map[" (first children) "," (second children) "]")))
|
||||
|
||||
(defmethod visit :union [_ _ children _]
|
||||
(str/join " | " children))
|
||||
@@ -156,61 +168,104 @@
|
||||
(or (:title props)
|
||||
"*")))
|
||||
|
||||
|
||||
(defn- format-map
|
||||
[schema children]
|
||||
(let [props (m/properties schema)
|
||||
closed? (get props :closed)
|
||||
title (some->> (:title props) str/camel str/capital)
|
||||
optional (into #{} (comp (filter (m/-comp :optional second))
|
||||
(map first))
|
||||
children)
|
||||
entries (->> children
|
||||
(map (fn [[k _ s]]
|
||||
;; NOTE: maybe we can detect multiple lines
|
||||
;; and then just insert a break line
|
||||
(str " " (str/camel k)
|
||||
(when (contains? optional k) "?")
|
||||
": " (str/trim s))))
|
||||
(str/join ",\n"))
|
||||
|
||||
header (cond-> (str "type " title)
|
||||
closed? (str "!")
|
||||
(some? title) (str " "))]
|
||||
|
||||
(str header "{\n" entries "\n}")))
|
||||
|
||||
(defmethod visit :map
|
||||
[_ schema children {:keys [::level ::max-level] :as options}]
|
||||
(let [props (m/properties schema)
|
||||
closed? (:closed props)
|
||||
title (some->> (:title props) str/camel str/capital)]
|
||||
[_ schema children {:keys [::level] :as options}]
|
||||
(let [props (m/properties schema)
|
||||
extracted? (get props ::extracted false)]
|
||||
|
||||
(if (>= level max-level)
|
||||
(or (some-> title str)
|
||||
"<untitled>")
|
||||
(let [optional (into #{} (comp (filter (m/-comp :optional second))
|
||||
(map first))
|
||||
children)
|
||||
entries (->> children
|
||||
(map (fn [[k _ s]]
|
||||
(str (pad " " level) (str/camel k)
|
||||
(when (contains? optional k) "?")
|
||||
": " s)))
|
||||
(str/join ",\n"))
|
||||
(cond
|
||||
(or (= level 0) extracted?)
|
||||
(format-map schema children)
|
||||
|
||||
header (cond-> (str "type " title)
|
||||
closed? (str "!")
|
||||
(some? title) (str " "))]
|
||||
:else
|
||||
(let [schema (mu/update-properties schema assoc ::extracted true)
|
||||
title (or (some->> (:title props) str/camel str/capital) "<untitled>")]
|
||||
(swap! *definitions* conj (format-map schema children))
|
||||
title))))
|
||||
|
||||
(str (pad header level) "{\n" entries "\n" (pad "}\n" level))))))
|
||||
(defn format-multi
|
||||
[s children]
|
||||
(let [props (m/properties s)
|
||||
title (or (some-> (:title props) str/camel str/capital) "<untitled>")
|
||||
dispatcher (or (-> s m/properties :dispatch-description)
|
||||
(-> s m/properties :dispatch))
|
||||
entries (->> children
|
||||
(map (fn [[_ _ entry]]
|
||||
(pad entry 1)))
|
||||
(str/join ",\n"))
|
||||
|
||||
header (str "type " title " [dispatch=" (d/name dispatcher) "]")]
|
||||
(str header " {\n" entries "\n}")))
|
||||
|
||||
(defmethod visit :multi
|
||||
[_ s children {:keys [::level ::max-level] :as options}]
|
||||
(let [props (m/properties s)
|
||||
title (some-> (:title props) str/camel str/capital)]
|
||||
(if (>= level max-level)
|
||||
title
|
||||
(let [dispatcher (or (-> s m/properties :dispatch-description)
|
||||
(-> s m/properties :dispatch))
|
||||
[_ schema children {:keys [::level] :as options}]
|
||||
(let [props (m/properties schema)
|
||||
title (or (some-> (:title props) str/camel str/capital) "<untitled>")
|
||||
extracted? (get props ::extracted false)]
|
||||
|
||||
prefix (apply str (take (inc level) (repeat " ")))
|
||||
(cond
|
||||
(or (zero? level) extracted?)
|
||||
(format-multi schema children)
|
||||
|
||||
entries (->> children
|
||||
(map (fn [[_ _ shape]]
|
||||
(str prefix shape)))
|
||||
(str/join ",\n"))
|
||||
:else
|
||||
(let [schema (mu/update-properties schema assoc ::extracted true)]
|
||||
(swap! *definitions* conj (format-multi schema children))
|
||||
title))))
|
||||
|
||||
header (cond-> "multi"
|
||||
(some? title) (str " " title)
|
||||
:always (str " [dispatch=" (d/name dispatcher) "]"))]
|
||||
(defn- format-merge
|
||||
[schema children]
|
||||
|
||||
(str header " {\n" entries "\n" (pad "}" level))))))
|
||||
(let [props (m/properties schema)
|
||||
entries (->> children
|
||||
(map (fn [shape]
|
||||
(pad shape 1)))
|
||||
(str/join ",\n"))
|
||||
title (some-> (:title props) str/camel str/capital)
|
||||
|
||||
|
||||
header (str "merge type " title)]
|
||||
|
||||
(str header " {\n" entries "\n}")))
|
||||
|
||||
(defmethod visit :merge
|
||||
[_ schema children _]
|
||||
(let [entries (str/join ",\n" children)
|
||||
props (m/properties schema)
|
||||
title (or (some-> (:title props) str/camel str/capital)
|
||||
"<untitled>")]
|
||||
(str "merge type " title " { \n" entries "\n}\n")))
|
||||
[_ schema children {:keys [::level] :as options}]
|
||||
(let [props (m/properties schema)
|
||||
title (some-> (:title props) str/camel str/capital)
|
||||
extracted? (get props ::extracted false)]
|
||||
|
||||
(cond
|
||||
(or (zero? level) extracted?)
|
||||
(format-merge schema children)
|
||||
|
||||
:else
|
||||
(let [schema (mu/update-properties schema assoc ::extracted true)]
|
||||
(swap! *definitions* conj
|
||||
(format-merge schema children))
|
||||
title))))
|
||||
|
||||
(defmethod visit ::sm/one-of
|
||||
[_ _ children _]
|
||||
@@ -219,45 +274,37 @@
|
||||
(map d/name)
|
||||
(str/join "|")) ")")))
|
||||
|
||||
(defmethod visit :schema [_ schema children options]
|
||||
(visit ::m/schema schema children options))
|
||||
|
||||
(defmethod visit ::m/schema
|
||||
[_ schema _ {:keys [::level ::limit ::max-level] :as options}]
|
||||
(let [schema' (m/deref schema)
|
||||
props (merge
|
||||
(m/properties schema)
|
||||
(m/properties schema'))
|
||||
ref (m/-ref schema)
|
||||
title (:title props)]
|
||||
(defmethod visit :schema
|
||||
[_ schema children options]
|
||||
(let [props (m/properties schema)
|
||||
title (some-> (:title props) str/camel str/capital)
|
||||
extracted? (get props ::extracted false)]
|
||||
|
||||
(cond
|
||||
(::inline props)
|
||||
(do
|
||||
(if (>= limit max-level)
|
||||
title
|
||||
(describe* schema' options)))
|
||||
(not title)
|
||||
(visit ::m/schema schema children options)
|
||||
|
||||
(and ref title)
|
||||
(do
|
||||
(when (<= limit max-level)
|
||||
(swap! *definitions* conj (describe* schema' (assoc options ::base-limit limit))))
|
||||
|
||||
title)
|
||||
|
||||
(>= limit max-level)
|
||||
(or title
|
||||
(some-> ref d/name str/camel str/capital)
|
||||
"<untitled>")
|
||||
extracted?
|
||||
(let [title (or title "<untitled>")]
|
||||
(str "type " title ": "
|
||||
(visit ::m/schema schema children options)))
|
||||
|
||||
:else
|
||||
(describe* schema' (assoc options ::base-level level ::base-limit limit)))))
|
||||
(let [schema (mu/update-properties schema assoc ::extracted true)
|
||||
title (or title "<untitled>")]
|
||||
(swap! *definitions* conj
|
||||
(str "type " title ": "
|
||||
(visit ::m/schema schema children (update options ::level inc))))
|
||||
title))))
|
||||
|
||||
(defmethod visit ::m/schema
|
||||
[_ schema _ {:keys [::level] :as options}]
|
||||
(let [schema' (m/deref schema)]
|
||||
(describe* schema' (assoc options ::base-level level))))
|
||||
|
||||
(defn describe* [s options]
|
||||
(letfn [(walk-fn [schema path children {:keys [::base-level ::base-limit] :or {base-level 0 base-limit 0} :as options}]
|
||||
(let [options (assoc options
|
||||
::limit (+ base-limit (count path))
|
||||
::level (+ base-level (count path)))]
|
||||
(letfn [(walk-fn [schema path children {:keys [::base-level] :or {base-level 0} :as options}]
|
||||
(let [options (assoc options ::level (+ base-level (count path)))]
|
||||
(visit (m/type schema) schema children options)))]
|
||||
(m/walk s walk-fn options)))
|
||||
|
||||
@@ -275,8 +322,7 @@
|
||||
(mu/update-properties assoc ::root true))
|
||||
|
||||
options (into {::m/walk-entry-vals true
|
||||
::level 0
|
||||
::max-level 300}
|
||||
::level 0}
|
||||
options)]
|
||||
|
||||
(binding [*definitions* defs]
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
(ns app.common.schema.openapi
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.schema :as-alias sm]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[malli.core :as m]))
|
||||
@@ -15,16 +17,44 @@
|
||||
(declare transform*)
|
||||
|
||||
(defmulti visit (fn [name _schema _children _options] name) :default ::default)
|
||||
(defmethod visit ::default [_ _ _ _] {})
|
||||
|
||||
(defmethod visit ::default [_ schema _ _]
|
||||
(let [props (m/type-properties schema)]
|
||||
(d/without-nils
|
||||
{:type (get props ::type)
|
||||
:format (get props ::format)
|
||||
:title (get props :title)
|
||||
:description (get props :description)})))
|
||||
|
||||
(defmethod visit :> [_ _ [value] _] {:type "number" :exclusiveMinimum value})
|
||||
(defmethod visit :>= [_ _ [value] _] {:type "number" :minimum value})
|
||||
(defmethod visit :< [_ _ [value] _] {:type "number" :exclusiveMaximum value})
|
||||
(defmethod visit :<= [_ _ [value] _] {:type "number" :maximum value})
|
||||
(defmethod visit := [_ _ [value] _] {:const value})
|
||||
|
||||
(defmethod visit := [_ schema children _]
|
||||
(let [props (m/properties schema)
|
||||
type (get props :type :string)]
|
||||
(d/without-nils
|
||||
{:type (or (get props ::type)
|
||||
(d/name type))
|
||||
:enum (if (= :string type)
|
||||
(mapv d/name children)
|
||||
(vec children))})))
|
||||
|
||||
|
||||
(defmethod visit :not= [_ _ _ _] {})
|
||||
|
||||
(defmethod visit :fn [_ _ _ _]
|
||||
nil)
|
||||
|
||||
(defmethod visit ::sm/contains-any [_ _ _ _]
|
||||
nil)
|
||||
|
||||
(defmethod visit :not [_ _ children _] {:not (last children)})
|
||||
(defmethod visit :and [_ _ children _] {:allOf children})
|
||||
(defmethod visit :and [_ _ children _]
|
||||
{:allOf (keep not-empty children)})
|
||||
|
||||
|
||||
(defmethod visit :or [_ _ children _] {:anyOf children})
|
||||
(defmethod visit :orn [_ _ children _] {:anyOf (map last children)})
|
||||
|
||||
@@ -71,14 +101,28 @@
|
||||
:minProperties
|
||||
:maxProperties))
|
||||
|
||||
(defmethod visit :vector [_ schema children _]
|
||||
(let [child (-> schema m/children first)
|
||||
props (m/properties (m/deref child))]
|
||||
(minmax-properties
|
||||
{:type "array", :items (first children) :title (:title props)}
|
||||
schema
|
||||
:minItems
|
||||
:maxItems)))
|
||||
(defmethod visit :any [_ _ _ _]
|
||||
{:description "Any Value"})
|
||||
|
||||
(defmethod visit ::sm/set [_ schema children _]
|
||||
(minmax-properties
|
||||
{:type "array", :items (first children), :uniqueItems true}
|
||||
schema
|
||||
:minItems
|
||||
:maxItems))
|
||||
|
||||
(defmethod visit ::sm/vec [_ schema children _]
|
||||
(minmax-properties
|
||||
{:type "array", :items (first children)}
|
||||
schema
|
||||
:minItems
|
||||
:maxItems))
|
||||
|
||||
(defmethod visit :vector [_ schema children options]
|
||||
(visit ::sm/vec schema children options))
|
||||
|
||||
(defmethod visit :set [_ schema children options]
|
||||
(visit ::sm/set schema children options))
|
||||
|
||||
(defmethod visit :sequential [_ schema children _]
|
||||
(minmax-properties
|
||||
@@ -87,36 +131,64 @@
|
||||
:minItems
|
||||
:maxItems))
|
||||
|
||||
(defmethod visit :set [_ schema children _]
|
||||
(minmax-properties
|
||||
{:type "array", :items (first children), :uniqueItems true}
|
||||
schema
|
||||
:minItems
|
||||
:maxItems))
|
||||
(defmethod visit :enum [_ _ children options]
|
||||
(merge (some-> (m/-infer children) (transform* options)) {:enum children}))
|
||||
|
||||
(defmethod visit :maybe [_ _ children _]
|
||||
(let [children (first children)]
|
||||
(assoc children :nullable true)))
|
||||
|
||||
(defmethod visit :tuple [_ _ children _]
|
||||
{:type "array", :items children, :additionalItems false})
|
||||
|
||||
(defmethod visit :enum [_ _ children options] (merge (some-> (m/-infer children) (transform* options)) {:enum children}))
|
||||
(defmethod visit :maybe [_ _ children _] {:oneOf (conj children {:type "null"})})
|
||||
(defmethod visit :tuple [_ _ children _] {:type "array", :items children, :additionalItems false})
|
||||
(defmethod visit :re [_ schema _ options]
|
||||
{:type "string", :pattern (str (first (m/children schema options)))})
|
||||
|
||||
(defmethod visit :nil [_ _ _ _] {:type "null"})
|
||||
|
||||
(defmethod visit :string [_ schema _ _]
|
||||
(merge {:type "string"} (-> schema m/properties (select-keys [:min :max]) (set/rename-keys {:min :minLength, :max :maxLength}))))
|
||||
|
||||
|
||||
(defmethod visit ::sm/one-of [_ _ children _]
|
||||
(let [options (->> (first children)
|
||||
(mapv d/name))]
|
||||
{:type "string"
|
||||
:enum options}))
|
||||
|
||||
(defmethod visit :int [_ schema _ _]
|
||||
(merge {:type "integer"} (-> schema m/properties (select-keys [:min :max]) (set/rename-keys {:min :minimum, :max :maximum}))))
|
||||
(minmax-properties
|
||||
{:type "integer"}
|
||||
schema
|
||||
:minimum
|
||||
:maximum))
|
||||
|
||||
(defmethod visit :double [_ schema _ _]
|
||||
(merge {:type "number"}
|
||||
(-> schema m/properties (select-keys [:min :max]) (set/rename-keys {:min :minimum, :max :maximum}))))
|
||||
(minmax-properties
|
||||
{:type "number"
|
||||
:format "double"}
|
||||
schema
|
||||
:minimum
|
||||
:maximum))
|
||||
|
||||
(defmethod visit ::sm/int
|
||||
[_ schema children options]
|
||||
(visit :int schema children options))
|
||||
|
||||
(defmethod visit ::sm/double
|
||||
[_ schema children options]
|
||||
(visit :double schema children options))
|
||||
|
||||
(defmethod visit :boolean [_ _ _ _] {:type "boolean"})
|
||||
(defmethod visit ::sm/boolean [_ _ _ _] {:type "boolean"})
|
||||
|
||||
|
||||
(defmethod visit :keyword [_ _ _ _] {:type "string"})
|
||||
(defmethod visit :qualified-keyword [_ _ _ _] {:type "string"})
|
||||
(defmethod visit :symbol [_ _ _ _] {:type "string"})
|
||||
(defmethod visit :qualified-symbol [_ _ _ _] {:type "string"})
|
||||
(defmethod visit :uuid [_ _ _ _] {:type "string" :format "uuid"})
|
||||
(defmethod visit ::sm/uuid [_ _ _ _] {:type "string" :format "uuid"})
|
||||
|
||||
(defmethod visit :schema [_ schema children options]
|
||||
(visit ::m/schema schema children options))
|
||||
@@ -124,11 +196,41 @@
|
||||
(defmethod visit ::m/schema [_ schema _ options]
|
||||
(let [result (transform* (m/deref schema) options)
|
||||
defpath (::definitions-path options "#/definitions/")]
|
||||
(if-let [ref (m/-ref schema)]
|
||||
(let [rkey (str/concat (str/camel (namespace ref)) "$" (name ref))]
|
||||
(some-> *definitions* (swap! assoc rkey result))
|
||||
{"$ref" (str/concat defpath rkey)})
|
||||
result)))
|
||||
|
||||
(if (::embed options)
|
||||
result
|
||||
(if-let [ref (m/-ref schema)]
|
||||
(let [nname (namespace ref)
|
||||
tname (name ref)
|
||||
tname (str/capital (str/camel tname))
|
||||
|
||||
nname (cond
|
||||
(or (= nname "app.common.schema")
|
||||
(= nname "app.common.time")
|
||||
(= nname "app.common.features"))
|
||||
""
|
||||
|
||||
(= nname "datoteka.fs")
|
||||
"Filesystem"
|
||||
|
||||
(str/starts-with? nname "app.common.geom")
|
||||
(-> (str/replace nname #"app\.common\.geom\.\w+" "geom")
|
||||
(str/camel)
|
||||
(str/capital))
|
||||
|
||||
(str/starts-with? nname "app.")
|
||||
(-> (subs nname 4)
|
||||
(str/camel)
|
||||
(str/capital))
|
||||
|
||||
:else
|
||||
(str/capital (str/camel nname)))
|
||||
|
||||
rkey (str nname tname)]
|
||||
|
||||
(some-> *definitions* (swap! assoc rkey result))
|
||||
{"$ref" (str/concat defpath rkey)})
|
||||
result))))
|
||||
|
||||
(defmethod visit :merge [_ schema _ options] (transform* (m/deref schema) options))
|
||||
(defmethod visit :union [_ schema _ options] (transform* (m/deref schema) options))
|
||||
|
||||
@@ -307,6 +307,7 @@
|
||||
|
||||
file' (thf/apply-changes file changes)]
|
||||
(when new-shape-label
|
||||
(thi/rm-id! (:id new-shape))
|
||||
(thi/set-id! new-shape-label (:id new-shape)))
|
||||
(if propagate-fn
|
||||
(propagate-fn file')
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
[label id]
|
||||
(swap! idmap assoc label id))
|
||||
|
||||
(defn rm-id!
|
||||
[id]
|
||||
(swap! idmap #(into {} (remove (comp #{id} val) %))))
|
||||
|
||||
(defn new-id!
|
||||
[label]
|
||||
(let [id (uuid/next)]
|
||||
|
||||
@@ -77,22 +77,25 @@
|
||||
[file shape-label token-name token-attrs shape-attrs resolved-value]
|
||||
(let [page (thf/current-page file)
|
||||
shape (ths/get-shape file shape-label)
|
||||
shape' (as-> shape $
|
||||
(cto/apply-token-to-shape {:shape $
|
||||
:token {:name token-name}
|
||||
:attributes token-attrs})
|
||||
(reduce (fn [shape attr]
|
||||
(case attr
|
||||
:stroke-width (set-stroke-width shape resolved-value)
|
||||
:stroke-color (set-stroke-color shape resolved-value)
|
||||
:fill (set-fill-color shape resolved-value)
|
||||
(ctn/set-shape-attr shape attr resolved-value {:ignore-touched true})))
|
||||
$
|
||||
shape-attrs))]
|
||||
shape' (when shape
|
||||
(as-> shape $
|
||||
(cto/apply-token-to-shape {:shape $
|
||||
:token {:name token-name}
|
||||
:attributes token-attrs})
|
||||
(reduce (fn [shape attr]
|
||||
(case attr
|
||||
:stroke-width (set-stroke-width shape resolved-value)
|
||||
:stroke-color (set-stroke-color shape resolved-value)
|
||||
:fill (set-fill-color shape resolved-value)
|
||||
(ctn/set-shape-attr shape attr resolved-value {:ignore-touched true})))
|
||||
$
|
||||
shape-attrs)))]
|
||||
|
||||
(ctf/update-file-data
|
||||
file
|
||||
(fn [file-data]
|
||||
(ctpl/update-page file-data
|
||||
(:id page)
|
||||
#(ctst/set-shape % shape'))))))
|
||||
(if shape'
|
||||
(ctf/update-file-data
|
||||
file
|
||||
(fn [file-data]
|
||||
(ctpl/update-page file-data
|
||||
(:id page)
|
||||
#(ctst/set-shape % shape'))))
|
||||
file)))
|
||||
|
||||
@@ -56,6 +56,22 @@
|
||||
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property 1" :value "Value2"}]}))))
|
||||
|
||||
|
||||
(defn add-variant-with-copy
|
||||
[file variant-label component1-label root1-label component2-label root2-label child1-label child2-label component-copy-label]
|
||||
(let [file (ths/add-sample-shape file variant-label :type :frame :is-variant-container true)
|
||||
variant-id (thi/id variant-label)]
|
||||
(-> file
|
||||
(ths/add-sample-shape root2-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value2")
|
||||
(ths/add-sample-shape root1-label :type :frame :parent-label variant-label :variant-id variant-id :variant-name "Value1")
|
||||
(thc/instantiate-component component-copy-label child1-label :parent-label root1-label)
|
||||
(thc/instantiate-component component-copy-label child2-label :parent-label root2-label)
|
||||
(thc/make-component component1-label root1-label)
|
||||
(thc/update-component component1-label {:variant-id variant-id :variant-properties [{:name "Property 1" :value "Value1"}]})
|
||||
(thc/make-component component2-label root2-label)
|
||||
(thc/update-component component2-label {:variant-id variant-id :variant-properties [{:name "Property 1" :value "Value2"}]}))))
|
||||
|
||||
|
||||
|
||||
(defn add-variant-with-text
|
||||
[file variant-label component1-label root1-label component2-label root2-label child1-label child2-label text1 text2
|
||||
& {:keys [text1-params text2-params]}]
|
||||
|
||||
@@ -119,9 +119,9 @@
|
||||
[o]
|
||||
(instance? Duration o)))
|
||||
|
||||
(defn duration
|
||||
[ms-or-obj]
|
||||
#?(:clj
|
||||
#?(:clj
|
||||
(defn duration
|
||||
[ms-or-obj]
|
||||
(cond
|
||||
(string? ms-or-obj)
|
||||
(Duration/parse (str "PT" ms-or-obj))
|
||||
@@ -134,10 +134,7 @@
|
||||
(Duration/ofMillis ms-or-obj)
|
||||
|
||||
:else
|
||||
(obj->duration ms-or-obj))
|
||||
|
||||
:cljs
|
||||
(clj->js ms-or-obj)))
|
||||
(obj->duration ms-or-obj))))
|
||||
|
||||
#?(:clj
|
||||
(defn parse-duration
|
||||
@@ -262,6 +259,9 @@
|
||||
(defn inst
|
||||
[s]
|
||||
(cond
|
||||
(nil? s)
|
||||
s
|
||||
|
||||
(inst? s)
|
||||
s
|
||||
|
||||
@@ -292,7 +292,7 @@
|
||||
|
||||
(defn plus
|
||||
[d ta]
|
||||
(let [ta (duration ta)]
|
||||
(let [ta #?(:clj (duration ta) :cljs ta)]
|
||||
(cond
|
||||
#?@(:clj [(duration? d) (.plus ^Duration d ^TemporalAmount ta)])
|
||||
|
||||
@@ -307,7 +307,7 @@
|
||||
|
||||
(defn minus
|
||||
[d ta]
|
||||
(let [^TemporalAmount ta (duration ta)]
|
||||
(let [ta #?(:clj (duration ta) :cljs ta)]
|
||||
(cond
|
||||
#?@(:clj [(duration? d) (.minus ^Duration d ^TemporalAmount ta)])
|
||||
|
||||
@@ -429,3 +429,8 @@
|
||||
:encode/json format-duration
|
||||
::oapi/type "string"
|
||||
::oapi/format "duration"}})))
|
||||
|
||||
#?(:cljs
|
||||
(extend-protocol cljs.core/IEncodeJS
|
||||
js/Date
|
||||
(-clj->js [x] x)))
|
||||
@@ -60,16 +60,17 @@
|
||||
{:type ::hex-color
|
||||
:pred hex-color-string?
|
||||
:type-properties
|
||||
{:title "hex-color"
|
||||
{:title "HexColor"
|
||||
:description "HEX Color String"
|
||||
:error/message "expected a valid HEX color"
|
||||
:error/code "errors.invalid-hex-color"
|
||||
:gen/gen hex-color-generator
|
||||
::oapi/type "integer"
|
||||
::oapi/format "int64"}}))
|
||||
::oapi/type "string"
|
||||
::oapi/format "rgb"}}))
|
||||
|
||||
(def schema:plain-color
|
||||
[:map [:color schema:hex-color]])
|
||||
[:map {:title "PlainColorAttrs"}
|
||||
[:color schema:hex-color]])
|
||||
|
||||
(def schema:image
|
||||
[:map {:title "ImageColor" :closed true}
|
||||
@@ -85,7 +86,8 @@
|
||||
(sm/keys schema:image))
|
||||
|
||||
(def schema:image-color
|
||||
[:map [:image schema:image]])
|
||||
[:map {:title "ImageColorAttrs"}
|
||||
[:image schema:image]])
|
||||
|
||||
(def gradient-types
|
||||
#{:linear :radial})
|
||||
@@ -110,10 +112,11 @@
|
||||
(sm/keys schema:gradient))
|
||||
|
||||
(def schema:gradient-color
|
||||
[:map [:gradient schema:gradient]])
|
||||
[:map {:title "GradientColorAttrs"}
|
||||
[:gradient schema:gradient]])
|
||||
|
||||
(def schema:color-attrs
|
||||
[:map {:title "ColorAttrs" :closed true}
|
||||
[:map {:title "GenericColorAttrs" :closed true}
|
||||
[:opacity {:optional true} [::sm/number {:min 0 :max 1}]]
|
||||
[:ref-id {:optional true} ::sm/uuid]
|
||||
[:ref-file {:optional true} ::sm/uuid]])
|
||||
@@ -132,13 +135,13 @@
|
||||
(into required-color-attrs (sm/keys schema:color-attrs)))
|
||||
|
||||
(def schema:library-color-attrs
|
||||
[:map {:title "ColorAttrs" :closed true}
|
||||
[:map {:title "LibraryColorAttrs" :closed true}
|
||||
[:id ::sm/uuid]
|
||||
[:name ::sm/text]
|
||||
[:path {:optional true} :string]
|
||||
[:opacity {:optional true} [::sm/number {:min 0 :max 1}]]
|
||||
[:modified-at {:optional true} ::ct/inst]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]])
|
||||
[:plugin-data {:optional true} ctpg/schema:plugin-data]])
|
||||
|
||||
(def schema:library-color
|
||||
"Used for in-transit representation of a color (per example when user
|
||||
|
||||
@@ -19,19 +19,17 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def schema:component
|
||||
(sm/register!
|
||||
^{::sm/type ::component}
|
||||
[:merge
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:path {:optional true} [:maybe :string]]
|
||||
[:modified-at {:optional true} ::ct/inst]
|
||||
[:objects {:gen/max 10 :optional true} ctp/schema:objects]
|
||||
[:main-instance-id ::sm/uuid]
|
||||
[:main-instance-page ::sm/uuid]
|
||||
[:plugin-data {:optional true} ctpg/schema:plugin-data]]
|
||||
ctv/schema:variant-component]))
|
||||
[:merge
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:path {:optional true} [:maybe :string]]
|
||||
[:modified-at {:optional true} ::ct/inst]
|
||||
[:objects {:gen/max 10 :optional true} ctp/schema:objects]
|
||||
[:main-instance-id ::sm/uuid]
|
||||
[:main-instance-page ::sm/uuid]
|
||||
[:plugin-data {:optional true} ctpg/schema:plugin-data]]
|
||||
ctv/schema:variant-component])
|
||||
|
||||
(def check-component
|
||||
(sm/check-fn schema:component))
|
||||
@@ -99,6 +97,8 @@
|
||||
:exports :exports-group
|
||||
:grids :grids-group
|
||||
|
||||
:show-content :show-content
|
||||
|
||||
:layout :layout-container
|
||||
|
||||
:layout-align-content :layout-align-content
|
||||
|
||||
@@ -389,12 +389,13 @@
|
||||
[(remap-ids new-shape)
|
||||
(map remap-ids new-shapes)])))
|
||||
|
||||
(defn get-first-not-copy-parent
|
||||
"Go trough the parents until we find a shape that is not a copy of a component."
|
||||
(defn get-first-valid-parent
|
||||
"Go trough the parents until we find a shape that is not a copy of a component nor
|
||||
a variant container."
|
||||
[objects id]
|
||||
(let [shape (get objects id)]
|
||||
(if (ctk/in-component-copy? shape)
|
||||
(get-first-not-copy-parent objects (:parent-id shape))
|
||||
(if (or (ctk/in-component-copy? shape) (ctk/is-variant-container? shape))
|
||||
(get-first-valid-parent objects (:parent-id shape))
|
||||
shape)))
|
||||
|
||||
(defn has-any-copy-parent?
|
||||
@@ -425,7 +426,6 @@
|
||||
(not (has-any-main? objects shape))
|
||||
(not (has-any-copy-parent? objects shape))))
|
||||
|
||||
|
||||
(defn collect-main-shapes [shape objects]
|
||||
(if (ctk/main-instance? shape)
|
||||
[shape]
|
||||
@@ -433,7 +433,11 @@
|
||||
(mapcat collect-main-shapes children objects)
|
||||
[])))
|
||||
|
||||
(defn- invalid-structure-for-component?
|
||||
(defn get-component-from-shape
|
||||
[shape libraries]
|
||||
(get-in libraries [(:component-file shape) :data :components (:component-id shape)]))
|
||||
|
||||
(defn invalid-structure-for-component?
|
||||
"Check if the structure generated nesting children in parent is invalid in terms of nested components"
|
||||
[objects parent children pasting? libraries]
|
||||
(let [; If the original shapes had been cutted, and we are pasting them now, they aren't
|
||||
@@ -445,7 +449,7 @@
|
||||
; original component doesn't exist or is deleted. So for this function purposes, they
|
||||
; are removed from the list
|
||||
remove? (fn [shape]
|
||||
(let [component (get-in libraries [(:component-file shape) :data :components (:component-id shape)])]
|
||||
(let [component (get-component-from-shape shape libraries)]
|
||||
(and component (not (:deleted component)))))
|
||||
|
||||
selected-components (cond->> (mapcat collect-main-shapes children objects)
|
||||
@@ -475,17 +479,17 @@
|
||||
(letfn [(get-frame [parent-id]
|
||||
(if (cfh/frame-shape? objects parent-id) parent-id (get-in objects [parent-id :frame-id])))]
|
||||
(let [parent (get objects parent-id)
|
||||
;; We can always move the children to the parent they already have.
|
||||
;; But if we are pasting, those are new items, so it is considered a change
|
||||
no-changes?
|
||||
(and (every? #(= parent-id (:parent-id %)) children)
|
||||
(not pasting?))
|
||||
|
||||
;; When pasting frames, children have the frames and their children
|
||||
;; We need to check only the top shapes
|
||||
children-ids (set (map :id children))
|
||||
top-children (remove #(contains? children-ids (:parent-id %)) children)
|
||||
|
||||
;; We can always move the children to the parent they already have.
|
||||
;; But if we are pasting, those are new items, so it is considered a change
|
||||
no-changes?
|
||||
(and (every? #(= parent-id (:parent-id %)) top-children)
|
||||
(not pasting?))
|
||||
|
||||
;; Are all the top-children a main-instance of a component?
|
||||
all-main?
|
||||
(every? ctk/main-instance? top-children)
|
||||
|
||||
@@ -110,7 +110,6 @@
|
||||
|
||||
(sm/register! ::data schema:data)
|
||||
(sm/register! ::file schema:file)
|
||||
(sm/register! ::media schema:media)
|
||||
(sm/register! ::colors schema:colors)
|
||||
(sm/register! ::typographies schema:typographies)
|
||||
|
||||
@@ -156,7 +155,7 @@
|
||||
|
||||
(defn make-file
|
||||
[{:keys [id project-id name revn is-shared features migrations
|
||||
metadata backend ignore-sync-until created-at modified-at deleted-at]
|
||||
ignore-sync-until created-at modified-at deleted-at]
|
||||
:as params}
|
||||
|
||||
& {:keys [create-page with-data page-id]
|
||||
@@ -187,9 +186,8 @@
|
||||
:data data
|
||||
:features features
|
||||
:migrations migrations
|
||||
:metadata metadata
|
||||
:backend backend
|
||||
:ignore-sync-until ignore-sync-until
|
||||
:has-media-trimmed false
|
||||
:created-at created-at
|
||||
:modified-at modified-at
|
||||
:deleted-at deleted-at})]
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
(c/assoc position fill)))
|
||||
(if (nil? fills)
|
||||
[fill]
|
||||
(-> (coerce fills)
|
||||
(-> fills
|
||||
(c/assoc position fill)))))
|
||||
|
||||
(defn update
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(ns app.common.types.fills.impl
|
||||
(:require
|
||||
#?(:clj [clojure.data.json :as json])
|
||||
#?(:cljs [app.common.weak-map :as weak-map])
|
||||
#?(:cljs [app.common.weak :as weak])
|
||||
[app.common.buffer :as buf]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
@@ -443,7 +443,7 @@
|
||||
:code :invalid-fill
|
||||
:hint "found invalid fill on encoding fills to binary format")))))
|
||||
|
||||
#?(:cljs (Fills. total dbuffer mbuffer image-ids (weak-map/create) nil)
|
||||
#?(:cljs (Fills. total dbuffer mbuffer image-ids (weak/weak-value-map) nil)
|
||||
:clj (Fills. total dbuffer mbuffer nil))))))
|
||||
|
||||
(defn fills?
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def schema:grid-color
|
||||
[:map {:title "PageGridColor"}
|
||||
[:map {:title "GridColor"}
|
||||
[:color clr/schema:hex-color]
|
||||
[:opacity ::sm/safe-number]])
|
||||
|
||||
(def schema:column-params
|
||||
[:map
|
||||
[:map {:title "ColumnGridParams"}
|
||||
[:color schema:grid-color]
|
||||
[:type {:optional true} [::sm/one-of #{:stretch :left :center :right}]]
|
||||
[:size {:optional true} [:maybe ::sm/safe-number]]
|
||||
@@ -28,7 +28,7 @@
|
||||
[:gutter {:optional true} [:maybe ::sm/safe-number]]])
|
||||
|
||||
(def schema:square-params
|
||||
[:map
|
||||
[:map {:title "SquareGridParams"}
|
||||
[:size {:optional true} [:maybe ::sm/safe-number]]
|
||||
[:color schema:grid-color]])
|
||||
|
||||
@@ -37,33 +37,28 @@
|
||||
:dispatch :type
|
||||
:decode/json #(update % :type keyword)}
|
||||
[:column
|
||||
[:map
|
||||
[:map {:title "ColumnGridAttrs"}
|
||||
[:type [:= :column]]
|
||||
[:display :boolean]
|
||||
[:params schema:column-params]]]
|
||||
|
||||
[:row
|
||||
[:map
|
||||
[:map {:title "RowGridAttrs"}
|
||||
[:type [:= :row]]
|
||||
[:display :boolean]
|
||||
[:params schema:column-params]]]
|
||||
|
||||
[:square
|
||||
[:map
|
||||
[:map {:title "SquareGridAttrs"}
|
||||
[:type [:= :square]]
|
||||
[:display :boolean]
|
||||
[:params schema:square-params]]]])
|
||||
|
||||
(def schema:default-grids
|
||||
[:map {:title "PageGrid"}
|
||||
[:square {:optional true} ::square-params]
|
||||
[:row {:optional true} ::column-params]
|
||||
[:column {:optional true} ::column-params]])
|
||||
|
||||
(sm/register! ::square-params schema:square-params)
|
||||
(sm/register! ::column-params schema:column-params)
|
||||
(sm/register! ::grid schema:grid)
|
||||
(sm/register! ::default-grids schema:default-grids)
|
||||
[:square {:optional true} schema:square-params]
|
||||
[:row {:optional true} schema:column-params]
|
||||
[:column {:optional true} schema:column-params]])
|
||||
|
||||
(def ^:private default-square-params
|
||||
{:size 16
|
||||
|
||||
@@ -466,7 +466,12 @@
|
||||
(dm/assert! (#{:width :height} attr))
|
||||
(dm/assert! (number? value))
|
||||
|
||||
(let [{:keys [proportion proportion-lock]} shape
|
||||
(let [;; Avoid havig shapes with zero size
|
||||
value (if (< (mth/abs value) 0.01)
|
||||
0.01
|
||||
value)
|
||||
|
||||
{:keys [proportion proportion-lock]} shape
|
||||
size (select-keys (:selrect shape) [:width :height])
|
||||
new-size (if-not (and (not ignore-lock?) proportion-lock)
|
||||
(assoc size attr value)
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
[:map-of {:gen/max 2} ::sm/uuid schema:guide])
|
||||
|
||||
(def schema:objects
|
||||
[:map-of {:gen/max 5} ::sm/uuid ::cts/shape])
|
||||
[:map-of {:gen/max 5} ::sm/uuid cts/schema:shape])
|
||||
|
||||
(def schema:comment-thread-position
|
||||
[:map {:title "CommentThreadPosition"}
|
||||
@@ -62,11 +62,6 @@
|
||||
[:comment-thread-positions {:optional true}
|
||||
[:map-of ::sm/uuid schema:comment-thread-position]]])
|
||||
|
||||
(sm/register! ::objects schema:objects)
|
||||
(sm/register! ::page schema:page)
|
||||
(sm/register! ::guide schema:guide)
|
||||
(sm/register! ::flow schema:flow)
|
||||
|
||||
(def valid-guide?
|
||||
(sm/lazy-validator schema:guide))
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
(def schema:segments impl/schema:segments)
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TRANSFORMATIONS
|
||||
;; CONSTRUCTORS & TYPE METHODS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn content?
|
||||
@@ -55,6 +55,10 @@
|
||||
[data]
|
||||
(impl/from-string data))
|
||||
|
||||
(defn from-plain
|
||||
[data]
|
||||
(impl/from-plain data))
|
||||
|
||||
(defn check-content
|
||||
[content]
|
||||
(impl/check-content content))
|
||||
@@ -189,6 +193,12 @@
|
||||
[content]
|
||||
(some-> content segment/get-points))
|
||||
|
||||
(defn calc-selrect
|
||||
"Calculate selrect from a content. The content can be in a PathData
|
||||
instance or plain vector of segments."
|
||||
[content]
|
||||
(segment/content->selrect content))
|
||||
|
||||
(defn- calc-bool-content*
|
||||
"Calculate the boolean content from shape and objects. Returns plain
|
||||
vector of segments"
|
||||
|
||||
@@ -393,17 +393,15 @@
|
||||
defined by the constant num-segments"
|
||||
[start end h1 h2]
|
||||
(let [offset (/ 1 num-segments)
|
||||
tp (fn [t] (curve-values start end h1 h2 t))]
|
||||
(loop [from 0
|
||||
tp (fn [t] (curve-values start end h1 h2 t))]
|
||||
(loop [from 0.0
|
||||
result []]
|
||||
|
||||
(let [to (min 1 (+ from offset))
|
||||
line [(tp from) (tp to)]
|
||||
(let [to (mth/min 1.0 (+ from offset))
|
||||
line [(tp from) (tp to)]
|
||||
result (conj result line)]
|
||||
|
||||
(if (>= to 1)
|
||||
(if (>= to 1.0)
|
||||
result
|
||||
(recur to result))))))
|
||||
(recur (double to) result))))))
|
||||
|
||||
(defn curve-split
|
||||
"Splits a curve into two at the given parametric value `t`.
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
(:require
|
||||
#?(:clj [app.common.fressian :as fres])
|
||||
#?(:clj [clojure.data.json :as json])
|
||||
#?(:cljs [app.common.weak-map :as weak-map])
|
||||
#?(:cljs [app.common.weak :as weak])
|
||||
[app.common.buffer :as buf]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.openapi :as oapi]
|
||||
[app.common.svg.path :as svg.path]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.path :as-alias path]
|
||||
@@ -378,7 +379,7 @@
|
||||
(-transform [this m]
|
||||
(let [buffer (buf/clone buffer)]
|
||||
(impl-transform buffer m size)
|
||||
(PathData. size buffer (weak-map/create) nil)))
|
||||
(PathData. size buffer (weak/weak-value-map) nil)))
|
||||
|
||||
(-walk [_ f initial]
|
||||
(impl-walk buffer f initial size))
|
||||
@@ -537,7 +538,8 @@
|
||||
(sg/fmap from-plain))]
|
||||
{:pred path-data?
|
||||
:type-properties
|
||||
{:gen/gen generator
|
||||
{::oapi/type "string"
|
||||
:gen/gen generator
|
||||
:encode/json identity
|
||||
:decode/json (fn [s]
|
||||
(cond
|
||||
@@ -598,14 +600,14 @@
|
||||
count (long (/ size SEGMENT-U8-SIZE))]
|
||||
(PathData. count
|
||||
(js/DataView. buffer)
|
||||
(weak-map/create)
|
||||
(weak/weak-value-map)
|
||||
nil))
|
||||
|
||||
(instance? js/DataView buffer)
|
||||
(let [buffer' (.-buffer ^js/DataView buffer)
|
||||
size (.-byteLength ^js/ArrayBuffer buffer')
|
||||
count (long (/ size SEGMENT-U8-SIZE))]
|
||||
(PathData. count buffer (weak-map/create) nil))
|
||||
(PathData. count buffer (weak/weak-value-map) nil))
|
||||
|
||||
(instance? js/Uint8Array buffer)
|
||||
(from-bytes (.-buffer buffer))
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.common.types.plugins
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -22,15 +21,13 @@
|
||||
:keyword])
|
||||
|
||||
(def schema:plugin-data
|
||||
(sm/register!
|
||||
^{::sm/type ::plugin-data}
|
||||
[:map-of {:gen/max 5 :title "PluginsData"}
|
||||
schema:keyword
|
||||
[:map-of {:gen/max 5}
|
||||
schema:keyword
|
||||
[:map-of {:gen/max 5}
|
||||
schema:string
|
||||
schema:string]]))
|
||||
schema:string
|
||||
schema:string]])
|
||||
|
||||
(def ^:private schema:registry-entry
|
||||
(def schema:registry-entry
|
||||
[:map
|
||||
[:plugin-id :string]
|
||||
[:name :string]
|
||||
@@ -47,6 +44,3 @@
|
||||
[:map-of {:gen/max 5}
|
||||
:string
|
||||
schema:registry-entry]]])
|
||||
|
||||
(sm/register! ::plugin-registry schema:plugin-registry)
|
||||
(sm/register! ::registry-entry schema:registry-entry)
|
||||
|
||||
23
common/src/app/common/types/profile.cljc
Normal file
23
common/src/app/common/types/profile.cljc
Normal file
@@ -0,0 +1,23 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.common.types.profile
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as cm]))
|
||||
|
||||
(def schema:profile
|
||||
[:map {:title "Profile"}
|
||||
[:id ::sm/uuid]
|
||||
[:created-at {:optional true} ::cm/inst]
|
||||
[:fullname {:optional true} :string]
|
||||
[:email {:optional true} :string]
|
||||
[:lang {:optional true} :string]
|
||||
[:theme {:optional true} :string]
|
||||
[:photo-id {:optional true} ::sm/uuid]
|
||||
;; Only present on resolved profile objects, the resolve process
|
||||
;; takes the photo-id or geneates an image from the name
|
||||
[:photo-url {:optional true} :string]])
|
||||
@@ -22,7 +22,6 @@
|
||||
[app.common.types.fills :refer [schema:fill fill->color]]
|
||||
[app.common.types.grid :as ctg]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.path.segment :as path.segment]
|
||||
[app.common.types.plugins :as ctpg]
|
||||
[app.common.types.shape.attrs :refer [default-color]]
|
||||
[app.common.types.shape.blur :as ctsb]
|
||||
@@ -119,8 +118,6 @@
|
||||
(def schema:points
|
||||
[:vector {:gen/max 4 :gen/min 4} ::gpt/point])
|
||||
|
||||
;; FIXME: the register is necessary until this is moved to a separated
|
||||
;; ns because it is used on shapes.text
|
||||
(def valid-stroke-attrs
|
||||
"A set used for proper check if color should contain only one of the
|
||||
attrs listed in this set."
|
||||
@@ -156,10 +153,8 @@
|
||||
(sm/keys schema:stroke-attrs))
|
||||
|
||||
(def schema:stroke
|
||||
(sm/register!
|
||||
^{::sm/type ::stroke}
|
||||
[:and schema:stroke-attrs
|
||||
[:fn has-valid-stroke-attrs?]]))
|
||||
[:and schema:stroke-attrs
|
||||
[:fn has-valid-stroke-attrs?]])
|
||||
|
||||
(def check-stroke
|
||||
(sm/check-fn schema:stroke))
|
||||
@@ -184,7 +179,7 @@
|
||||
[:height ::sm/safe-number]])
|
||||
|
||||
(def schema:shape-generic-attrs
|
||||
[:map {:title "ShapeAttrs"}
|
||||
[:map {:title "ShapeGenericAttrs"}
|
||||
[:page-id {:optional true} ::sm/uuid]
|
||||
[:component-id {:optional true} ::sm/uuid]
|
||||
[:component-file {:optional true} ::sm/uuid]
|
||||
@@ -213,22 +208,22 @@
|
||||
[:r4 {:optional true} ::sm/safe-number]
|
||||
[:opacity {:optional true} ::sm/safe-number]
|
||||
[:grids {:optional true}
|
||||
[:vector {:gen/max 2} ::ctg/grid]]
|
||||
[:vector {:gen/max 2} ctg/schema:grid]]
|
||||
[:exports {:optional true}
|
||||
[:vector {:gen/max 2} ::ctse/export]]
|
||||
[:vector {:gen/max 2} ctse/schema:export]]
|
||||
[:strokes {:optional true}
|
||||
[:vector {:gen/max 2} schema:stroke]]
|
||||
[:blend-mode {:optional true}
|
||||
[::sm/one-of blend-modes]]
|
||||
[:interactions {:optional true}
|
||||
[:vector {:gen/max 2} ::ctsi/interaction]]
|
||||
[:vector {:gen/max 2} ctsi/schema:interaction]]
|
||||
[:shadow {:optional true}
|
||||
[:vector {:gen/max 1} ctss/schema:shadow]]
|
||||
[:blur {:optional true} ::ctsb/blur]
|
||||
[:blur {:optional true} ctsb/schema:blur]
|
||||
[:grow-type {:optional true}
|
||||
[::sm/one-of grow-types]]
|
||||
[:applied-tokens {:optional true} cto/schema:applied-tokens]
|
||||
[:plugin-data {:optional true} ::ctpg/plugin-data]])
|
||||
[:plugin-data {:optional true} ctpg/schema:plugin-data]])
|
||||
|
||||
(def schema:group-attrs
|
||||
[:map {:title "GroupAttrs"}
|
||||
@@ -274,7 +269,8 @@
|
||||
|
||||
(def ^:private schema:text-attrs
|
||||
[:map {:title "TextAttrs"}
|
||||
[:content {:optional true} [:maybe ::ctsx/content]]])
|
||||
[:position-data {:optional true} [:maybe ctsx/schema:position-data]]
|
||||
[:content {:optional true} [:maybe ctsx/schema:content]]])
|
||||
|
||||
(defn- decode-shape
|
||||
[o]
|
||||
@@ -326,8 +322,8 @@
|
||||
schema:shape-generic-attrs
|
||||
schema:shape-geom-attrs
|
||||
schema:shape-base-attrs
|
||||
::ctv/variant-shape
|
||||
::ctv/variant-container]]
|
||||
ctv/schema:variant-shape
|
||||
ctv/schema:variant-container]]
|
||||
|
||||
[:bool
|
||||
[:merge {:title "BoolShape"}
|
||||
@@ -384,13 +380,11 @@
|
||||
schema:shape-base-attrs]]])
|
||||
|
||||
(def schema:shape
|
||||
(sm/register!
|
||||
^{::sm/type ::shape}
|
||||
[:and {:title "Shape"
|
||||
:gen/gen (shape-generator)
|
||||
:decode/json {:leave decode-shape}}
|
||||
[:fn shape?]
|
||||
schema:shape-attrs]))
|
||||
[:and {:title "Shape"
|
||||
:gen/gen (shape-generator)
|
||||
:decode/json {:leave decode-shape}}
|
||||
[:fn shape?]
|
||||
schema:shape-attrs])
|
||||
|
||||
(def check-shape-generic-attrs
|
||||
(sm/check-fn schema:shape-generic-attrs))
|
||||
@@ -418,7 +412,7 @@
|
||||
#{:page-id :component-id :component-file :component-root :main-instance
|
||||
:remote-synced :shape-ref :touched :blocked :collapsed :locked
|
||||
:hidden :masked-group :fills :proportion :proportion-lock :constraints-h
|
||||
:constraints-v :fixed-scroll :r1 :r2 :r3 :r4 :opacity :grids :exports
|
||||
:constraints-v :fixed-scroll :r1 :r2 :r3 :r4 :rotation :opacity :grids :exports
|
||||
:strokes :blend-mode :interactions :shadow :blur :grow-type :applied-tokens
|
||||
:plugin-data})
|
||||
|
||||
@@ -593,12 +587,16 @@
|
||||
(defn setup-path
|
||||
[{:keys [content selrect points] :as shape}]
|
||||
(let [selrect (or selrect
|
||||
(path.segment/content->selrect content)
|
||||
(path/calc-selrect content)
|
||||
(grc/make-rect))
|
||||
points (or points (grc/rect->points selrect))]
|
||||
points (or points
|
||||
(grc/rect->points selrect))
|
||||
;; Ensure we hace correct type here for Path Data
|
||||
content (path/content content)]
|
||||
(-> shape
|
||||
(assoc :selrect selrect)
|
||||
(assoc :points points))))
|
||||
(assoc :points points)
|
||||
(assoc :content content))))
|
||||
|
||||
(defn- setup-image
|
||||
[{:keys [metadata] :as shape}]
|
||||
|
||||
@@ -6,30 +6,11 @@
|
||||
|
||||
(ns app.common.types.shape.blur
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.spec :as us]
|
||||
[clojure.spec.alpha :as s]))
|
||||
[app.common.schema :as sm]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SPEC
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::id uuid?)
|
||||
(s/def ::type #{:layer-blur})
|
||||
(s/def ::value ::us/safe-number)
|
||||
(s/def ::hidden boolean?)
|
||||
|
||||
(s/def ::blur
|
||||
(s/keys :req-un [::id ::type ::value ::hidden]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMA
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(sm/register!
|
||||
^{::sm/type ::blur}
|
||||
[:map {:title "Blur"}
|
||||
[:id ::sm/uuid]
|
||||
[:type [:= :layer-blur]]
|
||||
[:value ::sm/safe-number]
|
||||
[:hidden :boolean]])
|
||||
(def schema:blur
|
||||
[:map {:title "Blur"}
|
||||
[:id ::sm/uuid]
|
||||
[:type [:= :layer-blur]]
|
||||
[:value ::sm/safe-number]
|
||||
[:hidden :boolean]])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user