Compare commits

..

79 Commits

Author SHA1 Message Date
Whitewater
1ceaf87a86 chore: skip sync when offline (#5786) 2024-02-19 10:22:22 +08:00
Peng Xiao
d1c4e6141a refactor(component): cmdk ordering (#5722)
Replace internal CMDK command filtering/sorting logic.
The new implementation includes the following for command scoring:
- categories weights
- highlighted fragments
- original command score value

The new logic should be much cleaner and remove some hacks in the original implementation.

Not sure if this is optimal yet. Could be changed later.

fix https://github.com/toeverything/AFFiNE/issues/5699
2024-02-16 13:20:24 +00:00
LongYinan
9e7eb5629c chore: bump up eslint-plugin-simple-import-sort version to v12 (#5815)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

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

---

### Release Notes

<details>
<summary>lydell/eslint-plugin-simple-import-sort (eslint-plugin-simple-import-sort)</summary>

### [`v12.0.0`](https://togithub.com/lydell/eslint-plugin-simple-import-sort/blob/HEAD/CHANGELOG.md#Version-1200-2024-02-10)

[Compare Source](https://togithub.com/lydell/eslint-plugin-simple-import-sort/compare/v11.0.0...v12.0.0)

This release removes the support for import assignments added in version 11.0.0:

-   Turns out it was broken in some cases.
-   The suggested fix went past my complexity tolerance for such an esoteric feature.
-   I also learned that they aren’t really imports, and that I don’t understand their semantics well enough to know how sorting them affects your program.

If you miss the support for import assignments, I suggest you write your own ESLint rule which moves them out of the way from the actual imports, sorting them or not.

### [`v11.0.0`](https://togithub.com/lydell/eslint-plugin-simple-import-sort/blob/HEAD/CHANGELOG.md#Version-1100-2024-02-08)

[Compare Source](https://togithub.com/lydell/eslint-plugin-simple-import-sort/compare/v10.0.0...v11.0.0)

This release adds support for TypeScript import assignments (`import A = B.C` and `import A = require("module")`). Thanks to Szabolcs Kurdi ([@&#8203;szku01](https://togithub.com/szku01)) and Svyatoslav Zaytsev ([@&#8203;MillerSvt](https://togithub.com/MillerSvt))!

It’s only a breaking change if you use TypeScript import assignments, and only in the form that you need to autofix your files.

In other news, this release adds the `meta` plugin property in preparation for ESLint Flat Config, and avoids the deprecated `context.getSourceCode()` method (while still being backwards compatible).

</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:eyJjcmVhdGVkSW5WZXIiOiIzNy4xNzMuMCIsInVwZGF0ZWRJblZlciI6IjM3LjE3My4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-02-12 17:12:47 +00:00
Ayush Agrawal
5dc7cd336f feat: bump blocksuite (#5817) 2024-02-11 10:23:30 +08:00
Flrande
2e3ffeced9 feat: bump blocksuite (#5812) 2024-02-08 08:18:03 +08:00
LongYinan
a84a91d896 chore: bump up eslint-plugin-unicorn version to v51 (#5810)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [eslint-plugin-unicorn](https://togithub.com/sindresorhus/eslint-plugin-unicorn) | [`^50.0.0` -> `^51.0.0`](https://renovatebot.com/diffs/npm/eslint-plugin-unicorn/50.0.0/51.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-unicorn/51.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/eslint-plugin-unicorn/51.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/eslint-plugin-unicorn/50.0.0/51.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-unicorn/50.0.0/51.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>sindresorhus/eslint-plugin-unicorn (eslint-plugin-unicorn)</summary>

### [`v51.0.0`](https://togithub.com/sindresorhus/eslint-plugin-unicorn/releases/tag/v51.0.0)

[Compare Source](https://togithub.com/sindresorhus/eslint-plugin-unicorn/compare/v50.0.1...v51.0.0)

##### Breaking

-   `consistent-destructuring`: Remove from `recommended` preset ([#&#8203;2260](https://togithub.com/sindresorhus/eslint-plugin-unicorn/issues/2260))  [`702d51b`](https://togithub.com/sindresorhus/eslint-plugin-unicorn/commit/702d51b)

##### Improvements

-   `no-array-method-this-argument`: Check `Array.from()` ([#&#8203;2262](https://togithub.com/sindresorhus/eslint-plugin-unicorn/issues/2262))  [`797caee`](https://togithub.com/sindresorhus/eslint-plugin-unicorn/commit/797caee)

##### Fixes

-   `no-thenable`: Fix crash on `{[Symbol.prototype]: 0}` ([#&#8203;2248](https://togithub.com/sindresorhus/eslint-plugin-unicorn/issues/2248))  [`3c7d7c0`](https://togithub.com/sindresorhus/eslint-plugin-unicorn/commit/3c7d7c0)
-   `prefer-prototype-methods`: Fix argument of `isMethodCall` ([#&#8203;2247](https://togithub.com/sindresorhus/eslint-plugin-unicorn/issues/2247))  [`3b504fa`](https://togithub.com/sindresorhus/eslint-plugin-unicorn/commit/3b504fa)

### [`v50.0.1`](https://togithub.com/sindresorhus/eslint-plugin-unicorn/releases/tag/v50.0.1)

[Compare Source](https://togithub.com/sindresorhus/eslint-plugin-unicorn/compare/v50.0.0...v50.0.1)

##### Fixes

-   `no-unnecessary-polyfills`: Fix missing dependency error ([#&#8203;2242](https://togithub.com/sindresorhus/eslint-plugin-unicorn/issues/2242))  [`3df1606`](https://togithub.com/sindresorhus/eslint-plugin-unicorn/commit/3df16068)

</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:eyJjcmVhdGVkSW5WZXIiOiIzNy4xNzMuMCIsInVwZGF0ZWRJblZlciI6IjM3LjE3My4wIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-02-06 09:42:03 +00:00
Peng Xiao
005c02f148 fix(core): flaky tests (#5804) 2024-02-06 03:20:54 +00:00
Peng Xiao
afccf3d8c9 feat(core): add an option to throttle cpu in e2e (#5803)
To debug e2e flakyness locally
2024-02-06 03:20:49 +00:00
liuyi
296d47f102 refactor(server): separate s3 & r2 storage to plugin (#5805) 2024-02-05 15:10:09 +00:00
DarkSky
25e8a2a22f feat: sync client versioning (#5645)
after this pr, server will only accept client that have some major version
the client version <0.12 will be rejected by the server, >= 0.12 can receive outdated messages and notify users
2024-02-05 08:43:50 +00:00
Peng Xiao
5ca0d65241 test(core): config e2e output dir (#5783) 2024-02-05 04:10:17 +00:00
LongYinan
51680da33b fix(core): prevent data loss (hot-fix) (#5798) (#5800) 2024-02-05 03:53:32 +00:00
liuyi
d9c2dc8dfb fix(server): apply env overrides after all config merged (#5795) 2024-02-04 06:38:31 +00:00
LongYinan
899528dcfd fix(core): load fonts from selfhost url (#5789) 2024-02-02 14:41:06 +00:00
HeJiachen-PM
2ed30a0c2e docs: change community url on README (#5788)
changed community to discord
2024-02-02 10:31:15 +00:00
Joooye_34
514e6f6b80 fix: static resource not found in web server (#5787) 2024-02-02 09:11:31 +00:00
liuyi
bef266ae3b refactor(server): reorganize server configs (#5753) 2024-02-02 08:32:07 +00:00
LongYinan
ee3d195811 chore: bump up jotai-effect version to ^0.5.0 (#5781)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

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

---

### Release Notes

<details>
<summary>jotaijs/jotai-effect (jotai-effect)</summary>

### [`v0.5.0`](https://togithub.com/jotaijs/jotai-effect/releases/tag/v0.5.0)

[Compare Source](https://togithub.com/jotaijs/jotai-effect/compare/v0.4.0...v0.5.0)

Adds support for reading an atom's value without subscribing to it.

Read atom data without subscribing to changes with `get.peek`.

```ts
const countAtom = atom(0)
atomEffect((get, set) => {
  // will not rerun when countAtom changes
  const count = get.peek(countAtom)
})
```

#### What's Changed

-   feat: get.peek reads atom value without subscribing to updates by [@&#8203;dmaskasky](https://togithub.com/dmaskasky) in [https://github.com/jotaijs/jotai-effect/pull/30](https://togithub.com/jotaijs/jotai-effect/pull/30)

**Full Changelog**: https://github.com/jotaijs/jotai-effect/compare/v0.4.0...v0.5.0

</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:eyJjcmVhdGVkSW5WZXIiOiIzNy4xNTMuMiIsInVwZGF0ZWRJblZlciI6IjM3LjE1My4yIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-02-02 07:16:11 +00:00
LongYinan
4b3808faf9 chore: bump up @vanilla-extract/vite-plugin version to v4 (#5730)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@vanilla-extract/vite-plugin](https://togithub.com/vanilla-extract-css/vanilla-extract) ([source](https://togithub.com/vanilla-extract-css/vanilla-extract/tree/HEAD/packages/vite-plugin)) | [`^3.9.2` -> `^4.0.0`](https://renovatebot.com/diffs/npm/@vanilla-extract%2fvite-plugin/3.9.2/4.0.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@vanilla-extract%2fvite-plugin/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vanilla-extract%2fvite-plugin/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vanilla-extract%2fvite-plugin/3.9.2/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vanilla-extract%2fvite-plugin/3.9.2/4.0.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>vanilla-extract-css/vanilla-extract (@&#8203;vanilla-extract/vite-plugin)</summary>

### [`v4.0.2`](https://togithub.com/vanilla-extract-css/vanilla-extract/blob/HEAD/packages/vite-plugin/CHANGELOG.md#402)

[Compare Source](https://togithub.com/vanilla-extract-css/vanilla-extract/compare/@vanilla-extract/vite-plugin@4.0.1...@vanilla-extract/vite-plugin@4.0.2)

##### Patch Changes

-   [#&#8203;1304](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1304) [`545bf82`](545bf82f12) Thanks [@&#8203;fukumasuya](https://togithub.com/fukumasuya)! - Pass Vite `resolve` config to vite-node compiler

    The plugin passes through the project's Vite `resolve` config to the vite-node compiler, which will be used for resolving imports. These options include [`resolve.alias`][resolve.alias], [`resolve.dedupe`][resolve.dedupe], [`resolve.conditions`][resolve.conditions], [`resolve.mainFields`][resolve.mainFields], [`resolve.extensions`][resolve.extensions], and others.

    [`resolve.alias`]: https://vitejs.dev/config/shared-options.html#resolve-alias

    [`resolve.dedupe`]: https://vitejs.dev/config/shared-options.html#resolve-dedupe

    [`resolve.conditions`]: https://vitejs.dev/config/shared-options.html#resolve-conditions

    [`resolve.mainFields`]: https://vitejs.dev/config/shared-options.html#resolve-mainfields

    [`resolve.extensions`]: https://vitejs.dev/config/shared-options.html#resolve-extensions

-   Updated dependencies \[[`545bf82`](545bf82f12)]:
    -   [@&#8203;vanilla-extract/integration](https://togithub.com/vanilla-extract/integration)[@&#8203;6](https://togithub.com/6).5.0

### [`v4.0.1`](https://togithub.com/vanilla-extract-css/vanilla-extract/blob/HEAD/packages/vite-plugin/CHANGELOG.md#401)

[Compare Source](https://togithub.com/vanilla-extract-css/vanilla-extract/compare/@vanilla-extract/vite-plugin@4.0.0...@vanilla-extract/vite-plugin@4.0.1)

##### Patch Changes

-   [#&#8203;1300](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1300) [`d0b84f6`](d0b84f6340) Thanks [@&#8203;mrm007](https://togithub.com/mrm007)! - Skip loading plugins added by Vitest

-   [#&#8203;1297](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1297) [`85e1131`](85e11318f0) Thanks [@&#8203;mrm007](https://togithub.com/mrm007)! - Correctly resolve the user's Vite plugins

### [`v4.0.0`](https://togithub.com/vanilla-extract-css/vanilla-extract/blob/HEAD/packages/vite-plugin/CHANGELOG.md#400)

[Compare Source](https://togithub.com/vanilla-extract-css/vanilla-extract/compare/@vanilla-extract/vite-plugin@3.9.5...@vanilla-extract/vite-plugin@4.0.0)

##### Major Changes

-   [#&#8203;1264](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1264) [`e531c41`](e531c4170d) Thanks [@&#8203;mrm007](https://togithub.com/mrm007)! - The Vite plugin now uses a newer, faster, Vite-based compiler by default.

    The new compiler uses [`vite-node`](https://togithub.com/vitest-dev/vitest/tree/main/packages/vite-node) to parse and extract CSS from `.css.ts` files. This comes with all the benefits of using Vite, faster builds and the ability to use Vite plugins.

    The new compiler has been used in Remix ever since support for Vanilla Extract was introduced.

-   [#&#8203;1264](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1264) [`e531c41`](e531c4170d) Thanks [@&#8203;mrm007](https://togithub.com/mrm007)! - The behaviour previously known as `emitCssInSsr` has been turned on by default. The `emitCssInSsr` option has been removed.

    This means that the CSS emitted by the plugin is now processed by Vite, so the plugin no longer needs to resolve PostCSS plugins and process the CSS output itself.

-   [#&#8203;1264](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1264) [`e531c41`](e531c4170d) Thanks [@&#8203;mrm007](https://togithub.com/mrm007)! - The `esbuildOptions` option has been removed as we are no longer using esbuild internally

-   [#&#8203;1264](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1264) [`e531c41`](e531c4170d) Thanks [@&#8203;mrm007](https://togithub.com/mrm007)! - Drop support for Vite < 4

##### Patch Changes

-   [#&#8203;1264](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1264) [`e531c41`](e531c4170d) Thanks [@&#8203;mrm007](https://togithub.com/mrm007)! - Update dependencies

-   Updated dependencies \[[`e531c41`](e531c4170d), [`e531c41`](e531c4170d)]:
    -   [@&#8203;vanilla-extract/integration](https://togithub.com/vanilla-extract/integration)[@&#8203;6](https://togithub.com/6).4.0

### [`v3.9.5`](https://togithub.com/vanilla-extract-css/vanilla-extract/blob/HEAD/packages/vite-plugin/CHANGELOG.md#395)

[Compare Source](https://togithub.com/vanilla-extract-css/vanilla-extract/compare/@vanilla-extract/vite-plugin@3.9.4...@vanilla-extract/vite-plugin@3.9.5)

##### Patch Changes

-   [#&#8203;1291](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1291) [`00af971`](00af9715e5) Thanks [@&#8203;mrm007](https://togithub.com/mrm007)! - Update dependency `@vanilla-extract/integration`

-   [#&#8203;1254](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1254) [`f373d7f`](f373d7f6b5) Thanks [@&#8203;EvgenNoskov](https://togithub.com/EvgenNoskov)! - Allow hyphens in class names when using a custom identifier

### [`v3.9.4`](https://togithub.com/vanilla-extract-css/vanilla-extract/blob/HEAD/packages/vite-plugin/CHANGELOG.md#394)

[Compare Source](https://togithub.com/vanilla-extract-css/vanilla-extract/compare/@vanilla-extract/vite-plugin@3.9.3...@vanilla-extract/vite-plugin@3.9.4)

##### Patch Changes

-   [#&#8203;1262](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1262) [`610c50b`](610c50b001) Thanks [@&#8203;mrm007](https://togithub.com/mrm007)! - Update Babel config to target Node.js 14

-   [#&#8203;1262](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1262) [`610c50b`](610c50b001) Thanks [@&#8203;mrm007](https://togithub.com/mrm007)! - Lazy load Vite to avoid the CJS warning

-   Updated dependencies \[[`610c50b`](610c50b001), [`610c50b`](610c50b001)]:
    -   [@&#8203;vanilla-extract/integration](https://togithub.com/vanilla-extract/integration)[@&#8203;6](https://togithub.com/6).2.5

### [`v3.9.3`](https://togithub.com/vanilla-extract-css/vanilla-extract/blob/HEAD/packages/vite-plugin/CHANGELOG.md#393)

[Compare Source](https://togithub.com/vanilla-extract-css/vanilla-extract/compare/@vanilla-extract/vite-plugin@3.9.2...@vanilla-extract/vite-plugin@3.9.3)

##### Patch Changes

-   [#&#8203;1250](https://togithub.com/vanilla-extract-css/vanilla-extract/pull/1250) [`bc349fd`](bc349fd7cb) Thanks [@&#8203;kosmotema](https://togithub.com/kosmotema)! - Prevent unnecessary module invalidations when using PostCSS

</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:eyJjcmVhdGVkSW5WZXIiOiIzNy4xNTMuMiIsInVwZGF0ZWRJblZlciI6IjM3LjE1My4yIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-02-02 07:02:22 +00:00
LongYinan
67ab814108 chore: bump up nodemailer version to v6.9.9 [SECURITY] (#5780)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

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

### GitHub Vulnerability Alerts

#### [GHSA-9h6g-pr28-7cqp](https://togithub.com/nodemailer/nodemailer/security/advisories/GHSA-9h6g-pr28-7cqp)

### Summary
A ReDoS vulnerability occurs when nodemailer tries to parse img files with the parameter `attachDataUrls` set, causing the stuck of event loop.
Another flaw was found when nodemailer tries to parse an attachments with a embedded file, causing the stuck of event loop.

### Details

Regex: /^data:((?:[^;]*;)*(?:[^,]*)),(.*)$/

Path: compile -> getAttachments -> _processDataUrl

Regex: /(<img\b[^>]* src\s*=[\s"']*)(data:([^;]+);[^"'>\s]+)/

Path: _convertDataImages

### PoC

https://gist.github.com/francoatmega/890dd5053375333e40c6fdbcc8c58df6
https://gist.github.com/francoatmega/9aab042b0b24968d7b7039818e8b2698

### Impact

ReDoS causes the event loop to stuck a specially crafted evil email can cause this problem.

---

### Release Notes

<details>
<summary>nodemailer/nodemailer (nodemailer)</summary>

### [`v6.9.9`](https://togithub.com/nodemailer/nodemailer/blob/HEAD/CHANGELOG.md#699-2024-02-01)

[Compare Source](https://togithub.com/nodemailer/nodemailer/compare/v6.9.8...v6.9.9)

##### Bug Fixes

-   **security:** Fix issues described in GHSA-9h6g-pr28-7cqp. Do not use eternal matching pattern if only a few occurences are expected ([dd8f5e8](dd8f5e8a4d))
-   **tests:** Use native node test runner, added code coverage support, removed grunt ([#&#8203;1604](https://togithub.com/nodemailer/nodemailer/issues/1604)) ([be45c1b](be45c1b299))

### [`v6.9.8`](https://togithub.com/nodemailer/nodemailer/blob/HEAD/CHANGELOG.md#698-2023-12-30)

[Compare Source](https://togithub.com/nodemailer/nodemailer/compare/v6.9.7...v6.9.8)

##### Bug Fixes

-   **punycode:** do not use native punycode module ([b4d0e0c](b4d0e0c7cc))

</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 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:eyJjcmVhdGVkSW5WZXIiOiIzNy4xNTMuMiIsInVwZGF0ZWRJblZlciI6IjM3LjE1My4yIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-02-02 05:53:44 +00:00
LongYinan
d23f8f8087 ci: fix test-done job condition (#5784) 2024-02-02 05:21:59 +00:00
LongYinan
8f4b4e20ab fix(core): typecheck after jotai upgraded (#5779) 2024-02-01 16:10:14 +00:00
LongYinan
45b5800a23 chore: bump up all non-major dependencies (#5550)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence | Type | Update |
|---|---|---|---|---|---|---|---|
| [@aws-sdk/client-s3](https://togithub.com/aws/aws-sdk-js-v3/tree/main/clients/client-s3) ([source](https://togithub.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3)) | [`3.499.0` -> `3.504.0`](https://renovatebot.com/diffs/npm/@aws-sdk%2fclient-s3/3.499.0/3.504.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@aws-sdk%2fclient-s3/3.504.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@aws-sdk%2fclient-s3/3.504.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@aws-sdk%2fclient-s3/3.499.0/3.504.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@aws-sdk%2fclient-s3/3.499.0/3.504.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@marsidev/react-turnstile](https://togithub.com/marsidev/react-turnstile) | [`^0.4.0` -> `^0.5.0`](https://renovatebot.com/diffs/npm/@marsidev%2freact-turnstile/0.4.0/0.5.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@marsidev%2freact-turnstile/0.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@marsidev%2freact-turnstile/0.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@marsidev%2freact-turnstile/0.4.0/0.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@marsidev%2freact-turnstile/0.4.0/0.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@napi-rs/cli](https://togithub.com/napi-rs/napi-rs) | [`3.0.0-alpha.33` -> `3.0.0-alpha.36`](https://renovatebot.com/diffs/npm/@napi-rs%2fcli/3.0.0-alpha.33/3.0.0-alpha.36) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@napi-rs%2fcli/3.0.0-alpha.36?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@napi-rs%2fcli/3.0.0-alpha.36?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@napi-rs%2fcli/3.0.0-alpha.33/3.0.0-alpha.36?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@napi-rs%2fcli/3.0.0-alpha.33/3.0.0-alpha.36?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [@node-rs/jsonwebtoken](https://togithub.com/napi-rs/node-rs) | [`^0.3.0` -> `^0.4.0`](https://renovatebot.com/diffs/npm/@node-rs%2fjsonwebtoken/0.3.1/0.4.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@node-rs%2fjsonwebtoken/0.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@node-rs%2fjsonwebtoken/0.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@node-rs%2fjsonwebtoken/0.3.1/0.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@node-rs%2fjsonwebtoken/0.3.1/0.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@nx/vite](https://nx.dev) ([source](https://togithub.com/nrwl/nx/tree/HEAD/packages/vite)) | [`17.2.8` -> `17.3.1`](https://renovatebot.com/diffs/npm/@nx%2fvite/17.2.8/17.3.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@nx%2fvite/17.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@nx%2fvite/17.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@nx%2fvite/17.2.8/17.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nx%2fvite/17.2.8/17.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@opentelemetry/exporter-prometheus](https://togithub.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-exporter-prometheus) ([source](https://togithub.com/open-telemetry/opentelemetry-js)) | [`^0.47.0` -> `^0.48.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fexporter-prometheus/0.47.0/0.48.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fexporter-prometheus/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2fexporter-prometheus/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2fexporter-prometheus/0.47.0/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fexporter-prometheus/0.47.0/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@opentelemetry/host-metrics](https://togithub.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/opentelemetry-host-metrics#readme) ([source](https://togithub.com/open-telemetry/opentelemetry-js-contrib)) | [`^0.34.0` -> `^0.35.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fhost-metrics/0.34.1/0.35.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fhost-metrics/0.35.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2fhost-metrics/0.35.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2fhost-metrics/0.34.1/0.35.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fhost-metrics/0.34.1/0.35.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@opentelemetry/instrumentation](https://togithub.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation) ([source](https://togithub.com/open-telemetry/opentelemetry-js)) | [`^0.47.0` -> `^0.48.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation/0.47.0/0.48.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation/0.47.0/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation/0.47.0/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@opentelemetry/instrumentation-graphql](https://togithub.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-graphql#readme) ([source](https://togithub.com/open-telemetry/opentelemetry-js-contrib)) | [`^0.36.0` -> `^0.37.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-graphql/0.36.1/0.37.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-graphql/0.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation-graphql/0.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation-graphql/0.36.1/0.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-graphql/0.36.1/0.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@opentelemetry/instrumentation-http](https://togithub.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-http) ([source](https://togithub.com/open-telemetry/opentelemetry-js)) | [`^0.47.0` -> `^0.48.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-http/0.47.0/0.48.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-http/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation-http/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation-http/0.47.0/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-http/0.47.0/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@opentelemetry/instrumentation-ioredis](https://togithub.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-ioredis#readme) ([source](https://togithub.com/open-telemetry/opentelemetry-js-contrib)) | [`^0.36.0` -> `^0.37.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-ioredis/0.36.1/0.37.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-ioredis/0.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation-ioredis/0.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation-ioredis/0.36.1/0.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-ioredis/0.36.1/0.37.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@opentelemetry/instrumentation-nestjs-core](https://togithub.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-nestjs-core#readme) ([source](https://togithub.com/open-telemetry/opentelemetry-js-contrib)) | [`^0.33.3` -> `^0.34.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-nestjs-core/0.33.4/0.34.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-nestjs-core/0.34.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation-nestjs-core/0.34.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation-nestjs-core/0.33.4/0.34.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-nestjs-core/0.33.4/0.34.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@opentelemetry/instrumentation-socket.io](https://togithub.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-socket.io#readme) ([source](https://togithub.com/open-telemetry/opentelemetry-js-contrib)) | [`^0.35.0` -> `^0.36.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2finstrumentation-socket.io/0.35.0/0.36.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2finstrumentation-socket.io/0.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2finstrumentation-socket.io/0.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2finstrumentation-socket.io/0.35.0/0.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2finstrumentation-socket.io/0.35.0/0.36.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@opentelemetry/sdk-node](https://togithub.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-sdk-node) ([source](https://togithub.com/open-telemetry/opentelemetry-js)) | [`^0.47.0` -> `^0.48.0`](https://renovatebot.com/diffs/npm/@opentelemetry%2fsdk-node/0.47.0/0.48.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@opentelemetry%2fsdk-node/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@opentelemetry%2fsdk-node/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@opentelemetry%2fsdk-node/0.47.0/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@opentelemetry%2fsdk-node/0.47.0/0.48.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [@taplo/cli](https://taplo.tamasfe.dev) ([source](https://togithub.com/tamasfe/taplo)) | [`^0.5.2` -> `^0.7.0`](https://renovatebot.com/diffs/npm/@taplo%2fcli/0.5.2/0.7.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@taplo%2fcli/0.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@taplo%2fcli/0.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@taplo%2fcli/0.5.2/0.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@taplo%2fcli/0.5.2/0.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@vitest/coverage-istanbul](https://togithub.com/vitest-dev/vitest/tree/main/packages/coverage-istanbul#readme) ([source](https://togithub.com/vitest-dev/vitest/tree/HEAD/packages/coverage-istanbul)) | [`1.1.3` -> `1.2.2`](https://renovatebot.com/diffs/npm/@vitest%2fcoverage-istanbul/1.1.3/1.2.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@vitest%2fcoverage-istanbul/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vitest%2fcoverage-istanbul/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vitest%2fcoverage-istanbul/1.1.3/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitest%2fcoverage-istanbul/1.1.3/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [@vitest/ui](https://togithub.com/vitest-dev/vitest/tree/main/packages/ui#readme) ([source](https://togithub.com/vitest-dev/vitest/tree/HEAD/packages/ui)) | [`1.1.3` -> `1.2.2`](https://renovatebot.com/diffs/npm/@vitest%2fui/1.1.3/1.2.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@vitest%2fui/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vitest%2fui/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vitest%2fui/1.1.3/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitest%2fui/1.1.3/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [cloudflare/wrangler-action](https://togithub.com/cloudflare/wrangler-action) | `v3.4.0` -> `v3.4.1` | [![age](https://developer.mend.io/api/mc/badges/age/github-tags/cloudflare%2fwrangler-action/v3.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/github-tags/cloudflare%2fwrangler-action/v3.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/github-tags/cloudflare%2fwrangler-action/v3.4.0/v3.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/github-tags/cloudflare%2fwrangler-action/v3.4.0/v3.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | action | patch |
| [esbuild](https://togithub.com/evanw/esbuild) | [`^0.19.7` -> `^0.20.0`](https://renovatebot.com/diffs/npm/esbuild/0.19.8/0.20.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/esbuild/0.20.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/esbuild/0.20.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/esbuild/0.19.8/0.20.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/esbuild/0.19.8/0.20.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [jotai-effect](https://togithub.com/jotaijs/jotai-effect) | [`^0.2.3` -> `^0.4.0`](https://renovatebot.com/diffs/npm/jotai-effect/0.2.3/0.4.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/jotai-effect/0.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/jotai-effect/0.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/jotai-effect/0.2.3/0.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jotai-effect/0.2.3/0.4.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| [jotai-scope](https://togithub.com/jotaijs/jotai-scope) | [`^0.4.1` -> `^0.5.0`](https://renovatebot.com/diffs/npm/jotai-scope/0.4.1/0.5.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/jotai-scope/0.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/jotai-scope/0.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/jotai-scope/0.4.1/0.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jotai-scope/0.4.1/0.5.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | dependencies | minor |
| openresty/openresty | `1.21.4.3-0-buster` -> `1.25.3.1-0-buster` | [![age](https://developer.mend.io/api/mc/badges/age/docker/openresty%2fopenresty/1.25.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/docker/openresty%2fopenresty/1.25.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/docker/openresty%2fopenresty/1.21.4.3/1.25.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/docker/openresty%2fopenresty/1.21.4.3/1.25.3.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | final | minor |
| [vite-plugin-dts](https://togithub.com/qmhc/vite-plugin-dts) | [`3.7.0` -> `3.7.2`](https://renovatebot.com/diffs/npm/vite-plugin-dts/3.7.0/3.7.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/vite-plugin-dts/3.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vite-plugin-dts/3.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vite-plugin-dts/3.7.0/3.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite-plugin-dts/3.7.0/3.7.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | patch |
| [vitest](https://togithub.com/vitest-dev/vitest) ([source](https://togithub.com/vitest-dev/vitest/tree/HEAD/packages/vitest)) | [`1.1.3` -> `1.2.2`](https://renovatebot.com/diffs/npm/vitest/1.1.3/1.2.2) | [![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vitest/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vitest/1.1.3/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/1.1.3/1.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/) | devDependencies | minor |
| [yarn](https://togithub.com/yarnpkg/berry) ([source](https://togithub.com/yarnpkg/berry/tree/HEAD/packages/yarnpkg-cli)) | [`4.0.2` -> `4.1.0`](https://renovatebot.com/diffs/npm/yarn/4.0.2/4.1.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/yarn/4.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/yarn/4.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/yarn/4.0.2/4.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/yarn/4.0.2/4.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | packageManager | minor |

---

### Release Notes

<details>
<summary>aws/aws-sdk-js-v3 (@&#8203;aws-sdk/client-s3)</summary>

### [`v3.504.0`](https://togithub.com/aws/aws-sdk-js-v3/blob/HEAD/clients/client-s3/CHANGELOG.md#35040-2024-01-31)

[Compare Source](https://togithub.com/aws/aws-sdk-js-v3/compare/v3.503.1...v3.504.0)

**Note:** Version bump only for package [@&#8203;aws-sdk/client-s3](https://togithub.com/aws-sdk/client-s3)

#### [3.503.1](https://togithub.com/aws/aws-sdk-js-v3/compare/v3.503.0...v3.503.1) (2024-01-30)

**Note:** Version bump only for package [@&#8203;aws-sdk/client-s3](https://togithub.com/aws-sdk/client-s3)

### [`v3.503.1`](https://togithub.com/aws/aws-sdk-js-v3/blob/HEAD/clients/client-s3/CHANGELOG.md#35031-2024-01-30)

[Compare Source](https://togithub.com/aws/aws-sdk-js-v3/compare/v3.503.0...v3.503.1)

**Note:** Version bump only for package [@&#8203;aws-sdk/client-s3](https://togithub.com/aws-sdk/client-s3)

### [`v3.503.0`](https://togithub.com/aws/aws-sdk-js-v3/blob/HEAD/clients/client-s3/CHANGELOG.md#35030-2024-01-30)

[Compare Source](https://togithub.com/aws/aws-sdk-js-v3/compare/v3.502.0...v3.503.0)

**Note:** Version bump only for package [@&#8203;aws-sdk/client-s3](https://togithub.com/aws-sdk/client-s3)

### [`v3.502.0`](https://togithub.com/aws/aws-sdk-js-v3/blob/HEAD/clients/client-s3/CHANGELOG.md#35020-2024-01-29)

[Compare Source](https://togithub.com/aws/aws-sdk-js-v3/compare/v3.501.0...v3.502.0)

##### Features

-   **credential-providers:** lazy load STS & SSO clients in credential providers ([#&#8203;5681](https://togithub.com/aws/aws-sdk-js-v3/issues/5681)) ([d27301d](d27301d48f))

### [`v3.501.0`](https://togithub.com/aws/aws-sdk-js-v3/blob/HEAD/clients/client-s3/CHANGELOG.md#35010-2024-01-26)

[Compare Source](https://togithub.com/aws/aws-sdk-js-v3/compare/v3.499.0...v3.501.0)

**Note:** Version bump only for package [@&#8203;aws-sdk/client-s3](https://togithub.com/aws-sdk/client-s3)

</details>

<details>
<summary>marsidev/react-turnstile (@&#8203;marsidev/react-turnstile)</summary>

### [`v0.5.1`](https://togithub.com/marsidev/react-turnstile/releases/tag/v0.5.1)

[Compare Source](https://togithub.com/marsidev/react-turnstile/compare/v0.5.0...v0.5.1)

#####    🐞 Bug Fixes

-   Remove nullish coalescing operator (`??`) to improve compatibility with old browsers  -  by [@&#8203;marsidev](https://togithub.com/marsidev) [<samp>(80f61)</samp>](https://togithub.com/marsidev/react-turnstile/commit/80f61c6)

#####     [View changes on GitHub](https://togithub.com/marsidev/react-turnstile/compare/v0.5.0...v0.5.1)

### [`v0.5.0`](https://togithub.com/marsidev/react-turnstile/releases/tag/v0.5.0)

[Compare Source](https://togithub.com/marsidev/react-turnstile/compare/v0.4.1...v0.5.0)

#####    🚀 Features

-   Add `getResponsePromise`  -  by [@&#8203;marsidev](https://togithub.com/marsidev) [<samp>(61308)</samp>](https://togithub.com/marsidev/react-turnstile/commit/6130898)

#####     [View changes on GitHub](https://togithub.com/marsidev/react-turnstile/compare/v0.4.1...v0.5.0)

### [`v0.4.1`](https://togithub.com/marsidev/react-turnstile/releases/tag/v0.4.1)

[Compare Source](https://togithub.com/marsidev/react-turnstile/compare/v0.4.0...v0.4.1)

#####    🚀 Features

-   Add `onWidgetLoad` callback  -  by [@&#8203;marsidev](https://togithub.com/marsidev) [<samp>(6811b)</samp>](https://togithub.com/marsidev/react-turnstile/commit/6811bce)

#####     [View changes on GitHub](https://togithub.com/marsidev/react-turnstile/compare/v0.4.0...v0.4.1)

</details>

<details>
<summary>napi-rs/napi-rs (@&#8203;napi-rs/cli)</summary>

### [`v3.0.0-alpha.36`](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.35...@napi-rs/cli@3.0.0-alpha.36)

[Compare Source](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.35...@napi-rs/cli@3.0.0-alpha.36)

### [`v3.0.0-alpha.35`](https://togithub.com/napi-rs/napi-rs/releases/tag/%40napi-rs/cli%403.0.0-alpha.35)

[Compare Source](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.34...@napi-rs/cli@3.0.0-alpha.35)

##### What's Changed

-   chore: bump memfs-browser by [@&#8203;Brooooooklyn](https://togithub.com/Brooooooklyn) in [https://github.com/napi-rs/napi-rs/pull/1900](https://togithub.com/napi-rs/napi-rs/pull/1900)
-   feat(cli): Add support for s390x linux arch in js bindings template by [@&#8203;mgcm](https://togithub.com/mgcm) in [https://github.com/napi-rs/napi-rs/pull/1901](https://togithub.com/napi-rs/napi-rs/pull/1901)
-   feat: add wasm runtime package by [@&#8203;Brooooooklyn](https://togithub.com/Brooooooklyn) in [https://github.com/napi-rs/napi-rs/pull/1904](https://togithub.com/napi-rs/napi-rs/pull/1904)

##### New Contributors

-   [@&#8203;mgcm](https://togithub.com/mgcm) made their first contribution in [https://github.com/napi-rs/napi-rs/pull/1901](https://togithub.com/napi-rs/napi-rs/pull/1901)

**Full Changelog**: https://github.com/napi-rs/napi-rs/compare/[@&#8203;napi-rs/cli](https://togithub.com/napi-rs/cli)[@&#8203;3](https://togithub.com/3).0.0-alpha.34...[@&#8203;napi-rs/cli](https://togithub.com/napi-rs/cli)[@&#8203;3](https://togithub.com/3).0.0-alpha.35

### [`v3.0.0-alpha.34`](https://togithub.com/napi-rs/napi-rs/releases/tag/%40napi-rs/cli%403.0.0-alpha.34)

[Compare Source](https://togithub.com/napi-rs/napi-rs/compare/@napi-rs/cli@3.0.0-alpha.33...@napi-rs/cli@3.0.0-alpha.34)

##### What's Changed

-   fix(cli): add browser entry by [@&#8203;Brooooooklyn](https://togithub.com/Brooooooklyn) in [https://github.com/napi-rs/napi-rs/pull/1899](https://togithub.com/napi-rs/napi-rs/pull/1899)

**Full Changelog**: https://github.com/napi-rs/napi-rs/compare/[@&#8203;napi-rs/cli](https://togithub.com/napi-rs/cli)[@&#8203;3](https://togithub.com/3).0.0-alpha.33...[@&#8203;napi-rs/cli](https://togithub.com/napi-rs/cli)[@&#8203;3](https://togithub.com/3).0.0-alpha.34

</details>

<details>
<summary>napi-rs/node-rs (@&#8203;node-rs/jsonwebtoken)</summary>

### [`v0.4.0`](https://togithub.com/napi-rs/node-rs/compare/@node-rs/jsonwebtoken@0.3.1...@node-rs/jsonwebtoken@0.4.0)

[Compare Source](https://togithub.com/napi-rs/node-rs/compare/@node-rs/jsonwebtoken@0.3.1...@node-rs/jsonwebtoken@0.4.0)

</details>

<details>
<summary>nrwl/nx (@&#8203;nx/vite)</summary>

### [`v17.3.1`](https://togithub.com/nrwl/nx/releases/tag/17.3.1)

[Compare Source](https://togithub.com/nrwl/nx/compare/17.3.0...17.3.1)

##### 17.3.1 (2024-01-31)

##### 🚀 Features

-   **angular:** add generator to convert targets to use the esbuild-based application executor ([#&#8203;21333](https://togithub.com/nrwl/nx/pull/21333))
-   **core:** don't clear NX_BASE or NX_HEAD ([#&#8203;20125](https://togithub.com/nrwl/nx/pull/20125))
-   **devkit:** improving error handeling read target options ([#&#8203;20336](https://togithub.com/nrwl/nx/pull/20336))
-   **react-native:** add useTransformReactJSX babel config to generators ([#&#8203;19170](https://togithub.com/nrwl/nx/pull/19170))
-   **vite:** allow passing path to custom tsconfig file when skipTypeCheck is false ([#&#8203;19784](https://togithub.com/nrwl/nx/pull/19784))

##### 🩹 Fixes

-   nestjs lib generator pass skipPackageJson flag to js lib generator ([#&#8203;20442](https://togithub.com/nrwl/nx/pull/20442))
-   **angular:** fix dev-server validation for esbuildMiddleware ([#&#8203;21413](https://togithub.com/nrwl/nx/pull/21413))
-   **angular:** should not log invalid extension includedScripts ([#&#8203;21441](https://togithub.com/nrwl/nx/pull/21441))
-   **bundling:** consider index/folder imports in manual file resolution ([#&#8203;19030](https://togithub.com/nrwl/nx/pull/19030))
-   **core:** remove misleading message at front of error ([#&#8203;21443](https://togithub.com/nrwl/nx/pull/21443))
-   **core:** new generator not skipping package installation ([#&#8203;17927](https://togithub.com/nrwl/nx/pull/17927))
-   **core:** include typescript package when calculating project hashes ([#&#8203;21285](https://togithub.com/nrwl/nx/pull/21285))
-   **core:** do not append node_module paths in `run-script` executor ([#&#8203;21445](https://togithub.com/nrwl/nx/pull/21445))
-   **graph:** repair externalApiService usage broken after refactor ([#&#8203;21422](https://togithub.com/nrwl/nx/pull/21422))
-   **js:** allow inlined libs without imports ([#&#8203;20649](https://togithub.com/nrwl/nx/pull/20649))
-   **linter:** remove extend of [@&#8203;angular-eslint/recommended--extra](https://togithub.com/angular-eslint/recommended--extra) config ([#&#8203;18465](https://togithub.com/nrwl/nx/pull/18465))
-   **linter:** restore rules to match previous [@&#8203;typescript-eslint/recommended](https://togithub.com/typescript-eslint/recommended) ([#&#8203;21424](https://togithub.com/nrwl/nx/pull/21424))
-   **linter:** make target default migrations a bit more robust ([#&#8203;21446](https://togithub.com/nrwl/nx/pull/21446))
-   **misc:** prevent ts-node from reading tsconfig when registering transpiler ([#&#8203;21381](https://togithub.com/nrwl/nx/pull/21381))
-   **nextjs:** Add missing support swc for custom server ([#&#8203;21401](https://togithub.com/nrwl/nx/pull/21401))
-   **nextjs:** lazy load plugin import ([#&#8203;21426](https://togithub.com/nrwl/nx/pull/21426))
-   **nx-dev:** remove unmatched bracket. updates warning description ([#&#8203;19481](https://togithub.com/nrwl/nx/pull/19481))
-   **react:** import SupportedStyles type from correct path ([#&#8203;20239](https://togithub.com/nrwl/nx/pull/20239))
-   **vite:** prevent vite:build copying package.json when generatePackageJson false ([#&#8203;19780](https://togithub.com/nrwl/nx/pull/19780))
-   **vue:** do not add [@&#8203;vue/tsconfig](https://togithub.com/vue/tsconfig) dependency ([#&#8203;19873](https://togithub.com/nrwl/nx/pull/19873))

##### ❤️  Thank You

-   Adam Hunter [@&#8203;adamrhunter](https://togithub.com/adamrhunter)
-   Benjamin Kroeger [@&#8203;benkroeger](https://togithub.com/benkroeger)
-   Christian Käslin
-   Craigory Coppola [@&#8203;AgentEnder](https://togithub.com/AgentEnder)
-   Denis Frenademetz [@&#8203;skrtheboss](https://togithub.com/skrtheboss)
-   James Henry [@&#8203;JamesHenry](https://togithub.com/JamesHenry)
-   Jan Pretzel
-   Jonathan Cammisuli
-   Leosvel Pérez Espinosa [@&#8203;leosvelperez](https://togithub.com/leosvelperez)
-   MaxKless [@&#8203;MaxKless](https://togithub.com/MaxKless)
-   Michal Jez [@&#8203;MJez29](https://togithub.com/MJez29)
-   Miloš Lajtman [@&#8203;miluoshi](https://togithub.com/miluoshi)
-   Nicholas Cunningham [@&#8203;ndcunningham](https://togithub.com/ndcunningham)
-   Tobbb
-   Vinit Neogi [@&#8203;vneogi199](https://togithub.com/vneogi199)
-   wout junius [@&#8203;wout-junius](https://togithub.com/wout-junius)
-   Zac Bristow

### [`v17.3.0`](https://togithub.com/nrwl/nx/releases/tag/17.3.0)

[Compare Source](https://togithub.com/nrwl/nx/compare/17.2.8...17.3.0)

#### 17.3.0 (2024-01-29)

##### 🚀 Features

-   **angular:** support esbuild middleware functions ([#&#8203;21048](https://togithub.com/nrwl/nx/pull/21048))
-   **angular:** support angular 17.1.0 ([#&#8203;20556](https://togithub.com/nrwl/nx/pull/20556))
-   **core:** remove prompt from nx connect command ([67b5bd6c9f](https://togithub.com/nrwl/nx/commit/67b5bd6c9f))
-   **core:** use Nx plugins and inferred targets when running "nx init" ([#&#8203;20872](https://togithub.com/nrwl/nx/pull/20872))
-   **core:** create a new function to run child processes via rust ([#&#8203;21070](https://togithub.com/nrwl/nx/pull/21070))
-   **core:** read name from package.json if present and no inference plugin provides name ([#&#8203;21125](https://togithub.com/nrwl/nx/pull/21125))
-   **core:** add keepExistingVersions to all packages ([#&#8203;21169](https://togithub.com/nrwl/nx/pull/21169))
-   **core:** add the "add" cli command ([#&#8203;20976](https://togithub.com/nrwl/nx/pull/20976))
-   **core:** add target defaults in configuration generators rather th… ([#&#8203;21105](https://togithub.com/nrwl/nx/pull/21105))
-   **core:** update ci-workflow generator ([#&#8203;21141](https://togithub.com/nrwl/nx/pull/21141))
-   **core:** extend nxCloud prompt to include basic CI workflow options ([#&#8203;21094](https://togithub.com/nrwl/nx/pull/21094))
-   **core:** move target defaults handling to nx plugin ([#&#8203;21104](https://togithub.com/nrwl/nx/pull/21104))
-   **core:** forward stdin to commands started via rust ([#&#8203;21195](https://togithub.com/nrwl/nx/pull/21195))
-   **core:** reveal --web flag on show project ([#&#8203;21293](https://togithub.com/nrwl/nx/pull/21293))
-   **core:** use runCommand for runScript ([#&#8203;21292](https://togithub.com/nrwl/nx/pull/21292))
-   **core:** support args to be an array for command ([#&#8203;21290](https://togithub.com/nrwl/nx/pull/21290))
-   **core:** improve generated CI workflows ([#&#8203;21324](https://togithub.com/nrwl/nx/pull/21324))
-   **core:** guide users to view the graph after nx init ([#&#8203;21303](https://togithub.com/nrwl/nx/pull/21303))
-   **core:** pass down help to run-commands ([#&#8203;21331](https://togithub.com/nrwl/nx/pull/21331))
-   **cypress:** simplify inferred cypress command ([#&#8203;21337](https://togithub.com/nrwl/nx/pull/21337))
-   **detox:** add createNodes for detox ([#&#8203;21016](https://togithub.com/nrwl/nx/pull/21016))
-   **devkit:** add a flag to keep existing versions when calling addDependenciesToPackageJson ([#&#8203;21123](https://togithub.com/nrwl/nx/pull/21123))
-   **docs:** add {% project-details %} as a tag in markdown docs ([#&#8203;21288](https://togithub.com/nrwl/nx/pull/21288))
-   **expo:** support createNodes for expo ([#&#8203;21014](https://togithub.com/nrwl/nx/pull/21014))
-   **graph:** add nx console data loader ([#&#8203;20744](https://togithub.com/nrwl/nx/pull/20744))
-   **graph:** rework pdv target section & remove unused code ([#&#8203;21159](https://togithub.com/nrwl/nx/pull/21159))
-   **graph:** decouple graph client from nx.dev <Fence> component ([#&#8203;21186](https://togithub.com/nrwl/nx/pull/21186))
-   **graph:** allow expanding target when opening pdv from external api ([#&#8203;21189](https://togithub.com/nrwl/nx/pull/21189))
-   **graph:** hover to see source & more UI updates ([#&#8203;21182](https://togithub.com/nrwl/nx/pull/21182))
-   **graph:** add tooltips to project details view ([#&#8203;21205](https://togithub.com/nrwl/nx/pull/21205))
-   **graph:** show open config button in graph web ([#&#8203;21181](https://togithub.com/nrwl/nx/pull/21181))
-   **linter:** add .nx to ignored folders ([#&#8203;20720](https://togithub.com/nrwl/nx/pull/20720))
-   **linter:** update @&#8203;typescript-eslint/\* package versions ([#&#8203;20602](https://togithub.com/nrwl/nx/pull/20602))
-   **linter:** make init generator public ([51c039b252](https://togithub.com/nrwl/nx/commit/51c039b252))
-   **linter:** move common options to target defaults ([#&#8203;20583](https://togithub.com/nrwl/nx/pull/20583))
-   **misc:** align version of [@&#8203;types/node](https://togithub.com/types/node) throughout repo ([#&#8203;20883](https://togithub.com/nrwl/nx/pull/20883))
-   **misc:** add layout for project details view ([#&#8203;21172](https://togithub.com/nrwl/nx/pull/21172))
-   **misc:** update minimatch version used across packages ([#&#8203;21207](https://togithub.com/nrwl/nx/pull/21207))
-   **misc:** identify and set up more nx core plugins during nx init ([#&#8203;21254](https://togithub.com/nrwl/nx/pull/21254))
-   **misc:** optionally update package.json scripts in init generators ([#&#8203;21204](https://togithub.com/nrwl/nx/pull/21204))
-   **misc:** hide unpublished links in project details view ([#&#8203;21362](https://togithub.com/nrwl/nx/pull/21362))
-   **nextjs:** Update [@&#8203;nx/next](https://togithub.com/nx/next) to Next.js 14 ([#&#8203;20703](https://togithub.com/nrwl/nx/pull/20703))
-   **nextjs:** Add support for experimental-https when running dev server ([#&#8203;20836](https://togithub.com/nrwl/nx/pull/20836))
-   **nextjs:** Standalone projects now default to src ([#&#8203;21010](https://togithub.com/nrwl/nx/pull/21010))
-   **nuxt:** make nuxt public ([#&#8203;20656](https://togithub.com/nrwl/nx/pull/20656))
-   **nx-dev:** add homepage updates ([#&#8203;20592](https://togithub.com/nrwl/nx/pull/20592))
-   **nx-dev:** improve related docs section ([#&#8203;20796](https://togithub.com/nrwl/nx/pull/20796))
-   **nx-dev:** adjust related section title padding ([#&#8203;20803](https://togithub.com/nrwl/nx/pull/20803))
-   **nx-dev:** update documentation dropdown menu links ([#&#8203;20792](https://togithub.com/nrwl/nx/pull/20792))
-   **nx-dev:** adjust highlighting of tagline ([#&#8203;20877](https://togithub.com/nrwl/nx/pull/20877))
-   **nx-dev:** new year challenge ([#&#8203;20639](https://togithub.com/nrwl/nx/pull/20639))
-   **nx-dev:** new year challenge ([868721a157](https://togithub.com/nrwl/nx/commit/868721a157))
-   **nx-dev:** modals and flip cards ([e7dcce057b](https://togithub.com/nrwl/nx/commit/e7dcce057b))
-   **nx-dev:** rectangle cards ([49a8d84023](https://togithub.com/nrwl/nx/commit/49a8d84023))
-   **react:** Treat window and var library types the same ([#&#8203;20597](https://togithub.com/nrwl/nx/pull/20597))
-   **react:** Add playwright support to generators ([#&#8203;21150](https://togithub.com/nrwl/nx/pull/21150))
-   **react-native:** add support for createNodes in react native ([#&#8203;21013](https://togithub.com/nrwl/nx/pull/21013))
-   **react-native:** generate pod install target ([#&#8203;21166](https://togithub.com/nrwl/nx/pull/21166))
-   **release:** support Revert commits in changelog renderer ([#&#8203;20663](https://togithub.com/nrwl/nx/pull/20663))
-   **release:** conventional commits support for independent projects ([#&#8203;21012](https://togithub.com/nrwl/nx/pull/21012))
-   **release:** enable git operations by default ([#&#8203;21082](https://togithub.com/nrwl/nx/pull/21082))
-   **release:** add fallback for currentVersionResolver in the version step ([#&#8203;21155](https://togithub.com/nrwl/nx/pull/21155))
-   **release:** support conventionalCommits shorthand for version config ([#&#8203;21187](https://togithub.com/nrwl/nx/pull/21187))
-   **release:** add formal entrypoint for programmatic API at nx/release ([#&#8203;21211](https://togithub.com/nrwl/nx/pull/21211))
-   **release:** support version prefix for dependents ([#&#8203;21209](https://togithub.com/nrwl/nx/pull/21209))
-   **release:** update lockfile after version command ([#&#8203;21107](https://togithub.com/nrwl/nx/pull/21107))
-   **release:** global stageChanges option & changelog fixes ([#&#8203;21223](https://togithub.com/nrwl/nx/pull/21223))
-   **release:** allow overriding generator and generatorOptions per project ([#&#8203;21298](https://togithub.com/nrwl/nx/pull/21298))
-   **remix:** add remix ([#&#8203;20641](https://togithub.com/nrwl/nx/pull/20641))
-   **remix:** add createNodes support for target inference ([#&#8203;21073](https://togithub.com/nrwl/nx/pull/21073))
-   **remix:** generate vitest file instead vite.config ([#&#8203;21100](https://togithub.com/nrwl/nx/pull/21100))
-   **remix:** remove projects prompt from artifact generators ([#&#8203;21112](https://togithub.com/nrwl/nx/pull/21112))
-   **remix:** use esm config file ([#&#8203;21111](https://togithub.com/nrwl/nx/pull/21111))
-   **remix:** add init generator ([#&#8203;21146](https://togithub.com/nrwl/nx/pull/21146))
-   **remix:** add nx welcome component ([#&#8203;21383](https://togithub.com/nrwl/nx/pull/21383))
-   **storybook:** nodes plugin ([#&#8203;20562](https://togithub.com/nrwl/nx/pull/20562))
-   **testing:** add create-nodes plugin for playwright e2e targets ([#&#8203;20099](https://togithub.com/nrwl/nx/pull/20099))
-   **testing:** add option to allow filtering test files in playwright executor ([#&#8203;20862](https://togithub.com/nrwl/nx/pull/20862))
-   **testing:** add jest create-nodes plugin ([#&#8203;20045](https://togithub.com/nrwl/nx/pull/20045))
-   **vite:** update to vitest v1 ([#&#8203;20747](https://togithub.com/nrwl/nx/pull/20747))
-   **vite:** recognize all vite.config file extensions ([#&#8203;20971](https://togithub.com/nrwl/nx/pull/20971))
-   **vue:** add nuxt as cnw vue framework ([#&#8203;20626](https://togithub.com/nrwl/nx/pull/20626))
-   **webpack:** simplify inferred webpack-cli command ([#&#8203;21340](https://togithub.com/nrwl/nx/pull/21340))
-   **workspace:** update readme to point people to the graph ([#&#8203;21325](https://togithub.com/nrwl/nx/pull/21325))

##### 🩹 Fixes

-   **angular:** add missing package update for [@&#8203;angular/pwa](https://togithub.com/angular/pwa) ([#&#8203;20690](https://togithub.com/nrwl/nx/pull/20690))
-   **angular:** safely update task runner cacheable operations when setting up ssr ([#&#8203;20736](https://togithub.com/nrwl/nx/pull/20736))
-   **angular:** fix standalone eslint config generation ([#&#8203;20885](https://togithub.com/nrwl/nx/pull/20885))
-   **angular:** add named export for moduleFederationDevServerExecutor ([#&#8203;20944](https://togithub.com/nrwl/nx/pull/20944))
-   **angular:** support scheduling inferred angular cli builder targets ([#&#8203;21019](https://togithub.com/nrwl/nx/pull/21019))
-   **angular:** run function is not called in setup-ssr/application-builder ([#&#8203;21157](https://togithub.com/nrwl/nx/pull/21157))
-   **angular:** support scoped project names and entrypoints in library secondary entrypoint generator ([#&#8203;21300](https://togithub.com/nrwl/nx/pull/21300))
-   **angular:** update autoprefixer migration to the right file ([#&#8203;21363](https://togithub.com/nrwl/nx/pull/21363))
-   **angular:** update setup-ssr generator to support the outputPath object variant ([#&#8203;21385](https://togithub.com/nrwl/nx/pull/21385))
-   **bundling:** added back code to handle skipTypeField option of rollup executor options + tests ([#&#8203;20460](https://togithub.com/nrwl/nx/pull/20460))
-   **core:** properly handle negated paths in cache outputs ([#&#8203;20661](https://togithub.com/nrwl/nx/pull/20661))
-   **core:** show warning if workspaceRoot starts with ! ([#&#8203;20705](https://togithub.com/nrwl/nx/pull/20705))
-   **core:** fallback to checking stderr if stdout is empty on publish executor ([#&#8203;20737](https://togithub.com/nrwl/nx/pull/20737))
-   **core:** correctly move project and target strings ([#&#8203;20726](https://togithub.com/nrwl/nx/pull/20726))
-   **core:** handle "." project roots properly for hashing ([#&#8203;20979](https://togithub.com/nrwl/nx/pull/20979))
-   **core:** prioritize nxignore for watcher updates ([#&#8203;20975](https://togithub.com/nrwl/nx/pull/20975))
-   **core:** formatter should not fail when absolute paths are provided as "--files" ([#&#8203;20331](https://togithub.com/nrwl/nx/pull/20331))
-   **core:** handle invalid group glob groups ([#&#8203;21027](https://togithub.com/nrwl/nx/pull/21027))
-   **core:** ensure connect-to-nx-cloud works with lerna workspaces ([#&#8203;20895](https://togithub.com/nrwl/nx/pull/20895))
-   **core:** accept vue as preset in cnw ([#&#8203;21262](https://togithub.com/nrwl/nx/pull/21262))
-   **core:** properly disconnect daemon & reject promise ([#&#8203;21283](https://togithub.com/nrwl/nx/pull/21283))
-   **core:** fix socket dir removal for macos ([#&#8203;21306](https://togithub.com/nrwl/nx/pull/21306))
-   **core:** remove deprecated recursive rmdir with rm -rf ([#&#8203;21327](https://togithub.com/nrwl/nx/pull/21327))
-   **core:** exit with sigint when sigint is received ([#&#8203;21336](https://togithub.com/nrwl/nx/pull/21336))
-   **core:** yargs array-like prompts initial field is number ([#&#8203;21349](https://togithub.com/nrwl/nx/pull/21349))
-   **core:** clarify error log when a project exists in a directory ([#&#8203;21355](https://togithub.com/nrwl/nx/pull/21355))
-   **core:** do not create new targets from target defaults when packag… ([#&#8203;21365](https://togithub.com/nrwl/nx/pull/21365))
-   **core:** fix sending sigint to child tasks with the new psuedo tty … ([#&#8203;21369](https://togithub.com/nrwl/nx/pull/21369))
-   **core:** fix compilerOptions may not exist ([#&#8203;21364](https://togithub.com/nrwl/nx/pull/21364))
-   **core:** fix conflicting types from merge conflict ([#&#8203;21371](https://togithub.com/nrwl/nx/pull/21371))
-   **core:** address some wonkiness when merging command and run-commands ([#&#8203;21315](https://togithub.com/nrwl/nx/pull/21315))
-   **devkit:** update the ci generators to use the correct launch template ([#&#8203;21304](https://togithub.com/nrwl/nx/pull/21304))
-   **devkit:** fix extractLayoutDirectory typescript types to better reflect allowed params and return value ([#&#8203;15339](https://togithub.com/nrwl/nx/pull/15339))
-   **expo:** fix externalDependencies for expo plugin ([#&#8203;21213](https://togithub.com/nrwl/nx/pull/21213))
-   **graph:** take vscode light/dark theme into account ([#&#8203;21208](https://togithub.com/nrwl/nx/pull/21208))
-   **graph:** refresh pdv periodically in watch mode ([#&#8203;21218](https://togithub.com/nrwl/nx/pull/21218))
-   **graph:** correct value when inputs/outputs are copied ([#&#8203;21245](https://togithub.com/nrwl/nx/pull/21245))
-   **graph:** fix 404 when / in name ([#&#8203;21318](https://togithub.com/nrwl/nx/pull/21318))
-   **js:** fixing output based on test runner selection ([#&#8203;20788](https://togithub.com/nrwl/nx/pull/20788))
-   **js:** allow inlineable dependency to be added to externals ([#&#8203;21051](https://togithub.com/nrwl/nx/pull/21051))
-   **js:** ensure result is valid before attempting to close it during rollup watch ([ea3c2426d3](https://togithub.com/nrwl/nx/commit/ea3c2426d3))
-   **js:** set the unsafeHttpWhitelist when the set has any items ([#&#8203;21216](https://togithub.com/nrwl/nx/pull/21216))
-   **js:** add [@&#8203;swc/helpers](https://togithub.com/swc/helpers) when initializing js plugin since it is needed by other plugins ([#&#8203;21316](https://togithub.com/nrwl/nx/pull/21316))
-   **js:** fix missing top-level dependencies in publishable libs ([#&#8203;17730](https://togithub.com/nrwl/nx/pull/17730))
-   **linter:** move should migrate all eslint configs ([#&#8203;20709](https://togithub.com/nrwl/nx/pull/20709))
-   **linter:** fix workspace-rule naming with flat config ([#&#8203;20782](https://togithub.com/nrwl/nx/pull/20782))
-   **linter:** ensure angular entry point checks are correct ([#&#8203;20859](https://togithub.com/nrwl/nx/pull/20859))
-   **linter:** flat config should always set path to config when using API ([#&#8203;20867](https://togithub.com/nrwl/nx/pull/20867))
-   **linter:** only update overrides when applicable ([#&#8203;20917](https://togithub.com/nrwl/nx/pull/20917))
-   **linter:** add links to docs to rules ([#&#8203;21199](https://togithub.com/nrwl/nx/pull/21199))
-   **linter:** fix import of chalk for reporting ([#&#8203;21201](https://togithub.com/nrwl/nx/pull/21201))
-   **linter:** update eslint plugins for [@&#8203;typescript-eslint](https://togithub.com/typescript-eslint) v6 naming ([#&#8203;21221](https://togithub.com/nrwl/nx/pull/21221))
-   **misc:** disallow path segments and allow scoped package name in --newProjectName option of move generator ([#&#8203;20768](https://togithub.com/nrwl/nx/pull/20768))
-   **misc:** ignore .nx/cache when running nx init in an angular cli project ([#&#8203;21000](https://togithub.com/nrwl/nx/pull/21000))
-   **misc:** install nx when no plugins selected during nx init ([#&#8203;21228](https://togithub.com/nrwl/nx/pull/21228))
-   **misc:** identify usage of playwright correctly when running nx init ([#&#8203;21236](https://togithub.com/nrwl/nx/pull/21236))
-   **misc:** install required deps during nx init without overriding existing versions ([#&#8203;21237](https://togithub.com/nrwl/nx/pull/21237))
-   **misc:** do not print formatting errors while setting up nx cloud in nx init ([#&#8203;21302](https://togithub.com/nrwl/nx/pull/21302))
-   **misc:** await async function invocations ([#&#8203;21299](https://togithub.com/nrwl/nx/pull/21299))
-   **module-federation:** allow relative remote paths ([#&#8203;20763](https://togithub.com/nrwl/nx/pull/20763))
-   **module-federation:** support buildable libs ([#&#8203;20786](https://togithub.com/nrwl/nx/pull/20786))
-   **nextjs:** empty port should not overwrite env port ([#&#8203;20751](https://togithub.com/nrwl/nx/pull/20751))
-   **nextjs:** Add missing setParserOptionProject ([#&#8203;20754](https://togithub.com/nrwl/nx/pull/20754))
-   **nextjs:** Page generator should work out of the box ([#&#8203;20775](https://togithub.com/nrwl/nx/pull/20775))
-   **nextjs:** enhance page generator to work when --project is not supplied ([#&#8203;20778](https://togithub.com/nrwl/nx/pull/20778))
-   **nextjs:** remove temporary patch for next eslint rules ([#&#8203;20863](https://togithub.com/nrwl/nx/pull/20863))
-   **nextjs:** correct inferred outputs for root Next.js projects ([#&#8203;20891](https://togithub.com/nrwl/nx/pull/20891))
-   **nextjs:** update migration to handle projects without eslintrc ([#&#8203;20932](https://togithub.com/nrwl/nx/pull/20932))
-   **nextjs:** Playwright should work with workspace libs ([#&#8203;20933](https://togithub.com/nrwl/nx/pull/20933))
-   **nextjs:** Missing deps for image and css optimization ([#&#8203;20941](https://togithub.com/nrwl/nx/pull/20941))
-   **nextjs:** Add support for mjs next config file ([#&#8203;21007](https://togithub.com/nrwl/nx/pull/21007))
-   **nextjs:** PCV3 with Cypress and Playwright should work with standalone Next.js Projects ([#&#8203;21103](https://togithub.com/nrwl/nx/pull/21103))
-   **nextjs:** custom server unable to run production builds ([#&#8203;21222](https://togithub.com/nrwl/nx/pull/21222))
-   **node:** E2E test port conflicts ([#&#8203;20826](https://togithub.com/nrwl/nx/pull/20826))
-   **nuxt:** add all target names when adding vite plugin ([#&#8203;21332](https://togithub.com/nrwl/nx/pull/21332))
-   **nx-dev:** change to optimized for monorepos ([#&#8203;20668](https://togithub.com/nrwl/nx/pull/20668))
-   **nx-dev:** adjust blog links ([#&#8203;20608](https://togithub.com/nrwl/nx/pull/20608))
-   **nx-dev:** typo on the homepage ([#&#8203;20767](https://togithub.com/nrwl/nx/pull/20767))
-   **nx-dev:** fix plugin stats ([#&#8203;20741](https://togithub.com/nrwl/nx/pull/20741))
-   **nx-dev:** dynamic classes not allowed ([#&#8203;20800](https://togithub.com/nrwl/nx/pull/20800))
-   **nx-dev:** improve styles ([48bcb534fb](https://togithub.com/nrwl/nx/commit/48bcb534fb))
-   **nx-dev:** fix text colors ([69523f1eed](https://togithub.com/nrwl/nx/commit/69523f1eed))
-   **nx-dev:** fix heading size and improve contrast ([#&#8203;21057](https://togithub.com/nrwl/nx/pull/21057))
-   **nx-dev:** standardize nx cloud naming ([#&#8203;21059](https://togithub.com/nrwl/nx/pull/21059))
-   **nx-dev:** increase shorts video size to show volume control ([#&#8203;21142](https://togithub.com/nrwl/nx/pull/21142))
-   **nx-dev:** do not open official plugins in new tab ([#&#8203;21179](https://togithub.com/nrwl/nx/pull/21179))
-   **nx-dev:** align button sizes on hero ([#&#8203;21163](https://togithub.com/nrwl/nx/pull/21163))
-   **nx-dev:** table of contents with code ([#&#8203;21173](https://togithub.com/nrwl/nx/pull/21173))
-   **react:** webpack backwards compat for `@nx/react/plugin/webpack` ([#&#8203;20697](https://togithub.com/nrwl/nx/pull/20697))
-   **react:** skip adding comma to config when adding remote to host if… ([#&#8203;20620](https://togithub.com/nrwl/nx/pull/20620))
-   **react:** remove <base> tag from generated index.html ([#&#8203;20750](https://togithub.com/nrwl/nx/pull/20750))
-   **react:** update default webpack config for component testing ([#&#8203;20749](https://togithub.com/nrwl/nx/pull/20749))
-   **release:** changelog renderer should prefer breaking change explanation text ([#&#8203;20798](https://togithub.com/nrwl/nx/pull/20798))
-   **release:** ensure leading v is stripped from provided semver version ([#&#8203;20815](https://togithub.com/nrwl/nx/pull/20815))
-   **release:** add overall nx release command ([#&#8203;20535](https://togithub.com/nrwl/nx/pull/20535))
-   **release:** publish error handling, dry-run in dependsOn ([#&#8203;20889](https://togithub.com/nrwl/nx/pull/20889))
-   **release:** capture all release titles during parse ([#&#8203;20864](https://togithub.com/nrwl/nx/pull/20864))
-   **release:** do not set extra v on GitHub release, improve GH API error handling ([#&#8203;20999](https://togithub.com/nrwl/nx/pull/20999))
-   **release:** update error message check for npm dist-tags ([#&#8203;20995](https://togithub.com/nrwl/nx/pull/20995))
-   **release:** stage changes when versioning with --projects argument ([#&#8203;21054](https://togithub.com/nrwl/nx/pull/21054))
-   **release:** default changelog git commit and tag true ([#&#8203;21129](https://togithub.com/nrwl/nx/pull/21129))
-   **release:** versionPrefix should default to auto ([#&#8203;21256](https://togithub.com/nrwl/nx/pull/21256))
-   **release:** filtering publish by project or group should exclude task deps ([#&#8203;21231](https://togithub.com/nrwl/nx/pull/21231))
-   **release:** fix --first-release with conventional commits and independent projects ([#&#8203;21320](https://togithub.com/nrwl/nx/pull/21320))
-   **release:** only add nx-release-publish to public packages ([#&#8203;21338](https://togithub.com/nrwl/nx/pull/21338))
-   **release:** disable workspace changelogs in config when not valid ([#&#8203;21341](https://togithub.com/nrwl/nx/pull/21341))
-   **release:** do not restart the daemon when skipLockFileUpdate is set ([#&#8203;21389](https://togithub.com/nrwl/nx/pull/21389))
-   **release:** ensure non-zero exit code is propagated, change missing target handling ([#&#8203;21388](https://togithub.com/nrwl/nx/pull/21388))
-   **remix:** legacy package pointing to incorrect readme ([#&#8203;21113](https://togithub.com/nrwl/nx/pull/21113))
-   **remix:** import of config file should invalidate cache ([#&#8203;21121](https://togithub.com/nrwl/nx/pull/21121))
-   **remix:** required property in schema should be project ([#&#8203;21258](https://togithub.com/nrwl/nx/pull/21258))
-   **remix:** use twStyles as import to prevent conflicts ([#&#8203;21276](https://togithub.com/nrwl/nx/pull/21276))
-   **remix:** tsconfigs were being incorrectly generated causing errors [#&#8203;21002](https://togithub.com/nrwl/nx/issues/21002) ([#&#8203;21387](https://togithub.com/nrwl/nx/pull/21387), [#&#8203;21002](https://togithub.com/nrwl/nx/issues/21002))
-   **repo:** add missing packages to nightly ([#&#8203;20908](https://togithub.com/nrwl/nx/pull/20908))
-   **repo:** update nightly matrix with new packages ([#&#8203;20911](https://togithub.com/nrwl/nx/pull/20911))
-   **repo:** fix version calculation on nx-release ([#&#8203;21382](https://togithub.com/nrwl/nx/pull/21382))
-   **storybook:** do not throw for versions >=7 ([#&#8203;20770](https://togithub.com/nrwl/nx/pull/20770))
-   **storybook:** handle output-dir properly for outputs ([#&#8203;21168](https://togithub.com/nrwl/nx/pull/21168))
-   **storybook:** throw if no project name for angular ([#&#8203;21308](https://togithub.com/nrwl/nx/pull/21308))
-   **storybook:** add storybook-static to gitignore for pcv3 ([#&#8203;21309](https://togithub.com/nrwl/nx/pull/21309))
-   **testing:** avoid overwriting environment variables in nx cypress preset ([#&#8203;20748](https://togithub.com/nrwl/nx/pull/20748))
-   **testing:** run playwright with the correct project option for multiple values ([#&#8203;20850](https://togithub.com/nrwl/nx/pull/20850))
-   **testing:** safely handle circular deps in component testing plugin ([#&#8203;20852](https://togit

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

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://togithub.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- 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:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMjEuMCIsInVwZGF0ZWRJblZlciI6IjM3LjE1My4yIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-02-01 14:33:25 +00:00
LongYinan
b524564223 chore: bump up react-datepicker version to v6 (#5777)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [react-datepicker](https://togithub.com/Hacker0x01/react-datepicker) | [`^5.0.0` -> `^6.0.0`](https://renovatebot.com/diffs/npm/react-datepicker/5.1.0/6.0.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-datepicker/6.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/react-datepicker/6.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/react-datepicker/5.1.0/6.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/react-datepicker/5.1.0/6.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>Hacker0x01/react-datepicker (react-datepicker)</summary>

### [`v6.0.0`](https://togithub.com/Hacker0x01/react-datepicker/releases/tag/v6.0.0): 6.0.0

[Compare Source](https://togithub.com/Hacker0x01/react-datepicker/compare/v5.1.0...v6.0.0)

#### What's Changed

-   Upgrade date-fns to v3 by [@&#8203;ethanve](https://togithub.com/ethanve) in [https://github.com/Hacker0x01/react-datepicker/pull/4481](https://togithub.com/Hacker0x01/react-datepicker/pull/4481)
-   Switch workflows to Node 20 by [@&#8203;martijnrusschen](https://togithub.com/martijnrusschen) in [https://github.com/Hacker0x01/react-datepicker/pull/4490](https://togithub.com/Hacker0x01/react-datepicker/pull/4490)

#### New Contributors

-   [@&#8203;ethanve](https://togithub.com/ethanve) made their first contribution in [https://github.com/Hacker0x01/react-datepicker/pull/4481](https://togithub.com/Hacker0x01/react-datepicker/pull/4481)

**Full Changelog**: https://github.com/Hacker0x01/react-datepicker/compare/v5.1.0...v6.0.0

</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:eyJjcmVhdGVkSW5WZXIiOiIzNy4xNTMuMiIsInVwZGF0ZWRJblZlciI6IjM3LjE1My4yIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-02-01 14:06:11 +00:00
LongYinan
83e7afeb6b chore: exclude oxlint from non-major dependencies group (#5775) 2024-02-01 10:58:09 +00:00
liuyi
2f3c6f104e fix(server): doc upsert without row lock (#5765) 2024-02-01 09:49:02 +00:00
Peng Xiao
7d951a975f fix(core): replace most --affine with cssVar (#5728)
using a [babel plugin](https://gist.github.com/pengx17/49e24ae8a5a609bdaff122ee8c679d1c) to transform all var(--affine-xxx) to cssVar

Some issues:
- tried ast-grep but it seems to be not easy to add imports conditionally
- current work does not work well with ts with types because babel will strip them out
2024-02-01 09:33:11 +00:00
LongYinan
5612424b85 chore: bump up react-datepicker version to v5 (#5691)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [react-datepicker](https://togithub.com/Hacker0x01/react-datepicker) | [`^4.20.0` -> `^5.0.0`](https://renovatebot.com/diffs/npm/react-datepicker/4.23.0/5.1.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/react-datepicker/5.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/react-datepicker/5.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/react-datepicker/4.23.0/5.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/react-datepicker/4.23.0/5.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>Hacker0x01/react-datepicker (react-datepicker)</summary>

### [`v5.1.0`](https://togithub.com/Hacker0x01/react-datepicker/compare/v5.0.0...v5.1.0)

[Compare Source](https://togithub.com/Hacker0x01/react-datepicker/compare/v5.0.0...v5.1.0)

### [`v5.0.0`](https://togithub.com/Hacker0x01/react-datepicker/releases/tag/v5.0.0): 5.0.0

[Compare Source](https://togithub.com/Hacker0x01/react-datepicker/compare/v4.25.0...v5.0.0)

#### Breaking changes

-   Migrate from Popper.js to Floating-UI by [@&#8203;G07cha](https://togithub.com/G07cha) in [https://github.com/Hacker0x01/react-datepicker/pull/4393](https://togithub.com/Hacker0x01/react-datepicker/pull/4393)

#### What's Changed

-   🐛 FIX: readability-isMonthinRange by [@&#8203;mary139](https://togithub.com/mary139) in [https://github.com/Hacker0x01/react-datepicker/pull/4421](https://togithub.com/Hacker0x01/react-datepicker/pull/4421)
-   Fix [#&#8203;4431](https://togithub.com/Hacker0x01/react-datepicker/issues/4431): Update the excludedDate to match the year to check of the isYearDisabled by [@&#8203;balajis-qb](https://togithub.com/balajis-qb) in [https://github.com/Hacker0x01/react-datepicker/pull/4432](https://togithub.com/Hacker0x01/react-datepicker/pull/4432)
-   Fix [#&#8203;4420](https://togithub.com/Hacker0x01/react-datepicker/issues/4420): Update home key and end key navigation in Calendar component by [@&#8203;balajis-qb](https://togithub.com/balajis-qb) in [https://github.com/Hacker0x01/react-datepicker/pull/4430](https://togithub.com/Hacker0x01/react-datepicker/pull/4430)
-   Document [#&#8203;4420](https://togithub.com/Hacker0x01/react-datepicker/issues/4420): 📝 Update the behavior of Home Key and End Key in the README file by [@&#8203;balajis-qb](https://togithub.com/balajis-qb) in [https://github.com/Hacker0x01/react-datepicker/pull/4438](https://togithub.com/Hacker0x01/react-datepicker/pull/4438)
-   Fix [#&#8203;4076](https://togithub.com/Hacker0x01/react-datepicker/issues/4076): Trigger onCalendarClose event and onChange even when the same date is selected as the start and the end date in a date range by [@&#8203;balajis-qb](https://togithub.com/balajis-qb) in [https://github.com/Hacker0x01/react-datepicker/pull/4394](https://togithub.com/Hacker0x01/react-datepicker/pull/4394)
-   Excluded dates message by [@&#8203;dvelazquez1282](https://togithub.com/dvelazquez1282) in [https://github.com/Hacker0x01/react-datepicker/pull/4437](https://togithub.com/Hacker0x01/react-datepicker/pull/4437)
-   Fix [#&#8203;4456](https://togithub.com/Hacker0x01/react-datepicker/issues/4456): Add shift+pageUp key and shift+pageDown key navigation in Calendar component by [@&#8203;balajis-qb](https://togithub.com/balajis-qb) in [https://github.com/Hacker0x01/react-datepicker/pull/4457](https://togithub.com/Hacker0x01/react-datepicker/pull/4457)
-   Fix options passed to date-fns/format by [@&#8203;emilecantin](https://togithub.com/emilecantin) in [https://github.com/Hacker0x01/react-datepicker/pull/4469](https://togithub.com/Hacker0x01/react-datepicker/pull/4469)

#### New Contributors

-   [@&#8203;mary139](https://togithub.com/mary139) made their first contribution in [https://github.com/Hacker0x01/react-datepicker/pull/4421](https://togithub.com/Hacker0x01/react-datepicker/pull/4421)
-   [@&#8203;G07cha](https://togithub.com/G07cha) made their first contribution in [https://github.com/Hacker0x01/react-datepicker/pull/4393](https://togithub.com/Hacker0x01/react-datepicker/pull/4393)
-   [@&#8203;dvelazquez1282](https://togithub.com/dvelazquez1282) made their first contribution in [https://github.com/Hacker0x01/react-datepicker/pull/4437](https://togithub.com/Hacker0x01/react-datepicker/pull/4437)
-   [@&#8203;emilecantin](https://togithub.com/emilecantin) made their first contribution in [https://github.com/Hacker0x01/react-datepicker/pull/4469](https://togithub.com/Hacker0x01/react-datepicker/pull/4469)

**Full Changelog**: https://github.com/Hacker0x01/react-datepicker/compare/v4.25.0...v5.0.0

### [`v4.25.0`](https://togithub.com/Hacker0x01/react-datepicker/releases/tag/v4.25.0): 4.25.0

[Compare Source](https://togithub.com/Hacker0x01/react-datepicker/compare/v4.24.0...v4.25.0)

#### What's Changed

-   feature: Add day parameter to renderMonthContent function by [@&#8203;omarhoumz](https://togithub.com/omarhoumz) in [https://github.com/Hacker0x01/react-datepicker/pull/4405](https://togithub.com/Hacker0x01/react-datepicker/pull/4405)
-   Update 'Local Development' instruction of README.md by [@&#8203;raceStarter](https://togithub.com/raceStarter) in [https://github.com/Hacker0x01/react-datepicker/pull/4391](https://togithub.com/Hacker0x01/react-datepicker/pull/4391)
-   Feature [#&#8203;4091](https://togithub.com/Hacker0x01/react-datepicker/issues/4091) - Make the Calendar Icon clickable by [@&#8203;balajis-qb](https://togithub.com/balajis-qb) in [https://github.com/Hacker0x01/react-datepicker/pull/4417](https://togithub.com/Hacker0x01/react-datepicker/pull/4417)

#### New Contributors

-   [@&#8203;omarhoumz](https://togithub.com/omarhoumz) made their first contribution in [https://github.com/Hacker0x01/react-datepicker/pull/4405](https://togithub.com/Hacker0x01/react-datepicker/pull/4405)
-   [@&#8203;raceStarter](https://togithub.com/raceStarter) made their first contribution in [https://github.com/Hacker0x01/react-datepicker/pull/4391](https://togithub.com/Hacker0x01/react-datepicker/pull/4391)

**Full Changelog**: https://github.com/Hacker0x01/react-datepicker/compare/v4.24.0...v4.25.0

### [`v4.24.0`](https://togithub.com/Hacker0x01/react-datepicker/releases/tag/v4.24.0): 4.24.0

[Compare Source](https://togithub.com/Hacker0x01/react-datepicker/compare/v4.23.0...v4.24.0)

#### What's Changed

-   containerRef div shouldnt affect styling by [@&#8203;joaopaulo-capy](https://togithub.com/joaopaulo-capy) in [https://github.com/Hacker0x01/react-datepicker/pull/4384](https://togithub.com/Hacker0x01/react-datepicker/pull/4384)
-   Fix: reflect the `holidays` prop change by [@&#8203;shimech](https://togithub.com/shimech) in [https://github.com/Hacker0x01/react-datepicker/pull/4373](https://togithub.com/Hacker0x01/react-datepicker/pull/4373)
-   Disable clear button when the component is disabled by [@&#8203;Rafatcb](https://togithub.com/Rafatcb) in [https://github.com/Hacker0x01/react-datepicker/pull/4392](https://togithub.com/Hacker0x01/react-datepicker/pull/4392)

#### New Contributors

-   [@&#8203;joaopaulo-capy](https://togithub.com/joaopaulo-capy) made their first contribution in [https://github.com/Hacker0x01/react-datepicker/pull/4384](https://togithub.com/Hacker0x01/react-datepicker/pull/4384)
-   [@&#8203;shimech](https://togithub.com/shimech) made their first contribution in [https://github.com/Hacker0x01/react-datepicker/pull/4373](https://togithub.com/Hacker0x01/react-datepicker/pull/4373)
-   [@&#8203;Rafatcb](https://togithub.com/Rafatcb) made their first contribution in [https://github.com/Hacker0x01/react-datepicker/pull/4392](https://togithub.com/Hacker0x01/react-datepicker/pull/4392)

**Full Changelog**: https://github.com/Hacker0x01/react-datepicker/compare/v4.23.0...v4.24.0

</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:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMzUuMCIsInVwZGF0ZWRJblZlciI6IjM3LjE1My4yIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-02-01 09:23:45 +00:00
LongYinan
15d32926c3 ci: use free m1 macos runner (#5766)
https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/
2024-02-01 09:03:07 +00:00
Peng Xiao
df8e8051c3 chore: bump electron dependencies (#5770)
to include this fix https://github.com/electron/electron/pull/40994
2024-02-01 08:53:20 +00:00
Joooye_34
338c3001b0 feat: support sign-in with subscription coupon (#5768) 2024-02-01 08:43:47 +00:00
Yifeng Wang
fec2090de5 feat: bump blocksuite (#5767)
Co-authored-by: LongYinan <lynweklm@gmail.com>
2024-02-01 16:28:22 +08:00
LongYinan
61677b2ac4 chore: change bump-blocksuite script to js (#5763)
`./scripts/bump-blocksuite.js`
2024-02-01 07:16:19 +00:00
EYHN
799fa9cfa6 fix(workspace): fix sync stuck (#5762)
* remove MultipleBatchSyncSender
* add timeout (30 seconds) on socket.emit
2024-02-01 06:58:09 +00:00
Chen
aa33bf60d6 fix: update blocksuite local debug config (#5742)
Due to the reconstruction of the blocksuite package, the corresponding local debug configuration needs to be updated.

The related commit: https://github.com/toeverything/blocksuite/pull/6133
2024-02-01 06:49:16 +00:00
DarkSky
1db8019292 feat: ignore case for email (#5754)
fix #5738
2024-02-01 05:05:16 +00:00
LongYinan
349f7c3f15 chore: bump up codecov/codecov-action action to v4 (#5758)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

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

---

### Release Notes

<details>
<summary>codecov/codecov-action (codecov/codecov-action)</summary>

### [`v4`](https://togithub.com/codecov/codecov-action/compare/v3...v4)

[Compare Source](https://togithub.com/codecov/codecov-action/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:eyJjcmVhdGVkSW5WZXIiOiIzNy4xNTMuMiIsInVwZGF0ZWRJblZlciI6IjM3LjE1My4yIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-02-01 02:17:20 +00:00
LongYinan
057796e691 chore: bump up kentaro-m/auto-assign-action action to v2 (#5757)
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [kentaro-m/auto-assign-action](https://togithub.com/kentaro-m/auto-assign-action) | action | major | `v1.2.5` -> `v2.0.0` |

---

### Release Notes

<details>
<summary>kentaro-m/auto-assign-action (kentaro-m/auto-assign-action)</summary>

### [`v2.0.0`](https://togithub.com/kentaro-m/auto-assign-action/releases/tag/v2.0.0)

[Compare Source](https://togithub.com/kentaro-m/auto-assign-action/compare/v1.2.6...v2.0.0)

#### What's Changed

-   chore(deps): update dependency prettier to v3.2.4 by [@&#8203;renovate](https://togithub.com/renovate) in [https://github.com/kentaro-m/auto-assign-action/pull/166](https://togithub.com/kentaro-m/auto-assign-action/pull/166)
-   fix: update Node.js version and action configuration by [@&#8203;kentaro-m](https://togithub.com/kentaro-m) in [https://github.com/kentaro-m/auto-assign-action/pull/170](https://togithub.com/kentaro-m/auto-assign-action/pull/170)
-   fix: update [@&#8203;types/node](https://togithub.com/types/node) version to 20.11.13 by [@&#8203;kentaro-m](https://togithub.com/kentaro-m) in [https://github.com/kentaro-m/auto-assign-action/pull/171](https://togithub.com/kentaro-m/auto-assign-action/pull/171)

**Full Changelog**: https://github.com/kentaro-m/auto-assign-action/compare/v1.2.6...v2.0.0

### [`v1.2.6`](https://togithub.com/kentaro-m/auto-assign-action/releases/tag/v1.2.6)

[Compare Source](https://togithub.com/kentaro-m/auto-assign-action/compare/v1.2.5...v1.2.6)

#### Changes

-   fix: fix security issue ([#&#8203;165](https://togithub.com/kentaro-m/auto-assign-action/issues/165)) [@&#8203;kentaro-m](https://togithub.com/kentaro-m)
-   chore(deps): update dependency prettier to v3.1.1 ([#&#8203;163](https://togithub.com/kentaro-m/auto-assign-action/issues/163)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update actions/setup-node action to v4 ([#&#8203;158](https://togithub.com/kentaro-m/auto-assign-action/issues/158)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update actions/checkout action to v4 ([#&#8203;152](https://togithub.com/kentaro-m/auto-assign-action/issues/152)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency typescript to v5.3.3 ([#&#8203;162](https://togithub.com/kentaro-m/auto-assign-action/issues/162)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency typescript to v5.3.2 ([#&#8203;161](https://togithub.com/kentaro-m/auto-assign-action/issues/161)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency prettier to v3.1.0 ([#&#8203;160](https://togithub.com/kentaro-m/auto-assign-action/issues/160)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update unit test packages ([#&#8203;138](https://togithub.com/kentaro-m/auto-assign-action/issues/138)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency typescript to v5.2.2 ([#&#8203;151](https://togithub.com/kentaro-m/auto-assign-action/issues/151)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency [@&#8203;vercel/ncc](https://togithub.com/vercel/ncc) to v0.38.1 ([#&#8203;156](https://togithub.com/kentaro-m/auto-assign-action/issues/156)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency lint-staged to v13.3.0 ([#&#8203;150](https://togithub.com/kentaro-m/auto-assign-action/issues/150)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency [@&#8203;vercel/ncc](https://togithub.com/vercel/ncc) to v0.38.0 ([#&#8203;149](https://togithub.com/kentaro-m/auto-assign-action/issues/149)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency [@&#8203;octokit/webhooks-types](https://togithub.com/octokit/webhooks-types) to v7.3.1 ([#&#8203;143](https://togithub.com/kentaro-m/auto-assign-action/issues/143)) [@&#8203;renovate](https://togithub.com/renovate)
-   fix(deps): update dependency [@&#8203;actions/core](https://togithub.com/actions/core) to v1.10.1 ([#&#8203;148](https://togithub.com/kentaro-m/auto-assign-action/issues/148)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency prettier to v3.0.3 ([#&#8203;147](https://togithub.com/kentaro-m/auto-assign-action/issues/147)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency node to v16.20.2 ([#&#8203;142](https://togithub.com/kentaro-m/auto-assign-action/issues/142)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency [@&#8203;octokit/webhooks-types](https://togithub.com/octokit/webhooks-types) to v7 ([#&#8203;135](https://togithub.com/kentaro-m/auto-assign-action/issues/135)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency prettier to v3.0.1 ([#&#8203;141](https://togithub.com/kentaro-m/auto-assign-action/issues/141)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency prettier to v3 ([#&#8203;140](https://togithub.com/kentaro-m/auto-assign-action/issues/140)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency typescript to v5.1.6 ([#&#8203;139](https://togithub.com/kentaro-m/auto-assign-action/issues/139)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency lint-staged to v13.2.3 ([#&#8203;137](https://togithub.com/kentaro-m/auto-assign-action/issues/137)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update node.js to v16.20.1 ([#&#8203;134](https://togithub.com/kentaro-m/auto-assign-action/issues/134)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency typescript to v5.1.3 ([#&#8203;132](https://togithub.com/kentaro-m/auto-assign-action/issues/132)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update node.js to v16.20.0 ([#&#8203;126](https://togithub.com/kentaro-m/auto-assign-action/issues/126)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency ts-jest to v29.1.0 ([#&#8203;130](https://togithub.com/kentaro-m/auto-assign-action/issues/130)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency [@&#8203;octokit/webhooks-types](https://togithub.com/octokit/webhooks-types) to v6.11.0 ([#&#8203;129](https://togithub.com/kentaro-m/auto-assign-action/issues/129)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency prettier to v2.8.8 ([#&#8203;131](https://togithub.com/kentaro-m/auto-assign-action/issues/131)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency lint-staged to v13.2.2 ([#&#8203;127](https://togithub.com/kentaro-m/auto-assign-action/issues/127)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency typescript to v5.0.4 ([#&#8203;128](https://togithub.com/kentaro-m/auto-assign-action/issues/128)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency typescript to v5.0.3 ([#&#8203;125](https://togithub.com/kentaro-m/auto-assign-action/issues/125)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency prettier to v2.8.7 ([#&#8203;123](https://togithub.com/kentaro-m/auto-assign-action/issues/123)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency typescript to v5 ([#&#8203;122](https://togithub.com/kentaro-m/auto-assign-action/issues/122)) [@&#8203;renovate](https://togithub.com/renovate)
-   fix(deps): update dependency js-yaml to v3.14.1 ([#&#8203;121](https://togithub.com/kentaro-m/auto-assign-action/issues/121)) [@&#8203;renovate](https://togithub.com/renovate)
-   fix(deps): update dependency [@&#8203;actions/github](https://togithub.com/actions/github) to v5.1.1 ([#&#8203;120](https://togithub.com/kentaro-m/auto-assign-action/issues/120)) [@&#8203;renovate](https://togithub.com/renovate)
-   fix(deps): update dependency [@&#8203;actions/core](https://togithub.com/actions/core) to v1.10.0 ([#&#8203;119](https://togithub.com/kentaro-m/auto-assign-action/issues/119)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update unit test packages to v29.5.0 ([#&#8203;118](https://togithub.com/kentaro-m/auto-assign-action/issues/118)) [@&#8203;renovate](https://togithub.com/renovate)
-   chore(deps): update dependency lint-staged to v13.2.0 ([#&#8203;117](https://togithub.com/kentaro-m/auto-assign-action/issues/117)) [@&#8203;renovate](https://togithub.com/renovate)

</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:eyJjcmVhdGVkSW5WZXIiOiIzNy4xNTMuMiIsInVwZGF0ZWRJblZlciI6IjM3LjE1My4yIiwidGFyZ2V0QnJhbmNoIjoiY2FuYXJ5In0=-->
2024-01-31 15:19:32 +00:00
LongYinan
e26d978b26 fix(server): resolve cherry-pick issue 2024-01-31 22:00:20 +08:00
liuyi
e3b8d0dba4 feat(server): allow pass coupon to checkout session (#5749) 2024-01-31 21:34:22 +08:00
liuyi
f1ccc504b5 fix(server): doc upsert race condition (#5755) 2024-01-31 21:13:29 +08:00
liuyi
26db1d436d refactor(server): server errors (#5741)
standardize the error raising in both GraphQL Resolvers and Controllers.

Now, All user aware errors should be throwed with `HttpException`'s variants, for example `NotFoundException`.

> Directly throwing `GraphQLError` are forbidden.
The GraphQL errorFormatter will handle it automatically and set `code`, `status` in error extensions.

At the same time, the frontend `GraphQLError` should be imported from `@affine/graphql`, which introduce a better error extensions type.

----
controller example:
```js
@Get('/docs/${id}')
doc() {
  // ...
  // imported from '@nestjs/common'
  throw new NotFoundException('Doc is not found.');
  // ...
}
```
the above will response as:
```
status: 404 Not Found
{
  "message": "Doc is not found.",
  "statusCode": 404,
  "error": "Not Found"
}
```

resolver example:
```js
@Mutation()
invite() {
  // ...
  throw new PayloadTooLargeException('Workspace seats is full.')
  // ...
}
```

the above will response as:
```
status: 200 Ok
{
  "data": null,
  "errors": [
    {
      "message": "Workspace seats is full.",
      "extensions": {
        "code": 404,
        "status": "Not Found"
      }
    }
  ]
}
```

for frontend GraphQLError user-friend, a helper function introduced:

```js
import { findGraphQLError } from '@affine/graphql'

fetch(query)
  .catch(errOrArr => {
    const e = findGraphQLError(errOrArr, e => e.extensions.code === 404)
    if (e) {
      // handle
    }
})
```
2024-01-31 08:43:03 +00:00
liuyi
72d9cc1e5b chore(storage): bump y-octo (#5751) 2024-01-31 06:54:33 +00:00
liuyi
db8e49b046 refactor(server): throw Unauthorized instead if user is not signed in (#5746) 2024-01-31 02:12:22 +00:00
Cats Juice
5f3c04b51e fix(core): set createDate to journal's date when journal created (#5701) 2024-01-30 23:11:07 +08:00
Cats Juice
6b350b1735 feat(core): append to today's joruanl via CMDK (#5692) 2024-01-30 23:08:25 +08:00
EYHN
329fc19852 refactor(infra): migrate to new infra (#5565) 2024-01-30 07:16:39 +00:00
EYHN
1e3499c323 feat(infra): page infra (#5618) 2024-01-30 06:31:26 +00:00
EYHN
2e71c980cf feat(infra): new workspace infra (#5617)
This PR copying @affine/workspace into common/infra, and adding definitions for services and unit tests.
2024-01-30 06:31:24 +00:00
EYHN
4f7e0d012d chore: fix vitest error on tinykeys (#5693) 2024-01-30 06:31:21 +00:00
EYHN
88cd83fed1 feat(infra): standard lifecycle service (#5564) 2024-01-30 06:31:19 +00:00
EYHN
b3a8e62984 feat(infra): standard storage service (#5563) 2024-01-30 06:31:15 +00:00
EYHN
48eb6c50e1 feat(infra): di container (#5497)
docs: https://insider.affine.pro/share/055f9c4b-497a-43ec-a1c9-29d5baf184b9/N785YJ__oLMb2fUaLOv-k
2024-01-30 06:31:13 +00:00
EYHN
c9f8e49f75 feat(infra): livedata (#5562)
LiveData is a reactive data type.

## basic usage

@example
```ts
const livedata = new LiveData(0); // create livedata with initial value

livedata.next(1); // update value

console.log(livedata.value); // get current value

livedata.subscribe(v => { // subscribe to value changes
 console.log(v); // 1
});
```

## observable

LiveData is a rxjs observable, you can use rxjs operators.

@example
```ts
new LiveData(0).pipe(
  map(v => v + 1),
  filter(v => v > 1),
  ...
)
```

NOTICE: different from normal observable, LiveData will always emit the latest value when you subscribe to it.

## from observable

LiveData can be created from observable or from other livedata.

@example
```ts
const A = LiveData.from(
  of(1, 2, 3, 4), // from observable
  0 // initial value
);

const B = LiveData.from(
  A.pipe(map(v => 'from a ' + v)), // from other livedata
  '' // initial value
);
```

NOTICE: LiveData.from will not complete when the observable completes, you can use `spreadComplete` option to change
this behavior.

## Why is it called LiveData

This API is very similar to LiveData in Android, as both are based on Observable, so I named it LiveData.
2024-01-30 06:31:11 +00: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
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
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
561 changed files with 18072 additions and 13036 deletions

View File

@@ -64,7 +64,7 @@ const allPackages = [
'packages/frontend/i18n',
'packages/frontend/native',
'packages/frontend/templates',
'packages/frontend/workspace',
'packages/frontend/workspace-impl',
'packages/common/debug',
'packages/common/env',
'packages/common/infra',

View File

@@ -113,6 +113,7 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string graphql.app.payment.stripe.webhookKey="${STRIPE_WEBHOOK_KEY}"`,
`--set graphql.app.experimental.enableJwstCodec=true`,
`--set graphql.app.features.earlyAccessPreview=false`,
`--set graphql.app.features.syncClientVersionCheck=true`,
`--set sync.replicaCount=${syncReplicaCount}`,
`--set-string sync.image.tag="${imageTag}"`,
...serviceAnnotations,

View File

@@ -1,6 +1,6 @@
FROM openresty/openresty:1.21.4.3-0-buster
FROM openresty/openresty:1.25.3.1-0-buster
WORKDIR /app
COPY ./packages/frontend/core/dist/index.html ./dist/index.html
COPY ./packages/frontend/core/dist ./dist
COPY ./.github/deployment/front/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY ./.github/deployment/front/affine.nginx.conf /etc/nginx/conf.d/affine.nginx.conf

View File

@@ -27,9 +27,7 @@ services:
- 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:

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

@@ -39,6 +39,8 @@ spec:
value: "--max-old-space-size=4096"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "affine"
- name: SERVER_FLAVOR
value: "graphql"
- name: AFFINE_ENV
@@ -81,6 +83,8 @@ spec:
value: "{{ .Values.app.captcha.enabled }}"
- name: FEATURES_EARLY_ACCESS_PREVIEW
value: "{{ .Values.app.features.earlyAccessPreview }}"
- name: FEATURES_SYNC_CLIENT_VERSION_CHECK
value: "{{ .Values.app.features.syncClientVersionCheck }}"
- name: OAUTH_EMAIL_SENDER
valueFrom:
secretKeyRef:

View File

@@ -60,6 +60,7 @@ app:
webhookKey: ''
features:
earlyAccessPreview: false
syncClientVersionCheck: false
serviceAccount:
create: true

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

@@ -36,6 +36,8 @@ spec:
value: "{{ .Values.env }}"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "affine"
- name: SERVER_FLAVOR
value: "sync"
- name: NEXTAUTH_URL

5
.github/labeler.yml vendored
View File

@@ -29,11 +29,6 @@ mod:plugin-cli:
- any-glob-to-any-file:
- 'tools/plugin-cli/**/*'
mod:workspace:
- changed-files:
- any-glob-to-any-file:
- 'packages/common/workspace/**/*'
mod:workspace-impl:
- changed-files:
- any-glob-to-any-file:

12
.github/renovate.json vendored
View File

@@ -47,17 +47,22 @@
"groupName": "electron-forge"
},
{
"groupName": "blocksuite-nightly",
"matchPackageNames": ["oxlint"],
"rangeStrategy": "replace",
"groupName": "oxlint"
},
{
"groupName": "blocksuite-canary",
"matchPackagePatterns": ["^@blocksuite"],
"excludePackageNames": ["@blocksuite/icons"],
"rangeStrategy": "replace",
"followTag": "nightly"
"followTag": "canary"
},
{
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch",
"matchPackagePatterns": ["*"],
"excludePackagePatterns": ["^@blocksuite/"],
"excludePackagePatterns": ["^@blocksuite/", "oxlint"],
"matchUpdateTypes": ["minor", "patch"]
},
{
@@ -70,6 +75,7 @@
"commitMessageAction": "bump up",
"commitMessageTopic": "{{depName}} version",
"ignoreDeps": [],
"postUpdateOptions": ["yarnDedupeHighest"],
"lockFileMaintenance": {
"enabled": true,
"extends": ["schedule:weekly"]

View File

@@ -19,7 +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
DEPLOYMENT_TYPE: affine
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -190,7 +190,7 @@ jobs:
run: yarn nx test:coverage @affine/monorepo
- name: Upload unit test coverage results
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/store/lcov.info
@@ -209,8 +209,8 @@ jobs:
spec:
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
- { os: windows-latest, target: x86_64-pc-windows-msvc }
- { os: macos-latest, target: x86_64-apple-darwin }
- { os: macos-latest, target: aarch64-apple-darwin }
- { os: macos-14, target: x86_64-apple-darwin }
- { os: macos-14, target: aarch64-apple-darwin }
steps:
- uses: actions/checkout@v4
@@ -291,6 +291,7 @@ jobs:
runs-on: ubuntu-latest
needs: build-storage
env:
NODE_ENV: test
DISTRIBUTION: browser
services:
postgres:
@@ -353,7 +354,7 @@ jobs:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info
@@ -462,22 +463,21 @@ jobs:
runs-on: ${{ matrix.spec.os }}
strategy:
fail-fast: false
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- {
os: macos-latest,
os: macos-14,
platform: macos,
arch: x64,
target: x86_64-apple-darwin,
test: true,
test: false,
}
- {
os: macos-latest,
os: macos-14,
platform: macos,
arch: arm64,
target: aarch64-apple-darwin,
test: false,
test: true,
}
- {
os: ubuntu-latest,
@@ -534,7 +534,7 @@ jobs:
run: yarn workspace @affine/electron build
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
if: ${{ matrix.spec.os == 'ubuntu-latest' }}
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-desktop e2e
- name: Run desktop tests
@@ -542,7 +542,7 @@ jobs:
run: yarn workspace @affine-test/affine-desktop e2e
- name: Make bundle
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
if: ${{ matrix.spec.os == 'macos-14' && matrix.spec.arch == 'arm64' }}
env:
SKIP_BUNDLE: true
SKIP_WEB_BUILD: true
@@ -550,7 +550,7 @@ jobs:
run: yarn workspace @affine/electron package --platform=darwin --arch=arm64
- name: Output check
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
if: ${{ matrix.spec.os == 'macos-14' && matrix.spec.arch == 'arm64' }}
run: |
yarn workspace @affine/electron exec node --loader ts-node/esm/transpile-only ./scripts/macos-arm64-output-check.ts
@@ -561,3 +561,22 @@ jobs:
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
path: ./test-results
if-no-files-found: ignore
test-done:
needs:
- analyze
- lint
- check-yarn-binary
- e2e-test
- e2e-migration-test
- unit-test
- server-test
- server-e2e-test
- desktop-test
if: always()
runs-on: ubuntu-latest
name: 3, 2, 1 Launch
steps:
- run: exit 1
# Thank you, next https://github.com/vercel/next.js/blob/canary/.github/workflows/build_and_test.yml#L379
if: ${{ always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }}

View File

@@ -9,4 +9,4 @@ jobs:
add-reviews:
runs-on: ubuntu-latest
steps:
- uses: kentaro-m/auto-assign-action@v1.2.5
- uses: kentaro-m/auto-assign-action@v2.0.0

View File

@@ -68,15 +68,13 @@ jobs:
make-distribution:
strategy:
# all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64
# For windows, we need a separate approach
matrix:
spec:
- runner: macos-latest
- runner: macos-14
platform: darwin
arch: x64
target: x86_64-apple-darwin
- runner: macos-latest
- runner: macos-14
platform: darwin
arch: arm64
target: aarch64-apple-darwin
@@ -153,8 +151,6 @@ jobs:
package-distribution-windows:
strategy:
# all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64
# For windows, we need a separate approach
matrix:
spec:
- runner: windows-latest
@@ -228,8 +224,6 @@ jobs:
make-windows-installer:
needs: sign-packaged-artifacts-windows
strategy:
# all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64
# For windows, we need a separate approach
matrix:
spec:
- runner: windows-latest
@@ -281,8 +275,6 @@ jobs:
finalize-installer-windows:
needs: sign-installer-artifacts-windows
strategy:
# all combinations: macos-latest x64, macos-latest arm64, ubuntu-latest x64
# For windows, we need a separate approach
matrix:
spec:
- runner: windows-latest

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Publish
uses: cloudflare/wrangler-action@v3.4.0
uses: cloudflare/wrangler-action@v3.4.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -12,4 +12,4 @@ npmPublishAccess: public
npmPublishRegistry: "https://registry.npmjs.org"
yarnPath: .yarn/releases/yarn-4.0.2.cjs
yarnPath: .yarn/releases/yarn-4.1.0.cjs

1000
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -61,7 +61,7 @@
## Join our community
Before we tell you how to get started with AFFiNE, we'd like to shamelessly plug our awesome user and developer communities across [official social platforms](https://community.affine.pro/c/start-here/)! Once youre familiar with using the software, maybe you will share your wisdom with others and even consider joining the [AFFiNE Ambassador program](https://community.affine.pro/c/start-here/affine-ambassador) to help spread AFFiNE to the world.
Before we tell you how to get started with AFFiNE, we'd like to shamelessly plug our awesome user and developer communities across [official discord server](https://discord.gg/tSNqN4S4)! Once youre familiar with using the software, maybe you will share your wisdom with others and even consider joining the [AFFiNE Ambassador program](https://community.affine.pro/c/start-here/affine-ambassador) to help spread AFFiNE to the world.
## Getting started & staying tuned with us.

View File

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

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",
@@ -60,9 +61,9 @@
"@faker-js/faker": "^8.3.1",
"@istanbuljs/schema": "^0.1.3",
"@magic-works/i18n-codegen": "^0.5.0",
"@nx/vite": "17.2.8",
"@nx/vite": "17.3.1",
"@playwright/test": "^1.41.0",
"@taplo/cli": "^0.5.2",
"@taplo/cli": "^0.7.0",
"@testing-library/react": "^14.1.2",
"@toeverything/infra": "workspace:*",
"@types/affine__env": "workspace:*",
@@ -70,25 +71,25 @@
"@types/node": "^20.9.3",
"@typescript-eslint/eslint-plugin": "^6.13.1",
"@typescript-eslint/parser": "^6.13.1",
"@vanilla-extract/vite-plugin": "^3.9.2",
"@vanilla-extract/vite-plugin": "^4.0.0",
"@vanilla-extract/webpack-plugin": "^2.3.1",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/coverage-istanbul": "1.1.3",
"@vitest/ui": "1.1.3",
"electron": "^28.1.4",
"@vitest/coverage-istanbul": "1.2.2",
"@vitest/ui": "1.2.2",
"electron": "^28.2.1",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-i": "^2.29.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-sonarjs": "^0.23.0",
"eslint-plugin-unicorn": "^50.0.0",
"eslint-plugin-unicorn": "^51.0.0",
"eslint-plugin-unused-imports": "^3.0.0",
"eslint-plugin-vue": "^9.18.1",
"fake-indexeddb": "5.0.2",
"happy-dom": "^13.0.0",
"husky": "^8.0.3",
"husky": "^9.0.6",
"lint-staged": "^15.1.0",
"msw": "^2.0.8",
"nanoid": "^5.0.3",
@@ -105,11 +106,11 @@
"vite-plugin-istanbul": "^5.0.0",
"vite-plugin-static-copy": "^1.0.0",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "1.1.3",
"vitest": "1.2.2",
"vitest-fetch-mock": "^0.2.2",
"vitest-mock-extended": "^1.3.1"
},
"packageManager": "yarn@4.0.2",
"packageManager": "yarn@4.1.0",
"resolutions": {
"vite": "^5.0.6",
"array-buffer-byte-length": "npm:@nolyfill/array-buffer-byte-length@latest",

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": {
@@ -20,7 +20,7 @@
"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",
@@ -38,21 +38,21 @@
"@nestjs/websockets": "^10.2.10",
"@node-rs/argon2": "^1.5.2",
"@node-rs/crc32": "^1.7.2",
"@node-rs/jsonwebtoken": "^0.3.0",
"@node-rs/jsonwebtoken": "^0.4.0",
"@opentelemetry/api": "^1.7.0",
"@opentelemetry/core": "^1.20.0",
"@opentelemetry/exporter-prometheus": "^0.47.0",
"@opentelemetry/exporter-prometheus": "^0.48.0",
"@opentelemetry/exporter-zipkin": "^1.20.0",
"@opentelemetry/host-metrics": "^0.34.0",
"@opentelemetry/instrumentation": "^0.47.0",
"@opentelemetry/instrumentation-graphql": "^0.36.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/host-metrics": "^0.35.0",
"@opentelemetry/instrumentation": "^0.48.0",
"@opentelemetry/instrumentation-graphql": "^0.37.0",
"@opentelemetry/instrumentation-http": "^0.48.0",
"@opentelemetry/instrumentation-ioredis": "^0.37.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.34.0",
"@opentelemetry/instrumentation-socket.io": "^0.36.0",
"@opentelemetry/resources": "^1.20.0",
"@opentelemetry/sdk-metrics": "^1.20.0",
"@opentelemetry/sdk-node": "^0.47.0",
"@opentelemetry/sdk-node": "^0.48.0",
"@opentelemetry/sdk-trace-node": "^1.20.0",
"@opentelemetry/semantic-conventions": "^1.20.0",
"@prisma/client": "^5.7.1",
@@ -162,7 +162,6 @@
"env": {
"TS_NODE_TRANSPILE_ONLY": true,
"TS_NODE_PROJECT": "./tsconfig.json",
"NODE_ENV": "development",
"DEBUG": "affine:*",
"FORCE_COLOR": true,
"DEBUG_COLORS": true

View File

@@ -265,7 +265,9 @@ model Snapshot {
seq Int @default(0) @db.Integer
state Bytes? @db.ByteA
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
// the `updated_at` field will not record the time of record changed,
// but the created time of last seen update that has been merged into snapshot.
updatedAt DateTime @map("updated_at") @db.Timestamptz(6)
@@id([id, workspaceId])
@@map("snapshots")

View File

@@ -13,7 +13,10 @@ const configFiles = [
];
function configCleaner(content) {
return content.replace(/(\/\/#.*$)|(\/\/\s+TODO.*$)/gm, '');
return content.replace(
/(^\/\/#.*$)|(^\/\/\s+TODO.*$)|("use\sstrict";?)|(^.*eslint-disable.*$)/gm,
''
);
}
function prepare() {

View File

@@ -11,6 +11,7 @@ export class AppController {
return {
compatibility: this.config.version,
message: `AFFiNE ${this.config.version} Server`,
type: this.config.type,
flavor: this.config.flavor,
};
}

View File

@@ -29,6 +29,7 @@ import { MailModule } from './fundamentals/mailer';
import { MetricsModule } from './fundamentals/metrics';
import { PrismaModule } from './fundamentals/prisma';
import { SessionModule } from './fundamentals/session';
import { StorageProviderModule } from './fundamentals/storage';
import { RateLimiterModule } from './fundamentals/throttler';
import { WebSocketModule } from './fundamentals/websocket';
import { pluginsMap } from './plugins';
@@ -43,6 +44,7 @@ export const FunctionalityModules = [
RateLimiterModule,
SessionModule,
MailModule,
StorageProviderModule,
];
export class AppModuleBuilder {
@@ -109,7 +111,7 @@ export class AppModuleBuilder {
},
],
imports: this.modules,
controllers: this.config.flavor.selfhosted ? [] : [AppController],
controllers: this.config.isSelfhosted ? [] : [AppController],
})
class AppModule {}
@@ -132,9 +134,9 @@ function buildAppModule() {
// sync server only
.useIf(config => config.flavor.sync, SyncModule)
// main server only
// graphql server only
.useIf(
config => config.flavor.main,
config => config.flavor.graphql,
ServerConfigModule,
WebSocketModule,
GqlModule,
@@ -147,7 +149,7 @@ function buildAppModule() {
// self hosted server only
.useIf(
config => config.flavor.selfhosted,
config => config.isSelfhosted,
ServeStaticModule.forRoot({
rootPath: join('/app', 'static'),
})

View File

@@ -4,9 +4,8 @@ import type { NestExpressApplication } from '@nestjs/platform-express';
import cookieParser from 'cookie-parser';
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { SocketIoAdapter } from './fundamentals';
import { SocketIoAdapterImpl } from './fundamentals/websocket';
import { ExceptionLogger } from './middleware/exception-logger';
import { GlobalExceptionFilter } from './fundamentals';
import { SocketIoAdapter, SocketIoAdapterImpl } from './fundamentals/websocket';
import { serverTimingAndCache } from './middleware/timing';
export async function createApp() {
@@ -29,7 +28,7 @@ export async function createApp() {
})
);
app.useGlobalFilters(new ExceptionLogger());
app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));
app.use(cookieParser());
if (AFFiNE.flavor.sync) {

View File

@@ -3,8 +3,7 @@ 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',
AFFINE_SERVER_HTTPS: ['https', 'boolean'],
DATABASE_URL: 'db.url',
ENABLE_CAPTCHA: ['auth.captcha.enable', 'boolean'],
CAPTCHA_TURNSTILE_SECRET: ['auth.captcha.turnstile.secret', 'string'],
@@ -28,13 +27,15 @@ AFFiNE.ENV_MAP = {
REDIS_SERVER_DATABASE: ['plugins.redis.db', 'int'],
DOC_MERGE_INTERVAL: ['doc.manager.updatePollInterval', 'int'],
DOC_MERGE_USE_JWST_CODEC: [
'doc.manager.experimentalMergeWithJwstCodec',
'doc.manager.experimentalMergeWithYOcto',
'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'],
FEATURES_SYNC_CLIENT_VERSION_CHECK: [
'featureFlags.syncClientVersionCheck',
'boolean',
],
};
export default AFFiNE;

View File

@@ -0,0 +1,46 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
// Custom configurations for AFFiNE Cloud
// ====================================================================================
// Q: WHY THIS FILE EXISTS?
// A: AFFiNE deployment environment may have a lot of custom environment variables,
// which are not suitable to be put in the `affine.ts` file.
// For example, AFFiNE Cloud Clusters are deployed on Google Cloud Platform.
// We need to enable the `gcloud` plugin to make sure the nodes working well,
// but the default selfhost version may not require it.
// So it's not a good idea to put such logic in the common `affine.ts` file.
//
// ```
// if (AFFiNE.deploy) {
// AFFiNE.plugins.use('gcloud');
// }
// ```
// ====================================================================================
const env = process.env;
AFFiNE.metrics.enabled = !AFFiNE.node.test;
if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
AFFiNE.plugins.use('cloudflare-r2', {
accountId: env.R2_OBJECT_STORAGE_ACCOUNT_ID,
credentials: {
accessKeyId: env.R2_OBJECT_STORAGE_ACCESS_KEY_ID!,
secretAccessKey: env.R2_OBJECT_STORAGE_SECRET_ACCESS_KEY!,
},
});
AFFiNE.storage.storages.avatar.provider = 'cloudflare-r2';
AFFiNE.storage.storages.avatar.bucket = 'account-avatar';
AFFiNE.storage.storages.avatar.publicLinkFactory = key =>
`https://avatar.affineassets.com/${key}`;
AFFiNE.storage.storages.blob.provider = 'cloudflare-r2';
AFFiNE.storage.storages.blob.bucket = `workspace-blobs-${
AFFiNE.affine.canary ? 'canary' : 'prod'
}`;
}
AFFiNE.plugins.use('redis');
AFFiNE.plugins.use('payment');
if (AFFiNE.deploy) {
AFFiNE.plugins.use('gcloud');
}

View File

@@ -1,39 +1,117 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
// Custom configurations
const env = process.env;
// TODO(@forehalo): detail explained
// Storage
if (env.R2_OBJECT_STORAGE_ACCOUNT_ID) {
AFFiNE.storage.providers.r2 = {
accountId: env.R2_OBJECT_STORAGE_ACCOUNT_ID,
credentials: {
accessKeyId: env.R2_OBJECT_STORAGE_ACCESS_KEY_ID!,
secretAccessKey: env.R2_OBJECT_STORAGE_SECRET_ACCESS_KEY!,
},
};
AFFiNE.storage.storages.avatar.provider = 'r2';
AFFiNE.storage.storages.avatar.bucket = 'account-avatar';
AFFiNE.storage.storages.avatar.publicLinkFactory = key =>
`https://avatar.affineassets.com/${key}`;
AFFiNE.storage.storages.blob.provider = 'r2';
AFFiNE.storage.storages.blob.bucket = `workspace-blobs-${
AFFiNE.affine.canary ? 'canary' : 'prod'
}`;
}
// Metrics
AFFiNE.metrics.enabled = true;
// Plugins Section Start
AFFiNE.plugins.use('payment', {
stripe: {
keys: {},
apiVersion: '2023-10-16',
},
//
// ###############################################################
// ## AFFiNE Configuration System ##
// ###############################################################
// Here is the file of all AFFiNE configurations that will affect runtime behavior.
// Override any configuration here and it will be merged when starting the server.
// Any changes in this file won't take effect before server restarted.
//
//
// > Configurations merge order
// 1. load environment variables (`.env` if provided, and from system)
// 2. load `src/fundamentals/config/default.ts` for all default settings
// 3. apply `./affine.ts` patches (this file)
// 4. apply `./affine.env.ts` patches
//
//
// ###############################################################
// ## General settings ##
// ###############################################################
//
// /* The unique identity of the server */
// AFFiNE.serverId = 'some-randome-uuid';
//
// /* The name of AFFiNE Server, may show on the UI */
// AFFiNE.serverName = 'Your Cool AFFiNE Selfhosted Cloud';
//
// /* Whether the server is deployed behind a HTTPS proxied environment */
AFFiNE.https = false;
// /* Domain of your server that your server will be available at */
AFFiNE.host = 'localhost';
// /* The local port of your server that will listen on */
AFFiNE.port = 3010;
// /* The sub path of your server */
// /* For example, if you set `AFFiNE.path = '/affine'`, then the server will be available at `${domain}/affine` */
// AFFiNE.path = '/affine';
//
//
// ###############################################################
// ## Database settings ##
// ###############################################################
//
// /* The URL of the database where most of AFFiNE server data will be stored in */
// AFFiNE.db.url = 'postgres://user:passsword@localhost:5432/affine';
//
//
// ###############################################################
// ## Server Function settings ##
// ###############################################################
//
// /* Whether enable metrics and tracing while running the server */
// /* The metrics will be available at `http://localhost:9464/metrics` with [Prometheus] format exported */
// AFFiNE.metrics.enabled = true;
//
// /* GraphQL configurations that control the behavior of the Apollo Server behind */
// /* @see https://www.apollographql.com/docs/apollo-server/api/apollo-server */
// AFFiNE.graphql = {
// /* Path to mount GraphQL API */
// path: '/graphql',
// buildSchemaOptions: {
// numberScalarMode: 'integer',
// },
// /* Whether allow client to query the schema introspection */
// introspection: true,
// /* Whether enable GraphQL Playground UI */
// playground: true,
// }
//
// /* Doc Store & Collaberation */
// /* How long the buffer time of creating a new history snapshot when doc get updated */
// AFFiNE.doc.history.interval = 1000 * 60 * 10; // 10 minutes
//
// /* Use `y-octo` to merge updates at the same time when merging using Yjs */
// AFFiNE.doc.manager.experimentalMergeWithYOcto = true;
//
// /* How often the manager will start a new turn of merging pending updates into doc snapshot */
// AFFiNE.doc.manager.updatePollInterval = 1000 * 3;
//
//
// ###############################################################
// ## Plugins settings ##
// ###############################################################
//
// /* Redis Plugin */
// /* Provide caching and session storing backed by Redis. */
// /* Useful when you deploy AFFiNE server in a cluster. */
AFFiNE.plugins.use('redis', {
/* override options */
});
AFFiNE.plugins.use('redis');
// Plugins Section end
export default AFFiNE;
//
//
// /* Payment Plugin */
AFFiNE.plugins.use('payment', {
stripe: { keys: {}, apiVersion: '2023-10-16' },
});
//
//
// /* Cloudflare R2 Plugin */
// /* Enable if you choose to store workspace blobs or user avatars in Cloudflare R2 Storage Service */
// AFFiNE.plugins.use('cloudflare-r2', {
// accountId: '',
// credentials: {
// accessKeyId: '',
// secretAccessKey: '',
// },
// });
//
// /* AWS S3 Plugin */
// /* Enable if you choose to store workspace blobs or user avatars in AWS S3 Storage Service */
// AFFiNE.plugins.use('aws-s3', {
// credentials: {
// accessKeyId: '',
// secretAccessKey: '',
// })
// /* Update the provider of storages */
// AFFiNE.storage.storages.blob.provider = 'r2';
// AFFiNE.storage.storages.avatar.provider = 'r2';

View File

@@ -4,6 +4,7 @@ import {
Inject,
Injectable,
SetMetadata,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@@ -69,6 +70,10 @@ class AuthGuard implements CanActivate {
'isPublic',
context.getHandler()
);
// FIXME(@forehalo): @Publicable() is duplicated with @CurrentUser() user?: User
// ^ optional
// we can prefetch user session in each request even before this `Guard`
// api can be public, but if user is logged in, we can get user info
const isPublicable = this.reflector.get<boolean>(
'isPublicable',
@@ -94,7 +99,7 @@ class AuthGuard implements CanActivate {
const { body = {}, cookies, status = 200 } = session;
if (!body && !isPublicable) {
return false;
throw new UnauthorizedException('You are not signed in.');
}
// @ts-expect-error body is user here

View File

@@ -244,7 +244,10 @@ export const NextAuthOptionsProvider: FactoryProvider<NextAuthOptions> = {
.count({
where: {
user: {
email,
email: {
equals: email,
mode: 'insensitive',
},
},
feature: {
feature: FeatureType.EarlyAccess,

View File

@@ -107,7 +107,10 @@ export class NextAuthController {
if (email) {
const user = await this.prisma.user.findFirst({
where: {
email,
email: {
equals: email,
mode: 'insensitive',
},
},
});
if (!user) {

View File

@@ -136,7 +136,7 @@ export class AuthService {
return (
!!outcome.success &&
// skip hostname check in dev mode
(this.config.affineEnv === 'dev' || outcome.hostname === this.config.host)
(this.config.node.dev || outcome.hostname === this.config.host)
);
}
@@ -151,7 +151,10 @@ export class AuthService {
async signIn(email: string, password: string): Promise<User> {
const user = await this.prisma.user.findFirst({
where: {
email,
email: {
equals: email,
mode: 'insensitive',
},
},
});
@@ -179,7 +182,10 @@ export class AuthService {
async signUp(name: string, email: string, password: string): Promise<User> {
const user = await this.prisma.user.findFirst({
where: {
email,
email: {
equals: email,
mode: 'insensitive',
},
},
});
@@ -213,7 +219,10 @@ export class AuthService {
async createAnonymousUser(email: string): Promise<User> {
const user = await this.prisma.user.findFirst({
where: {
email,
email: {
equals: email,
mode: 'insensitive',
},
},
});
@@ -241,9 +250,12 @@ export class AuthService {
}
async getUserByEmail(email: string): Promise<User | null> {
return this.prisma.user.findUnique({
return this.prisma.user.findFirst({
where: {
email,
email: {
equals: email,
mode: 'insensitive',
},
},
});
}
@@ -251,7 +263,10 @@ export class AuthService {
async isUserHasPassword(email: string): Promise<boolean> {
const user = await this.prisma.user.findFirst({
where: {
email,
email: {
equals: email,
mode: 'insensitive',
},
},
});
if (!user) {
@@ -261,9 +276,12 @@ export class AuthService {
}
async changePassword(email: string, newPassword: string): Promise<User> {
const user = await this.prisma.user.findUnique({
const user = await this.prisma.user.findFirst({
where: {
email,
email: {
equals: email,
mode: 'insensitive',
},
emailVerified: {
not: null,
},

View File

@@ -1,6 +1,8 @@
import { Module } from '@nestjs/common';
import { Field, ObjectType, Query, registerEnumType } from '@nestjs/graphql';
import { DeploymentType } from '../fundamentals';
export enum ServerFeature {
Payment = 'payment',
}
@@ -9,6 +11,10 @@ registerEnumType(ServerFeature, {
name: 'ServerFeature',
});
registerEnumType(DeploymentType, {
name: 'ServerDeploymentType',
});
const ENABLED_FEATURES: ServerFeature[] = [];
export function ADD_ENABLED_FEATURES(feature: ServerFeature) {
ENABLED_FEATURES.push(feature);
@@ -28,6 +34,9 @@ export class ServerConfigType {
@Field({ description: 'server base url' })
baseUrl!: string;
@Field(() => DeploymentType, { description: 'server type' })
type!: DeploymentType;
/**
* @deprecated
*/
@@ -46,7 +55,11 @@ export class ServerConfigResolver {
name: AFFiNE.serverName,
version: AFFiNE.version,
baseUrl: AFFiNE.baseUrl,
flavor: AFFiNE.flavor.type,
type: AFFiNE.type,
// BACKWARD COMPATIBILITY
// the old flavors contains `selfhosted` but it actually not flavor but deployment type
// this field should be removed after frontend feature flags implemented
flavor: AFFiNE.type,
features: ENABLED_FEATURES,
};
}

View File

@@ -10,7 +10,6 @@ import { chunk } from 'lodash-es';
import { defer, retry } from 'rxjs';
import {
applyUpdate,
decodeStateVector,
Doc,
encodeStateAsUpdate,
encodeStateVector,
@@ -19,6 +18,7 @@ import {
import {
Cache,
CallTimer,
Config,
EventEmitter,
type EventPayload,
@@ -45,36 +45,6 @@ function compare(yBinary: Buffer, jwstBinary: Buffer, strict = false): boolean {
return compare(yBinary, yBinary2, true);
}
/**
* Detect whether rhs state is newer than lhs state.
*
* How could we tell a state is newer:
*
* i. if the state vector size is larger, it's newer
* ii. if the state vector size is same, compare each client's state
*/
function isStateNewer(lhs: Buffer, rhs: Buffer): boolean {
const lhsVector = decodeStateVector(lhs);
const rhsVector = decodeStateVector(rhs);
if (lhsVector.size < rhsVector.size) {
return true;
}
for (const [client, state] of lhsVector) {
const rstate = rhsVector.get(client);
if (!rstate) {
return false;
}
if (state < rstate) {
return true;
}
}
return false;
}
export function isEmptyBuffer(buf: Buffer): boolean {
return (
buf.length === 0 ||
@@ -119,6 +89,7 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
this.destroy();
}
@CallTimer('doc', 'yjs_recover_updates_to_doc')
private recoverDoc(...updates: Buffer[]): Promise<Doc> {
const doc = new Doc();
const chunks = chunk(updates, 10);
@@ -154,11 +125,7 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
const doc = await this.recoverDoc(...updates);
// test jwst codec
if (
this.config.affine.canary &&
this.config.doc.manager.experimentalMergeWithJwstCodec &&
updates.length < 100 /* avoid overloading */
) {
if (this.config.doc.manager.experimentalMergeWithYOcto) {
metrics.jwst.counter('codec_merge_counter').add(1);
const yjsResult = Buffer.from(encodeStateAsUpdate(doc));
let log = false;
@@ -209,7 +176,7 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
}, this.config.doc.manager.updatePollInterval);
this.logger.log('Automation started');
if (this.config.doc.manager.experimentalMergeWithJwstCodec) {
if (this.config.doc.manager.experimentalMergeWithYOcto) {
this.logger.warn(
'Experimental feature enabled: merge updates with jwst codec is enabled'
);
@@ -382,7 +349,7 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
const updates = await this.getUpdates(workspaceId, guid);
if (updates.length) {
const doc = await this.squash(updates, snapshot);
const doc = await this.squash(snapshot, updates);
return Buffer.from(encodeStateVector(doc));
}
@@ -415,7 +382,7 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
// take it ease, we don't want to overload db and or cpu
// if we limit the taken number here,
// user will never see the latest doc if there are too many updates pending to be merged.
take: 100,
take: this.config.doc.manager.maxUpdatesPullCount,
});
// perf(memory): avoid sorting in db
@@ -463,80 +430,92 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
});
}
/**
* @returns whether the snapshot is updated to the latest, `undefined` means the doc to be upserted is outdated.
*/
@CallTimer('doc', 'upsert')
private async upsert(
workspaceId: string,
guid: string,
doc: Doc,
// we always delay the snapshot update to avoid db overload,
// so the value of `updatedAt` will not be accurate to user's real action time
// so the value of auto updated `updatedAt` by db will never be accurate to user's real action time
updatedAt: Date,
initialSeq?: number
seq: number
) {
return this.lockSnapshotForUpsert(workspaceId, guid, async () => {
const blob = Buffer.from(encodeStateAsUpdate(doc));
const blob = Buffer.from(encodeStateAsUpdate(doc));
if (isEmptyBuffer(blob)) {
return false;
if (isEmptyBuffer(blob)) {
return undefined;
}
const state = Buffer.from(encodeStateVector(doc));
// CONCERNS:
// i. Because we save the real user's last seen action time as `updatedAt`,
// it's possible to simply compare the `updatedAt` to determine if the snapshot is older than the one we are going to save.
//
// ii. Prisma doesn't support `upsert` with additional `where` condition along side unique constraint.
// In our case, we need to manually check the `updatedAt` to avoid overriding the newer snapshot.
// where: { id_workspaceId: {}, updatedAt: { lt: updatedAt } }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//
// iii. Only set the seq number when creating the snapshot.
// For updating scenario, the seq number will be updated when updates pushed to db.
try {
const result: { updatedAt: Date }[] = await this.db.$queryRaw`
INSERT INTO "snapshots" ("workspace_id", "guid", "blob", "state", "seq", "created_at", "updated_at")
VALUES (${workspaceId}, ${guid}, ${blob}, ${state}, ${seq}, DEFAULT, ${updatedAt})
ON CONFLICT ("workspace_id", "guid")
DO UPDATE SET "blob" = ${blob}, "state" = ${state}, "updated_at" = ${updatedAt}, "seq" = ${seq}
WHERE "snapshots"."workspace_id" = ${workspaceId} AND "snapshots"."guid" = ${guid} AND "snapshots"."updated_at" <= ${updatedAt}
RETURNING "snapshots"."workspace_id" as "workspaceId", "snapshots"."guid" as "id", "snapshots"."updated_at" as "updatedAt"
`;
// const result = await this.db.snapshot.upsert({
// select: {
// updatedAt: true,
// seq: true,
// },
// where: {
// id_workspaceId: {
// workspaceId,
// id: guid,
// },
// ⬇️ NOT SUPPORTED BY PRISMA YET
// updatedAt: {
// lt: updatedAt,
// },
// },
// update: {
// blob,
// state,
// updatedAt,
// },
// create: {
// workspaceId,
// id: guid,
// blob,
// state,
// updatedAt,
// seq,
// },
// });
// if the condition `snapshot.updatedAt > updatedAt` is true, by which means the snapshot has already been updated by other process,
// the updates has been applied to current `doc` must have been seen by the other process as well.
// The `updatedSnapshot` will be `undefined` in this case.
const updatedSnapshot = result.at(0);
if (!updatedSnapshot) {
return undefined;
}
const state = Buffer.from(encodeStateVector(doc));
return await this.db.$transaction(async db => {
const snapshot = await db.snapshot.findUnique({
where: {
id_workspaceId: {
id: guid,
workspaceId,
},
},
});
// update
if (snapshot) {
// only update if state is newer
if (isStateNewer(snapshot.state ?? Buffer.from([0]), state)) {
await db.snapshot.update({
select: {
seq: true,
},
where: {
id_workspaceId: {
workspaceId,
id: guid,
},
},
data: {
blob,
state,
updatedAt,
},
});
return true;
} else {
return false;
}
} else {
// create
await db.snapshot.create({
select: {
seq: true,
},
data: {
id: guid,
workspaceId,
blob,
state,
seq: initialSeq,
createdAt: updatedAt,
updatedAt,
},
});
return true;
}
});
});
return true;
} catch (e) {
this.logger.error('Failed to upsert snapshot', e);
return false;
}
}
private async _get(
@@ -548,7 +527,7 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
if (updates.length) {
return {
doc: await this.squash(updates, snapshot),
doc: await this.squash(snapshot, updates),
};
}
@@ -559,17 +538,17 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
* Squash updates into a single update and save it as snapshot,
* and delete the updates records at the same time.
*/
private async squash(updates: Update[], snapshot: Snapshot | null) {
@CallTimer('doc', 'squash')
private async squash(snapshot: Snapshot | null, updates: Update[]) {
if (!updates.length) {
throw new Error('No updates to squash');
}
const first = updates[0];
const last = updates[updates.length - 1];
const { id, workspaceId } = first;
const last = updates[updates.length - 1];
const { id, workspaceId } = last;
const doc = await this.applyUpdates(
first.id,
id,
snapshot ? snapshot.blob : Buffer.from([0, 0]),
...updates.map(u => u.blob)
);
@@ -600,19 +579,24 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
);
}
// always delete updates
// the upsert will return false if the state is not newer, so we don't need to worry about it
const { count } = await this.db.update.deleteMany({
where: {
id,
workspaceId,
seq: {
in: updates.map(u => u.seq),
// we will keep the updates only if the upsert failed on unknown reason
// `done === undefined` means the updates is outdated(have already been merged by other process), safe to be deleted
// `done === true` means the upsert is successful, safe to be deleted
if (done !== false) {
// always delete updates
// the upsert will return false if the state is not newer, so we don't need to worry about it
const { count } = await this.db.update.deleteMany({
where: {
id,
workspaceId,
seq: {
in: updates.map(u => u.seq),
},
},
},
});
});
await this.updateCachedUpdatesCount(workspaceId, id, -count);
await this.updateCachedUpdatesCount(workspaceId, id, -count);
}
return doc;
}
@@ -761,18 +745,6 @@ export class DocManager implements OnModuleInit, OnModuleDestroy {
);
}
async lockSnapshotForUpsert<T>(
workspaceId: string,
guid: string,
job: () => Promise<T>
) {
return this.doWithLock(
'doc:manager:snapshot',
`${workspaceId}::${guid}`,
job
);
}
@Cron(CronExpression.EVERY_MINUTE)
async reportUpdatesQueueCount() {
metrics.doc

View File

@@ -50,7 +50,7 @@ export class UnlimitedWorkspaceFeatureConfig extends FeatureConfig {
super(data);
if (this.config.feature !== FeatureType.UnlimitedWorkspace) {
throw new Error('Invalid feature config: type is not EarlyAccess');
throw new Error('Invalid feature config: type is not UnlimitedWorkspace');
}
}
}

View File

@@ -50,7 +50,10 @@ export class FeatureManagementService {
async isEarlyAccessUser(email: string) {
const user = await this.prisma.user.findFirst({
where: {
email,
email: {
equals: email,
mode: 'insensitive',
},
},
});
if (user) {

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,8 +13,7 @@ import { QuotaManagementService } from './storage';
* - quota statistics
*/
@Module({
// FIXME: Quota really need to know `Storage`?
imports: [StorageModule],
imports: [FeatureModule, StorageModule],
providers: [PermissionService, QuotaService, QuotaManagementService],
exports: [QuotaService, QuotaManagementService],
})

View File

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

@@ -71,11 +71,33 @@ export const Quotas: Quota[] = [
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[3].feature,
version: Quotas[3].version,
feature: Quotas[4].feature,
version: Quotas[4].version,
};
export const Quota_ProPlanV1 = {

View File

@@ -1,13 +1,18 @@
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 { QuotaQueryType } from './types';
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
@@ -22,10 +27,10 @@ export class QuotaManagementService {
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,
humanReadableName: quota.feature.humanReadable.name,
};
}
@@ -42,16 +47,60 @@ export class QuotaManagementService {
// get workspace's owner quota and total size of used
// quota was apply to owner's account
async getWorkspaceUsage(workspaceId: string): Promise<QuotaQueryType> {
async getWorkspaceUsage(workspaceId: string): Promise<QuotaBusinessType> {
const { user: owner } =
await this.permissions.getWorkspaceOwner(workspaceId);
if (!owner) throw new NotFoundException('Workspace owner not found');
const { humanReadableName, storageQuota, blobLimit } =
await this.getUserQuota(owner.id);
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);
return { humanReadableName, storageQuota, usedSize, blobLimit };
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) {

View File

@@ -7,6 +7,13 @@ 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',
@@ -26,6 +33,7 @@ const quotaPlan = z.object({
storageQuota: z.number().positive().int(),
historyPeriod: z.number().positive().int(),
memberLimit: z.number().positive().int(),
businessBlobLimit: z.number().positive().int().nullish(),
}),
});
@@ -41,19 +49,46 @@ 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)
humanReadableName!: string;
name!: string;
@Field(() => SafeIntResolver)
blobLimit!: number;
@Field(() => SafeIntResolver)
historyPeriod!: number;
@Field(() => SafeIntResolver)
memberLimit!: number;
@Field(() => SafeIntResolver)
storageQuota!: number;
@Field(() => SafeIntResolver)
usedSize!: number;
@Field(() => HumanReadableQuotaType)
humanReadable!: HumanReadableQuotaType;
@Field(() => SafeIntResolver)
blobLimit!: number;
usedSize!: number;
}
/// ======== utils ========

View File

@@ -6,15 +6,18 @@ import type {
PutObjectMetadata,
StorageProvider,
} from '../../../fundamentals';
import { Config, createStorageProvider, OnEvent } from '../../../fundamentals';
import { Config, OnEvent, StorageProviderFactory } from '../../../fundamentals';
@Injectable()
export class AvatarStorage {
public readonly provider: StorageProvider;
private readonly storageConfig: Config['storage']['storages']['avatar'];
constructor(private readonly config: Config) {
this.provider = createStorageProvider(this.config.storage, 'avatar');
constructor(
private readonly config: Config,
private readonly storageFactory: StorageProviderFactory
) {
this.provider = this.storageFactory.create('avatar');
this.storageConfig = this.config.storage.storages.avatar;
}

View File

@@ -6,10 +6,9 @@ import type {
StorageProvider,
} from '../../../fundamentals';
import {
Config,
createStorageProvider,
EventEmitter,
OnEvent,
StorageProviderFactory,
} from '../../../fundamentals';
@Injectable()
@@ -18,9 +17,9 @@ export class WorkspaceBlobStorage {
constructor(
private readonly event: EventEmitter,
private readonly config: Config
private readonly storageFactory: StorageProviderFactory
) {
this.provider = createStorageProvider(this.config.storage, 'blob');
this.provider = this.storageFactory.create('blob');
}
async put(workspaceId: string, key: string, blob: BlobInputType) {

View File

@@ -1,4 +1,4 @@
enum EventErrorCode {
export enum EventErrorCode {
WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND',
DOC_NOT_FOUND = 'DOC_NOT_FOUND',
NOT_IN_WORKSPACE = 'NOT_IN_WORKSPACE',

View File

@@ -22,6 +22,7 @@ import {
AccessDeniedError,
DocNotFoundError,
EventError,
EventErrorCode,
InternalError,
NotInWorkspaceError,
} from './error';
@@ -112,13 +113,42 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
metrics.socketio.gauge('realtime_connections').record(this.connectionCount);
}
checkVersion(client: Socket, version?: string) {
if (
// @todo(@darkskygit): remove this flag after 0.12 goes stable
AFFiNE.featureFlags.syncClientVersionCheck &&
version !== AFFiNE.version
) {
client.emit('server-version-rejected', {
currentVersion: version,
requiredVersion: AFFiNE.version,
reason: `Client version${
version ? ` ${version}` : ''
} is outdated, please update to ${AFFiNE.version}`,
});
return {
error: new EventError(
EventErrorCode.VERSION_REJECTED,
`Client version ${version} is outdated, please update to ${AFFiNE.version}`
),
};
}
return null;
}
@Auth()
@SubscribeMessage('client-handshake-sync')
async handleClientHandshakeSync(
@CurrentUser() user: UserType,
@MessageBody() workspaceId: string,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string | undefined,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
const versionError = this.checkVersion(client, version);
if (versionError) {
return versionError;
}
const canWrite = await this.permissions.tryCheckWorkspace(
workspaceId,
user.id,
@@ -143,9 +173,15 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@SubscribeMessage('client-handshake-awareness')
async handleClientHandshakeAwareness(
@CurrentUser() user: UserType,
@MessageBody() workspaceId: string,
@MessageBody('workspaceId') workspaceId: string,
@MessageBody('version') version: string | undefined,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
const versionError = this.checkVersion(client, version);
if (versionError) {
return versionError;
}
const canWrite = await this.permissions.tryCheckWorkspace(
workspaceId,
user.id,
@@ -172,29 +208,17 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
@Auth()
@SubscribeMessage('client-handshake')
async handleClientHandShake(
@CurrentUser() user: UserType,
@MessageBody()
workspaceId: string,
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse<{ clientId: string }>> {
const canWrite = await this.permissions.tryCheckWorkspace(
workspaceId,
user.id,
Permission.Write
);
if (canWrite) {
await client.join([`${workspaceId}:sync`, `${workspaceId}:awareness`]);
return {
data: {
clientId: client.id,
},
};
} else {
return {
error: new AccessDeniedError(workspaceId),
};
const versionError = this.checkVersion(client);
if (versionError) {
return versionError;
}
// should unreachable
return {
error: new AccessDeniedError(workspaceId),
};
}
@SubscribeMessage('client-leave-sync')
@@ -227,118 +251,6 @@ export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
}
}
/**
* @deprecated use `client-leave-sync` and `client-leave-awareness` instead
*/
@SubscribeMessage('client-leave')
async handleClientLeave(
@MessageBody() workspaceId: string,
@ConnectedSocket() client: Socket
): Promise<EventResponse> {
if (client.rooms.has(`${workspaceId}:sync`)) {
await client.leave(`${workspaceId}:sync`);
}
if (client.rooms.has(`${workspaceId}:awareness`)) {
await client.leave(`${workspaceId}:awareness`);
}
return {};
}
/**
* This is the old version of the `client-update` event without any data protocol.
* It only exists for backwards compatibility to adapt older clients.
*
* @deprecated
*/
@SubscribeMessage('client-update')
async handleClientUpdateV1(
@MessageBody()
{
workspaceId,
guid,
update,
}: {
workspaceId: string;
guid: string;
update: string;
},
@ConnectedSocket() client: Socket
) {
if (!client.rooms.has(`${workspaceId}:sync`)) {
this.logger.verbose(
`Client ${client.id} tried to push update to workspace ${workspaceId} without joining it first`
);
return;
}
const docId = new DocID(guid, workspaceId);
client
.to(`${docId.workspace}:sync`)
.emit('server-update', { workspaceId, guid, update });
// broadcast to all clients with newer version that only listen to `server-updates`
client
.to(`${docId.workspace}:sync`)
.emit('server-updates', { workspaceId, guid, updates: [update] });
const buf = Buffer.from(update, 'base64');
await this.docManager.push(docId.workspace, docId.guid, buf);
}
/**
* This is the old version of the `doc-load` event without any data protocol.
* It only exists for backwards compatibility to adapt older clients.
*
* @deprecated
*/
@Auth()
@SubscribeMessage('doc-load')
async loadDocV1(
@ConnectedSocket() client: Socket,
@CurrentUser() user: UserType,
@MessageBody()
{
workspaceId,
guid,
stateVector,
}: {
workspaceId: string;
guid: string;
stateVector?: string;
}
): Promise<{ missing: string; state?: string } | false> {
if (!client.rooms.has(`${workspaceId}:sync`)) {
const canRead = await this.permissions.tryCheckWorkspace(
workspaceId,
user.id
);
if (!canRead) {
return false;
}
}
const docId = new DocID(guid, workspaceId);
const doc = await this.docManager.get(docId.workspace, docId.guid);
if (!doc) {
return false;
}
const missing = Buffer.from(
encodeStateAsUpdate(
doc,
stateVector ? Buffer.from(stateVector, 'base64') : undefined
)
).toString('base64');
const state = Buffer.from(encodeStateVector(doc)).toString('base64');
return {
missing,
state,
};
}
@SubscribeMessage('client-update-v2')
async handleClientUpdateV2(
@MessageBody()

View File

@@ -1,4 +1,4 @@
import { BadRequestException, HttpStatus, UseGuards } from '@nestjs/common';
import { BadRequestException, UseGuards } from '@nestjs/common';
import {
Args,
Int,
@@ -8,13 +8,13 @@ import {
Resolver,
} from '@nestjs/graphql';
import type { User } from '@prisma/client';
import { GraphQLError } from 'graphql';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import {
CloudThrottlerGuard,
EventEmitter,
type FileUpload,
PaymentRequiredException,
PrismaService,
Throttle,
} from '../../fundamentals';
@@ -97,14 +97,8 @@ export class UserResolver {
@Args('email') email?: string
) {
if (!email || !(await this.feature.canEarlyAccess(email))) {
return new GraphQLError(
`You don't have early access permission\nVisit https://community.affine.pro/c/insider-general/ for more information`,
{
extensions: {
status: HttpStatus[HttpStatus.PAYMENT_REQUIRED],
code: HttpStatus.PAYMENT_REQUIRED,
},
}
throw new PaymentRequiredException(
`You don't have early access permission\nVisit https://community.affine.pro/c/insider-general/ for more information`
);
}

View File

@@ -7,13 +7,14 @@ export class UsersService {
constructor(private readonly prisma: PrismaService) {}
async findUserByEmail(email: string) {
return this.prisma.user
.findUnique({
where: { email },
})
.catch(() => {
return null;
});
return this.prisma.user.findFirst({
where: {
email: {
equals: email,
mode: 'insensitive',
},
},
});
}
async findUserById(id: string) {

View File

@@ -56,7 +56,7 @@ export class WorkspacesController {
this.logger.warn(`Blob ${workspaceId}/${name} has no metadata`);
}
res.setHeader('cache-control', 'public, max-age=31536000, immutable');
res.setHeader('cache-control', 'public, max-age=2592000, immutable');
body.pipe(res);
}
@@ -106,6 +106,7 @@ export class WorkspacesController {
}
res.setHeader('content-type', 'application/octet-stream');
res.setHeader('cache-control', 'no-cache');
res.send(update);
}
@@ -142,6 +143,7 @@ export class WorkspacesController {
if (history) {
res.setHeader('content-type', 'application/octet-stream');
res.setHeader('cache-control', 'public, max-age=2592000, immutable');
res.send(history.blob);
} else {
throw new NotFoundException('Doc history not found');

View File

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

@@ -299,6 +299,18 @@ export class PermissionService {
return this.tryCheckWorkspace(ws, user, permission);
}
async isPublicPage(ws: string, page: string) {
return this.prisma.workspacePage
.count({
where: {
workspaceId: ws,
pageId: page,
public: true,
},
})
.then(count => count > 0);
}
async publishPage(ws: string, page: string, mode = PublicPageMode.Page) {
return this.prisma.workspacePage.upsert({
where: {
@@ -321,26 +333,19 @@ export class PermissionService {
}
async revokePublicPage(ws: string, page: string) {
const workspacePage = await this.prisma.workspacePage.findUnique({
return this.prisma.workspacePage.upsert({
where: {
workspaceId_pageId: {
workspaceId: ws,
pageId: page,
},
},
});
if (!workspacePage) {
throw new Error('Page is not public');
}
return this.prisma.workspacePage.update({
where: {
workspaceId_pageId: {
workspaceId: ws,
pageId: page,
},
update: {
public: false,
},
data: {
create: {
workspaceId: ws,
pageId: page,
public: false,
},
});

View File

@@ -1,4 +1,9 @@
import { HttpStatus, Logger, UseGuards } from '@nestjs/common';
import {
ForbiddenException,
Logger,
PayloadTooLargeException,
UseGuards,
} from '@nestjs/common';
import {
Args,
Int,
@@ -8,7 +13,6 @@ import {
ResolveField,
Resolver,
} from '@nestjs/graphql';
import { GraphQLError } from 'graphql';
import { SafeIntResolver } from 'graphql-scalars';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
@@ -128,7 +132,7 @@ export class WorkspaceBlobResolver {
Permission.Write
);
const { storageQuota, usedSize, blobLimit } =
const { storageQuota, usedSize, businessBlobLimit } =
await this.quota.getWorkspaceUsage(workspaceId);
const unlimited = await this.feature.hasWorkspaceFeature(
@@ -138,12 +142,7 @@ export class WorkspaceBlobResolver {
const checkExceeded = (recvSize: number) => {
if (!storageQuota) {
throw new GraphQLError('cannot find user quota', {
extensions: {
status: HttpStatus[HttpStatus.FORBIDDEN],
code: HttpStatus.FORBIDDEN,
},
});
throw new ForbiddenException('Cannot find user quota.');
}
const total = usedSize + recvSize;
// only skip total storage check if workspace has unlimited feature
@@ -152,8 +151,10 @@ export class WorkspaceBlobResolver {
`storage size limit exceeded: ${total} > ${storageQuota}`
);
return true;
} else if (recvSize > blobLimit) {
this.logger.log(`blob size limit exceeded: ${recvSize} > ${blobLimit}`);
} else if (recvSize > businessBlobLimit) {
this.logger.log(
`blob size limit exceeded: ${recvSize} > ${businessBlobLimit}`
);
return true;
} else {
return false;
@@ -161,12 +162,9 @@ export class WorkspaceBlobResolver {
};
if (checkExceeded(0)) {
throw new GraphQLError('storage or blob size limit exceeded', {
extensions: {
status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE],
code: HttpStatus.PAYLOAD_TOO_LARGE,
},
});
throw new PayloadTooLargeException(
'Storage or blob size limit exceeded.'
);
}
const buffer = await new Promise<Buffer>((resolve, reject) => {
const stream = blob.createReadStream();
@@ -178,12 +176,7 @@ export class WorkspaceBlobResolver {
const bufferSize = chunks.reduce((acc, cur) => acc + cur.length, 0);
if (checkExceeded(bufferSize)) {
reject(
new GraphQLError('storage or blob size limit exceeded', {
extensions: {
status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE],
code: HttpStatus.PAYLOAD_TOO_LARGE,
},
})
new PayloadTooLargeException('Storage or blob size limit exceeded.')
);
}
});
@@ -192,14 +185,7 @@ export class WorkspaceBlobResolver {
const buffer = Buffer.concat(chunks);
if (checkExceeded(buffer.length)) {
reject(
new GraphQLError('storage limit exceeded', {
extensions: {
status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE],
code: HttpStatus.PAYLOAD_TOO_LARGE,
},
})
);
reject(new PayloadTooLargeException('Storage limit exceeded.'));
} else {
resolve(buffer);
}

View File

@@ -1,4 +1,4 @@
import { ForbiddenException, UseGuards } from '@nestjs/common';
import { BadRequestException, UseGuards } from '@nestjs/common';
import {
Args,
Field,
@@ -111,7 +111,7 @@ export class PagePermissionResolver {
const docId = new DocID(pageId, workspaceId);
if (docId.isWorkspace) {
throw new ForbiddenException('Expect page not to be workspace');
throw new BadRequestException('Expect page not to be workspace');
}
await this.permission.checkWorkspace(
@@ -148,7 +148,7 @@ export class PagePermissionResolver {
const docId = new DocID(pageId, workspaceId);
if (docId.isWorkspace) {
throw new ForbiddenException('Expect page not to be workspace');
throw new BadRequestException('Expect page not to be workspace');
}
await this.permission.checkWorkspace(
@@ -157,6 +157,15 @@ export class PagePermissionResolver {
Permission.Read
);
const isPublic = await this.permission.isPublicPage(
docId.workspace,
docId.guid
);
if (!isPublic) {
throw new BadRequestException('Page is not public');
}
return this.permission.revokePublicPage(docId.workspace, docId.guid);
}
}

View File

@@ -1,8 +1,9 @@
import {
ForbiddenException,
HttpStatus,
InternalServerErrorException,
Logger,
NotFoundException,
PayloadTooLargeException,
UseGuards,
} from '@nestjs/common';
import {
@@ -16,7 +17,6 @@ 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';
@@ -30,7 +30,6 @@ import {
} from '../../../fundamentals';
import { Auth, CurrentUser, Public } from '../../auth';
import { AuthService } from '../../auth/service';
import { FeatureManagementService, FeatureType } from '../../features';
import { QuotaManagementService, QuotaQueryType } from '../../quota';
import { WorkspaceBlobStorage } from '../../storage';
import { UsersService, UserType } from '../../users';
@@ -60,7 +59,6 @@ export class WorkspaceResolver {
private readonly mailer: MailService,
private readonly prisma: PrismaService,
private readonly permissions: PermissionService,
private readonly feature: FeatureManagementService,
private readonly quota: QuotaManagementService,
private readonly users: UsersService,
private readonly event: EventEmitter,
@@ -279,6 +277,7 @@ export class WorkspaceResolver {
id: workspace.id,
workspaceId: workspace.id,
blob: buffer,
updatedAt: new Date(),
},
});
}
@@ -338,26 +337,15 @@ export class WorkspaceResolver {
throw new ForbiddenException('Cannot change owner');
}
const unlimited = await this.feature.hasWorkspaceFeature(
workspaceId,
FeatureType.UnlimitedWorkspace
);
if (!unlimited) {
// member limit check
const [memberCount, quota] = await Promise.all([
this.prisma.workspaceUserPermission.count({
where: { workspaceId },
}),
this.quota.getUserQuota(user.id),
]);
if (memberCount >= quota.memberLimit) {
throw new GraphQLError('Workspace member limit reached', {
extensions: {
status: HttpStatus[HttpStatus.PAYLOAD_TOO_LARGE],
code: HttpStatus.PAYLOAD_TOO_LARGE,
},
});
}
// 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 PayloadTooLargeException('Workspace member limit reached.');
}
let target = await this.users.findUserByEmail(email);
@@ -409,14 +397,8 @@ export class WorkspaceResolver {
`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 new InternalServerErrorException(
'Failed to send invite email. Please try again.'
);
}
}

View File

@@ -8,7 +8,7 @@ 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 (config.isSelfhosted) {
if (
!process.env.AFFINE_ADMIN_EMAIL ||
!process.env.AFFINE_ADMIN_PASSWORD

View File

@@ -1,65 +1,14 @@
import { PrismaClient } from '@prisma/client';
import { FeatureKind } from '../../core/features';
import { Quotas } from '../../core/quota';
import { upsertFeature } from './utils/user-features';
import { upgradeQuotaVersion } from './utils/user-quotas';
export class NewFreePlan1705395933447 {
// do the migration
static async up(db: PrismaClient) {
// add new free plan
await upsertFeature(db, Quotas[3]);
// migrate all free plan users to new free plan
await db.$transaction(async tx => {
const latestFreePlan = await tx.features.findFirstOrThrow({
where: { feature: Quotas[3].feature },
orderBy: { version: 'desc' },
select: { id: true },
});
// find all users that have old free plan
const userIds = await db.user.findMany({
where: {
features: {
every: {
feature: {
type: FeatureKind.Quota,
feature: Quotas[3].feature,
version: { lt: Quotas[3].version },
},
activated: true,
},
},
},
select: { id: true },
});
// deactivate all old quota for the user
await tx.userFeatures.updateMany({
where: {
id: undefined,
userId: {
in: userIds.map(({ id }) => id),
},
feature: {
type: FeatureKind.Quota,
},
activated: true,
},
data: {
activated: false,
},
});
await tx.userFeatures.createMany({
data: userIds.map(({ id: userId }) => ({
userId,
featureId: latestFreePlan.id,
reason: 'free plan 1.0 migration',
activated: true,
})),
});
});
// free plan 1.0
const quota = Quotas[3];
await upgradeQuotaVersion(db, quota, 'free plan 1.0 migration');
}
// revert the migration

View File

@@ -0,0 +1,16 @@
import { PrismaClient } from '@prisma/client';
import { Quotas } from '../../core/quota';
import { upgradeQuotaVersion } from './utils/user-quotas';
export class BusinessBlobLimit1706513866287 {
// do the migration
static async up(db: PrismaClient) {
// free plan 1.1
const quota = Quotas[4];
await upgradeQuotaVersion(db, quota, 'free plan 1.1 migration');
}
// revert the migration
static async down(_db: PrismaClient) {}
}

View File

@@ -0,0 +1,65 @@
import { PrismaClient } from '@prisma/client';
import { FeatureKind } from '../../../core/features';
import { Quota } from '../../../core/quota/types';
import { upsertFeature } from './user-features';
export async function upgradeQuotaVersion(
db: PrismaClient,
quota: Quota,
reason: string
) {
// add new quota
await upsertFeature(db, quota);
// migrate all users that using old quota to new quota
await db.$transaction(async tx => {
const latestQuotaVersion = await tx.features.findFirstOrThrow({
where: { feature: quota.feature },
orderBy: { version: 'desc' },
select: { id: true },
});
// find all users that have old free plan
const userIds = await db.user.findMany({
where: {
features: {
every: {
feature: {
type: FeatureKind.Quota,
feature: quota.feature,
version: { lt: quota.version },
},
activated: true,
},
},
},
select: { id: true },
});
// deactivate all old quota for the user
await tx.userFeatures.updateMany({
where: {
id: undefined,
userId: {
in: userIds.map(({ id }) => id),
},
feature: {
type: FeatureKind.Quota,
},
activated: true,
},
data: {
activated: false,
},
});
await tx.userFeatures.createMany({
data: userIds.map(({ id: userId }) => ({
userId,
featureId: latestQuotaVersion.id,
reason,
activated: true,
})),
});
});
}

View File

@@ -18,18 +18,22 @@ export enum ExternalAccount {
firebase = 'firebase',
}
export type ServerFlavor =
| 'allinone'
| 'main'
// @deprecated
| 'graphql'
| 'sync'
| 'selfhosted';
export type ServerFlavor = 'allinone' | 'graphql' | 'sync';
export type AFFINE_ENV = 'dev' | 'beta' | 'production';
export type NODE_ENV = 'development' | 'test' | 'production';
export enum DeploymentType {
Affine = 'affine',
Selfhosted = 'selfhosted',
}
export type ConfigPaths = LeafPaths<
Omit<
AFFiNEConfig,
| 'ENV_MAP'
| 'version'
| 'type'
| 'isSelfhosted'
| 'flavor'
| 'env'
| 'affine'
@@ -63,27 +67,36 @@ export interface AFFiNEConfig {
*/
readonly version: string;
/**
* Deployment type, AFFiNE Cloud, or Selfhosted
*/
get type(): DeploymentType;
/**
* Fast detect whether currently deployed in a selfhosted environment
*/
get isSelfhosted(): boolean;
/**
* Server flavor
*/
get flavor(): {
type: string;
main: boolean;
graphql: boolean;
sync: boolean;
selfhosted: boolean;
};
/**
* Deployment environment
*/
readonly affineEnv: 'dev' | 'beta' | 'production';
readonly AFFINE_ENV: AFFINE_ENV;
/**
* alias to `process.env.NODE_ENV`
*
* @default 'production'
* @default 'development'
* @env NODE_ENV
*/
readonly env: string;
readonly NODE_ENV: NODE_ENV;
/**
* fast AFFiNE environment judge
@@ -101,6 +114,7 @@ export interface AFFiNEConfig {
dev: boolean;
test: boolean;
};
get deploy(): boolean;
/**
@@ -159,6 +173,7 @@ export interface AFFiNEConfig {
*/
featureFlags: {
earlyAccessPreview: boolean;
syncClientVersionCheck: boolean;
};
/**
@@ -302,11 +317,17 @@ export interface AFFiNEConfig {
updatePollInterval: number;
/**
* Use JwstCodec to merge updates at the same time when merging using Yjs.
* The maximum number of updates that will be pulled from the server at once.
* Existing for avoiding the server to be overloaded when there are too many updates for one doc.
*/
maxUpdatesPullCount: number;
/**
* Use `y-octo` to merge updates at the same time when merging using Yjs.
*
* This is an experimental feature, and aimed to check the correctness of JwstCodec.
*/
experimentalMergeWithJwstCodec: boolean;
experimentalMergeWithYOcto: boolean;
};
history: {
/**

View File

@@ -6,7 +6,14 @@ import { merge } from 'lodash-es';
import parse from 'parse-duration';
import pkg from '../../../package.json' assert { type: 'json' };
import type { AFFiNEConfig, ServerFlavor } from './def';
import {
type AFFINE_ENV,
AFFiNEConfig,
DeploymentType,
type NODE_ENV,
type ServerFlavor,
} from './def';
import { readEnv } from './env';
import { getDefaultAFFiNEStorageConfig } from './storage';
// Don't use this in production
@@ -46,40 +53,62 @@ const jwtKeyPair = (function () {
})();
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
let isHttps: boolean | null = null;
let flavor = (process.env.SERVER_FLAVOR ?? 'allinone') as ServerFlavor;
const NODE_ENV = readEnv<NODE_ENV>('NODE_ENV', 'development', [
'development',
'test',
'production',
]);
const AFFINE_ENV = readEnv<AFFINE_ENV>('AFFINE_ENV', 'dev', [
'dev',
'beta',
'production',
]);
const flavor = readEnv<ServerFlavor>('SERVER_FLAVOR', 'allinone', [
'allinone',
'graphql',
'sync',
]);
const deploymentType = readEnv<DeploymentType>(
'DEPLOYMENT_TYPE',
NODE_ENV === 'development'
? DeploymentType.Affine
: DeploymentType.Selfhosted,
Object.values(DeploymentType)
);
const isSelfhosted = deploymentType === DeploymentType.Selfhosted;
const defaultConfig = {
serverId: 'affine-nestjs-server',
serverName: flavor === 'selfhosted' ? 'Self-Host Cloud' : 'AFFiNE Cloud',
serverName: isSelfhosted ? 'Self-Host Cloud' : 'AFFiNE Cloud',
version: pkg.version,
get type() {
return deploymentType;
},
get isSelfhosted() {
return isSelfhosted;
},
get flavor() {
if (flavor === 'graphql') {
flavor = 'main';
}
return {
type: flavor,
main: flavor === 'main' || flavor === 'allinone',
graphql: flavor === 'graphql' || flavor === 'allinone',
sync: flavor === 'sync' || flavor === 'allinone',
selfhosted: flavor === 'selfhosted',
};
},
ENV_MAP: {},
affineEnv: 'dev',
AFFINE_ENV,
get affine() {
const env = this.affineEnv;
return {
canary: env === 'dev',
beta: env === 'beta',
stable: env === 'production',
canary: AFFINE_ENV === 'dev',
beta: AFFINE_ENV === 'beta',
stable: AFFINE_ENV === 'production',
};
},
env: process.env.NODE_ENV ?? 'development',
NODE_ENV,
get node() {
const env = this.env;
return {
prod: env === 'production',
dev: env === 'development',
test: env === 'test',
prod: NODE_ENV === 'production',
dev: NODE_ENV === 'development',
test: NODE_ENV === 'test',
};
},
get deploy() {
@@ -87,13 +116,9 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
},
featureFlags: {
earlyAccessPreview: false,
syncClientVersionCheck: false,
},
get https() {
return isHttps ?? !this.node.dev;
},
set https(value: boolean) {
isHttps = value;
},
https: false,
host: 'localhost',
port: 3010,
path: '',
@@ -160,7 +185,8 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
manager: {
enableUpdateAutoMerging: flavor !== 'sync',
updatePollInterval: 3000,
experimentalMergeWithJwstCodec: false,
maxUpdatesPullCount: 500,
experimentalMergeWithYOcto: false,
},
history: {
interval: 1000 * 60 * 10 /* 10 mins */,

View File

@@ -48,3 +48,24 @@ export function applyEnvToConfig(rawConfig: AFFiNEConfig) {
}
}
}
export function readEnv<T>(
env: string,
defaultValue: T,
availableValues?: T[]
) {
const value = process.env[env];
if (value === undefined) {
return defaultValue;
}
if (availableValues && !availableValues.includes(value as any)) {
throw new Error(
`Invalid value '${value}' for environment variable ${env}, expected one of [${availableValues.join(
', '
)}]`
);
}
return value as T;
}

View File

@@ -1,37 +1,34 @@
import { homedir } from 'node:os';
import { join } from 'node:path';
import { S3ClientConfigType } from '@aws-sdk/client-s3';
export type StorageProviderType = 'fs' | 'r2' | 's3';
export interface FsStorageConfig {
path: string;
}
export type R2StorageConfig = S3ClientConfigType & {
accountId: string;
};
export type S3StorageConfig = S3ClientConfigType;
export type StorageTargetConfig<Ext = unknown> = {
export interface StorageProvidersConfig {
fs: FsStorageConfig;
}
export type StorageProviderType = keyof StorageProvidersConfig;
export type StorageConfig<Ext = unknown> = {
provider: StorageProviderType;
bucket: string;
} & Ext;
export interface StoragesConfig {
avatar: StorageConfig<{ publicLinkFactory: (key: string) => string }>;
blob: StorageConfig;
}
export interface AFFiNEStorageConfig {
/**
* All providers for object storage
*
* Support different providers for different usage at the same time.
*/
providers: {
fs?: FsStorageConfig;
s3?: S3StorageConfig;
r2?: R2StorageConfig;
};
storages: {
avatar: StorageTargetConfig<{ publicLinkFactory: (key: string) => string }>;
blob: StorageTargetConfig;
};
providers: StorageProvidersConfig;
storages: StoragesConfig;
}
export type StorageProviders = AFFiNEStorageConfig['providers'];

View File

@@ -0,0 +1 @@
export * from './payment-required';

View File

@@ -0,0 +1,10 @@
import { HttpException, HttpStatus } from '@nestjs/common';
export class PaymentRequiredException extends HttpException {
constructor(desc?: string, code: string = 'Payment Required') {
super(
HttpException.createBody(desc ?? code, code, HttpStatus.PAYMENT_REQUIRED),
HttpStatus.PAYMENT_REQUIRED
);
}
}

View File

@@ -3,9 +3,10 @@ import { fileURLToPath } from 'node:url';
import type { ApolloDriverConfig } from '@nestjs/apollo';
import { ApolloDriver } from '@nestjs/apollo';
import { Global, Module } from '@nestjs/common';
import { Global, HttpException, HttpStatus, Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { Request, Response } from 'express';
import { GraphQLError } from 'graphql';
import { Config } from '../config';
import { GQLLoggerPlugin } from './logger-plugin';
@@ -34,7 +35,37 @@ import { GQLLoggerPlugin } from './logger-plugin';
res,
isAdminQuery: false,
}),
includeStacktraceInErrorResponses: !config.node.prod,
plugins: [new GQLLoggerPlugin()],
formatError: (formattedError, error) => {
// @ts-expect-error allow assign
formattedError.extensions ??= {};
if (
error instanceof GraphQLError &&
error.originalError instanceof HttpException
) {
const statusCode = error.originalError.getStatus();
const statusName = HttpStatus[statusCode];
// originally be 'INTERNAL_SERVER_ERROR'
formattedError.extensions['code'] = statusCode;
formattedError.extensions['status'] = statusName;
delete formattedError.extensions['originalError'];
return formattedError;
} else {
// @ts-expect-error allow assign
formattedError.message = 'Internal Server Error';
formattedError.extensions['code'] =
HttpStatus.INTERNAL_SERVER_ERROR;
formattedError.extensions['status'] =
HttpStatus[HttpStatus.INTERNAL_SERVER_ERROR];
}
return formattedError;
},
};
},
inject: [Config],

View File

@@ -4,7 +4,7 @@ import {
GraphQLRequestListener,
} from '@apollo/server';
import { Plugin } from '@nestjs/apollo';
import { Logger } from '@nestjs/common';
import { HttpException, Logger } from '@nestjs/common';
import { Response } from 'express';
import { metrics } from '../metrics/metrics';
@@ -27,28 +27,44 @@ export class GQLLoggerPlugin implements ApolloServerPlugin {
metrics.gql.counter('query_counter').add(1, { operation });
const start = Date.now();
function endTimer() {
return Date.now() - start;
}
return Promise.resolve({
willSendResponse: () => {
const costInMilliseconds = Date.now() - start;
res.setHeader(
'Server-Timing',
`gql;dur=${costInMilliseconds};desc="GraphQL"`
);
metrics.gql
.histogram('query_duration')
.record(costInMilliseconds, { operation });
const time = endTimer();
res.setHeader('Server-Timing', `gql;dur=${time};desc="GraphQL"`);
metrics.gql.histogram('query_duration').record(time, { operation });
return Promise.resolve();
},
didEncounterErrors: () => {
const costInMilliseconds = Date.now() - start;
res.setHeader(
'Server-Timing',
`gql;dur=${costInMilliseconds};desc="GraphQL ${operation}"`
);
metrics.gql
.histogram('query_duration')
.record(costInMilliseconds, { operation });
didEncounterErrors: ctx => {
metrics.gql.counter('query_error_counter').add(1, { operation });
ctx.errors.forEach(err => {
// only log non-user errors
let msg: string | undefined;
if (!err.originalError) {
msg = err.toString();
} else {
const originalError = err.originalError;
// do not log client errors, and put more information in the error extensions.
if (!(originalError instanceof HttpException)) {
if (originalError.cause && originalError.cause instanceof Error) {
msg = originalError.cause.stack ?? originalError.cause.message;
} else {
msg = originalError.stack ?? originalError.message;
}
}
}
if (msg) {
this.logger.error('GraphQL Unhandled Error', msg);
}
});
return Promise.resolve();
},
});

View File

@@ -9,15 +9,22 @@ export {
applyEnvToConfig,
Config,
type ConfigPaths,
DeploymentType,
getDefaultAFFiNEStorageConfig,
} from './config';
export * from './error';
export { EventEmitter, type EventPayload, OnEvent } from './event';
export { MailService } from './mailer';
export { CallCounter, CallTimer, metrics } from './metrics';
export { getOptionalModuleMetadata, OptionalModule } from './nestjs';
export {
getOptionalModuleMetadata,
GlobalExceptionFilter,
OptionalModule,
} from './nestjs';
export { PrismaService } from './prisma';
export { SessionService } from './session';
export * from './storage';
export { type StorageProvider, StorageProviderFactory } from './storage';
export { AuthThrottlerGuard, CloudThrottlerGuard, Throttle } from './throttler';
export {
getRequestFromHost,
@@ -25,4 +32,3 @@ export {
getRequestResponseFromHost,
} from './utils/request';
export type * from './utils/types';
export { SocketIoAdapter } from './websocket';

View File

@@ -1,28 +1,48 @@
import { Global, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import {
Global,
Module,
OnModuleDestroy,
OnModuleInit,
Provider,
} from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { NodeSDK } from '@opentelemetry/sdk-node';
import { Config, parseEnvValue } from '../config';
import { createSDK, registerCustomMetrics } from './opentelemetry';
import { Config } from '../config';
import {
LocalOpentelemetryFactory,
OpentelemetryFactory,
registerCustomMetrics,
} from './opentelemetry';
const factorProvider: Provider = {
provide: OpentelemetryFactory,
useFactory: (config: Config) => {
return config.metrics.enabled ? new LocalOpentelemetryFactory() : null;
},
inject: [Config],
};
@Global()
@Module({})
@Module({
providers: [factorProvider],
exports: [factorProvider],
})
export class MetricsModule implements OnModuleInit, OnModuleDestroy {
private sdk: NodeSDK | null = null;
constructor(private readonly config: Config) {}
constructor(private readonly ref: ModuleRef) {}
onModuleInit() {
if (
this.config.metrics.enabled &&
!parseEnvValue(process.env.DISABLE_TELEMETRY, 'boolean')
) {
this.sdk = createSDK();
const factor = this.ref.get(OpentelemetryFactory, { strict: false });
if (factor) {
this.sdk = factor.create();
this.sdk.start();
registerCustomMetrics();
}
}
async onModuleDestroy() {
if (this.config.metrics.enabled && this.sdk) {
if (this.sdk) {
await this.sdk.shutdown();
}
}
@@ -30,3 +50,4 @@ export class MetricsModule implements OnModuleInit, OnModuleDestroy {
export * from './metrics';
export * from './utils';
export { OpentelemetryFactory };

View File

@@ -1,6 +1,4 @@
import { MetricExporter } from '@google-cloud/opentelemetry-cloud-monitoring-exporter';
import { TraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter';
import { GcpDetectorSync } from '@google-cloud/opentelemetry-resource-util';
import { OnModuleDestroy } from '@nestjs/common';
import { metrics } from '@opentelemetry/api';
import {
CompositePropagator,
@@ -18,16 +16,13 @@ import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'
import { SocketIoInstrumentation } from '@opentelemetry/instrumentation-socket.io';
import { Resource } from '@opentelemetry/resources';
import {
ConsoleMetricExporter,
type MeterProvider,
MetricProducer,
MetricReader,
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
BatchSpanProcessor,
ConsoleSpanExporter,
SpanExporter,
TraceIdRatioBasedSampler,
} from '@opentelemetry/sdk-trace-node';
@@ -38,7 +33,7 @@ import { PrismaMetricProducer } from './prisma';
const { PrismaInstrumentation } = prismaInstrument;
abstract class OpentelemetryFactor {
export abstract class OpentelemetryFactory {
abstract getMetricReader(): MetricReader;
abstract getSpanExporter(): SpanExporter;
@@ -59,7 +54,7 @@ abstract class OpentelemetryFactor {
getResource() {
return new Resource({
[SemanticResourceAttributes.K8S_NAMESPACE_NAME]: AFFiNE.affineEnv,
[SemanticResourceAttributes.K8S_NAMESPACE_NAME]: AFFiNE.AFFINE_ENV,
[SemanticResourceAttributes.SERVICE_NAME]: AFFiNE.flavor.type,
[SemanticResourceAttributes.SERVICE_VERSION]: AFFiNE.version,
});
@@ -85,32 +80,20 @@ abstract class OpentelemetryFactor {
}
}
class GCloudOpentelemetryFactor extends OpentelemetryFactor {
override getResource(): Resource {
return super.getResource().merge(new GcpDetectorSync().detect());
export class LocalOpentelemetryFactory
extends OpentelemetryFactory
implements OnModuleDestroy
{
private readonly metricsExporter = new PrometheusExporter({
metricProducers: this.getMetricsProducers(),
});
async onModuleDestroy() {
await this.metricsExporter.shutdown();
}
override getMetricReader(): MetricReader {
return new PeriodicExportingMetricReader({
exportIntervalMillis: 30000,
exportTimeoutMillis: 10000,
exporter: new MetricExporter({
prefix: 'custom.googleapis.com',
}),
metricProducers: this.getMetricsProducers(),
});
}
override getSpanExporter(): SpanExporter {
return new TraceExporter();
}
}
class LocalOpentelemetryFactor extends OpentelemetryFactor {
override getMetricReader(): MetricReader {
return new PrometheusExporter({
metricProducers: this.getMetricsProducers(),
});
return this.metricsExporter;
}
override getSpanExporter(): SpanExporter {
@@ -118,33 +101,6 @@ class LocalOpentelemetryFactor extends OpentelemetryFactor {
}
}
class DebugOpentelemetryFactor extends OpentelemetryFactor {
override getMetricReader(): MetricReader {
return new PeriodicExportingMetricReader({
exporter: new ConsoleMetricExporter(),
metricProducers: this.getMetricsProducers(),
});
}
override getSpanExporter(): SpanExporter {
return new ConsoleSpanExporter();
}
}
// TODO(@forehalo): make it configurable
export function createSDK() {
let factor: OpentelemetryFactor | null = null;
if (process.env.NODE_ENV === 'production') {
factor = new GCloudOpentelemetryFactor();
} else if (process.env.DEBUG_METRICS) {
factor = new DebugOpentelemetryFactor();
} else {
factor = new LocalOpentelemetryFactor();
}
return factor?.create();
}
function getMeterProvider() {
return metrics.getMeterProvider();
}

View File

@@ -0,0 +1,25 @@
import { ArgumentsHost, Catch, HttpException } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { GqlContextType } from '@nestjs/graphql';
import { Response } from 'express';
@Catch()
export class GlobalExceptionFilter extends BaseExceptionFilter {
override catch(exception: Error, host: ArgumentsHost) {
// with useGlobalFilters, the context is always HTTP
if (host.getType<GqlContextType>() === 'graphql') {
// let Graphql LoggerPlugin handle it
// see '../graphql/logger-plugin.ts'
throw exception;
} else {
if (exception instanceof HttpException) {
const res = host.switchToHttp().getResponse<Response>();
res.status(exception.getStatus()).send(exception.getResponse());
return;
} else {
super.catch(exception, host);
}
}
}
}

View File

@@ -1 +1,2 @@
export * from './exception';
export * from './optional-module';

View File

@@ -1,38 +1,24 @@
import { createRequire } from 'node:module';
import { Global, Module } from '@nestjs/common';
export const StorageProvide = Symbol('Storage');
import { registerStorageProvider, StorageProviderFactory } from './providers';
import { FsStorageProvider } from './providers/fs';
let storageModule: typeof import('@affine/storage');
try {
storageModule = await import('@affine/storage');
} catch {
const require = createRequire(import.meta.url);
storageModule =
process.arch === 'arm64'
? require('../../../storage.arm64.node')
: process.arch === 'arm'
? require('../../../storage.armv7.node')
: require('../../../storage.node');
}
registerStorageProvider('fs', (config, bucket) => {
if (!config.storage.providers.fs) {
throw new Error('Missing fs storage provider configuration');
}
export { storageModule as OctoBaseStorageModule };
return new FsStorageProvider(config.storage.providers.fs, bucket);
});
export const mergeUpdatesInApplyWay = storageModule.mergeUpdatesInApplyWay;
export const verifyChallengeResponse = async (
response: any,
bits: number,
resource: string
) => {
if (typeof response !== 'string' || !response || !resource) return false;
return storageModule.verifyChallengeResponse(response, bits, resource);
};
export const mintChallengeResponse = async (resource: string, bits: number) => {
if (!resource) return null;
return storageModule.mintChallengeResponse(resource, bits);
};
@Global()
@Module({
providers: [StorageProviderFactory],
exports: [StorageProviderFactory],
})
export class StorageProviderModule {}
export * from './native';
export type {
BlobInputType,
BlobOutputType,
@@ -41,5 +27,5 @@ export type {
PutObjectMetadata,
StorageProvider,
} from './providers';
export { createStorageProvider } from './providers';
export { toBuffer } from './providers/utils';
export { registerStorageProvider, StorageProviderFactory } from './providers';
export { autoMetadata, toBuffer } from './providers/utils';

View File

@@ -0,0 +1,30 @@
import { createRequire } from 'node:module';
let storageModule: typeof import('@affine/storage');
try {
storageModule = await import('@affine/storage');
} catch {
const require = createRequire(import.meta.url);
storageModule =
process.arch === 'arm64'
? require('../../../storage.arm64.node')
: process.arch === 'arm'
? require('../../../storage.armv7.node')
: require('../../../storage.node');
}
export const mergeUpdatesInApplyWay = storageModule.mergeUpdatesInApplyWay;
export const verifyChallengeResponse = async (
response: any,
bits: number,
resource: string
) => {
if (typeof response !== 'string' || !response || !resource) return false;
return storageModule.verifyChallengeResponse(response, bits, resource);
};
export const mintChallengeResponse = async (resource: string, bits: number) => {
if (!resource) return null;
return storageModule.mintChallengeResponse(resource, bits);
};

View File

@@ -1,34 +1,37 @@
import { AFFiNEStorageConfig, Storages } from '../../config/storage';
import { FsStorageProvider } from './fs';
import { Injectable } from '@nestjs/common';
import { Config } from '../../config';
import type { StorageProviderType, Storages } from '../../config/storage';
import type { StorageProvider } from './provider';
import { R2StorageProvider } from './r2';
import { S3StorageProvider } from './s3';
export function createStorageProvider(
config: AFFiNEStorageConfig,
storage: Storages
): StorageProvider {
const storageConfig = config.storages[storage];
const providerConfig = config.providers[storageConfig.provider] as any;
if (!providerConfig) {
throw new Error(
`Failed to create ${storageConfig.provider} storage, configuration not correctly set`
);
const availableProviders = new Map<
StorageProviderType,
(config: Config, bucket: string) => StorageProvider
>();
export function registerStorageProvider(
type: StorageProviderType,
providerFactory: (config: Config, bucket: string) => StorageProvider
) {
availableProviders.set(type, providerFactory);
}
@Injectable()
export class StorageProviderFactory {
constructor(private readonly config: Config) {}
create(storage: Storages): StorageProvider {
const storageConfig = this.config.storage.storages[storage];
const providerFactory = availableProviders.get(storageConfig.provider);
if (!providerFactory) {
throw new Error(
`Unknown storage provider type: ${storageConfig.provider}`
);
}
return providerFactory(this.config, storageConfig.bucket);
}
if (storageConfig.provider === 's3') {
return new S3StorageProvider(providerConfig, storageConfig.bucket);
}
if (storageConfig.provider === 'r2') {
return new R2StorageProvider(providerConfig, storageConfig.bucket);
}
if (storageConfig.provider === 'fs') {
return new FsStorageProvider(providerConfig, storageConfig.bucket);
}
throw new Error(`Unknown storage provider type: ${storageConfig.provider}`);
}
export type * from './provider';

View File

@@ -1,14 +1,16 @@
/// <reference types="./global.d.ts" />
// keep the config import at the top
// eslint-disable-next-line simple-import-sort/imports
import './prelude';
import { Logger } from '@nestjs/common';
import { createApp } from './app';
const app = await createApp();
const listeningHost = AFFiNE.deploy ? '0.0.0.0' : 'localhost';
await app.listen(AFFiNE.port, listeningHost);
console.log(
`AFFiNE Server has been started on http://${listeningHost}:${AFFiNE.port}.`
);
console.log(`And the public server should be recognized as ${AFFiNE.baseUrl}`);
const logger = new Logger('App');
logger.log(`AFFiNE Server is running in [${AFFiNE.type}] mode`);
logger.log(`Listening on http://${listeningHost}:${AFFiNE.port}`);
logger.log(`And the public server should be recognized as ${AFFiNE.baseUrl}`);

View File

@@ -1,53 +0,0 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
Logger,
NotFoundException,
} from '@nestjs/common';
import { GqlContextType } from '@nestjs/graphql';
import { Request, Response } from 'express';
const TrivialExceptions = [NotFoundException];
export const REQUEST_ID_HEADER = 'x-request-id';
@Catch()
export class ExceptionLogger implements ExceptionFilter {
private readonly logger = new Logger('ExceptionLogger');
catch(exception: Error, host: ArgumentsHost) {
// with useGlobalFilters, the context is always HTTP
const ctx = host.switchToHttp();
const request = ctx.getRequest<Request>();
const requestId = request?.header(REQUEST_ID_HEADER);
const shouldVerboseLog = !TrivialExceptions.some(
e => exception instanceof e
);
this.logger.error(
new Error(
`${requestId ? `requestId-${requestId}: ` : ''}${exception.message}${
shouldVerboseLog ? '\n' + exception.stack : ''
}`,
{ cause: exception }
)
);
if (host.getType<GqlContextType>() === 'graphql') {
return;
}
const response = ctx.getResponse<Response>();
if (exception instanceof HttpException) {
response.status(exception.getStatus()).json(exception.getResponse());
} else {
response.status(500).json({
statusCode: 500,
error: exception.message,
});
}
}
}

View File

@@ -21,7 +21,5 @@ export const serverTimingAndCache = (
res.setHeader('Server-Timing', serverTimingValue);
});
res.setHeader('Cache-Control', 'max-age=0, private, must-revalidate');
next();
};

View File

@@ -1,10 +1,15 @@
import { GCloudConfig } from './gcloud/config';
import { PaymentConfig } from './payment';
import { RedisOptions } from './redis';
import { R2StorageConfig, S3StorageConfig } from './storage';
declare module '../fundamentals/config' {
interface PluginsConfig {
readonly payment: PaymentConfig;
readonly redis: RedisOptions;
readonly gcloud: GCloudConfig;
readonly 'cloudflare-r2': R2StorageConfig;
readonly 'aws-s3': S3StorageConfig;
}
export type AvailablePlugins = keyof PluginsConfig;

View File

@@ -0,0 +1 @@
export interface GCloudConfig {}

View File

@@ -0,0 +1,10 @@
import { Global } from '@nestjs/common';
import { OptionalModule } from '../../fundamentals';
import { GCloudMetrics } from './metrics';
@Global()
@OptionalModule({
imports: [GCloudMetrics],
})
export class GCloudModule {}

View File

@@ -0,0 +1,46 @@
import { MetricExporter } from '@google-cloud/opentelemetry-cloud-monitoring-exporter';
import { TraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter';
import { GcpDetectorSync } from '@google-cloud/opentelemetry-resource-util';
import { Global, Provider } from '@nestjs/common';
import { Resource } from '@opentelemetry/resources';
import {
MetricReader,
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { SpanExporter } from '@opentelemetry/sdk-trace-node';
import { OptionalModule } from '../../fundamentals';
import { OpentelemetryFactory } from '../../fundamentals/metrics';
export class GCloudOpentelemetryFactory extends OpentelemetryFactory {
override getResource(): Resource {
return super.getResource().merge(new GcpDetectorSync().detect());
}
override getMetricReader(): MetricReader {
return new PeriodicExportingMetricReader({
exportIntervalMillis: 30000,
exportTimeoutMillis: 10000,
exporter: new MetricExporter({
prefix: 'custom.googleapis.com',
}),
metricProducers: this.getMetricsProducers(),
});
}
override getSpanExporter(): SpanExporter {
return new TraceExporter();
}
}
const factorProvider: Provider = {
provide: OpentelemetryFactory,
useFactory: () => new GCloudOpentelemetryFactory(),
};
@Global()
@OptionalModule({
if: config => config.metrics.enabled,
overrides: [factorProvider],
})
export class GCloudMetrics {}

View File

@@ -1,8 +1,13 @@
import type { AvailablePlugins } from '../fundamentals/config';
import { GCloudModule } from './gcloud';
import { PaymentModule } from './payment';
import { RedisModule } from './redis';
import { AwsS3Module, CloudflareR2Module } from './storage';
export const pluginsMap = new Map<AvailablePlugins, AFFiNEModule>([
['payment', PaymentModule],
['redis', RedisModule],
['gcloud', GCloudModule],
['cloudflare-r2', CloudflareR2Module],
['aws-s3', AwsS3Module],
]);

View File

@@ -23,6 +23,7 @@ import { StripeWebhook } from './webhook';
// 'plugins.payment.stripe.keys.webhookKey',
// ],
contributesTo: ServerFeature.Payment,
if: config => config.flavor.graphql,
})
export class PaymentModule {}

View File

@@ -1,8 +1,13 @@
import { HttpStatus } from '@nestjs/common';
import {
BadGatewayException,
ForbiddenException,
InternalServerErrorException,
} from '@nestjs/common';
import {
Args,
Context,
Field,
InputType,
Int,
Mutation,
ObjectType,
@@ -13,7 +18,6 @@ import {
Resolver,
} from '@nestjs/graphql';
import type { User, UserInvoice, UserSubscription } from '@prisma/client';
import { GraphQLError } from 'graphql';
import { groupBy } from 'lodash-es';
import { Auth, CurrentUser, Public } from '../../core/auth';
@@ -125,6 +129,31 @@ class UserInvoiceType implements Partial<UserInvoice> {
updatedAt!: Date;
}
@InputType()
class CreateCheckoutSessionInput {
@Field(() => SubscriptionRecurring, {
nullable: true,
defaultValue: SubscriptionRecurring.Yearly,
})
recurring!: SubscriptionRecurring;
@Field(() => SubscriptionPlan, {
nullable: true,
defaultValue: SubscriptionPlan.Pro,
})
plan!: SubscriptionPlan;
@Field(() => String, { nullable: true })
coupon!: string | null;
@Field(() => String, { nullable: true })
successCallbackLink!: string | null;
// @FIXME(forehalo): we should put this field in the header instead of as a explicity args
@Field(() => String)
idempotencyKey!: string;
}
@Auth()
@Resolver(() => UserSubscriptionType)
export class SubscriptionResolver {
@@ -164,12 +193,9 @@ export class SubscriptionResolver {
);
if (!yearly || !monthly) {
throw new GraphQLError('The prices are not configured correctly', {
extensions: {
status: HttpStatus[HttpStatus.BAD_GATEWAY],
code: HttpStatus.BAD_GATEWAY,
},
});
throw new InternalServerErrorException(
'The prices are not configured correctly.'
);
}
return {
@@ -182,7 +208,11 @@ export class SubscriptionResolver {
});
}
/**
* @deprecated
*/
@Mutation(() => String, {
deprecationReason: 'use `createCheckoutSession` instead',
description: 'Create a subscription checkout link of stripe',
})
async checkout(
@@ -193,18 +223,39 @@ export class SubscriptionResolver {
) {
const session = await this.service.createCheckoutSession({
user,
plan: SubscriptionPlan.Pro,
recurring,
redirectUrl: `${this.config.baseUrl}/upgrade-success`,
idempotencyKey,
});
if (!session.url) {
throw new GraphQLError('Failed to create checkout session', {
extensions: {
status: HttpStatus[HttpStatus.BAD_GATEWAY],
code: HttpStatus.BAD_GATEWAY,
},
});
throw new BadGatewayException('Failed to create checkout session.');
}
return session.url;
}
@Mutation(() => String, {
description: 'Create a subscription checkout link of stripe',
})
async createCheckoutSession(
@CurrentUser() user: User,
@Args({ name: 'input', type: () => CreateCheckoutSessionInput })
input: CreateCheckoutSessionInput
) {
const session = await this.service.createCheckoutSession({
user,
plan: input.plan,
recurring: input.recurring,
promotionCode: input.coupon,
redirectUrl:
input.successCallbackLink ?? `${this.config.baseUrl}/upgrade-success`,
idempotencyKey: input.idempotencyKey,
});
if (!session.url) {
throw new BadGatewayException('Failed to create checkout session.');
}
return session.url;
@@ -263,20 +314,14 @@ export class UserSubscriptionResolver {
) {
// allow admin to query other user's subscription
if (!ctx.isAdminQuery && me.id !== user.id) {
throw new GraphQLError(
'You are not allowed to access this subscription',
{
extensions: {
status: HttpStatus[HttpStatus.FORBIDDEN],
code: HttpStatus.FORBIDDEN,
},
}
throw new ForbiddenException(
'You are not allowed to access this subscription.'
);
}
// @FIXME(@forehalo): should not mock any api for selfhosted server
// the frontend should avoid calling such api if feature is not enabled
if (this.config.flavor.selfhosted) {
if (this.config.isSelfhosted) {
const start = new Date();
const end = new Date();
end.setFullYear(start.getFullYear() + 1);
@@ -310,12 +355,9 @@ export class UserSubscriptionResolver {
@Args('skip', { type: () => Int, nullable: true }) skip?: number
) {
if (me.id !== user.id) {
throw new GraphQLError('You are not allowed to access this invoices', {
extensions: {
status: HttpStatus[HttpStatus.FORBIDDEN],
code: HttpStatus.FORBIDDEN,
},
});
throw new ForbiddenException(
'You are not allowed to access this invoices'
);
}
return this.db.userInvoice.findMany({

View File

@@ -69,13 +69,15 @@ export class SubscriptionService {
async createCheckoutSession({
user,
recurring,
plan,
promotionCode,
redirectUrl,
idempotencyKey,
plan = SubscriptionPlan.Pro,
}: {
user: User;
plan?: SubscriptionPlan;
recurring: SubscriptionRecurring;
plan: SubscriptionPlan;
promotionCode?: string | null;
redirectUrl: string;
idempotencyKey: string;
}) {
@@ -95,7 +97,28 @@ export class SubscriptionService {
`${idempotencyKey}-getOrCreateCustomer`,
user
);
const coupon = await this.getAvailableCoupon(user, CouponType.EarlyAccess);
let discount: { coupon?: string; promotion_code?: string } | undefined;
if (promotionCode) {
const code = await this.getAvailablePromotionCode(
promotionCode,
customer.stripeCustomerId
);
if (code) {
discount ??= {};
discount.promotion_code = code;
}
} else {
const coupon = await this.getAvailableCoupon(
user,
CouponType.EarlyAccess
);
if (coupon) {
discount ??= {};
discount.coupon = coupon;
}
}
return await this.stripe.checkout.sessions.create(
{
@@ -108,13 +131,11 @@ export class SubscriptionService {
tax_id_collection: {
enabled: true,
},
...(coupon
...(discount
? {
discounts: [{ coupon }],
discounts: [discount],
}
: {
allow_promotion_codes: true,
}),
: { allow_promotion_codes: true }),
mode: 'subscription',
success_url: redirectUrl,
customer: customer.stripeCustomerId,
@@ -643,4 +664,33 @@ export class SubscriptionService {
return null;
}
private async getAvailablePromotionCode(
userFacingPromotionCode: string,
customer?: string
) {
const list = await this.stripe.promotionCodes.list({
code: userFacingPromotionCode,
active: true,
limit: 1,
});
const code = list.data[0];
if (!code) {
return null;
}
let available = false;
if (code.customer) {
available =
typeof code.customer === 'string'
? code.customer === customer
: code.customer.id === customer;
} else {
available = true;
}
return available ? code.id : null;
}
}

View File

@@ -2,7 +2,7 @@ import { createAdapter } from '@socket.io/redis-adapter';
import { Redis } from 'ioredis';
import { Server, ServerOptions } from 'socket.io';
import { SocketIoAdapter } from '../../fundamentals';
import { SocketIoAdapter } from '../../fundamentals/websocket';
export function createSockerIoAdapterImpl(
redis: Redis

View File

@@ -0,0 +1,40 @@
import { OptionalModule } from '../../fundamentals';
import { registerStorageProvider } from '../../fundamentals/storage';
import { R2StorageProvider } from './providers/r2';
import { S3StorageProvider } from './providers/s3';
registerStorageProvider('cloudflare-r2', (config, bucket) => {
if (!config.plugins['cloudflare-r2']) {
throw new Error('Missing cloudflare-r2 storage provider configuration');
}
return new R2StorageProvider(config.plugins['cloudflare-r2'], bucket);
});
registerStorageProvider('aws-s3', (config, bucket) => {
if (!config.plugins['aws-s3']) {
throw new Error('Missing aws-s3 storage provider configuration');
}
return new S3StorageProvider(config.plugins['aws-s3'], bucket);
});
@OptionalModule({
requires: [
'plugins.cloudflare-r2.accountId',
'plugins.cloudflare-r2.credentials.accessKeyId',
'plugins.cloudflare-r2.credentials.secretAccessKey',
],
if: config => config.flavor.graphql,
})
export class CloudflareR2Module {}
@OptionalModule({
requires: [
'plugins.aws-s3.credentials.accessKeyId',
'plugins.aws-s3.credentials.secretAccessKey',
],
if: config => config.flavor.graphql,
})
export class AwsS3Module {}
export type { R2StorageConfig, S3StorageConfig } from './types';

View File

@@ -1,15 +1,16 @@
import { Logger } from '@nestjs/common';
import { R2StorageConfig } from '../../config/storage';
import type { R2StorageConfig } from '../types';
import { S3StorageProvider } from './s3';
export class R2StorageProvider extends S3StorageProvider {
override readonly type = 'r2' as any /* cast 'r2' to 's3' */;
override readonly type = 'cloudflare-r2' as any /* cast 'r2' to 's3' */;
constructor(config: R2StorageConfig, bucket: string) {
super(
{
...config,
forcePathStyle: true,
endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
},
bucket

View File

@@ -11,21 +11,22 @@ import {
} from '@aws-sdk/client-s3';
import { Logger } from '@nestjs/common';
import { S3StorageConfig } from '../../config/storage';
import {
autoMetadata,
BlobInputType,
GetObjectMetadata,
ListObjectsMetadata,
PutObjectMetadata,
StorageProvider,
} from './provider';
import { autoMetadata, toBuffer } from './utils';
toBuffer,
} from '../../../fundamentals/storage';
import type { S3StorageConfig } from '../types';
export class S3StorageProvider implements StorageProvider {
protected logger: Logger;
protected client: S3Client;
readonly type = 's3';
readonly type = 'aws-s3';
constructor(
config: S3StorageConfig,
@@ -49,7 +50,7 @@ export class S3StorageProvider implements StorageProvider {
new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: body,
Body: blob,
// metadata
ContentType: metadata.contentType,

View File

@@ -0,0 +1,16 @@
import { S3ClientConfigType } from '@aws-sdk/client-s3';
type WARNING = '__YOU_SHOULD_NOT_MANUALLY_CONFIGURATE_THIS_TYPE__';
export type R2StorageConfig = S3ClientConfigType & {
accountId: string;
};
export type S3StorageConfig = S3ClientConfigType;
declare module '../../fundamentals/config/storage' {
interface StorageProvidersConfig {
// the type here is only existing for extends [StorageProviderType] with better type inference and checking.
'cloudflare-r2'?: WARNING;
'aws-s3'?: WARNING;
}
}

View File

@@ -5,6 +5,7 @@ import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { config } from 'dotenv';
import { omit } from 'lodash-es';
import {
applyEnvToConfig,
@@ -43,14 +44,23 @@ async function load() {
// 3. load env => config map to `globalThis.AFFiNE.ENV_MAP
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.env.js');
// 4. apply `process.env` map overriding to `globalThis.AFFiNE`
applyEnvToConfig(globalThis.AFFiNE);
// 5. load `config/affine` to patch custom configs
// 4. load `config/affine` to patch custom configs
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.js');
if (process.env.NODE_ENV === 'development') {
console.log('AFFiNE Config:', JSON.stringify(globalThis.AFFiNE, null, 2));
// 5. load `config/affine.self` to patch custom configs
// This is the file only take effect in [AFFiNE Cloud]
if (!AFFiNE.isSelfhosted) {
await loadRemote(AFFiNE_CONFIG_PATH, 'affine.self.js');
}
// 6. apply `process.env` map overriding to `globalThis.AFFiNE`
applyEnvToConfig(globalThis.AFFiNE);
if (AFFiNE.node.dev) {
console.log(
'AFFiNE Config:',
JSON.stringify(omit(globalThis.AFFiNE, 'ENV_MAP'), null, 2)
);
}
}

View File

@@ -2,6 +2,14 @@
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
input CreateCheckoutSessionInput {
coupon: String
idempotencyKey: String!
plan: SubscriptionPlan = Pro
recurring: SubscriptionRecurring = Yearly
successCallbackLink: String
}
"""
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
"""
@@ -24,6 +32,14 @@ enum FeatureType {
UnlimitedWorkspace
}
type HumanReadableQuotaType {
blobLimit: String!
historyPeriod: String!
memberLimit: String!
name: String!
storageQuota: String!
}
type InvitationType {
"""Invitee information"""
invitee: UserType!
@@ -99,7 +115,10 @@ type Mutation {
changePassword(newPassword: String!, token: String!): UserType!
"""Create a subscription checkout link of stripe"""
checkout(idempotencyKey: String!, recurring: SubscriptionRecurring!): String!
checkout(idempotencyKey: String!, recurring: SubscriptionRecurring!): String! @deprecated(reason: "use `createCheckoutSession` instead")
"""Create a subscription checkout link of stripe"""
createCheckoutSession(input: CreateCheckoutSessionInput!): String!
"""Create a stripe customer portal to manage payment methods"""
createCustomerPortal: String!
@@ -191,7 +210,10 @@ type Query {
type QuotaQueryType {
blobLimit: SafeInt!
humanReadableName: String!
historyPeriod: SafeInt!
humanReadable: HumanReadableQuotaType!
memberLimit: SafeInt!
name: String!
storageQuota: SafeInt!
usedSize: SafeInt!
}
@@ -218,10 +240,18 @@ type ServerConfigType {
"""server identical name could be shown as badge on user interface"""
name: String!
"""server type"""
type: ServerDeploymentType!
"""server version"""
version: String!
}
enum ServerDeploymentType {
Affine
Selfhosted
}
enum ServerFeature {
Payment
}

View File

@@ -18,7 +18,7 @@ test.afterEach.always(async () => {
test('should be able to get config', t => {
t.true(typeof config.host === 'string');
t.is(config.env, 'test');
t.is(config.NODE_ENV, 'test');
});
test('should be able to override config', async t => {

View File

@@ -4,12 +4,7 @@ import { TestingModule } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import test from 'ava';
import * as Sinon from 'sinon';
import {
applyUpdate,
decodeStateVector,
Doc as YDoc,
encodeStateAsUpdate,
} from 'yjs';
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import { DocManager, DocModule } from '../src/core/doc';
import { QuotaModule } from '../src/core/quota';
@@ -277,72 +272,120 @@ test('should throw if meet max retry times', async t => {
t.is(stub.callCount, 5);
});
test('should not update snapshot if state is outdated', async t => {
const db = m.get(PrismaClient);
test('should be able to insert the snapshot if it is new created', async t => {
const manager = m.get(DocManager);
await db.snapshot.create({
data: {
id: '2',
workspaceId: '2',
blob: Buffer.from([0, 0]),
seq: 1,
},
});
const doc = new YDoc();
const text = doc.getText('content');
const updates: Buffer[] = [];
doc.on('update', update => {
updates.push(Buffer.from(update));
});
text.insert(0, 'hello');
text.insert(5, 'world');
text.insert(5, ' ');
const update = encodeStateAsUpdate(doc);
await Promise.all(updates.map(update => manager.push('2', '2', update)));
await manager.push('1', '1', Buffer.from(update));
const updateWith3Records = await manager.getUpdates('2', '2');
text.insert(11, '!');
await manager.push('2', '2', updates[3]);
const updateWith4Records = await manager.getUpdates('2', '2');
// Simulation:
// Node A get 3 updates and squash them at time 1, will finish at time 10
// Node B get 4 updates and squash them at time 3, will finish at time 8
// Node B finish the squash first, and update the snapshot
// Node A finish the squash later, and update the snapshot to an outdated state
// Time: ---------------------->
// A: ^get ^upsert
// B: ^get ^upsert
//
// We should avoid such situation
const updates = await manager.getUpdates('1', '1');
t.is(updates.length, 1);
// @ts-expect-error private
await manager.squash(updateWith4Records, null);
// @ts-expect-error private
await manager.squash(updateWith3Records, null);
const snapshot = await manager.squash(null, updates);
const result = await db.snapshot.findUnique({
t.truthy(snapshot);
t.is(snapshot.getText('content').toString(), 'hello');
const restUpdates = await manager.getUpdates('1', '1');
t.is(restUpdates.length, 0);
});
test('should be able to merge updates into snapshot', async t => {
const manager = m.get(DocManager);
const updates: Buffer[] = [];
{
const doc = new YDoc();
doc.on('update', data => {
updates.push(Buffer.from(data));
});
const text = doc.getText('content');
text.insert(0, 'hello');
text.insert(5, 'world');
text.insert(5, ' ');
text.insert(11, '!');
}
{
await manager.batchPush('1', '1', updates.slice(0, 2));
// do the merge
const doc = (await manager.get('1', '1'))!;
t.is(doc.getText('content').toString(), 'helloworld');
}
{
await manager.batchPush('1', '1', updates.slice(2));
const doc = (await manager.get('1', '1'))!;
t.is(doc.getText('content').toString(), 'hello world!');
}
const restUpdates = await manager.getUpdates('1', '1');
t.is(restUpdates.length, 0);
});
test('should not update snapshot if doc is outdated', async t => {
const manager = m.get(DocManager);
const db = m.get(PrismaClient);
const updates: Buffer[] = [];
{
const doc = new YDoc();
doc.on('update', data => {
updates.push(Buffer.from(data));
});
const text = doc.getText('content');
text.insert(0, 'hello');
text.insert(5, 'world');
text.insert(5, ' ');
text.insert(11, '!');
}
await manager.batchPush('2', '1', updates.slice(0, 2)); // 'helloworld'
// merge updates into snapshot
await manager.get('2', '1');
// fake the snapshot is a lot newer
await db.snapshot.update({
where: {
id_workspaceId: {
id: '2',
workspaceId: '2',
id: '1',
},
},
data: {
updatedAt: new Date(Date.now() + 10000),
},
});
if (!result) {
t.fail('snapshot not found');
return;
{
const snapshot = await manager.getSnapshot('2', '1');
await manager.batchPush('2', '1', updates.slice(2)); // 'hello world!'
const updateRecords = await manager.getUpdates('2', '1');
// @ts-expect-error private
const doc = await manager.squash(snapshot, updateRecords);
// all updated will merged into doc not matter it's timestamp is outdated or not,
// but the snapshot record will not be updated
t.is(doc.getText('content').toString(), 'hello world!');
}
const state = decodeStateVector(result.state!);
t.is(state.get(doc.clientID), 12);
{
const doc = new YDoc();
applyUpdate(doc, (await manager.getSnapshot('2', '1'))!.blob);
// the snapshot will not get touched if the new doc's timestamp is outdated
t.is(doc.getText('content').toString(), 'helloworld');
const d = new YDoc();
applyUpdate(d, result.blob!);
const dtext = d.getText('content');
t.is(dtext.toString(), 'hello world!');
// the updates are known as outdated, so they will be deleted
t.is((await manager.getUpdates('2', '1')).length, 0);
}
});

View File

@@ -0,0 +1,90 @@
import {
ForbiddenException,
HttpStatus,
INestApplication,
} from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Test } from '@nestjs/testing';
import testFn, { TestFn } from 'ava';
import request from 'supertest';
import { ConfigModule } from '../src/fundamentals/config';
import { GqlModule } from '../src/fundamentals/graphql';
@Resolver(() => String)
class TestResolver {
greating = 'hello world';
@Query(() => String)
hello() {
return this.greating;
}
@Mutation(() => String)
update(@Args('greating') greating: string) {
this.greating = greating;
return this.greating;
}
@Query(() => String)
errorQuery() {
throw new ForbiddenException('forbidden query');
}
@Query(() => String)
unknownErrorQuery() {
throw new Error('unknown error');
}
}
const test = testFn as TestFn<{ app: INestApplication }>;
function gql(app: INestApplication, query: string) {
return request(app.getHttpServer())
.post('/graphql')
.send({ query })
.expect(200);
}
test.beforeEach(async ctx => {
const module = await Test.createTestingModule({
imports: [ConfigModule.forRoot(), GqlModule],
providers: [TestResolver],
}).compile();
ctx.context.app = await module
.createNestApplication({
logger: false,
})
.init();
});
test('should be able to execute query', async t => {
const res = await gql(t.context.app, `query { hello }`);
t.is(res.body.data.hello, 'hello world');
});
test('should be able to execute mutation', async t => {
const res = await gql(t.context.app, `mutation { update(greating: "hi") }`);
t.is(res.body.data.update, 'hi');
const newRes = await gql(t.context.app, `query { hello }`);
t.is(newRes.body.data.hello, 'hi');
});
test('should be able to handle known http exception', async t => {
const res = await gql(t.context.app, `query { errorQuery }`);
const err = res.body.errors[0];
t.is(err.message, 'forbidden query');
t.is(err.extensions.code, HttpStatus.FORBIDDEN);
t.is(err.extensions.status, HttpStatus[HttpStatus.FORBIDDEN]);
});
test('should be able to handle unknown internal error', async t => {
const res = await gql(t.context.app, `query { unknownErrorQuery }`);
const err = res.body.errors[0];
t.is(err.message, 'Internal Server Error');
t.is(err.extensions.code, HttpStatus.INTERNAL_SERVER_ERROR);
t.is(err.extensions.status, HttpStatus[HttpStatus.INTERNAL_SERVER_ERROR]);
});

View File

@@ -48,7 +48,7 @@ test('should be able to set quota', async t => {
const q1 = await quota.getUserQuota(u1.id);
t.truthy(q1, 'should have quota');
t.is(q1?.feature.name, QuotaType.FreePlanV1, 'should be free plan');
t.is(q1?.feature.version, 2, 'should be version 2');
t.is(q1?.feature.version, 3, 'should be version 2');
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
@@ -64,8 +64,8 @@ test('should be able to check storage quota', async t => {
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
const q1 = await storageQuota.getUserQuota(u1.id);
t.is(q1?.blobLimit, Quotas[3].configs.blobLimit, 'should be free plan');
t.is(q1?.storageQuota, Quotas[3].configs.storageQuota, 'should be free plan');
t.is(q1?.blobLimit, Quotas[4].configs.blobLimit, 'should be free plan');
t.is(q1?.storageQuota, Quotas[4].configs.storageQuota, 'should be free plan');
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
const q2 = await storageQuota.getUserQuota(u1.id);
@@ -78,8 +78,8 @@ test('should be able revert quota', async t => {
const u1 = await auth.signUp('DarkSky', 'darksky@example.org', '123456');
const q1 = await storageQuota.getUserQuota(u1.id);
t.is(q1?.blobLimit, Quotas[3].configs.blobLimit, 'should be free plan');
t.is(q1?.storageQuota, Quotas[3].configs.storageQuota, 'should be free plan');
t.is(q1?.blobLimit, Quotas[4].configs.blobLimit, 'should be free plan');
t.is(q1?.storageQuota, Quotas[4].configs.storageQuota, 'should be free plan');
await quota.switchUserQuota(u1.id, QuotaType.ProPlanV1);
const q2 = await storageQuota.getUserQuota(u1.id);
@@ -88,7 +88,7 @@ test('should be able revert quota', async t => {
await quota.switchUserQuota(u1.id, QuotaType.FreePlanV1);
const q3 = await storageQuota.getUserQuota(u1.id);
t.is(q3?.blobLimit, Quotas[3].configs.blobLimit, 'should be free plan');
t.is(q3?.blobLimit, Quotas[4].configs.blobLimit, 'should be free plan');
const quotas = await quota.getUserQuotas(u1.id);
t.is(quotas.length, 3, 'should have 3 quotas');

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