Compare commits

...

123 Commits

Author SHA1 Message Date
Cats Juice
ea17e86032 feat(core): ignore empty journals for page lists (#5744) 2024-01-30 17:21:20 +08:00
Joooye_34
48cd8999bd fix: static resource not found in web server (#5745) 2024-01-30 17:21:05 +08:00
李华桥
cdf1d9002e Merge branch 'canary' into stable 2024-01-29 17:53:10 +08:00
LongYinan
588b3bcf33 chore: revert "chore: bump up @reforged/maker-appimage version to v4" (#5736)
link #5709
This reverts commit 59788aa334.
2024-01-29 08:41:35 +00:00
Yifeng Wang
1cf902bdb6 feat: bump blocksuite (#5735) 2024-01-29 16:40:23 +08:00
DarkSky
fc8a48fb43 feat: add business blob limit (#5734) 2024-01-29 08:32:35 +00:00
LongYinan
0044be972f chore: bump up @blocksuite/icons version to v2.1.44 (#5732)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@blocksuite/icons](https://togithub.com/toeverything/icons) | [`2.1.43` -> `2.1.44`](https://renovatebot.com/diffs/npm/@blocksuite%2ficons/2.1.43/2.1.44) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@blocksuite%2ficons/2.1.44?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@blocksuite%2ficons/2.1.44?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@blocksuite%2ficons/2.1.43/2.1.44?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@blocksuite%2ficons/2.1.43/2.1.44?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>toeverything/icons (@&#8203;blocksuite/icons)</summary>

### [`v2.1.44`](13299f7ede...bc31d70961)

[Compare Source](13299f7ede...bc31d70961)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xNTMuMiIsInVwZGF0ZWRJblZlciI6IjM3LjE1My4yIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-29 07:39:17 +00:00
LongYinan
5bd339bed7 feat: generate blocksuite changelog (#5724) 2024-01-29 07:28:02 +00:00
LongYinan
25e8b8306f fix: path issue on Windows (#5725) 2024-01-29 07:18:25 +00:00
DarkSky
070d5ca471 feat: impl unlimited features (#5659) 2024-01-26 08:28:54 +00:00
Yifeng Wang
04b9029d1b feat: bump blocksuite (#5720)
Co-authored-by: xiaoxin <hongtao.lye@toeverything.info>
2024-01-26 16:27:58 +08:00
Cats Juice
387e292ed9 fix(core): journal title's day tag may be invalid (#5703)
Fix [TOV-470](https://linear.app/affine-design/issue/TOV-470/journal-invalid-date)
2024-01-26 07:55:35 +00:00
JimmFly
18068f4ae2 feat(core): add collection and tag filters to all pages (#5567)
close TOV-69

Added the `filterMode` parameter to the `/all` route.
Abstracted the `PageList` and associated components into more universal ones.
Added the `useTagMetas` hook to get and update  the workspace tags.

https://github.com/toeverything/AFFiNE/assets/102217452/7595944d-a056-40c2-8d89-d8df9e901a4b
2024-01-26 07:42:47 +00:00
Peng Xiao
b867dcbdeb fix: add padding between editor and link panel (#5713)
Fix [AFF-561](https://linear.app/affine-design/issue/AFF-561/bi-directional-link-与文本编辑区域的-top-padding)
2024-01-26 07:32:22 +00:00
Cats Juice
6ca2043697 feat(component): optimize week-date-picker keyboard navigation (#5684) 2024-01-26 07:05:19 +00:00
LongYinan
16ef255f51 chore: upgrade husky to latest (#5719)
- https://github.com/typicode/husky/pull/1336
2024-01-26 06:37:39 +00:00
LongYinan
1cf182b7ca ci: add postUpdateOptions to renovate (#5714) 2024-01-26 06:19:38 +00:00
Peng Xiao
e8a6b6ad5e fix(core): bidi links rendering issue (#5707) 2024-01-26 06:07:04 +00:00
Joooye_34
fd9a7f6aad chore: remove unused blocksuite deps in e2e (#5717) 2024-01-26 05:55:13 +00:00
LongYinan
af45b93d26 chore: bump up husky version to v9 (#5694)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [husky](https://togithub.com/typicode/husky) | [`^8.0.3` -> `^9.0.0`](https://renovatebot.com/diffs/npm/husky/8.0.3/9.0.6) | [![age](https://developer.mend.io/api/mc/badges/age/npm/husky/9.0.6?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/husky/9.0.6?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/husky/8.0.3/9.0.6?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/husky/8.0.3/9.0.6?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>typicode/husky (husky)</summary>

### [`v9.0.6`](https://togithub.com/typicode/husky/releases/tag/v9.0.6)

[Compare Source](https://togithub.com/typicode/husky/compare/v9.0.5...v9.0.6)

-   docs: add favicon by [@&#8203;rakleed](https://togithub.com/rakleed) in [https://github.com/typicode/husky/pull/1354](https://togithub.com/typicode/husky/pull/1354)
-   chore: apply editorconfig to code only by [@&#8203;typicode](https://togithub.com/typicode) in [https://github.com/typicode/husky/pull/1355](https://togithub.com/typicode/husky/pull/1355)
-   docs: update install script to mjs and fix CI checks by [@&#8203;starnayuta](https://togithub.com/starnayuta) in [https://github.com/typicode/husky/pull/1357](https://togithub.com/typicode/husky/pull/1357)
-   Fix  unbound variable by [@&#8203;typicode](https://togithub.com/typicode) in [https://github.com/typicode/husky/pull/1359](https://togithub.com/typicode/husky/pull/1359)

### [`v9.0.5`](https://togithub.com/typicode/husky/releases/tag/v9.0.5)

[Compare Source](https://togithub.com/typicode/husky/compare/v9.0.4...v9.0.5)

-   docs: update path to startup files by [@&#8203;ManuelRauber](https://togithub.com/ManuelRauber) in [https://github.com/typicode/husky/pull/1350](https://togithub.com/typicode/husky/pull/1350)
-   fix: init error by [@&#8203;typicode](https://togithub.com/typicode) in [https://github.com/typicode/husky/pull/1353](https://togithub.com/typicode/husky/pull/1353)

### [`v9.0.4`](https://togithub.com/typicode/husky/releases/tag/v9.0.4)

[Compare Source](https://togithub.com/typicode/husky/compare/v9.0.3...v9.0.4)

-   fix: init create dir before by [@&#8203;typicode](https://togithub.com/typicode) in [https://github.com/typicode/husky/pull/1348](https://togithub.com/typicode/husky/pull/1348)
-   refactor: simplify by [@&#8203;typicode](https://togithub.com/typicode) in [https://github.com/typicode/husky/pull/1349](https://togithub.com/typicode/husky/pull/1349)
-   fix: init not working on pnpm [#&#8203;1334](https://togithub.com/typicode/husky/issues/1334) by [@&#8203;rozbo](https://togithub.com/rozbo) in [https://github.com/typicode/husky/pull/1347](https://togithub.com/typicode/husky/pull/1347)

### [`v9.0.3`](https://togithub.com/typicode/husky/releases/tag/v9.0.3)

[Compare Source](https://togithub.com/typicode/husky/compare/v9.0.2...v9.0.3)

-   docs: fix link by [@&#8203;typicode](https://togithub.com/typicode) in [https://github.com/typicode/husky/pull/1340](https://togithub.com/typicode/husky/pull/1340)
-   chore: fix links in issue template by [@&#8203;julien-f](https://togithub.com/julien-f) in [https://github.com/typicode/husky/pull/1341](https://togithub.com/typicode/husky/pull/1341)
-   fix: add scripts field if not present by [@&#8203;chalkygames123](https://togithub.com/chalkygames123) in [https://github.com/typicode/husky/pull/1338](https://togithub.com/typicode/husky/pull/1338)
-   docs: changelog link by [@&#8203;typicode](https://togithub.com/typicode) in [https://github.com/typicode/husky/pull/1343](https://togithub.com/typicode/husky/pull/1343)
-   fix: insert final newline by [@&#8203;chalkygames123](https://togithub.com/chalkygames123) in [https://github.com/typicode/husky/pull/1339](https://togithub.com/typicode/husky/pull/1339)
-   fix: fix git hooks path on windows by [@&#8203;rozbo](https://togithub.com/rozbo) in [https://github.com/typicode/husky/pull/1346](https://togithub.com/typicode/husky/pull/1346)

### [`v9.0.2`](https://togithub.com/typicode/husky/releases/tag/v9.0.2)

[Compare Source](https://togithub.com/typicode/husky/compare/v9.0.1...v9.0.2)

#### What's Changed

-   fix: exit code by [@&#8203;gergelypap](https://togithub.com/gergelypap) in [https://github.com/typicode/husky/pull/1336](https://togithub.com/typicode/husky/pull/1336)
-   docs: typo by [@&#8203;chalkygames123](https://togithub.com/chalkygames123) in [https://github.com/typicode/husky/pull/1337](https://togithub.com/typicode/husky/pull/1337)

#### New Contributors

-   [@&#8203;gergelypap](https://togithub.com/gergelypap) made their first contribution in [https://github.com/typicode/husky/pull/1336](https://togithub.com/typicode/husky/pull/1336)

**Full Changelog**: https://github.com/typicode/husky/compare/v9.0.1...v9.0.2

### [`v9.0.1`](https://togithub.com/typicode/husky/releases/tag/v9.0.1)

[Compare Source](https://togithub.com/typicode/husky/compare/v8.0.3...v9.0.1)

<p align="center">
Kicking off the year with an exciting update!
</p>

<p align="center">
<img src="https://github.com/typicode/husky/assets/5502029/457ab087-e935-4196-b99f-601ecf37f263" height="400px" alt="" />
</p>

#### TLDR;

Improved user experience and a (even) smaller package size while packing in more features!

#### 👋 By the Way

**I'm available for remote work** (Front-end/Back-end mainly JS/TS but open to other stacks Rails, Go, Elixir). You can contact me at my mail: typicode at gmail 🙂

#### Introducing `husky init`

Adding husky to a project is now easier than ever. Although the installation process was straightforward, it often required consulting the documentation.

##### v8

```shell
npm pkg set scripts.prepare="husky install"
npm run prepare
npx husky add .husky/pre-commit "npm test"
```

##### v9

```shell
npx husky init
```

#### Adding a New Hook

Adding a hook is now as simple as creating a file. This can be accomplished using your favorite editor, a script or a basic `echo` command.

##### v8

```shell
npx husky add  .husky/pre-commit "npm test"
git add --chmod=+x .husky/pre-commit # On Windows
```

##### v9

```shell
echo "npm test" > .husky/pre-commit
```

#### Further Size Reduction

`v8` was already the most compact Git hooks manager at approximately `6kB`.

`v9` takes this a step further, reducing the size to just `3kB`, likely making it the smallest devDependency in your toolkit.

**To give you an idea of how small it is, the biggest file in the project is the MIT license 😄**

#### More to Come

Additional features are in the pipeline for `v9`. Stay tuned 🙌

#### Other Changes

-   **Enhanced security** with CI and npm `--provenance` for safer publishing.
-   Added **`$XDG_CONFIG_HOME`** support. Move `~/.huskyrc` to `~/.config/husky/init.sh` for centralized configuration.
-   **Fixed permission issue for Windows-created hooks**; they no longer need to be executable.
-   Removed `husky install`. Use `husky` or `husky some/dir` for the same functionality (deprecation notice to be added).
-   Modified behavior when `.git` is missing; it now triggers a warning instead of failure.
-   Replaced `HUSKY_DEBUG=1` with `HUSKY=2` for debugging.
-   Updated the Husky API for module usage.
-   Transitioned to `ESM` for module usage.
-   Dropped support for Node 14 and 16.
-   Revamped docs.

#### How to Migrate

`v9` is backward compatible with `v8`, allowing you to freely upgrade and migrate your hooks later.

`package.json`

```diff
{
  "scripts": {
-   "prepare": "husky install"
+   "prepare": "husky"
  }
}
```

`.husky/pre-commit`

```diff
- #!/usr/bin/env sh
- . "$(dirname -- "$0")/_/husky.sh"
npm test
```

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMzUuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEzNS4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-26 02:53:34 +00:00
LongYinan
59788aa334 chore: bump up @reforged/maker-appimage version to v4 (#5709)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@reforged/maker-appimage](https://togithub.com/SpacingBat3/ReForged) | [`^3.3.1` -> `^4.0.0`](https://renovatebot.com/diffs/npm/@reforged%2fmaker-appimage/3.3.1/4.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@reforged%2fmaker-appimage/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@reforged%2fmaker-appimage/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@reforged%2fmaker-appimage/3.3.1/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@reforged%2fmaker-appimage/3.3.1/4.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>SpacingBat3/ReForged (@&#8203;reforged/maker-appimage)</summary>

### [`v4.0.0`](1657b03766...f829e7a954)

[Compare Source](1657b03766...f829e7a954)

### [`v3.3.2`](45d28765c2...1657b03766)

[Compare Source](45d28765c2...1657b03766)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMzUuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEzNS4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-26 02:41:08 +00:00
DarkSky
fdffe90892 fix: consume blob stream correctly (#5706)
- use correctly endpoint in r2
- consume blob stream correctly
2024-01-25 10:59:53 +00:00
LongYinan
db3891ba33 chore: bump up @blocksuite/icons version to v2.1.43 (#5702)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@blocksuite/icons](https://togithub.com/toeverything/icons) | [`2.1.42` -> `2.1.43`](https://renovatebot.com/diffs/npm/@blocksuite%2ficons/2.1.42/2.1.43) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@blocksuite%2ficons/2.1.43?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@blocksuite%2ficons/2.1.43?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@blocksuite%2ficons/2.1.42/2.1.43?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@blocksuite%2ficons/2.1.42/2.1.43?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>toeverything/icons (@&#8203;blocksuite/icons)</summary>

### [`v2.1.43`](fe902db67e...13299f7ede)

[Compare Source](fe902db67e...13299f7ede)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMzUuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEzNS4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-25 08:56:39 +00:00
李华桥
79b39f14d2 Merge branch 'canary' into stable 2024-01-25 13:46:21 +08:00
Cats Juice
e7307d969c chore(core): remove journal experimental flag (#5698) 2024-01-25 05:33:57 +00:00
Joooye_34
bd8c7751db chore: update base version to 0.12.0 (#5695) 2024-01-25 05:17:35 +00:00
李华桥
619420cfd1 chore: recover yarn.lock 2024-01-25 00:38:29 +08:00
李华桥
739e914b5f Merge branch 'canary' into stable 2024-01-25 00:33:28 +08:00
Cats Juice
9aa421d5e1 fix(core): modify journal page title today style, add weekday tag (#5687)
fix [TOV-434](https://linear.app/affine-design/issue/TOV-434/修改-journal-中-「today」的字体大小和日期-title-大小)

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://github.com/toeverything/AFFiNE/assets/39363750/ac0ab085-5ff7-4d27-a0b1-11d97fdd8074">
  <img width="100%" alt="" src="https://github.com/toeverything/AFFiNE/assets/39363750/3b311130-ded2-4651-badd-65dd059b7bc5">
</picture>
2024-01-24 13:07:20 +00:00
Cats Juice
3f96b9778f fix(core): change journal header week-date-picker's max-width (#5688)
Fix [TOV-444](https://linear.app/affine-design/issue/TOV-444/修改-journal-date-picker-max-width)
2024-01-24 12:54:34 +00:00
JimmFly
ad1521fd81 feat(core): adjust member order (#5685)
close #5686

`Owner >> Unaccepted > Admin > Write > Read`

This algorithm contains two parts: firstly, it calculates the weight of the Member (calculateWeight), and then, it uses set weights to sort the Members (useMembers).

In the calculateWeight part, the computation of the weight involves three primary factors:

1. **Permission Level**: The `Owner` is given the highest weight of `4`, followed by `Admin` with a weight of `3`, then `Write` with a weight of `2`, and finally, `Read`, with a weight of `1`.
2. **Acceptance Status**: `Unaccepted` members have a higher weight, this weight is `1`.
3. The weight corresponding to the permission level, if the member does not exist, this weight is 0.

These three `factors` are stored in the factors array and then processed through the reduce function for weighted sum calculation. Among them, `factor * Math.pow(10, arr.length - 1 - index)` means the more significant weight factors (i.e., the earlier factors) will be assigned a higher value.

Sorting rules are primarily based on the weight values obtained from the calculateWeight function. If the weights are identical, it then sorts by name, here assuming the weight of the name being `null` is the highest. If the names are not `null`, they are sorted alphabetically.
2024-01-24 12:44:01 +00:00
liuyi
0f67c683c9 fix(server): add metrics missing attributes (#5682) 2024-01-24 08:06:34 +00:00
JimmFly
25897dc404 feat(workspace): add blob and storage limit (#5535)
close TOV-343 AFF-508 TOV-461 TOV-460 TOV-419

Add `isOverCapacity ` status to detect if blob usage exceeds limits.
Add `onCapacityChange` and `onBlobSet` to monitor if the storage or blob exceeds the capacity limit.
Global modals `LocalQuotaModal` and `CloudQuotaModal` have been added, with the upload size of the blob being limited within the modal components.
The notification component has been adjusted, now you can pass in `action` click events and `actionLabel` .
2024-01-24 07:34:51 +00:00
Chen
c566952e09 feat: bump blocksuite (#5673) 2024-01-24 15:13:12 +08:00
liuyi
151a53c575 fix(server): disable payment module requirements temporarily (#5683) 2024-01-24 03:17:21 +00:00
Peng Xiao
a687e7c0ed fix(core): workspace feature should be workspace specific (#5677)
fix TOV-429
2024-01-24 03:07:51 +00:00
Peng Xiao
994ab96688 fix(playground): storybook story load issue (#5672)
Since the properties adapter now depends on whenLoaded status of the ydoc, we may need to explicitly mark doc as emit if the workspace is created without using a provider.
2024-01-24 02:56:07 +00:00
Peng Xiao
c2a978f0f2 fix: create page button offset issue on non-windows desktop app (#5669)
fix TOV-428
2024-01-24 02:56:04 +00:00
Peng Xiao
f62b67de61 fix(core): should set lang when locale changes (#5679)
this + https://github.com/toeverything/design/pull/110 should fix #5591

fix TOV-457

Reason: HTML requires `lang` to be set to render the correct CJK glyphs.

https://heistak.github.io/your-code-displays-japanese-wrong

<img width="692" alt="image" src="https://github.com/toeverything/AFFiNE/assets/584378/1d350219-5157-42cb-8e98-76d92d55b41e">
2024-01-24 02:43:22 +00:00
Peng Xiao
65b538ee45 chore: bump @toeverything/theme (#5680) 2024-01-24 02:43:19 +00:00
Joooye_34
fecf055867 ci: check yarn dedupe in ci lint stage (#5678) 2024-01-23 12:41:50 +00:00
liuyi
5e9739eb3a fix(server): del staled update count cache if unmatch (#5674) 2024-01-23 16:55:49 +08:00
DarkSky
8bbe2e3bb1 feat: use custom image/preview link (#5584)
dep toeverything/blocksuite#5969
2024-01-23 08:53:57 +00:00
liuyi
62169c59c8 fix(server): del staled update count cache if unmatch (#5674) 2024-01-23 08:19:29 +00:00
Cats Juice
8300df4a26 fix(core): correct typo in onboarding 'get start' (#5666) 2024-01-23 04:54:22 +00:00
Cats Juice
6c8621bdcd chore(core): bump @toeverything/theme, add cssVar use case (#5667) 2024-01-23 04:39:07 +00:00
LongYinan
021105c115 chore: bump up @blocksuite/icons version to v2.1.42 (#5668)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@blocksuite/icons](https://togithub.com/toeverything/icons) | [`2.1.41` -> `2.1.42`](https://renovatebot.com/diffs/npm/@blocksuite%2ficons/2.1.41/2.1.42) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@blocksuite%2ficons/2.1.42?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@blocksuite%2ficons/2.1.42?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@blocksuite%2ficons/2.1.41/2.1.42?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@blocksuite%2ficons/2.1.41/2.1.42?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>toeverything/icons (@&#8203;blocksuite/icons)</summary>

### [`v2.1.42`](fdca7d8e2e...fe902db67e)

[Compare Source](fdca7d8e2e...fe902db67e)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMzUuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEzNS4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-23 04:27:58 +00:00
Peng Xiao
ecdb5b3407 fix(core): properties adapter reactivitiy issue (#5661) 2024-01-23 01:41:44 +00:00
Peng Xiao
03b60a63cd fix: remove incorrect commit (#5660) 2024-01-23 01:41:39 +00:00
Cats Juice
45cc75a814 ci(component): deploy ui storybook (#5655) 2024-01-22 10:25:33 +00:00
Cats Juice
35e7b41ca8 fix(component): adjust the style of Switch to match the design (#5636)
closes #5635
2024-01-22 10:14:32 +00:00
Joooye_34
fccdf8ede7 feat: bump blocksuite (#5642)
f0c45fd...4e2c95a
2024-01-22 08:37:32 +00:00
Peng Xiao
0ed26f51af feat(core): adopt editor features for journal (#5638) 2024-01-22 08:25:31 +00:00
Peng Xiao
f41b7d7e71 feat(component): react wrapper for blocksuite editor (#5606)
A React wrapper for blocksuite editor from title/meta/doc/edgless fragments.
This PR only **clones** the `AffineEditorContainer`'s existing behavior and make it easier for extension in affine later.

fix TOV-315

### Some core issues:

A customized version of `createComponent` from `@lit/react`. The [existing and solutions in the community](https://github.com/lit/lit/issues/4435) does not work well in our case.
Alternatively in this PR the approach we have is to create web component instances in React lifecycle and then append them to DOM. However this make it hard to wrap the exported Lit component's using React and therefore we will have an additional wrapper tag for the wrapped web component.

To mitigate the migration issue on using React instead of Lit listed on last day, we now use [a proxy to mimic the wrapped React component](https://github.com/toeverything/AFFiNE/pull/5606/files#diff-5b7f0ae7b52a08739d50e78e9ec803c26ff3d3e5437581c692add0de12d3ede5R142-R183) into an `AffineEditorContainer` instance.
2024-01-22 08:25:29 +00:00
Peng Xiao
735e1cb117 feat(core): page info adapter for journal (#5561)
Page info adapter + schema.
Adapted for journal features.

![image](https://github.com/toeverything/AFFiNE/assets/584378/2731ed2b-a125-4d62-b658-f2aff49d0e17)
2024-01-22 08:25:27 +00:00
LongYinan
8b92cc0cae chore: decrease the instances count on gcp (#5658) 2024-01-22 08:02:26 +00:00
LongYinan
3de6424a65 fix(server): missing google oauth in auth providers (#5656) 2024-01-22 08:02:24 +00:00
liuyi
e516e0db23 refactor(server): plugin modules (#5630)
- [x] separates modules into `fundamental`, `core`, `plugins`
- [x] optional modules with `@OptionalModule` decorator to install modules with requirements met(`requires`, `if`)
- [x] `module.contributesTo` defines optional features that will be enabled if module registered
- [x] `AFFiNE.plugins.use('payment', {})` to enable a optional/plugin module
- [x] `PaymentModule` is the first plugin module
- [x] GraphQLSchema will not be generated for non-included modules
- [x] Frontend can use `ServerConfigType` query to detect which features are enabled
- [x] override existing provider globally
2024-01-22 07:40:28 +00:00
DarkSky
ae8401b6f4 feat: skip update quota if same as latest activated quota (#5631) 2024-01-22 06:50:05 +00:00
LongYinan
fb93f59aea chore: bump up vite version to v5.0.12 [SECURITY] (#5648)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [vite](https://vitejs.dev) ([source](https://togithub.com/vitejs/vite/tree/HEAD/packages/vite)) | [`5.0.6` -> `5.0.12`](https://renovatebot.com/diffs/npm/vite/5.0.6/5.0.12) | [![age](https://developer.mend.io/api/mc/badges/age/npm/vite/5.0.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vite/5.0.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vite/5.0.6/5.0.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite/5.0.6/5.0.12?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

### GitHub Vulnerability Alerts

#### [CVE-2024-23331](https://togithub.com/vitejs/vite/security/advisories/GHSA-c24v-8rfc-w8vw)

### Summary
[Vite dev server option](https://vitejs.dev/config/server-options.html#server-fs-deny) `server.fs.deny` can be bypassed on case-insensitive file systems using case-augmented versions of filenames. Notably this affects servers hosted on Windows.

This bypass is similar to https://nvd.nist.gov/vuln/detail/CVE-2023-34092 -- with surface area reduced to hosts having case-insensitive filesystems.

### Patches
Fixed in vite@5.0.12, vite@4.5.2, vite@3.2.8, vite@2.9.17

### Details
Since `picomatch` defaults to case-sensitive glob matching, but the file server doesn't discriminate; a blacklist bypass is possible.

See `picomatch`  usage, where `nocase` is defaulted to `false`: https://github.com/vitejs/vite/blob/v5.1.0-beta.1/packages/vite/src/node/server/index.ts#L632

By requesting raw filesystem paths using augmented casing, the matcher derived from `config.server.fs.deny` fails to block access to sensitive files.

### PoC
**Setup**
1. Created vanilla Vite project using `npm create vite@latest` on a Standard Azure hosted Windows 10 instance.
    - `npm run dev -- --host 0.0.0.0`
    - Publicly accessible for the time being here: http://20.12.242.81:5173/
2. Created dummy secret files, e.g. `custom.secret` and `production.pem`
3. Populated `vite.config.js` with
```javascript
export default { server: { fs: { deny: ['.env', '.env.*', '*.{crt,pem}', 'custom.secret'] } } }
```

**Reproduction**
1. `curl -s http://20.12.242.81:5173/@&#8203;fs//`
    - Descriptive error page reveals absolute filesystem path to project root
2. `curl -s http://20.12.242.81:5173/@&#8203;fs/C:/Users/darbonzo/Desktop/vite-project/vite.config.js`
    - Discoverable configuration file reveals locations of secrets
3. `curl -s http://20.12.242.81:5173/@&#8203;fs/C:/Users/darbonzo/Desktop/vite-project/custom.sEcReT`
    - Secrets are directly accessible using case-augmented version of filename

**Proof**
![Screenshot 2024-01-19 022736](https://user-images.githubusercontent.com/907968/298020728-3a8d3c06-fcfd-4009-9182-e842f66a6ea5.png)

### Impact
**Who**
- Users with exposed dev servers on environments with case-insensitive filesystems

**What**
- Files protected by `server.fs.deny` are both discoverable, and accessible

---

### Release Notes

<details>
<summary>vitejs/vite (vite)</summary>

### [`v5.0.12`](https://togithub.com/vitejs/vite/releases/tag/v5.0.12)

[Compare Source](https://togithub.com/vitejs/vite/compare/v5.0.11...v5.0.12)

Please refer to [CHANGELOG.md](https://togithub.com/vitejs/vite/blob/v5.0.12/packages/vite/CHANGELOG.md) for details.

### [`v5.0.11`](https://togithub.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small5011-2024-01-05-small)

[Compare Source](https://togithub.com/vitejs/vite/compare/v5.0.10...v5.0.11)

-   fix: don't pretransform classic script links ([#&#8203;15361](https://togithub.com/vitejs/vite/issues/15361)) ([19e3c9a](https://togithub.com/vitejs/vite/commit/19e3c9a)), closes [#&#8203;15361](https://togithub.com/vitejs/vite/issues/15361)
-   fix: inject `__vite__mapDeps` code before sourcemap file comment ([#&#8203;15483](https://togithub.com/vitejs/vite/issues/15483)) ([d2aa096](https://togithub.com/vitejs/vite/commit/d2aa096)), closes [#&#8203;15483](https://togithub.com/vitejs/vite/issues/15483)
-   fix(assets): avoid splitting `,` inside base64 value of `srcset` attribute ([#&#8203;15422](https://togithub.com/vitejs/vite/issues/15422)) ([8de7bd2](https://togithub.com/vitejs/vite/commit/8de7bd2)), closes [#&#8203;15422](https://togithub.com/vitejs/vite/issues/15422)
-   fix(html): handle offset magic-string slice error ([#&#8203;15435](https://togithub.com/vitejs/vite/issues/15435)) ([5ea9edb](https://togithub.com/vitejs/vite/commit/5ea9edb)), closes [#&#8203;15435](https://togithub.com/vitejs/vite/issues/15435)
-   chore(deps): update dependency strip-literal to v2 ([#&#8203;15475](https://togithub.com/vitejs/vite/issues/15475)) ([49d21fe](https://togithub.com/vitejs/vite/commit/49d21fe)), closes [#&#8203;15475](https://togithub.com/vitejs/vite/issues/15475)
-   chore(deps): update tj-actions/changed-files action to v41 ([#&#8203;15476](https://togithub.com/vitejs/vite/issues/15476)) ([2a540ee](https://togithub.com/vitejs/vite/commit/2a540ee)), closes [#&#8203;15476](https://togithub.com/vitejs/vite/issues/15476)

### [`v5.0.10`](https://togithub.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small5010-2023-12-15-small)

[Compare Source](https://togithub.com/vitejs/vite/compare/v5.0.9...v5.0.10)

-   fix: omit protocol does not require pre-transform ([#&#8203;15355](https://togithub.com/vitejs/vite/issues/15355)) ([d9ae1b2](https://togithub.com/vitejs/vite/commit/d9ae1b2)), closes [#&#8203;15355](https://togithub.com/vitejs/vite/issues/15355)
-   fix(build): use base64 for inline SVG if it contains both single and double quotes ([#&#8203;15271](https://togithub.com/vitejs/vite/issues/15271)) ([1bbff16](https://togithub.com/vitejs/vite/commit/1bbff16)), closes [#&#8203;15271](https://togithub.com/vitejs/vite/issues/15271)

### [`v5.0.9`](https://togithub.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small509-2023-12-14-small)

[Compare Source](https://togithub.com/vitejs/vite/compare/v5.0.8...v5.0.9)

-   fix: htmlFallbackMiddleware for favicon ([#&#8203;15301](https://togithub.com/vitejs/vite/issues/15301)) ([c902545](https://togithub.com/vitejs/vite/commit/c902545)), closes [#&#8203;15301](https://togithub.com/vitejs/vite/issues/15301)
-   fix: more stable hash calculation for depsOptimize ([#&#8203;15337](https://togithub.com/vitejs/vite/issues/15337)) ([2b39fe6](https://togithub.com/vitejs/vite/commit/2b39fe6)), closes [#&#8203;15337](https://togithub.com/vitejs/vite/issues/15337)
-   fix(scanner): catch all external files for glob imports ([#&#8203;15286](https://togithub.com/vitejs/vite/issues/15286)) ([129d0d0](https://togithub.com/vitejs/vite/commit/129d0d0)), closes [#&#8203;15286](https://togithub.com/vitejs/vite/issues/15286)
-   fix(server): avoid chokidar throttling on startup ([#&#8203;15347](https://togithub.com/vitejs/vite/issues/15347)) ([56a5740](https://togithub.com/vitejs/vite/commit/56a5740)), closes [#&#8203;15347](https://togithub.com/vitejs/vite/issues/15347)
-   fix(worker): replace `import.meta` correctly for IIFE worker ([#&#8203;15321](https://togithub.com/vitejs/vite/issues/15321)) ([08d093c](https://togithub.com/vitejs/vite/commit/08d093c)), closes [#&#8203;15321](https://togithub.com/vitejs/vite/issues/15321)
-   feat: log re-optimization reasons ([#&#8203;15339](https://togithub.com/vitejs/vite/issues/15339)) ([b1a6c84](https://togithub.com/vitejs/vite/commit/b1a6c84)), closes [#&#8203;15339](https://togithub.com/vitejs/vite/issues/15339)
-   chore: temporary typo ([#&#8203;15329](https://togithub.com/vitejs/vite/issues/15329)) ([7b71854](https://togithub.com/vitejs/vite/commit/7b71854)), closes [#&#8203;15329](https://togithub.com/vitejs/vite/issues/15329)
-   perf: avoid computing paths on each request ([#&#8203;15318](https://togithub.com/vitejs/vite/issues/15318)) ([0506812](https://togithub.com/vitejs/vite/commit/0506812)), closes [#&#8203;15318](https://togithub.com/vitejs/vite/issues/15318)
-   perf: temporary hack to avoid fs checks for /[@&#8203;react-refresh](https://togithub.com/react-refresh) ([#&#8203;15299](https://togithub.com/vitejs/vite/issues/15299)) ([b1d6211](https://togithub.com/vitejs/vite/commit/b1d6211)), closes [#&#8203;15299](https://togithub.com/vitejs/vite/issues/15299)

### [`v5.0.8`](https://togithub.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small508-2023-12-12-small)

[Compare Source](https://togithub.com/vitejs/vite/compare/v5.0.7...v5.0.8)

-   perf: cached fs utils ([#&#8203;15279](https://togithub.com/vitejs/vite/issues/15279)) ([c9b61c4](https://togithub.com/vitejs/vite/commit/c9b61c4)), closes [#&#8203;15279](https://togithub.com/vitejs/vite/issues/15279)
-   fix: missing warmupRequest in transformIndexHtml ([#&#8203;15303](https://togithub.com/vitejs/vite/issues/15303)) ([103820f](https://togithub.com/vitejs/vite/commit/103820f)), closes [#&#8203;15303](https://togithub.com/vitejs/vite/issues/15303)
-   fix: public files map will be updated on add/unlink in windows ([#&#8203;15317](https://togithub.com/vitejs/vite/issues/15317)) ([921ca41](https://togithub.com/vitejs/vite/commit/921ca41)), closes [#&#8203;15317](https://togithub.com/vitejs/vite/issues/15317)
-   fix(build): decode urls in CSS files (fix [#&#8203;15109](https://togithub.com/vitejs/vite/issues/15109)) ([#&#8203;15246](https://togithub.com/vitejs/vite/issues/15246)) ([ea6a7a6](https://togithub.com/vitejs/vite/commit/ea6a7a6)), closes [#&#8203;15109](https://togithub.com/vitejs/vite/issues/15109) [#&#8203;15246](https://togithub.com/vitejs/vite/issues/15246)
-   fix(deps): update all non-major dependencies ([#&#8203;15304](https://togithub.com/vitejs/vite/issues/15304)) ([bb07f60](https://togithub.com/vitejs/vite/commit/bb07f60)), closes [#&#8203;15304](https://togithub.com/vitejs/vite/issues/15304)
-   fix(ssr): check esm file with normal file path ([#&#8203;15307](https://togithub.com/vitejs/vite/issues/15307)) ([1597170](https://togithub.com/vitejs/vite/commit/1597170)), closes [#&#8203;15307](https://togithub.com/vitejs/vite/issues/15307)

### [`v5.0.7`](https://togithub.com/vitejs/vite/blob/HEAD/packages/vite/CHANGELOG.md#small507-2023-12-08-small)

[Compare Source](https://togithub.com/vitejs/vite/compare/v5.0.6...v5.0.7)

-   fix: suppress terser warning if minify disabled ([#&#8203;15275](https://togithub.com/vitejs/vite/issues/15275)) ([3e42611](https://togithub.com/vitejs/vite/commit/3e42611)), closes [#&#8203;15275](https://togithub.com/vitejs/vite/issues/15275)
-   fix: symbolic links in public dir ([#&#8203;15264](https://togithub.com/vitejs/vite/issues/15264)) ([ef2a024](https://togithub.com/vitejs/vite/commit/ef2a024)), closes [#&#8203;15264](https://togithub.com/vitejs/vite/issues/15264)
-   fix(html): skip inlining icon and manifest links ([#&#8203;14958](https://togithub.com/vitejs/vite/issues/14958)) ([8ad81b4](https://togithub.com/vitejs/vite/commit/8ad81b4)), closes [#&#8203;14958](https://togithub.com/vitejs/vite/issues/14958)
-   chore: remove unneeded condition in getRealPath ([#&#8203;15267](https://togithub.com/vitejs/vite/issues/15267)) ([8e4655c](https://togithub.com/vitejs/vite/commit/8e4655c)), closes [#&#8203;15267](https://togithub.com/vitejs/vite/issues/15267)
-   perf: cache empty optimizer result ([#&#8203;15245](https://togithub.com/vitejs/vite/issues/15245)) ([8409b66](https://togithub.com/vitejs/vite/commit/8409b66)), closes [#&#8203;15245](https://togithub.com/vitejs/vite/issues/15245)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMzUuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEzNS4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-20 13:36:05 +00:00
JimmFly
353b27d796 refactor(component): adapt the questionnaire component to the desktop client (#5514)
close TOV-240

Windows desktop:
<img width="1145" alt="image" src="https://github.com/toeverything/AFFiNE/assets/102217452/6b12a6bd-c020-4d02-a366-e97e1afd1e10">

MacOS desktop:
<img width="1187" alt="image" src="https://github.com/toeverything/AFFiNE/assets/102217452/9a3c909c-b283-4ddc-a187-3db12b26a7f5">

Web:
<img width="1438" alt="image" src="https://github.com/toeverything/AFFiNE/assets/102217452/e8e04258-9275-470d-a3f8-5d18f5b29eb3">
2024-01-19 10:07:18 +00:00
LongYinan
fa8655e43e style: apply prefer-node-protocol lint rule (#5627)
it makes it perfectly clear that the package is a Node.js builtin module.
2024-01-19 03:47:08 +00:00
LongYinan
9d28eb530a feat: upgrade electron and playwright (#5632) 2024-01-19 03:16:06 +00:00
LongYinan
cf2ad141ea chore: upgrade perfsee sdk (#5629)
packages version info are missing: https://perfsee.com/projects/perfsee/bundle/1346?tab=packages
2024-01-19 10:58:36 +08:00
HeJiachen-PM
dc68ffd127 docs: update readme (#5637) 2024-01-19 02:02:25 +00:00
Cats Juice
351f1b73b4 feat(core): add journal entrance for app-sidebar (#5579) 2024-01-18 14:40:35 +00:00
Cats Juice
65bcdcafde feat(core): add experimental flag for journal (#5578) 2024-01-18 14:05:17 +00:00
Cats Juice
f4b26a16f8 feat(core): journal sidebar conflict block (#5574) 2024-01-18 14:05:15 +00:00
Cats Juice
7aaec3ad51 feat(core): add daily count for journal sidebar (#5559) 2024-01-18 12:54:44 +00:00
JimmFly
a7e8664959 fix(core): reserve space for the editor scrollbar (#5625)
Because the space for the scroll bar on the right is reserved, in order to make the editor symmetrical, padding corresponding to the width of the scroll bar is added to the left.
2024-01-18 12:44:27 +00:00
Cats Juice
70ea1e5ef8 feat(core): journal sidebar dater-picker navigation (#5558) 2024-01-18 12:34:23 +00:00
Cats Juice
496dc588be feat(core): journal extension loader (#5557) 2024-01-18 09:27:56 +00:00
regischen
8b1b5b2e93 feat: bump blocksuite (#5624) 2024-01-18 17:26:31 +08:00
DarkSky
c3fda80599 feat: use nx cloud runner (#5626) 2024-01-18 08:18:37 +00:00
Peng Xiao
9a944048e8 feat(core): experimental features ui (#5338)
fix TOV-280

experimental features ui
- enabled in the workspace settings for a cloud workspace; only show for workspace owner + early access
- a disclaimer prompt will be shown before going to the next feature setting page
- for now only show the ai poc switch, which controls the display of the ai tab in editor's sidepanel
2024-01-18 07:53:15 +00:00
Cats Juice
cabedef426 feat(core): journal hooks and page header layout (#5549)
feat(core): split page header items

feat(core): journal page judgment and header layout

feat(core): Add journal today button and style changes to share menu
2024-01-18 07:17:14 +00:00
Cats Juice
a64854319e feat(core): add document title for shared page (#5596) 2024-01-18 07:04:37 +00:00
LongYinan
c5ea6fd2c3 fix(server): selfhost issues (#5623)
- env name in helm chart
- omit health check controller in selfhost env
2024-01-18 05:55:54 +00:00
liuyi
9fdbb3ac3d fix(server): should not listen on user defined host (#5622) 2024-01-18 04:59:53 +00:00
EYHN
74a3a795bd fix(workspace): check session before get workspaces (#5621) 2024-01-18 04:46:52 +00:00
LongYinan
aa437bcd35 fix(core): remove hash prefix from cdn path (#5509)
The hash prefix will cause cache invalidate during deployment
It's for debug purpose but I forgot to remove it
2024-01-18 03:50:24 +00:00
Cats Juice
943ede4ffd feat(component): new inline-edit component (#5517)
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://github.com/toeverything/AFFiNE/assets/39363750/6dad59f0-5e63-4c25-a81c-dff397da1d34">
  <img height="100" alt="" src="https://github.com/toeverything/AFFiNE/assets/39363750/7c30d7b2-55c9-49eb-82e7-a0882f2e0493">
</picture>
2024-01-18 03:33:47 +00:00
liuyi
f419867437 chore(server): remove useless log (#5620) 2024-01-18 03:19:20 +00:00
liuyi
d9324286d4 chore(server): add port to host if it is 0.0.0.0 (#5619) 2024-01-18 03:04:36 +00:00
LongYinan
5b84366de3 chore: bump up actions/cache action to v4 (#5616)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/cache](https://togithub.com/actions/cache) | action | major | `v3` -> `v4` |

---

### Release Notes

<details>
<summary>actions/cache (actions/cache)</summary>

### [`v4`](https://togithub.com/actions/cache/compare/v3...v4)

[Compare Source](https://togithub.com/actions/cache/compare/v3...v4)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMzUuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEzNS4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-18 02:37:47 +00:00
Cats Juice
2b92b27f8f feat(component): new week-date-picker component (#5477)
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://github.com/toeverything/AFFiNE/assets/39363750/49d7a1ee-2832-4b61-a427-e627dae92952">
  <img height="100" alt="" src="https://github.com/toeverything/AFFiNE/assets/39363750/819d6ee9-38e0-4537-ad0f-ec9faf96f505">
</picture>
2024-01-18 02:14:27 +00:00
DarkSky
ee8ec47a4f feat: use SafeInt replace Float (#5613) 2024-01-17 12:36:21 +00:00
liuyi
b9f20877d0 feat(core): make password sigin default if user has one (#5577) 2024-01-17 11:13:58 +00:00
liuyi
bf88b6edaa chore(server): remove too verbose logs (#5555)
chore(server): remove too verbose logs

chore(server): make logs less verbose
2024-01-17 10:37:22 +00:00
liuyi
00acc49342 chore(server): remove octobase storage usage (#5594)
since all blobs have been successfully migrated to r2, the octobase blob functions are no longer necessary.
2024-01-17 10:22:35 +00:00
Cats Juice
2db3c933fa refactor(component): move date-picker to ui, add story, support responsive (#5468)
- move to `component/ui`
- add `AFFiNEDatePicker` & `BlocksuiteDatePicker` story
- inline mode support
- responsive support
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://github.com/toeverything/AFFiNE/assets/39363750/320bef49-380f-40a2-b3b2-4b74dd2d8da4">
    <img  alt="" src="https://github.com/toeverything/AFFiNE/assets/39363750/fc9e7808-02fe-49a1-aa78-aea254fb1f9d">
  </picture>
2024-01-17 09:16:46 +00:00
DarkSky
8f80bdb7af feat: new free plan (#5604) 2024-01-17 07:20:18 +00:00
LongYinan
3f87d04481 chore: bump up postcss-loader version to v8 (#5609)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [postcss-loader](https://togithub.com/webpack-contrib/postcss-loader) | [`^7.3.3` -> `^8.0.0`](https://renovatebot.com/diffs/npm/postcss-loader/7.3.3/8.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/postcss-loader/8.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/postcss-loader/8.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/postcss-loader/7.3.3/8.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/postcss-loader/7.3.3/8.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>webpack-contrib/postcss-loader (postcss-loader)</summary>

### [`v8.0.0`](https://togithub.com/webpack-contrib/postcss-loader/blob/HEAD/CHANGELOG.md#800-2024-01-16)

[Compare Source](https://togithub.com/webpack-contrib/postcss-loader/compare/v7.3.4...v8.0.0)

##### ⚠ BREAKING CHANGES

-   minimum supported Node.js version is `18.12.0` ([#&#8203;677](https://togithub.com/webpack-contrib/postcss-loader/issues/677)) ([8dd0315](8dd0315c03))

##### [7.3.4](https://togithub.com/webpack-contrib/postcss-loader/compare/v7.3.3...v7.3.4) (2023-12-27)

##### Bug Fixes

-   do not crash if pkg.(d|devD)ependencies unset ([#&#8203;667](https://togithub.com/webpack-contrib/postcss-loader/issues/667)) ([8ef0c7e](8ef0c7e5c6))

##### [7.3.3](https://togithub.com/webpack-contrib/postcss-loader/compare/v7.3.2...v7.3.3) (2023-06-10)

##### Bug Fixes

-   **perf:** avoid using `klona` for postcss options ([#&#8203;658](https://togithub.com/webpack-contrib/postcss-loader/issues/658)) ([e754c3f](e754c3f845))
-   bug with loading configurations after updating `cosmiconfig` to version 8.2 ([684d265](684d265439))

##### [7.3.2](https://togithub.com/webpack-contrib/postcss-loader/compare/v7.3.1...v7.3.2) (2023-05-28)

##### Bug Fixes

-   use `cause` to keep original errors and warnings ([#&#8203;655](https://togithub.com/webpack-contrib/postcss-loader/issues/655)) ([e8873f4](e8873f46b4))

##### [7.3.1](https://togithub.com/webpack-contrib/postcss-loader/compare/v7.3.0...v7.3.1) (2023-05-26)

##### Bug Fixes

-   warning and error serialization ([65748ec](65748ece39))

### [`v7.3.4`](https://togithub.com/webpack-contrib/postcss-loader/blob/HEAD/CHANGELOG.md#734-2023-12-27)

[Compare Source](https://togithub.com/webpack-contrib/postcss-loader/compare/v7.3.3...v7.3.4)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMzUuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEzNS4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-17 02:56:58 +00:00
LongYinan
41083b7fec chore: bump up copy-webpack-plugin version to v12 (#5568)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [copy-webpack-plugin](https://togithub.com/webpack-contrib/copy-webpack-plugin) | [`^11.0.0` -> `^12.0.0`](https://renovatebot.com/diffs/npm/copy-webpack-plugin/11.0.0/12.0.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/copy-webpack-plugin/12.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/copy-webpack-plugin/12.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/copy-webpack-plugin/11.0.0/12.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/copy-webpack-plugin/11.0.0/12.0.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>webpack-contrib/copy-webpack-plugin (copy-webpack-plugin)</summary>

### [`v12.0.1`](https://togithub.com/webpack-contrib/copy-webpack-plugin/blob/HEAD/CHANGELOG.md#1201-2024-01-11)

[Compare Source](https://togithub.com/webpack-contrib/copy-webpack-plugin/compare/v12.0.0...v12.0.1)

### [`v12.0.0`](https://togithub.com/webpack-contrib/copy-webpack-plugin/blob/HEAD/CHANGELOG.md#1200-2024-01-10)

[Compare Source](https://togithub.com/webpack-contrib/copy-webpack-plugin/compare/v11.0.0...v12.0.0)

##### ⚠ BREAKING CHANGES

-   update `globby` to `14.0.0`
-   minimum supported `Node.js` version is `18.12.0` ([#&#8203;759](https://togithub.com/webpack-contrib/copy-webpack-plugin/issues/759)) ([a5b7d06](a5b7d06a05))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMjcuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEyNy4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-16 15:44:18 +00:00
LongYinan
823ea92f62 chore: bump up happy-dom version to v13 (#5569)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [happy-dom](https://togithub.com/capricorn86/happy-dom) | [`^12.10.3` -> `^13.0.0`](https://renovatebot.com/diffs/npm/happy-dom/12.10.3/13.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/happy-dom/13.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/happy-dom/13.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/happy-dom/12.10.3/13.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/happy-dom/12.10.3/13.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>capricorn86/happy-dom (happy-dom)</summary>

### [`v13.0.0`](https://togithub.com/capricorn86/happy-dom/releases/tag/v13.0.0)

[Compare Source](https://togithub.com/capricorn86/happy-dom/compare/v12.10.3...v13.0.0)

##### 💣 Breaking Changes

-   This is a big release where a lot of the code has been refactored to improve security and to be able to support the new Browser API. A big release is always a potential risk for bugs and therefore it make sense to make this a major release to avoid that consumers automatically updates to it. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))

##### 🎨 Features

-   Adds support for a Browser API similar to [Puppeteer](https://pptr.dev/) and [Playwright](https://playwright.dev/). With the Browser API, it is for example possible to create new pages and navigate in them. You can read more about it in the [Happy DOM Wiki](https://togithub.com/capricorn86/happy-dom/wiki). ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Improves security by not exposing sensitive internal logic to scripts running within the Happy DOM Browser. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Improves support for CORS management. It now supports "OPTIONS" requests to detect if the client is allowed to proceed with a cross origin request. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Adds support for HTTP response cache. The cache is in memory, but the plan is to add support for storing it on disk in the future. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Improves support for `XMLHttpRequest`. It now supports the GZip, Deflate and Brotli encodings. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Adds support for adding a process level error event listener for capturing errors. This is useful when using the Browser API, but will not work in environments such as [Jest](https://jestjs.io/) and [Vitest](https://vitest.dev/), as it collides with their error listener. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Adds support for `Ẁindow.open()`. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Adds support for `Ẁindow.close()`. This function should now be used when tearing down the environment. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Improves support for cookies. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Improves support for `HTMLIFrameElement`. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Changes export of types to use "import type" and "export type" in "index.js". This will allow transpilers/compilers to optimize better. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Adds support for navigating when clicking on an anchor link. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Adds support for navigating when setting `Location.href`. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Improves support for `MutationObserver`. It will not collect multiple records with a microtask. It now also supports `MutationObserver.takeRecords()` for records that has not yet been published. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Deprecates "[@&#8203;happy-dom/uncaught-exception-observer](https://togithub.com/happy-dom/uncaught-exception-observer)" as the functionality is supported by "happy-dom" out of the box now. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Uses Symbol for public internal properties instead of using "\_" as a prefix, so that internal properties won't be enumerable. This will also make sure that these properties won't clash with properties defined by the consumer. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))

##### 👷‍♂️ Patch fixes

-   Improves the check for if the property is a class to avoid that it gets bound in "[@&#8203;happy-dom/global-registrator](https://togithub.com/happy-dom/global-registrator)". We only want functions to get bound to the global context. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Fixes bug where `new Document()` did'nt work according to spec. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Fixes bug where several Element classes wheren't available as properties on `Window`. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Fixes bug in `Document.importNode()` where it didn't change `ownerDocument` on child nodes. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))
-   Resets static `ownerDocument` state used when creating Node instances, so that it can be garbage collected if not used anymore. ([#&#8203;466](https://togithub.com/capricorn86/happy-dom/issues/466))

***

Merry Christmas and a Happy New Year! 🎅 

This release took some time as I didn't want to release the Browser API without applying security fixes. I've also stumbled across a few other issues along the way that I felt I needed to address.

I hope you will enjoy the release!

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMjcuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEyNy4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-16 15:27:24 +00:00
LongYinan
880588ad11 chore: bump up @blocksuite/icons version to v2.1.41 (#5580)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@blocksuite/icons](https://togithub.com/toeverything/icons) | [`2.1.40` -> `2.1.41`](https://renovatebot.com/diffs/npm/@blocksuite%2ficons/2.1.40/2.1.41) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@blocksuite%2ficons/2.1.41?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@blocksuite%2ficons/2.1.41?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@blocksuite%2ficons/2.1.40/2.1.41?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@blocksuite%2ficons/2.1.40/2.1.41?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>toeverything/icons (@&#8203;blocksuite/icons)</summary>

### [`v2.1.41`](2b42f403fb...fdca7d8e2e)

[Compare Source](2b42f403fb...fdca7d8e2e)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMjcuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEyNy4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-16 15:11:36 +00:00
DarkSky
ee2520ec18 feat: add query quota of workspace (#5603) 2024-01-16 09:45:55 +00:00
JimmFly
4f4d057aad refactor(core): replace WorkspaceSubPath in env package with the one in core package (#5537) 2024-01-16 08:26:11 +00:00
Peng Xiao
238d1ad44e fix(component): add back lottie color hack for dark mode (#5576)
Not sure why but it seems the changes in https://github.com/toeverything/AFFiNE/pull/4953 are lost
2024-01-16 08:11:51 +00:00
JimmFly
baeb5cc732 feat(core): get cloud workspace usage limit from user quota (#5518) 2024-01-16 07:57:23 +00:00
Joooye_34
d6dd837bdb chore: update favicon (#5566) 2024-01-16 07:42:32 +00:00
DarkSky
dea0aab5e3 feat: update nx config (#5597) 2024-01-16 06:41:46 +00:00
Lewis Liu
94e24d1b82 style: import from './index' instead of '.' (#5590)
Co-authored-by: LongYinan <lynweklm@gmail.com>
2024-01-16 14:39:08 +08:00
liuyi
0a89b7f528 fix(server): standalone early access users detection (#5601) 2024-01-16 11:39:36 +08:00
liuyi
75fb0a9f1a fix(server): standalone early access users detection (#5601) 2024-01-16 03:27:44 +00:00
LongYinan
2fb0e3ef15 chore: bump up source-map-loader version to v5 (#5599)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [source-map-loader](https://togithub.com/webpack-contrib/source-map-loader) | [`^4.0.1` -> `^5.0.0`](https://renovatebot.com/diffs/npm/source-map-loader/4.0.1/5.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/source-map-loader/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/source-map-loader/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/source-map-loader/4.0.1/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/source-map-loader/4.0.1/5.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>webpack-contrib/source-map-loader (source-map-loader)</summary>

### [`v5.0.0`](https://togithub.com/webpack-contrib/source-map-loader/blob/HEAD/CHANGELOG.md#500-2024-01-15)

[Compare Source](https://togithub.com/webpack-contrib/source-map-loader/compare/v4.0.2...v5.0.0)

##### ⚠ BREAKING CHANGES

-   minimum supported Node.js version is `18.12.0` ([#&#8203;230](https://togithub.com/webpack-contrib/source-map-loader/issues/230)) ([7fcab17](7fcab17805))

##### [4.0.2](https://togithub.com/webpack-contrib/source-map-loader/compare/v4.0.1...v4.0.2) (2023-12-27)

##### Bug Fixes

-   avoid deprecation message of `abab` package ([#&#8203;228](https://togithub.com/webpack-contrib/source-map-loader/issues/228)) ([9daafb3](9daafb3662))

##### [4.0.1](https://togithub.com/webpack-contrib/source-map-loader/compare/v4.0.0...v4.0.1) (2022-10-07)

##### Bug Fixes

-   logic when sourceRoot contains absolute URL ([e724a1f](e724a1ffbf))

### [`v4.0.2`](https://togithub.com/webpack-contrib/source-map-loader/blob/HEAD/CHANGELOG.md#402-2023-12-27)

[Compare Source](https://togithub.com/webpack-contrib/source-map-loader/compare/v4.0.1...v4.0.2)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMjcuMCIsInVwZGF0ZWRJblZlciI6IjM3LjEyNy4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-16 03:14:57 +00:00
liuyi
24e18dd475 fix: improve self-host convenience (#5582) 2024-01-15 09:24:53 +00:00
DarkSky
2f9b4fd0cf fix: add field polyfill for old feature (#5586) 2024-01-15 08:33:06 +00:00
liuyi
4c49b62ab7 fix(server): node imports order (#5583) 2024-01-14 05:47:56 +00:00
DarkSky
0a0ee37ac2 fix: return empty resp if user not exists in login preflight (#5588) 2024-01-13 23:30:01 +08:00
DarkSky
18907ebe57 fix: return empty resp if user not exists in login preflight (#5588) 2024-01-13 15:26:55 +00:00
Peng Xiao
5aea84af8f fix(electron): remove cors headers hack (#5581) 2024-01-12 16:48:55 +08:00
regischen
3602f1cac0 feat: bump blocksuite (#5575) 2024-01-12 12:41:27 +08:00
liuyi
89b5c96d25 refactor(server): folder structure (#5573) 2024-01-12 04:18:39 +00:00
DarkSky
d6f65ea414 feat: blob size limit with quota (#5524)
fix AFF-506 TOV-342
2024-01-11 10:21:40 +00:00
EYHN
d1c2b2a7b0 fix(core): workspace not found after import (#5571)
close TOV-393
2024-01-11 09:47:24 +00:00
liuyi
12fdb18a80 test(server): make server testing utils (#5544) 2024-01-11 06:40:55 +00:00
liuyi
9253e522aa test(server): avoid progress get hold after tests finished (#5522) 2024-01-11 06:40:53 +00:00
EYHN
5aee480c50 refactor(core): move page list to core (#5556) 2024-01-10 11:09:39 +00:00
LongYinan
237722f7f9 feat: support self-host docker build (#5506)
Test command: `docker compose -f ./.github/deployment/self-host/compose.yaml up`
2024-01-10 08:35:21 +00:00
DarkSky
0d7ffb0511 feat: add unlimited workspace support (#5523)
fix AFF-505
2024-01-10 07:28:53 +00:00
DarkSky
a59fe1b49e feat: adapted user quota for member api (#5521)
fix AFF-494 TOV-337
2024-01-10 07:28:46 +00:00
Peng Xiao
275ea74772 chore(core): remove affine/cmdk package (#5552)
patch cmdk based on https://github.com/pengx17/cmdk/tree/patch-1
fix https://github.com/toeverything/AFFiNE/issues/5548
2024-01-10 05:25:37 +00:00
529 changed files with 16199 additions and 6318 deletions

View File

@@ -217,6 +217,7 @@ const config = {
'unicorn/no-useless-promise-resolve-reject': 'error',
'unicorn/no-new-array': 'error',
'unicorn/new-for-builtins': 'error',
'unicorn/prefer-node-protocol': 'error',
'sonarjs/no-all-duplicated-branches': 'error',
'sonarjs/no-element-overwrite': 'error',
'sonarjs/no-empty-collection': 'error',

View File

@@ -37,7 +37,7 @@ runs:
echo "TARGET_CC=clang" >> "$GITHUB_ENV"
- name: Cache cargo
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/

View File

@@ -65,8 +65,16 @@ const createHelmCommand = ({ isDryRun }) => {
]
: [];
const webReplicaCount = isProduction ? 3 : isBeta ? 2 : 2;
const graphqlReplicaCount = isProduction ? 10 : isBeta ? 5 : 2;
const syncReplicaCount = isProduction ? 10 : isBeta ? 5 : 2;
const graphqlReplicaCount = isProduction
? Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 3
: isBeta
? Number(process.env.isBeta_GRAPHQL_REPLICA) || 2
: 2;
const syncReplicaCount = isProduction
? Number(process.env.PRODUCTION_SYNC_REPLICA) || 3
: isBeta
? Number(process.env.BETA_SYNC_REPLICA) || 2
: 2;
const namespace = isProduction
? 'production'
: isBeta

View File

@@ -63,7 +63,7 @@ runs:
run: node -e "const p = $(yarn config cacheFolder --json).effective; console.log('yarn_global_cache=' + p)" >> $GITHUB_OUTPUT
- name: Cache non-full yarn cache on Linux
uses: actions/cache@v3
uses: actions/cache@v4
if: ${{ inputs.full-cache != 'true' && runner.os == 'Linux' }}
with:
path: |
@@ -75,7 +75,7 @@ runs:
# and the decompression performance on Windows is very terrible
# so we reduce the number of cached files on non-Linux systems by remove node_modules from cache path.
- name: Cache non-full yarn cache on non-Linux
uses: actions/cache@v3
uses: actions/cache@v4
if: ${{ inputs.full-cache != 'true' && runner.os != 'Linux' }}
with:
path: |
@@ -83,7 +83,7 @@ runs:
key: node_modules-cache-${{ github.job }}-${{ runner.os }}
- name: Cache full yarn cache on Linux
uses: actions/cache@v3
uses: actions/cache@v4
if: ${{ inputs.full-cache == 'true' && runner.os == 'Linux' }}
with:
path: |
@@ -92,7 +92,7 @@ runs:
key: node_modules-cache-full-${{ runner.os }}
- name: Cache full yarn cache on non-Linux
uses: actions/cache@v3
uses: actions/cache@v4
if: ${{ inputs.full-cache == 'true' && runner.os != 'Linux' }}
with:
path: |
@@ -134,7 +134,7 @@ runs:
# Note: Playwright's cache directory is hard coded because that's what it
# says to do in the docs. There doesn't appear to be a command that prints
# it out for us.
- uses: actions/cache@v3
- uses: actions/cache@v4
id: playwright-cache
if: ${{ inputs.playwright-install == 'true' }}
with:
@@ -167,7 +167,7 @@ runs:
run: |
echo "version=$(yarn why --json electron | grep -h 'workspace:.' | jq --raw-output '.children[].locator' | sed -e 's/@playwright\/test@.*://' | head -n 1)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
- uses: actions/cache@v4
id: electron-cache
if: ${{ inputs.electron-install == 'true' }}
with:

View File

@@ -1,6 +1,7 @@
FROM node:18-bookworm-slim
COPY ./packages/backend/server /app
COPY ./packages/frontend/core/dist /app/static
WORKDIR /app
RUN apt-get update && \

View File

@@ -0,0 +1,61 @@
services:
affine:
image: ghcr.io/toeverything/affine-graphql:beta
container_name: affine_selfhosted
command:
['sh', '-c', 'node ./scripts/self-host-predeploy && node ./dist/index.js']
ports:
- '3010:3010'
- '5555:5555'
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
volumes:
# custom configurations
- ~/.affine/self-host/config:/root/.affine/config
# blob storage
- ~/.affine/self-host/storage:/root/.affine/storage
logging:
driver: 'json-file'
options:
max-size: '1000m'
restart: unless-stopped
environment:
- NODE_OPTIONS=--es-module-specifier-resolution node
- AFFINE_CONFIG_PATH=/root/.affine/config
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgres://affine:affine@postgres:5432/affine
- DISABLE_TELEMETRY=true
- NODE_ENV=production
- SERVER_FLAVOR=selfhosted
- AFFINE_ADMIN_EMAIL=${AFFINE_ADMIN_EMAIL}
- AFFINE_ADMIN_PASSWORD=${AFFINE_ADMIN_PASSWORD}
redis:
image: redis
container_name: affine_redis
restart: unless-stopped
volumes:
- ~/.affine/self-host/redis:/data
healthcheck:
test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
interval: 10s
timeout: 5s
retries: 5
postgres:
image: postgres
container_name: affine_postgres
restart: unless-stopped
volumes:
- ~/.affine/self-host/postgres:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U affine']
interval: 10s
timeout: 5s
retries: 5
environment:
POSTGRES_USER: affine
POSTGRES_PASSWORD: affine
POSTGRES_DB: affine
PGDATA: /var/lib/postgresql/data/pgdata

View File

@@ -3,4 +3,4 @@ name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: "0.11.0"
appVersion: "0.12.0"

View File

@@ -3,7 +3,7 @@ name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: "0.11.0"
appVersion: "0.12.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -73,6 +73,8 @@ spec:
value: "{{ .Values.app.path }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: AFFINE_SERVER_HTTPS
value: "{{ .Values.app.https }}"
- name: ENABLE_R2_OBJECT_STORAGE
value: "{{ .Values.app.objectStorage.r2.enabled }}"
- name: ENABLE_CAPTCHA
@@ -145,6 +147,8 @@ spec:
key: turnstileSecret
{{ end }}
{{ if .Values.app.oauth.google.enabled }}
- name: OAUTH_GOOGLE_ENABLED
value: "true"
- name: OAUTH_GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:

View File

@@ -16,6 +16,7 @@ app:
path: ''
# AFFINE_SERVER_HOST
host: '0.0.0.0'
https: true
doc:
mergeInterval: "3000"
jwt:

View File

@@ -3,7 +3,7 @@ name: sync
description: AFFiNE Sync Server
type: application
version: 0.0.0
appVersion: "0.11.0"
appVersion: "0.12.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -70,6 +70,7 @@
"commitMessageAction": "bump up",
"commitMessageTopic": "{{depName}} version",
"ignoreDeps": [],
"postUpdateOptions": ["yarnDedupeHighest"],
"lockFileMaintenance": {
"enabled": true,
"extends": ["schedule:weekly"]

View File

@@ -19,6 +19,7 @@ env:
MACOSX_DEPLOYMENT_TARGET: '10.13'
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/node_modules/.cache/ms-playwright
DISABLE_TELEMETRY: true
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -95,6 +96,8 @@ jobs:
run: |
git checkout .yarnrc.yml
yarn lint:prettier
- name: Yarn Dedupe
run: yarn dedupe --check
- name: Run Type Check
run: yarn typecheck

View File

@@ -29,6 +29,7 @@ jobs:
uses: ./.github/actions/setup-node
with:
electron-install: false
extra-flags: workspaces focus @affine/server
- name: Build Server
run: yarn workspace @affine/server build
- name: Upload server dist
@@ -62,6 +63,7 @@ jobs:
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
PERFSEE_TOKEN: ${{ secrets.PERFSEE_TOKEN }}
- name: Upload core artifact
uses: actions/upload-artifact@v4
with:
@@ -69,10 +71,10 @@ jobs:
path: ./packages/frontend/core/dist
if-no-files-found: error
build-storage:
name: Build Storage
build-core-selfhost:
name: Build @affine/core
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
@@ -80,22 +82,33 @@ jobs:
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Rust
uses: ./.github/actions/build-rust
with:
target: 'x86_64-unknown-linux-gnu'
package: '@affine/storage'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Upload storage.node
- name: Build Core
run: yarn nx build @affine/core --skip-nx-cache
env:
BUILD_TYPE: ${{ github.event.inputs.flavor }}
SHOULD_REPORT_TRACE: false
PUBLIC_PATH: '/'
- name: Download selfhost fonts
run: node ./scripts/download-blocksuite-fonts.mjs
- name: Upload core artifact
uses: actions/upload-artifact@v4
with:
name: storage.node
path: ./packages/backend/storage/storage.node
name: selfhost-core
path: ./packages/frontend/core/dist
if-no-files-found: error
build-storage-arm64:
name: Build Storage arm64
build-storage:
name: Build Storage - ${{ matrix.targets.name }}
runs-on: ubuntu-latest
strategy:
matrix:
targets:
- name: x86_64-unknown-linux-gnu
file: storage.node
- name: aarch64-unknown-linux-gnu
file: storage.arm64.node
- name: armv7-unknown-linux-gnueabihf
file: storage.armv7.node
steps:
- uses: actions/checkout@v4
@@ -104,16 +117,19 @@ jobs:
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
extra-flags: workspaces focus @affine/storage
- name: Build Rust
uses: ./.github/actions/build-rust
with:
target: 'aarch64-unknown-linux-gnu'
target: ${{ matrix.targets.name }}
package: '@affine/storage'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Upload storage.node
- name: Upload ${{ matrix.targets.file }}
uses: actions/upload-artifact@v4
with:
name: storage.arm64.node
name: ${{ matrix.targets.file }}
path: ./packages/backend/storage/storage.node
if-no-files-found: error
@@ -123,8 +139,8 @@ jobs:
needs:
- build-server
- build-core
- build-core-selfhost
- build-storage
- build-storage-arm64
steps:
- uses: actions/checkout@v4
- name: Download core artifact
@@ -147,8 +163,15 @@ jobs:
with:
name: storage.arm64.node
path: ./packages/backend/storage
- name: move storage.arm64.node
run: mv ./packages/backend/storage/storage.node ./packages/backend/server/storage.arm64.node
- name: Download storage.node arm64
uses: actions/download-artifact@v4
with:
name: storage.armv7.node
path: .
- name: move storage files
run: |
mv ./packages/backend/storage/storage.node ./packages/backend/server/storage.arm64.node
mv storage.node ./packages/backend/server/storage.armv7.node
- name: Setup env
run: |
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
@@ -190,9 +213,19 @@ jobs:
registry-url: https://npm.pkg.github.com
scope: '@toeverything'
- name: Remove core dist
run: rm -rf ./packages/frontend/core/dist
- name: Download selfhost core artifact
uses: actions/download-artifact@v4
with:
name: selfhost-core
path: ./packages/frontend/core/dist
- name: Install Node.js dependencies
run: |
yarn config set --json supportedArchitectures.cpu '["x64", "arm64"]'
yarn config set --json supportedArchitectures.cpu '["x64", "arm64", "arm"]'
yarn config set --json supportedArchitectures.libc '["glibc"]'
yarn workspaces focus @affine/server --production
- name: Generate Prisma client
@@ -204,7 +237,7 @@ jobs:
context: .
push: true
pull: true
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7
provenance: true
file: .github/deployment/node/Dockerfile
tags: ghcr.io/toeverything/affine-graphql:${{env.RELEASE_FLAVOR}}-${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-graphql:${{env.RELEASE_FLAVOR}}

View File

@@ -0,0 +1,51 @@
name: Publish UI Storybook
env:
NODE_OPTIONS: --max-old-space-size=4096
on:
workflow_dispatch:
push:
branches:
- canary
pull_request:
branches:
- canary
paths-ignore:
- README.md
- .github/**
- packages/backend/server
- packages/frontend/electron
- '!.github/workflows/publish-storybook.yml'
jobs:
publish-ui-storybook:
name: Publish UI Storybook
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
# This is required to fetch all commits for chromatic
fetch-depth: 0
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- uses: chromaui/action-next@v1
with:
workingDir: packages/frontend/component
buildScriptName: build:storybook
exitOnceUploaded: true
onlyChanged: false
diagnostics: true
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_UI_PROJECT_TOKEN }}
NODE_OPTIONS: ${{ env.NODE_OPTIONS }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: chromatic-build-artifacts-${{ github.run_id }}
path: |
chromatic-diagnostics.json
**/build-storybook.log

View File

@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged && yarn lint:ox

View File

@@ -138,24 +138,6 @@ We would like to express our gratitude to all the individuals who have already c
<img alt="contributors" src="https://opencollective.com/affine/contributors.svg?width=890&button=false" />
</a>
## Data Compatibility
Data compatibility is a very important issue for us. We will try our best to ensure that the data is compatible with the previous version.
If you encounter any problems when upgrading the version, please feel free to [contact us](mailto:developer@toeverything.info).
| AFFiNE Version | Export/Import workspace | Data auto migration |
| --------------- | ----------------------- | ------------------- |
| <= 0.5.4 | ❌️ | ❌ |
| 0.6.x | ✅️ | ✅ |
| 0.7.x | ✅️ | ✅ |
| 0.8.x (current) | ✅ | ✅ |
| 0.9.x (next) | 🚧 | 🚧 |
- ❌️: Not compatible
- ✅: Compatible
- 🚧: Work in progress
## Self-Host
> We know that the self-host version has been out of date for a long time.

View File

@@ -19,5 +19,5 @@
],
"ext": "ts,md,json"
},
"version": "0.10.3-canary.2"
"version": "0.12.0"
}

View File

@@ -1,12 +1,13 @@
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"npmScope": "toeverything",
"nxCloudAccessToken": "MzUwNTU4YWItZGFhYi00YjE2LWIxODAtODk4NmIwYjMwYzZkfHJlYWQ=",
"tasksRunnerOptions": {
"default": {
"runner": "nx-cloud",
"options": {
"cacheableOperations": ["build", "test", "e2e", "lint"],
"accessToken": "YmQ2NTg1ODktZTk5Mi00YzhiLTk2ZmUtNWQzMDg0NDBkOWM3fHJlYWQtb25seQ=="
"runtimeCacheInputs": ["node -v"]
}
}
},

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/monorepo",
"version": "0.11.0",
"version": "0.12.0",
"private": true,
"author": "toeverything",
"license": "MIT",
@@ -37,7 +37,8 @@
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"typecheck": "tsc -b tsconfig.json --diagnostics",
"postinstall": "node ./scripts/check-version.mjs && yarn i18n-codegen gen && yarn husky install"
"postinstall": "node ./scripts/check-version.mjs && yarn i18n-codegen gen && yarn husky install",
"prepare": "husky"
},
"lint-staged": {
"*": "prettier --write --ignore-unknown --cache",
@@ -61,8 +62,7 @@
"@istanbuljs/schema": "^0.1.3",
"@magic-works/i18n-codegen": "^0.5.0",
"@nx/vite": "17.2.8",
"@perfsee/sdk": "^1.9.0",
"@playwright/test": "^1.40.0",
"@playwright/test": "^1.41.0",
"@taplo/cli": "^0.5.2",
"@testing-library/react": "^14.1.2",
"@toeverything/infra": "workspace:*",
@@ -76,7 +76,7 @@
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/coverage-istanbul": "1.1.3",
"@vitest/ui": "1.1.3",
"electron": "^27.1.0",
"electron": "^28.1.4",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-i": "^2.29.0",
@@ -88,13 +88,12 @@
"eslint-plugin-unused-imports": "^3.0.0",
"eslint-plugin-vue": "^9.18.1",
"fake-indexeddb": "5.0.2",
"happy-dom": "^12.10.3",
"husky": "^8.0.3",
"happy-dom": "^13.0.0",
"husky": "^9.0.6",
"lint-staged": "^15.1.0",
"msw": "^2.0.8",
"nanoid": "^5.0.3",
"nx": "^17.1.3",
"nx-cloud": "^16.5.2",
"nx": "^17.2.8",
"nyc": "^15.1.0",
"oxlint": "0.0.22",
"prettier": "^3.1.0",

View File

@@ -1,8 +1,4 @@
DATABASE_URL="postgresql://affine@localhost:5432/affine"
NEXTAUTH_URL="http://localhost:8080"
OAUTH_EMAIL_SENDER="noreply@toeverything.info"
OAUTH_EMAIL_LOGIN=""
OAUTH_EMAIL_PASSWORD=""
ENABLE_LOCAL_EMAIL="true"
STRIPE_API_KEY=
STRIPE_WEBHOOK_KEY=
# AFFINE_SERVER_PORT=3010
# AFFINE_SERVER_HOST=app.affine.pro
# AFFINE_SERVER_HTTPS=true
# DATABASE_URL="postgres://affine@localhost:5432/affine"

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.11.0",
"version": "0.12.0",
"description": "Affine Node.js server",
"type": "module",
"bin": {
@@ -14,15 +14,16 @@
"test": "ava --concurrency 1 --serial",
"test:coverage": "c8 ava --concurrency 1 --serial",
"postinstall": "prisma generate",
"data-migration": "node --loader ts-node/esm/transpile-only.mjs --es-module-specifier-resolution node ./src/data/app.ts",
"predeploy": "yarn prisma migrate deploy && node --es-module-specifier-resolution node ./dist/data/app.js run"
"data-migration": "node --loader ts-node/esm/transpile-only.mjs --es-module-specifier-resolution node ./src/data/index.ts",
"predeploy": "yarn prisma migrate deploy && node --es-module-specifier-resolution node ./dist/data/index.js run"
},
"dependencies": {
"@apollo/server": "^4.9.5",
"@auth/prisma-adapter": "^1.0.7",
"@aws-sdk/client-s3": "^3.454.0",
"@aws-sdk/client-s3": "^3.499.0",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.17.0",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.1.0",
"@google-cloud/opentelemetry-resource-util": "^2.1.0",
"@keyv/redis": "^2.8.0",
"@nestjs/apollo": "^12.0.11",
"@nestjs/common": "^10.2.10",
@@ -32,35 +33,39 @@
"@nestjs/platform-express": "^10.2.10",
"@nestjs/platform-socket.io": "^10.2.10",
"@nestjs/schedule": "^4.0.0",
"@nestjs/serve-static": "^4.0.0",
"@nestjs/throttler": "^5.0.1",
"@nestjs/websockets": "^10.2.10",
"@node-rs/argon2": "^1.5.2",
"@node-rs/crc32": "^1.7.2",
"@node-rs/jsonwebtoken": "^0.3.0",
"@opentelemetry/api": "^1.7.0",
"@opentelemetry/core": "^1.19.0",
"@opentelemetry/exporter-prometheus": "^0.46.0",
"@opentelemetry/exporter-zipkin": "^1.19.0",
"@opentelemetry/core": "^1.20.0",
"@opentelemetry/exporter-prometheus": "^0.47.0",
"@opentelemetry/exporter-zipkin": "^1.20.0",
"@opentelemetry/host-metrics": "^0.34.0",
"@opentelemetry/instrumentation": "^0.46.0",
"@opentelemetry/instrumentation": "^0.47.0",
"@opentelemetry/instrumentation-graphql": "^0.36.0",
"@opentelemetry/instrumentation-http": "^0.46.0",
"@opentelemetry/instrumentation-http": "^0.47.0",
"@opentelemetry/instrumentation-ioredis": "^0.36.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.33.3",
"@opentelemetry/instrumentation-socket.io": "^0.35.0",
"@opentelemetry/resources": "^1.19.0",
"@opentelemetry/sdk-metrics": "^1.19.0",
"@opentelemetry/sdk-node": "^0.46.0",
"@opentelemetry/sdk-trace-node": "^1.19.0",
"@opentelemetry/resources": "^1.20.0",
"@opentelemetry/sdk-metrics": "^1.20.0",
"@opentelemetry/sdk-node": "^0.47.0",
"@opentelemetry/sdk-trace-node": "^1.20.0",
"@opentelemetry/semantic-conventions": "^1.20.0",
"@prisma/client": "^5.7.1",
"@prisma/instrumentation": "^5.7.1",
"@socket.io/redis-adapter": "^8.2.1",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
"dotenv-cli": "^7.3.0",
"express": "^4.18.2",
"file-type": "^19.0.0",
"get-stream": "^8.0.1",
"graphql": "^16.8.1",
"graphql-scalars": "^1.22.4",
"graphql-type-json": "^0.3.2",
"graphql-upload": "^16.0.2",
"ioredis": "^5.3.2",
@@ -112,6 +117,7 @@
"typescript": "^5.3.2"
},
"ava": {
"timeout": "1m",
"extensions": {
"ts": "module"
},

View File

@@ -0,0 +1,51 @@
import { execSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
const SELF_HOST_CONFIG_DIR = '/root/.affine/config';
/**
* @type {Array<{ from: string; to?: string, modifier?: (content: string): string }>}
*/
const configFiles = [
{ from: './.env.example', to: '.env' },
{ from: './dist/config/affine.js', modifier: configCleaner },
{ from: './dist/config/affine.env.js', modifier: configCleaner },
];
function configCleaner(content) {
return content.replace(/(\/\/#.*$)|(\/\/\s+TODO.*$)/gm, '');
}
function prepare() {
fs.mkdirSync(SELF_HOST_CONFIG_DIR, { recursive: true });
for (const { from, to, modifier } of configFiles) {
const targetFileName = to ?? path.parse(from).base;
const targetFilePath = path.join(SELF_HOST_CONFIG_DIR, targetFileName);
if (!fs.existsSync(targetFilePath)) {
console.log(`creating config file [${targetFilePath}].`);
if (modifier) {
const content = fs.readFileSync(from, 'utf-8');
fs.writeFileSync(targetFilePath, modifier(content), 'utf-8');
} else {
fs.cpSync(from, targetFilePath, {
force: false,
});
}
}
}
}
function runPredeployScript() {
console.log('running predeploy script.');
execSync('yarn predeploy', {
env: {
...process.env,
NODE_OPTIONS:
(process.env.NODE_OPTIONS ?? '') + ' --import ./dist/prelude.js',
},
});
}
prepare();
runPredeployScript();

View File

@@ -1,3 +0,0 @@
import { getDefaultAFFiNEConfig } from './config/default';
globalThis.AFFiNE = getDefaultAFFiNEConfig();

View File

@@ -1,13 +1,17 @@
import { Controller, Get } from '@nestjs/common';
import { Config } from './fundamentals/config';
@Controller('/')
export class AppController {
constructor(private readonly config: Config) {}
@Get()
info() {
const version = AFFiNE.version;
return {
compatibility: version,
message: `AFFiNE ${version} Server`,
compatibility: this.config.version,
message: `AFFiNE ${this.config.version} Server`,
flavor: this.config.flavor,
};
}
}

View File

@@ -0,0 +1,169 @@
import { join } from 'node:path';
import { Logger, Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static';
import { get } from 'lodash-es';
import { AppController } from './app.controller';
import { AuthModule } from './core/auth';
import { ADD_ENABLED_FEATURES, ServerConfigModule } from './core/config';
import { DocModule } from './core/doc';
import { FeatureModule } from './core/features';
import { QuotaModule } from './core/quota';
import { StorageModule } from './core/storage';
import { SyncModule } from './core/sync';
import { UsersModule } from './core/users';
import { WorkspaceModule } from './core/workspaces';
import { getOptionalModuleMetadata } from './fundamentals';
import { CacheInterceptor, CacheModule } from './fundamentals/cache';
import {
type AvailablePlugins,
Config,
ConfigModule,
} from './fundamentals/config';
import { EventModule } from './fundamentals/event';
import { GqlModule } from './fundamentals/graphql';
import { MailModule } from './fundamentals/mailer';
import { MetricsModule } from './fundamentals/metrics';
import { PrismaModule } from './fundamentals/prisma';
import { SessionModule } from './fundamentals/session';
import { RateLimiterModule } from './fundamentals/throttler';
import { WebSocketModule } from './fundamentals/websocket';
import { pluginsMap } from './plugins';
export const FunctionalityModules = [
ConfigModule.forRoot(),
ScheduleModule.forRoot(),
EventModule,
CacheModule,
PrismaModule,
MetricsModule,
RateLimiterModule,
SessionModule,
MailModule,
];
export class AppModuleBuilder {
private readonly modules: AFFiNEModule[] = [];
constructor(private readonly config: Config) {}
use(...modules: AFFiNEModule[]): this {
modules.forEach(m => {
const requirements = getOptionalModuleMetadata(m, 'requires');
// if condition not set or condition met, include the module
if (requirements?.length) {
const nonMetRequirements = requirements.filter(c => {
const value = get(this.config, c);
return (
value === undefined ||
value === null ||
(typeof value === 'string' && value.trim().length === 0)
);
});
if (nonMetRequirements.length) {
const name = 'module' in m ? m.module.name : m.name;
new Logger(name).warn(
`${name} is not enabled because of the required configuration is not satisfied.`,
'Unsatisfied configuration:',
...nonMetRequirements.map(config => ` AFFiNE.${config}`)
);
return;
}
}
const predicator = getOptionalModuleMetadata(m, 'if');
if (predicator && !predicator(this.config)) {
return;
}
const contribution = getOptionalModuleMetadata(m, 'contributesTo');
if (contribution) {
ADD_ENABLED_FEATURES(contribution);
}
this.modules.push(m);
});
return this;
}
useIf(
predicator: (config: Config) => boolean,
...modules: AFFiNEModule[]
): this {
if (predicator(this.config)) {
this.use(...modules);
}
return this;
}
compile() {
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: CacheInterceptor,
},
],
imports: this.modules,
controllers: this.config.flavor.selfhosted ? [] : [AppController],
})
class AppModule {}
return AppModule;
}
}
function buildAppModule() {
const factor = new AppModuleBuilder(AFFiNE);
factor
// common fundamental modules
.use(...FunctionalityModules)
// auth
.use(AuthModule)
// business modules
.use(DocModule)
// sync server only
.useIf(config => config.flavor.sync, SyncModule)
// main server only
.useIf(
config => config.flavor.main,
ServerConfigModule,
WebSocketModule,
GqlModule,
StorageModule,
UsersModule,
WorkspaceModule,
FeatureModule,
QuotaModule
)
// self hosted server only
.useIf(
config => config.flavor.selfhosted,
ServeStaticModule.forRoot({
rootPath: join('/app', 'static'),
})
);
// plugin modules
AFFiNE.plugins.enabled.forEach(name => {
const plugin = pluginsMap.get(name as AvailablePlugins);
if (!plugin) {
throw new Error(`Unknown plugin ${name}`);
}
factor.use(plugin);
});
return factor.compile();
}
export const AppModule = buildAppModule();

View File

@@ -1,34 +1,48 @@
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { Type } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import cookieParser from 'cookie-parser';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { AppController } from './app.controller';
import { CacheInterceptor, CacheModule } from './cache';
import { ConfigModule } from './config';
import { EventModule } from './event';
import { BusinessModules } from './modules';
import { AuthModule } from './modules/auth';
import { PrismaModule } from './prisma';
import { SessionModule } from './session';
import { RateLimiterModule } from './throttler';
import { SocketIoAdapter } from './fundamentals';
import { SocketIoAdapterImpl } from './fundamentals/websocket';
import { ExceptionLogger } from './middleware/exception-logger';
import { serverTimingAndCache } from './middleware/timing';
const BasicModules = [
PrismaModule,
ConfigModule.forRoot(),
CacheModule,
EventModule,
SessionModule,
RateLimiterModule,
AuthModule,
];
export async function createApp() {
const { AppModule } = await import('./app.module');
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: CacheInterceptor,
},
],
imports: [...BasicModules, ...BusinessModules],
controllers: [AppController],
})
export class AppModule {}
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
cors: true,
rawBody: true,
bodyParser: true,
logger: AFFiNE.affine.stable ? ['log'] : ['verbose'],
});
app.use(serverTimingAndCache);
app.use(
graphqlUploadExpress({
// TODO: dynamic limit by quota
maxFileSize: 100 * 1024 * 1024,
maxFiles: 5,
})
);
app.useGlobalFilters(new ExceptionLogger());
app.use(cookieParser());
if (AFFiNE.flavor.sync) {
const SocketIoAdapter = app.get<Type<SocketIoAdapter>>(
SocketIoAdapterImpl,
{
strict: false,
}
);
const adapter = new SocketIoAdapter(app);
app.useWebSocketAdapter(adapter);
}
return app;
}

View File

@@ -1,26 +0,0 @@
import { FactoryProvider, Global, Module } from '@nestjs/common';
import { Redis } from 'ioredis';
import { Config } from '../config';
import { LocalCache } from './cache';
import { RedisCache } from './redis';
const CacheProvider: FactoryProvider = {
provide: LocalCache,
useFactory: (config: Config) => {
return config.redis.enabled
? new RedisCache(new Redis(config.redis))
: new LocalCache();
},
inject: [Config],
};
@Global()
@Module({
providers: [CacheProvider],
exports: [CacheProvider],
})
export class CacheModule {}
export { LocalCache as Cache };
export { CacheInterceptor, MakeCache, PreventCache } from './interceptor';

View File

@@ -0,0 +1,40 @@
// Convenient way to map environment variables to config values.
AFFiNE.ENV_MAP = {
AFFINE_SERVER_PORT: ['port', 'int'],
AFFINE_SERVER_HOST: 'host',
AFFINE_SERVER_SUB_PATH: 'path',
AFFIHE_SERVER_HTTPS: ['https', 'boolean'],
AFFINE_ENV: 'affineEnv',
DATABASE_URL: 'db.url',
ENABLE_CAPTCHA: ['auth.captcha.enable', 'boolean'],
CAPTCHA_TURNSTILE_SECRET: ['auth.captcha.turnstile.secret', 'string'],
OAUTH_GOOGLE_ENABLED: ['auth.oauthProviders.google.enabled', 'boolean'],
OAUTH_GOOGLE_CLIENT_ID: 'auth.oauthProviders.google.clientId',
OAUTH_GOOGLE_CLIENT_SECRET: 'auth.oauthProviders.google.clientSecret',
OAUTH_GITHUB_ENABLED: ['auth.oauthProviders.github.enabled', 'boolean'],
OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId',
OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret',
OAUTH_EMAIL_LOGIN: 'auth.email.login',
OAUTH_EMAIL_SENDER: 'auth.email.sender',
OAUTH_EMAIL_SERVER: 'auth.email.server',
OAUTH_EMAIL_PORT: ['auth.email.port', 'int'],
OAUTH_EMAIL_PASSWORD: 'auth.email.password',
THROTTLE_TTL: ['rateLimiter.ttl', 'int'],
THROTTLE_LIMIT: ['rateLimiter.limit', 'int'],
REDIS_SERVER_HOST: 'plugins.redis.host',
REDIS_SERVER_PORT: ['plugins.redis.port', 'int'],
REDIS_SERVER_USER: 'plugins.redis.username',
REDIS_SERVER_PASSWORD: 'plugins.redis.password',
REDIS_SERVER_DATABASE: ['plugins.redis.db', 'int'],
DOC_MERGE_INTERVAL: ['doc.manager.updatePollInterval', 'int'],
DOC_MERGE_USE_JWST_CODEC: [
'doc.manager.experimentalMergeWithJwstCodec',
'boolean',
],
ENABLE_LOCAL_EMAIL: ['auth.localEmail', 'boolean'],
STRIPE_API_KEY: 'plugins.payment.stripe.keys.APIKey',
STRIPE_WEBHOOK_KEY: 'plugins.payment.stripe.keys.webhookKey',
FEATURES_EARLY_ACCESS_PREVIEW: ['featureFlags.earlyAccessPreview', 'boolean'],
};
export default AFFiNE;

View File

@@ -1,11 +1,10 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
// Custom configurations
const env = process.env;
const node = AFFiNE.node;
// TODO: may be separate config overring in `affine.[env].config`?
if (node.prod && env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
// TODO(@forehalo): detail explained
// Storage
if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
AFFiNE.storage.providers.r2 = {
accountId: env.R2_OBJECT_STORAGE_ACCOUNT_ID,
credentials: {
@@ -23,3 +22,18 @@ if (node.prod && env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
AFFiNE.affine.canary ? 'canary' : 'prod'
}`;
}
// Metrics
AFFiNE.metrics.enabled = true;
// Plugins Section Start
AFFiNE.plugins.use('payment', {
stripe: {
keys: {},
apiVersion: '2023-10-16',
},
});
AFFiNE.plugins.use('redis');
// Plugins Section end
export default AFFiNE;

View File

@@ -1,17 +0,0 @@
import { set } from 'lodash-es';
import { type AFFiNEConfig, parseEnvValue } from './def';
export function applyEnvToConfig(rawConfig: AFFiNEConfig) {
for (const env in rawConfig.ENV_MAP) {
const config = rawConfig.ENV_MAP[env];
const [path, value] =
typeof config === 'string'
? [config, process.env[env]]
: [config[0], parseEnvValue(process.env[env], config[1])];
if (value !== undefined) {
set(rawConfig, path, value);
}
}
}

View File

@@ -1,3 +0,0 @@
export const OPERATION_NAME = 'x-operation-name';
export const REQUEST_ID = 'x-request-id';

View File

@@ -10,8 +10,10 @@ import { Reflector } from '@nestjs/core';
import type { NextAuthOptions } from 'next-auth';
import { AuthHandler } from 'next-auth/core';
import { PrismaService } from '../../prisma';
import { getRequestResponseFromContext } from '../../utils/nestjs';
import {
getRequestResponseFromContext,
PrismaService,
} from '../../fundamentals';
import { NextAuthOptionsProvide } from './next-auth-options';
import { AuthService } from './service';

View File

@@ -1,7 +1,5 @@
import { Global, Module } from '@nestjs/common';
import { SessionModule } from '../../session';
import { MAILER, MailService } from './mailer';
import { NextAuthController } from './next-auth.controller';
import { NextAuthOptionsProvider } from './next-auth-options';
import { AuthResolver } from './resolver';
@@ -9,18 +7,12 @@ import { AuthService } from './service';
@Global()
@Module({
imports: [SessionModule],
providers: [
AuthService,
AuthResolver,
NextAuthOptionsProvider,
MAILER,
MailService,
],
exports: [AuthService, NextAuthOptionsProvider, MailService],
providers: [AuthService, AuthResolver, NextAuthOptionsProvider],
exports: [AuthService, NextAuthOptionsProvider],
controllers: [NextAuthController],
})
export class AuthModule {}
export * from './guard';
export { TokenType } from './resolver';
export { AuthService };

View File

@@ -8,12 +8,14 @@ import Email from 'next-auth/providers/email';
import Github from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
import { Config } from '../../config';
import { PrismaService } from '../../prisma';
import { SessionService } from '../../session';
import {
Config,
MailService,
PrismaService,
SessionService,
} from '../../fundamentals';
import { FeatureType } from '../features';
import { Quota_FreePlanV1 } from '../quota';
import { MailService } from './mailer';
import { Quota_FreePlanV1_1 } from '../quota';
import {
decode,
encode,
@@ -50,7 +52,7 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
activated: true,
feature: {
connect: {
feature_version: Quota_FreePlanV1,
feature_version: Quota_FreePlanV1_1,
},
},
},
@@ -179,7 +181,7 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
);
}
if (config.auth.oauthProviders.google) {
if (config.auth.oauthProviders.google?.enabled) {
nextAuthOptions.providers.push(
// @ts-expect-error esm interop issue
Google.default({

View File

@@ -22,11 +22,14 @@ import { nanoid } from 'nanoid';
import type { AuthAction, CookieOption, NextAuthOptions } from 'next-auth';
import { AuthHandler } from 'next-auth/core';
import { Config } from '../../config';
import { metrics } from '../../metrics';
import { PrismaService } from '../../prisma/service';
import { SessionService } from '../../session';
import { AuthThrottlerGuard, Throttle } from '../../throttler';
import {
AuthThrottlerGuard,
Config,
metrics,
PrismaService,
SessionService,
Throttle,
} from '../../fundamentals';
import { NextAuthOptionsProvide } from './next-auth-options';
import { AuthService } from './service';
@@ -186,15 +189,17 @@ export class NextAuthController {
}
let nextAuthTokenCookie: (CookieOption & { value: string }) | undefined;
const cookiePrefix = this.config.node.prod ? '__Secure-' : '';
const sessionCookieName = `${cookiePrefix}next-auth.session-token`;
const secureCookiePrefix = '__Secure-';
const sessionCookieName = `next-auth.session-token`;
// next-auth credentials login only support JWT strategy
// https://next-auth.js.org/configuration/providers/credentials
// let's store the session token in the database
if (
credentialsSignIn &&
(nextAuthTokenCookie = cookies?.find(
({ name }) => name === sessionCookieName
({ name }) =>
name === sessionCookieName ||
name === `${secureCookiePrefix}${sessionCookieName}`
))
) {
const cookieExpires = new Date();

View File

@@ -16,9 +16,12 @@ import {
import type { Request } from 'express';
import { nanoid } from 'nanoid';
import { Config } from '../../config';
import { SessionService } from '../../session';
import { CloudThrottlerGuard, Throttle } from '../../throttler';
import {
CloudThrottlerGuard,
Config,
SessionService,
Throttle,
} from '../../fundamentals';
import { UserType } from '../users';
import { Auth, CurrentUser } from './guard';
import { AuthService } from './service';
@@ -167,8 +170,13 @@ export class AuthResolver {
@CurrentUser() user: UserType,
@Args('token') token: string
) {
const key = await this.session.get(token);
if (!key) {
throw new ForbiddenException('Invalid token');
}
// email has set token in `sendVerifyChangeEmail`
const [id, email] = (await this.session.get(token)).split(',');
const [id, email] = key.split(',');
if (!id || id !== user.id || !email) {
throw new ForbiddenException('Invalid token');
}

View File

@@ -11,11 +11,13 @@ import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken';
import type { User } from '@prisma/client';
import { nanoid } from 'nanoid';
import { Config } from '../../config';
import { PrismaService } from '../../prisma';
import { verifyChallengeResponse } from '../../storage';
import { Quota_FreePlanV1 } from '../quota';
import { MailService } from './mailer';
import {
Config,
MailService,
PrismaService,
verifyChallengeResponse,
} from '../../fundamentals';
import { Quota_FreePlanV1_1 } from '../quota';
export type UserClaim = Pick<
User,
@@ -192,13 +194,14 @@ export class AuthService {
name,
email,
password: hashedPassword,
// TODO(@forehalo): handle in event system
features: {
create: {
reason: 'created by api sign up',
activated: true,
feature: {
connect: {
feature_version: Quota_FreePlanV1,
feature_version: Quota_FreePlanV1_1,
},
},
},
@@ -228,7 +231,7 @@ export class AuthService {
activated: true,
feature: {
connect: {
feature_version: Quota_FreePlanV1,
feature_version: Quota_FreePlanV1_1,
},
},
},

View File

@@ -4,8 +4,7 @@ import { BadRequestException } from '@nestjs/common';
import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken';
import { JWT } from 'next-auth/jwt';
import { Config } from '../../../config';
import { PrismaService } from '../../../prisma';
import { Config, PrismaService } from '../../../fundamentals';
import { getUtcTimestamp, UserClaim } from '../service';
export const jwtEncode = async (

View File

@@ -2,9 +2,7 @@ import { Logger } from '@nestjs/common';
import { nanoid } from 'nanoid';
import type { SendVerificationRequestParams } from 'next-auth/providers/email';
import { Config } from '../../../config';
import { SessionService } from '../../../session';
import { MailService } from '../mailer';
import { Config, MailService, SessionService } from '../../../fundamentals';
export async function sendVerificationRequest(
config: Config,

View File

@@ -0,0 +1,58 @@
import { Module } from '@nestjs/common';
import { Field, ObjectType, Query, registerEnumType } from '@nestjs/graphql';
export enum ServerFeature {
Payment = 'payment',
}
registerEnumType(ServerFeature, {
name: 'ServerFeature',
});
const ENABLED_FEATURES: ServerFeature[] = [];
export function ADD_ENABLED_FEATURES(feature: ServerFeature) {
ENABLED_FEATURES.push(feature);
}
@ObjectType()
export class ServerConfigType {
@Field({
description:
'server identical name could be shown as badge on user interface',
})
name!: string;
@Field({ description: 'server version' })
version!: string;
@Field({ description: 'server base url' })
baseUrl!: string;
/**
* @deprecated
*/
@Field({ description: 'server flavor', deprecationReason: 'use `features`' })
flavor!: string;
@Field(() => [ServerFeature], { description: 'enabled server features' })
features!: ServerFeature[];
}
export class ServerConfigResolver {
@Query(() => ServerConfigType, {
description: 'server config',
})
serverConfig(): ServerConfigType {
return {
name: AFFiNE.serverName,
version: AFFiNE.version,
baseUrl: AFFiNE.baseUrl,
flavor: AFFiNE.flavor.type,
features: ENABLED_FEATURES,
};
}
}
@Module({
providers: [ServerConfigResolver],
})
export class ServerConfigModule {}

View File

@@ -3,10 +3,13 @@ import { isDeepStrictEqual } from 'node:util';
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Config } from '../../config';
import { type EventPayload, OnEvent } from '../../event';
import { metrics } from '../../metrics';
import { PrismaService } from '../../prisma';
import {
Config,
type EventPayload,
metrics,
OnEvent,
PrismaService,
} from '../../fundamentals';
import { QuotaService } from '../quota';
import { Permission } from '../workspaces/types';
import { isEmptyBuffer } from './manager';

View File

@@ -4,6 +4,7 @@ import {
OnModuleDestroy,
OnModuleInit,
} from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { Snapshot, Update } from '@prisma/client';
import { chunk } from 'lodash-es';
import { defer, retry } from 'rxjs';
@@ -16,12 +17,16 @@ import {
transact,
} from 'yjs';
import { Cache } from '../../cache';
import { Config } from '../../config';
import { EventEmitter, type EventPayload, OnEvent } from '../../event';
import { metrics } from '../../metrics/metrics';
import { PrismaService } from '../../prisma';
import { mergeUpdatesInApplyWay as jwstMergeUpdates } from '../../storage';
import {
Cache,
Config,
EventEmitter,
type EventPayload,
mergeUpdatesInApplyWay as jwstMergeUpdates,
metrics,
OnEvent,
PrismaService,
} from '../../fundamentals';
function compare(yBinary: Buffer, jwstBinary: Buffer, strict = false): boolean {
if (yBinary.equals(jwstBinary)) {
@@ -79,6 +84,7 @@ export function isEmptyBuffer(buf: Buffer): boolean {
}
const MAX_SEQ_NUM = 0x3fffffff; // u31
const UPDATES_QUEUE_CACHE_KEY = 'doc:manager:updates';
/**
* Since we can't directly save all client updates into database, in which way the database will overload,
@@ -663,26 +669,44 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
count: number
) {
const result = await this.cache.mapIncrease(
`doc:manager:updates`,
UPDATES_QUEUE_CACHE_KEY,
`${workspaceId}::${guid}`,
count
);
if (result <= 0) {
await this.cache.mapDelete(
`doc:manager:updates`,
UPDATES_QUEUE_CACHE_KEY,
`${workspaceId}::${guid}`
);
}
}
private async getAutoSquashCandidateFromCache() {
const key = await this.cache.mapRandomKey('doc:manager:updates');
const key = await this.cache.mapRandomKey(UPDATES_QUEUE_CACHE_KEY);
if (key) {
const count = await this.cache.mapGet<number>('doc:manager:updates', key);
if (typeof count === 'number' && count > 0) {
const cachedCount = await this.cache.mapIncrease(
UPDATES_QUEUE_CACHE_KEY,
key,
0
);
if (cachedCount > 0) {
const [workspaceId, id] = key.split('::');
const count = await this.db.update.count({
where: {
workspaceId,
id,
},
});
// FIXME(@forehalo): somehow the update count in cache is not accurate
if (count === 0) {
await this.cache.mapDelete(UPDATES_QUEUE_CACHE_KEY, key);
return null;
}
return { id, workspaceId };
}
}
@@ -690,22 +714,38 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
return null;
}
private async doWithLock<T>(lock: string, job: () => Promise<T>) {
private async doWithLock<T>(
lockScope: string,
lockResource: string,
job: () => Promise<T>
) {
const lock = `lock:${lockScope}:${lockResource}`;
const acquired = await this.cache.setnx(lock, 1, {
ttl: 60 * 1000,
});
metrics.doc.counter('lock').add(1, { scope: lockScope });
if (!acquired) {
metrics.doc.counter('lock_failed').add(1, { scope: lockScope });
return;
}
metrics.doc.counter('lock_required').add(1, { scope: lockScope });
try {
return await job();
} finally {
await this.cache.delete(lock).catch(e => {
// safe, the lock will be expired when ttl ends
this.logger.error(`Failed to release lock ${lock}`, e);
});
await this.cache
.delete(lock)
.then(() => {
metrics.doc.counter('lock_released').add(1, { scope: lockScope });
})
.catch(e => {
metrics.doc
.counter('lock_release_failed')
.add(1, { scope: lockScope });
// safe, the lock will be expired when ttl ends
this.logger.error(`Failed to release lock ${lock}`, e);
});
}
}
@@ -715,7 +755,8 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
job: () => Promise<T>
) {
return this.doWithLock(
`doc:manager:updates-lock:${workspaceId}::${guid}`,
'doc:manager:updates',
`${workspaceId}::${guid}`,
job
);
}
@@ -726,8 +767,16 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
job: () => Promise<T>
) {
return this.doWithLock(
`doc:manager:snapshot-lock:${workspaceId}::${guid}`,
'doc:manager:snapshot',
`${workspaceId}::${guid}`,
job
);
}
@Cron(CronExpression.EVERY_MINUTE)
async reportUpdatesQueueCount() {
metrics.doc
.gauge('updates_queue_count')
.record(await this.db.update.count());
}
}

View File

@@ -1,4 +1,5 @@
import { PrismaService } from '../../prisma';
import { PrismaClient } from '@prisma/client';
import { Feature, FeatureSchema, FeatureType } from './types';
class FeatureConfig {
@@ -42,9 +43,22 @@ export class EarlyAccessFeatureConfig extends FeatureConfig {
}
}
export class UnlimitedWorkspaceFeatureConfig extends FeatureConfig {
override config!: Feature & { feature: FeatureType.UnlimitedWorkspace };
constructor(data: any) {
super(data);
if (this.config.feature !== FeatureType.UnlimitedWorkspace) {
throw new Error('Invalid feature config: type is not UnlimitedWorkspace');
}
}
}
const FeatureConfigMap = {
[FeatureType.Copilot]: CopilotFeatureConfig,
[FeatureType.EarlyAccess]: EarlyAccessFeatureConfig,
[FeatureType.UnlimitedWorkspace]: UnlimitedWorkspaceFeatureConfig,
};
export type FeatureConfigType<F extends FeatureType> = InstanceType<
@@ -53,7 +67,7 @@ export type FeatureConfigType<F extends FeatureType> = InstanceType<
const FeatureCache = new Map<number, FeatureConfigType<FeatureType>>();
export async function getFeature(prisma: PrismaService, featureId: number) {
export async function getFeature(prisma: PrismaClient, featureId: number) {
const cachedQuota = FeatureCache.get(featureId);
if (cachedQuota) {

View File

@@ -1,6 +1,5 @@
import { Module } from '@nestjs/common';
import { PrismaService } from '../../prisma';
import { FeatureManagementService } from './management';
import { FeatureService } from './service';
@@ -18,4 +17,4 @@ export class FeatureModule {}
export { type CommonFeature, commonFeatureSchema } from './types';
export { FeatureKind, Features, FeatureType } from './types';
export { FeatureManagementService, FeatureService, PrismaService };
export { FeatureManagementService, FeatureService };

View File

@@ -1,7 +1,6 @@
import { Injectable, Logger } from '@nestjs/common';
import { Config } from '../../config';
import { PrismaService } from '../../prisma';
import { Config, PrismaService } from '../../fundamentals';
import { FeatureService } from './service';
import { FeatureType } from './types';
@@ -48,22 +47,26 @@ export class FeatureManagementService {
return this.feature.listFeatureUsers(FeatureType.EarlyAccess);
}
async isEarlyAccessUser(email: string) {
const user = await this.prisma.user.findFirst({
where: {
email,
},
});
if (user) {
const canEarlyAccess = await this.feature
.hasUserFeature(user.id, FeatureType.EarlyAccess)
.catch(() => false);
return canEarlyAccess;
}
return false;
}
/// check early access by email
async canEarlyAccess(email: string) {
if (this.config.featureFlags.earlyAccessPreview && !this.isStaff(email)) {
const user = await this.prisma.user.findFirst({
where: {
email,
},
});
if (user) {
const canEarlyAccess = await this.feature
.hasUserFeature(user.id, FeatureType.EarlyAccess)
.catch(() => false);
return canEarlyAccess;
}
return false;
return this.isEarlyAccessUser(email);
} else {
return true;
}

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma';
import { PrismaService } from '../../fundamentals';
import { UserType } from '../users/types';
import { WorkspaceType } from '../workspaces/types';
import { FeatureConfigType, getFeature } from './feature';

View File

@@ -3,6 +3,7 @@ import { registerEnumType } from '@nestjs/graphql';
export enum FeatureType {
Copilot = 'copilot',
EarlyAccess = 'early_access',
UnlimitedWorkspace = 'unlimited_workspace',
}
registerEnumType(FeatureType, {

View File

@@ -4,5 +4,8 @@ import { FeatureType } from './common';
export const featureEarlyAccess = z.object({
feature: z.literal(FeatureType.EarlyAccess),
configs: z.object({}),
configs: z.object({
// field polyfill, make it optional in the future
whitelist: z.string().array(),
}),
});

View File

@@ -3,6 +3,7 @@ import { z } from 'zod';
import { FeatureType } from './common';
import { featureCopilot } from './copilot';
import { featureEarlyAccess } from './early-access';
import { featureUnlimitedWorkspace } from './unlimited-workspace';
/// ======== common schema ========
@@ -41,6 +42,14 @@ export const Features: Feature[] = [
feature: FeatureType.EarlyAccess,
type: FeatureKind.Feature,
version: 2,
configs: {
whitelist: [],
},
},
{
feature: FeatureType.UnlimitedWorkspace,
type: FeatureKind.Feature,
version: 1,
configs: {},
},
];
@@ -51,7 +60,13 @@ export const FeatureSchema = commonFeatureSchema
.extend({
type: z.literal(FeatureKind.Feature),
})
.and(z.discriminatedUnion('feature', [featureCopilot, featureEarlyAccess]));
.and(
z.discriminatedUnion('feature', [
featureCopilot,
featureEarlyAccess,
featureUnlimitedWorkspace,
])
);
export type Feature = z.infer<typeof FeatureSchema>;

View File

@@ -0,0 +1,8 @@
import { z } from 'zod';
import { FeatureType } from './common';
export const featureUnlimitedWorkspace = z.object({
feature: z.literal(FeatureType.UnlimitedWorkspace),
configs: z.object({}),
});

View File

@@ -1,5 +1,6 @@
import { Module } from '@nestjs/common';
import { FeatureModule } from '../features';
import { StorageModule } from '../storage';
import { PermissionService } from '../workspaces/permission';
import { QuotaService } from './service';
@@ -12,12 +13,12 @@ import { QuotaManagementService } from './storage';
* - quota statistics
*/
@Module({
imports: [StorageModule],
imports: [FeatureModule, StorageModule],
providers: [PermissionService, QuotaService, QuotaManagementService],
exports: [QuotaService, QuotaManagementService],
})
export class QuotaModule {}
export { QuotaManagementService, QuotaService };
export { Quota_FreePlanV1, Quota_ProPlanV1, Quotas } from './schema';
export { QuotaType } from './types';
export { Quota_FreePlanV1_1, Quota_ProPlanV1, Quotas } from './schema';
export { QuotaQueryType, QuotaType } from './types';

View File

@@ -1,4 +1,4 @@
import { PrismaService } from '../../prisma';
import { PrismaService } from '../../fundamentals';
import { formatDate, formatSize, Quota, QuotaSchema } from './types';
const QuotaCache = new Map<number, QuotaConfig>();
@@ -44,6 +44,10 @@ export class QuotaConfig {
}
}
get version() {
return this.config.version;
}
/// feature name of quota
get name() {
return this.config.feature;
@@ -53,6 +57,12 @@ export class QuotaConfig {
return this.config.configs.blobLimit;
}
get businessBlobLimit() {
return (
this.config.configs.businessBlobLimit || this.config.configs.blobLimit
);
}
get storageQuota() {
return this.config.configs.storageQuota;
}

View File

@@ -0,0 +1,106 @@
import { FeatureKind } from '../features';
import { OneDay, OneGB, OneMB } from './constant';
import { Quota, QuotaType } from './types';
export const Quotas: Quota[] = [
{
feature: QuotaType.FreePlanV1,
type: FeatureKind.Quota,
version: 1,
configs: {
// quota name
name: 'Free',
// single blob limit 10MB
blobLimit: 10 * OneMB,
// total blob limit 10GB
storageQuota: 10 * OneGB,
// history period of validity 7 days
historyPeriod: 7 * OneDay,
// member limit 3
memberLimit: 3,
},
},
{
feature: QuotaType.ProPlanV1,
type: FeatureKind.Quota,
version: 1,
configs: {
// quota name
name: 'Pro',
// single blob limit 100MB
blobLimit: 100 * OneMB,
// total blob limit 100GB
storageQuota: 100 * OneGB,
// history period of validity 30 days
historyPeriod: 30 * OneDay,
// member limit 10
memberLimit: 10,
},
},
{
feature: QuotaType.RestrictedPlanV1,
type: FeatureKind.Quota,
version: 1,
configs: {
// quota name
name: 'Restricted',
// single blob limit 10MB
blobLimit: OneMB,
// total blob limit 1GB
storageQuota: 10 * OneMB,
// history period of validity 30 days
historyPeriod: 30 * OneDay,
// member limit 10
memberLimit: 10,
},
},
{
feature: QuotaType.FreePlanV1,
type: FeatureKind.Quota,
version: 2,
configs: {
// quota name
name: 'Free',
// single blob limit 10MB
blobLimit: 100 * OneMB,
// total blob limit 10GB
storageQuota: 10 * OneGB,
// history period of validity 7 days
historyPeriod: 7 * OneDay,
// member limit 3
memberLimit: 3,
},
},
{
feature: QuotaType.FreePlanV1,
type: FeatureKind.Quota,
version: 3,
configs: {
// quota name
name: 'Free',
// single blob limit 10MB
blobLimit: 10 * OneMB,
// server limit will larger then client to handle a edge case:
// when a user downgrades from pro to free, he can still continue
// to upload previously added files that exceed the free limit
// NOTE: this is a product decision, may change in future
businessBlobLimit: 100 * OneMB,
// total blob limit 10GB
storageQuota: 10 * OneGB,
// history period of validity 7 days
historyPeriod: 7 * OneDay,
// member limit 3
memberLimit: 3,
},
},
];
export const Quota_FreePlanV1_1 = {
feature: Quotas[4].feature,
version: Quotas[4].version,
};
export const Quota_ProPlanV1 = {
feature: Quotas[1].feature,
version: Quotas[1].version,
};

View File

@@ -1,10 +1,12 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma';
import { type EventPayload, OnEvent, PrismaService } from '../../fundamentals';
import { FeatureKind } from '../features';
import { QuotaConfig } from './quota';
import { QuotaType } from './types';
type Transaction = Parameters<Parameters<PrismaService['$transaction']>[0]>[0];
@Injectable()
export class QuotaService {
constructor(private readonly prisma: PrismaService) {}
@@ -83,6 +85,13 @@ export class QuotaService {
expiredAt?: Date
) {
await this.prisma.$transaction(async tx => {
const hasSameActivatedQuota = await this.hasQuota(userId, quota, tx);
if (hasSameActivatedQuota) {
// don't need to switch
return;
}
const latestPlanVersion = await tx.features.aggregate({
where: {
feature: quota,
@@ -130,8 +139,10 @@ export class QuotaService {
});
}
async hasQuota(userId: string, quota: QuotaType) {
return this.prisma.userFeatures
async hasQuota(userId: string, quota: QuotaType, transaction?: Transaction) {
const executor = transaction ?? this.prisma;
return executor.userFeatures
.count({
where: {
userId,
@@ -144,4 +155,26 @@ export class QuotaService {
})
.then(count => count > 0);
}
@OnEvent('user.subscription.activated')
async onSubscriptionUpdated({
userId,
}: EventPayload<'user.subscription.activated'>) {
await this.switchUserQuota(
userId,
QuotaType.ProPlanV1,
'subscription activated'
);
}
@OnEvent('user.subscription.canceled')
async onSubscriptionCanceled(
userId: EventPayload<'user.subscription.canceled'>
) {
await this.switchUserQuota(
userId,
QuotaType.FreePlanV1,
'subscription canceled'
);
}
}

View File

@@ -0,0 +1,112 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { FeatureService, FeatureType } from '../features';
import { WorkspaceBlobStorage } from '../storage';
import { PermissionService } from '../workspaces/permission';
import { OneGB } from './constant';
import { QuotaService } from './service';
import { formatSize, QuotaQueryType } from './types';
type QuotaBusinessType = QuotaQueryType & { businessBlobLimit: number };
@Injectable()
export class QuotaManagementService {
constructor(
private readonly feature: FeatureService,
private readonly quota: QuotaService,
private readonly permissions: PermissionService,
private readonly storage: WorkspaceBlobStorage
) {}
async getUserQuota(userId: string) {
const quota = await this.quota.getUserQuota(userId);
return {
name: quota.feature.name,
reason: quota.reason,
createAt: quota.createdAt,
expiredAt: quota.expiredAt,
blobLimit: quota.feature.blobLimit,
businessBlobLimit: quota.feature.businessBlobLimit,
storageQuota: quota.feature.storageQuota,
historyPeriod: quota.feature.historyPeriod,
memberLimit: quota.feature.memberLimit,
};
}
// TODO: lazy calc, need to be optimized with cache
async getUserUsage(userId: string) {
const workspaces = await this.permissions.getOwnedWorkspaces(userId);
const sizes = await Promise.all(
workspaces.map(workspace => this.storage.totalSize(workspace))
);
return sizes.reduce((total, size) => total + size, 0);
}
// get workspace's owner quota and total size of used
// quota was apply to owner's account
async getWorkspaceUsage(workspaceId: string): Promise<QuotaBusinessType> {
const { user: owner } =
await this.permissions.getWorkspaceOwner(workspaceId);
if (!owner) throw new NotFoundException('Workspace owner not found');
const {
feature: {
name,
blobLimit,
businessBlobLimit,
historyPeriod,
memberLimit,
storageQuota,
humanReadable,
},
} = await this.quota.getUserQuota(owner.id);
// get all workspaces size of owner used
const usedSize = await this.getUserUsage(owner.id);
const quota = {
name,
blobLimit,
businessBlobLimit,
historyPeriod,
memberLimit,
storageQuota,
humanReadable,
usedSize,
};
// relax restrictions if workspace has unlimited feature
// todo(@darkskygit): need a mechanism to allow feature as a middleware to edit quota
const unlimited = await this.feature.hasWorkspaceFeature(
workspaceId,
FeatureType.UnlimitedWorkspace
);
if (unlimited) {
return this.mergeUnlimitedQuota(quota);
}
return quota;
}
private mergeUnlimitedQuota(orig: QuotaBusinessType) {
return {
...orig,
storageQuota: 1000 * OneGB,
memberLimit: 1000,
humanReadable: {
...orig.humanReadable,
name: 'Unlimited',
storageQuota: formatSize(1000 * OneGB),
memberLimit: '1000',
},
};
}
async checkBlobQuota(workspaceId: string, size: number) {
const { storageQuota, usedSize } =
await this.getWorkspaceUsage(workspaceId);
return storageQuota - (size + usedSize);
}
}

View File

@@ -0,0 +1,110 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { SafeIntResolver } from 'graphql-scalars';
import { z } from 'zod';
import { commonFeatureSchema, FeatureKind } from '../features';
import { ByteUnit, OneDay, OneKB } from './constant';
/// ======== quota define ========
/**
* naming rule:
* we append Vx to the end of the feature name to indicate the version of the feature
* x is a number, start from 1, this number will be change only at the time we change the schema of config
* for example, we change the value of `blobLimit` from 10MB to 100MB, then we will only change `version` field from 1 to 2
* but if we remove the `blobLimit` field or rename it, then we will change the Vx to Vx+1
*/
export enum QuotaType {
FreePlanV1 = 'free_plan_v1',
ProPlanV1 = 'pro_plan_v1',
// only for test, smaller quota
RestrictedPlanV1 = 'restricted_plan_v1',
}
const quotaPlan = z.object({
feature: z.enum([
QuotaType.FreePlanV1,
QuotaType.ProPlanV1,
QuotaType.RestrictedPlanV1,
]),
configs: z.object({
name: z.string(),
blobLimit: z.number().positive().int(),
storageQuota: z.number().positive().int(),
historyPeriod: z.number().positive().int(),
memberLimit: z.number().positive().int(),
businessBlobLimit: z.number().positive().int().nullish(),
}),
});
/// ======== schema infer ========
export const QuotaSchema = commonFeatureSchema
.extend({
type: z.literal(FeatureKind.Quota),
})
.and(z.discriminatedUnion('feature', [quotaPlan]));
export type Quota = z.infer<typeof QuotaSchema>;
/// ======== query types ========
@ObjectType()
export class HumanReadableQuotaType {
@Field(() => String)
name!: string;
@Field(() => String)
blobLimit!: string;
@Field(() => String)
storageQuota!: string;
@Field(() => String)
historyPeriod!: string;
@Field(() => String)
memberLimit!: string;
}
@ObjectType()
export class QuotaQueryType {
@Field(() => String)
name!: string;
@Field(() => SafeIntResolver)
blobLimit!: number;
@Field(() => SafeIntResolver)
historyPeriod!: number;
@Field(() => SafeIntResolver)
memberLimit!: number;
@Field(() => SafeIntResolver)
storageQuota!: number;
@Field(() => HumanReadableQuotaType)
humanReadable!: HumanReadableQuotaType;
@Field(() => SafeIntResolver)
usedSize!: number;
}
/// ======== utils ========
export function formatSize(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 B';
const dm = decimals < 0 ? 0 : decimals;
const i = Math.floor(Math.log(bytes) / Math.log(OneKB));
return (
parseFloat((bytes / Math.pow(OneKB, i)).toFixed(dm)) + ' ' + ByteUnit[i]
);
}
export function formatDate(ms: number): string {
return `${(ms / OneDay).toFixed(0)} days`;
}

View File

@@ -1,18 +1,17 @@
import { Injectable } from '@nestjs/common';
import { AFFiNEStorageConfig, Config } from '../../../config';
import { type EventPayload, OnEvent } from '../../../event';
import {
import type {
BlobInputType,
createStorageProvider,
EventPayload,
PutObjectMetadata,
StorageProvider,
} from '../providers';
} from '../../../fundamentals';
import { Config, createStorageProvider, OnEvent } from '../../../fundamentals';
@Injectable()
export class AvatarStorage {
public readonly provider: StorageProvider;
private readonly storageConfig: AFFiNEStorageConfig['storages']['avatar'];
private readonly storageConfig: Config['storage']['storages']['avatar'];
constructor(private readonly config: Config) {
this.provider = createStorageProvider(this.config.storage, 'avatar');

View File

@@ -1,27 +1,21 @@
import { Readable } from 'node:stream';
import { Injectable } from '@nestjs/common';
import type { Storage } from '@affine/storage';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Config } from '../../../config';
import { EventEmitter, type EventPayload, OnEvent } from '../../../event';
import { OctoBaseStorageModule } from '../../../storage';
import {
import type {
BlobInputType,
createStorageProvider,
EventPayload,
StorageProvider,
} from '../providers';
import { toBuffer } from '../providers/utils';
} from '../../../fundamentals';
import {
Config,
createStorageProvider,
EventEmitter,
OnEvent,
} from '../../../fundamentals';
@Injectable()
export class WorkspaceBlobStorage implements OnModuleInit {
export class WorkspaceBlobStorage {
public readonly provider: StorageProvider;
/**
* @deprecated for backwards compatibility, need to be removed in next stable release
*/
private octobase: Storage | null = null;
constructor(
private readonly event: EventEmitter,
private readonly config: Config
@@ -29,42 +23,12 @@ export class WorkspaceBlobStorage implements OnModuleInit {
this.provider = createStorageProvider(this.config.storage, 'blob');
}
async onModuleInit() {
if (!this.config.node.test) {
this.octobase = await OctoBaseStorageModule.Storage.connect(
this.config.db.url
);
}
}
async put(workspaceId: string, key: string, blob: BlobInputType) {
const buf = await toBuffer(blob);
await this.provider.put(`${workspaceId}/${key}`, buf);
if (this.octobase) {
await this.octobase.uploadBlob(workspaceId, buf);
}
await this.provider.put(`${workspaceId}/${key}`, blob);
}
async get(workspaceId: string, key: string) {
const result = await this.provider.get(`${workspaceId}/${key}`);
if (!result.body && this.octobase) {
const blob = await this.octobase.getBlob(workspaceId, key);
if (!blob) {
return result;
}
return {
body: Readable.from(blob.data),
metadata: {
contentType: blob.contentType,
contentLength: blob.size,
lastModified: new Date(blob.lastModified),
},
};
}
return result;
return this.provider.get(`${workspaceId}/${key}`);
}
async list(workspaceId: string) {

View File

@@ -11,12 +11,11 @@ import {
import { Server, Socket } from 'socket.io';
import { encodeStateAsUpdate, encodeStateVector } from 'yjs';
import { metrics } from '../../../metrics';
import { CallTimer } from '../../../metrics/utils';
import { DocID } from '../../../utils/doc';
import { CallTimer, metrics } from '../../../fundamentals';
import { Auth, CurrentUser } from '../../auth';
import { DocManager } from '../../doc';
import { UserType } from '../../users';
import { DocID } from '../../utils/doc';
import { PermissionService } from '../../workspaces/permission';
import { Permission } from '../../workspaces/types';
import {

View File

@@ -5,7 +5,7 @@ import {
} from '@nestjs/common';
import { Args, Context, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CloudThrottlerGuard, Throttle } from '../../throttler';
import { CloudThrottlerGuard, Throttle } from '../../fundamentals';
import { Auth, CurrentUser } from '../auth/guard';
import { AuthService } from '../auth/service';
import { FeatureManagementService } from '../features';

View File

@@ -11,10 +11,13 @@ import type { User } from '@prisma/client';
import { GraphQLError } from 'graphql';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { EventEmitter } from '../../event';
import { PrismaService } from '../../prisma/service';
import { CloudThrottlerGuard, Throttle } from '../../throttler';
import type { FileUpload } from '../../types';
import {
CloudThrottlerGuard,
EventEmitter,
type FileUpload,
PrismaService,
Throttle,
} from '../../fundamentals';
import { Auth, CurrentUser, Public, Publicable } from '../auth/guard';
import { FeatureManagementService } from '../features';
import { QuotaService } from '../quota';
@@ -109,6 +112,9 @@ export class UserResolver {
const user = await this.users.findUserByEmail(email);
if (currentUser) return user;
// return empty response when user not exists
if (!user) return null;
// only return limited info when not logged in
return {
email: user?.email,

View File

@@ -1,5 +1,6 @@
import { createUnionType, Field, Float, ID, ObjectType } from '@nestjs/graphql';
import { createUnionType, Field, ID, ObjectType } from '@nestjs/graphql';
import type { User } from '@prisma/client';
import { SafeIntResolver } from 'graphql-scalars';
@ObjectType('UserQuotaHumanReadable')
export class UserQuotaHumanReadableType {
@@ -24,13 +25,13 @@ export class UserQuotaType {
@Field({ name: 'name' })
name!: string;
@Field(() => Float, { name: 'blobLimit' })
@Field(() => SafeIntResolver, { name: 'blobLimit' })
blobLimit!: number;
@Field(() => Float, { name: 'storageQuota' })
@Field(() => SafeIntResolver, { name: 'storageQuota' })
storageQuota!: number;
@Field(() => Float, { name: 'historyPeriod' })
@Field(() => SafeIntResolver, { name: 'historyPeriod' })
historyPeriod!: number;
@Field({ name: 'memberLimit' })

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma';
import { PrismaService } from '../../fundamentals';
@Injectable()
export class UsersService {

View File

@@ -9,13 +9,12 @@ import {
} from '@nestjs/common';
import type { Response } from 'express';
import { CallTimer } from '../../metrics';
import { PrismaService } from '../../prisma';
import { DocID } from '../../utils/doc';
import { CallTimer, PrismaService } from '../../fundamentals';
import { Auth, CurrentUser, Publicable } from '../auth';
import { DocHistoryManager, DocManager } from '../doc';
import { WorkspaceBlobStorage } from '../storage';
import { UserType } from '../users';
import { DocID } from '../utils/doc';
import { PermissionService, PublicPageMode } from './permission';
import { Permission } from './types';

View File

@@ -9,7 +9,7 @@ import {
Resolver,
} from '@nestjs/graphql';
import { CloudThrottlerGuard, Throttle } from '../../throttler';
import { CloudThrottlerGuard, Throttle } from '../../fundamentals';
import { Auth, CurrentUser } from '../auth';
import { FeatureManagementService, FeatureType } from '../features';
import { UserType } from '../users';
@@ -119,7 +119,8 @@ export class WorkspaceManagementResolver {
async availableFeatures(
@CurrentUser() user: UserType
): Promise<FeatureType[]> {
if (await this.feature.canEarlyAccess(user.email)) {
const isEarlyAccessUser = await this.feature.isEarlyAccessUser(user.email);
if (isEarlyAccessUser) {
return [FeatureType.Copilot];
} else {
return [];

View File

@@ -1,7 +1,7 @@
import { ForbiddenException, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../../prisma';
import { PrismaService } from '../../fundamentals';
import { Permission } from './types';
export enum PublicPageMode {

View File

@@ -1,7 +1,6 @@
import { ForbiddenException, Logger, UseGuards } from '@nestjs/common';
import { HttpStatus, Logger, UseGuards } from '@nestjs/common';
import {
Args,
Float,
Int,
Mutation,
Parent,
@@ -9,12 +8,18 @@ import {
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { GraphQLError } from 'graphql';
import { SafeIntResolver } from 'graphql-scalars';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { MakeCache, PreventCache } from '../../../cache';
import { CloudThrottlerGuard } from '../../../throttler';
import type { FileUpload } from '../../../types';
import {
CloudThrottlerGuard,
type FileUpload,
MakeCache,
PreventCache,
} from '../../../fundamentals';
import { Auth, CurrentUser } from '../../auth';
import { FeatureManagementService, FeatureType } from '../../features';
import { QuotaManagementService } from '../../quota';
import { WorkspaceBlobStorage } from '../../storage';
import { UserType } from '../../users';
@@ -28,10 +33,26 @@ export class WorkspaceBlobResolver {
logger = new Logger(WorkspaceBlobResolver.name);
constructor(
private readonly permissions: PermissionService,
private readonly feature: FeatureManagementService,
private readonly quota: QuotaManagementService,
private readonly storage: WorkspaceBlobStorage
) {}
@ResolveField(() => [String], {
description: 'List blobs of workspace',
complexity: 2,
})
async blobs(
@CurrentUser() user: UserType,
@Parent() workspace: WorkspaceType
) {
await this.permissions.checkWorkspace(workspace.id, user.id);
return this.storage
.list(workspace.id)
.then(list => list.map(item => item.key));
}
@ResolveField(() => Int, {
description: 'Blobs size of workspace',
complexity: 2,
@@ -79,7 +100,7 @@ export class WorkspaceBlobResolver {
async checkBlobSize(
@CurrentUser() user: UserType,
@Args('workspaceId') workspaceId: string,
@Args('size', { type: () => Float }) blobSize: number
@Args('size', { type: () => SafeIntResolver }) blobSize: number
) {
const canWrite = await this.permissions.tryCheckWorkspace(
workspaceId,
@@ -107,15 +128,33 @@ export class WorkspaceBlobResolver {
Permission.Write
);
const { quota, size } = await this.quota.getWorkspaceUsage(workspaceId);
const { storageQuota, usedSize, businessBlobLimit } =
await this.quota.getWorkspaceUsage(workspaceId);
const unlimited = await this.feature.hasWorkspaceFeature(
workspaceId,
FeatureType.UnlimitedWorkspace
);
const checkExceeded = (recvSize: number) => {
if (!quota) {
throw new ForbiddenException('cannot find user quota');
if (!storageQuota) {
throw new GraphQLError('cannot find user quota', {
extensions: {
status: HttpStatus[HttpStatus.FORBIDDEN],
code: HttpStatus.FORBIDDEN,
},
});
}
if (size + recvSize > quota) {
const total = usedSize + recvSize;
// only skip total storage check if workspace has unlimited feature
if (total > storageQuota && !unlimited) {
this.logger.log(
`storage size limit exceeded: ${size + recvSize} > ${quota}`
`storage size limit exceeded: ${total} > ${storageQuota}`
);
return true;
} else if (recvSize > businessBlobLimit) {
this.logger.log(
`blob size limit exceeded: ${recvSize} > ${businessBlobLimit}`
);
return true;
} else {
@@ -124,7 +163,12 @@ export class WorkspaceBlobResolver {
};
if (checkExceeded(0)) {
throw new ForbiddenException('storage size limit exceeded');
throw new GraphQLError('storage or blob size limit exceeded', {
extensions: {
status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE],
code: HttpStatus.PAYLOAD_TOO_LARGE,
},
});
}
const buffer = await new Promise<Buffer>((resolve, reject) => {
const stream = blob.createReadStream();
@@ -135,7 +179,14 @@ export class WorkspaceBlobResolver {
// check size after receive each chunk to avoid unnecessary memory usage
const bufferSize = chunks.reduce((acc, cur) => acc + cur.length, 0);
if (checkExceeded(bufferSize)) {
reject(new ForbiddenException('storage size limit exceeded'));
reject(
new GraphQLError('storage or blob size limit exceeded', {
extensions: {
status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE],
code: HttpStatus.PAYLOAD_TOO_LARGE,
},
})
);
}
});
stream.on('error', reject);
@@ -143,17 +194,20 @@ export class WorkspaceBlobResolver {
const buffer = Buffer.concat(chunks);
if (checkExceeded(buffer.length)) {
reject(new ForbiddenException('storage size limit exceeded'));
reject(
new GraphQLError('storage limit exceeded', {
extensions: {
status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE],
code: HttpStatus.PAYLOAD_TOO_LARGE,
},
})
);
} else {
resolve(buffer);
}
});
});
if (!(await this.quota.checkBlobQuota(workspaceId, buffer.length))) {
throw new ForbiddenException('blob size limit exceeded');
}
await this.storage.put(workspaceId, blob.filename, buffer);
return blob.filename;
}

View File

@@ -12,11 +12,11 @@ import {
} from '@nestjs/graphql';
import type { SnapshotHistory } from '@prisma/client';
import { CloudThrottlerGuard } from '../../../throttler';
import { DocID } from '../../../utils/doc';
import { CloudThrottlerGuard } from '../../../fundamentals';
import { Auth, CurrentUser } from '../../auth';
import { DocHistoryManager } from '../../doc/history';
import { DocHistoryManager } from '../../doc';
import { UserType } from '../../users';
import { DocID } from '../../utils/doc';
import { PermissionService } from '../permission';
import { Permission, WorkspaceType } from '../types';

View File

@@ -11,11 +11,10 @@ import {
} from '@nestjs/graphql';
import type { WorkspacePage as PrismaWorkspacePage } from '@prisma/client';
import { PrismaService } from '../../../prisma';
import { CloudThrottlerGuard } from '../../../throttler';
import { DocID } from '../../../utils/doc';
import { CloudThrottlerGuard, PrismaService } from '../../../fundamentals';
import { Auth, CurrentUser } from '../../auth';
import { UserType } from '../../users';
import { DocID } from '../../utils/doc';
import { PermissionService, PublicPageMode } from '../permission';
import { Permission, WorkspaceType } from '../types';

View File

@@ -1,6 +1,6 @@
import {
ForbiddenException,
InternalServerErrorException,
HttpStatus,
Logger,
NotFoundException,
UseGuards,
@@ -16,16 +16,21 @@ import {
} from '@nestjs/graphql';
import type { User } from '@prisma/client';
import { getStreamAsBuffer } from 'get-stream';
import { GraphQLError } from 'graphql';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { applyUpdate, Doc } from 'yjs';
import { EventEmitter } from '../../../event';
import { PrismaService } from '../../../prisma';
import { CloudThrottlerGuard, Throttle } from '../../../throttler';
import type { FileUpload } from '../../../types';
import {
CloudThrottlerGuard,
EventEmitter,
type FileUpload,
MailService,
PrismaService,
Throttle,
} from '../../../fundamentals';
import { Auth, CurrentUser, Public } from '../../auth';
import { MailService } from '../../auth/mailer';
import { AuthService } from '../../auth/service';
import { QuotaManagementService, QuotaQueryType } from '../../quota';
import { WorkspaceBlobStorage } from '../../storage';
import { UsersService, UserType } from '../../users';
import { PermissionService } from '../permission';
@@ -54,6 +59,7 @@ export class WorkspaceResolver {
private readonly mailer: MailService,
private readonly prisma: PrismaService,
private readonly permissions: PermissionService,
private readonly quota: QuotaManagementService,
private readonly users: UsersService,
private readonly event: EventEmitter,
private readonly blobStorage: WorkspaceBlobStorage
@@ -141,6 +147,15 @@ export class WorkspaceResolver {
}));
}
@ResolveField(() => QuotaQueryType, {
name: 'quota',
description: 'quota of workspace',
complexity: 2,
})
workspaceQuota(@Parent() workspace: WorkspaceType) {
return this.quota.getWorkspaceUsage(workspace.id);
}
@Query(() => Boolean, {
description: 'Get is owner of workspace',
complexity: 2,
@@ -321,8 +336,23 @@ export class WorkspaceResolver {
throw new ForbiddenException('Cannot change owner');
}
const target = await this.users.findUserByEmail(email);
// member limit check
const [memberCount, quota] = await Promise.all([
this.prisma.workspaceUserPermission.count({
where: { workspaceId },
}),
this.quota.getWorkspaceUsage(workspaceId),
]);
if (memberCount >= quota.memberLimit) {
throw new GraphQLError('Workspace member limit reached', {
extensions: {
status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE],
code: HttpStatus.PAYLOAD_TOO_LARGE,
},
});
}
let target = await this.users.findUserByEmail(email);
if (target) {
const originRecord = await this.prisma.workspaceUserPermission.findFirst({
where: {
@@ -330,94 +360,59 @@ export class WorkspaceResolver {
userId: target.id,
},
});
if (originRecord) {
return originRecord.id;
}
const inviteId = await this.permissions.grant(
workspaceId,
target.id,
permission
);
if (sendInviteMail) {
const inviteInfo = await this.getInviteInfo(inviteId);
try {
await this.mailer.sendInviteEmail(email, inviteId, {
workspace: {
id: inviteInfo.workspace.id,
name: inviteInfo.workspace.name,
avatar: inviteInfo.workspace.avatar,
},
user: {
avatar: inviteInfo.user?.avatarUrl || '',
name: inviteInfo.user?.name || '',
},
});
} catch (e) {
const ret = await this.permissions.revokeWorkspace(
workspaceId,
target.id
);
if (!ret) {
this.logger.fatal(
`failed to send ${workspaceId} invite email to ${email} and failed to revoke permission: ${inviteId}, ${e}`
);
} else {
this.logger.warn(
`failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}`
);
}
return new InternalServerErrorException(e);
}
}
return inviteId;
// only invite if the user is not already in the workspace
if (originRecord) return originRecord.id;
} else {
const user = await this.auth.createAnonymousUser(email);
const inviteId = await this.permissions.grant(
workspaceId,
user.id,
permission
);
if (sendInviteMail) {
const inviteInfo = await this.getInviteInfo(inviteId);
try {
await this.mailer.sendInviteEmail(email, inviteId, {
workspace: {
id: inviteInfo.workspace.id,
name: inviteInfo.workspace.name,
avatar: inviteInfo.workspace.avatar,
},
user: {
avatar: inviteInfo.user?.avatarUrl || '',
name: inviteInfo.user?.name || '',
},
});
} catch (e) {
const ret = await this.permissions.revokeWorkspace(
workspaceId,
user.id
);
if (!ret) {
this.logger.fatal(
`failed to send ${workspaceId} invite email to ${email} and failed to revoke permission: ${inviteId}, ${e}`
);
} else {
this.logger.warn(
`failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}`
);
}
return new InternalServerErrorException(e);
}
}
return inviteId;
target = await this.auth.createAnonymousUser(email);
}
const inviteId = await this.permissions.grant(
workspaceId,
target.id,
permission
);
if (sendInviteMail) {
const inviteInfo = await this.getInviteInfo(inviteId);
try {
await this.mailer.sendInviteEmail(email, inviteId, {
workspace: {
id: inviteInfo.workspace.id,
name: inviteInfo.workspace.name,
avatar: inviteInfo.workspace.avatar,
},
user: {
avatar: inviteInfo.user?.avatarUrl || '',
name: inviteInfo.user?.name || '',
},
});
} catch (e) {
const ret = await this.permissions.revokeWorkspace(
workspaceId,
target.id
);
if (!ret) {
this.logger.fatal(
`failed to send ${workspaceId} invite email to ${email} and failed to revoke permission: ${inviteId}, ${e}`
);
} else {
this.logger.warn(
`failed to send ${workspaceId} invite email to ${email}, but successfully revoked permission: ${e}`
);
}
return new GraphQLError(
'failed to send invite email, please try again',
{
extensions: {
status: HttpStatus[HttpStatus.INTERNAL_SERVER_ERROR],
code: HttpStatus.INTERNAL_SERVER_ERROR,
},
}
);
}
}
return inviteId;
}
@Throttle({

View File

@@ -1,6 +1,5 @@
import {
Field,
Float,
ID,
InputType,
ObjectType,
@@ -10,6 +9,7 @@ import {
registerEnumType,
} from '@nestjs/graphql';
import type { Workspace } from '@prisma/client';
import { SafeIntResolver } from 'graphql-scalars';
import { UserType } from '../users/types';
@@ -78,7 +78,7 @@ export class InvitationWorkspaceType {
@ObjectType()
export class WorkspaceBlobSizes {
@Field(() => Float)
@Field(() => SafeIntResolver)
size!: number;
}

View File

@@ -1,8 +1,7 @@
import { Logger, Module } from '@nestjs/common';
import { CommandFactory } from 'nest-commander';
import { Module } from '@nestjs/common';
import { AppModule as BusinessAppModule } from '../app';
import { ConfigModule } from '../config';
import { AppModule as BusinessAppModule } from '../app.module';
import { ConfigModule } from '../fundamentals/config';
import { CreateCommand, NameQuestion } from './commands/create';
import { RevertCommand, RunCommand } from './commands/run';
@@ -14,19 +13,12 @@ import { RevertCommand, RunCommand } from './commands/run';
enableUpdateAutoMerging: false,
},
},
metrics: {
enabled: false,
},
}),
BusinessAppModule,
],
providers: [NameQuestion, CreateCommand, RunCommand, RevertCommand],
})
class AppModule {}
async function bootstrap() {
await CommandFactory.run(AppModule, new Logger()).catch(e => {
console.error(e);
process.exit(1);
});
process.exit(0);
}
await bootstrap();
export class CliAppModule {}

View File

@@ -59,13 +59,13 @@ export class CreateCommand extends CommandRunner {
}
private createScript(name: string) {
const contents = ["import { PrismaService } from '../../prisma';", ''];
const contents = ["import { PrismaClient } from '@prisma/client';", ''];
contents.push(`export class ${name} {`);
contents.push(' // do the migration');
contents.push(' static async up(db: PrismaService) {}');
contents.push(' static async up(db: PrismaClient) {}');
contents.push('');
contents.push(' // revert the migration');
contents.push(' static async down(db: PrismaService) {}');
contents.push(' static async down(db: PrismaClient) {}');
contents.push('}');

View File

@@ -4,15 +4,14 @@ import { fileURLToPath } from 'node:url';
import { Logger } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { PrismaClient } from '@prisma/client';
import { Command, CommandRunner } from 'nest-commander';
import { PrismaService } from '../../prisma';
interface Migration {
file: string;
name: string;
up: (db: PrismaService, injector: ModuleRef) => Promise<void>;
down: (db: PrismaService, injector: ModuleRef) => Promise<void>;
up: (db: PrismaClient, injector: ModuleRef) => Promise<void>;
down: (db: PrismaClient, injector: ModuleRef) => Promise<void>;
}
export async function collectMigrations(): Promise<Migration[]> {
@@ -48,7 +47,7 @@ export async function collectMigrations(): Promise<Migration[]> {
export class RunCommand extends CommandRunner {
logger = new Logger(RunCommand.name);
constructor(
private readonly db: PrismaService,
private readonly db: PrismaClient,
private readonly injector: ModuleRef
) {
super();
@@ -139,7 +138,7 @@ export class RevertCommand extends CommandRunner {
logger = new Logger(RevertCommand.name);
constructor(
private readonly db: PrismaService,
private readonly db: PrismaClient,
private readonly injector: ModuleRef
) {
super();

View File

@@ -0,0 +1,16 @@
import '../prelude';
import { Logger } from '@nestjs/common';
import { CommandFactory } from 'nest-commander';
import { CliAppModule } from './app';
async function bootstrap() {
await CommandFactory.run(CliAppModule, new Logger()).catch(e => {
console.error(e);
process.exit(1);
});
process.exit(0);
}
await bootstrap();

View File

@@ -0,0 +1,39 @@
import { ModuleRef } from '@nestjs/core';
import { hash } from '@node-rs/argon2';
import { PrismaClient } from '@prisma/client';
import { Config } from '../../fundamentals';
export class SelfHostAdmin1605053000403 {
// do the migration
static async up(db: PrismaClient, ref: ModuleRef) {
const config = ref.get(Config, { strict: false });
if (config.flavor.selfhosted) {
if (
!process.env.AFFINE_ADMIN_EMAIL ||
!process.env.AFFINE_ADMIN_PASSWORD
) {
throw new Error(
'You have to set AFFINE_ADMIN_EMAIL and AFFINE_ADMIN_PASSWORD environment variables to generate the initial user for self-hosted AFFiNE Server.'
);
}
await db.user.create({
data: {
name: 'AFFINE First User',
email: process.env.AFFINE_ADMIN_EMAIL,
emailVerified: new Date(),
password: await hash(process.env.AFFINE_ADMIN_PASSWORD),
},
});
}
}
// revert the migration
static async down(db: PrismaClient) {
await db.user.deleteMany({
where: {
email: process.env.AFFINE_ADMIN_EMAIL ?? 'admin@example.com',
},
});
}
}

View File

@@ -1,11 +1,11 @@
import { PrismaClient } from '@prisma/client';
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs';
import { PrismaService } from '../../prisma';
import { DocID } from '../../utils/doc';
import { DocID } from '../../core/utils/doc';
export class Guid1698398506533 {
// do the migration
static async up(db: PrismaService) {
static async up(db: PrismaClient) {
let turn = 0;
let lastTurnCount = 100;
while (lastTurnCount === 100) {

View File

@@ -1,11 +1,12 @@
import { Features } from '../../modules/features';
import { Quotas } from '../../modules/quota/schema';
import { PrismaService } from '../../prisma';
import { PrismaClient } from '@prisma/client';
import { Features } from '../../core/features';
import { Quotas } from '../../core/quota/schema';
import { migrateNewFeatureTable, upsertFeature } from './utils/user-features';
export class UserFeaturesInit1698652531198 {
// do the migration
static async up(db: PrismaService) {
static async up(db: PrismaClient) {
// upgrade features from lower version to higher version
for (const feature of Features) {
await upsertFeature(db, feature);
@@ -18,7 +19,7 @@ export class UserFeaturesInit1698652531198 {
}
// revert the migration
static async down(_db: PrismaService) {
static async down(_db: PrismaClient) {
// TODO: revert the migration
}
}

View File

@@ -1,8 +1,7 @@
import { PrismaService } from '../../prisma';
import { PrismaClient } from '@prisma/client';
export class PagePermission1699005339766 {
// do the migration
static async up(db: PrismaService) {
static async up(db: PrismaClient) {
let turn = 0;
let lastTurnCount = 50;
const done = new Set<string>();
@@ -88,7 +87,7 @@ export class PagePermission1699005339766 {
}
// revert the migration
static async down(db: PrismaService) {
static async down(db: PrismaClient) {
await db.workspaceUserPermission.deleteMany({});
await db.workspacePageUserPermission.deleteMany({});
}

View File

@@ -1,9 +1,9 @@
import { QuotaType } from '../../modules/quota/types';
import { PrismaService } from '../../prisma';
import { PrismaClient } from '@prisma/client';
import { QuotaType } from '../../core/quota/types';
export class OldUserFeature1702620653283 {
// do the migration
static async up(db: PrismaService) {
static async up(db: PrismaClient) {
await db.$transaction(async tx => {
const latestFreePlan = await tx.features.findFirstOrThrow({
where: { feature: QuotaType.FreePlanV1 },
@@ -16,7 +16,6 @@ export class OldUserFeature1702620653283 {
where: { NOT: { features: { some: { NOT: { id: { gt: 0 } } } } } },
select: { id: true },
});
console.log(`migrating ${userIds.join('|')} users`);
await tx.userFeatures.createMany({
data: userIds.map(({ id: userId }) => ({
@@ -31,7 +30,7 @@ export class OldUserFeature1702620653283 {
// revert the migration
// WARN: this will drop all user features
static async down(db: PrismaService) {
static async down(db: PrismaClient) {
await db.userFeatures.deleteMany({});
}
}

View File

@@ -1,13 +1,12 @@
import type { UserType } from '../../modules/users';
import { PrismaService } from '../../prisma';
import { PrismaClient, type User } from '@prisma/client';
export class UnamedAccount1703756315970 {
// do the migration
static async up(db: PrismaService) {
static async up(db: PrismaClient) {
await db.$transaction(async tx => {
// only find users with empty names
const users = await db.$queryRaw<
UserType[]
User[]
>`SELECT * FROM users WHERE name ~ E'^[\\s\\u2000-\\u200F]*$';`;
console.log(
`renaming ${users.map(({ email }) => email).join('|')} users`
@@ -27,5 +26,5 @@ export class UnamedAccount1703756315970 {
}
// revert the migration
static async down(_db: PrismaService) {}
static async down(_db: PrismaClient) {}
}

View File

@@ -1,11 +1,11 @@
import { ModuleRef } from '@nestjs/core';
import { PrismaClient } from '@prisma/client';
import { WorkspaceBlobStorage } from '../../modules/storage';
import { PrismaService } from '../../prisma';
import { WorkspaceBlobStorage } from '../../core/storage';
export class WorkspaceBlobs1703828796699 {
// do the migration
static async up(db: PrismaService, injector: ModuleRef) {
static async up(db: PrismaClient, injector: ModuleRef) {
const blobStorage = injector.get(WorkspaceBlobStorage, { strict: false });
let hasMore = true;
let turn = 0;
@@ -32,7 +32,7 @@ export class WorkspaceBlobs1703828796699 {
}
// revert the migration
static async down(_db: PrismaService) {
static async down(_db: PrismaClient) {
// old data kept, no need to downgrade the migration
}
}

View File

@@ -1,10 +1,11 @@
import { Features } from '../../modules/features';
import { PrismaService } from '../../prisma';
import { PrismaClient } from '@prisma/client';
import { Features } from '../../core/features';
import { upsertFeature } from './utils/user-features';
export class RefreshUserFeatures1704352562369 {
// do the migration
static async up(db: PrismaService) {
static async up(db: PrismaClient) {
// add early access v2 & copilot feature
for (const feature of Features) {
await upsertFeature(db, feature);
@@ -12,5 +13,5 @@ export class RefreshUserFeatures1704352562369 {
}
// revert the migration
static async down(_db: PrismaService) {}
static async down(_db: PrismaClient) {}
}

Some files were not shown because too many files have changed in this diff Show More