From 2f6c4e36965bab1c77f36ea28774beac3aa2e998 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Tue, 29 Aug 2023 05:07:05 -0500 Subject: [PATCH] feat!: affine cloud support (#3813) Co-authored-by: Hongtao Lye Co-authored-by: liuyi Co-authored-by: LongYinan Co-authored-by: X1a0t <405028157@qq.com> Co-authored-by: JimmFly Co-authored-by: Peng Xiao Co-authored-by: xiaodong zuo <53252747+zuoxiaodong0815@users.noreply.github.com> Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com> Co-authored-by: danielchim --- .env.template | 2 + .eslintrc.js | 12 + .github/actions/build-rust/action.yml | 11 +- .github/actions/deploy/action.yml | 50 + .github/actions/deploy/deploy.mjs | 116 + .github/actions/setup-rust/action.yml | 31 - .github/deployment/front/affine.nginx.conf | 2 +- .../charts/graphql/templates/_helpers.tpl | 56 +- .../charts/graphql/templates/deployment.yaml | 72 +- .../charts/graphql/templates/migration.yaml | 15 +- .../charts/graphql/templates/monitoring.yaml | 13 + .../templates/oauth-github-secret.yaml | 10 - .../templates/oauth-google-secret.yaml | 10 - .../charts/graphql/templates/oauth.yaml | 33 + .../charts/graphql/templates/pg-secret.yaml | 9 + .../charts/graphql/templates/r2-secret.yaml | 5 +- .../graphql/templates/redis-secret.yaml | 9 + .../helm/affine/charts/graphql/values.yaml | 24 +- .github/helm/affine/charts/sync/.helmignore | 23 + .github/helm/affine/charts/sync/Chart.yaml | 6 + .../affine/charts/sync/templates/NOTES.txt | 16 + .../affine/charts/sync/templates/_helpers.tpl | 63 + .../charts/sync/templates/deployment.yaml | 110 + .../charts/sync/templates/monitoring.yaml | 13 + .../affine/charts/sync/templates/service.yaml | 19 + .../charts/sync/templates/serviceaccount.yaml | 12 + .../sync/templates/tests/test-connection.yaml | 15 + .github/helm/affine/charts/sync/values.yaml | 39 + .../affine/charts/web/templates/_helpers.tpl | 1 + .github/helm/affine/templates/ingress.yaml | 28 +- .github/helm/affine/values.yaml | 41 +- .github/workflows/build-desktop.yml | 1 + .github/workflows/build.yml | 105 +- .github/workflows/deploy.yml | 211 + .github/workflows/nightly-build.yml | 2 +- .github/workflows/publish-storybook.yml | 7 +- .github/workflows/release-desktop-app.yml | 2 + .github/workflows/release.yml | 6 +- .gitignore | 3 +- .prettierignore | 1 + .vscode/launch.template.json | 16 +- Cargo.lock | 3261 ++++++++++-- Cargo.toml | 11 +- LICENSE | 409 +- LICENSE-MPL2.0 | 384 ++ apps/core/.webpack/cache-group.ts | 6 +- apps/core/.webpack/config.ts | 75 +- apps/core/.webpack/runtime-config.ts | 39 +- apps/core/.webpack/s3-plugin.ts | 58 + apps/core/.webpack/template.html | 7 +- apps/core/.webpack/webpack.config.ts | 11 +- apps/core/package.json | 7 + apps/core/project.json | 14 +- apps/core/src/adapters/cloud/crud.ts | 164 + apps/core/src/adapters/cloud/ui.tsx | 75 + apps/core/src/adapters/local/index.tsx | 15 +- apps/core/src/adapters/public-cloud/ui.tsx | 45 + apps/core/src/adapters/shared.ts | 6 + apps/core/src/adapters/workspace.ts | 46 +- apps/core/src/app.tsx | 25 +- apps/core/src/atoms/index.ts | 18 +- .../components/adapter-worksapce-wrapper.tsx | 13 + .../affine/any-error-boundary/index.tsx | 11 + .../affine/auth/after-sign-in-send-email.tsx | 67 + .../affine/auth/after-sign-up-send-email.tsx | 54 + .../components/affine/auth/callback-url.ts | 16 + .../core/src/components/affine/auth/index.tsx | 155 + .../src/components/affine/auth/send-email.tsx | 185 + .../affine/auth/sign-in-with-password.tsx | 111 + .../src/components/affine/auth/sign-in.tsx | 151 + .../src/components/affine/auth/style.css.ts | 28 + .../enable-affine-cloud-modal/index.tsx | 42 +- .../affine/enable-affine-cloud-modal/style.ts | 32 +- .../delete-leave-workspace/delete/index.tsx | 24 +- .../delete-leave-workspace/index.tsx | 60 +- .../delete-leave-workspace/leave/index.tsx | 49 - .../delete-leave-workspace/leave/style.ts | 44 - .../new-workspace-setting-detail/export.tsx | 5 +- .../new-workspace-setting-detail/index.tsx | 27 +- .../new-workspace-setting-detail/members.tsx | 228 + .../new-workspace-setting-detail/profile.tsx | 11 +- .../new-workspace-setting-detail/publish.tsx | 68 +- .../new-workspace-setting-detail/storage.tsx | 6 +- .../new-workspace-setting-detail/style.css.ts | 73 + .../setting-modal/account-setting/index.tsx | 186 +- .../account-setting/style.css.ts | 41 + .../components/affine/setting-modal/index.tsx | 6 +- .../setting-modal/setting-sidebar/index.tsx | 143 +- .../setting-sidebar/style.css.ts | 18 +- .../affine/setting-modal/style.css.ts | 11 +- .../setting-modal/workspace-setting/index.tsx | 84 +- .../affine/share-page-modal/index.tsx | 48 + .../block-suite-header-title/index.tsx | 4 +- apps/core/src/components/cloud/login-card.tsx | 53 + apps/core/src/components/cloud/provider.tsx | 58 + .../core/src/components/pure/footer/index.tsx | 53 +- .../core/src/components/pure/footer/styles.ts | 42 +- .../pure/workspace-list-modal/index.tsx | 197 +- .../pure/workspace-list-modal/styles.ts | 19 +- apps/core/src/components/workspace-header.tsx | 22 +- .../hooks/affine/use-curren-login-status.ts | 10 + .../core/src/hooks/affine/use-current-user.ts | 45 + .../src/hooks/affine/use-invite-member.ts | 30 + .../src/hooks/affine/use-is-shared-page.ts | 57 + .../hooks/affine/use-is-workspace-owner.ts | 13 + .../src/hooks/affine/use-leave-workspace.ts | 23 + apps/core/src/hooks/affine/use-members.ts | 19 + .../core/src/hooks/affine/use-mutate-cloud.ts | 14 + .../affine/use-revoke-member-permission.ts | 23 + apps/core/src/hooks/affine/use-share-link.ts | 15 + .../hooks/affine/use-toggle-cloud-public.ts | 22 + .../hooks/root/use-on-transform-workspace.ts | 44 +- apps/core/src/hooks/use-navigate-helper.ts | 30 +- .../core/src/hooks/use-transform-workspace.ts | 41 - apps/core/src/hooks/use-workspace.ts | 5 +- apps/core/src/hooks/use-workspaces.ts | 21 + apps/core/src/layouts/workspace-layout.tsx | 39 +- apps/core/src/pages/auth.tsx | 133 + apps/core/src/pages/expired.tsx | 25 + apps/core/src/pages/invite.tsx | 103 + apps/core/src/pages/share/detail-page.tsx | 73 + apps/core/src/pages/sign-in.tsx | 75 + apps/core/src/providers/modal-provider.tsx | 64 +- apps/core/src/router.ts | 20 + apps/core/src/shared/index.ts | 10 - apps/core/src/utils/email-regex.ts | 2 + apps/core/src/utils/toast.ts | 7 +- apps/electron/e2e/basic.spec.ts | 16 +- apps/electron/e2e/fixture.ts | 6 +- apps/electron/forge.config.js | 6 + apps/electron/src/helper/index.ts | 4 +- .../src/main/__tests__/integration.spec.ts | 1 + apps/electron/src/main/config.ts | 26 + apps/electron/src/main/deep-link.ts | 35 + apps/electron/src/main/events.ts | 2 + apps/electron/src/main/helper-process.ts | 9 +- apps/electron/src/main/index.ts | 13 +- apps/electron/src/main/main-window.ts | 60 +- apps/electron/src/main/protocol.ts | 90 +- apps/electron/src/main/ui/events.ts | 14 + apps/electron/src/main/ui/handlers.ts | 56 + apps/electron/src/main/ui/index.ts | 53 +- apps/electron/src/main/ui/subject.ts | 5 + apps/electron/src/main/utils.ts | 110 + apps/prototype/project.json | 44 +- apps/prototype/tsconfig.node.json | 5 + apps/prototype/vite.config.ts | 16 + apps/server/.env.example | 3 + apps/server/LICENSE | 44 + .../migration.sql | 8 + .../migration.sql | 12 + .../migration.sql | 9 + .../migration.sql | 18 + .../migration.sql | 42 + .../20230714065216_snapshot_id/migration.sql | 12 + .../migration.sql | 5 + .../migration.sql | 12 + .../migration.sql | 8 + apps/server/package.json | 37 +- apps/server/schema.prisma | 78 +- apps/server/scripts/init-db.ts | 6 +- apps/server/scripts/run-test.ts | 1 + apps/server/src/app.ts | 4 +- apps/server/src/config/def.ts | 64 +- apps/server/src/config/default.ts | 64 +- apps/server/src/constants.ts | 3 + apps/server/src/graphql.module.ts | 12 +- apps/server/src/graphql/logger-plugin.ts | 60 + apps/server/src/index.ts | 72 +- apps/server/src/metrics/controller.ts | 18 + apps/server/src/metrics/index.ts | 12 + apps/server/src/metrics/metrics.ts | 25 + apps/server/src/metrics/utils.ts | 73 + apps/server/src/middleware/timing.ts | 27 + apps/server/src/modules/auth/guard.ts | 86 +- apps/server/src/modules/auth/index.ts | 12 +- apps/server/src/modules/auth/mailer/index.ts | 2 + .../src/modules/auth/mailer/mail.service.ts | 130 + apps/server/src/modules/auth/mailer/mailer.ts | 27 + .../src/modules/auth/mailer/template.ts | 195 + .../src/modules/auth/next-auth-options.ts | 501 ++ .../src/modules/auth/next-auth.controller.ts | 303 +- apps/server/src/modules/auth/resolver.ts | 62 +- apps/server/src/modules/auth/service.ts | 119 +- apps/server/src/modules/doc/index.ts | 42 + apps/server/src/modules/doc/manager.ts | 351 ++ apps/server/src/modules/doc/redis-manager.ts | 150 + apps/server/src/modules/index.ts | 37 +- apps/server/src/modules/storage/fs.ts | 11 +- .../src/modules/storage/storage.service.ts | 16 +- .../src/modules/sync/events/events.gateway.ts | 153 + .../src/modules/sync/events/events.module.ts | 11 + .../src/modules/sync/events/workspace.ts | 48 + apps/server/src/modules/sync/index.ts | 8 + apps/server/src/modules/sync/redis-adapter.ts | 37 + apps/server/src/modules/sync/utils.ts | 11 + apps/server/src/modules/users/resolver.ts | 133 +- apps/server/src/modules/users/types.ts | 3 + .../src/modules/workspaces/controller.ts | 48 +- apps/server/src/modules/workspaces/index.ts | 7 +- .../src/modules/workspaces/permission.ts | 152 +- .../server/src/modules/workspaces/resolver.ts | 352 +- apps/server/src/modules/workspaces/utils.ts | 2 + apps/server/src/prisma/service.ts | 15 +- apps/server/src/schema.gql | 209 +- apps/server/src/storage/index.ts | 21 +- apps/server/src/tests/app.e2e.ts | 1 + apps/server/src/tests/auth.spec.ts | 41 +- apps/server/src/tests/doc.spec.ts | 158 + apps/server/src/tests/mailer.spec.ts | 86 + .../src/tests/prometheus-metrics.spec.ts | 61 + apps/server/src/tests/user.spec.ts | 77 + apps/server/src/tests/utils.ts | 465 ++ apps/server/src/tests/workspace-blobs.spec.ts | 70 + .../server/src/tests/workspace-invite.spec.ts | 189 + apps/server/src/tests/workspace.spec.ts | 322 +- apps/server/src/types.ts | 6 + apps/server/src/utils/doc.ts | 7 + apps/server/tsconfig.json | 1 + apps/storybook/.storybook/main.ts | 6 + apps/storybook/.storybook/preview.tsx | 51 +- apps/storybook/package.json | 1 + .../src/stories/share-menu.stories.tsx | 20 +- apps/storybook/tsconfig.node.json | 2 +- nx.json | 1 + package.json | 7 +- packages/cli/src/bin/build-core.ts | 11 +- packages/component/package.json | 1 + .../auth-components/auth-content.tsx | 14 + .../components/auth-components/auth-input.tsx | 49 + .../auth-components/auth-page-container.tsx | 32 + .../auth-components/back-button.tsx | 24 + .../auth-components/change-email-page.tsx | 89 + .../auth-components/change-password-page.tsx | 58 + .../src/components/auth-components/index.tsx | 14 + .../src/components/auth-components/logo.tsx | 18 + .../auth-components/modal-header.tsx | 18 + .../src/components/auth-components/modal.tsx | 43 + .../auth-components/password-input/error.tsx | 30 + .../auth-components/password-input/index.tsx | 100 + .../password-input/style.css.ts | 28 + .../password-input/success.tsx | 28 + .../auth-components/password-input/tag.tsx | 28 + .../auth-components/resend-button.tsx | 76 + .../auth-components/set-password-page.tsx | 60 + .../auth-components/set-password.tsx | 50 + .../components/auth-components/share.css.ts | 180 + .../sign-in-page-container.tsx | 6 + .../auth-components/sign-in-success-page.tsx | 21 + .../auth-components/sign-up-page.tsx | 65 + .../src/components/auth-components/utils.ts | 2 + .../components/card/workspace-card/index.tsx | 31 +- .../components/card/workspace-card/styles.ts | 7 + .../member-components/accept-invite-page.tsx | 51 + .../components/member-components/index.tsx | 2 + .../member-components/invite-modal.tsx | 147 + .../member-components/styles.css.tsx | 23 + .../components/notification-center/index.tsx | 1 + .../page-list/operation-menu-items/export.tsx | 51 +- .../setting-components/setting-row.tsx | 7 +- .../setting-components/share.css.ts | 12 + .../share-menu/disable-public-link/index.tsx | 66 +- .../share-menu/disable-public-link/style.ts | 53 +- .../src/components/share-menu/export.tsx | 27 - .../src/components/share-menu/index.css.ts | 107 +- .../src/components/share-menu/index.jotai.ts | 3 + .../src/components/share-menu/index.tsx | 1 - .../components/share-menu/share-export.tsx | 48 + .../src/components/share-menu/share-menu.tsx | 161 +- .../src/components/share-menu/share-page.tsx | 242 +- .../components/share-menu/share-workspace.tsx | 67 - .../src/components/workspace/index.tsx | 14 +- .../inter/Inter-VariableFont_slnt,wght.ttf | Bin .../component/src}/fonts/inter/OFL.txt | 0 .../component/src}/fonts/kalam/Kalam-Bold.ttf | Bin .../src}/fonts/kalam/Kalam-Light.ttf | Bin .../src}/fonts/kalam/Kalam-Regular.ttf | Bin .../component/src}/fonts/kalam/OFL.txt | 0 .../src}/fonts/source-code-pro/OFL.txt | 0 ...SourceCodePro-Italic-VariableFont_wght.ttf | Bin .../SourceCodePro-VariableFont_wght.ttf | Bin .../src}/fonts/source-serif-4/OFL.txt | 0 .../source-serif-4/SourceSerif4-Bold.ttf | Bin .../SourceSerif4-BoldItalic.ttf | Bin ...ceSerif4-Italic-VariableFont_opsz,wght.ttf | Bin .../source-serif-4/SourceSerif4-Italic.ttf | Bin .../source-serif-4/SourceSerif4-Light.ttf | Bin .../SourceSerif4-LightItalic.ttf | Bin .../source-serif-4/SourceSerif4-Medium.ttf | Bin .../SourceSerif4-MediumItalic.ttf | Bin .../source-serif-4/SourceSerif4-Regular.ttf | Bin .../source-serif-4/SourceSerif4-SemiBold.ttf | Bin .../SourceSerif4-SemiBoldItalic.ttf | Bin .../SourceSerif4-VariableFont_opsz,wght.ttf | Bin .../component/src}/fonts/space-mono/OFL.txt | 0 .../src}/fonts/space-mono/SpaceMono-Bold.ttf | Bin .../fonts/space-mono/SpaceMono-BoldItalic.ttf | Bin .../fonts/space-mono/SpaceMono-Italic.ttf | Bin .../fonts/space-mono/SpaceMono-Regular.ttf | Bin packages/component/src/theme/fonts.css | 36 +- packages/component/src/theme/global.css | 53 +- packages/component/src/theme/theme.css.ts | 1 - packages/component/src/ui/button/radio.tsx | 35 +- packages/component/src/ui/button/style.css.ts | 70 +- packages/component/src/ui/button/utils.ts | 9 +- packages/component/src/ui/input/index.css.ts | 34 - packages/component/src/ui/input/input.tsx | 140 +- packages/component/src/ui/input/style.css.ts | 76 + packages/component/src/ui/menu/menu-item.tsx | 20 +- packages/component/src/ui/menu/styles.ts | 24 +- .../component/src/ui/modal/confirm-modal.tsx | 72 + packages/component/src/ui/modal/index.tsx | 1 + .../src/ui/modal/modal-close-button.tsx | 2 - packages/component/src/ui/modal/styles.ts | 24 +- .../component/src/ui/scrollbar/scrollbar.tsx | 3 + packages/env/src/constant.ts | 1 + packages/env/src/global.ts | 33 +- packages/env/src/workspace.ts | 28 +- packages/graphql/package.json | 4 +- .../graphql/src/__tests__/fetcher.spec.ts | 67 +- packages/graphql/src/fetcher.ts | 80 +- packages/graphql/src/graphql/blob-delete.gql | 3 + packages/graphql/src/graphql/blob-list.gql | 3 + packages/graphql/src/graphql/blob-set.gql | 3 + packages/graphql/src/graphql/change-email.gql | 8 + .../graphql/src/graphql/change-password.gql | 8 + .../graphql/src/graphql/delete-account.gql | 5 + .../graphql/src/graphql/delete-workspace.gql | 3 + .../graphql/src/graphql/get-current-user.gql | 10 + .../graphql/src/graphql/get-invite-info.gql | 14 + packages/graphql/src/graphql/get-is-owner.gql | 3 + .../graphql/get-members-by-workspace-id.gql | 14 + .../src/graphql/get-public-workspace.gql | 5 + packages/graphql/src/graphql/get-user.gql | 9 + .../graphql/get-workspace-public-by-id.gql | 5 + .../graphql/get-workspace-shared-pages.gql | 5 + .../graphql/src/graphql/get-workspace.gql | 5 + .../graphql/src/graphql/get-workspaces.gql | 5 + packages/graphql/src/graphql/index.ts | 429 +- .../graphql/src/graphql/leave-workspace.gql | 3 + .../src/graphql/revoke-member-permission.gql | 3 + packages/graphql/src/graphql/revoke-page.gql | 3 + .../graphql/src/graphql/send-change-email.gql | 3 + .../graphql/send-change-password-email.gql | 3 + .../src/graphql/send-set-password-email.gql | 3 + .../graphql/src/graphql/set-revoke-page.gql | 3 + .../graphql/src/graphql/set-share-page.gql | 3 + .../graphql/set-workspace-public-by-id.gql | 5 + packages/graphql/src/graphql/share-page.gql | 3 + packages/graphql/src/graphql/sign-in.gql | 7 + packages/graphql/src/graphql/sign-up.gql | 7 + .../src/graphql/workspace-intive-by-email.gql | 13 + .../workspace-invite-accept-by-invite-id.gql | 3 + ...orkspace-invite-accept-by-workspace-id.gql | 3 + packages/graphql/src/graphql/workspace.gql | 7 - packages/graphql/src/index.ts | 1 + packages/graphql/src/schema.ts | 521 +- packages/graphql/src/utils.ts | 174 + packages/graphql/tsconfig.json | 7 +- packages/hooks/src/__tests__/index.spec.ts | 14 - ...se-block-suite-workspace-page-is-public.ts | 36 - packages/i18n/project.json | 3 +- packages/i18n/src/resources/en.json | 23 +- packages/infra/project.json | 3 +- packages/infra/src/preload/electron.ts | 21 +- packages/infra/src/type.ts | 6 + packages/infra/vite.config.ts | 2 +- packages/native/Cargo.toml | 5 +- packages/sdk/project.json | 3 +- packages/storage/Cargo.lock | 4428 ----------------- packages/storage/Cargo.toml | 12 +- packages/storage/__tests__/storage.spec.js | 7 +- packages/storage/index.d.ts | 36 +- packages/storage/index.js | 3 +- packages/storage/project.json | 3 + packages/storage/src/lib.rs | 171 +- packages/workers/src/index.ts | 7 +- packages/workspace/package.json | 5 + .../src/affine}/__tests__/gql.spec.tsx | 0 .../workspace/src/affine}/gql.ts | 41 +- packages/workspace/src/affine/index.ts | 184 + packages/workspace/src/affine/utils.ts | 45 + packages/workspace/src/atom.ts | 46 +- .../workspace/src/blob/cloud-blob-storage.ts | 51 + .../src/blob/local-static-storage.ts | 8 +- packages/workspace/src/local/crud.ts | 22 +- packages/workspace/src/manager/index.ts | 19 +- .../__tests__/socketio-provider.spec.ts | 103 + .../workspace/src/providers/cloud/index.ts | 88 + packages/workspace/src/providers/index.ts | 30 + .../src/providers/sqlite-providers.ts | 10 +- packages/workspace/tsconfig.json | 4 +- packages/y-indexeddb/src/provider.ts | 6 +- .../y-provider/src/__tests__/index.spec.ts | 7 +- packages/y-provider/src/lazy-provider.ts | 22 +- packages/y-provider/src/types.ts | 28 +- plugins/copilot/src/UI/debug-content.tsx | 1 - plugins/outline/src/atom.ts | 5 + scripts/check-version.mjs | 2 +- tests/affine-cloud/e2e/basic.spec.ts | 62 + tests/affine-cloud/e2e/login.spec.ts | 17 + tests/affine-cloud/package.json | 12 + tests/affine-cloud/playwright.config.ts | 64 + tests/affine-cloud/tsconfig.json | 16 + .../0.7.0-canary.18/e2e/basic.spec.ts | 3 +- .../0.7.0-canary.18/tsconfig.json | 7 +- .../e2e/local-first-delete-workspace.spec.ts | 10 +- .../e2e/local-first-workspace-list.spec.ts | 5 +- tests/affine-local/e2e/quick-search.spec.ts | 4 +- tests/affine-local/e2e/router.spec.ts | 2 +- tests/kit/package.json | 1 + tests/kit/utils/cloud.ts | 54 + tsconfig.json | 8 +- yarn.lock | 1564 +++++- 414 files changed, 19469 insertions(+), 7591 deletions(-) create mode 100644 .github/actions/deploy/action.yml create mode 100644 .github/actions/deploy/deploy.mjs delete mode 100644 .github/actions/setup-rust/action.yml create mode 100644 .github/helm/affine/charts/graphql/templates/monitoring.yaml delete mode 100644 .github/helm/affine/charts/graphql/templates/oauth-github-secret.yaml delete mode 100644 .github/helm/affine/charts/graphql/templates/oauth-google-secret.yaml create mode 100644 .github/helm/affine/charts/graphql/templates/oauth.yaml create mode 100644 .github/helm/affine/charts/graphql/templates/pg-secret.yaml create mode 100644 .github/helm/affine/charts/graphql/templates/redis-secret.yaml create mode 100644 .github/helm/affine/charts/sync/.helmignore create mode 100644 .github/helm/affine/charts/sync/Chart.yaml create mode 100644 .github/helm/affine/charts/sync/templates/NOTES.txt create mode 100644 .github/helm/affine/charts/sync/templates/_helpers.tpl create mode 100644 .github/helm/affine/charts/sync/templates/deployment.yaml create mode 100644 .github/helm/affine/charts/sync/templates/monitoring.yaml create mode 100644 .github/helm/affine/charts/sync/templates/service.yaml create mode 100644 .github/helm/affine/charts/sync/templates/serviceaccount.yaml create mode 100644 .github/helm/affine/charts/sync/templates/tests/test-connection.yaml create mode 100644 .github/helm/affine/charts/sync/values.yaml create mode 100644 .github/workflows/deploy.yml create mode 100644 LICENSE-MPL2.0 create mode 100644 apps/core/.webpack/s3-plugin.ts create mode 100644 apps/core/src/adapters/cloud/crud.ts create mode 100644 apps/core/src/adapters/cloud/ui.tsx create mode 100644 apps/core/src/adapters/public-cloud/ui.tsx create mode 100644 apps/core/src/components/adapter-worksapce-wrapper.tsx create mode 100644 apps/core/src/components/affine/any-error-boundary/index.tsx create mode 100644 apps/core/src/components/affine/auth/after-sign-in-send-email.tsx create mode 100644 apps/core/src/components/affine/auth/after-sign-up-send-email.tsx create mode 100644 apps/core/src/components/affine/auth/callback-url.ts create mode 100644 apps/core/src/components/affine/auth/index.tsx create mode 100644 apps/core/src/components/affine/auth/send-email.tsx create mode 100644 apps/core/src/components/affine/auth/sign-in-with-password.tsx create mode 100644 apps/core/src/components/affine/auth/sign-in.tsx create mode 100644 apps/core/src/components/affine/auth/style.css.ts delete mode 100644 apps/core/src/components/affine/new-workspace-setting-detail/delete-leave-workspace/leave/index.tsx delete mode 100644 apps/core/src/components/affine/new-workspace-setting-detail/delete-leave-workspace/leave/style.ts create mode 100644 apps/core/src/components/affine/new-workspace-setting-detail/members.tsx create mode 100644 apps/core/src/components/affine/setting-modal/account-setting/style.css.ts create mode 100644 apps/core/src/components/affine/share-page-modal/index.tsx create mode 100644 apps/core/src/components/cloud/login-card.tsx create mode 100644 apps/core/src/components/cloud/provider.tsx create mode 100644 apps/core/src/hooks/affine/use-curren-login-status.ts create mode 100644 apps/core/src/hooks/affine/use-current-user.ts create mode 100644 apps/core/src/hooks/affine/use-invite-member.ts create mode 100644 apps/core/src/hooks/affine/use-is-shared-page.ts create mode 100644 apps/core/src/hooks/affine/use-is-workspace-owner.ts create mode 100644 apps/core/src/hooks/affine/use-leave-workspace.ts create mode 100644 apps/core/src/hooks/affine/use-members.ts create mode 100644 apps/core/src/hooks/affine/use-mutate-cloud.ts create mode 100644 apps/core/src/hooks/affine/use-revoke-member-permission.ts create mode 100644 apps/core/src/hooks/affine/use-share-link.ts create mode 100644 apps/core/src/hooks/affine/use-toggle-cloud-public.ts delete mode 100644 apps/core/src/hooks/use-transform-workspace.ts create mode 100644 apps/core/src/pages/auth.tsx create mode 100644 apps/core/src/pages/expired.tsx create mode 100644 apps/core/src/pages/invite.tsx create mode 100644 apps/core/src/pages/share/detail-page.tsx create mode 100644 apps/core/src/pages/sign-in.tsx create mode 100644 apps/core/src/utils/email-regex.ts create mode 100644 apps/electron/src/main/config.ts create mode 100644 apps/electron/src/main/deep-link.ts create mode 100644 apps/electron/src/main/ui/events.ts create mode 100644 apps/electron/src/main/ui/handlers.ts create mode 100644 apps/electron/src/main/ui/subject.ts create mode 100644 apps/electron/src/main/utils.ts create mode 100644 apps/server/LICENSE create mode 100644 apps/server/migrations/20230705025556_workspace_id_fkey/migration.sql create mode 100644 apps/server/migrations/20230706065816_workspace_subpage/migration.sql create mode 100644 apps/server/migrations/20230706090316_change_avatar_url_field_name/migration.sql create mode 100644 apps/server/migrations/20230709091238_fix_blob_types/migration.sql create mode 100644 apps/server/migrations/20230713022301_update_manager/migration.sql create mode 100644 apps/server/migrations/20230714065216_snapshot_id/migration.sql create mode 100644 apps/server/migrations/20230717084417_remove_update_fkey/migration.sql create mode 100644 apps/server/migrations/20230822071646_add_new_features_waiting_list/migration.sql create mode 100644 apps/server/migrations/20230824091506_euser_email_is_not_nullable/migration.sql create mode 100644 apps/server/src/constants.ts create mode 100644 apps/server/src/graphql/logger-plugin.ts create mode 100644 apps/server/src/metrics/controller.ts create mode 100644 apps/server/src/metrics/index.ts create mode 100644 apps/server/src/metrics/metrics.ts create mode 100644 apps/server/src/metrics/utils.ts create mode 100644 apps/server/src/middleware/timing.ts create mode 100644 apps/server/src/modules/auth/mailer/index.ts create mode 100644 apps/server/src/modules/auth/mailer/mail.service.ts create mode 100644 apps/server/src/modules/auth/mailer/mailer.ts create mode 100644 apps/server/src/modules/auth/mailer/template.ts create mode 100644 apps/server/src/modules/auth/next-auth-options.ts create mode 100644 apps/server/src/modules/doc/index.ts create mode 100644 apps/server/src/modules/doc/manager.ts create mode 100644 apps/server/src/modules/doc/redis-manager.ts create mode 100644 apps/server/src/modules/sync/events/events.gateway.ts create mode 100644 apps/server/src/modules/sync/events/events.module.ts create mode 100644 apps/server/src/modules/sync/events/workspace.ts create mode 100644 apps/server/src/modules/sync/index.ts create mode 100644 apps/server/src/modules/sync/redis-adapter.ts create mode 100644 apps/server/src/modules/sync/utils.ts create mode 100644 apps/server/src/modules/users/types.ts create mode 100644 apps/server/src/modules/workspaces/utils.ts create mode 100644 apps/server/src/tests/doc.spec.ts create mode 100644 apps/server/src/tests/mailer.spec.ts create mode 100644 apps/server/src/tests/prometheus-metrics.spec.ts create mode 100644 apps/server/src/tests/user.spec.ts create mode 100644 apps/server/src/tests/utils.ts create mode 100644 apps/server/src/tests/workspace-blobs.spec.ts create mode 100644 apps/server/src/tests/workspace-invite.spec.ts create mode 100644 apps/server/src/utils/doc.ts create mode 100644 packages/component/src/components/auth-components/auth-content.tsx create mode 100644 packages/component/src/components/auth-components/auth-input.tsx create mode 100644 packages/component/src/components/auth-components/auth-page-container.tsx create mode 100644 packages/component/src/components/auth-components/back-button.tsx create mode 100644 packages/component/src/components/auth-components/change-email-page.tsx create mode 100644 packages/component/src/components/auth-components/change-password-page.tsx create mode 100644 packages/component/src/components/auth-components/index.tsx create mode 100644 packages/component/src/components/auth-components/logo.tsx create mode 100644 packages/component/src/components/auth-components/modal-header.tsx create mode 100644 packages/component/src/components/auth-components/modal.tsx create mode 100644 packages/component/src/components/auth-components/password-input/error.tsx create mode 100644 packages/component/src/components/auth-components/password-input/index.tsx create mode 100644 packages/component/src/components/auth-components/password-input/style.css.ts create mode 100644 packages/component/src/components/auth-components/password-input/success.tsx create mode 100644 packages/component/src/components/auth-components/password-input/tag.tsx create mode 100644 packages/component/src/components/auth-components/resend-button.tsx create mode 100644 packages/component/src/components/auth-components/set-password-page.tsx create mode 100644 packages/component/src/components/auth-components/set-password.tsx create mode 100644 packages/component/src/components/auth-components/share.css.ts create mode 100644 packages/component/src/components/auth-components/sign-in-page-container.tsx create mode 100644 packages/component/src/components/auth-components/sign-in-success-page.tsx create mode 100644 packages/component/src/components/auth-components/sign-up-page.tsx create mode 100644 packages/component/src/components/auth-components/utils.ts create mode 100644 packages/component/src/components/member-components/accept-invite-page.tsx create mode 100644 packages/component/src/components/member-components/index.tsx create mode 100644 packages/component/src/components/member-components/invite-modal.tsx create mode 100644 packages/component/src/components/member-components/styles.css.tsx delete mode 100644 packages/component/src/components/share-menu/export.tsx create mode 100644 packages/component/src/components/share-menu/index.jotai.ts create mode 100644 packages/component/src/components/share-menu/share-export.tsx delete mode 100644 packages/component/src/components/share-menu/share-workspace.tsx rename {apps/core/public => packages/component/src}/fonts/inter/Inter-VariableFont_slnt,wght.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/inter/OFL.txt (100%) rename {apps/core/public => packages/component/src}/fonts/kalam/Kalam-Bold.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/kalam/Kalam-Light.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/kalam/Kalam-Regular.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/kalam/OFL.txt (100%) rename {apps/core/public => packages/component/src}/fonts/source-code-pro/OFL.txt (100%) rename {apps/core/public => packages/component/src}/fonts/source-code-pro/SourceCodePro-Italic-VariableFont_wght.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-code-pro/SourceCodePro-VariableFont_wght.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/OFL.txt (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-Bold.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-BoldItalic.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-Italic-VariableFont_opsz,wght.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-Italic.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-Light.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-LightItalic.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-Medium.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-MediumItalic.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-Regular.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-SemiBold.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-SemiBoldItalic.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/source-serif-4/SourceSerif4-VariableFont_opsz,wght.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/space-mono/OFL.txt (100%) rename {apps/core/public => packages/component/src}/fonts/space-mono/SpaceMono-Bold.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/space-mono/SpaceMono-BoldItalic.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/space-mono/SpaceMono-Italic.ttf (100%) rename {apps/core/public => packages/component/src}/fonts/space-mono/SpaceMono-Regular.ttf (100%) delete mode 100644 packages/component/src/ui/input/index.css.ts create mode 100644 packages/component/src/ui/input/style.css.ts create mode 100644 packages/component/src/ui/modal/confirm-modal.tsx create mode 100644 packages/graphql/src/graphql/blob-delete.gql create mode 100644 packages/graphql/src/graphql/blob-list.gql create mode 100644 packages/graphql/src/graphql/blob-set.gql create mode 100644 packages/graphql/src/graphql/change-email.gql create mode 100644 packages/graphql/src/graphql/change-password.gql create mode 100644 packages/graphql/src/graphql/delete-account.gql create mode 100644 packages/graphql/src/graphql/delete-workspace.gql create mode 100644 packages/graphql/src/graphql/get-current-user.gql create mode 100644 packages/graphql/src/graphql/get-invite-info.gql create mode 100644 packages/graphql/src/graphql/get-is-owner.gql create mode 100644 packages/graphql/src/graphql/get-members-by-workspace-id.gql create mode 100644 packages/graphql/src/graphql/get-public-workspace.gql create mode 100644 packages/graphql/src/graphql/get-user.gql create mode 100644 packages/graphql/src/graphql/get-workspace-public-by-id.gql create mode 100644 packages/graphql/src/graphql/get-workspace-shared-pages.gql create mode 100644 packages/graphql/src/graphql/get-workspace.gql create mode 100644 packages/graphql/src/graphql/get-workspaces.gql create mode 100644 packages/graphql/src/graphql/leave-workspace.gql create mode 100644 packages/graphql/src/graphql/revoke-member-permission.gql create mode 100644 packages/graphql/src/graphql/revoke-page.gql create mode 100644 packages/graphql/src/graphql/send-change-email.gql create mode 100644 packages/graphql/src/graphql/send-change-password-email.gql create mode 100644 packages/graphql/src/graphql/send-set-password-email.gql create mode 100644 packages/graphql/src/graphql/set-revoke-page.gql create mode 100644 packages/graphql/src/graphql/set-share-page.gql create mode 100644 packages/graphql/src/graphql/set-workspace-public-by-id.gql create mode 100644 packages/graphql/src/graphql/share-page.gql create mode 100644 packages/graphql/src/graphql/sign-in.gql create mode 100644 packages/graphql/src/graphql/sign-up.gql create mode 100644 packages/graphql/src/graphql/workspace-intive-by-email.gql create mode 100644 packages/graphql/src/graphql/workspace-invite-accept-by-invite-id.gql create mode 100644 packages/graphql/src/graphql/workspace-invite-accept-by-workspace-id.gql delete mode 100644 packages/graphql/src/graphql/workspace.gql create mode 100644 packages/graphql/src/utils.ts delete mode 100644 packages/hooks/src/use-block-suite-workspace-page-is-public.ts delete mode 100644 packages/storage/Cargo.lock rename {apps/core/src/shared => packages/workspace/src/affine}/__tests__/gql.spec.tsx (100%) rename {apps/core/src/shared => packages/workspace/src/affine}/gql.ts (74%) create mode 100644 packages/workspace/src/affine/index.ts create mode 100644 packages/workspace/src/affine/utils.ts create mode 100644 packages/workspace/src/blob/cloud-blob-storage.ts create mode 100644 packages/workspace/src/providers/__tests__/socketio-provider.spec.ts create mode 100644 packages/workspace/src/providers/cloud/index.ts create mode 100644 plugins/outline/src/atom.ts create mode 100644 tests/affine-cloud/e2e/basic.spec.ts create mode 100644 tests/affine-cloud/e2e/login.spec.ts create mode 100644 tests/affine-cloud/package.json create mode 100644 tests/affine-cloud/playwright.config.ts create mode 100644 tests/affine-cloud/tsconfig.json create mode 100644 tests/kit/utils/cloud.ts diff --git a/.env.template b/.env.template index a1b60c7978..9c32c6d064 100644 --- a/.env.template +++ b/.env.template @@ -9,3 +9,5 @@ ENABLE_NEW_SETTING_UNSTABLE_API= ENABLE_NOTIFICATION_CENTER= ENABLE_CLOUD= ENABLE_MOVE_DATABASE= +SHOULD_REPORT_TRACE= +TRACE_REPORT_ENDPOINT= diff --git a/.eslintrc.js b/.eslintrc.js index 20d1fe1f71..57cbb897e3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,6 +31,12 @@ const createPattern = packageName => [ message: 'Use `useNavigateHelper` instead', importNames: ['useNavigate'], }, + { + group: ['next-auth/react'], + message: "Import hooks from 'use-current-user.tsx'", + // useSession is type unsafe + importNames: ['useSession'], + }, { group: ['yjs'], message: 'Do not use this API because it has a bug', @@ -160,6 +166,12 @@ const config = { message: 'Use `useNavigateHelper` instead', importNames: ['useNavigate'], }, + { + group: ['next-auth/react'], + message: "Import hooks from 'use-current-user.tsx'", + // useSession is type unsafe + importNames: ['useSession'], + }, { group: ['yjs'], message: 'Do not use this API because it has a bug', diff --git a/.github/actions/build-rust/action.yml b/.github/actions/build-rust/action.yml index 4c86ded84c..7b28d9215e 100644 --- a/.github/actions/build-rust/action.yml +++ b/.github/actions/build-rust/action.yml @@ -4,6 +4,9 @@ inputs: target: description: 'Cargo target' required: true + package: + description: 'Package to build' + required: true nx_token: description: 'Nx Cloud access token' required: false @@ -31,7 +34,7 @@ runs: if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }} shell: bash run: | - yarn nx build @affine/native --target ${{ inputs.target }} + yarn nx build ${{ inputs.package }} --target ${{ inputs.target }} env: NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }} @@ -44,7 +47,8 @@ runs: run: | export CC=x86_64-unknown-linux-gnu-gcc export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc - yarn nx build @affine/native --target ${{ inputs.target }} + export RUSTFLAGS="-C debuginfo=1" + yarn nx build ${{ inputs.package }} --target ${{ inputs.target }} chmod -R 777 node_modules/.cache chmod -R 777 target @@ -55,6 +59,7 @@ runs: image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }} run: | - yarn nx build @affine/native --target ${{ inputs.target }} + export RUSTFLAGS="-C debuginfo=1" + yarn nx build ${{ inputs.package }} --target ${{ inputs.target }} chmod -R 777 node_modules/.cache chmod -R 777 target diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml new file mode 100644 index 0000000000..703d058721 --- /dev/null +++ b/.github/actions/deploy/action.yml @@ -0,0 +1,50 @@ +name: 'Deploy to Cluster' +description: 'Deploy AFFiNE Cloud to cluster' +inputs: + build-type: + description: 'Align with App build type, canary|beta|stable|internal' + default: 'canary' + gcp-project-number: + description: 'GCP project number' + required: true + gcp-project-id: + description: 'GCP project id' + required: true + service-account: + description: 'Service account' + cluster-name: + description: 'Cluster name' + cluster-location: + description: 'Cluster location' + +runs: + using: 'composite' + steps: + - name: Setup Git short hash + shell: bash + run: | + echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV" + - uses: azure/setup-helm@v3 + - id: auth + uses: google-github-actions/auth@v1 + with: + workload_identity_provider: 'projects/${{ inputs.gcp-project-number }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions-helm-deploy' + service_account: '${{ inputs.service-account }}' + token_format: 'access_token' + project_id: '${{ inputs.gcp-project-id }}' + + - name: 'Setup gcloud cli' + uses: 'google-github-actions/setup-gcloud@v1' + with: + install_components: 'gke-gcloud-auth-plugin' + + - id: get-gke-credentials + shell: bash + run: | + gcloud container clusters get-credentials ${{ inputs.cluster-name }} --region ${{ inputs.cluster-location }} --project ${{ inputs.gcp-project-id }} + + - name: Deploy + shell: bash + run: node ./.github/actions/deploy/deploy.mjs + env: + BUILD_TYPE: '${{ inputs.build-type }}' diff --git a/.github/actions/deploy/deploy.mjs b/.github/actions/deploy/deploy.mjs new file mode 100644 index 0000000000..f2135915ee --- /dev/null +++ b/.github/actions/deploy/deploy.mjs @@ -0,0 +1,116 @@ +import { execSync } from 'node:child_process'; + +const { + BUILD_TYPE, + DEPLOY_HOST, + CANARY_DEPLOY_HOST, + GIT_SHORT_HASH, + DATABASE_URL, + DATABASE_USERNAME, + DATABASE_PASSWORD, + DATABASE_NAME, + R2_ACCOUNT_ID, + R2_ACCESS_KEY_ID, + R2_SECRET_ACCESS_KEY, + R2_BUCKET, + OAUTH_EMAIL_SENDER, + OAUTH_EMAIL_LOGIN, + OAUTH_EMAIL_PASSWORD, + AFFINE_GOOGLE_CLIENT_ID, + AFFINE_GOOGLE_CLIENT_SECRET, + CLOUD_SQL_IAM_ACCOUNT, + GCLOUD_CONNECTION_NAME, + GCLOUD_CLOUD_SQL_INTERNAL_ENDPOINT, + REDIS_HOST, + REDIS_PASSWORD, +} = process.env; + +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +const buildType = BUILD_TYPE || 'canary'; + +const isProduction = buildType === 'stable'; +const isBeta = buildType === 'beta'; + +const createHelmCommand = ({ isDryRun }) => { + const flag = isDryRun ? '--dry-run' : '--atomic'; + const imageTag = `${buildType}-${GIT_SHORT_HASH}`; + const staticIpName = isProduction + ? 'affine-cluster-production' + : isBeta + ? 'affine-cluster-beta' + : 'affine-cluster-dev'; + const redisAndPostgres = + isProduction || isBeta + ? [ + `--set-string global.database.url=${DATABASE_URL}`, + `--set-string global.database.user=${DATABASE_USERNAME}`, + `--set-string global.database.password=${DATABASE_PASSWORD}`, + `--set-string global.database.name=${DATABASE_NAME}`, + `--set global.database.gcloud.enabled=true`, + `--set-string global.database.gcloud.connectionName="${GCLOUD_CONNECTION_NAME}"`, + `--set-string global.database.gcloud.cloudSqlInternal="${GCLOUD_CLOUD_SQL_INTERNAL_ENDPOINT}"`, + `--set-string global.redis.host="${REDIS_HOST}"`, + `--set-string global.redis.password="${REDIS_PASSWORD}"`, + ] + : []; + const serviceAnnotations = + isProduction || isBeta + ? [ + `--set-json web.service.annotations=\"{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }\"`, + `--set-json graphql.serviceAccount.annotations=\"{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }\"`, + `--set-json graphql.service.annotations=\"{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }\"`, + `--set-json sync.serviceAccount.annotations=\"{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }\"`, + `--set-json sync.service.annotations=\"{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }\"`, + ] + : []; + const webReplicaCount = isProduction ? 3 : isBeta ? 2 : 1; + const graphqlReplicaCount = isProduction ? 3 : isBeta ? 2 : 1; + const syncReplicaCount = isProduction ? 6 : isBeta ? 3 : 1; + const namespace = isProduction ? 'production' : isBeta ? 'beta' : 'dev'; + const deployCommand = [ + `helm upgrade --install affine .github/helm/affine`, + `--namespace ${namespace}`, + `--set global.ingress.enabled=true`, + `--set-json global.ingress.annotations=\"{ \\"kubernetes.io/ingress.class\\": \\"gce\\", \\"kubernetes.io/ingress.allow-http\\": \\"true\\", \\"kubernetes.io/ingress.global-static-ip-name\\": \\"${staticIpName}\\" }\"`, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + `--set-string global.ingress.host="${DEPLOY_HOST || CANARY_DEPLOY_HOST}"`, + ...redisAndPostgres, + `--set web.replicaCount=${webReplicaCount}`, + `--set-string web.image.tag="${imageTag}"`, + `--set graphql.replicaCount=${graphqlReplicaCount}`, + `--set-string graphql.image.tag="${imageTag}"`, + `--set graphql.app.objectStorage.r2.enabled=true`, + `--set-string graphql.app.objectStorage.r2.accountId="${R2_ACCOUNT_ID}"`, + `--set-string graphql.app.objectStorage.r2.accessKeyId="${R2_ACCESS_KEY_ID}"`, + `--set-string graphql.app.objectStorage.r2.secretAccessKey="${R2_SECRET_ACCESS_KEY}"`, + `--set-string graphql.app.objectStorage.r2.bucket="${R2_BUCKET}"`, + `--set-string graphql.app.oauth.email.sender="${OAUTH_EMAIL_SENDER}"`, + `--set-string graphql.app.oauth.email.login="${OAUTH_EMAIL_LOGIN}"`, + `--set-string graphql.app.oauth.email.password="${OAUTH_EMAIL_PASSWORD}"`, + `--set-string graphql.app.oauth.google.enabled=true`, + `--set-string graphql.app.oauth.google.clientId="${AFFINE_GOOGLE_CLIENT_ID}"`, + `--set-string graphql.app.oauth.google.clientSecret="${AFFINE_GOOGLE_CLIENT_SECRET}"`, + `--set graphql.app.experimental.enableJwstCodec=true`, + `--set sync.replicaCount=${syncReplicaCount}`, + `--set-string sync.image.tag="${imageTag}"`, + ...serviceAnnotations, + `--version "0.0.0-${buildType}.${GIT_SHORT_HASH}" --timeout 10m`, + flag, + ].join(' '); + return deployCommand; +}; + +const output = execSync(createHelmCommand({ isDryRun: true }), { + encoding: 'utf-8', + stdio: ['inherit', 'pipe', 'inherit'], +}); +const templates = output + .split('---') + .filter(yml => !yml.split('\n').some(line => line.trim() === 'kind: Secret')) + .join('---'); +console.log(templates); + +execSync(createHelmCommand({ isDryRun: false }), { + encoding: 'utf-8', + stdio: 'inherit', +}); diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml deleted file mode 100644 index 6a8500c5cd..0000000000 --- a/.github/actions/setup-rust/action.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: 'AFFiNE Rust setup' -description: 'Rust setup, including cache configuration' -inputs: - target: - description: 'Cargo target' - required: true - toolchain: - description: 'Rustup toolchain' - required: false - default: 'stable' - -runs: - using: 'composite' - steps: - - name: Setup Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: ${{ inputs.toolchain }} - targets: ${{ inputs.target }} - - - name: Cache cargo - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: cargo-cache-${{ runner.os }}-${{ inputs.toolchain }}-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - cargo-cache-${{ runner.os }}-${{ inputs.toolchain }}- diff --git a/.github/deployment/front/affine.nginx.conf b/.github/deployment/front/affine.nginx.conf index f5a2ec0c15..c424ee3655 100644 --- a/.github/deployment/front/affine.nginx.conf +++ b/.github/deployment/front/affine.nginx.conf @@ -3,7 +3,7 @@ server { root /app/dist; location / { - try_files $uri $uri/index.html $uri.html =404; + try_files $uri $uri/ /index.html; } error_page 404 /404.html; diff --git a/.github/helm/affine/charts/graphql/templates/_helpers.tpl b/.github/helm/affine/charts/graphql/templates/_helpers.tpl index fc5851ede6..3e9d608e14 100644 --- a/.github/helm/affine/charts/graphql/templates/_helpers.tpl +++ b/.github/helm/affine/charts/graphql/templates/_helpers.tpl @@ -40,6 +40,7 @@ helm.sh/chart: {{ include "graphql.chart" . }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} +monitoring: enabled {{- end }} {{/* @@ -75,58 +76,3 @@ key: {{ $secret.data.private }} key: {{ genPrivateKey "ecdsa" | b64enc }} {{- end -}} {{- end -}} - -{{- define "objectStorage.r2" -}} -{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.objectStorage.r2.secretName -}} -{{- if $secret -}} -{{/* - Reusing existing secret data -*/}} -accountId: {{ $secret.data.accountId }} -accessKeyId: {{ $secret.data.accessKeyId }} -secretAccessKey: {{ $secret.data.secretAccessKey }} -bucket: {{ $secret.data.bucket }} -{{- else -}} -{{/* - Generate new data -*/}} -accountId: {{ .Values.app.objectStorage.r2.accountId | b64enc }} -accessKeyId: {{ .Values.app.objectStorage.r2.accessKeyId | b64enc }} -secretAccessKey: {{ .Values.app.objectStorage.r2.secretAccessKey | b64enc }} -bucket: {{ .Values.app.objectStorage.r2.bucket | b64enc }} -{{- end -}} -{{- end -}} - -{{- define "objectStorage.oauth.google" -}} -{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.oauth.google.secretName -}} -{{- if $secret -}} -{{/* - Reusing existing secret data -*/}} -clientId: {{ $secret.data.clientId }} -clientSecret: {{ $secret.data.clientSecret }} -{{- else -}} -{{/* - Generate new data -*/}} -clientId: "{{ .Values.app.oauth.google.clientId | b64enc }}" -clientSecret: "{{ .Values.app.oauth.google.clientSecret | b64enc }}" -{{- end -}} -{{- end -}} - -{{- define "objectStorage.oauth.github" -}} -{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.oauth.github.secretName -}} -{{- if $secret -}} -{{/* - Reusing existing secret data -*/}} -clientId: {{ $secret.data.clientId }} -clientSecret: {{ $secret.data.clientSecret }} -{{- else -}} -{{/* - Generate new data -*/}} -clientId: "{{ .Values.app.oauth.github.clientId | b64enc }}" -clientSecret: "{{ .Values.app.oauth.github.clientSecret | b64enc }}" -{{- end -}} -{{- end -}} diff --git a/.github/helm/affine/charts/graphql/templates/deployment.yaml b/.github/helm/affine/charts/graphql/templates/deployment.yaml index 5c8762ee54..41c68fa5fc 100644 --- a/.github/helm/affine/charts/graphql/templates/deployment.yaml +++ b/.github/helm/affine/charts/graphql/templates/deployment.yaml @@ -35,13 +35,36 @@ spec: key: key - name: NODE_ENV value: "{{ .Values.env }}" - - name: DATABSE_PASSWORD + - name: NO_COLOR + value: "1" + - name: SERVER_FLAVOR + value: "graphql" + - name: AFFINE_ENV + value: "{{ .Release.Namespace }}" + - name: NEXTAUTH_URL + value: "{{ .Values.global.ingress.host }}" + - name: DATABASE_PASSWORD valueFrom: secretKeyRef: name: pg-postgresql key: postgres-password - name: DATABASE_URL - value: postgres://{{ .Values.database.user }}:$(DATABSE_PASSWORD)@{{ .Values.database.url }}:{{ .Values.database.port }}/{{ .Values.database.name }} + value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.url }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }} + - name: REDIS_SERVER_ENABLED + value: "true" + - name: REDIS_SERVER_HOST + value: "{{ .Values.global.redis.host }}" + - name: REDIS_SERVER_PORT + value: "{{ .Values.global.redis.port }}" + - name: REDIS_SERVER_USER + value: "{{ .Values.global.redis.username }}" + - name: REDIS_SERVER_PASSWORD + valueFrom: + secretKeyRef: + name: redis + key: redis-password + - name: REDIS_SERVER_DATABASE + value: "{{ .Values.global.redis.database }}" - name: AFFINE_SERVER_PORT value: "{{ .Values.service.port }}" - name: AFFINE_SERVER_SUB_PATH @@ -50,6 +73,37 @@ spec: value: "{{ .Values.app.host }}" - name: ENABLE_R2_OBJECT_STORAGE value: "{{ .Values.app.objectStorage.r2.enabled }}" + - name: OAUTH_EMAIL_SENDER + valueFrom: + secretKeyRef: + name: "{{ .Values.app.oauth.email.secretName }}" + key: sender + - name: OAUTH_EMAIL_LOGIN + valueFrom: + secretKeyRef: + name: "{{ .Values.app.oauth.email.secretName }}" + key: login + - name: OAUTH_EMAIL_SERVER + valueFrom: + secretKeyRef: + name: "{{ .Values.app.oauth.email.secretName }}" + key: server + - name: OAUTH_EMAIL_PORT + valueFrom: + secretKeyRef: + name: "{{ .Values.app.oauth.email.secretName }}" + key: port + - name: OAUTH_EMAIL_PASSWORD + valueFrom: + secretKeyRef: + name: "{{ .Values.app.oauth.email.secretName }}" + key: password + - name: DOC_MERGE_INTERVAL + value: "{{ .Values.app.doc.mergeInterval }}" + {{ if .Values.app.experimental.enableJwstCodec }} + - name: DOC_MERGE_USE_JWST_CODEC + value: "true" + {{ end }} {{ if .Values.app.objectStorage.r2.enabled }} - name: R2_OBJECT_STORAGE_ACCOUNT_ID valueFrom: @@ -112,6 +166,20 @@ spec: initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} resources: {{- toYaml .Values.resources | nindent 12 }} + {{ if .Values.global.database.gcloud.enabled }} + - name: cloud-sql-proxy + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.6.0 + args: + - "--structured-logs" + - "--auto-iam-authn" + - "{{ .Values.global.database.gcloud.connectionName }}" + securityContext: + runAsNonRoot: true + resources: + requests: + memory: "2Gi" + cpu: "1" + {{ end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/.github/helm/affine/charts/graphql/templates/migration.yaml b/.github/helm/affine/charts/graphql/templates/migration.yaml index 4aa885f8eb..b57692be07 100644 --- a/.github/helm/affine/charts/graphql/templates/migration.yaml +++ b/.github/helm/affine/charts/graphql/templates/migration.yaml @@ -5,13 +5,14 @@ metadata: labels: {{- include "graphql.labels" . | nindent 4 }} annotations: - "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook": post-install,pre-upgrade "helm.sh/hook-weight": "-1" "helm.sh/hook-delete-policy": before-hook-creation spec: template: spec: + serviceAccountName: {{ include "graphql.serviceAccountName" . }} containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" @@ -19,13 +20,21 @@ spec: env: - name: NODE_ENV value: "{{ .Values.env }}" - - name: DATABSE_PASSWORD + - name: AFFINE_ENV + value: "{{ .Release.Namespace }}" + - name: DATABASE_PASSWORD valueFrom: secretKeyRef: name: pg-postgresql key: postgres-password + {{ if not .Values.global.database.gcloud.enabled }} - name: DATABASE_URL - value: postgres://{{ .Values.database.user }}:$(DATABSE_PASSWORD)@{{ .Values.database.url }}:{{ .Values.database.port }}/{{ .Values.database.name }} + value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.url }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }} + {{ end }} + {{ if .Values.global.database.gcloud.enabled }} + - name: DATABASE_URL + value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.gcloud.cloudSqlInternal }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }} + {{ end }} resources: requests: cpu: '100m' diff --git a/.github/helm/affine/charts/graphql/templates/monitoring.yaml b/.github/helm/affine/charts/graphql/templates/monitoring.yaml new file mode 100644 index 0000000000..5b7da3aa7c --- /dev/null +++ b/.github/helm/affine/charts/graphql/templates/monitoring.yaml @@ -0,0 +1,13 @@ +{{- if .Values.global.gke.enabled -}} +apiVersion: monitoring.googleapis.com/v1 +kind: PodMonitoring +metadata: + name: "{{ .Chart.Name }}-monitoring" +spec: + selector: + matchLabels: + app.kubernetes.io/name: "{{ include "graphql.name" . }}" + endpoints: + - port: {{ .Values.service.port }} + interval: 30s +{{- end }} diff --git a/.github/helm/affine/charts/graphql/templates/oauth-github-secret.yaml b/.github/helm/affine/charts/graphql/templates/oauth-github-secret.yaml deleted file mode 100644 index 3c9f6f7e7a..0000000000 --- a/.github/helm/affine/charts/graphql/templates/oauth-github-secret.yaml +++ /dev/null @@ -1,10 +0,0 @@ -{{- if .Values.app.oauth.github.enabled -}} -apiVersion: v1 -kind: Secret -metadata: - name: "{{ .Values.app.oauth.github.secretName }}" -type: Opaque -data: -{{- ( include "objectStorage.oauth.github" . ) | indent 2 -}} - -{{- end }} diff --git a/.github/helm/affine/charts/graphql/templates/oauth-google-secret.yaml b/.github/helm/affine/charts/graphql/templates/oauth-google-secret.yaml deleted file mode 100644 index fd939fa63a..0000000000 --- a/.github/helm/affine/charts/graphql/templates/oauth-google-secret.yaml +++ /dev/null @@ -1,10 +0,0 @@ -{{- if .Values.app.oauth.google.enabled -}} -apiVersion: v1 -kind: Secret -metadata: - name: "{{ .Values.app.oauth.google.secretName }}" -type: Opaque -data: -{{- ( include "objectStorage.oauth.google" . ) | indent 2 -}} - -{{- end }} diff --git a/.github/helm/affine/charts/graphql/templates/oauth.yaml b/.github/helm/affine/charts/graphql/templates/oauth.yaml new file mode 100644 index 0000000000..300bcc60ea --- /dev/null +++ b/.github/helm/affine/charts/graphql/templates/oauth.yaml @@ -0,0 +1,33 @@ +apiVersion: v1 +kind: Secret +metadata: + name: "{{ .Values.app.oauth.email.secretName }}" +type: Opaque +data: + sender: "{{ .Values.app.oauth.email.sender | b64enc }}" + login: "{{ .Values.app.oauth.email.login | b64enc }}" + password: "{{ .Values.app.oauth.email.password | b64enc }}" + server: "{{ .Values.app.oauth.email.server | b64enc }}" + port: "{{ .Values.app.oauth.email.port | b64enc }}" +--- +{{- if .Values.app.oauth.google.enabled -}} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ .Values.app.oauth.google.secretName }}" +type: Opaque +data: + clientId: "{{ .Values.app.oauth.google.clientId | b64enc }}" + clientSecret: "{{ .Values.app.oauth.google.clientSecret | b64enc }}" +{{- end }} +--- +{{- if .Values.app.oauth.github.enabled -}} +apiVersion: v1 +kind: Secret +metadata: + name: "{{ .Values.app.oauth.github.secretName }}" +type: Opaque +data: + clientId: "{{ .Values.app.oauth.github.clientId | b64enc }}" + clientSecret: "{{ .Values.app.oauth.github.clientSecret | b64enc }}" +{{- end }} diff --git a/.github/helm/affine/charts/graphql/templates/pg-secret.yaml b/.github/helm/affine/charts/graphql/templates/pg-secret.yaml new file mode 100644 index 0000000000..a52395d27f --- /dev/null +++ b/.github/helm/affine/charts/graphql/templates/pg-secret.yaml @@ -0,0 +1,9 @@ +{{- if .Values.global.database.password -}} +apiVersion: v1 +kind: Secret +metadata: + name: pg-postgresql +type: Opaque +data: + postgres-password: {{ .Values.global.database.password | b64enc }} +{{- end }} diff --git a/.github/helm/affine/charts/graphql/templates/r2-secret.yaml b/.github/helm/affine/charts/graphql/templates/r2-secret.yaml index 50b7336dc7..d4a655b947 100644 --- a/.github/helm/affine/charts/graphql/templates/r2-secret.yaml +++ b/.github/helm/affine/charts/graphql/templates/r2-secret.yaml @@ -5,5 +5,8 @@ metadata: name: "{{ .Values.app.objectStorage.r2.secretName }}" type: Opaque data: - {{- ( include "objectStorage.r2" . ) | indent 2 -}} + accountId: {{ .Values.app.objectStorage.r2.accountId | b64enc }} + accessKeyId: {{ .Values.app.objectStorage.r2.accessKeyId | b64enc }} + secretAccessKey: {{ .Values.app.objectStorage.r2.secretAccessKey | b64enc }} + bucket: {{ .Values.app.objectStorage.r2.bucket | b64enc }} {{- end }} diff --git a/.github/helm/affine/charts/graphql/templates/redis-secret.yaml b/.github/helm/affine/charts/graphql/templates/redis-secret.yaml new file mode 100644 index 0000000000..eebcbc4121 --- /dev/null +++ b/.github/helm/affine/charts/graphql/templates/redis-secret.yaml @@ -0,0 +1,9 @@ +{{- if .Values.global.redis.password -}} +apiVersion: v1 +kind: Secret +metadata: + name: redis +type: Opaque +data: + redis-password: {{ .Values.global.redis.password | b64enc }} +{{- end }} diff --git a/.github/helm/affine/charts/graphql/values.yaml b/.github/helm/affine/charts/graphql/values.yaml index c99969e6ea..bdb270b7e4 100644 --- a/.github/helm/affine/charts/graphql/values.yaml +++ b/.github/helm/affine/charts/graphql/values.yaml @@ -9,16 +9,15 @@ nameOverride: '' fullnameOverride: '' # map to NODE_ENV environment variable env: 'production' -database: - user: 'postgres' - url: 'pg-postgresql' - port: '5432' - name: 'affine' app: + experimental: + enableJwstCodec: true # AFFINE_SERVER_SUB_PATH path: '' # AFFINE_SERVER_HOST host: '0.0.0.0' + doc: + mergeInterval: "3000" jwt: secretName: jwt-private-key # base64 encoded ecdsa private key @@ -32,6 +31,13 @@ app: secretAccessKey: '' bucket: '' oauth: + email: + secretName: 'oauth-email' + sender: 'noreply@toeverything.info' + login: '' + password: '' + server: 'smtp.gmail.com' + port: '465' google: enabled: false secretName: oauth-google @@ -55,11 +61,11 @@ podSecurityContext: resources: limits: - cpu: '2000m' - memory: 4Gi + cpu: '4' + memory: 8Gi requests: - cpu: '1000m' - memory: 2Gi + cpu: '2' + memory: 4Gi probe: initialDelaySeconds: 20 diff --git a/.github/helm/affine/charts/sync/.helmignore b/.github/helm/affine/charts/sync/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/.github/helm/affine/charts/sync/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/.github/helm/affine/charts/sync/Chart.yaml b/.github/helm/affine/charts/sync/Chart.yaml new file mode 100644 index 0000000000..9fde73dfcd --- /dev/null +++ b/.github/helm/affine/charts/sync/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: sync +description: A Helm chart for Kubernetes +type: application +version: 0.0.0 +appVersion: "0.7.0-canary.18" diff --git a/.github/helm/affine/charts/sync/templates/NOTES.txt b/.github/helm/affine/charts/sync/templates/NOTES.txt new file mode 100644 index 0000000000..2852abbb2e --- /dev/null +++ b/.github/helm/affine/charts/sync/templates/NOTES.txt @@ -0,0 +1,16 @@ +1. Get the application URL by running these commands: +{{- if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sync.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sync.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sync.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sync.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/.github/helm/affine/charts/sync/templates/_helpers.tpl b/.github/helm/affine/charts/sync/templates/_helpers.tpl new file mode 100644 index 0000000000..1c0337ff72 --- /dev/null +++ b/.github/helm/affine/charts/sync/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "sync.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "sync.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "sync.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "sync.labels" -}} +helm.sh/chart: {{ include "sync.chart" . }} +{{ include "sync.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +monitoring: enabled +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "sync.selectorLabels" -}} +app.kubernetes.io/name: {{ include "sync.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "sync.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "sync.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/.github/helm/affine/charts/sync/templates/deployment.yaml b/.github/helm/affine/charts/sync/templates/deployment.yaml new file mode 100644 index 0000000000..f1b6c4a99c --- /dev/null +++ b/.github/helm/affine/charts/sync/templates/deployment.yaml @@ -0,0 +1,110 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "sync.fullname" . }} + labels: + {{- include "sync.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "sync.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "sync.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "sync.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: NODE_ENV + value: "{{ .Values.env }}" + - name: NO_COLOR + value: "1" + - name: SERVER_FLAVOR + value: "sync" + - name: NEXTAUTH_URL + value: "{{ .Values.global.ingress.host }}" + - name: AFFINE_ENV + value: "{{ .Release.Namespace }}" + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: pg-postgresql + key: postgres-password + - name: DATABASE_URL + value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.url }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }} + - name: REDIS_SERVER_ENABLED + value: "true" + - name: REDIS_SERVER_HOST + value: "{{ .Values.global.redis.host }}" + - name: REDIS_SERVER_PORT + value: "{{ .Values.global.redis.port }}" + - name: REDIS_SERVER_USER + value: "{{ .Values.global.redis.username }}" + - name: REDIS_SERVER_PASSWORD + valueFrom: + secretKeyRef: + name: redis + key: redis-password + - name: REDIS_SERVER_DATABASE + value: "{{ .Values.global.redis.database }}" + - name: AFFINE_SERVER_PORT + value: "{{ .Values.service.port }}" + - name: AFFINE_SERVER_HOST + value: "{{ .Values.app.host }}" + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{ if .Values.global.database.gcloud.enabled }} + - name: cloud-sql-proxy + image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.6.0 + args: + - "--structured-logs" + - "--auto-iam-authn" + - "{{ .Values.global.database.gcloud.connectionName }}" + securityContext: + runAsNonRoot: true + resources: + requests: + memory: "2Gi" + cpu: "1" + {{ end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/.github/helm/affine/charts/sync/templates/monitoring.yaml b/.github/helm/affine/charts/sync/templates/monitoring.yaml new file mode 100644 index 0000000000..b7a03e6401 --- /dev/null +++ b/.github/helm/affine/charts/sync/templates/monitoring.yaml @@ -0,0 +1,13 @@ +{{- if .Values.global.gke.enabled -}} +apiVersion: monitoring.googleapis.com/v1 +kind: PodMonitoring +metadata: + name: "{{ .Chart.Name }}-monitoring" +spec: + selector: + matchLabels: + app.kubernetes.io/name: "{{ include "sync.name" . }}" + endpoints: + - port: {{ .Values.service.port }} + interval: 30s +{{- end }} diff --git a/.github/helm/affine/charts/sync/templates/service.yaml b/.github/helm/affine/charts/sync/templates/service.yaml new file mode 100644 index 0000000000..02e9cefb70 --- /dev/null +++ b/.github/helm/affine/charts/sync/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "sync.fullname" . }} + labels: + {{- include "sync.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "sync.selectorLabels" . | nindent 4 }} diff --git a/.github/helm/affine/charts/sync/templates/serviceaccount.yaml b/.github/helm/affine/charts/sync/templates/serviceaccount.yaml new file mode 100644 index 0000000000..c03fe228a5 --- /dev/null +++ b/.github/helm/affine/charts/sync/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "sync.serviceAccountName" . }} + labels: + {{- include "sync.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/.github/helm/affine/charts/sync/templates/tests/test-connection.yaml b/.github/helm/affine/charts/sync/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..59f29465a7 --- /dev/null +++ b/.github/helm/affine/charts/sync/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "sync.fullname" . }}-test-connection" + labels: + {{- include "sync.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "sync.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/.github/helm/affine/charts/sync/values.yaml b/.github/helm/affine/charts/sync/values.yaml new file mode 100644 index 0000000000..513a512cb2 --- /dev/null +++ b/.github/helm/affine/charts/sync/values.yaml @@ -0,0 +1,39 @@ +replicaCount: 1 +image: + repository: ghcr.io/toeverything/affine-graphql + pullPolicy: IfNotPresent + tag: '' + +imagePullSecrets: [] +nameOverride: '' +fullnameOverride: '' +# map to NODE_ENV environment variable +env: 'production' +app: + # AFFINE_SERVER_HOST + host: '0.0.0.0' + +serviceAccount: + create: true + annotations: {} + name: 'affine-sync' + +podAnnotations: {} + +podSecurityContext: + fsGroup: 2000 + +resources: + limits: + cpu: '4' + memory: 8Gi + requests: + cpu: '2' + memory: 4Gi + +probe: + initialDelaySeconds: 20 + +nodeSelector: {} +tolerations: [] +affinity: {} diff --git a/.github/helm/affine/charts/web/templates/_helpers.tpl b/.github/helm/affine/charts/web/templates/_helpers.tpl index dff203a79d..46e6f5eba3 100644 --- a/.github/helm/affine/charts/web/templates/_helpers.tpl +++ b/.github/helm/affine/charts/web/templates/_helpers.tpl @@ -40,6 +40,7 @@ helm.sh/chart: {{ include "web.chart" . }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} +monitoring: enabled {{- end }} {{/* diff --git a/.github/helm/affine/templates/ingress.yaml b/.github/helm/affine/templates/ingress.yaml index 9df2d3254a..5a391029d8 100644 --- a/.github/helm/affine/templates/ingress.yaml +++ b/.github/helm/affine/templates/ingress.yaml @@ -1,8 +1,8 @@ -{{- if .Values.ingress.enabled -}} +{{- if .Values.global.ingress.enabled -}} {{- $fullName := include "affine.fullname" . -}} -{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} - {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} +{{- if and .Values.global.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.global.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.global.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} {{- end }} {{- end }} {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} @@ -17,17 +17,17 @@ metadata: name: {{ $fullName }} labels: {{- include "affine.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} + {{- with .Values.global.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} + {{- if and .Values.global.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.global.ingress.className }} {{- end }} - {{- if .Values.ingress.tls }} + {{- if .Values.global.ingress.tls }} tls: - {{- range .Values.ingress.tls }} + {{- range .Values.global.ingress.tls }} - hosts: {{- range .hosts }} - {{ . | quote }} @@ -36,9 +36,16 @@ spec: {{- end }} {{- end }} rules: - - host: "{{ .Values.ingress.host }}" + - host: "{{ .Values.global.ingress.host }}" http: paths: + - path: /socket.io + pathType: Prefix + backend: + service: + name: affine-sync + port: + number: {{ .Values.sync.service.port }} - path: /graphql pathType: Prefix backend: @@ -60,5 +67,4 @@ spec: name: affine-web port: number: {{ .Values.web.service.port }} - {{- end }} diff --git a/.github/helm/affine/values.yaml b/.github/helm/affine/values.yaml index 0bf6aaa9b5..b545130912 100644 --- a/.github/helm/affine/values.yaml +++ b/.github/helm/affine/values.yaml @@ -1,16 +1,43 @@ -ingress: - enabled: false - className: '' - annotations: - kubernetes.io/ingress.class: nginx - host: affine.pro - tls: [] +global: + ingress: + enabled: false + className: '' + host: affine.pro + tls: [] + database: + user: 'postgres' + url: 'pg-postgresql' + port: '5432' + name: 'affine' + password: '' + gcloud: + enabled: false + # use for migration + cloudSqlInternal: '' + connectionName: '' + serviceAccount: '' + redis: + enabled: true + host: 'redis-master' + port: '6379' + username: '' + password: '' + database: 0 + gke: + enabled: true graphql: service: type: ClusterIP port: 3000 +sync: + service: + type: ClusterIP + port: 3010 + annotations: + cloud.google.com/backend-config: '{"default": "affine-backendconfig"}' + web: service: type: ClusterIP diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 85c6bc72d5..94b28e89a1 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -105,6 +105,7 @@ jobs: uses: ./.github/actions/build-rust with: target: ${{ matrix.spec.target }} + package: '@affine/native' nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - name: Run unit tests if: ${{ matrix.spec.test }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cebc9b6068..dbbfc68795 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,6 +29,7 @@ env: DEBUG: napi:* BUILD_TYPE: canary APP_NAME: affine + AFFINE_ENV: dev COVERAGE: true DISTRIBUTION: browser MACOSX_DEPLOYMENT_TARGET: '10.13' @@ -113,30 +114,11 @@ jobs: env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - build-storybook: - name: Build Storybook - runs-on: ubuntu-latest - environment: development - - steps: - - uses: actions/checkout@v3 - - name: Setup Node.js - uses: ./.github/actions/setup-node - with: - electron-install: false - - run: yarn nx build @affine/storybook - env: - NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - - name: Upload storybook artifact - uses: actions/upload-artifact@v3 - with: - name: storybook - path: ./apps/storybook/storybook-static - if-no-files-found: error - build-storage: name: Build Storage runs-on: ubuntu-latest + env: + RUSTFLAGS: '-C debuginfo=1' environment: development steps: @@ -144,11 +126,11 @@ jobs: - name: Setup Node.js uses: ./.github/actions/setup-node - name: Setup Rust - uses: ./.github/actions/setup-rust + uses: ./.github/actions/build-rust with: target: 'x86_64-unknown-linux-gnu' - - name: Build Storage - run: yarn build:storage + package: '@affine/storage' + nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - name: Upload storage.node uses: actions/upload-artifact@v3 with: @@ -216,6 +198,81 @@ jobs: name: affine fail_ci_if_error: false + server-e2e-test: + name: Server E2E Test + runs-on: ubuntu-latest + environment: development + needs: build-storage + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: affine + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: ./.github/actions/setup-node + with: + playwright-install: true + - name: Initialize database + run: | + psql -h localhost -U postgres -c "CREATE DATABASE affine;" + psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';" + psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;" + env: + PGPASSWORD: affine + - name: Generate prisma client + run: | + yarn exec prisma generate + yarn exec prisma db push + working-directory: apps/server + env: + DATABASE_URL: postgresql://affine:affine@localhost:5432/affine + - name: Run init-db script + run: yarn exec ts-node-esm ./scripts/init-db.ts + working-directory: apps/server + env: + DATABASE_URL: postgresql://affine:affine@localhost:5432/affine + - name: Download storage.node + uses: actions/download-artifact@v3 + with: + name: storage.node + path: ./apps/server + + - name: Run playwright tests + run: yarn e2e --forbid-only + working-directory: tests/affine-cloud + env: + COVERAGE: true + DATABASE_URL: postgresql://affine:affine@localhost:5432/affine + + - name: Collect code coverage report + run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov + + - name: Upload e2e test coverage results + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./.coverage/lcov.info + flags: server-e2etest + name: affine + fail_ci_if_error: false + + - name: Upload test results + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: test-results-e2e-server + path: ./tests/affine-cloud/test-results + if-no-files-found: ignore + e2e-plugin-test: name: E2E Plugin Test runs-on: ubuntu-latest diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..7c85dbd268 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,211 @@ +name: Deploy + +on: + push: + branches: + - master + workflow_dispatch: + inputs: + flavor: + description: 'Build type (canary, beta, internal or stable)' + type: string + default: canary + +env: + BUILD_TYPE: canary + APP_NAME: affine + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + +jobs: + build-server: + name: Build Server + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.flavor }} + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: ./.github/actions/setup-node + with: + electron-install: false + - name: Build Server + run: yarn workspace @affine/server build + - name: Upload server dist + uses: actions/upload-artifact@v3 + with: + name: server-dist + path: ./apps/server/dist + if-no-files-found: error + build-core: + name: Build @affine/core + runs-on: ubuntu-latest + environment: production + + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: ./.github/actions/setup-node + - name: Build Plugins + run: yarn run build:plugins + - name: Build Core + run: yarn nx build @affine/core + env: + R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} + R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + BUILD_TYPE_OVERRIDE: ${{ github.event.inputs.flavor }} + SHOULD_REPORT_TRACE: true + TRACE_REPORT_ENDPOINT: ${{ secrets.TRACE_REPORT_ENDPOINT }} + - name: Upload core artifact + uses: actions/upload-artifact@v3 + with: + name: core + path: ./apps/core/dist + if-no-files-found: error + + build-storage: + name: Build Storage + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.flavor }} + + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: ./.github/actions/setup-node + - name: Setup Rust + uses: ./.github/actions/build-rust + with: + target: 'x86_64-unknown-linux-gnu' + package: '@affine/storage' + nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + - name: Upload storage.node + uses: actions/upload-artifact@v3 + with: + name: storage.node + path: ./packages/storage/storage.node + if-no-files-found: error + + build-docker: + name: Build Docker + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.flavor }} + needs: + - build-server + - build-core + - build-storage + steps: + - uses: actions/checkout@v3 + - name: Download core artifact + uses: actions/download-artifact@v3 + with: + name: core + path: ./apps/core/dist + - name: Download server dist + uses: actions/download-artifact@v3 + with: + name: server-dist + path: ./apps/server/dist + - name: Download storage.node + uses: actions/download-artifact@v3 + with: + name: storage.node + path: ./apps/server + - name: Setup env + run: | + echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV" + if [ -z "${{ inputs.flavor }}" ] + then + echo "RELEASE_FLAVOR=canary" >> "$GITHUB_ENV" + else + echo "RELEASE_FLAVOR=${{ inputs.flavor }}" >> "$GITHUB_ENV" + fi + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + logout: false + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Build front Dockerfile + uses: docker/build-push-action@v4 + with: + context: . + push: true + pull: true + platforms: linux/amd64,linux/arm64 + provenance: true + file: .github/deployment/front/Dockerfile + tags: ghcr.io/toeverything/affine-front:${{env.RELEASE_FLAVOR}}-${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-front:${{env.RELEASE_FLAVOR}} + + # setup node without cache configuration + # Prisma cache is not compatible with docker build cache + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + registry-url: https://npm.pkg.github.com + scope: '@toeverything' + + - name: Install Node.js dependencies + run: yarn workspaces focus @affine/server --production + + - name: Generate Prisma client + run: yarn workspace @affine/server prisma generate + + - name: Build graphql Dockerfile + uses: docker/build-push-action@v4 + with: + context: . + push: true + pull: true + platforms: linux/amd64,linux/arm64 + provenance: true + file: .github/deployment/node/Dockerfile + tags: ghcr.io/toeverything/affine-graphql:${{env.RELEASE_FLAVOR}}-${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-front:${{env.RELEASE_FLAVOR}} + + deploy: + name: Deploy to cluster + environment: ${{ github.event.inputs.flavor }} + permissions: + contents: 'write' + id-token: 'write' + needs: + - build-docker + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Deploy to dev + uses: ./.github/actions/deploy + with: + build-type: ${{ github.event.inputs.flavor }} + gcp-project-number: ${{ secrets.GCP_PROJECT_NUMBER }} + gcp-project-id: ${{ secrets.GCP_PROJECT_ID }} + service-account: ${{ secrets.GCP_HELM_DEPLOY_SERVICE_ACCOUNT }} + cluster-name: ${{ secrets.GCP_CLUSTER_NAME }} + cluster-location: ${{ secrets.GCP_CLUSTER_LOCATION }} + env: + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + CANARY_DEPLOY_HOST: ${{ secrets.CANARY_DEPLOY_HOST }} + R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} + R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + R2_BUCKET: ${{ secrets.R2_BUCKET }} + OAUTH_EMAIL_SENDER: ${{ secrets.OAUTH_EMAIL_SENDER }} + OAUTH_EMAIL_LOGIN: ${{ secrets.OAUTH_EMAIL_LOGIN }} + OAUTH_EMAIL_PASSWORD: ${{ secrets.OAUTH_EMAIL_PASSWORD }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AFFINE_GOOGLE_CLIENT_ID: ${{ secrets.AFFINE_GOOGLE_CLIENT_ID }} + AFFINE_GOOGLE_CLIENT_SECRET: ${{ secrets.AFFINE_GOOGLE_CLIENT_SECRET }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }} + DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + DATABASE_NAME: ${{ secrets.DATABASE_NAME }} + GCLOUD_CONNECTION_NAME: ${{ secrets.GCLOUD_CONNECTION_NAME }} + GCLOUD_CLOUD_SQL_INTERNAL_ENDPOINT: ${{ secrets.GCLOUD_CLOUD_SQL_INTERNAL_ENDPOINT }} + REDIS_HOST: ${{ secrets.REDIS_HOST }} + REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }} + CLOUD_SQL_IAM_ACCOUNT: ${{ secrets.CLOUD_SQL_IAM_ACCOUNT }} diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 0e7745ae06..22981c929e 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -12,7 +12,6 @@ on: - .github/** - '!.github/workflows/nightly-build.yml' - '!.github/actions/build-rust/action.yml' - - '!.github/actions/setup-rust/action.yml' - '!.github/actions/setup-node/action.yml' permissions: @@ -114,6 +113,7 @@ jobs: uses: ./.github/actions/build-rust with: target: ${{ matrix.spec.target }} + package: '@affine/native' nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - name: Replace Version run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }} diff --git a/.github/workflows/publish-storybook.yml b/.github/workflows/publish-storybook.yml index dbd7db584f..de54b52f25 100644 --- a/.github/workflows/publish-storybook.yml +++ b/.github/workflows/publish-storybook.yml @@ -1,5 +1,8 @@ name: Publish Storybook +env: + NODE_OPTIONS: --max-old-space-size=4096 + on: push: branches: @@ -34,5 +37,7 @@ jobs: with: workingDir: apps/storybook buildScriptName: build - onlyChanged: true projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + zip: true + env: + NODE_OPTIONS: ${{ env.NODE_OPTIONS }} diff --git a/.github/workflows/release-desktop-app.yml b/.github/workflows/release-desktop-app.yml index 694e7b30fa..2142a4e8f0 100644 --- a/.github/workflows/release-desktop-app.yml +++ b/.github/workflows/release-desktop-app.yml @@ -112,6 +112,7 @@ jobs: uses: ./.github/actions/build-rust with: target: ${{ matrix.spec.target }} + package: '@affine/native' nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - uses: actions/download-artifact@v3 with: @@ -182,6 +183,7 @@ jobs: uses: ./.github/actions/build-rust with: target: ${{ matrix.spec.target }} + package: '@affine/native' nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - uses: actions/download-artifact@v3 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e8b3005cd..b73ca85f63 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,11 +76,11 @@ jobs: - name: Setup Node.js uses: ./.github/actions/setup-node - name: Setup Rust - uses: ./.github/actions/setup-rust + uses: ./.github/actions/build-rust with: target: 'x86_64-unknown-linux-gnu' - - name: Build Storage - run: yarn build:storage + package: '@affine/storage' + nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} - name: Upload storage.node uses: actions/upload-artifact@v3 with: diff --git a/.gitignore b/.gitignore index 3fb3b199d5..e1e8744557 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,7 @@ node_modules # IDEs and editors -/.idea +**/.idea .project .classpath .c9/ @@ -77,3 +77,4 @@ target tsconfig.node.tsbuildinfo lib affine.db +apps/web/next-routes.conf diff --git a/.prettierignore b/.prettierignore index d65a1b317f..f5977b9717 100644 --- a/.prettierignore +++ b/.prettierignore @@ -14,3 +14,4 @@ _next storybook-static web-static public +apps/server/src/schema.gql diff --git a/.vscode/launch.template.json b/.vscode/launch.template.json index eeb8902687..77681d7a18 100644 --- a/.vscode/launch.template.json +++ b/.vscode/launch.template.json @@ -2,16 +2,24 @@ "version": "0.2.0", "configurations": [ { - "command": "yarn run dev", "name": "Run Dev", + "type": "node-terminal", "request": "launch", - "type": "node-terminal" + "command": "yarn run dev" }, { - "command": "yarn run dev:local", "name": "Run Dev Locally", + "type": "node-terminal", "request": "launch", - "type": "node-terminal" + "command": "yarn run dev:local" + }, + { + "name": "Launch AFFiNE Cloud", + "type": "node", + "request": "launch", + "runtimeExecutable": "yarn", + "cwd": "${workspaceFolder}", + "runtimeArgs": ["workspace", "@affine/server", "dev"] } ] } diff --git a/Cargo.lock b/Cargo.lock index e86a3ee635..7d306fd6ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,31 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "affine_native" version = "0.0.0" @@ -27,13 +52,26 @@ dependencies = [ name = "affine_schema" version = "0.0.0" +[[package]] +name = "affine_storage" +version = "1.0.0" +dependencies = [ + "jwst", + "jwst-codec", + "jwst-storage", + "napi", + "napi-build", + "napi-derive", + "yrs", +] + [[package]] name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom", + "getrandom 0.2.10", "once_cell", "version_check", ] @@ -45,19 +83,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if", + "getrandom 0.2.10", "once_cell", "version_check", ] [[package]] name = "aho-corasick" -version = "1.0.1" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] +[[package]] +name = "aho-corasick" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -68,10 +134,126 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.71" +name = "anstream" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" + +[[package]] +name = "arbitrary" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-compat" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b48b4ff0c2026db683dea961cd8ea874737f56cffca86fa84415eaddc51c00d" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "async-trait" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] [[package]] name = "atoi" @@ -82,12 +264,51 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic_refcell" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112ef6b3f6cb3cb6fc5b6b494ef7a848492cff1ab0ef4de10b0f7d572861c905" + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backon" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a6197b2120bb2185a267f6515038558b019e92b832bb0320e96d66268dcf9" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "pin-project", + "tokio", +] + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.2" @@ -100,6 +321,23 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -108,9 +346,33 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +dependencies = [ + "serde", +] + +[[package]] +name = "bitpacking" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +dependencies = [ + "crunchy", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] [[package]] name = "block-buffer" @@ -121,12 +383,85 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +dependencies = [ + "borsh-derive", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bumpalo" version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +[[package]] +name = "bytecheck" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + [[package]] name = "byteorder" version = "1.4.3" @@ -140,10 +475,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] -name = "cc" -version = "1.0.79" +name = "cang-jie" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "1238ed330d627f47a2309023a7425a4c73cd586ccbda77151e18ece8f9495b92" +dependencies = [ + "jieba-rs", + "log", + "tantivy", +] + +[[package]] +name = "cc" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cedarwood" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d910bedd62c24733263d0bed247460853c9d22e8956bd4cd964302095e04e90" +dependencies = [ + "smallvec", +] + +[[package]] +name = "census" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fafee10a5dd1cffcb5cc560e0d0df8803d7355a2b12272e3557dee57314cb6e" [[package]] name = "cfg-if" @@ -153,24 +518,109 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", - "time", + "serde", + "time 0.1.45", "wasm-bindgen", "winapi", ] [[package]] -name = "const-oid" -version = "0.9.2" +name = "clap" +version = "4.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "const-random" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" +dependencies = [ + "getrandom 0.2.10", + "once_cell", + "proc-macro-hack", + "tiny-keccak", +] [[package]] name = "convert_case" @@ -181,6 +631,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -189,9 +649,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -211,6 +671,15 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -221,6 +690,30 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + [[package]] name = "crossbeam-queue" version = "0.3.8" @@ -233,13 +726,19 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -252,25 +751,69 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4056f63fce3b82d852c3da92b08ea59959890813a7f4ce9c0ff85b10cf301b" +checksum = "1f34ba9a9bcb8645379e9de8cb3ecfcf4d1c85ba66d90deb3259206fa5aa193b" dependencies = [ "quote", - "syn 2.0.18", + "syn 2.0.28", +] + +[[package]] +name = "dashmap" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +dependencies = [ + "cfg-if", + "hashbrown 0.14.0", + "lock_api", + "once_cell", + "parking_lot_core", ] [[package]] name = "der" -version = "0.6.1" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ "const-oid", "pem-rfc7468", "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +dependencies = [ + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "digest" version = "0.10.7" @@ -283,6 +826,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dlv-list" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d529fd73d344663edfd598ccb3f344e46034db51ebd103518eae34338248ad73" +dependencies = [ + "const-random", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -296,19 +848,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] -name = "either" -version = "1.8.1" +name = "downcast-rs" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dyn-clone" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" dependencies = [ "serde", ] [[package]] -name = "errno" -version = "0.3.1" +name = "encoding_rs" +version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-iterator" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7add3873b5dd076766ee79c8e406ad1a472c385476b9e38849f8eec24f1be689" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", @@ -342,6 +941,53 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "exr" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e481eb11a482815d3e9d618db8c42a93207134662873809335a92327440c18" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fail" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5e43d0f78a42ad591453aedb1d7ae631ce7ee445c7643691055a9ed8d3b01c" +dependencies = [ + "log", + "once_cell", + "rand 0.8.5", +] + +[[package]] +name = "fastdivide" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25c7df09945d65ea8d70b3321547ed414bbc540aad5bac6883d021b970f35b04" + +[[package]] +name = "fastfield_codecs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374a3a53c1bd5fb31b10084229290eafb0a05f260ec90f1f726afffda4877a8a" +dependencies = [ + "fastdivide", + "itertools", + "log", + "ownedbytes", + "tantivy-bitpacker", + "tantivy-common", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -352,17 +998,48 @@ dependencies = [ ] [[package]] -name = "filetime" -version = "0.2.21" +name = "fastrand" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filetime" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "windows-sys 0.48.0", ] +[[package]] +name = "flagset" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda653ca797810c02f7ca4b804b40b8b95ae046eb989d356bce17919a8c25499" + +[[package]] +name = "flate2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.10.14" @@ -371,19 +1048,36 @@ checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" dependencies = [ "futures-core", "futures-sink", + "nanorand", "pin-project", "spin 0.9.8", ] [[package]] -name = "form_urlencoded" -version = "1.1.0" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -393,6 +1087,27 @@ dependencies = [ "libc", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -437,6 +1152,17 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "futures-sink" version = "0.3.28" @@ -449,14 +1175,22 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -465,6 +1199,28 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -477,13 +1233,121 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "git2" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc" +dependencies = [ + "bitflags 1.3.2", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "governor" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "quanta", + "rand 0.8.5", + "smallvec", +] + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", ] [[package]] @@ -491,6 +1355,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.6", +] [[package]] name = "hashbrown" @@ -502,12 +1369,22 @@ dependencies = [ ] [[package]] -name = "hashlink" -version = "0.8.2" +name = "hashbrown" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0761a1b9491c4f2e3d66aa0f62d0fba0af9a0e2852e4d48ea506632a4b56e6aa" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ - "hashbrown 0.13.2", + "ahash 0.8.3", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +dependencies = [ + "hashbrown 0.14.0", ] [[package]] @@ -521,18 +1398,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hex" @@ -568,10 +1436,88 @@ dependencies = [ ] [[package]] -name = "iana-time-zone" -version = "0.1.56" +name = "htmlescape" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -592,14 +1538,34 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", ] +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "qoi", + "tiff", + "webp", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -610,6 +1576,28 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", + "serde", +] + +[[package]] +name = "inherent" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce243b1bfa62ffc028f1cc3b6034ec63d649f3031bc8a4fbbb004e1ac17d1f68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "inotify" version = "0.9.6" @@ -637,16 +1625,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] -name = "io-lifetimes" -version = "1.0.11" +name = "ipnet" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.1", - "libc", + "hermit-abi", + "rustix", "windows-sys 0.48.0", ] @@ -661,24 +1658,157 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "jieba-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f0c1347cd3ac8d7c6e3a2dc33ac496d365cf09fc0831aa61111e1a6738983e" +dependencies = [ + "cedarwood", + "fxhash", + "hashbrown 0.14.0", + "lazy_static", + "phf", + "phf_codegen", + "regex", +] + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] [[package]] name = "js-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jwst" +version = "0.1.1" +source = "git+https://github.com/toeverything/OctoBase.git#857a047a0565c82b604236790355cc6985a7eabb" +dependencies = [ + "async-trait", + "base64 0.21.2", + "bytes", + "cang-jie", + "chrono", + "convert_case", + "futures", + "jwst-codec", + "lib0", + "nanoid", + "schemars", + "serde", + "serde_json", + "tantivy", + "thiserror", + "tokio", + "tracing", + "type-map", + "utoipa", + "vergen", + "yrs", +] + +[[package]] +name = "jwst-codec" +version = "0.1.0" +source = "git+https://github.com/toeverything/OctoBase.git#857a047a0565c82b604236790355cc6985a7eabb" +dependencies = [ + "arbitrary", + "bitvec", + "byteorder", + "jwst-logger", + "loom 0.6.1", + "nanoid", + "nom", + "ordered-float", + "rand 0.8.5", + "rand_chacha 0.3.1", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "jwst-logger" +version = "0.1.0" +source = "git+https://github.com/toeverything/OctoBase.git#857a047a0565c82b604236790355cc6985a7eabb" +dependencies = [ + "chrono", + "nu-ansi-term", + "tracing", + "tracing-log", + "tracing-stackdriver", + "tracing-subscriber", +] + +[[package]] +name = "jwst-storage" +version = "0.1.0" +source = "git+https://github.com/toeverything/OctoBase.git#857a047a0565c82b604236790355cc6985a7eabb" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "dotenvy", + "futures", + "governor", + "image", + "jwst", + "jwst-codec", + "jwst-logger", + "jwst-storage-migration", + "lib0", + "opendal", + "path-ext", + "sea-orm", + "sea-orm-migration", + "sha2", + "thiserror", + "tokio", + "tokio-util", + "url", + "yrs", +] + +[[package]] +name = "jwst-storage-migration" +version = "0.1.0" +source = "git+https://github.com/toeverything/OctoBase.git#857a047a0565c82b604236790355cc6985a7eabb" +dependencies = [ + "sea-orm-migration", + "tokio", +] + [[package]] name = "kqueue" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" dependencies = [ "kqueue-sys", "libc", @@ -686,9 +1816,9 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", @@ -704,10 +1834,44 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.144" +name = "lebe" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + +[[package]] +name = "lib0" +version = "0.16.5" +source = "git+https://github.com/toeverything/y-crdt?rev=a700f09#a700f0990a993f905531f7acf589c6a736bb7429" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libgit2-sys" +version = "0.14.2+1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] [[package]] name = "libloading" @@ -737,16 +1901,38 @@ dependencies = [ ] [[package]] -name = "linux-raw-sys" -version = "0.3.8" +name = "libwebp-sys" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "a5df1e76f0acef0058aa2164ccf74e610e716e7f9eeb3ee2283de7d43659d823" +dependencies = [ + "cc", + "glob", +] + +[[package]] +name = "libz-sys" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -754,9 +1940,71 @@ dependencies = [ [[package]] name = "log" -version = "0.4.18" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "pin-utils", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "loom" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9394216e2be01e607cf9e9e2b64c387506df1e768b14cbd2854a3650c3c03e" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lz4_flex" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a8cbbb2831780bc3b9c15a41f5b49222ef756b6730a95f3decfdd15903eb5a3" + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] [[package]] name = "md-5" @@ -767,12 +2015,46 @@ dependencies = [ "digest", ] +[[package]] +name = "measure_time" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +dependencies = [ + "instant", + "log", +] + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -780,10 +2062,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] -name = "mio" -version = "0.8.7" +name = "miniz_oxide" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebffdb73fe72e917997fad08bdbf31ac50b0fa91cec93e69a0662e4264d454c" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", @@ -792,13 +2084,40 @@ dependencies = [ ] [[package]] -name = "napi" -version = "2.13.1" +name = "murmurhash32" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f0a2e93526dd9c8c522d72a4d0c88678be8966fabe9fb8f2947fde6339b682" +checksum = "d736ff882f0e85fe9689fb23db229616c4c00aee2b3ac282f666d8f20eb25d4a" +dependencies = [ + "byteorder", +] + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.10", +] + +[[package]] +name = "napi" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede2d12cd6fce44da537a4be1f5510c73be2506c2e32dfaaafd1f36968f3a0e" dependencies = [ "anyhow", - "bitflags 2.3.1", + "bitflags 2.4.0", "chrono", "ctor", "napi-derive", @@ -853,6 +2172,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.3" @@ -864,10 +2189,16 @@ dependencies = [ ] [[package]] -name = "notify" -version = "6.0.0" +name = "nonzero_ext" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d9ba6c734de18ca27c8cef5cd7058aa4ac9f63596131e4c7e41e579319032a2" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "notify" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" dependencies = [ "bitflags 1.3.2", "crossbeam-channel", @@ -883,10 +2214,31 @@ dependencies = [ ] [[package]] -name = "num-bigint-dig" -version = "0.8.2" +name = "nu-ansi-term" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ "byteorder", "lazy_static", @@ -894,7 +2246,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -921,10 +2273,21 @@ dependencies = [ ] [[package]] -name = "num-traits" -version = "0.2.15" +name = "num-rational" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", "libm", @@ -932,19 +2295,134 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] [[package]] -name = "once_cell" -version = "1.17.1" +name = "object" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oneshot" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc22d22931513428ea6cc089e942d38600e3d00976eef8c86de6b8a3aadec6eb" +dependencies = [ + "loom 0.5.6", +] + +[[package]] +name = "opendal" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df645b6012162c04c8949e9b96ae2ef002e79189cfb154e507e51ac5be76a09" +dependencies = [ + "anyhow", + "async-compat", + "async-trait", + "backon", + "base64 0.21.2", + "bytes", + "chrono", + "flagset", + "futures", + "http", + "hyper", + "log", + "md-5", + "once_cell", + "parking_lot", + "percent-encoding", + "pin-project", + "quick-xml 0.27.1", + "reqsign", + "reqwest", + "serde", + "serde_json", + "tokio", + "uuid", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "ordered-float" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc2dbde8f8a79f2102cc474ceb0ad68e3b80b85289ea62389b60e66777e4213" +dependencies = [ + "arbitrary", + "num-traits", +] + +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + +[[package]] +name = "ouroboros" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ba07320d39dfea882faa70554b4bd342a5f273ed59ba7c1c6b4c840492c954" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4c6225c69b4ca778c0aea097321a64c421cf4577b331c61b229267edabb6f8" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "ownedbytes" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e957eaa64a299f39755416e5b3128c505e9d63a91d0453771ad2ccd3907f8db" +dependencies = [ + "stable_deref_trait", +] [[package]] name = "parking_lot" @@ -958,63 +2436,110 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.2", ] [[package]] name = "paste" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + +[[package]] +name = "path-ext" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8285c3c3c3085f8819bdcebc9c7e783851527f34974d7d283ced36c977ae812" +dependencies = [ + "walkdir", +] [[package]] name = "pem-rfc7468" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] [[package]] name = "pin-project" -version = "1.1.0" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.0" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", ] [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -1024,21 +2549,20 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs1" -version = "0.4.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ "der", "pkcs8", "spki", - "zeroize", ] [[package]] name = "pkcs8" -version = "0.9.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "spki", @@ -1050,6 +2574,19 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1057,21 +2594,144 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] -name = "proc-macro2" -version = "1.0.59" +name = "proc-macro-crate" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] -name = "quote" -version = "1.0.28" +name = "ptr_meta" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" dependencies = [ "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quanta" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8" +dependencies = [ + "crossbeam-utils", + "libc", + "mach", + "once_cell", + "raw-cpuid", + "wasi 0.10.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-xml" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", ] [[package]] @@ -1081,8 +2741,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -1092,7 +2762,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -1101,18 +2780,49 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.10", ] [[package]] -name = "redox_syscall" -version = "0.2.16" +name = "rand_hc" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -1124,20 +2834,126 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.3" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ - "aho-corasick", + "aho-corasick 1.0.4", "memchr", - "regex-syntax", + "regex-automata 0.3.6", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +dependencies = [ + "aho-corasick 1.0.4", + "memchr", + "regex-syntax 0.7.4", ] [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "rend" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581008d2099240d37fb08d77ad713bcaec2c4d89d50b5b21a8bb1996bbab68ab" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqsign" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cb65eb3405f9c2de5c18bfc37338d6bbdb2c35eb8eb0e946208cbb564e4833" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.21.2", + "chrono", + "form_urlencoded", + "hex", + "hmac", + "home", + "http", + "log", + "once_cell", + "percent-encoding", + "quick-xml 0.28.2", + "rand 0.8.5", + "reqwest", + "rsa", + "rust-ini", + "serde", + "serde_json", + "sha1", + "sha2", +] + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg", +] [[package]] name = "ring" @@ -1155,12 +2971,41 @@ dependencies = [ ] [[package]] -name = "rsa" -version = "0.8.2" +name = "rkyv" +version = "0.7.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4" +checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" +dependencies = [ + "bitvec", + "bytecheck", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rsa" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" dependencies = [ "byteorder", + "const-oid", "digest", "num-bigint-dig", "num-integer", @@ -1168,21 +3013,79 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", + "spki", "subtle", "zeroize", ] [[package]] -name = "rustix" -version = "0.37.19" +name = "rust-ini" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" dependencies = [ - "bitflags 1.3.2", + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "rust_decimal" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a2ab0025103a60ecaaf3abf24db1db240a4e1c15837090d2c32f625ac98abea" +dependencies = [ + "arrayvec", + "borsh", + "byteorder", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +dependencies = [ + "bitflags 2.4.0", "errno", - "io-lifetimes", "libc", "linux-raw-sys", "windows-sys 0.48.0", @@ -1190,39 +3093,58 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.1" +version = "0.21.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" dependencies = [ + "log", "ring", "rustls-webpki", "sct", ] [[package]] -name = "rustls-pemfile" -version = "1.0.2" +name = "rustls-native-certs" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ - "base64", + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", ] [[package]] name = "rustls-webpki" -version = "0.100.1" +version = "0.101.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" dependencies = [ "ring", "untrusted", ] [[package]] -name = "ryu" -version = "1.0.13" +name = "rustversion" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "same-file" @@ -1234,10 +3156,49 @@ dependencies = [ ] [[package]] -name = "scopeguard" -version = "1.1.0" +name = "schannel" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "schemars" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" @@ -1250,42 +3211,253 @@ dependencies = [ ] [[package]] -name = "semver" -version = "1.0.17" +name = "sea-bae" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "3bd3534a9978d0aa7edd2808dc1f8f31c4d0ecd31ddf71d997b3c98e9f3c9114" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "sea-orm" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f6c7daef05dde3476d97001e11fca7a52b655aa3bf4fd610ab2da1176a2ed5" +dependencies = [ + "async-stream", + "async-trait", + "bigdecimal", + "chrono", + "futures", + "log", + "ouroboros", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-query-binder", + "serde", + "serde_json", + "sqlx", + "strum", + "thiserror", + "time 0.3.25", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-cli" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e3f0ff2fa5672e2e7314d107c6498a18e469beeb340a0ed84e3075fce73c2cd" +dependencies = [ + "chrono", + "clap", + "dotenvy", + "glob", + "regex", + "sea-schema", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "sea-orm-macros" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd90e73d5f5b184bad525767da29fbfec132b4e62ebd6f60d2f2737ec6468f62" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "sea-bae", + "syn 2.0.28", + "unicode-ident", +] + +[[package]] +name = "sea-orm-migration" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f673fcefb3a7e7b89a12b6c0e854ec0be14367635ac3435369c8ad7f11e09e" +dependencies = [ + "async-trait", + "clap", + "dotenvy", + "futures", + "sea-orm", + "sea-orm-cli", + "sea-schema", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-query" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeb899964df7038e7274306b742951b82a04f835bca8a4683a4c254a6bf35fa" +dependencies = [ + "bigdecimal", + "chrono", + "derivative", + "inherent", + "ordered-float", + "rust_decimal", + "sea-query-derive", + "serde_json", + "time 0.3.25", + "uuid", +] + +[[package]] +name = "sea-query-binder" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36bbb68df92e820e4d5aeb17b4acd5cc8b5d18b2c36a4dd6f4626aabfa7ab1b9" +dependencies = [ + "bigdecimal", + "chrono", + "rust_decimal", + "sea-query", + "serde_json", + "sqlx", + "time 0.3.25", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd78f2e0ee8e537e9195d1049b752e0433e2cac125426bccb7b5c3e508096117" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", + "thiserror", +] + +[[package]] +name = "sea-schema" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e09eb40c78cee8fef8dfbb648036a26b7ad1f618499203ad0e8b6f97593f7f" +dependencies = [ + "futures", + "sea-query", + "sea-schema-derive", +] + +[[package]] +name = "sea-schema-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f686050f76bffc4f635cda8aea6df5548666b830b52387e8bc7de11056d11e" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", +] + +[[package]] +name = "serde_derive_internals" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.5" @@ -1299,15 +3471,24 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1324,9 +3505,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + [[package]] name = "slab" version = "0.4.8" @@ -1337,10 +3536,19 @@ dependencies = [ ] [[package]] -name = "smallvec" -version = "1.10.0" +name = "smallstr" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f" +dependencies = [ + "smallvec", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "socket2" @@ -1352,6 +3560,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -1369,9 +3587,9 @@ dependencies = [ [[package]] name = "spki" -version = "0.6.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" dependencies = [ "base64ct", "der", @@ -1390,9 +3608,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.0-alpha.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afd8985c8822235a9ebeedf0bff971462470162759663d3184593c807ab6e898" +checksum = "8e58421b6bc416714d5115a2ca953718f6c621a51b68e4f4922aea5a4391a721" dependencies = [ "sqlx-core", "sqlx-macros", @@ -1403,13 +3621,13 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.0-alpha.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c12403de02d88e6808de30eb2153c6997d39cc9511a446b510d5944a3ea6727" +checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53" dependencies = [ - "ahash 0.7.6", + "ahash 0.8.3", "atoi", - "bitflags 1.3.2", + "bigdecimal", "byteorder", "bytes", "chrono", @@ -1425,12 +3643,13 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap", + "indexmap 2.0.0", "log", "memchr", "once_cell", "paste", "percent-encoding", + "rust_decimal", "rustls", "rustls-pemfile", "serde", @@ -1439,18 +3658,20 @@ dependencies = [ "smallvec", "sqlformat", "thiserror", + "time 0.3.25", "tokio", "tokio-stream", "tracing", "url", + "uuid", "webpki-roots", ] [[package]] name = "sqlx-macros" -version = "0.7.0-alpha.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be74801a0852ace9d86bc8cc8ac36241e7dc712fea26b8f32bd80ce29c98a10" +checksum = "208e3165167afd7f3881b16c1ef3f2af69fa75980897aac8874a0696516d12c2" dependencies = [ "proc-macro2", "quote", @@ -1461,9 +3682,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.0-alpha.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ce71dd8afc7ad2aeff001bb6affa7128c9087bbdcab07fa97a7952e8ee3d1da" +checksum = "8a4a8336d278c62231d87f24e8a7a74898156e34c1c18942857be2acb29c7dfc" dependencies = [ "dotenvy", "either", @@ -1487,13 +3708,14 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.0-alpha.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c430536df19e8b5b048a9ae19b266aba77f9f3e2255b7195f465d678cb2d0a" +checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" dependencies = [ "atoi", - "base64", - "bitflags 1.3.2", + "base64 0.21.2", + "bigdecimal", + "bitflags 2.4.0", "byteorder", "bytes", "chrono", @@ -1515,8 +3737,9 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", + "rust_decimal", "serde", "sha1", "sha2", @@ -1524,19 +3747,22 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", + "time 0.3.25", "tracing", + "uuid", "whoami", ] [[package]] name = "sqlx-postgres" -version = "0.7.0-alpha.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210e0a1523b6d46ca73db1c5197a233a8e14787596910ce88ff5d47a00da0241" +checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" dependencies = [ "atoi", - "base64", - "bitflags 1.3.2", + "base64 0.21.2", + "bigdecimal", + "bitflags 2.4.0", "byteorder", "chrono", "crc", @@ -1554,8 +3780,10 @@ dependencies = [ "log", "md-5", "memchr", + "num-bigint", "once_cell", - "rand", + "rand 0.8.5", + "rust_decimal", "serde", "serde_json", "sha1", @@ -1564,18 +3792,19 @@ dependencies = [ "sqlx-core", "stringprep", "thiserror", + "time 0.3.25", "tracing", + "uuid", "whoami", ] [[package]] name = "sqlx-sqlite" -version = "0.7.0-alpha.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f446c04b2d2d06b49b905e33c877b282e0f70b1b60a22513eacee8bf56d8afbe" +checksum = "be4c21bf34c7cae5b283efb3ac1bcc7670df7561124dc2f8bdc0b59be40f79a2" dependencies = [ "atoi", - "bitflags 1.3.2", "chrono", "flume", "futures-channel", @@ -1588,20 +3817,46 @@ dependencies = [ "percent-encoding", "serde", "sqlx-core", + "time 0.3.25", "tracing", "url", + "uuid", ] [[package]] -name = "stringprep" -version = "0.1.2" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" dependencies = [ "unicode-bidi", "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + [[package]] name = "subtle" version = "2.5.0" @@ -1621,9 +3876,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -1631,36 +3886,153 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.5.0" +name = "tantivy" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "5bb26a6b22c84d8be41d99a14016d6f04d30d8d31a2ea411a8ab553af5cc490d" +dependencies = [ + "aho-corasick 0.7.20", + "arc-swap", + "async-trait", + "base64 0.13.1", + "bitpacking", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fail", + "fastdivide", + "fastfield_codecs", + "fs2", + "htmlescape", + "itertools", + "levenshtein_automata", + "log", + "lru", + "lz4_flex", + "measure_time", + "memmap2", + "murmurhash32", + "num_cpus", + "once_cell", + "oneshot", + "ownedbytes", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash", + "serde", + "serde_json", + "smallvec", + "stable_deref_trait", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tempfile", + "thiserror", + "time 0.3.25", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e71a0c95b82d4292b097a09b989a6380d28c3a86800c841a2d03bae1fc8b9fa6" + +[[package]] +name = "tantivy-common" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14fef4182bb60df9a4b92cd8ecab39ba2e50a05542934af17eef1f49660705cb" +dependencies = [ + "byteorder", + "ownedbytes", +] + +[[package]] +name = "tantivy-fst" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +dependencies = [ + "byteorder", + "regex-syntax 0.6.29", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343e3ada4c1c480953f6960f8a21ce9c76611480ffdd4f4e230fdddce0fc5331" +dependencies = [ + "combine", + "once_cell", + "regex", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" dependencies = [ "cfg-if", - "fastrand", - "redox_syscall 0.3.5", + "fastrand 2.0.0", + "redox_syscall", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", ] [[package]] @@ -1674,6 +4046,43 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +dependencies = [ + "deranged", + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +dependencies = [ + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1691,11 +4100,11 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.2" +version = "1.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +checksum = "40de3a2ba249dcb097e01be5e67a5ff53cf250397715a071a81543e8a832a920" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -1703,7 +4112,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys 0.48.0", ] @@ -1716,7 +4125,17 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", ] [[package]] @@ -1730,6 +4149,35 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.37" @@ -1745,13 +4193,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", ] [[package]] @@ -1761,6 +4209,79 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-stackdriver" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff9dd91761e07727176a3dd3a1d64bbb577ea656b7b82fa4be4021832674c49" +dependencies = [ + "Inflector", + "serde", + "serde_json", + "thiserror", + "time 0.3.25", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "type-map" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +dependencies = [ + "rustc-hash", ] [[package]] @@ -1777,9 +4298,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-normalization" @@ -1810,9 +4331,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", @@ -1820,22 +4341,81 @@ dependencies = [ ] [[package]] -name = "uuid" -version = "1.3.3" +name = "utf8-ranges" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "utoipa" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de634b7f8178c9c246c88ea251f3a0215c9a4d80778db2d7bd4423a78b5170ec" dependencies = [ - "getrandom", - "rand", + "indexmap 2.0.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcba79cb3e5020d9bcc8313cd5aadaf51d6d54a6b3fd08c3d0360ae6b3c83d0" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.28", +] + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom 0.2.10", + "rand 0.8.5", "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vergen" +version = "7.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f21b881cd6636ece9735721cf03c1fe1e774fe258683d084bb2812ab67435749" +dependencies = [ + "anyhow", + "cfg-if", + "enum-iterator", + "getset", + "git2", + "rustc_version", + "rustversion", + "thiserror", + "time 0.3.25", +] + [[package]] name = "version_check" version = "0.9.4" @@ -1852,6 +4432,21 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -1866,9 +4461,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1876,24 +4471,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", "wasm-bindgen-shared", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.86" +name = "wasm-bindgen-futures" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1901,47 +4508,75 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "wasm-streams" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] name = "web-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] -name = "webpki-roots" -version = "0.23.0" +name = "webp" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa54963694b65584e170cf5dc46aeb4dcaa5584e652ff5f3952e56d66aff0125" +checksum = "12ff0ebb440d1db63b778cb609db8a8abfda825a7841664a76a70b628502c7e1" +dependencies = [ + "libwebp-sys", +] + +[[package]] +name = "webpki-roots" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" dependencies = [ "rustls-webpki", ] [[package]] -name = "whoami" -version = "1.4.0" +name = "weezl" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c70234412ca409cc04e864e89523cb0fc37f5e1344ebed5a3ebf4192b6b9f68" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" [[package]] name = "winapi" @@ -1980,7 +4615,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.2", ] [[package]] @@ -1998,7 +4633,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.2", ] [[package]] @@ -2018,17 +4653,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "d1eeca1c172a285ee6c2c84c341ccea837e7c01b12fbb2d0fe3c9e550ce49ec8" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.2", + "windows_aarch64_msvc 0.48.2", + "windows_i686_gnu 0.48.2", + "windows_i686_msvc 0.48.2", + "windows_x86_64_gnu 0.48.2", + "windows_x86_64_gnullvm 0.48.2", + "windows_x86_64_msvc 0.48.2", ] [[package]] @@ -2039,9 +4674,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "b10d0c968ba7f6166195e13d593af609ec2e3d24f916f081690695cf5eaffb2f" [[package]] name = "windows_aarch64_msvc" @@ -2051,9 +4686,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "571d8d4e62f26d4932099a9efe89660e8bd5087775a2ab5cdd8b747b811f1058" [[package]] name = "windows_i686_gnu" @@ -2063,9 +4698,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "2229ad223e178db5fbbc8bd8d3835e51e566b8474bfca58d2e6150c48bb723cd" [[package]] name = "windows_i686_msvc" @@ -2075,9 +4710,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "600956e2d840c194eedfc5d18f8242bc2e17c7775b6684488af3a9fff6fe3287" [[package]] name = "windows_x86_64_gnu" @@ -2087,9 +4722,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "ea99ff3f8b49fb7a8e0d305e5aec485bd068c2ba691b6e277d29eaeac945868a" [[package]] name = "windows_x86_64_gnullvm" @@ -2099,9 +4734,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "8f1a05a1ece9a7a0d5a7ccf30ba2c33e3a61a30e042ffd247567d1de1d94120d" [[package]] name = "windows_x86_64_msvc" @@ -2111,12 +4746,52 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yrs" +version = "0.16.5" +source = "git+https://github.com/toeverything/y-crdt?rev=a700f09#a700f0990a993f905531f7acf589c6a736bb7429" +dependencies = [ + "atomic_refcell", + "lib0", + "rand 0.7.3", + "smallstr", + "smallvec", + "thiserror", +] [[package]] name = "zeroize" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index 02853b04c0..8413bd3a19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,10 @@ [workspace] -members = ["./packages/native", "./packages/native/schema"] +resolver = "2" +members = [ + "./packages/native", + "./packages/native/schema", + "./packages/storage", +] [profile.dev.package.sqlx-macros] opt-level = 3 @@ -9,3 +14,7 @@ lto = true codegen-units = 1 opt-level = 3 strip = "symbols" + +[patch.crates-io] +lib0 = { git = "https://github.com/toeverything/y-crdt", rev = "a700f09" } +yrs = { git = "https://github.com/toeverything/y-crdt", rev = "a700f09" } diff --git a/LICENSE b/LICENSE index 9433e8889a..6c198644b4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,384 +1,25 @@ -# Mozilla Public License Version 2.0 - -Copyright (c) TOEVERYTHING PTE. LTD. and its affiliates. - -1. Definitions - ---- - -1.1. "Contributor" -means each individual or legal entity that creates, contributes to -the creation of, or owns Covered Software. - -1.2. "Contributor Version" -means the combination of the Contributions of others (if any) used -by a Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" -means Covered Software of a particular Contributor. - -1.4. "Covered Software" -means Source Code Form to which the initial Contributor has attached -the notice in Exhibit A, the Executable Form of such Source Code -Form, and Modifications of such Source Code Form, in each case -including portions thereof. - -1.5. "Incompatible With Secondary Licenses" -means - - (a) that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or - - (b) that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the - terms of a Secondary License. - -1.6. "Executable Form" -means any form of the work other than Source Code Form. - -1.7. "Larger Work" -means a work that combines Covered Software with other material, in -a separate file or files, that is not Covered Software. - -1.8. "License" -means this document. - -1.9. "Licensable" -means having the right to grant, to the maximum extent possible, -whether at the time of the initial grant or subsequently, any and -all of the rights conveyed by this License. - -1.10. "Modifications" -means any of the following: - - (a) any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered - Software; or - - (b) any new file in Source Code Form that contains any Covered - Software. - -1.11. "Patent Claims" of a Contributor -means any patent claim(s), including without limitation, method, -process, and apparatus claims, in any patent Licensable by such -Contributor that would be infringed, but for the grant of the -License, by the making, using, selling, offering for sale, having -made, import, or transfer of either its Contributions or its -Contributor Version. - -1.12. "Secondary License" -means either the GNU General Public License, Version 2.0, the GNU -Lesser General Public License, Version 2.1, the GNU Affero General -Public License, Version 3.0, or any later versions of those -licenses. - -1.13. "Source Code Form" -means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") -means an individual or a legal entity exercising rights under this -License. For legal entities, "You" includes any entity that -controls, is controlled by, or is under common control with You. For -purposes of this definition, "control" means (a) the power, direct -or indirect, to cause the direction or management of such entity, -whether by contract or otherwise, or (b) ownership of more than -fifty percent (50%) of the outstanding shares or beneficial -ownership of such entity. - -2. License Grants and Conditions - ---- - -2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -(a) under intellectual property rights (other than patent or trademark) -Licensable by such Contributor to use, reproduce, make available, -modify, display, perform, distribute, and otherwise exploit its -Contributions, either on an unmodified basis, with Modifications, or -as part of a Larger Work; and - -(b) under Patent Claims of such Contributor to make, use, sell, offer -for sale, have made, import, and otherwise transfer either its -Contributions or its Contributor Version. - -2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -(a) for any code that a Contributor has removed from Covered Software; -or - -(b) for infringements caused by: (i) Your and any other third party's -modifications of Covered Software, or (ii) the combination of its -Contributions with other software (except as part of its Contributor -Version); or - -(c) under Patent Claims infringed by Covered Software in the absence of -its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -3. Responsibilities - ---- - -3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -(a) such Covered Software must also be made available in Source Code -Form, as described in Section 3.1, and You must inform recipients of -the Executable Form how they can obtain a copy of such Source Code -Form by reasonable means in a timely manner, at a charge no more -than the cost of distribution to the recipient; and - -(b) You may distribute such Executable Form under the terms of this -License, or sublicense it under different terms, provided that the -license for the Executable Form does not attempt to limit or alter -the recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -4. Inability to Comply Due to Statute or Regulation - ---- - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -5. Termination - ---- - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - ---- - -- * -- 6. Disclaimer of Warranty \* -- ------------------------- \* -- * -- Covered Software is provided under this License on an "as is" \* -- basis, without warranty of any kind, either expressed, implied, or \* -- statutory, including, without limitation, warranties that the \* -- Covered Software is free of defects, merchantable, fit for a \* -- particular purpose or non-infringing. The entire risk as to the \* -- quality and performance of the Covered Software is with You. \* -- Should any Covered Software prove defective in any respect, You \* -- (not any Contributor) assume the cost of any necessary servicing, \* -- repair, or correction. This disclaimer of warranty constitutes an \* -- essential part of this License. No use of any Covered Software is \* -- authorized under this License except under this disclaimer. \* -- * - ---- - ---- - -- * -- 7. Limitation of Liability \* -- -------------------------- \* -- * -- Under no circumstances and under no legal theory, whether tort \* -- (including negligence), contract, or otherwise, shall any \* -- Contributor, or anyone who distributes Covered Software as \* -- permitted above, be liable to You for any direct, indirect, \* -- special, incidental, or consequential damages of any character \* -- including, without limitation, damages for lost profits, loss of \* -- goodwill, work stoppage, computer failure or malfunction, or any \* -- and all other commercial damages or losses, even if such party \* -- shall have been informed of the possibility of such damages. This \* -- limitation of liability shall not apply to liability for death or \* -- personal injury resulting from such party's negligence to the \* -- extent applicable law prohibits such limitation. Some \* -- jurisdictions do not allow the exclusion or limitation of \* -- incidental or consequential damages, so this exclusion and \* -- limitation may not apply to You. \* -- * - ---- - -8. Litigation - ---- - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -9. Miscellaneous - ---- - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -10. Versions of the License - ---- - -10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -## Exhibit A - Source Code Form License Notice - -This Source Code Form is subject to the terms of the Mozilla Public -License, v. 2.0. If a copy of the MPL was not distributed with this -file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -## Exhibit B - "Incompatible With Secondary Licenses" Notice - -This Source Code Form is "Incompatible With Secondary Licenses", as -defined by the Mozilla Public License, v. 2.0. +Copyright (c) 2022-present TOEVERYTHING PTE. LTD. and its affiliates. + +Portions of this software are licensed as follows: + +* All content that resides under the "apps/server" directory of this repository, if that directory exists, is licensed under the license defined in "apps/server/LICENSE". +* All third party components incorporated into the AFFiNE Software are licensed under the original license provided by the owner of the applicable component. +* Content outside of the above mentioned directories or restrictions above is available under the "MPL2.0" license as defined in "LICENSE-MPL2.0". + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE-MPL2.0 b/LICENSE-MPL2.0 new file mode 100644 index 0000000000..9433e8889a --- /dev/null +++ b/LICENSE-MPL2.0 @@ -0,0 +1,384 @@ +# Mozilla Public License Version 2.0 + +Copyright (c) TOEVERYTHING PTE. LTD. and its affiliates. + +1. Definitions + +--- + +1.1. "Contributor" +means each individual or legal entity that creates, contributes to +the creation of, or owns Covered Software. + +1.2. "Contributor Version" +means the combination of the Contributions of others (if any) used +by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" +means Covered Software of a particular Contributor. + +1.4. "Covered Software" +means Source Code Form to which the initial Contributor has attached +the notice in Exhibit A, the Executable Form of such Source Code +Form, and Modifications of such Source Code Form, in each case +including portions thereof. + +1.5. "Incompatible With Secondary Licenses" +means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" +means any form of the work other than Source Code Form. + +1.7. "Larger Work" +means a work that combines Covered Software with other material, in +a separate file or files, that is not Covered Software. + +1.8. "License" +means this document. + +1.9. "Licensable" +means having the right to grant, to the maximum extent possible, +whether at the time of the initial grant or subsequently, any and +all of the rights conveyed by this License. + +1.10. "Modifications" +means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor +means any patent claim(s), including without limitation, method, +process, and apparatus claims, in any patent Licensable by such +Contributor that would be infringed, but for the grant of the +License, by the making, using, selling, offering for sale, having +made, import, or transfer of either its Contributions or its +Contributor Version. + +1.12. "Secondary License" +means either the GNU General Public License, Version 2.0, the GNU +Lesser General Public License, Version 2.1, the GNU Affero General +Public License, Version 3.0, or any later versions of those +licenses. + +1.13. "Source Code Form" +means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") +means an individual or a legal entity exercising rights under this +License. For legal entities, "You" includes any entity that +controls, is controlled by, or is under common control with You. For +purposes of this definition, "control" means (a) the power, direct +or indirect, to cause the direction or management of such entity, +whether by contract or otherwise, or (b) ownership of more than +fifty percent (50%) of the outstanding shares or beneficial +ownership of such entity. + +2. License Grants and Conditions + +--- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) +Licensable by such Contributor to use, reproduce, make available, +modify, display, perform, distribute, and otherwise exploit its +Contributions, either on an unmodified basis, with Modifications, or +as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer +for sale, have made, import, and otherwise transfer either its +Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; +or + +(b) for infringements caused by: (i) Your and any other third party's +modifications of Covered Software, or (ii) the combination of its +Contributions with other software (except as part of its Contributor +Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of +its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities + +--- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code +Form, as described in Section 3.1, and You must inform recipients of +the Executable Form how they can obtain a copy of such Source Code +Form by reasonable means in a timely manner, at a charge no more +than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this +License, or sublicense it under different terms, provided that the +license for the Executable Form does not attempt to limit or alter +the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + +--- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination + +--- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +--- + +- * +- 6. Disclaimer of Warranty \* +- ------------------------- \* +- * +- Covered Software is provided under this License on an "as is" \* +- basis, without warranty of any kind, either expressed, implied, or \* +- statutory, including, without limitation, warranties that the \* +- Covered Software is free of defects, merchantable, fit for a \* +- particular purpose or non-infringing. The entire risk as to the \* +- quality and performance of the Covered Software is with You. \* +- Should any Covered Software prove defective in any respect, You \* +- (not any Contributor) assume the cost of any necessary servicing, \* +- repair, or correction. This disclaimer of warranty constitutes an \* +- essential part of this License. No use of any Covered Software is \* +- authorized under this License except under this disclaimer. \* +- * + +--- + +--- + +- * +- 7. Limitation of Liability \* +- -------------------------- \* +- * +- Under no circumstances and under no legal theory, whether tort \* +- (including negligence), contract, or otherwise, shall any \* +- Contributor, or anyone who distributes Covered Software as \* +- permitted above, be liable to You for any direct, indirect, \* +- special, incidental, or consequential damages of any character \* +- including, without limitation, damages for lost profits, loss of \* +- goodwill, work stoppage, computer failure or malfunction, or any \* +- and all other commercial damages or losses, even if such party \* +- shall have been informed of the possibility of such damages. This \* +- limitation of liability shall not apply to liability for death or \* +- personal injury resulting from such party's negligence to the \* +- extent applicable law prohibits such limitation. Some \* +- jurisdictions do not allow the exclusion or limitation of \* +- incidental or consequential damages, so this exclusion and \* +- limitation may not apply to You. \* +- * + +--- + +8. Litigation + +--- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous + +--- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License + +--- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +## Exhibit A - Source Code Form License Notice + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +## Exhibit B - "Incompatible With Secondary Licenses" Notice + +This Source Code Form is "Incompatible With Secondary Licenses", as +defined by the Mozilla Public License, v. 2.0. diff --git a/apps/core/.webpack/cache-group.ts b/apps/core/.webpack/cache-group.ts index 89f7a4b127..9e710c2c0c 100644 --- a/apps/core/.webpack/cache-group.ts +++ b/apps/core/.webpack/cache-group.ts @@ -3,11 +3,15 @@ function testPackageName(regexp: RegExp): (module: any) => boolean { module.nameForCondition && regexp.test(module.nameForCondition()); } +// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758 export const productionCacheGroups = { asyncVendor: { test: /[\\/]node_modules[\\/]/, name(module: any) { - // https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758 + // monorepo linked in node_modules, so it's not a npm package + if (!module.context.includes('node_modules')) { + return `app-async`; + } const name = module.context.match( /[\\/]node_modules[\\/](.*?)([\\/]|$)/ )?.[1]; diff --git a/apps/core/.webpack/config.ts b/apps/core/.webpack/config.ts index 0f5caad90e..84333df92a 100644 --- a/apps/core/.webpack/config.ts +++ b/apps/core/.webpack/config.ts @@ -1,6 +1,7 @@ import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { createRequire } from 'node:module'; + import type { Configuration as DevServerConfiguration } from 'webpack-dev-server'; import { PerfseePlugin } from '@perfsee/webpack'; import { sentryWebpackPlugin } from '@sentry/webpack-plugin'; @@ -10,13 +11,14 @@ import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import TerserPlugin from 'terser-webpack-plugin'; import webpack from 'webpack'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import { compact } from 'lodash-es'; import { productionCacheGroups } from './cache-group.js'; import type { BuildFlags } from '@affine/cli/config'; import { projectRoot } from '@affine/cli/config'; import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin'; -import { computeCacheKey } from './utils.js'; import type { RuntimeConfig } from '@affine/env/global'; +import { WebpackS3Plugin, gitShortHash } from './s3-plugin.js'; const IN_CI = !!process.env.CI; @@ -67,16 +69,26 @@ const OptimizeOptionOptions: ( }, }); +export const publicPath = (function () { + const { BUILD_TYPE } = process.env; + const publicPath = process.env.PUBLIC_PATH ?? '/'; + if (process.env.COVERAGE) { + return publicPath; + } + if (BUILD_TYPE === 'canary') { + return `https://dev.affineassets.com/${gitShortHash()}/`; + } else if (BUILD_TYPE === 'beta' || BUILD_TYPE === 'stable') { + return `https://prod.affineassets.com/${gitShortHash()}/`; + } + return publicPath; +})(); + export const createConfiguration: ( buildFlags: BuildFlags, runtimeConfig: RuntimeConfig ) => webpack.Configuration = (buildFlags, runtimeConfig) => { - let publicPath = process.env.PUBLIC_PATH ?? '/'; - const blocksuiteBaseDir = buildFlags.localBlockSuite; - const cacheKey = computeCacheKey(buildFlags); - const config = { name: 'affine', // to set a correct base path for the source map @@ -96,8 +108,11 @@ export const createConfiguration: ( ? 'js/[name]-[contenthash:8].js' : 'js/[name].js', // In some cases webpack will emit files starts with "_" which is reserved in web extension. - chunkFilename: 'js/chunk.[name].js', - assetModuleFilename: 'assets/[contenthash:8][ext][query]', + chunkFilename: + buildFlags.mode === 'production' + ? 'js/chunk.[name]-[contenthash:8].js' + : 'js/chunk.[name].js', + assetModuleFilename: 'assets/[name]-[contenthash:8][ext][query]', devtoolModuleFilenameTemplate: 'webpack://[namespace]/[resource-path]', hotUpdateChunkFilename: 'hot/[id].[fullhash].js', hotUpdateMainFilename: 'hot/[runtime].[fullhash].json', @@ -192,14 +207,6 @@ export const createConfiguration: ( }, }, - cache: { - type: 'filesystem', - buildDependencies: { - config: [fileURLToPath(import.meta.url)], - }, - version: cacheKey, - }, - module: { parser: { javascript: { @@ -308,7 +315,7 @@ export const createConfiguration: ( { loader: 'css-loader', options: { - url: false, + url: true, sourceMap: false, modules: false, import: true, @@ -333,22 +340,23 @@ export const createConfiguration: ( }, ], }, - - plugins: [ - ...(IN_CI ? [] : [new webpack.ProgressPlugin({ percentBy: 'entries' })]), - ...(buildFlags.mode === 'development' - ? [new ReactRefreshWebpackPlugin({ overlay: false, esModule: true })] - : [ - new MiniCssExtractPlugin({ - filename: `[name].[contenthash:8].css`, - ignoreOrder: true, - }), - ]), + plugins: compact([ + IN_CI ? null : new webpack.ProgressPlugin({ percentBy: 'entries' }), + buildFlags.mode === 'development' + ? new ReactRefreshWebpackPlugin({ overlay: false, esModule: true }) + : new MiniCssExtractPlugin({ + filename: `[name].[contenthash:8].css`, + ignoreOrder: true, + }), new VanillaExtractPlugin(), new webpack.DefinePlugin({ 'process.env': JSON.stringify({}), 'process.env.COVERAGE': JSON.stringify(!!buildFlags.coverage), 'process.env.NODE_ENV': JSON.stringify(buildFlags.mode), + 'process.env.SHOULD_REPORT_TRACE': `${Boolean( + process.env.SHOULD_REPORT_TRACE + )}`, + 'process.env.TRACE_REPORT_ENDPOINT': `"${process.env.TRACE_REPORT_ENDPOINT}"`, runtimeConfig: JSON.stringify(runtimeConfig), }), new CopyPlugin({ @@ -359,7 +367,10 @@ export const createConfiguration: ( }, ], }), - ], + buildFlags.mode === 'production' && process.env.R2_SECRET_ACCESS_KEY + ? new WebpackS3Plugin() + : null, + ]), optimization: OptimizeOptionOptions(buildFlags), @@ -373,6 +384,14 @@ export const createConfiguration: ( publicPath: '/', watch: true, }, + proxy: { + '/api': 'http://localhost:3010', + '/socket.io': { + target: 'http://localhost:3010', + ws: true, + }, + '/graphql': 'http://localhost:3010', + }, } as DevServerConfiguration, } satisfies webpack.Configuration; diff --git a/apps/core/.webpack/runtime-config.ts b/apps/core/.webpack/runtime-config.ts index 432a62ed2f..3351f72f03 100644 --- a/apps/core/.webpack/runtime-config.ts +++ b/apps/core/.webpack/runtime-config.ts @@ -24,9 +24,9 @@ const editorFlags: BlockSuiteFeatureFlags = { }; export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig { - const buildPreset: Record = { + const buildPreset: Record = { stable: { - enablePlugin: false, + enablePlugin: true, enableTestProperties: false, enableBroadcastChannelProvider: true, enableDebugPage: true, @@ -37,13 +37,26 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig { enableNewSettingUnstableApi: false, enableSQLiteProvider: true, enableMoveDatabase: false, - enableNotificationCenter: false, - enableCloud: false, - serverAPI: 'https://localhost:3010', + enableNotificationCenter: true, + enableCloud: true, + enableEnhanceShareMode: false, + serverUrlPrefix: 'https://app.affine.pro', editorFlags, appVersion: packageJson.version, editorVersion: packageJson.dependencies['@blocksuite/editor'], }, + get beta() { + return { + ...this.stable, + serverUrlPrefix: 'https://ambassador.affine.pro', + }; + }, + get internal() { + return { + ...this.stable, + serverUrlPrefix: 'https://affine.fail', + }; + }, // canary will be aggressive and enable all features canary: { enablePlugin: true, @@ -58,18 +71,15 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig { enableSQLiteProvider: true, enableMoveDatabase: false, enableNotificationCenter: true, - enableCloud: false, - serverAPI: 'https://localhost:3010', + enableCloud: true, + enableEnhanceShareMode: false, + serverUrlPrefix: 'https://affine.fail', editorFlags, appVersion: packageJson.version, editorVersion: packageJson.dependencies['@blocksuite/editor'], }, }; - // beta and internal versions are the same as stable - buildPreset.beta = buildPreset.stable; - buildPreset.internal = buildPreset.stable; - const currentBuild = buildFlags.channel; if (!(currentBuild in buildPreset)) { @@ -107,11 +117,18 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig { enableCloud: process.env.ENABLE_CLOUD ? process.env.ENABLE_CLOUD === 'true' : currentBuildPreset.enableCloud, + enableEnhanceShareMode: process.env.ENABLE_ENHANCE_SHARE_MODE + ? process.env.ENABLE_ENHANCE_SHARE_MODE === 'true' + : currentBuildPreset.enableEnhanceShareMode, enableMoveDatabase: process.env.ENABLE_MOVE_DATABASE ? process.env.ENABLE_MOVE_DATABASE === 'true' : currentBuildPreset.enableMoveDatabase, }; + if (buildFlags.mode === 'development') { + currentBuildPreset.serverUrlPrefix = 'http://localhost:8080'; + } + return { ...currentBuildPreset, // environment preset will overwrite current build preset diff --git a/apps/core/.webpack/s3-plugin.ts b/apps/core/.webpack/s3-plugin.ts new file mode 100644 index 0000000000..35b4b8c269 --- /dev/null +++ b/apps/core/.webpack/s3-plugin.ts @@ -0,0 +1,58 @@ +import { join } from 'node:path'; +import { execSync } from 'node:child_process'; +import { readFile } from 'node:fs/promises'; + +import type { PutObjectCommandInput } from '@aws-sdk/client-s3'; +import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { once } from 'lodash-es'; +import { lookup } from 'mime-types'; +import type { Compiler, WebpackPluginInstance } from 'webpack'; + +export const gitShortHash = once(() => { + const { GITHUB_SHA } = process.env; + if (GITHUB_SHA) { + return GITHUB_SHA.substring(0, 9); + } + const sha = execSync(`git rev-parse --short HEAD`, { + encoding: 'utf-8', + }).trim(); + return sha; +}); + +export const R2_BUCKET = + process.env.R2_BUCKET! ?? + (process.env.BUILD_TYPE === 'canary' ? 'assets-dev' : 'assets-prod'); + +export class WebpackS3Plugin implements WebpackPluginInstance { + private readonly s3 = new S3Client({ + region: 'auto', + endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, + credentials: { + accessKeyId: process.env.R2_ACCESS_KEY_ID!, + secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!, + }, + }); + + apply(compiler: Compiler) { + compiler.hooks.assetEmitted.tapPromise( + 'WebpackS3Plugin', + async (asset, { outputPath }) => { + if (asset === 'index.html') { + return; + } + const assetPath = join(outputPath, asset); + const assetSource = await readFile(assetPath); + const putObjectCommandOptions: PutObjectCommandInput = { + Body: assetSource, + Bucket: R2_BUCKET, + Key: join(gitShortHash(), asset), + }; + const contentType = lookup(asset); + if (contentType) { + putObjectCommandOptions.ContentType = contentType; + } + await this.s3.send(new PutObjectCommand(putObjectCommandOptions)); + } + ); + } +} diff --git a/apps/core/.webpack/template.html b/apps/core/.webpack/template.html index b7045d4543..64496348af 100644 --- a/apps/core/.webpack/template.html +++ b/apps/core/.webpack/template.html @@ -17,14 +17,14 @@ + -
+
diff --git a/apps/core/.webpack/webpack.config.ts b/apps/core/.webpack/webpack.config.ts index 78d6b66729..ac759e3ca3 100644 --- a/apps/core/.webpack/webpack.config.ts +++ b/apps/core/.webpack/webpack.config.ts @@ -1,10 +1,12 @@ -import { createConfiguration, rootPath } from './config.js'; +import { createConfiguration, rootPath, publicPath } from './config.js'; import { merge } from 'webpack-merge'; import { join, resolve } from 'node:path'; import type { BuildFlags } from '@affine/cli/config'; import { getRuntimeConfig } from './runtime-config.js'; import HTMLPlugin from 'html-webpack-plugin'; +import { gitShortHash } from './s3-plugin.js'; + export default async function (cli_env: any, _: any) { const flags: BuildFlags = JSON.parse( Buffer.from(cli_env.flags, 'hex').toString('utf-8') @@ -44,12 +46,16 @@ export default async function (cli_env: any, _: any) { minify: false, chunks: ['app', 'plugin', 'polyfill/intl-segmenter', 'polyfill/ses'], filename: 'index.html', + templateParameters: { + GIT_SHORT_SHA: gitShortHash(), + }, }), new HTMLPlugin({ template: join(rootPath, '.webpack', 'template.html'), inject: 'body', scriptLoading: 'module', minify: false, + publicPath, chunks: [ '_plugin/index.test', 'plugin', @@ -57,6 +63,9 @@ export default async function (cli_env: any, _: any) { 'polyfill/ses', ], filename: '_plugin/index.html', + templateParameters: { + GIT_SHORT_SHA: gitShortHash(), + }, }), ], }); diff --git a/apps/core/package.json b/apps/core/package.json index f48344af8c..8578fc0bb1 100644 --- a/apps/core/package.json +++ b/apps/core/package.json @@ -38,6 +38,7 @@ "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.0", "@mui/material": "^5.14.6", + "@radix-ui/react-select": "^1.2.2", "@react-hookz/web": "^23.1.0", "@toeverything/components": "^0.0.19", "async-call-rpc": "^6.3.1", @@ -52,6 +53,7 @@ "lodash.debounce": "^4.0.8", "lottie-web": "^5.12.2", "mini-css-extract-plugin": "^2.7.6", + "next-auth": "^4.22.1", "next-themes": "^0.2.1", "postcss-loader": "^7.3.3", "react": "18.2.0", @@ -62,22 +64,27 @@ "rxjs": "^7.8.1", "ses": "^0.18.7", "swr": "2.2.1", + "valtio": "^1.10.6", "y-protocols": "^1.0.5", "yjs": "^13.6.7", "zod": "^3.22.2" }, "devDependencies": { + "@aws-sdk/client-s3": "3.400.0", "@perfsee/webpack": "^1.8.4", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@sentry/webpack-plugin": "^2.7.0", "@svgr/webpack": "^8.1.0", "@swc/core": "^1.3.80", + "@types/lodash-es": "^4.17.8", "@types/lodash.debounce": "^4.0.7", "@types/webpack-env": "^1.18.1", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.8.1", "express": "^4.18.2", "html-webpack-plugin": "^5.5.3", + "lodash-es": "^4.17.21", + "mime-types": "^2.1.35", "raw-loader": "^4.0.2", "source-map-loader": "^4.0.1", "style-loader": "^3.3.3", diff --git a/apps/core/project.json b/apps/core/project.json index e8edde6149..09ac9fbf09 100644 --- a/apps/core/project.json +++ b/apps/core/project.json @@ -10,20 +10,18 @@ "target": "build", "params": "ignore" }, + { + "projects": ["tag:infra"], + "target": "build", + "params": "ignore" + }, "^build" ], "inputs": [ "{projectRoot}/.webpack/**/*", "{projectRoot}/**/*", "{projectRoot}/public/**/*", - "{workspaceRoot}/packages/env/src/**/*", - "{workspaceRoot}/packages/component/src/**/*", - "{workspaceRoot}/packages/debug/src/**/*", - "{workspaceRoot}/packages/graphql/src/**/*", - "{workspaceRoot}/packages/hooks/src/**/*", - "{workspaceRoot}/packages/jotai/src/**/*", - "{workspaceRoot}/packages/templates/src/**/*", - "{workspaceRoot}/packages/workspace/src/**/*", + "{workspaceRoot}/packages/**/*", { "env": "BUILD_TYPE" }, diff --git a/apps/core/src/adapters/cloud/crud.ts b/apps/core/src/adapters/cloud/crud.ts new file mode 100644 index 0000000000..e0bbf520ff --- /dev/null +++ b/apps/core/src/adapters/cloud/crud.ts @@ -0,0 +1,164 @@ +import type { + AffineCloudWorkspace, + WorkspaceCRUD, +} from '@affine/env/workspace'; +import { WorkspaceFlavour } from '@affine/env/workspace'; +import { + createWorkspaceMutation, + deleteWorkspaceMutation, + getWorkspaceQuery, + getWorkspacesQuery, +} from '@affine/graphql'; +import { fetcher } from '@affine/workspace/affine/gql'; +import { getOrCreateWorkspace } from '@affine/workspace/manager'; +import { createIndexeddbStorage, Workspace } from '@blocksuite/store'; +import { migrateLocalBlobStorage } from '@toeverything/infra/blocksuite'; +import { + createIndexedDBProvider, + DEFAULT_DB_NAME, +} from '@toeverything/y-indexeddb'; +import { getSession } from 'next-auth/react'; +import { proxy } from 'valtio/vanilla'; + +const Y = Workspace.Y; + +async function deleteLocalBlobStorage(id: string) { + const storage = createIndexeddbStorage(id); + const keys = await storage.crud.list(); + for (const key of keys) { + await storage.crud.delete(key); + } +} + +// we don't need to persistence the state into local storage +// because if a user clicks create multiple time and nothing happened +// because of the server delay or something, he/she will wait. +// and also the user journey of creating workspace is long. +const createdWorkspaces = proxy([]); + +export const CRUD: WorkspaceCRUD = { + create: async blockSuiteWorkspace => { + if (createdWorkspaces.some(id => id === blockSuiteWorkspace.id)) { + throw new Error('workspace already created'); + } + const { createWorkspace } = await fetcher({ + query: createWorkspaceMutation, + variables: { + init: new File( + [Y.encodeStateAsUpdate(blockSuiteWorkspace.doc)], + 'initBinary.yDoc' + ), + }, + }); + createdWorkspaces.push(blockSuiteWorkspace.id); + const newBLockSuiteWorkspace = getOrCreateWorkspace( + createWorkspace.id, + WorkspaceFlavour.AFFINE_CLOUD + ); + + Y.applyUpdate( + newBLockSuiteWorkspace.doc, + Y.encodeStateAsUpdate(blockSuiteWorkspace.doc) + ); + + await Promise.all( + [...blockSuiteWorkspace.doc.subdocs].map(async subdoc => { + subdoc.load(); + return subdoc.whenLoaded.then(() => { + newBLockSuiteWorkspace.doc.subdocs.forEach(newSubdoc => { + if (newSubdoc.guid === subdoc.guid) { + Y.applyUpdate(newSubdoc, Y.encodeStateAsUpdate(subdoc)); + } + }); + }); + }) + ); + + const provider = createIndexedDBProvider( + newBLockSuiteWorkspace.doc, + DEFAULT_DB_NAME + ); + provider.connect(); + migrateLocalBlobStorage(blockSuiteWorkspace.id, createWorkspace.id) + .then(() => deleteLocalBlobStorage(blockSuiteWorkspace.id)) + .catch(e => { + console.error('error when moving blob storage:', e); + }); + // todo(himself65): delete old workspace in the future + return createWorkspace.id; + }, + delete: async workspace => { + await fetcher({ + query: deleteWorkspaceMutation, + variables: { + id: workspace.id, + }, + }); + }, + get: async id => { + if (!environment.isServer && !navigator.onLine) { + // no network + return null; + } + if ( + !(await getSession() + .then(() => true) + .catch(() => false)) + ) { + return null; + } + try { + await fetcher({ + query: getWorkspaceQuery, + variables: { + id, + }, + }); + return { + id, + flavour: WorkspaceFlavour.AFFINE_CLOUD, + blockSuiteWorkspace: getOrCreateWorkspace( + id, + WorkspaceFlavour.AFFINE_CLOUD + ), + } satisfies AffineCloudWorkspace; + } catch (e) { + console.error('error when fetching cloud workspace:', e); + return null; + } + }, + list: async () => { + if (!environment.isServer && !navigator.onLine) { + // no network + return []; + } + if ( + !(await getSession() + .then(() => true) + .catch(() => false)) + ) { + return []; + } + try { + const { workspaces } = await fetcher({ + query: getWorkspacesQuery, + }); + const ids = workspaces.map(({ id }) => id); + + return ids.map( + id => + ({ + id, + flavour: WorkspaceFlavour.AFFINE_CLOUD, + blockSuiteWorkspace: getOrCreateWorkspace( + id, + WorkspaceFlavour.AFFINE_CLOUD + ), + }) satisfies AffineCloudWorkspace + ); + } catch (e) { + console.error('error when fetching cloud workspaces:', e); + return []; + } + }, +}; diff --git a/apps/core/src/adapters/cloud/ui.tsx b/apps/core/src/adapters/cloud/ui.tsx new file mode 100644 index 0000000000..7aeca09954 --- /dev/null +++ b/apps/core/src/adapters/cloud/ui.tsx @@ -0,0 +1,75 @@ +import { initEmptyPage } from '@affine/env/blocksuite'; +import { PageNotFoundError } from '@affine/env/constant'; +import type { + WorkspaceFlavour, + WorkspaceUISchema, +} from '@affine/env/workspace'; +import { lazy, useCallback } from 'react'; + +import { useIsWorkspaceOwner } from '../../hooks/affine/use-is-workspace-owner'; +import { useWorkspace } from '../../hooks/use-workspace'; +import { + BlockSuitePageList, + NewWorkspaceSettingDetail, + PageDetailEditor, + Provider, + WorkspaceHeader, +} from '../shared'; + +const LoginCard = lazy(() => + import('../../components/cloud/login-card').then(({ LoginCard }) => ({ + default: LoginCard, + })) +); + +export const UI = { + Provider, + LoginCard, + Header: WorkspaceHeader, + PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => { + const workspace = useWorkspace(currentWorkspaceId); + const page = workspace.blockSuiteWorkspace.getPage(currentPageId); + if (!page) { + throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId); + } + return ( + <> + initEmptyPage(page), [])} + onLoad={onLoadEditor} + workspace={workspace.blockSuiteWorkspace} + /> + + ); + }, + PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => { + return ( + + ); + }, + NewSettingsDetail: ({ + currentWorkspaceId, + onTransformWorkspace, + onDeleteLocalWorkspace, + onDeleteCloudWorkspace, + onLeaveWorkspace, + }) => { + const isOwner = useIsWorkspaceOwner(currentWorkspaceId); + return ( + + ); + }, +} satisfies WorkspaceUISchema; diff --git a/apps/core/src/adapters/local/index.tsx b/apps/core/src/adapters/local/index.tsx index f5501b444f..2f469d8562 100644 --- a/apps/core/src/adapters/local/index.tsx +++ b/apps/core/src/adapters/local/index.tsx @@ -29,6 +29,7 @@ import { BlockSuitePageList, NewWorkspaceSettingDetail, PageDetailEditor, + Provider, WorkspaceHeader, } from '../shared'; @@ -39,6 +40,7 @@ export const LocalAdapter: WorkspaceAdapter = { flavour: WorkspaceFlavour.LOCAL, loadPriority: LoadPriority.LOW, Events: { + 'app:access': async () => true, 'app:init': () => { const blockSuiteWorkspace = getOrCreateWorkspace( nanoid(), @@ -79,9 +81,7 @@ export const LocalAdapter: WorkspaceAdapter = { CRUD, UI: { Header: WorkspaceHeader, - Provider: ({ children }) => { - return <>{children}; - }, + Provider, PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => { const workspace = useStaticBlockSuiteWorkspace(currentWorkspaceId); const page = workspace.getPage(currentPageId); @@ -111,14 +111,19 @@ export const LocalAdapter: WorkspaceAdapter = { }, NewSettingsDetail: ({ currentWorkspaceId, - onDeleteWorkspace, onTransformWorkspace, + onDeleteLocalWorkspace, + onDeleteCloudWorkspace, + onLeaveWorkspace, }) => { return ( ); }, diff --git a/apps/core/src/adapters/public-cloud/ui.tsx b/apps/core/src/adapters/public-cloud/ui.tsx new file mode 100644 index 0000000000..626abb7637 --- /dev/null +++ b/apps/core/src/adapters/public-cloud/ui.tsx @@ -0,0 +1,45 @@ +import { initEmptyPage } from '@affine/env/blocksuite'; +import { PageNotFoundError } from '@affine/env/constant'; +import type { WorkspaceFlavour } from '@affine/env/workspace'; +import { type WorkspaceUISchema } from '@affine/env/workspace'; +import { useCallback } from 'react'; + +import { useWorkspace } from '../../hooks/use-workspace'; +import { BlockSuitePageList, PageDetailEditor, Provider } from '../shared'; + +export const UI = { + Provider, + Header: () => { + return null; + }, + PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => { + const workspace = useWorkspace(currentWorkspaceId); + const page = workspace.blockSuiteWorkspace.getPage(currentPageId); + if (!page) { + throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId); + } + return ( + <> + initEmptyPage(page), [])} + onLoad={onLoadEditor} + workspace={workspace.blockSuiteWorkspace} + /> + + ); + }, + PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => { + return ( + + ); + }, + NewSettingsDetail: () => { + throw new Error('Not implemented'); + }, +} satisfies WorkspaceUISchema; diff --git a/apps/core/src/adapters/shared.ts b/apps/core/src/adapters/shared.ts index 90ad5e5716..ac54b8d24c 100644 --- a/apps/core/src/adapters/shared.ts +++ b/apps/core/src/adapters/shared.ts @@ -1,5 +1,11 @@ import { lazy } from 'react'; +export const Provider = lazy(() => + import('../components/cloud/provider').then(({ Provider }) => ({ + default: Provider, + })) +); + export const NewWorkspaceSettingDetail = lazy(() => import('../components/affine/new-workspace-setting-detail').then( ({ WorkspaceSettingDetail }) => ({ diff --git a/apps/core/src/adapters/workspace.ts b/apps/core/src/adapters/workspace.ts index dbb74d709f..77bd0f2c88 100644 --- a/apps/core/src/adapters/workspace.ts +++ b/apps/core/src/adapters/workspace.ts @@ -10,7 +10,10 @@ import { WorkspaceFlavour, } from '@affine/env/workspace'; +import { CRUD as CloudCRUD } from './cloud/crud'; +import { UI as CloudUI } from './cloud/ui'; import { LocalAdapter } from './local'; +import { UI as PublicCloudUI } from './public-cloud/ui'; const unimplemented = () => { throw new Error('Not implemented'); @@ -26,26 +29,24 @@ export const WorkspaceAdapters = { releaseType: ReleaseType.UNRELEASED, flavour: WorkspaceFlavour.AFFINE_CLOUD, loadPriority: LoadPriority.HIGH, - Events: {} as Partial, - // todo: implement this - CRUD: { - get: unimplemented, - list: bypassList, - delete: unimplemented, - create: unimplemented, - }, - // todo: implement this - UI: { - Provider: unimplemented, - Header: unimplemented, - PageDetail: unimplemented, - PageList: unimplemented, - NewSettingsDetail: unimplemented, - }, + Events: { + 'app:access': async () => { + try { + const { getSession } = await import('next-auth/react'); + const session = await getSession(); + return !!session; + } catch (e) { + console.error('failed to get session', e); + return false; + } + }, + } as Partial, + CRUD: CloudCRUD, + UI: CloudUI, }, - [WorkspaceFlavour.PUBLIC]: { + [WorkspaceFlavour.AFFINE_PUBLIC]: { releaseType: ReleaseType.UNRELEASED, - flavour: WorkspaceFlavour.PUBLIC, + flavour: WorkspaceFlavour.AFFINE_PUBLIC, loadPriority: LoadPriority.LOW, Events: {} as Partial, // todo: implement this @@ -55,14 +56,7 @@ export const WorkspaceAdapters = { delete: unimplemented, create: unimplemented, }, - // todo: implement this - UI: { - Provider: unimplemented, - Header: unimplemented, - PageDetail: unimplemented, - PageList: unimplemented, - NewSettingsDetail: unimplemented, - }, + UI: PublicCloudUI, }, } satisfies { [Key in WorkspaceFlavour]: WorkspaceAdapter; diff --git a/apps/core/src/app.tsx b/apps/core/src/app.tsx index df4a81ccba..920b461436 100644 --- a/apps/core/src/app.tsx +++ b/apps/core/src/app.tsx @@ -7,6 +7,7 @@ import { WorkspaceFallback } from '@affine/component/workspace'; import { CacheProvider } from '@emotion/react'; import { getCurrentStore } from '@toeverything/infra/atom'; import { use } from 'foxact/use'; +import { SessionProvider } from 'next-auth/react'; import type { PropsWithChildren, ReactElement } from 'react'; import { lazy, memo, Suspense } from 'react'; import { RouterProvider } from 'react-router-dom'; @@ -47,16 +48,18 @@ const languageLoadingPromise = loadLanguage().catch(console.error); export const App = memo(function App() { use(languageLoadingPromise); return ( - - - - } - router={router} - future={future} - /> - - - + + + + + } + router={router} + future={future} + /> + + + + ); }); diff --git a/apps/core/src/atoms/index.ts b/apps/core/src/atoms/index.ts index 5bdabf5e9e..9a4ef22831 100644 --- a/apps/core/src/atoms/index.ts +++ b/apps/core/src/atoms/index.ts @@ -3,9 +3,9 @@ import { atom } from 'jotai'; import { atomFamily, atomWithStorage } from 'jotai/utils'; import type { AtomFamily } from 'jotai/vanilla/utils/atomFamily'; +import type { AuthProps } from '../components/affine/auth'; import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal'; import type { SettingProps } from '../components/affine/setting-modal'; - // modal atoms export const openWorkspacesModalAtom = atom(false); export const openCreateWorkspaceModalAtom = atom(false); @@ -22,6 +22,22 @@ export const openSettingModalAtom = atom({ open: false, }); +export type AuthAtom = { + openModal: boolean; + state: AuthProps['state']; + email?: string; + emailType?: AuthProps['emailType']; + // Only used for sign in page callback, after called, it will be set to undefined + onceSignedIn?: () => void; +}; + +export const authAtom = atom({ + openModal: false, + state: 'signIn', + email: '', + emailType: 'changeEmail', +}); + export const openDisableCloudAlertModalAtom = atom(false); type PageMode = 'page' | 'edgeless'; diff --git a/apps/core/src/components/adapter-worksapce-wrapper.tsx b/apps/core/src/components/adapter-worksapce-wrapper.tsx new file mode 100644 index 0000000000..35e5c3ec30 --- /dev/null +++ b/apps/core/src/components/adapter-worksapce-wrapper.tsx @@ -0,0 +1,13 @@ +import { assertExists } from '@blocksuite/global/utils'; +import type { FC, PropsWithChildren } from 'react'; + +import { WorkspaceAdapters } from '../adapters/workspace'; +import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; + +export const AdapterProviderWrapper: FC = ({ children }) => { + const [currentWorkspace] = useCurrentWorkspace(); + + const Provider = WorkspaceAdapters[currentWorkspace.flavour].UI.Provider; + assertExists(Provider); + return {children}; +}; diff --git a/apps/core/src/components/affine/any-error-boundary/index.tsx b/apps/core/src/components/affine/any-error-boundary/index.tsx new file mode 100644 index 0000000000..35a72366c4 --- /dev/null +++ b/apps/core/src/components/affine/any-error-boundary/index.tsx @@ -0,0 +1,11 @@ +import type { ReactElement } from 'react'; +import type { FallbackProps } from 'react-error-boundary'; + +export const AnyErrorBoundary = (props: FallbackProps): ReactElement => { + return ( +
+

Something went wrong:

+

{props.error.toString()}

+
+ ); +}; diff --git a/apps/core/src/components/affine/auth/after-sign-in-send-email.tsx b/apps/core/src/components/affine/auth/after-sign-in-send-email.tsx new file mode 100644 index 0000000000..d2df62352d --- /dev/null +++ b/apps/core/src/components/affine/auth/after-sign-in-send-email.tsx @@ -0,0 +1,67 @@ +import { + AuthContent, + BackButton, + ModalHeader, + ResendButton, +} from '@affine/component/auth-components'; +import { Trans } from '@affine/i18n'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { signIn } from 'next-auth/react'; +import { type FC, useCallback } from 'react'; + +import { buildCallbackUrl } from './callback-url'; +import type { AuthPanelProps } from './index'; +import * as style from './style.css'; + +export const AfterSignInSendEmail: FC = ({ + setAuthState, + email, +}) => { + const t = useAFFiNEI18N(); + + return ( + <> + + + {t['com.affine.auth.sign.sent.email.message.start']()} + {email} + {t['com.affine.auth.sign.sent.email.message.end']()} + + + { + signIn('email', { + email, + callbackUrl: buildCallbackUrl('signIn'), + redirect: true, + }).catch(console.error); + }, [email])} + /> + +
+ {/*prettier-ignore*/} + + If you haven't received the email, please check your spam folder. + Or { + setAuthState('signInWithPassword'); + }, [setAuthState])} + > + sign in with password + instead. + +
+ + { + setAuthState('signIn'); + }, [setAuthState])} + /> + + ); +}; diff --git a/apps/core/src/components/affine/auth/after-sign-up-send-email.tsx b/apps/core/src/components/affine/auth/after-sign-up-send-email.tsx new file mode 100644 index 0000000000..148cb852c6 --- /dev/null +++ b/apps/core/src/components/affine/auth/after-sign-up-send-email.tsx @@ -0,0 +1,54 @@ +import { + AuthContent, + BackButton, + ModalHeader, + ResendButton, +} from '@affine/component/auth-components'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { signIn } from 'next-auth/react'; +import { type FC, useCallback } from 'react'; + +import { buildCallbackUrl } from './callback-url'; +import type { AuthPanelProps } from './index'; +import * as style from './style.css'; + +export const AfterSignUpSendEmail: FC = ({ + setAuthState, + email, +}) => { + const t = useAFFiNEI18N(); + + return ( + <> + + + {t['com.affine.auth.sign.sent.email.message.start']()} + {email} + {t['com.affine.auth.sign.sent.email.message.end']()} + + + { + signIn('email', { + email: email, + callbackUrl: buildCallbackUrl('signUp'), + redirect: true, + }).catch(console.error); + }, [email])} + /> + +
+ {t['com.affine.auth.sign.auth.code.message']()} +
+ + { + setAuthState('signIn'); + }, [setAuthState])} + /> + + ); +}; diff --git a/apps/core/src/components/affine/auth/callback-url.ts b/apps/core/src/components/affine/auth/callback-url.ts new file mode 100644 index 0000000000..cbc2086b60 --- /dev/null +++ b/apps/core/src/components/affine/auth/callback-url.ts @@ -0,0 +1,16 @@ +import { isDesktop } from '@affine/env/constant'; + +type Action = 'signUp' | 'changePassword' | 'signIn' | 'signUp'; + +export function buildCallbackUrl(action: Action) { + const callbackUrl = `/auth/${action}`; + const params: string[][] = []; + if (isDesktop && window.appInfo.schema) { + params.push(['schema', window.appInfo.schema]); + } + const query = + params.length > 0 + ? '?' + params.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&') + : ''; + return callbackUrl + query; +} diff --git a/apps/core/src/components/affine/auth/index.tsx b/apps/core/src/components/affine/auth/index.tsx new file mode 100644 index 0000000000..314fed2780 --- /dev/null +++ b/apps/core/src/components/affine/auth/index.tsx @@ -0,0 +1,155 @@ +import { + AuthModal as AuthModalBase, + type AuthModalProps as AuthModalBaseProps, +} from '@affine/component/auth-components'; +import { isDesktop } from '@affine/env/constant'; +import { atom, useAtom } from 'jotai'; +import { type FC, useCallback, useEffect, useMemo } from 'react'; + +import { AfterSignInSendEmail } from './after-sign-in-send-email'; +import { AfterSignUpSendEmail } from './after-sign-up-send-email'; +import { SendEmail } from './send-email'; +import { SignIn } from './sign-in'; +import { SignInWithPassword } from './sign-in-with-password'; + +export type AuthProps = { + state: + | 'signIn' + | 'afterSignUpSendEmail' + | 'afterSignInSendEmail' + // throw away + | 'signInWithPassword' + | 'sendEmail'; + setAuthState: (state: AuthProps['state']) => void; + setAuthEmail: (state: AuthProps['email']) => void; + setEmailType: (state: AuthProps['emailType']) => void; + email: string; + emailType: 'setPassword' | 'changePassword' | 'changeEmail'; + onSignedIn?: () => void; +}; + +export type AuthPanelProps = { + email: string; + setAuthState: AuthProps['setAuthState']; + setAuthEmail: AuthProps['setAuthEmail']; + setEmailType: AuthProps['setEmailType']; + emailType: AuthProps['emailType']; + onSignedIn?: () => void; + authStore: AuthStoreAtom; + setAuthStore: (data: Partial) => void; +}; + +const config: { + [k in AuthProps['state']]: FC; +} = { + signIn: SignIn, + afterSignUpSendEmail: AfterSignUpSendEmail, + afterSignInSendEmail: AfterSignInSendEmail, + signInWithPassword: SignInWithPassword, + sendEmail: SendEmail, +}; + +type AuthStoreAtom = { + hasSentEmail: boolean; + resendCountDown: number; +}; +export const authStoreAtom = atom({ + hasSentEmail: false, + resendCountDown: 60, +}); + +export const AuthModal: FC = ({ + open, + state, + setOpen, + email, + setAuthEmail, + setAuthState, + setEmailType, + emailType, +}) => { + const [, setAuthStore] = useAtom(authStoreAtom); + + useEffect(() => { + if (!open) { + setAuthStore({ + hasSentEmail: false, + resendCountDown: 60, + }); + setAuthEmail(''); + } + }, [open, setAuthEmail, setAuthStore]); + + useEffect(() => { + if (isDesktop) { + return window.events?.ui.onFinishLogin(() => { + setOpen(false); + }); + } + return; + }, [setOpen]); + + const onSignedIn = useCallback(() => { + setOpen(false); + }, [setOpen]); + + return ( + + + + ); +}; + +export const AuthPanel: FC = ({ + state, + email, + setAuthEmail, + setAuthState, + setEmailType, + emailType, + onSignedIn, +}) => { + const [authStore, setAuthStore] = useAtom(authStoreAtom); + + const CurrentPanel = useMemo(() => { + return config[state]; + }, [state]); + + useEffect(() => { + return () => { + setAuthStore({ + hasSentEmail: false, + resendCountDown: 60, + }); + }; + }, [setAuthEmail, setAuthStore]); + + return ( + ) => { + setAuthStore(prev => ({ + ...prev, + ...data, + })); + }, + [setAuthStore] + )} + /> + ); +}; diff --git a/apps/core/src/components/affine/auth/send-email.tsx b/apps/core/src/components/affine/auth/send-email.tsx new file mode 100644 index 0000000000..8d13421528 --- /dev/null +++ b/apps/core/src/components/affine/auth/send-email.tsx @@ -0,0 +1,185 @@ +import { Wrapper } from '@affine/component'; +import { + AuthContent, + AuthInput, + BackButton, + ModalHeader, +} from '@affine/component/auth-components'; +import { pushNotificationAtom } from '@affine/component/notification-center'; +import { isDesktop } from '@affine/env/constant'; +import { + sendChangeEmailMutation, + sendChangePasswordEmailMutation, + sendSetPasswordEmailMutation, +} from '@affine/graphql'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { useMutation } from '@affine/workspace/affine/gql'; +import { Button } from '@toeverything/components/button'; +import { useSetAtom } from 'jotai/react'; +import { type FC, useCallback } from 'react'; + +import type { AuthPanelProps } from './index'; + +const useEmailTitle = (emailType: AuthPanelProps['emailType']) => { + const t = useAFFiNEI18N(); + + switch (emailType) { + case 'setPassword': + return t['com.affine.auth.set.password'](); + case 'changePassword': + return t['com.affine.auth.reset.password'](); + case 'changeEmail': + return t['com.affine.settings.email.action'](); + } +}; + +const useNotificationHint = (emailType: AuthPanelProps['emailType']) => { + const t = useAFFiNEI18N(); + + switch (emailType) { + case 'setPassword': + return t['com.affine.auth.sent.set.password.hint'](); + case 'changePassword': + return t['com.affine.auth.sent.change.password.hint'](); + case 'changeEmail': + return t['com.affine.auth.sent.change.email.hint'](); + } +}; +const useButtonContent = (emailType: AuthPanelProps['emailType']) => { + const t = useAFFiNEI18N(); + + switch (emailType) { + case 'setPassword': + return t['com.affine.auth.send.set.password.link'](); + case 'changePassword': + return t['com.affine.auth.send.reset.password.link'](); + case 'changeEmail': + return t['com.affine.auth.send.change.email.link'](); + } +}; + +const useSendEmail = (emailType: AuthPanelProps['emailType']) => { + const { + trigger: sendChangePasswordEmail, + isMutating: isChangePasswordMutating, + } = useMutation({ + mutation: sendChangePasswordEmailMutation, + }); + const { trigger: sendSetPasswordEmail, isMutating: isSetPasswordMutating } = + useMutation({ + mutation: sendSetPasswordEmailMutation, + }); + const { trigger: sendChangeEmail, isMutating: isChangeEmailMutating } = + useMutation({ + mutation: sendChangeEmailMutation, + }); + + return { + loading: + isChangePasswordMutating || + isSetPasswordMutating || + isChangeEmailMutating, + sendEmail: useCallback( + (email: string) => { + let trigger: (args: { + email: string; + callbackUrl: string; + }) => Promise; + let callbackUrl; + switch (emailType) { + case 'setPassword': + trigger = sendSetPasswordEmail; + callbackUrl = 'setPassword'; + break; + case 'changePassword': + trigger = sendChangePasswordEmail; + callbackUrl = 'changePassword'; + break; + case 'changeEmail': + trigger = sendChangeEmail; + callbackUrl = 'changeEmail'; + break; + } + // TODO: add error handler + return trigger({ + email, + callbackUrl: `/auth/${callbackUrl}?isClient=${ + isDesktop ? 'true' : 'false' + }`, + }); + }, + [ + emailType, + sendChangeEmail, + sendChangePasswordEmail, + sendSetPasswordEmail, + ] + ), + }; +}; + +export const SendEmail: FC = ({ + setAuthState, + setAuthStore, + email, + authStore: { hasSentEmail }, + emailType, +}) => { + const t = useAFFiNEI18N(); + const pushNotification = useSetAtom(pushNotificationAtom); + + const title = useEmailTitle(emailType); + const hint = useNotificationHint(emailType); + const buttonContent = useButtonContent(emailType); + const { loading, sendEmail } = useSendEmail(emailType); + + const onSendEmail = useCallback(async () => { + // TODO: add error handler + await sendEmail(email); + + pushNotification({ + title: hint, + message: '', + key: Date.now().toString(), + type: 'success', + }); + setAuthStore({ hasSentEmail: true }); + }, [email, hint, pushNotification, sendEmail, setAuthStore]); + + return ( + <> + + {t['com.affine.auth.reset.password.message']()} + + + + + + + { + setAuthState('signIn'); + }, [setAuthState])} + /> + + ); +}; diff --git a/apps/core/src/components/affine/auth/sign-in-with-password.tsx b/apps/core/src/components/affine/auth/sign-in-with-password.tsx new file mode 100644 index 0000000000..27c31ee7c7 --- /dev/null +++ b/apps/core/src/components/affine/auth/sign-in-with-password.tsx @@ -0,0 +1,111 @@ +import { Wrapper } from '@affine/component'; +import { + AuthInput, + BackButton, + ModalHeader, +} from '@affine/component/auth-components'; +import { pushNotificationAtom } from '@affine/component/notification-center'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button } from '@toeverything/components/button'; +import { useSetAtom } from 'jotai'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { signIn, useSession } from 'next-auth/react'; +import type { FC } from 'react'; +import { useCallback, useState } from 'react'; + +import type { AuthPanelProps } from './index'; +import { forgetPasswordButton } from './style.css'; + +export const SignInWithPassword: FC = ({ + setAuthState, + email, + onSignedIn, +}) => { + const t = useAFFiNEI18N(); + const { update } = useSession(); + + const pushNotification = useSetAtom(pushNotificationAtom); + + const [password, setPassword] = useState(''); + const [passwordError, setPasswordError] = useState(false); + + const onSignIn = useCallback(async () => { + const res = await signIn('credentials', { + redirect: false, + email, + password, + }).catch(console.error); + + if (!res?.ok) { + return setPasswordError(true); + } + + await update(); + onSignedIn?.(); + pushNotification({ + title: `${email}${t['com.affine.auth.has.signed']()}`, + message: '', + key: Date.now().toString(), + type: 'success', + }); + }, [email, password, pushNotification, onSignedIn, t, update]); + + return ( + <> + + + + + { + setPassword(value); + }, [])} + error={passwordError} + errorHint={t['com.affine.auth.password.error']()} + onEnter={onSignIn} + /> + + + + + + { + setAuthState('afterSignInSendEmail'); + }, [setAuthState])} + /> + + ); +}; diff --git a/apps/core/src/components/affine/auth/sign-in.tsx b/apps/core/src/components/affine/auth/sign-in.tsx new file mode 100644 index 0000000000..d5c3786092 --- /dev/null +++ b/apps/core/src/components/affine/auth/sign-in.tsx @@ -0,0 +1,151 @@ +import { AuthInput, ModalHeader } from '@affine/component/auth-components'; +import { pushNotificationAtom } from '@affine/component/notification-center'; +import type { Notification } from '@affine/component/notification-center/index.jotai'; +import { getUserQuery } from '@affine/graphql'; +import { Trans } from '@affine/i18n'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { useMutation } from '@affine/workspace/affine/gql'; +import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons'; +import { Button } from '@toeverything/components/button'; +import { useSetAtom } from 'jotai'; +import { signIn, type SignInResponse } from 'next-auth/react'; +import { type FC, useState } from 'react'; +import { useCallback } from 'react'; + +import { emailRegex } from '../../../utils/email-regex'; +import { buildCallbackUrl } from './callback-url'; +import type { AuthPanelProps } from './index'; +import * as style from './style.css'; + +function validateEmail(email: string) { + return emailRegex.test(email); +} + +function handleSendEmailError( + res: SignInResponse | undefined, + pushNotification: (notification: Notification) => void +) { + if (res?.error) { + pushNotification({ + title: 'Send email error', + message: 'Please back to home and try again', + type: 'error', + }); + } +} + +export const SignIn: FC = ({ + setAuthState, + setAuthEmail, + email, +}) => { + const t = useAFFiNEI18N(); + + const { trigger: verifyUser, isMutating } = useMutation({ + mutation: getUserQuery, + }); + const [isValidEmail, setIsValidEmail] = useState(true); + const pushNotification = useSetAtom(pushNotificationAtom); + const onContinue = useCallback(async () => { + if (!validateEmail(email)) { + setIsValidEmail(false); + return; + } + + setIsValidEmail(true); + const { user } = await verifyUser({ email }); + + setAuthEmail(email); + if (user) { + signIn('email', { + email: email, + callbackUrl: buildCallbackUrl('signIn'), + redirect: false, + }) + .then(res => handleSendEmailError(res, pushNotification)) + .catch(console.error); + setAuthState('afterSignInSendEmail'); + } else { + signIn('email', { + email: email, + callbackUrl: buildCallbackUrl('signUp'), + redirect: false, + }) + .then(res => handleSendEmailError(res, pushNotification)) + .catch(console.error); + + setAuthState('afterSignUpSendEmail'); + } + }, [email, setAuthEmail, setAuthState, verifyUser, pushNotification]); + return ( + <> + + + + +
+ { + setAuthEmail(value); + }, + [setAuthEmail] + )} + error={!isValidEmail} + errorHint={ + isValidEmail ? '' : t['com.affine.auth.sign.email.error']() + } + onEnter={onContinue} + /> + + + +
+ {/*prettier-ignore*/} + + By clicking "Continue with Google/Email" above, you acknowledge that + you agree to AFFiNE's Terms of Conditions and Privacy Policy. + +
+
+ + ); +}; diff --git a/apps/core/src/components/affine/auth/style.css.ts b/apps/core/src/components/affine/auth/style.css.ts new file mode 100644 index 0000000000..3ee63abd4f --- /dev/null +++ b/apps/core/src/components/affine/auth/style.css.ts @@ -0,0 +1,28 @@ +import { globalStyle, style } from '@vanilla-extract/css'; + +export const authModalContent = style({ + marginTop: '30px', +}); + +export const authMessage = style({ + marginTop: '30px', + color: 'var(--affine-text-secondary-color)', + fontSize: 'var(--affine-font-xs)', + lineHeight: 1.5, +}); +globalStyle(`${authMessage} a`, { + color: 'var(--affine-link-color)', +}); +globalStyle(`${authMessage} .link`, { + cursor: 'pointer', + color: 'var(--affine-link-color)', +}); + +export const forgetPasswordButton = style({ + fontSize: 'var(--affine-font-sm)', + color: 'var(--affine-text-secondary-color)', + position: 'absolute', + right: 0, + bottom: 0, + display: 'none', +}); diff --git a/apps/core/src/components/affine/enable-affine-cloud-modal/index.tsx b/apps/core/src/components/affine/enable-affine-cloud-modal/index.tsx index e99245f8c6..657ec8c1b0 100644 --- a/apps/core/src/components/affine/enable-affine-cloud-modal/index.tsx +++ b/apps/core/src/components/affine/enable-affine-cloud-modal/index.tsx @@ -1,9 +1,9 @@ -import { Modal, ModalWrapper, Wrapper } from '@affine/component'; +import { Modal, ModalWrapper } from '@affine/component'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CloseIcon } from '@blocksuite/icons'; import { Button, IconButton } from '@toeverything/components/button'; -import { Content, ContentTitle, Header, StyleTips } from './style'; +import { ButtonContainer, Content, Header, StyleTips, Title } from './style'; interface EnableAffineCloudModalProps { open: boolean; @@ -20,32 +20,32 @@ export const EnableAffineCloudModal = ({ return ( - +
+ {t['Enable AFFiNE Cloud']()}
- {t['Enable AFFiNE Cloud']()}? {t['Enable AFFiNE Cloud Description']()} - {/* {t('Retain cached cloud data')} */} - - - - + +
+ +
+
+ +
+
diff --git a/apps/core/src/components/affine/enable-affine-cloud-modal/style.ts b/apps/core/src/components/affine/enable-affine-cloud-modal/style.ts index e47d99858e..40fe762b7a 100644 --- a/apps/core/src/components/affine/enable-affine-cloud-modal/style.ts +++ b/apps/core/src/components/affine/enable-affine-cloud-modal/style.ts @@ -1,31 +1,35 @@ import { styled } from '@affine/component'; export const Header = styled('div')({ - height: '44px', display: 'flex', - flexDirection: 'row-reverse', - paddingRight: '10px', - paddingTop: '10px', - flexShrink: 0, + justifyContent: 'space-between', + paddingRight: '20px', + paddingTop: '20px', + paddingLeft: '24px', + alignItems: 'center', }); export const Content = styled('div')({ - textAlign: 'center', + padding: '12px 24px 20px 24px', }); -export const ContentTitle = styled('h1')({ - fontSize: '20px', - lineHeight: '28px', +export const Title = styled('div')({ + fontSize: 'var(--affine-font-h6)', + lineHeight: '26px', fontWeight: 600, - textAlign: 'center', }); export const StyleTips = styled('div')(() => { return { userSelect: 'none', - width: '400px', - margin: 'auto', - marginBottom: '32px', - marginTop: '12px', + marginBottom: '20px', + }; +}); +export const ButtonContainer = styled('div')(() => { + return { + display: 'flex', + justifyContent: 'flex-end', + gap: '20px', + paddingTop: '20px', }; }); diff --git a/apps/core/src/components/affine/new-workspace-setting-detail/delete-leave-workspace/delete/index.tsx b/apps/core/src/components/affine/new-workspace-setting-detail/delete-leave-workspace/delete/index.tsx index e9dbc3a70d..bb7c253447 100644 --- a/apps/core/src/components/affine/new-workspace-setting-detail/delete-leave-workspace/delete/index.tsx +++ b/apps/core/src/components/affine/new-workspace-setting-detail/delete-leave-workspace/delete/index.tsx @@ -1,13 +1,12 @@ import { Input, Modal, ModalCloseButton } from '@affine/component'; +import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { Button } from '@toeverything/components/button'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; -import { useCallback, useState } from 'react'; +import { useState } from 'react'; -import type { AffineOfficialWorkspace } from '../../../../../shared'; -import { toast } from '../../../../../utils'; import { StyledButtonContent, StyledInputContent, @@ -21,14 +20,14 @@ interface WorkspaceDeleteProps { open: boolean; onClose: () => void; workspace: AffineOfficialWorkspace; - onDeleteWorkspace: (id: string) => Promise; + onConfirm: () => void; } export const WorkspaceDeleteModal = ({ open, onClose, + onConfirm, workspace, - onDeleteWorkspace, }: WorkspaceDeleteProps) => { const [workspaceName] = useBlockSuiteWorkspaceName( workspace.blockSuiteWorkspace @@ -37,19 +36,6 @@ export const WorkspaceDeleteModal = ({ const allowDelete = deleteStr === workspaceName; const t = useAFFiNEI18N(); - const handleDelete = useCallback(() => { - onDeleteWorkspace(workspace.id) - .then(() => { - toast(t['Successfully deleted'](), { - portal: document.body, - }); - onClose(); - }) - .catch(() => { - // ignore error - }); - }, [onClose, onDeleteWorkspace, t, workspace.id]); - return ( @@ -99,7 +85,7 @@ export const WorkspaceDeleteModal = ({ - - - - - ); -}; diff --git a/apps/core/src/components/affine/new-workspace-setting-detail/delete-leave-workspace/leave/style.ts b/apps/core/src/components/affine/new-workspace-setting-detail/delete-leave-workspace/leave/style.ts deleted file mode 100644 index 5a54f0deef..0000000000 --- a/apps/core/src/components/affine/new-workspace-setting-detail/delete-leave-workspace/leave/style.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { styled } from '@affine/component'; - -export const StyledModalWrapper = styled('div')(() => { - return { - position: 'relative', - padding: '0px', - width: '460px', - background: 'var(--affine-white)', - borderRadius: '12px', - }; -}); - -export const StyledModalHeader = styled('div')(() => { - return { - margin: '44px 0px 12px 0px', - width: '460px', - fontWeight: '600', - fontSize: '20px;', - textAlign: 'center', - }; -}); - -// export const StyledModalContent = styled('div')(({ theme }) => {}); - -export const StyledTextContent = styled('div')(() => { - return { - margin: 'auto', - width: '425px', - fontStyle: 'normal', - fontWeight: '400', - fontSize: '18px', - lineHeight: '26px', - textAlign: 'center', - }; -}); - -export const StyledButtonContent = styled('div')(() => { - return { - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - margin: '0px 0 32px 0', - }; -}); diff --git a/apps/core/src/components/affine/new-workspace-setting-detail/export.tsx b/apps/core/src/components/affine/new-workspace-setting-detail/export.tsx index e2bc1bc45c..66510bb647 100644 --- a/apps/core/src/components/affine/new-workspace-setting-detail/export.tsx +++ b/apps/core/src/components/affine/new-workspace-setting-detail/export.tsx @@ -1,13 +1,12 @@ import { toast } from '@affine/component'; import { SettingRow } from '@affine/component/setting-components'; import { isDesktop } from '@affine/env/constant'; +import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { Button } from '@toeverything/components/button'; import type { SaveDBFileResult } from '@toeverything/infra/type'; import { useCallback } from 'react'; -import type { AffineOfficialWorkspace } from '../../../shared'; - async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) { if (window.apis && isDesktop) { const bs = workspace.blockSuiteWorkspace.blobs; @@ -41,7 +40,7 @@ export const ExportPanel = ({ workspace }: ExportPanelProps) => { const result: SaveDBFileResult = await window.apis?.dialog.saveDBFileAs(workspaceId); if (result?.error) { - toast(t[result.error]()); + toast(result.error); } else if (!result?.canceled) { toast(t['Export success']()); } diff --git a/apps/core/src/components/affine/new-workspace-setting-detail/index.tsx b/apps/core/src/components/affine/new-workspace-setting-detail/index.tsx index 87d159044c..18c295585f 100644 --- a/apps/core/src/components/affine/new-workspace-setting-detail/index.tsx +++ b/apps/core/src/components/affine/new-workspace-setting-detail/index.tsx @@ -14,13 +14,17 @@ import { useMemo } from 'react'; import { useWorkspace } from '../../../hooks/use-workspace'; import { DeleteLeaveWorkspace } from './delete-leave-workspace'; import { ExportPanel } from './export'; +import { MembersPanel } from './members'; import { ProfilePanel } from './profile'; import { PublishPanel } from './publish'; import { StoragePanel } from './storage'; export interface WorkspaceSettingDetailProps { workspaceId: string; - onDeleteWorkspace: (id: string) => Promise; + isOwner: boolean; + onDeleteLocalWorkspace: () => void; + onDeleteCloudWorkspace: () => void; + onLeaveWorkspace: () => void; onTransferWorkspace: < From extends WorkspaceFlavour, To extends WorkspaceFlavour, @@ -31,11 +35,8 @@ export interface WorkspaceSettingDetailProps { ) => void; } -export const WorkspaceSettingDetail = ({ - workspaceId, - onDeleteWorkspace, - ...props -}: WorkspaceSettingDetailProps) => { +export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => { + const { workspaceId } = props; const t = useAFFiNEI18N(); const workspace = useWorkspace(workspaceId); const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace); @@ -67,22 +68,16 @@ export const WorkspaceSettingDetail = ({ desc={t['com.affine.settings.workspace.not-owner']()} spreadCol={false} > - + - + + {storageAndExportSetting} - + ); diff --git a/apps/core/src/components/affine/new-workspace-setting-detail/members.tsx b/apps/core/src/components/affine/new-workspace-setting-detail/members.tsx new file mode 100644 index 0000000000..ad07afd2da --- /dev/null +++ b/apps/core/src/components/affine/new-workspace-setting-detail/members.tsx @@ -0,0 +1,228 @@ +import { Menu, MenuItem } from '@affine/component'; +import { + InviteModal, + type InviteModalProps, +} from '@affine/component/member-components'; +import { pushNotificationAtom } from '@affine/component/notification-center'; +import { SettingRow } from '@affine/component/setting-components'; +import type { AffineOfficialWorkspace } from '@affine/env/workspace'; +import { WorkspaceFlavour } from '@affine/env/workspace'; +import { Permission } from '@affine/graphql'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { MoreVerticalIcon } from '@blocksuite/icons'; +import { Avatar } from '@toeverything/components/avatar'; +import { Button, IconButton } from '@toeverything/components/button'; +import { Tooltip } from '@toeverything/components/tooltip'; +import clsx from 'clsx'; +import { useSetAtom } from 'jotai/react'; +import type { ReactElement } from 'react'; +import { Suspense, useCallback, useMemo, useState } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; + +import type { CheckedUser } from '../../../hooks/affine/use-current-user'; +import { useCurrentUser } from '../../../hooks/affine/use-current-user'; +import { useInviteMember } from '../../../hooks/affine/use-invite-member'; +import { type Member, useMembers } from '../../../hooks/affine/use-members'; +import { useRevokeMemberPermission } from '../../../hooks/affine/use-revoke-member-permission'; +import { AnyErrorBoundary } from '../any-error-boundary'; +import { type WorkspaceSettingDetailProps } from './index'; +import * as style from './style.css'; + +export interface MembersPanelProps extends WorkspaceSettingDetailProps { + workspace: AffineOfficialWorkspace; +} + +const MembersPanelLocal = () => { + const t = useAFFiNEI18N(); + return ( + +
+ + + +
+
+ ); +}; + +export const CloudWorkspaceMembersPanel = ({ + workspace, + isOwner, +}: MembersPanelProps): ReactElement => { + const workspaceId = workspace.id; + const members = useMembers(workspaceId); + const t = useAFFiNEI18N(); + const currentUser = useCurrentUser(); + const { invite, isMutating } = useInviteMember(workspaceId); + const [open, setOpen] = useState(false); + const pushNotification = useSetAtom(pushNotificationAtom); + const revokeMemberPermission = useRevokeMemberPermission(workspaceId); + + const memberCount = members.length; + const memberList = useMemo( + () => + members.sort((a, b) => { + if ( + a.permission === Permission.Owner && + b.permission !== Permission.Owner + ) { + return -1; + } + if ( + a.permission !== Permission.Owner && + b.permission === Permission.Owner + ) { + return 1; + } + return 0; + }), + [members] + ); + + const openModal = useCallback(() => { + setOpen(true); + }, []); + + const onInviteConfirm = useCallback( + async ({ email, permission }) => { + const success = await invite( + email, + permission, + // send invite email + true + ); + if (success) { + pushNotification({ + title: t['Invitation sent'](), + message: t['Invitation sent hint'](), + type: 'success', + }); + setOpen(false); + } + }, + [invite, pushNotification, t] + ); + + return ( + <> + + {isOwner ? ( + <> + + + + ) : null} + +
+ {memberList.map(member => ( + + ))} +
+ + ); +}; + +const MemberItem = ({ + member, + isOwner, + currentUser, + onRevoke, +}: { + member: Member; + isOwner: boolean; + currentUser: CheckedUser; + onRevoke: (memberId: string) => void; +}) => { + const t = useAFFiNEI18N(); + + const handleRevoke = useCallback(() => { + onRevoke(member.id); + }, [onRevoke, member.id]); + + const operationButtonInfo = useMemo(() => { + return { + show: isOwner && currentUser.id !== member.id, + leaveOrRevokeText: t['Remove from workspace'](), + }; + }, [currentUser.id, isOwner, member.id, t]); + + return ( + <> +
+ +
+ {member.emailVerified ? ( + <> +
{member.name}
+
{member.email}
+ + ) : ( +
{member.email}
+ )} +
+
+ {member.accepted + ? member.permission === Permission.Owner + ? 'Workspace Owner' + : 'Member' + : 'Pending'} +
+ + {operationButtonInfo.leaveOrRevokeText} + + } + placement="bottom" + disablePortal={true} + trigger="click" + > + + + + +
+ + ); +}; + +export const MembersPanel = (props: MembersPanelProps): ReactElement | null => { + if (props.workspace.flavour === WorkspaceFlavour.LOCAL) { + return ; + } + return ( + + + + + + ); +}; diff --git a/apps/core/src/components/affine/new-workspace-setting-detail/profile.tsx b/apps/core/src/components/affine/new-workspace-setting-detail/profile.tsx index 5aea314987..5b49e8c8f3 100644 --- a/apps/core/src/components/affine/new-workspace-setting-detail/profile.tsx +++ b/apps/core/src/components/affine/new-workspace-setting-detail/profile.tsx @@ -1,21 +1,23 @@ import { FlexWrapper, Input, toast, Wrapper } from '@affine/component'; import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; +import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CameraIcon, DoneIcon } from '@blocksuite/icons'; import { IconButton } from '@toeverything/components/button'; import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; +import clsx from 'clsx'; import { useCallback, useState } from 'react'; -import type { AffineOfficialWorkspace } from '../../../shared'; import { Upload } from '../../pure/file-upload'; +import { type WorkspaceSettingDetailProps } from './index'; import * as style from './style.css'; -interface ProfilePanelProps { +export interface ProfilePanelProps extends WorkspaceSettingDetailProps { workspace: AffineOfficialWorkspace; } -export const ProfilePanel = ({ workspace }: ProfilePanelProps) => { +export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => { const t = useAFFiNEI18N(); const [, update] = useBlockSuiteWorkspaceAvatarUrl( @@ -38,7 +40,7 @@ export const ProfilePanel = ({ workspace }: ProfilePanelProps) => { return (
-
+
{
{t['Workspace Name']()}
{ - workspace: AffineCloudWorkspace; + workspace: AffineCloudWorkspace | AffinePublicWorkspace; } const PublishPanelAffine = (props: PublishPanelAffineProps) => { const { workspace } = props; const t = useAFFiNEI18N(); // const toggleWorkspacePublish = useToggleWorkspacePublish(workspace); - + const isPublic = useMemo(() => { + return workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC; + }, [workspace]); const [origin, setOrigin] = useState(''); const shareUrl = origin + '/public-workspace/' + workspace.id; @@ -54,35 +57,35 @@ const PublishPanelAffine = (props: PublishPanelAffineProps) => { }, [shareUrl, t]); return ( - <> +
- {/* toggleWorkspacePublish(checked)} - /> */} + { + // console.log('onChange', value); + // }, [])} + /> - - - - - + {isPublic ? ( + + + + + ) : null} +
); }; @@ -164,7 +167,10 @@ const PublishPanelLocal = ({ }; export const PublishPanel = (props: PublishPanelProps) => { - if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) { + if ( + props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD || + props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC + ) { return ; } else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) { return ; diff --git a/apps/core/src/components/affine/new-workspace-setting-detail/storage.tsx b/apps/core/src/components/affine/new-workspace-setting-detail/storage.tsx index 3e2d402995..afe7b918f0 100644 --- a/apps/core/src/components/affine/new-workspace-setting-detail/storage.tsx +++ b/apps/core/src/components/affine/new-workspace-setting-detail/storage.tsx @@ -1,5 +1,6 @@ import { FlexWrapper, toast } from '@affine/component'; import { SettingRow } from '@affine/component/setting-components'; +import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { Button } from '@toeverything/components/button'; import { Tooltip } from '@toeverything/components/tooltip'; @@ -7,9 +8,6 @@ import type { MoveDBFileResult } from '@toeverything/infra/type'; import { useMemo } from 'react'; import { useCallback, useEffect, useState } from 'react'; -import type { AffineOfficialWorkspace } from '../../../shared'; -import * as style from './style.css'; - const useDBFileSecondaryPath = (workspaceId: string) => { const [path, setPath] = useState(undefined); useEffect(() => { @@ -83,7 +81,7 @@ export const StoragePanel = ({ workspace }: StoragePanelProps) => { > + + + + + + { + signOut().catch(console.error); + }, [])} + > + + + {/**/} + {/* {t['com.affine.setting.account.delete']()}*/} + {/* */} + {/* }*/} + {/* desc={t['com.affine.setting.account.delete.message']()}*/} + {/* style={{ cursor: 'pointer' }}*/} + {/* onClick={useCallback(() => {*/} + {/* toast('Function coming soon');*/} + {/* }, [])}*/} + {/* testId="delete-account-button"*/} + {/*>*/} + {/* */} + {/**/} + + ); }; diff --git a/apps/core/src/components/affine/setting-modal/account-setting/style.css.ts b/apps/core/src/components/affine/setting-modal/account-setting/style.css.ts new file mode 100644 index 0000000000..8c012c498b --- /dev/null +++ b/apps/core/src/components/affine/setting-modal/account-setting/style.css.ts @@ -0,0 +1,41 @@ +import { globalStyle, style } from '@vanilla-extract/css'; +export const profileInputWrapper = style({ + marginLeft: '20px', +}); +globalStyle(`${profileInputWrapper} label`, { + display: 'block', + fontSize: 'var(--affine-font-xs)', + color: 'var(--affine-text-secondary-color)', + marginBottom: '4px', +}); + +export const avatarWrapper = style({ + width: '56px', + height: '56px', + borderRadius: '50%', + position: 'relative', + overflow: 'hidden', + cursor: 'pointer', + flexShrink: '0', + selectors: { + '&.disable': { + cursor: 'default', + pointerEvents: 'none', + }, + }, +}); +globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, { + display: 'flex', +}); +globalStyle(`${avatarWrapper} .camera-icon-wrapper`, { + width: '100%', + height: '100%', + position: 'absolute', + display: 'none', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(60, 61, 63, 0.5)', + zIndex: '1', + color: 'var(--affine-white)', + fontSize: 'var(--affine-font-h-4)', +}); diff --git a/apps/core/src/components/affine/setting-modal/index.tsx b/apps/core/src/components/affine/setting-modal/index.tsx index 20236f4096..3864f02872 100644 --- a/apps/core/src/components/affine/setting-modal/index.tsx +++ b/apps/core/src/components/affine/setting-modal/index.tsx @@ -7,6 +7,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { ContactWithUsIcon } from '@blocksuite/icons'; import { Suspense, useCallback } from 'react'; +import { useCurrenLoginStatus } from '../../../hooks/affine/use-curren-login-status'; import { AccountSetting } from './account-setting'; import { GeneralSetting, @@ -38,6 +39,7 @@ export const SettingModal = ({ onSettingClick, }: SettingModalProps) => { const t = useAFFiNEI18N(); + const loginStatus = useCurrenLoginStatus(); const generalSettingList = useGeneralSettingList(); @@ -85,7 +87,9 @@ export const SettingModal = ({ {generalSettingList.find(v => v.key === activeTab) ? ( ) : null} - {activeTab === 'account' ? : null} + {activeTab === 'account' && loginStatus === 'authenticated' ? ( + + ) : null}
diff --git a/apps/core/src/components/affine/setting-modal/setting-sidebar/index.tsx b/apps/core/src/components/affine/setting-modal/setting-sidebar/index.tsx index 3d2a93b20e..a7e28e132f 100644 --- a/apps/core/src/components/affine/setting-modal/setting-sidebar/index.tsx +++ b/apps/core/src/components/affine/setting-modal/setting-sidebar/index.tsx @@ -1,41 +1,101 @@ -import { ScrollableContainer } from '@affine/component'; import { WorkspaceListItemSkeleton, WorkspaceListSkeleton, } from '@affine/component/setting-components'; +import { UserAvatar } from '@affine/component/user-avatar'; import { WorkspaceAvatar } from '@affine/component/workspace-avatar'; +import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; +import { Logo1Icon } from '@blocksuite/icons'; import { Tooltip } from '@toeverything/components/tooltip'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react'; import clsx from 'clsx'; -import { useAtomValue } from 'jotai'; -import { Suspense, useRef } from 'react'; +import { useAtom, useAtomValue } from 'jotai/react'; +import { + type ReactElement, + Suspense, + useCallback, + useMemo, + useRef, +} from 'react'; +import { authAtom } from '../../../../atoms'; +import { useCurrenLoginStatus } from '../../../../hooks/affine/use-curren-login-status'; +import { useCurrentUser } from '../../../../hooks/affine/use-current-user'; import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace'; import type { GeneralSettingKeys, GeneralSettingList, } from '../general-setting'; import { + accountButton, currentWorkspaceLabel, settingSlideBar, + sidebarFooter, sidebarItemsWrapper, sidebarSelectItem, sidebarSubtitle, sidebarTitle, } from './style.css'; -interface SettingSidebarProps { - generalSettingList: GeneralSettingList; - onGeneralSettingClick: (key: GeneralSettingKeys) => void; - onWorkspaceSettingClick: (workspaceId: string) => void; - selectedWorkspaceId: string | null; - selectedGeneralKey: string | null; +export type UserInfoProps = { onAccountSettingClick: () => void; -} +}; + +export const UserInfo = ({ + onAccountSettingClick, +}: UserInfoProps): ReactElement => { + const user = useCurrentUser(); + return ( +
+ + +
+
+ {user.name} +
+
+ {user.email} +
+
+
+ ); +}; + +export const SignInButton = () => { + const t = useAFFiNEI18N(); + const [, setAuthModal] = useAtom(authAtom); + + return ( +
{ + setAuthModal({ openModal: true, state: 'signIn' }); + }, [setAuthModal])} + > +
+ +
+ +
+
+ {t['com.affine.settings.sign']()} +
+
+ {t['com.affine.setting.sign.message']()} +
+
+
+ ); +}; export const SettingSidebar = ({ generalSettingList, @@ -43,9 +103,17 @@ export const SettingSidebar = ({ onWorkspaceSettingClick, selectedWorkspaceId, selectedGeneralKey, -}: SettingSidebarProps) => { + onAccountSettingClick, +}: { + generalSettingList: GeneralSettingList; + onGeneralSettingClick: (key: GeneralSettingKeys) => void; + onWorkspaceSettingClick: (workspaceId: string) => void; + selectedWorkspaceId: string | null; + selectedGeneralKey: string | null; + onAccountSettingClick: () => void; +}) => { const t = useAFFiNEI18N(); - + const loginStatus = useCurrenLoginStatus(); return (
{t['Settings']()}
@@ -79,32 +147,43 @@ export const SettingSidebar = ({
}> - - - +
+ +
+ {runtimeConfig.enableCloud && loginStatus === 'unauthenticated' ? ( + + ) : null} + + {runtimeConfig.enableCloud && loginStatus === 'authenticated' ? ( + + ) : null} +
); }; -interface WorkspaceListProps { - onWorkspaceSettingClick: (workspaceId: string) => void; - selectedWorkspaceId: string | null; -} - export const WorkspaceList = ({ onWorkspaceSettingClick, selectedWorkspaceId, -}: WorkspaceListProps) => { +}: { + onWorkspaceSettingClick: (workspaceId: string) => void; + selectedWorkspaceId: string | null; +}) => { const workspaces = useAtomValue(rootWorkspacesMetadataAtom); const [currentWorkspace] = useCurrentWorkspace(); + const workspaceList = useMemo(() => { + return workspaces.filter( + ({ flavour }) => flavour !== WorkspaceFlavour.AFFINE_PUBLIC + ); + }, [workspaces]); return ( <> - {workspaces.map(workspace => { + {workspaceList.map(workspace => { return ( }> void; - isCurrent: boolean; - isActive: boolean; -} - const WorkspaceListItem = ({ meta, onClick, isCurrent, isActive, -}: WorkspaceListItemProps) => { +}: { + meta: RootWorkspaceMetadata; + onClick: () => void; + isCurrent: boolean; + isActive: boolean; +}) => { const workspace = useStaticBlockSuiteWorkspace(meta.id); const [workspaceName] = useBlockSuiteWorkspaceName(workspace); const ref = useRef(null); diff --git a/apps/core/src/components/affine/setting-modal/setting-sidebar/style.css.ts b/apps/core/src/components/affine/setting-modal/setting-sidebar/style.css.ts index 6b885300ad..a9af8c8a11 100644 --- a/apps/core/src/components/affine/setting-modal/setting-sidebar/style.css.ts +++ b/apps/core/src/components/affine/setting-modal/setting-sidebar/style.css.ts @@ -34,7 +34,7 @@ export const sidebarItemsWrapper = style({ selectors: { '&.scroll': { flexGrow: 1, - overflowY: 'hidden', + overflowY: 'auto', }, }, }); @@ -92,6 +92,8 @@ export const currentWorkspaceLabel = style({ }, }); +export const sidebarFooter = style({ padding: '0 16px' }); + export const accountButton = style({ height: '42px', padding: '4px 8px', @@ -110,6 +112,20 @@ globalStyle(`${accountButton} .avatar`, { border: '1px solid', borderColor: 'var(--affine-white)', marginRight: '10px', + flexShrink: 0, +}); + +globalStyle(`${accountButton} .avatar.not-sign`, { + width: '28px', + height: '28px', + borderRadius: '50%', + fontSize: '22px', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderColor: 'var(--affine-border-color)', + color: 'var(--affine-border-color)', + background: 'var(--affine-white)', }); globalStyle(`${accountButton} .content`, { flexGrow: '1', diff --git a/apps/core/src/components/affine/setting-modal/style.css.ts b/apps/core/src/components/affine/setting-modal/style.css.ts index 856cfaab91..d6b72b60f2 100644 --- a/apps/core/src/components/affine/setting-modal/style.css.ts +++ b/apps/core/src/components/affine/setting-modal/style.css.ts @@ -3,16 +3,17 @@ import { globalStyle, style } from '@vanilla-extract/css'; export const settingContent = style({ flexGrow: '1', height: '100%', - padding: '40px 15px 20px', - overflow: 'auto', + padding: '40px 15px', + overflow: 'hidden', }); globalStyle(`${settingContent} .wrapper`, { - width: '66%', - minWidth: '450px', + width: '60%', + padding: '0 15px', height: '100%', - maxWidth: '560px', + minWidth: '560px', margin: '0 auto', + overflowY: 'auto', }); globalStyle(`${settingContent} .wrapper::-webkit-scrollbar`, { diff --git a/apps/core/src/components/affine/setting-modal/workspace-setting/index.tsx b/apps/core/src/components/affine/setting-modal/workspace-setting/index.tsx index 0a9213305a..50659b588c 100644 --- a/apps/core/src/components/affine/setting-modal/workspace-setting/index.tsx +++ b/apps/core/src/components/affine/setting-modal/workspace-setting/index.tsx @@ -1,9 +1,16 @@ +import { pushNotificationAtom } from '@affine/component/notification-center'; +import { WorkspaceSubPath } from '@affine/env/workspace'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/react'; import { useSetAtom } from 'jotai'; +import { useAtomValue } from 'jotai'; import { useCallback } from 'react'; import { getUIAdapter } from '../../../../adapters/workspace'; import { openSettingModalAtom } from '../../../../atoms'; +import { useLeaveWorkspace } from '../../../../hooks/affine/use-leave-workspace'; +import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace'; import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace'; import { RouteLogic, @@ -13,28 +20,83 @@ import { useWorkspace } from '../../../../hooks/use-workspace'; import { useAppHelper } from '../../../../hooks/use-workspaces'; export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => { + const t = useAFFiNEI18N(); + + const { jumpToSubPath, jumpToIndex } = useNavigateHelper(); + const [currentWorkspace] = useCurrentWorkspace(); const workspace = useWorkspace(workspaceId); + const workspaces = useAtomValue(rootWorkspacesMetadataAtom); + const pushNotification = useSetAtom(pushNotificationAtom); + + const leaveWorkspace = useLeaveWorkspace(); usePassiveWorkspaceEffect(workspace.blockSuiteWorkspace); const setSettingModal = useSetAtom(openSettingModalAtom); - const helper = useAppHelper(); - const { jumpToIndex } = useNavigateHelper(); + const { deleteWorkspace } = useAppHelper(); const { NewSettingsDetail } = getUIAdapter(workspace.flavour); - const onDeleteWorkspace = useCallback( - async (id: string) => { - await helper.deleteWorkspace(id); - setSettingModal(prev => ({ ...prev, open: false, workspaceId: null })); - jumpToIndex(RouteLogic.REPLACE); - }, - [helper, jumpToIndex, setSettingModal] - ); + const closeAndJumpOut = useCallback(() => { + setSettingModal(prev => ({ ...prev, open: false, workspaceId: null })); + + if (currentWorkspace.id === workspaceId) { + const backWorkspace = workspaces.find(ws => ws.id !== workspaceId); + // TODO: if there is no workspace, jump to a new page(wait for design) + if (backWorkspace) { + jumpToSubPath( + backWorkspace?.id || '', + WorkspaceSubPath.ALL, + RouteLogic.REPLACE + ); + } else { + setTimeout(() => { + jumpToIndex(RouteLogic.REPLACE); + }, 100); + } + } + }, [ + currentWorkspace.id, + jumpToIndex, + jumpToSubPath, + setSettingModal, + workspaceId, + workspaces, + ]); + + const handleDeleteWorkspace = useCallback(async () => { + closeAndJumpOut(); + await deleteWorkspace(workspaceId); + + pushNotification({ + title: t['Successfully deleted'](), + type: 'success', + }); + }, [closeAndJumpOut, deleteWorkspace, pushNotification, t, workspaceId]); + + const handleLeaveWorkspace = useCallback(async () => { + closeAndJumpOut(); + await leaveWorkspace(workspaceId); + + pushNotification({ + title: 'Successfully leave', + type: 'success', + }); + }, [closeAndJumpOut, leaveWorkspace, pushNotification, workspaceId]); + const onTransformWorkspace = useOnTransformWorkspace(); + // const handleDelete = useCallback(async () => { + // await onDeleteWorkspace(); + // toast(t['Successfully deleted'](), { + // portal: document.body, + // }); + // onClose(); + // }, [onClose, onDeleteWorkspace, t, workspace.id]); return ( ); diff --git a/apps/core/src/components/affine/share-page-modal/index.tsx b/apps/core/src/components/affine/share-page-modal/index.tsx new file mode 100644 index 0000000000..7a270c7520 --- /dev/null +++ b/apps/core/src/components/affine/share-page-modal/index.tsx @@ -0,0 +1,48 @@ +import { ShareMenu } from '@affine/component/share-menu'; +import { + type AffineOfficialWorkspace, + WorkspaceFlavour, +} from '@affine/env/workspace'; +import type { Page } from '@blocksuite/store'; +import { useState } from 'react'; + +import { useIsSharedPage } from '../../../hooks/affine/use-is-shared-page'; +import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace'; +import { EnableAffineCloudModal } from '../enable-affine-cloud-modal'; + +type SharePageModalProps = { + workspace: AffineOfficialWorkspace; + page: Page; +}; + +export const SharePageModal = ({ workspace, page }: SharePageModalProps) => { + const onTransformWorkspace = useOnTransformWorkspace(); + const [open, setOpen] = useState(false); + return ( + <> + setOpen(true)} + togglePagePublic={async () => {}} + /> + {workspace.flavour === WorkspaceFlavour.LOCAL ? ( + { + setOpen(false); + }} + onConfirm={() => { + onTransformWorkspace( + WorkspaceFlavour.LOCAL, + WorkspaceFlavour.AFFINE_CLOUD, + workspace + ); + setOpen(false); + }} + /> + ) : null} + + ); +}; diff --git a/apps/core/src/components/blocksuite/block-suite-header-title/index.tsx b/apps/core/src/components/blocksuite/block-suite-header-title/index.tsx index e8523413bb..0f0006c218 100644 --- a/apps/core/src/components/blocksuite/block-suite-header-title/index.tsx +++ b/apps/core/src/components/blocksuite/block-suite-header-title/index.tsx @@ -1,3 +1,4 @@ +import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useBlockSuitePageMeta, @@ -12,7 +13,6 @@ import { useState, } from 'react'; -import type { AffineOfficialWorkspace } from '../../../shared'; import { EditorModeSwitch } from '../block-suite-mode-switch'; import { PageMenu } from './operation-menu'; import * as styles from './styles.css'; @@ -139,7 +139,7 @@ const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => { }; export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => { - if (props.workspace.flavour === WorkspaceFlavour.PUBLIC) { + if (props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC) { return ; } return ; diff --git a/apps/core/src/components/cloud/login-card.tsx b/apps/core/src/components/cloud/login-card.tsx new file mode 100644 index 0000000000..881dfb4eaa --- /dev/null +++ b/apps/core/src/components/cloud/login-card.tsx @@ -0,0 +1,53 @@ +import { UserAvatar } from '@affine/component/user-avatar'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { CloudWorkspaceIcon } from '@blocksuite/icons'; +import { signIn } from 'next-auth/react'; + +import { useCurrenLoginStatus } from '../../hooks/affine/use-curren-login-status'; +import { useCurrentUser } from '../../hooks/affine/use-current-user'; +import { StyledSignInButton } from '../pure/footer/styles'; + +export const LoginCard = () => { + const t = useAFFiNEI18N(); + const loginStatus = useCurrenLoginStatus(); + if (loginStatus === 'authenticated') { + return ; + } + return ( + { + // jump to login page + signIn().catch(console.error); + }} + > +
+ +
{' '} + {t['Sign in']()} +
+ ); +}; + +const UserCard = () => { + const user = useCurrentUser(); + return ( +
+ +
+
{user.name}
+
{user.email}
+
+
+ ); +}; diff --git a/apps/core/src/components/cloud/provider.tsx b/apps/core/src/components/cloud/provider.tsx new file mode 100644 index 0000000000..fe01c9ac36 --- /dev/null +++ b/apps/core/src/components/cloud/provider.tsx @@ -0,0 +1,58 @@ +import { pushNotificationAtom } from '@affine/component/notification-center'; +import { assertExists } from '@blocksuite/global/utils'; +import { GraphQLError } from 'graphql'; +import { useSetAtom } from 'jotai'; +import type { PropsWithChildren, ReactElement } from 'react'; +import { useCallback } from 'react'; +import type { SWRConfiguration } from 'swr'; +import { SWRConfig } from 'swr'; + +const cloudConfig: SWRConfiguration = { + suspense: true, + use: [ + useSWRNext => (key, fetcher, config) => { + const pushNotification = useSetAtom(pushNotificationAtom); + const fetcherWrapper = useCallback( + async (...args: any[]) => { + assertExists(fetcher); + const d = fetcher(...args); + if (d instanceof Promise) { + return d.catch(e => { + if ( + e instanceof GraphQLError || + (Array.isArray(e) && e[0] instanceof GraphQLError) + ) { + const graphQLError = e instanceof GraphQLError ? e : e[0]; + pushNotification({ + title: 'GraphQL Error', + message: graphQLError.toString(), + key: Date.now().toString(), + type: 'error', + }); + } else { + pushNotification({ + title: 'Error', + message: e.toString(), + key: Date.now().toString(), + type: 'error', + }); + } + throw e; + }); + } + return d; + }, + [fetcher, pushNotification] + ); + return useSWRNext(key, fetcher ? fetcherWrapper : fetcher, config); + }, + ], +}; + +export const Provider = (props: PropsWithChildren): ReactElement => { + if (!runtimeConfig.enableCloud) { + return <>{props.children}; + } + + return {props.children}; +}; diff --git a/apps/core/src/components/pure/footer/index.tsx b/apps/core/src/components/pure/footer/index.tsx index 38ed80cee3..5c87218f5a 100644 --- a/apps/core/src/components/pure/footer/index.tsx +++ b/apps/core/src/components/pure/footer/index.tsx @@ -1,39 +1,42 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { CloudWorkspaceIcon } from '@blocksuite/icons'; -import { Button } from '@toeverything/components/button'; -import { useSetAtom } from 'jotai'; -import { type CSSProperties, forwardRef } from 'react'; +import { signIn } from 'next-auth/react'; +import { type CSSProperties, type FC, forwardRef, useCallback } from 'react'; -import { openDisableCloudAlertModalAtom } from '../../../atoms'; +import { useCurrenLoginStatus } from '../../../hooks/affine/use-curren-login-status'; +// import { openDisableCloudAlertModalAtom } from '../../../atoms'; import { stringToColour } from '../../../utils'; -import { StyledFooter } from './styles'; - -export const Footer = () => { - const t = useAFFiNEI18N(); - const setOpen = useSetAtom(openDisableCloudAlertModalAtom); +import { StyledFooter, StyledSignInButton } from './styles'; +export const Footer: FC = () => { + const loginStatus = useCurrenLoginStatus(); + // const setOpen = useSetAtom(openDisableCloudAlertModalAtom); return ( - + {loginStatus === 'authenticated' ? null : } ); }; +const SignInButton = () => { + const t = useAFFiNEI18N(); + + return ( + { + signIn().catch(console.error); + }, [])} + > +
+ +
+ + {t['Sign in']()} +
+ ); +}; + interface WorkspaceAvatarProps { size: number; name: string | undefined; diff --git a/apps/core/src/components/pure/footer/styles.ts b/apps/core/src/components/pure/footer/styles.ts index 826676036c..5330fee2bc 100644 --- a/apps/core/src/components/pure/footer/styles.ts +++ b/apps/core/src/components/pure/footer/styles.ts @@ -1,4 +1,19 @@ -import { displayFlex, styled, textEllipsis } from '@affine/component'; +import { + displayFlex, + displayInlineFlex, + styled, + textEllipsis, +} from '@affine/component'; + +export const StyledSplitLine = styled('div')(() => { + return { + width: '1px', + height: '20px', + background: 'var(--affine-border-color)', + marginRight: '24px', + }; +}); + export const StyleWorkspaceInfo = styled('div')(() => { return { marginLeft: '15px', @@ -110,3 +125,28 @@ export const StyledModalHeader = styled('div')(() => { ...displayFlex('space-between', 'center'), }; }); + +export const StyledSignInButton = styled('button')(() => { + return { + fontWeight: 600, + paddingLeft: 0, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + paddingRight: '15px', + borderRadius: '8px', + '&:hover': { + backgroundColor: 'var(--affine-hover-color)', + }, + '.circle': { + width: '40px', + height: '40px', + borderRadius: '20px', + color: 'var(--affine-primary-color)', + fontSize: '24px', + flexShrink: 0, + marginRight: '16px', + ...displayInlineFlex('center', 'center'), + }, + }; +}); diff --git a/apps/core/src/components/pure/workspace-list-modal/index.tsx b/apps/core/src/components/pure/workspace-list-modal/index.tsx index e688792ea9..9396daa361 100644 --- a/apps/core/src/components/pure/workspace-list-modal/index.tsx +++ b/apps/core/src/components/pure/workspace-list-modal/index.tsx @@ -8,19 +8,27 @@ import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { RootWorkspaceMetadata } from '@affine/workspace/atom'; import { + AccountIcon, CloudWorkspaceIcon, ImportIcon, MoreHorizontalIcon, PlusIcon, + SignOutIcon, } from '@blocksuite/icons'; import type { DragEndEvent } from '@dnd-kit/core'; import { Popover } from '@mui/material'; import { IconButton } from '@toeverything/components/button'; import { Divider } from '@toeverything/components/divider'; import { useSetAtom } from 'jotai'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { signOut, useSession } from 'next-auth/react'; import { useCallback } from 'react'; -import { openDisableCloudAlertModalAtom } from '../../../atoms'; +import { + authAtom, + openDisableCloudAlertModalAtom, + openSettingModalAtom, +} from '../../../atoms'; import type { AllWorkspace } from '../../../shared'; import { StyledCreateWorkspaceCardPill, @@ -39,6 +47,7 @@ import { StyledSignInCardPillTextCotainer, StyledSignInCardPillTextPrimary, StyledSignInCardPillTextSecondary, + StyledWorkspaceFlavourTitle, } from './styles'; interface WorkspaceModalProps { @@ -56,18 +65,31 @@ interface WorkspaceModalProps { const AccountMenu = () => { const t = useAFFiNEI18N(); + const setOpen = useSetAtom(openSettingModalAtom); return (
-
Unlimted
+ {/*
Unlimted
} data-testid="editor-option-menu-import"> {t['com.affine.workspace.cloud.join']()} - - } data-testid="editor-option-menu-import"> + */} + } + data-testid="editor-option-menu-import" + onClick={useCallback(() => { + setOpen(prev => ({ ...prev, open: true, activeTab: 'account' })); + }, [setOpen])} + > {t['com.affine.workspace.cloud.account.settings']()} - - } data-testid="editor-option-menu-import"> + + } + data-testid="editor-option-menu-import" + onClick={useCallback(() => { + signOut().catch(console.error); + }, [])} + > {t['com.affine.workspace.cloud.account.logout']()}
@@ -89,31 +111,16 @@ const CloudWorkSpaceList = ({ - {t['com.affine.workspace.cloud.sync']()} + {t['com.affine.workspace.cloud']()} - - - } - zIndex={1000} - > - } - type="plain" - /> - - flavour !== WorkspaceFlavour.PUBLIC + ({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD ) as (AffineCloudWorkspace | LocalWorkspace)[] } currentWorkspaceId={currentWorkspaceId} @@ -129,7 +136,6 @@ const CloudWorkSpaceList = ({ [onMoveWorkspace] )} /> - ); @@ -148,11 +154,18 @@ export const WorkspaceListModal = ({ onMoveWorkspace, }: WorkspaceModalProps) => { const t = useAFFiNEI18N(); - const setOpen = useSetAtom(openDisableCloudAlertModalAtom); + const setOpen = useSetAtom(authAtom); + const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom); // TODO: AFFiNE Cloud support - const isLoggedIn = false; + const { data: session, status } = useSession(); + const isLoggedIn = status === 'authenticated' ? true : false; const anchorEl = document.getElementById('current-workspace'); - + const cloudWorkspaces = workspaces.filter( + ({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD + ) as (AffineCloudWorkspace | LocalWorkspace)[]; + const localWorkspaces = workspaces.filter( + ({ flavour }) => flavour === WorkspaceFlavour.LOCAL + ) as (AffineCloudWorkspace | LocalWorkspace)[]; return ( - - - { - if (!runtimeConfig.enableCloud) { - setOpen(true); - } - }} - data-testid="cloud-signin-button" - > - - - - - - - {t['com.affine.workspace.cloud.auth']()} - - - Sync with AFFiNE Cloud - - - - - - - + {!isLoggedIn ? ( + + + { + if (!runtimeConfig.enableCloud) { + setDisableCloudOpen(true); + } else { + setOpen(state => ({ + ...state, + openModal: true, + })); + } + }} + data-testid="cloud-signin-button" + > + + + + + + + {t['com.affine.workspace.cloud.auth']()} + + + {t['com.affine.workspace.cloud.description']()} + + + + + + + + ) : ( + + + {session?.user.email} + + } + zIndex={1000} + > + } + type="plain" + /> + + + + + + )} - {isLoggedIn ? ( - + {isLoggedIn && cloudWorkspaces.length !== 0 ? ( + <> + + + ) : null} - {t['Local Workspace']()} + + {t['com.affine.workspace.local']()} + flavour !== WorkspaceFlavour.PUBLIC - ) as (AffineCloudWorkspace | LocalWorkspace)[] - } + items={localWorkspaces} currentWorkspaceId={currentWorkspaceId} onClick={onClickWorkspace} onSettingClick={onClickWorkspaceSetting} diff --git a/apps/core/src/components/pure/workspace-list-modal/styles.ts b/apps/core/src/components/pure/workspace-list-modal/styles.ts index 96d1f517ff..8af728d9be 100644 --- a/apps/core/src/components/pure/workspace-list-modal/styles.ts +++ b/apps/core/src/components/pure/workspace-list-modal/styles.ts @@ -70,7 +70,6 @@ export const StyledCreateWorkspaceCard = styled('div')(() => { }); export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => { return { - padding: '12px', borderRadius: '10px', display: 'flex', margin: '-8px -4px', @@ -173,6 +172,7 @@ export const StyledModalContent = styled('div')({ flexWrap: 'wrap', flexDirection: 'column', width: '100%', + gap: '4px', }); export const StyledModalFooterContent = styled('div')({ @@ -180,7 +180,7 @@ export const StyledModalFooterContent = styled('div')({ flexWrap: 'wrap', flexDirection: 'column', width: '100%', - padding: '12px', + marginTop: '12px', backgroundColor: 'var(--affine-background-overlay-panel-color)', }); @@ -189,7 +189,6 @@ export const StyledModalHeaderContent = styled('div')({ flexWrap: 'wrap', flexDirection: 'column', width: '100%', - padding: '12px 12px 0px 12px', backgroundColor: 'var(--affine-background-overlay-panel-color)', }); @@ -219,19 +218,27 @@ export const StyledModalHeader = styled('div')(() => { left: 0, top: 0, borderRadius: '24px 24px 0 0', - padding: '12px 14px', + padding: '0px 14px', ...displayFlex('space-between', 'center'), }; }); export const StyledModalBody = styled('div')(() => { return { - padding: '0px 12px', display: 'inline-flex', flexDirection: 'column', alignItems: 'flex-start', - gap: '12px', + gap: '4px', flex: 1, overflowY: 'auto', }; }); + +export const StyledWorkspaceFlavourTitle = styled('div')(() => { + return { + fontSize: '12px', + fontWeight: 600, + color: 'var(--affine-text-secondary-color)', + lineHeight: '20px', + }; +}); diff --git a/apps/core/src/components/workspace-header.tsx b/apps/core/src/components/workspace-header.tsx index 58501b25a2..49b4feeac8 100644 --- a/apps/core/src/components/workspace-header.tsx +++ b/apps/core/src/components/workspace-header.tsx @@ -6,15 +6,16 @@ import { } from '@affine/component/page-list'; import type { Collection } from '@affine/env/filter'; import type { PropertiesMeta } from '@affine/env/filter'; -import type { +import { WorkspaceFlavour, - WorkspaceHeaderProps, + type WorkspaceHeaderProps, } from '@affine/env/workspace'; import { WorkspaceSubPath } from '@affine/env/workspace'; import { useCallback } from 'react'; import { useGetPageInfoById } from '../hooks/use-get-page-info'; import { useWorkspace } from '../hooks/use-workspace'; +import { SharePageModal } from './affine/share-page-modal'; import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title'; import { filterContainerStyle } from './filter-container.css'; import { Header } from './pure/header'; @@ -77,7 +78,6 @@ export function WorkspaceHeader({ const setting = useCollectionManager(currentWorkspaceId); const currentWorkspace = useWorkspace(currentWorkspaceId); - const getPageInfoById = useGetPageInfoById( currentWorkspace.blockSuiteWorkspace ); @@ -117,6 +117,15 @@ export function WorkspaceHeader({ // route in edit page if ('pageId' in currentEntry) { + const isCloudWorkspace = + currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD; + const currentPage = currentWorkspace.blockSuiteWorkspace.getPage( + currentEntry.pageId + ); + const sharePageModal = + isCloudWorkspace && currentPage ? ( + + ) : null; return (
} - right={} + right={ +
+ {sharePageModal} + +
+ } /> ); } diff --git a/apps/core/src/hooks/affine/use-curren-login-status.ts b/apps/core/src/hooks/affine/use-curren-login-status.ts new file mode 100644 index 0000000000..8e835dba8a --- /dev/null +++ b/apps/core/src/hooks/affine/use-curren-login-status.ts @@ -0,0 +1,10 @@ +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { useSession } from 'next-auth/react'; + +export function useCurrenLoginStatus(): + | 'authenticated' + | 'unauthenticated' + | 'loading' { + const session = useSession(); + return session.status; +} diff --git a/apps/core/src/hooks/affine/use-current-user.ts b/apps/core/src/hooks/affine/use-current-user.ts new file mode 100644 index 0000000000..c9a210be8d --- /dev/null +++ b/apps/core/src/hooks/affine/use-current-user.ts @@ -0,0 +1,45 @@ +import type { DefaultSession } from 'next-auth'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { useSession } from 'next-auth/react'; +export type CheckedUser = { + id: string; + name: string; + email: string; + image: string; + hasPassword: boolean; + update: ReturnType['update']; +}; + +// FIXME: Should this namespace be here? +declare module 'next-auth' { + interface Session { + user: { + id: string; + hasPassword: boolean; + } & DefaultSession['user']; + } +} + +/** + * This hook checks if the user is logged in. + * If not, it will throw an error. + */ +export function useCurrentUser(): CheckedUser { + const { data: session, status, update } = useSession(); + // If you are seeing this error, it means that you are not logged in. + // This should be prohibited in the development environment, please re-write your component logic. + if (status === 'unauthenticated') { + throw new Error('session.status should be authenticated'); + } + + const user = session?.user; + + return { + id: user?.id ?? 'REPLACE_ME_DEFAULT_ID', + name: user?.name ?? 'REPLACE_ME_DEFAULT_NAME', + email: user?.email ?? 'REPLACE_ME_DEFAULT_EMAIL', + image: user?.image ?? 'REPLACE_ME_DEFAULT_URL', + hasPassword: user?.hasPassword ?? false, + update, + }; +} diff --git a/apps/core/src/hooks/affine/use-invite-member.ts b/apps/core/src/hooks/affine/use-invite-member.ts new file mode 100644 index 0000000000..43bccc59e3 --- /dev/null +++ b/apps/core/src/hooks/affine/use-invite-member.ts @@ -0,0 +1,30 @@ +import type { Permission } from '@affine/graphql'; +import { inviteByEmailMutation } from '@affine/graphql'; +import { useMutation } from '@affine/workspace/affine/gql'; +import { useCallback } from 'react'; + +import { useMutateCloud } from './use-mutate-cloud'; + +export function useInviteMember(workspaceId: string) { + const { trigger, isMutating } = useMutation({ + mutation: inviteByEmailMutation, + }); + const mutate = useMutateCloud(); + return { + invite: useCallback( + async (email: string, permission: Permission, sendInviteMail = false) => { + const res = await trigger({ + workspaceId, + email, + permission, + sendInviteMail, + }); + await mutate(); + // return is successful + return res?.invite; + }, + [mutate, trigger, workspaceId] + ), + isMutating, + }; +} diff --git a/apps/core/src/hooks/affine/use-is-shared-page.ts b/apps/core/src/hooks/affine/use-is-shared-page.ts new file mode 100644 index 0000000000..5f8cba129c --- /dev/null +++ b/apps/core/src/hooks/affine/use-is-shared-page.ts @@ -0,0 +1,57 @@ +import { + getWorkspaceSharedPagesQuery, + revokePageMutation, + sharePageMutation, +} from '@affine/graphql'; +import { useMutation, useQuery } from '@affine/workspace/affine/gql'; +import { useCallback, useMemo } from 'react'; + +export function useIsSharedPage( + workspaceId: string, + pageId: string +): [isSharedPage: boolean, setSharedPage: (enable: boolean) => void] { + const { data, mutate } = useQuery({ + query: getWorkspaceSharedPagesQuery, + variables: { + workspaceId, + }, + }); + const { trigger: enableSharePage } = useMutation({ + mutation: sharePageMutation, + }); + const { trigger: disableSharePage } = useMutation({ + mutation: revokePageMutation, + }); + return [ + useMemo( + () => data.workspace.sharedPages.some(id => id === pageId), + [data.workspace.sharedPages, pageId] + ), + useCallback( + (enable: boolean) => { + // todo: push notification + if (enable) { + enableSharePage({ + workspaceId, + pageId, + }) + .then(() => { + return mutate(); + }) + .catch(console.error); + } else { + disableSharePage({ + workspaceId, + pageId, + }) + .then(() => { + return mutate(); + }) + .catch(console.error); + } + mutate().catch(console.error); + }, + [disableSharePage, enableSharePage, mutate, pageId, workspaceId] + ), + ]; +} diff --git a/apps/core/src/hooks/affine/use-is-workspace-owner.ts b/apps/core/src/hooks/affine/use-is-workspace-owner.ts new file mode 100644 index 0000000000..c85f990561 --- /dev/null +++ b/apps/core/src/hooks/affine/use-is-workspace-owner.ts @@ -0,0 +1,13 @@ +import { getIsOwnerQuery } from '@affine/graphql'; +import { useQuery } from '@affine/workspace/affine/gql'; + +export function useIsWorkspaceOwner(workspaceId: string) { + const { data } = useQuery({ + query: getIsOwnerQuery, + variables: { + workspaceId, + }, + }); + + return data.isOwner; +} diff --git a/apps/core/src/hooks/affine/use-leave-workspace.ts b/apps/core/src/hooks/affine/use-leave-workspace.ts new file mode 100644 index 0000000000..eb208896da --- /dev/null +++ b/apps/core/src/hooks/affine/use-leave-workspace.ts @@ -0,0 +1,23 @@ +import { leaveWorkspaceMutation } from '@affine/graphql'; +import { useMutation } from '@affine/workspace/affine/gql'; +import { useCallback } from 'react'; + +import { useAppHelper } from '../use-workspaces'; + +export function useLeaveWorkspace() { + const { deleteWorkspaceMeta } = useAppHelper(); + + const { trigger: leaveWorkspace } = useMutation({ + mutation: leaveWorkspaceMutation, + }); + + return useCallback( + async (workspaceId: string) => { + deleteWorkspaceMeta(workspaceId); + await leaveWorkspace({ + workspaceId, + }); + }, + [deleteWorkspaceMeta, leaveWorkspace] + ); +} diff --git a/apps/core/src/hooks/affine/use-members.ts b/apps/core/src/hooks/affine/use-members.ts new file mode 100644 index 0000000000..05ca5d6192 --- /dev/null +++ b/apps/core/src/hooks/affine/use-members.ts @@ -0,0 +1,19 @@ +import { + type GetMembersByWorkspaceIdQuery, + getMembersByWorkspaceIdQuery, +} from '@affine/graphql'; +import { useQuery } from '@affine/workspace/affine/gql'; + +export type Member = Omit< + GetMembersByWorkspaceIdQuery['workspace']['members'][number], + '__typename' +>; +export function useMembers(workspaceId: string) { + const { data } = useQuery({ + query: getMembersByWorkspaceIdQuery, + variables: { + workspaceId, + }, + }); + return data.workspace.members; +} diff --git a/apps/core/src/hooks/affine/use-mutate-cloud.ts b/apps/core/src/hooks/affine/use-mutate-cloud.ts new file mode 100644 index 0000000000..a8eb465412 --- /dev/null +++ b/apps/core/src/hooks/affine/use-mutate-cloud.ts @@ -0,0 +1,14 @@ +import { useCallback } from 'react'; +import { useSWRConfig } from 'swr'; + +export function useMutateCloud() { + const { mutate } = useSWRConfig(); + return useCallback(async () => { + return mutate(key => { + if (Array.isArray(key)) { + return key[0] === 'cloud'; + } + return false; + }); + }, [mutate]); +} diff --git a/apps/core/src/hooks/affine/use-revoke-member-permission.ts b/apps/core/src/hooks/affine/use-revoke-member-permission.ts new file mode 100644 index 0000000000..379c90713b --- /dev/null +++ b/apps/core/src/hooks/affine/use-revoke-member-permission.ts @@ -0,0 +1,23 @@ +import { revokeMemberPermissionMutation } from '@affine/graphql'; +import { useMutation } from '@affine/workspace/affine/gql'; +import { useCallback } from 'react'; + +import { useMutateCloud } from './use-mutate-cloud'; + +export function useRevokeMemberPermission(workspaceId: string) { + const mutate = useMutateCloud(); + const { trigger } = useMutation({ + mutation: revokeMemberPermissionMutation, + }); + + return useCallback( + async (userId: string) => { + await trigger({ + workspaceId, + userId, + }); + await mutate(); + }, + [mutate, trigger, workspaceId] + ); +} diff --git a/apps/core/src/hooks/affine/use-share-link.ts b/apps/core/src/hooks/affine/use-share-link.ts new file mode 100644 index 0000000000..58e4fb37b9 --- /dev/null +++ b/apps/core/src/hooks/affine/use-share-link.ts @@ -0,0 +1,15 @@ +'use client'; +import { useMemo } from 'react'; + +export function useShareLink(workspaceId: string): string { + return useMemo(() => { + if (environment.isServer) { + throw new Error('useShareLink is not available on server side'); + } + if (environment.isDesktop) { + return '???'; + } else { + return origin + '/share/' + workspaceId; + } + }, [workspaceId]); +} diff --git a/apps/core/src/hooks/affine/use-toggle-cloud-public.ts b/apps/core/src/hooks/affine/use-toggle-cloud-public.ts new file mode 100644 index 0000000000..57c88d3669 --- /dev/null +++ b/apps/core/src/hooks/affine/use-toggle-cloud-public.ts @@ -0,0 +1,22 @@ +import { setWorkspacePublicByIdMutation } from '@affine/graphql'; +import { useMutation } from '@affine/workspace/affine/gql'; +import { useCallback } from 'react'; + +import { useMutateCloud } from './use-mutate-cloud'; + +export function useToggleCloudPublic(workspaceId: string) { + const mutate = useMutateCloud(); + const { trigger } = useMutation({ + mutation: setWorkspacePublicByIdMutation, + }); + return useCallback( + async (isPublic: boolean) => { + await trigger({ + id: workspaceId, + public: isPublic, + }); + await mutate(); + }, + [mutate, trigger, workspaceId] + ); +} diff --git a/apps/core/src/hooks/root/use-on-transform-workspace.ts b/apps/core/src/hooks/root/use-on-transform-workspace.ts index 88ea3e69aa..5ec1a5bce0 100644 --- a/apps/core/src/hooks/root/use-on-transform-workspace.ts +++ b/apps/core/src/hooks/root/use-on-transform-workspace.ts @@ -1,34 +1,62 @@ import type { WorkspaceRegistry } from '@affine/env/workspace'; import type { WorkspaceFlavour } from '@affine/env/workspace'; +import { WorkspaceSubPath } from '@affine/env/workspace'; +import { + rootWorkspacesMetadataAtom, + workspaceAdaptersAtom, +} from '@affine/workspace/atom'; import { currentPageIdAtom } from '@toeverything/infra/atom'; -import { useSetAtom } from 'jotai'; +import { WorkspaceVersion } from '@toeverything/infra/blocksuite'; +import { useAtomValue, useSetAtom } from 'jotai'; import { useCallback } from 'react'; -import { useTransformWorkspace } from '../use-transform-workspace'; +import { openSettingModalAtom } from '../../atoms'; +import { useNavigateHelper } from '../use-navigate-helper'; export function useOnTransformWorkspace() { - const transformWorkspace = useTransformWorkspace(); - const setWorkspaceId = useSetAtom(currentPageIdAtom); + const setSettingModal = useSetAtom(openSettingModalAtom); + const WorkspaceAdapters = useAtomValue(workspaceAdaptersAtom); + const setMetadata = useSetAtom(rootWorkspacesMetadataAtom); + const { openPage } = useNavigateHelper(); + const currentPageId = useAtomValue(currentPageIdAtom); return useCallback( async ( from: From, to: To, workspace: WorkspaceRegistry[From] ): Promise => { - const workspaceId = await transformWorkspace(from, to, workspace); + // create first, then delete, in case of failure + const newId = await WorkspaceAdapters[to].CRUD.create( + workspace.blockSuiteWorkspace + ); + await WorkspaceAdapters[from].CRUD.delete(workspace.blockSuiteWorkspace); + setMetadata(workspaces => { + const idx = workspaces.findIndex(ws => ws.id === workspace.id); + workspaces.splice(idx, 1, { + id: newId, + flavour: to, + version: WorkspaceVersion.SubDoc, + }); + return [...workspaces]; + }, newId); + // fixme(himself65): setting modal could still open and open the non-exist workspace + setSettingModal(settings => ({ + ...settings, + open: false, + })); window.dispatchEvent( new CustomEvent('affine-workspace:transform', { detail: { from, to, oldId: workspace.id, - newId: workspaceId, + newId: newId, }, }) ); - setWorkspaceId(workspaceId); + openPage(newId, currentPageId ?? WorkspaceSubPath.ALL); }, - [setWorkspaceId, transformWorkspace] + [WorkspaceAdapters, setMetadata, setSettingModal, openPage, currentPageId] ); } diff --git a/apps/core/src/hooks/use-navigate-helper.ts b/apps/core/src/hooks/use-navigate-helper.ts index 1a85b0bbb6..d5713eb697 100644 --- a/apps/core/src/hooks/use-navigate-helper.ts +++ b/apps/core/src/hooks/use-navigate-helper.ts @@ -1,7 +1,11 @@ import type { WorkspaceSubPath } from '@affine/env/workspace'; import { useCallback } from 'react'; -// eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { useLocation, useNavigate } from 'react-router-dom'; +import { + type NavigateOptions, + useLocation, + // eslint-disable-next-line @typescript-eslint/no-restricted-imports + useNavigate, +} from 'react-router-dom'; export enum RouteLogic { REPLACE = 'replace', @@ -78,6 +82,26 @@ export function useNavigateHelper() { }, [navigate] ); + const jumpToExpired = useCallback( + (logic: RouteLogic = RouteLogic.PUSH) => { + return navigate('/expired', { + replace: logic === RouteLogic.REPLACE, + }); + }, + [navigate] + ); + const jumpToSignIn = useCallback( + ( + logic: RouteLogic = RouteLogic.PUSH, + otherOptions?: Omit + ) => { + return navigate('/signIn', { + replace: logic === RouteLogic.REPLACE, + ...otherOptions, + }); + }, + [navigate] + ); return { jumpToPage, @@ -86,5 +110,7 @@ export function useNavigateHelper() { jumpToIndex, jumpTo404, openPage, + jumpToExpired, + jumpToSignIn, }; } diff --git a/apps/core/src/hooks/use-transform-workspace.ts b/apps/core/src/hooks/use-transform-workspace.ts deleted file mode 100644 index dcb996ddb4..0000000000 --- a/apps/core/src/hooks/use-transform-workspace.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { WorkspaceFlavour } from '@affine/env/workspace'; -import type { WorkspaceRegistry } from '@affine/env/workspace'; -import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; -import { WorkspaceVersion } from '@toeverything/infra/blocksuite'; -import { useSetAtom } from 'jotai'; -import { useCallback } from 'react'; - -import { WorkspaceAdapters } from '../adapters/workspace'; - -/** - * Transform workspace from one flavor to another - * - * The logic here is to delete the old workspace and create a new one. - */ -export function useTransformWorkspace() { - const set = useSetAtom(rootWorkspacesMetadataAtom); - return useCallback( - async ( - from: From, - to: To, - workspace: WorkspaceRegistry[From] - ): Promise => { - // create first, then delete, in case of failure - const newId = await WorkspaceAdapters[to].CRUD.create( - workspace.blockSuiteWorkspace - ); - await WorkspaceAdapters[from].CRUD.delete(workspace as any); - set(workspaces => { - const idx = workspaces.findIndex(ws => ws.id === workspace.id); - workspaces.splice(idx, 1, { - id: newId, - flavour: to, - version: WorkspaceVersion.DatabaseV3, - }); - return [...workspaces]; - }); - return newId; - }, - [set] - ); -} diff --git a/apps/core/src/hooks/use-workspace.ts b/apps/core/src/hooks/use-workspace.ts index 5ea3e26601..2d0eeeb637 100644 --- a/apps/core/src/hooks/use-workspace.ts +++ b/apps/core/src/hooks/use-workspace.ts @@ -1,3 +1,4 @@ +import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom'; import { assertExists } from '@blocksuite/global/utils'; import type { Workspace } from '@blocksuite/store'; @@ -5,8 +6,6 @@ import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/r import type { Atom } from 'jotai'; import { atom, useAtomValue } from 'jotai'; -import type { AffineOfficialWorkspace } from '../shared'; - const workspaceWeakMap = new WeakMap< Workspace, Atom> @@ -18,7 +17,7 @@ export function useWorkspace(workspaceId: string): AffineOfficialWorkspace { const baseAtom = atom(async get => { const metadata = await get(rootWorkspacesMetadataAtom); const flavour = metadata.find(({ id }) => id === workspaceId)?.flavour; - assertExists(flavour); + assertExists(flavour, 'workspace flavour not found'); return { id: workspaceId, flavour, diff --git a/apps/core/src/hooks/use-workspaces.ts b/apps/core/src/hooks/use-workspaces.ts index bb122720d1..c4e02890ee 100644 --- a/apps/core/src/hooks/use-workspaces.ts +++ b/apps/core/src/hooks/use-workspaces.ts @@ -43,6 +43,21 @@ export function useAppHelper() { }, [set] ), + addCloudWorkspace: useCallback( + (workspaceId: string) => { + getOrCreateWorkspace(workspaceId, WorkspaceFlavour.AFFINE_CLOUD); + set(workspaces => [ + ...workspaces, + { + id: workspaceId, + flavour: WorkspaceFlavour.AFFINE_CLOUD, + version: WorkspaceVersion.DatabaseV3, + }, + ]); + logger.debug('imported cloud workspace', workspaceId); + }, + [set] + ), createLocalWorkspace: useCallback( async (name: string): Promise => { const blockSuiteWorkspace = getOrCreateWorkspace( @@ -97,5 +112,11 @@ export function useAppHelper() { }, [jotaiWorkspaces, set] ), + deleteWorkspaceMeta: useCallback( + (workspaceId: string) => { + set(workspaces => workspaces.filter(ws => ws.id !== workspaceId)); + }, + [set] + ), }; } diff --git a/apps/core/src/layouts/workspace-layout.tsx b/apps/core/src/layouts/workspace-layout.tsx index b80f5cc411..9b875febca 100644 --- a/apps/core/src/layouts/workspace-layout.tsx +++ b/apps/core/src/layouts/workspace-layout.tsx @@ -33,16 +33,16 @@ import { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/reac import { currentWorkspaceIdAtom } from '@toeverything/infra/atom'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import type { PropsWithChildren, ReactElement } from 'react'; -import { lazy, Suspense, useCallback, useMemo } from 'react'; +import { lazy, Suspense, useCallback } from 'react'; import { useLocation, useParams } from 'react-router-dom'; -import { WorkspaceAdapters } from '../adapters/workspace'; import { openQuickSearchModalAtom, openSettingModalAtom, openWorkspacesModalAtom, } from '../atoms'; import { useAppSetting } from '../atoms/settings'; +import { AdapterProviderWrapper } from '../components/adapter-worksapce-wrapper'; import { AppContainer } from '../components/affine/app-container'; import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils'; import type { IslandItemNames } from '../components/pure/help-island'; @@ -68,10 +68,6 @@ const QuickSearchModal = lazy(() => })) ); -function DefaultProvider({ children }: PropsWithChildren) { - return <>{children}; -} - export const QuickSearch = () => { const [currentWorkspace] = useCurrentWorkspace(); const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom( @@ -117,32 +113,19 @@ export const CurrentWorkspaceContext = ({ export const WorkspaceLayout = function WorkspacesSuspense({ children, }: PropsWithChildren) { - const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom); - const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom); - const meta = useMemo( - () => jotaiWorkspaces.find(x => x.id === currentWorkspaceId), - [currentWorkspaceId, jotaiWorkspaces] - ); - - const Provider = - (meta && WorkspaceAdapters[meta.flavour].UI.Provider) ?? DefaultProvider; - return ( - <> - {/* load all workspaces is costly, do not block the whole UI */} - - - - {/* fixme(himself65): don't re-render whole modals */} - - - + - + {/* load all workspaces is costly, do not block the whole UI */} + + + + + }> {children} - + - + ); }; diff --git a/apps/core/src/pages/auth.tsx b/apps/core/src/pages/auth.tsx new file mode 100644 index 0000000000..f715a8ccb8 --- /dev/null +++ b/apps/core/src/pages/auth.tsx @@ -0,0 +1,133 @@ +import { + ChangeEmailPage, + ChangePasswordPage, + SetPasswordPage, + SignInSuccessPage, + SignUpPage, +} from '@affine/component/auth-components'; +import { isDesktop } from '@affine/env/constant'; +import { changeEmailMutation, changePasswordMutation } from '@affine/graphql'; +import { useMutation } from '@affine/workspace/affine/gql'; +import type { ReactElement } from 'react'; +import { useCallback } from 'react'; +import { type LoaderFunction, redirect, useParams } from 'react-router-dom'; +import { z } from 'zod'; + +import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status'; +import { useCurrentUser } from '../hooks/affine/use-current-user'; +import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper'; + +const authTypeSchema = z.enum([ + 'setPassword', + 'signIn', + 'changePassword', + 'signUp', + 'changeEmail', +]); + +export const AuthPage = (): ReactElement | null => { + const user = useCurrentUser(); + const { authType } = useParams(); + const { trigger: changePassword } = useMutation({ + mutation: changePasswordMutation, + }); + + const { trigger: changeEmail } = useMutation({ + mutation: changeEmailMutation, + }); + const { jumpToIndex } = useNavigateHelper(); + + const onChangeEmail = useCallback( + async (email: string) => { + const res = await changeEmail({ + id: user.id, + newEmail: email, + }); + return !!res?.changeEmail; + }, + [changeEmail, user.id] + ); + + const onSetPassword = useCallback( + (password: string) => { + changePassword({ + id: user.id, + newPassword: password, + }).catch(console.error); + }, + [changePassword, user.id] + ); + const onOpenAffine = useCallback(() => { + if (isDesktop) { + window.apis.ui.handleFinishLogin(); + } else { + jumpToIndex(RouteLogic.REPLACE); + } + }, [jumpToIndex]); + + switch (authType) { + case 'signUp': { + return ( + + ); + } + case 'signIn': { + return ; + } + case 'changePassword': { + return ( + + ); + } + case 'setPassword': { + return ( + + ); + } + case 'changeEmail': { + return ( + + ); + } + } + return null; +}; + +export const loader: LoaderFunction = async args => { + if (!args.params.authType) { + return redirect('/404'); + } + if (!authTypeSchema.safeParse(args.params.authType).success) { + return redirect('/404'); + } + return null; +}; +export const Component = () => { + const loginStatus = useCurrenLoginStatus(); + const { jumpToExpired } = useNavigateHelper(); + + if (loginStatus === 'unauthenticated') { + jumpToExpired(RouteLogic.REPLACE); + } + + if (loginStatus === 'authenticated') { + return ; + } + return null; +}; diff --git a/apps/core/src/pages/expired.tsx b/apps/core/src/pages/expired.tsx new file mode 100644 index 0000000000..5d4cd67ae8 --- /dev/null +++ b/apps/core/src/pages/expired.tsx @@ -0,0 +1,25 @@ +import { AuthPageContainer } from '@affine/component/auth-components'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button } from '@toeverything/components/button'; +import { useCallback } from 'react'; + +import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper'; + +export const Component = () => { + const t = useAFFiNEI18N(); + const { jumpToIndex } = useNavigateHelper(); + const onOpenAffine = useCallback(() => { + jumpToIndex(RouteLogic.REPLACE); + }, [jumpToIndex]); + + return ( + + + + ); +}; diff --git a/apps/core/src/pages/invite.tsx b/apps/core/src/pages/invite.tsx new file mode 100644 index 0000000000..dc51c802c5 --- /dev/null +++ b/apps/core/src/pages/invite.tsx @@ -0,0 +1,103 @@ +import { AcceptInvitePage } from '@affine/component/member-components'; +import { WorkspaceSubPath } from '@affine/env/workspace'; +import { + acceptInviteByInviteIdMutation, + type GetInviteInfoQuery, + getInviteInfoQuery, +} from '@affine/graphql'; +import { fetcher } from '@affine/workspace/affine/gql'; +import { useAtom } from 'jotai'; +import { useCallback, useEffect } from 'react'; +import { type LoaderFunction, redirect, useLoaderData } from 'react-router-dom'; + +import { authAtom } from '../atoms'; +import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status'; +import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper'; +import { useAppHelper } from '../hooks/use-workspaces'; + +export const loader: LoaderFunction = async args => { + const inviteId = args.params.inviteId || ''; + const res = await fetcher({ + query: getInviteInfoQuery, + variables: { + inviteId, + }, + }).catch(console.error); + + // If the inviteId is invalid, redirect to 404 page + if (!res || !res?.getInviteInfo) { + return redirect('/404'); + } + + // No mater sign in or not, we need to accept the invite + await fetcher({ + query: acceptInviteByInviteIdMutation, + variables: { + workspaceId: res.getInviteInfo.workspace.id, + inviteId, + }, + }).catch(console.error); + + return { + inviteId, + inviteInfo: res.getInviteInfo, + }; +}; + +export const Component = () => { + const loginStatus = useCurrenLoginStatus(); + const { jumpToSignIn } = useNavigateHelper(); + const { addCloudWorkspace } = useAppHelper(); + const { jumpToSubPath } = useNavigateHelper(); + + const [, setAuthAtom] = useAtom(authAtom); + const { inviteInfo } = useLoaderData() as { + inviteId: string; + inviteInfo: GetInviteInfoQuery['getInviteInfo']; + }; + + const loadWorkspaceAfterSignIn = useCallback(() => { + addCloudWorkspace(inviteInfo.workspace.id); + }, [addCloudWorkspace, inviteInfo.workspace.id]); + + const openWorkspace = useCallback(() => { + addCloudWorkspace(inviteInfo.workspace.id); + jumpToSubPath( + inviteInfo.workspace.id, + WorkspaceSubPath.ALL, + RouteLogic.REPLACE + ); + }, [addCloudWorkspace, inviteInfo.workspace.id, jumpToSubPath]); + + useEffect(() => { + if (loginStatus === 'unauthenticated') { + // We can not pass function to navigate state, so we need to save it in atom + setAuthAtom(prev => ({ + ...prev, + onceSignedIn: loadWorkspaceAfterSignIn, + })); + jumpToSignIn(RouteLogic.REPLACE, { + state: { + callbackURL: `/workspace/${inviteInfo.workspace.id}/all`, + }, + }); + } + }, [ + inviteInfo.workspace.id, + jumpToSignIn, + loadWorkspaceAfterSignIn, + loginStatus, + setAuthAtom, + ]); + + if (loginStatus === 'authenticated') { + return ( + + ); + } + + return null; +}; diff --git a/apps/core/src/pages/share/detail-page.tsx b/apps/core/src/pages/share/detail-page.tsx new file mode 100644 index 0000000000..54cb5a5d1e --- /dev/null +++ b/apps/core/src/pages/share/detail-page.tsx @@ -0,0 +1,73 @@ +import { MainContainer } from '@affine/component/workspace'; +import { DebugLogger } from '@affine/debug'; +import { WorkspaceFlavour } from '@affine/env/workspace'; +import { getOrCreateWorkspace } from '@affine/workspace/manager'; +import { downloadBinaryFromCloud } from '@affine/workspace/providers'; +import { assertExists } from '@blocksuite/global/utils'; +import type { Page } from '@blocksuite/store'; +import { noop } from 'foxact/noop'; +import type { ReactElement } from 'react'; +import { useCallback } from 'react'; +import type { LoaderFunction } from 'react-router-dom'; +import { redirect, useLoaderData } from 'react-router-dom'; +import { applyUpdate } from 'yjs'; + +import { PageDetailEditor } from '../../adapters/shared'; +import { AppContainer } from '../../components/affine/app-container'; + +function assertArrayBuffer(value: unknown): asserts value is ArrayBuffer { + if (!(value instanceof ArrayBuffer)) { + throw new Error('value is not ArrayBuffer'); + } +} + +const logger = new DebugLogger('public:share-page'); + +export const loader: LoaderFunction = async ({ params }) => { + const workspaceId = params?.workspaceId; + const pageId = params?.pageId; + if (!workspaceId || !pageId) { + return redirect('/404'); + } + const workspace = getOrCreateWorkspace( + workspaceId, + WorkspaceFlavour.AFFINE_PUBLIC + ); + // download root workspace + { + const buffer = await downloadBinaryFromCloud(workspaceId, workspaceId); + assertArrayBuffer(buffer); + applyUpdate(workspace.doc, new Uint8Array(buffer)); + } + const page = workspace.getPage(pageId); + assertExists(page, 'cannot find page'); + // download page + { + const buffer = await downloadBinaryFromCloud( + workspaceId, + page.spaceDoc.guid + ); + assertArrayBuffer(buffer); + applyUpdate(page.spaceDoc, new Uint8Array(buffer)); + } + logger.info('workspace', workspace); + workspace.awarenessStore.setReadonly(page, true); + return page; +}; + +export const Component = (): ReactElement => { + const page = useLoaderData() as Page; + return ( + + + noop, [])} + onLoad={useCallback(() => noop, [])} + /> + + + ); +}; diff --git a/apps/core/src/pages/sign-in.tsx b/apps/core/src/pages/sign-in.tsx new file mode 100644 index 0000000000..6da782ba93 --- /dev/null +++ b/apps/core/src/pages/sign-in.tsx @@ -0,0 +1,75 @@ +import { SignInPageContainer } from '@affine/component/auth-components'; +import { useAtom } from 'jotai'; +import { useCallback, useEffect } from 'react'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { useLocation, useNavigate } from 'react-router-dom'; + +import { authAtom } from '../atoms'; +import { AuthPanel } from '../components/affine/auth'; +import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status'; + +interface LocationState { + state: { + callbackURL?: string; + }; +} +export const Component = () => { + const [ + { state, email = '', emailType = 'changePassword', onceSignedIn }, + setAuthAtom, + ] = useAtom(authAtom); + const loginStatus = useCurrenLoginStatus(); + const location = useLocation() as LocationState; + const navigate = useNavigate(); + + useEffect(() => { + const afterSignedIn = async () => { + if (loginStatus === 'authenticated') { + if (onceSignedIn) { + await onceSignedIn(); + setAuthAtom(prev => ({ ...prev, onceSignedIn: undefined })); + } + if (location.state.callbackURL) { + navigate(location.state.callbackURL, { + replace: true, + }); + } + } + }; + afterSignedIn(); + }, [ + location.state.callbackURL, + loginStatus, + navigate, + onceSignedIn, + setAuthAtom, + ]); + + return ( + + { + setAuthAtom(prev => ({ ...prev, emailType })); + }, + [setAuthAtom] + )} + setAuthState={useCallback( + state => { + setAuthAtom(prev => ({ ...prev, state })); + }, + [setAuthAtom] + )} + setAuthEmail={useCallback( + email => { + setAuthAtom(prev => ({ ...prev, email })); + }, + [setAuthAtom] + )} + /> + + ); +}; diff --git a/apps/core/src/providers/modal-provider.tsx b/apps/core/src/providers/modal-provider.tsx index 6c9dda5663..5af4fbc33f 100644 --- a/apps/core/src/providers/modal-provider.tsx +++ b/apps/core/src/providers/modal-provider.tsx @@ -12,6 +12,7 @@ import { lazy, Suspense, useCallback, useTransition } from 'react'; import type { SettingAtom } from '../atoms'; import { + authAtom, openCreateWorkspaceModalAtom, openDisableCloudAlertModalAtom, openSettingModalAtom, @@ -25,6 +26,11 @@ const SettingModal = lazy(() => default: module.SettingModal, })) ); +const Auth = lazy(() => + import('../components/affine/auth').then(module => ({ + default: module.AuthModal, + })) +); const WorkspaceListModal = lazy(() => import('../components/pure/workspace-list-modal').then(module => ({ @@ -84,6 +90,45 @@ export const Setting = () => { ); }; +export const AuthModal = (): ReactElement => { + const [ + { openModal, state, email = '', emailType = 'changePassword' }, + setAuthAtom, + ] = useAtom(authAtom); + return ( + { + setAuthAtom(prev => ({ ...prev, emailType })); + }, + [setAuthAtom] + )} + setOpen={useCallback( + open => { + setAuthAtom(prev => ({ ...prev, openModal: open })); + }, + [setAuthAtom] + )} + setAuthState={useCallback( + state => { + setAuthAtom(prev => ({ ...prev, state })); + }, + [setAuthAtom] + )} + setAuthEmail={useCallback( + email => { + setAuthAtom(prev => ({ ...prev, email })); + }, + [setAuthAtom] + )} + /> + ); +}; + export function CurrentWorkspaceModals() { const [currentWorkspace] = useCurrentWorkspace(); const [openDisableCloudAlertModal, setOpenDisableCloudAlertModal] = useAtom( @@ -203,22 +248,21 @@ export const AllWorkspaceModals = (): ReactElement => { }, [setOpenCreateWorkspaceModal])} onCreate={useCallback( id => { - startTransition(() => { - setOpenCreateWorkspaceModal(false); - setOpenWorkspacesModal(false); - setCurrentWorkspaceId(id); + setOpenCreateWorkspaceModal(false); + setOpenWorkspacesModal(false); + // if jumping immediately, the page may stuck in loading state + // not sure why yet .. here is a workaround + setTimeout(() => { jumpToSubPath(id, WorkspaceSubPath.ALL); }); }, - [ - jumpToSubPath, - setCurrentWorkspaceId, - setOpenCreateWorkspaceModal, - setOpenWorkspacesModal, - ] + [jumpToSubPath, setOpenCreateWorkspaceModal, setOpenWorkspacesModal] )} /> + + + ); }; diff --git a/apps/core/src/router.ts b/apps/core/src/router.ts index 672bcbf5e2..68dcb5ba37 100644 --- a/apps/core/src/router.ts +++ b/apps/core/src/router.ts @@ -24,10 +24,30 @@ export const routes = [ }, ], }, + { + path: '/share/:workspaceId/:pageId', + lazy: () => import('./pages/share/detail-page'), + }, { path: '/404', lazy: () => import('./pages/404'), }, + { + path: '/auth/:authType', + lazy: () => import('./pages/auth'), + }, + { + path: '/expired', + lazy: () => import('./pages/expired'), + }, + { + path: '/invite/:inviteId', + lazy: () => import('./pages/invite'), + }, + { + path: '/signIn', + lazy: () => import('./pages/sign-in'), + }, { path: '*', lazy: () => import('./pages/404'), diff --git a/apps/core/src/shared/index.ts b/apps/core/src/shared/index.ts index 31ef035ba1..7e94890b6c 100644 --- a/apps/core/src/shared/index.ts +++ b/apps/core/src/shared/index.ts @@ -1,18 +1,8 @@ -import type { - AffineCloudWorkspace, - LocalWorkspace, -} from '@affine/env/workspace'; -import type { AffinePublicWorkspace } from '@affine/env/workspace'; import type { WorkspaceRegistry } from '@affine/env/workspace'; import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; export { BlockSuiteWorkspace }; -export type AffineOfficialWorkspace = - | AffineCloudWorkspace - | LocalWorkspace - | AffinePublicWorkspace; - export type AllWorkspace = WorkspaceRegistry[keyof WorkspaceRegistry]; export enum WorkspaceSubPath { diff --git a/apps/core/src/utils/email-regex.ts b/apps/core/src/utils/email-regex.ts new file mode 100644 index 0000000000..466a8ec8ea --- /dev/null +++ b/apps/core/src/utils/email-regex.ts @@ -0,0 +1,2 @@ +export const emailRegex = + /^(?:(?:[^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(?:(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|((?:[a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; diff --git a/apps/core/src/utils/toast.ts b/apps/core/src/utils/toast.ts index 7f31414e03..657c391901 100644 --- a/apps/core/src/utils/toast.ts +++ b/apps/core/src/utils/toast.ts @@ -2,11 +2,14 @@ import type { ToastOptions } from '@affine/component'; import { toast as basicToast } from '@affine/component'; export const toast = (message: string, options?: ToastOptions) => { + const modal = document.querySelector( + '[role=presentation]' + ) as HTMLElement | null; const mainContainer = document.querySelector( '.main-container' - ) as HTMLElement; + ) as HTMLElement | null; return basicToast(message, { - portal: mainContainer || document.body, + portal: modal || mainContainer || document.body, ...options, }); }; diff --git a/apps/electron/e2e/basic.spec.ts b/apps/electron/e2e/basic.spec.ts index a0ef8d9869..b68a56ae79 100644 --- a/apps/electron/e2e/basic.spec.ts +++ b/apps/electron/e2e/basic.spec.ts @@ -125,18 +125,6 @@ test('app theme', async ({ page, electronApp }) => { } }); -test('affine cloud disabled', async ({ page }) => { - await page.getByTestId('new-page-button').click({ - delay: 100, - }); - await page.waitForSelector('v-line'); - await page.getByTestId('current-workspace').click(); - await page.getByTestId('cloud-signin-button').click(); - await page.getByTestId('disable-affine-cloud-modal').waitFor({ - state: 'visible', - }); -}); - test('affine onboarding button', async ({ page }) => { await page.getByTestId('help-island').click(); await page.getByTestId('easy-guide').click(); @@ -181,9 +169,7 @@ test('delete workspace', async ({ page }) => { delay: 100, }); await page.waitForTimeout(1000); - await page.getByTestId('current-workspace').click(); - await page.getByTestId('workspace-card').nth(1).hover(); - await page.getByTestId('workspace-card-setting-button').nth(1).click(); + await clickSideBarSettingButton(page); await page.getByTestId('current-workspace-label').click(); expect(await page.getByTestId('workspace-name-input').inputValue()).toBe( 'Delete Me' diff --git a/apps/electron/e2e/fixture.ts b/apps/electron/e2e/fixture.ts index 1f8e9721a9..243c9ad0b3 100644 --- a/apps/electron/e2e/fixture.ts +++ b/apps/electron/e2e/fixture.ts @@ -8,8 +8,9 @@ import { test as base, testResultDir, } from '@affine-test/kit/playwright'; +import type { Page } from '@playwright/test'; import fs from 'fs-extra'; -import type { ElectronApplication, Page } from 'playwright'; +import type { ElectronApplication } from 'playwright'; import { _electron as electron } from 'playwright'; function generateUUID() { @@ -19,7 +20,6 @@ function generateUUID() { type RoutePath = 'setting'; export const test = base.extend<{ - page: Page; electronApp: ElectronApplication; appInfo: { appPath: string; @@ -60,7 +60,7 @@ export const test = base.extend<{ } ); } - await use(page); + await use(page as Page); if (enableCoverage) { await page.evaluate(() => // @ts-expect-error diff --git a/apps/electron/forge.config.js b/apps/electron/forge.config.js index ed2c9b38c7..918a8ac34a 100644 --- a/apps/electron/forge.config.js +++ b/apps/electron/forge.config.js @@ -114,6 +114,12 @@ module.exports = { // We need the following line for updater extraResource: ['./resources/app-update.yml'], ignore: ['e2e', 'tests'], + protocols: [ + { + name: productName, + schemes: [productName.toLowerCase()], + }, + ], }, makers, hooks: { diff --git a/apps/electron/src/helper/index.ts b/apps/electron/src/helper/index.ts index 9af19b2a46..bdc862659d 100644 --- a/apps/electron/src/helper/index.ts +++ b/apps/electron/src/helper/index.ts @@ -56,14 +56,14 @@ function setupRendererConnection(rendererPort: Electron.MessagePortMain) { for (const [namespace, namespaceEvents] of Object.entries(events)) { for (const [key, eventRegister] of Object.entries(namespaceEvents)) { - const subscription = eventRegister((...args: any[]) => { + const unsub = eventRegister((...args: any[]) => { const chan = `${namespace}:${key}`; rpc.postEvent(chan, ...args).catch(err => { console.error(err); }); }); process.on('exit', () => { - subscription(); + unsub(); }); } } diff --git a/apps/electron/src/main/__tests__/integration.spec.ts b/apps/electron/src/main/__tests__/integration.spec.ts index 230168d1ea..4c6cf23a46 100644 --- a/apps/electron/src/main/__tests__/integration.spec.ts +++ b/apps/electron/src/main/__tests__/integration.spec.ts @@ -95,6 +95,7 @@ const electronModule = { return [browserWindow]; }, }, + utilityProcess: {}, nativeTheme: nativeTheme, ipcMain, shell: {} as Partial, diff --git a/apps/electron/src/main/config.ts b/apps/electron/src/main/config.ts new file mode 100644 index 0000000000..9381ee5c99 --- /dev/null +++ b/apps/electron/src/main/config.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +export const ReleaseTypeSchema = z.enum([ + 'stable', + 'beta', + 'canary', + 'internal', +]); + +export const envBuildType = (process.env.BUILD_TYPE || 'canary') + .trim() + .toLowerCase(); +export const buildType = ReleaseTypeSchema.parse(envBuildType); + +export const mode = process.env.NODE_ENV; +export const isDev = mode === 'development'; + +const API_URL_MAPPING = { + stable: `https://app.affine.pro`, + beta: `https://ambassador.affine.pro`, + canary: `https://affine.fail`, + internal: `https://affine.fail`, +}; + +export const CLOUD_BASE_URL = + process.env.DEV_SERVER_URL || API_URL_MAPPING[buildType]; diff --git a/apps/electron/src/main/deep-link.ts b/apps/electron/src/main/deep-link.ts new file mode 100644 index 0000000000..85a695248c --- /dev/null +++ b/apps/electron/src/main/deep-link.ts @@ -0,0 +1,35 @@ +import type { App } from 'electron'; + +import { buildType, isDev } from './config'; +import { logger } from './logger'; +import { handleOpenUrlInPopup } from './main-window'; + +let protocol = buildType === 'stable' ? 'affine' : `affine-${buildType}`; +if (isDev) { + protocol = 'affine-dev'; +} + +export function setupDeepLink(app: App) { + app.setAsDefaultProtocolClient(protocol); + app.on('open-url', (event, url) => { + if (url.startsWith(`${protocol}://`)) { + event.preventDefault(); + handleAffineUrl(url).catch(e => { + logger.error('failed to handle affine url', e); + }); + } + }); +} + +async function handleAffineUrl(url: string) { + logger.info('open affine url', url); + const urlObj = new URL(url); + if (urlObj.hostname === 'open-url') { + const urlToOpen = urlObj.search.slice(1); + if (urlToOpen) { + handleOpenUrlInPopup(urlToOpen).catch(e => { + logger.error('failed to open url in popup', e); + }); + } + } +} diff --git a/apps/electron/src/main/events.ts b/apps/electron/src/main/events.ts index d9444603b8..dacb86107d 100644 --- a/apps/electron/src/main/events.ts +++ b/apps/electron/src/main/events.ts @@ -2,11 +2,13 @@ import { app, BrowserWindow } from 'electron'; import { applicationMenuEvents } from './application-menu'; import { logger } from './logger'; +import { uiEvents } from './ui'; import { updaterEvents } from './updater/event'; export const allEvents = { applicationMenu: applicationMenuEvents, updater: updaterEvents, + ui: uiEvents, }; function getActiveWindows() { diff --git a/apps/electron/src/main/helper-process.ts b/apps/electron/src/main/helper-process.ts index 0f0ecd9b7e..ed73b096d5 100644 --- a/apps/electron/src/main/helper-process.ts +++ b/apps/electron/src/main/helper-process.ts @@ -38,7 +38,14 @@ class HelperProcessManager { // a rpc server for the main process -> helper process rpc?: _AsyncVersionOf; - static instance = new HelperProcessManager(); + static _instance: HelperProcessManager | null = null; + + static get instance() { + if (!this._instance) { + this._instance = new HelperProcessManager(); + } + return this._instance; + } private constructor() { const helperProcess = utilityProcess.fork(HELPER_PROCESS_PATH); diff --git a/apps/electron/src/main/index.ts b/apps/electron/src/main/index.ts index 499daa3f6c..0aff3821f3 100644 --- a/apps/electron/src/main/index.ts +++ b/apps/electron/src/main/index.ts @@ -3,6 +3,7 @@ import './security-restrictions'; import { app } from 'electron'; import { createApplicationMenu } from './application-menu/create'; +import { setupDeepLink } from './deep-link'; import { registerEvents } from './events'; import { registerHandlers } from './handlers'; import { ensureHelperProcess } from './helper-process'; @@ -38,10 +39,6 @@ app.on('second-instance', () => { ); }); -app.on('open-url', (_, _url) => { - // todo: handle `affine://...` urls -}); - /** * Shout down background process if all windows was closed */ @@ -55,11 +52,13 @@ app.on('window-all-closed', () => { * @see https://www.electronjs.org/docs/v14-x-y/api/app#event-activate-macos Event: 'activate' */ app.on('activate', () => { - restoreOrCreateWindow().catch(err => { - console.error(err); - }); + restoreOrCreateWindow().catch(e => + console.error('Failed to restore or create window:', e) + ); }); +setupDeepLink(app); + /** * Create app window when background process will be ready */ diff --git a/apps/electron/src/main/main-window.ts b/apps/electron/src/main/main-window.ts index bc58aa4f79..69bbd48aee 100644 --- a/apps/electron/src/main/main-window.ts +++ b/apps/electron/src/main/main-window.ts @@ -5,9 +5,11 @@ import electronWindowState from 'electron-window-state'; import { join } from 'path'; import { isMacOS, isWindows } from '../shared/utils'; +import { CLOUD_BASE_URL } from './config'; import { getExposedMeta } from './exposed'; import { ensureHelperProcess } from './helper-process'; import { logger } from './logger'; +import { parseCookie } from './utils'; const IS_DEV: boolean = process.env.NODE_ENV === 'development' && !process.env.CI; @@ -108,7 +110,7 @@ async function createWindow() { /** * URL for main window. */ - const pageUrl = process.env.DEV_SERVER_URL || 'file://.'; // see protocol.ts + const pageUrl = CLOUD_BASE_URL; // see protocol.ts logger.info('loading page at', pageUrl); @@ -120,13 +122,43 @@ async function createWindow() { } // singleton -let browserWindow: Electron.BrowserWindow | undefined; +let browserWindow: BrowserWindow | undefined; +let popup: BrowserWindow | undefined; + +function createPopupWindow() { + if (!popup || popup?.isDestroyed()) { + const mainExposedMeta = getExposedMeta(); + popup = new BrowserWindow({ + width: 1200, + height: 600, + alwaysOnTop: true, + resizable: false, + webPreferences: { + preload: join(__dirname, './preload.js'), + additionalArguments: [ + `--main-exposed-meta=` + JSON.stringify(mainExposedMeta), + // popup window does not need helper process, right? + ], + }, + }); + popup.on('close', e => { + e.preventDefault(); + popup?.destroy(); + popup = undefined; + }); + browserWindow?.webContents.once('did-finish-load', () => { + closePopup(); + }); + } + return popup; +} /** * Restore existing BrowserWindow or Create new BrowserWindow */ export async function restoreOrCreateWindow() { - browserWindow = BrowserWindow.getAllWindows().find(w => !w.isDestroyed()); + browserWindow = + browserWindow || BrowserWindow.getAllWindows().find(w => !w.isDestroyed()); if (browserWindow === undefined) { browserWindow = await createWindow(); @@ -139,3 +171,25 @@ export async function restoreOrCreateWindow() { return browserWindow; } + +export async function handleOpenUrlInPopup(url: string) { + const popup = createPopupWindow(); + await popup.loadURL(url); +} + +export function closePopup() { + if (!popup?.isDestroyed()) { + popup?.close(); + popup?.destroy(); + popup = undefined; + } +} + +export function reloadApp() { + browserWindow?.reload(); +} + +export async function setCookie(origin: string, cookie: string) { + const window = await restoreOrCreateWindow(); + await window.webContents.session.cookies.set(parseCookie(cookie, origin)); +} diff --git a/apps/electron/src/main/protocol.ts b/apps/electron/src/main/protocol.ts index ee2ac5658a..23039f7b6d 100644 --- a/apps/electron/src/main/protocol.ts +++ b/apps/electron/src/main/protocol.ts @@ -1,51 +1,73 @@ -import { protocol, session } from 'electron'; +import { net, protocol, session } from 'electron'; import { join } from 'path'; -protocol.registerSchemesAsPrivileged([ - { - scheme: 'assets', - privileges: { - secure: false, - corsEnabled: true, - supportFetchAPI: true, - standard: true, - bypassCSP: true, - }, - }, -]); +import { CLOUD_BASE_URL } from './config'; +import { setCookie } from './main-window'; +import { simpleGet } from './utils'; -function toAbsolutePath(url: string) { - let realpath: string; - const webStaticDir = join(__dirname, '../resources/web-static'); - if (url.startsWith('./')) { - // if is a file type, load the file in resources - if (url.split('/').at(-1)?.includes('.')) { - realpath = join(webStaticDir, decodeURIComponent(url)); +const NETWORK_REQUESTS = ['/api', '/ws', '/socket.io', '/graphql']; +const webStaticDir = join(__dirname, '../resources/web-static'); + +function isNetworkResource(pathname: string) { + return NETWORK_REQUESTS.some(opt => pathname.startsWith(opt)); +} + +async function handleHttpRequest(request: Request) { + const clonedRequest = Object.assign(request.clone(), { + bypassCustomProtocolHandlers: true, + }); + const { pathname, origin } = new URL(request.url); + if ( + !origin.startsWith(CLOUD_BASE_URL) || + isNetworkResource(pathname) || + process.env.DEV_SERVER_URL // when debugging locally + ) { + // note: I don't find a good way to get over with 302 redirect + // by default in net.fetch, or don't know if there is a way to + // bypass http request handling to browser instead ... + if (pathname.startsWith('/api/auth/callback')) { + const originResponse = await simpleGet(request.url); + // hack: use window.webContents.session.cookies to set cookies + // since return set-cookie header in response doesn't work here + for (const [, cookie] of originResponse.headers.filter( + p => p[0] === 'set-cookie' + )) { + await setCookie(origin, cookie); + } + return new Response(originResponse.body, { + headers: originResponse.headers, + status: originResponse.statusCode, + }); } else { - // else, fallback to load the index.html instead - realpath = join(webStaticDir, 'index.html'); + // just pass through (proxy) + return net.fetch(request.url, clonedRequest); } } else { - realpath = join(webStaticDir, decodeURIComponent(url)); + // this will be file types (in the web-static folder) + let filepath = ''; + // if is a file type, load the file in resources + if (pathname.split('/').at(-1)?.includes('.')) { + filepath = join(webStaticDir, decodeURIComponent(pathname)); + } else { + // else, fallback to load the index.html instead + filepath = join(webStaticDir, 'index.html'); + } + return net.fetch('file://' + filepath, clonedRequest); } - return realpath; } export function registerProtocol() { - protocol.interceptFileProtocol('file', (request, callback) => { - const url = request.url.replace(/^file:\/\//, ''); - const realpath = toAbsolutePath(url); - callback(realpath); - return true; + // it seems that there is some issue to postMessage between renderer with custom protocol & helper process + protocol.handle('http', request => { + return handleHttpRequest(request); }); - protocol.registerFileProtocol('assets', (request, callback) => { - const url = request.url.replace(/^assets:\/\//, ''); - const realpath = toAbsolutePath(url); - callback(realpath); - return true; + protocol.handle('https', request => { + return handleHttpRequest(request); }); + // hack for CORS + // todo: should use a whitelist session.defaultSession.webRequest.onHeadersReceived( (responseDetails, callback) => { const { responseHeaders } = responseDetails; diff --git a/apps/electron/src/main/ui/events.ts b/apps/electron/src/main/ui/events.ts new file mode 100644 index 0000000000..eaeece0112 --- /dev/null +++ b/apps/electron/src/main/ui/events.ts @@ -0,0 +1,14 @@ +import type { MainEventRegister } from '../type'; +import { uiSubjects } from './subject'; + +/** + * Events triggered by application menu + */ +export const uiEvents = { + onFinishLogin: (fn: () => void) => { + const sub = uiSubjects.onFinishLogin.subscribe(fn); + return () => { + sub.unsubscribe(); + }; + }, +} satisfies Record; diff --git a/apps/electron/src/main/ui/handlers.ts b/apps/electron/src/main/ui/handlers.ts new file mode 100644 index 0000000000..e37f6690e9 --- /dev/null +++ b/apps/electron/src/main/ui/handlers.ts @@ -0,0 +1,56 @@ +import { app, BrowserWindow, nativeTheme } from 'electron'; + +import { isMacOS } from '../../shared/utils'; +import { closePopup } from '../main-window'; +import type { NamespaceHandlers } from '../type'; +import { getGoogleOauthCode } from './google-auth'; +import { uiSubjects } from './subject'; + +export const uiHandlers = { + handleThemeChange: async (_, theme: (typeof nativeTheme)['themeSource']) => { + nativeTheme.themeSource = theme; + }, + handleSidebarVisibilityChange: async (_, visible: boolean) => { + if (isMacOS()) { + const windows = BrowserWindow.getAllWindows(); + windows.forEach(w => { + // hide window buttons when sidebar is not visible + w.setWindowButtonVisibility(visible); + }); + } + }, + handleMinimizeApp: async () => { + const windows = BrowserWindow.getAllWindows(); + windows.forEach(w => { + w.minimize(); + }); + }, + handleMaximizeApp: async () => { + const windows = BrowserWindow.getAllWindows(); + windows.forEach(w => { + if (w.isMaximized()) { + w.unmaximize(); + } else { + w.maximize(); + } + }); + }, + handleCloseApp: async () => { + app.quit(); + }, + handleFinishLogin: async () => { + closePopup(); + uiSubjects.onFinishLogin.next(); + }, + getGoogleOauthCode: async () => { + return getGoogleOauthCode(); + }, + /** + * @deprecated Remove this when bookmark block plugin is migrated to plugin-infra + */ + getBookmarkDataByLink: async (_, link: string) => { + return globalThis.asyncCall[ + 'com.blocksuite.bookmark-block.get-bookmark-data-by-link' + ](link); + }, +} satisfies NamespaceHandlers; diff --git a/apps/electron/src/main/ui/index.ts b/apps/electron/src/main/ui/index.ts index d84a927d70..d56dc67573 100644 --- a/apps/electron/src/main/ui/index.ts +++ b/apps/electron/src/main/ui/index.ts @@ -1,50 +1,3 @@ -import { app, BrowserWindow, nativeTheme } from 'electron'; - -import { isMacOS } from '../../shared/utils'; -import type { NamespaceHandlers } from '../type'; -import { getGoogleOauthCode } from './google-auth'; - -export const uiHandlers = { - handleThemeChange: async (_, theme: (typeof nativeTheme)['themeSource']) => { - nativeTheme.themeSource = theme; - }, - handleSidebarVisibilityChange: async (_, visible: boolean) => { - if (isMacOS()) { - const windows = BrowserWindow.getAllWindows(); - windows.forEach(w => { - // hide window buttons when sidebar is not visible - w.setWindowButtonVisibility(visible); - }); - } - }, - handleMinimizeApp: async () => { - const windows = BrowserWindow.getAllWindows(); - windows.forEach(w => { - w.minimize(); - }); - }, - handleMaximizeApp: async () => { - const windows = BrowserWindow.getAllWindows(); - windows.forEach(w => { - if (w.isMaximized()) { - w.unmaximize(); - } else { - w.maximize(); - } - }); - }, - handleCloseApp: async () => { - app.quit(); - }, - getGoogleOauthCode: async () => { - return getGoogleOauthCode(); - }, - /** - * @deprecated Remove this when bookmark block plugin is migrated to plugin-infra - */ - getBookmarkDataByLink: async (_, link: string) => { - return globalThis.asyncCall[ - 'com.blocksuite.bookmark-block.get-bookmark-data-by-link' - ](link); - }, -} satisfies NamespaceHandlers; +export * from './events'; +export * from './handlers'; +export * from './subject'; diff --git a/apps/electron/src/main/ui/subject.ts b/apps/electron/src/main/ui/subject.ts new file mode 100644 index 0000000000..17f6179d3f --- /dev/null +++ b/apps/electron/src/main/ui/subject.ts @@ -0,0 +1,5 @@ +import { Subject } from 'rxjs'; + +export const uiSubjects = { + onFinishLogin: new Subject(), +}; diff --git a/apps/electron/src/main/utils.ts b/apps/electron/src/main/utils.ts new file mode 100644 index 0000000000..42f0a6802e --- /dev/null +++ b/apps/electron/src/main/utils.ts @@ -0,0 +1,110 @@ +import http from 'node:http'; +import https from 'node:https'; + +import type { CookiesSetDetails } from 'electron'; + +export function parseCookie( + cookieString: string, + url: string +): CookiesSetDetails { + const [nameValuePair, ...attributes] = cookieString + .split('; ') + .map(part => part.trim()); + + const [name, value] = nameValuePair.split('='); + + const details: CookiesSetDetails = { url, name, value }; + + attributes.forEach(attribute => { + const [key, val] = attribute.split('='); + + switch (key.toLowerCase()) { + case 'domain': + details.domain = val; + break; + case 'path': + details.path = val; + break; + case 'secure': + details.secure = true; + break; + case 'httponly': + details.httpOnly = true; + break; + case 'expires': + details.expirationDate = new Date(val).getTime() / 1000; // Convert to seconds + break; + case 'samesite': + if ( + ['unspecified', 'no_restriction', 'lax', 'strict'].includes( + val.toLowerCase() + ) + ) { + details.sameSite = val.toLowerCase() as + | 'unspecified' + | 'no_restriction' + | 'lax' + | 'strict'; + } + break; + default: + // Handle other cookie attributes if needed + break; + } + }); + + return details; +} + +/** + * Send a GET request to a specified URL. + * This function uses native http/https modules instead of fetch to + * bypassing set-cookies headers + */ +export async function simpleGet(requestUrl: string): Promise<{ + body: string; + headers: [string, string][]; + statusCode: number; +}> { + return new Promise((resolve, reject) => { + const parsedUrl = new URL(requestUrl); + const protocol = parsedUrl.protocol === 'https:' ? https : http; + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.pathname + parsedUrl.search, + method: 'GET', + }; + const req = protocol.request(options, res => { + let data = ''; + res.on('data', chunk => { + data += chunk; + }); + res.on('end', () => { + resolve({ + body: data, + headers: toStandardHeaders(res.headers), + statusCode: res.statusCode || 200, + }); + }); + }); + req.on('error', error => { + reject(error); + }); + req.end(); + }); + + function toStandardHeaders(headers: http.IncomingHttpHeaders) { + const result: [string, string][] = []; + for (const [key, value] of Object.entries(headers)) { + if (Array.isArray(value)) { + value.forEach(v => { + result.push([key, v]); + }); + } else { + result.push([key, value || '']); + } + } + return result; + } +} diff --git a/apps/prototype/project.json b/apps/prototype/project.json index 6900a83832..77425175c7 100644 --- a/apps/prototype/project.json +++ b/apps/prototype/project.json @@ -6,10 +6,52 @@ "targets": { "build": { "executor": "nx:run-script", - "dependsOn": ["^build"], + "dependsOn": [ + { + "projects": ["tag:plugin"], + "target": "build", + "params": "ignore" + }, + { + "projects": ["tag:infra"], + "target": "build", + "params": "ignore" + }, + "^build" + ], "options": { "script": "build" }, + "inputs": [ + "{projectRoot}/.webpack/**/*", + "{projectRoot}/**/*", + "{workspaceRoot}/apps/core/public/**/*", + "{workspaceRoot}/packages/**/*", + { + "env": "BUILD_TYPE" + }, + { + "env": "PERFSEE_TOKEN" + }, + { + "env": "SENTRY_ORG" + }, + { + "env": "SENTRY_PROJECT" + }, + { + "env": "SENTRY_AUTH_TOKEN" + }, + { + "env": "NEXT_PUBLIC_SENTRY_DSN" + }, + { + "env": "DISTRIBUTION" + }, + { + "env": "COVERAGE" + } + ], "outputs": ["{projectRoot}/dist"] } } diff --git a/apps/prototype/tsconfig.node.json b/apps/prototype/tsconfig.node.json index 02a6dd9886..7afc10a4e5 100644 --- a/apps/prototype/tsconfig.node.json +++ b/apps/prototype/tsconfig.node.json @@ -7,5 +7,10 @@ "outDir": "./lib", "allowSyntheticDefaultImports": true }, + "references": [ + { + "path": "../../apps/core" + } + ], "include": ["vite.config.ts"] } diff --git a/apps/prototype/vite.config.ts b/apps/prototype/vite.config.ts index b83f10efbe..d39904944f 100644 --- a/apps/prototype/vite.config.ts +++ b/apps/prototype/vite.config.ts @@ -3,6 +3,8 @@ import { resolve } from 'node:path'; import react from '@vitejs/plugin-react-swc'; import { defineConfig } from 'vite'; +import { getRuntimeConfig } from '../core/.webpack/runtime-config'; + // https://vitejs.dev/config/ export default defineConfig({ build: { @@ -18,5 +20,19 @@ export default defineConfig({ }, }, }, + define: { + 'process.env': {}, + 'process.env.COVERAGE': JSON.stringify(!!process.env.COVERAGE), + 'process.env.SHOULD_REPORT_TRACE': `${Boolean( + process.env.SHOULD_REPORT_TRACE + )}`, + 'process.env.TRACE_REPORT_ENDPOINT': `"${process.env.TRACE_REPORT_ENDPOINT}"`, + runtimeConfig: getRuntimeConfig({ + distribution: 'browser', + mode: 'development', + channel: 'canary', + coverage: false, + }), + }, plugins: [react()], }); diff --git a/apps/server/.env.example b/apps/server/.env.example index 6fd5336f41..2dcf75924d 100644 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -1,2 +1,5 @@ DATABASE_URL="postgresql://affine@localhost:5432/affine" NEXTAUTH_URL="http://localhost:8080" +OAUTH_EMAIL_SENDER="noreply@toeverything.info" +OAUTH_EMAIL_LOGIN="" +OAUTH_EMAIL_PASSWORD="" diff --git a/apps/server/LICENSE b/apps/server/LICENSE new file mode 100644 index 0000000000..053ad06aa0 --- /dev/null +++ b/apps/server/LICENSE @@ -0,0 +1,44 @@ +The AFFiNE Enterprise Edition (EE) license (the “EE License”) +Copyright (c) 2022-present TOEVERYTHING PTE. LTD. and its affiliates. + +With regard to the AFFiNE Software: + +This software and associated documentation files (the "Software") may only be +used in production, if you (and any entity that you represent) have agreed to, +and are in compliance with, the AFFiNE Subscription Terms of Service, available +at https://affine.pro/terms/#subscription (the “EE Terms”), or other +agreement governing the use of the Software, as agreed by you and AFFiNE, +and otherwise have a valid AFFiNE Enterprise Edition subscription for the +correct number of user seats. Subject to the foregoing sentence, you are free to +modify this Software and publish patches to the Software. You agree that AFFiNE +and/or its licensors (as applicable) retain all right, title and interest in and +to all such modifications and/or patches, and all such modifications and/or +patches may only be used, copied, modified, displayed, distributed, or otherwise +exploited with a valid AFFiNE Enterprise Edition subscription for the correct +number of user seats. Notwithstanding the foregoing, you may copy and modify +the Software for development and testing purposes, without requiring a +subscription. You agree that AFFiNE and/or its licensors (as applicable) retain +all right, title and interest in and to all such modifications. You are not +granted any other rights beyond what is expressly stated herein. Subject to the +foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, +and/or sell the Software. + +This EE License applies only to the part of this Software that is not +distributed as part of AFFiNE Community Edition (CE). Any part of this Software +distributed as part of AFFiNE CE or is served client-side as an image, font, +cascading stylesheet (CSS), file which produces or is compiled, arranged, +augmented, or combined into client-side JavaScript, in whole or in part, is +copyrighted under the MPL2.0 license. The full text of this EE License shall +be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +For all third party components incorporated into the AFFiNE Software, those +components are licensed under the original license provided by the owner of the +applicable component. diff --git a/apps/server/migrations/20230705025556_workspace_id_fkey/migration.sql b/apps/server/migrations/20230705025556_workspace_id_fkey/migration.sql new file mode 100644 index 0000000000..a6e92b655f --- /dev/null +++ b/apps/server/migrations/20230705025556_workspace_id_fkey/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE "blobs" DROP CONSTRAINT "blobs_workspace_id_fkey"; + +-- DropForeignKey +ALTER TABLE "docs" DROP CONSTRAINT "docs_workspace_id_fkey"; + +-- DropForeignKey +ALTER TABLE "optimized_blobs" DROP CONSTRAINT "optimized_blobs_workspace_id_fkey"; diff --git a/apps/server/migrations/20230706065816_workspace_subpage/migration.sql b/apps/server/migrations/20230706065816_workspace_subpage/migration.sql new file mode 100644 index 0000000000..5c16600b11 --- /dev/null +++ b/apps/server/migrations/20230706065816_workspace_subpage/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[workspace_id,sub_page_id,entity_id]` on the table `user_workspace_permissions` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "user_workspace_permissions" ADD COLUMN "sub_page_id" VARCHAR, +ALTER COLUMN "entity_id" DROP NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "user_workspace_permissions_workspace_id_sub_page_id_entity__key" ON "user_workspace_permissions"("workspace_id", "sub_page_id", "entity_id"); diff --git a/apps/server/migrations/20230706090316_change_avatar_url_field_name/migration.sql b/apps/server/migrations/20230706090316_change_avatar_url_field_name/migration.sql new file mode 100644 index 0000000000..48801a6e78 --- /dev/null +++ b/apps/server/migrations/20230706090316_change_avatar_url_field_name/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `image` on the `users` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "users" DROP COLUMN "image", +ADD COLUMN "avatar_url" VARCHAR; diff --git a/apps/server/migrations/20230709091238_fix_blob_types/migration.sql b/apps/server/migrations/20230709091238_fix_blob_types/migration.sql new file mode 100644 index 0000000000..093a8c37e8 --- /dev/null +++ b/apps/server/migrations/20230709091238_fix_blob_types/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - The primary key for the `blobs` table will be changed. If it partially fails, the table could be left without primary key constraint. + - The primary key for the `optimized_blobs` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- AlterTable +ALTER TABLE "blobs" DROP CONSTRAINT "blobs_pkey", +ADD COLUMN "id" SERIAL NOT NULL, +ALTER COLUMN "length" SET DATA TYPE BIGINT, +ADD CONSTRAINT "blobs_pkey" PRIMARY KEY ("id"); + +-- AlterTable +ALTER TABLE "optimized_blobs" DROP CONSTRAINT "optimized_blobs_pkey", +ADD COLUMN "id" SERIAL NOT NULL, +ALTER COLUMN "length" SET DATA TYPE BIGINT, +ADD CONSTRAINT "optimized_blobs_pkey" PRIMARY KEY ("id"); diff --git a/apps/server/migrations/20230713022301_update_manager/migration.sql b/apps/server/migrations/20230713022301_update_manager/migration.sql new file mode 100644 index 0000000000..ca81f12708 --- /dev/null +++ b/apps/server/migrations/20230713022301_update_manager/migration.sql @@ -0,0 +1,42 @@ +/* + Warnings: + + - You are about to drop the `docs` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "docs"; + +-- CreateTable +CREATE TABLE "snapshots" ( + "guid" VARCHAR NOT NULL, + "workspace_id" VARCHAR NOT NULL, + "blob" BYTEA NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "snapshots_pkey" PRIMARY KEY ("guid") +); + +-- CreateTable +CREATE TABLE "updates" ( + "object_id" VARCHAR NOT NULL, + "guid" VARCHAR NOT NULL, + "workspace_id" VARCHAR NOT NULL, + "blob" BYTEA NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "updates_pkey" PRIMARY KEY ("object_id") +); + +-- CreateIndex +CREATE INDEX "snapshots_workspace_id_idx" ON "snapshots"("workspace_id"); + +-- CreateIndex +CREATE INDEX "updates_guid_workspace_id_idx" ON "updates"("guid", "workspace_id"); + +-- AddForeignKey +ALTER TABLE "snapshots" ADD CONSTRAINT "snapshots_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "updates" ADD CONSTRAINT "updates_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server/migrations/20230714065216_snapshot_id/migration.sql b/apps/server/migrations/20230714065216_snapshot_id/migration.sql new file mode 100644 index 0000000000..9f331691eb --- /dev/null +++ b/apps/server/migrations/20230714065216_snapshot_id/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - The primary key for the `snapshots` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- DropIndex +DROP INDEX "snapshots_workspace_id_idx"; + +-- AlterTable +ALTER TABLE "snapshots" DROP CONSTRAINT "snapshots_pkey", +ADD CONSTRAINT "snapshots_pkey" PRIMARY KEY ("guid", "workspace_id"); diff --git a/apps/server/migrations/20230717084417_remove_update_fkey/migration.sql b/apps/server/migrations/20230717084417_remove_update_fkey/migration.sql new file mode 100644 index 0000000000..7cf6eff493 --- /dev/null +++ b/apps/server/migrations/20230717084417_remove_update_fkey/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "snapshots" DROP CONSTRAINT "snapshots_workspace_id_fkey"; + +-- DropForeignKey +ALTER TABLE "updates" DROP CONSTRAINT "updates_workspace_id_fkey"; diff --git a/apps/server/migrations/20230822071646_add_new_features_waiting_list/migration.sql b/apps/server/migrations/20230822071646_add_new_features_waiting_list/migration.sql new file mode 100644 index 0000000000..6988a5f61e --- /dev/null +++ b/apps/server/migrations/20230822071646_add_new_features_waiting_list/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "new_features_waiting_list" ( + "id" VARCHAR NOT NULL, + "email" TEXT NOT NULL, + "type" SMALLINT NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "new_features_waiting_list_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "new_features_waiting_list_email_key" ON "new_features_waiting_list"("email"); diff --git a/apps/server/migrations/20230824091506_euser_email_is_not_nullable/migration.sql b/apps/server/migrations/20230824091506_euser_email_is_not_nullable/migration.sql new file mode 100644 index 0000000000..3bf71a4746 --- /dev/null +++ b/apps/server/migrations/20230824091506_euser_email_is_not_nullable/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Made the column `email` on table `users` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "users" ALTER COLUMN "email" SET NOT NULL; diff --git a/apps/server/package.json b/apps/server/package.json index e60618be84..021f3b4617 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -9,6 +9,7 @@ }, "scripts": { "build": "tsc", + "start": "node --loader ts-node/esm.mjs --es-module-specifier-resolution node ./src/index.ts", "dev": "nodemon ./src/index.ts", "test": "yarn exec ts-node-esm ./scripts/run-test.ts all", "test:watch": "yarn exec ts-node-esm ./scripts/run-test.ts all --watch", @@ -18,30 +19,55 @@ "dependencies": { "@apollo/server": "^4.9.2", "@auth/prisma-adapter": "^1.0.1", - "@aws-sdk/client-s3": "^3.398.0", + "@aws-sdk/client-s3": "^3.400.0", + "@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.17.0", + "@google-cloud/opentelemetry-cloud-trace-exporter": "^2.1.0", "@nestjs/apollo": "^12.0.7", "@nestjs/common": "^10.2.1", "@nestjs/core": "^10.2.1", "@nestjs/graphql": "^12.0.8", - "@nestjs/platform-express": "^10.2.1", + "@nestjs/platform-express": "^10.1.3", + "@nestjs/platform-socket.io": "^10.0.5", + "@nestjs/websockets": "^10.0.5", "@node-rs/argon2": "^1.5.2", "@node-rs/crc32": "^1.7.2", "@node-rs/jsonwebtoken": "^0.2.3", + "@opentelemetry/api": "^1.4.1", + "@opentelemetry/instrumentation": "^0.41.1", + "@opentelemetry/instrumentation-graphql": "^0.35.0", + "@opentelemetry/instrumentation-http": "^0.41.1", + "@opentelemetry/instrumentation-ioredis": "^0.35.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.33.0", + "@opentelemetry/instrumentation-socket.io": "^0.34.0", + "@opentelemetry/sdk-metrics": "^1.15.1", + "@opentelemetry/sdk-node": "^0.41.1", + "@opentelemetry/sdk-trace-node": "^1.15.1", "@prisma/client": "^5.2.0", + "@prisma/instrumentation": "^5.0.0", + "@socket.io/redis-adapter": "^8.2.1", "cookie-parser": "^1.4.6", "dotenv": "^16.3.1", "express": "^4.18.2", + "file-type": "^18.5.0", + "get-stream": "^7.0.1", "graphql": "^16.8.0", "graphql-type-json": "^0.3.2", "graphql-upload": "^16.0.2", + "ioredis": "^5.3.2", "lodash-es": "^4.17.21", "next-auth": "4.22.5", "nodemailer": "^6.9.4", + "on-headers": "^1.0.2", "parse-duration": "^1.1.0", + "pretty-time": "^1.1.0", "prisma": "^5.1.1", + "prom-client": "^14.2.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", - "semver": "^7.5.4" + "semver": "^7.5.4", + "socket.io": "^4.7.1", + "ws": "^8.13.0", + "yjs": "^13.6.6" }, "devDependencies": { "@affine/storage": "workspace:*", @@ -52,9 +78,14 @@ "@types/lodash-es": "^4.17.8", "@types/node": "^18.17.11", "@types/nodemailer": "^6.4.9", + "@types/on-headers": "^1.0.0", + "@types/pretty-time": "^1.1.2", + "@types/sinon": "^10.0.15", "@types/supertest": "^2.0.12", + "@types/ws": "^8.5.5", "c8": "^8.0.1", "nodemon": "^3.0.1", + "sinon": "^15.2.0", "supertest": "^6.3.3", "ts-node": "^10.9.1", "typescript": "^5.2.2" diff --git a/apps/server/schema.prisma b/apps/server/schema.prisma index 3d8a6557fc..e5ad0e5073 100644 --- a/apps/server/schema.prisma +++ b/apps/server/schema.prisma @@ -1,6 +1,7 @@ generator client { provider = "prisma-client-js" binaryTargets = ["native", "debian-openssl-3.0.x"] + previewFeatures = ["metrics", "tracing"] } datasource db { @@ -9,13 +10,10 @@ datasource db { } model Workspace { - id String @id @default(uuid()) @db.VarChar - public Boolean - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) - users UserWorkspacePermission[] - blobs Blob[] - docs Doc[] - optimizedBlobs OptimizedBlob[] + id String @id @default(uuid()) @db.VarChar + public Boolean + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + users UserWorkspacePermission[] @@map("workspaces") } @@ -23,25 +21,27 @@ model Workspace { model UserWorkspacePermission { id String @id @default(uuid()) @db.VarChar workspaceId String @map("workspace_id") @db.VarChar - userId String @map("entity_id") @db.VarChar + subPageId String? @map("sub_page_id") @db.VarChar + userId String? @map("entity_id") @db.VarChar /// Read/Write/Admin/Owner type Int @db.SmallInt /// Whether the permission invitation is accepted by the user accepted Boolean @default(false) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: Cascade) workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) + @@unique([workspaceId, subPageId, userId]) @@map("user_workspace_permissions") } model User { id String @id @default(uuid()) @db.VarChar name String - email String? @unique + email String @unique emailVerified DateTime? @map("email_verified") // image field is for the next-auth - avatarUrl String? @map("image") @db.VarChar + avatarUrl String? @map("avatar_url") @db.VarChar accounts Account[] sessions Session[] workspaces UserWorkspacePermission[] @@ -92,42 +92,60 @@ model VerificationToken { } model Blob { - hash String @id @default(uuid()) @db.VarChar + id Int @id @default(autoincrement()) @db.Integer + hash String @db.VarChar workspaceId String @map("workspace_id") @db.VarChar blob Bytes @db.ByteA - length Int + length BigInt createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) - workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) - @@unique([workspaceId, hash]) @@map("blobs") } model OptimizedBlob { - hash String @id @default(uuid()) @db.VarChar + id Int @id @default(autoincrement()) @db.Integer + hash String @db.VarChar workspaceId String @map("workspace_id") @db.VarChar params String @db.VarChar blob Bytes @db.ByteA - length Int + length BigInt createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) - workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) - @@unique([workspaceId, hash, params]) @@map("optimized_blobs") } -model Doc { - id Int @id @default(autoincrement()) @db.Integer - workspaceId String @map("workspace_id") @db.VarChar - guid String @db.VarChar - is_workspace Boolean @default(true) @db.Boolean - blob Bytes @db.ByteA - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) +// the latest snapshot of each doc that we've seen +// Snapshot + Updates are the latest state of the doc +model Snapshot { + id String @default(uuid()) @map("guid") @db.VarChar + workspaceId String @map("workspace_id") @db.VarChar + blob Bytes @db.ByteA + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) - workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) - - @@index([workspaceId, guid]) - @@map("docs") + @@id([id, workspaceId]) + @@map("snapshots") +} + +// backup during other update operation queue downtime +model Update { + objectId String @id @default(uuid()) @map("object_id") @db.VarChar + id String @map("guid") @db.VarChar + workspaceId String @map("workspace_id") @db.VarChar + blob Bytes @db.ByteA + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + + @@index([id, workspaceId]) + @@map("updates") +} + +model NewFeaturesWaitingList { + id String @id @default(uuid()) @db.VarChar + email String @unique + type Int @db.SmallInt + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + + @@map("new_features_waiting_list") } diff --git a/apps/server/scripts/init-db.ts b/apps/server/scripts/init-db.ts index 3bb8bcd662..a43ebb4f58 100644 --- a/apps/server/scripts/init-db.ts +++ b/apps/server/scripts/init-db.ts @@ -1,10 +1,14 @@ import userA from '@affine-test/fixtures/userA.json' assert { type: 'json' }; +import { hash } from '@node-rs/argon2'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); async function main() { await prisma.user.create({ - data: userA, + data: { + ...userA, + password: await hash(userA.password), + }, }); } diff --git a/apps/server/scripts/run-test.ts b/apps/server/scripts/run-test.ts index 7c4e6067de..efdfc4eec4 100755 --- a/apps/server/scripts/run-test.ts +++ b/apps/server/scripts/run-test.ts @@ -24,6 +24,7 @@ const env = { PATH: process.env.PATH, NODE_ENV: 'test', DATABASE_URL: process.env.DATABASE_URL, + NODE_NO_WARNINGS: '1', }; if (process.argv[2] === 'all') { diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 224bbe40f7..db3b692626 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { ConfigModule } from './config'; -import { GqlModule } from './graphql.module'; +import { MetricsModule } from './metrics'; import { BusinessModules } from './modules'; import { PrismaModule } from './prisma'; import { StorageModule } from './storage'; @@ -10,9 +10,9 @@ import { StorageModule } from './storage'; @Module({ imports: [ PrismaModule, - GqlModule, ConfigModule.forRoot(), StorageModule.forRoot(), + MetricsModule, ...BusinessModules, ], controllers: [AppController], diff --git a/apps/server/src/config/def.ts b/apps/server/src/config/def.ts index 8c61cf10e5..0dfc420b2f 100644 --- a/apps/server/src/config/def.ts +++ b/apps/server/src/config/def.ts @@ -77,6 +77,10 @@ export interface AFFiNEConfig { * System version */ readonly version: string; + /** + * Deployment environment + */ + readonly affineEnv: 'dev' | 'beta' | 'production'; /** * alias to `process.env.NODE_ENV` * @@ -84,12 +88,22 @@ export interface AFFiNEConfig { * @env NODE_ENV */ readonly env: string; + /** + * fast AFFiNE environment judge + */ + get affine(): { + canary: boolean; + beta: boolean; + stable: boolean; + }; /** * fast environment judge */ - get prod(): boolean; - get dev(): boolean; - get test(): boolean; + get node(): { + prod: boolean; + dev: boolean; + test: boolean; + }; get deploy(): boolean; /** @@ -167,6 +181,28 @@ export interface AFFiNEConfig { path: string; }; }; + /** + * Redis Config + * + * whether to use redis as Socket.IO adapter + */ + redis: { + /** + * if not enabled, use in-memory adapter by default + */ + enabled: boolean; + /** + * url of redis host + */ + host: string; + /** + * port of redis + */ + port: number; + username: string; + password: string; + database: number; + }; /** * authentication config @@ -236,8 +272,30 @@ export interface AFFiNEConfig { email: { server: string; port: number; + login: string; sender: string; password: string; }; }; + + doc: { + manager: { + /** + * How often the [DocManager] will start a new turn of merging pending updates into doc snapshot. + * + * This is not the latency a new joint client will take to see the latest doc, + * but the buffer time we introduced to reduce the load of our service. + * + * in {ms} + */ + updatePollInterval: number; + + /** + * Use JwstCodec 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; + }; + }; } diff --git a/apps/server/src/config/default.ts b/apps/server/src/config/default.ts index 21e74f899d..a31a6dd517 100644 --- a/apps/server/src/config/default.ts +++ b/apps/server/src/config/default.ts @@ -51,37 +51,60 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { serverId: 'affine-nestjs-server', version: pkg.version, ENV_MAP: { - AFFINE_SERVER_PORT: 'port', + AFFINE_SERVER_PORT: ['port', 'int'], AFFINE_SERVER_HOST: 'host', AFFINE_SERVER_SUB_PATH: 'path', + AFFINE_ENV: 'affineEnv', DATABASE_URL: 'db.url', AUTH_PRIVATE_KEY: 'auth.privateKey', - ENABLE_R2_OBJECT_STORAGE: 'objectStorage.r2.enabled', + ENABLE_R2_OBJECT_STORAGE: ['objectStorage.r2.enabled', 'boolean'], R2_OBJECT_STORAGE_ACCOUNT_ID: 'objectStorage.r2.accountId', R2_OBJECT_STORAGE_ACCESS_KEY_ID: 'objectStorage.r2.accessKeyId', R2_OBJECT_STORAGE_SECRET_ACCESS_KEY: 'objectStorage.r2.secretAccessKey', R2_OBJECT_STORAGE_BUCKET: 'objectStorage.r2.bucket', + OAUTH_GOOGLE_ENABLED: ['auth.oauthProviders.google.enabled', 'boolean'], OAUTH_GOOGLE_CLIENT_ID: 'auth.oauthProviders.google.clientId', OAUTH_GOOGLE_CLIENT_SECRET: 'auth.oauthProviders.google.clientSecret', + OAUTH_GITHUB_ENABLED: ['auth.oauthProviders.github.enabled', 'boolean'], OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId', OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret', + OAUTH_EMAIL_LOGIN: 'auth.email.login', OAUTH_EMAIL_SENDER: 'auth.email.sender', OAUTH_EMAIL_SERVER: 'auth.email.server', - OAUTH_EMAIL_PORT: 'auth.email.port', + OAUTH_EMAIL_PORT: ['auth.email.port', 'int'], OAUTH_EMAIL_PASSWORD: 'auth.email.password', + REDIS_SERVER_ENABLED: ['redis.enabled', 'boolean'], + REDIS_SERVER_HOST: 'redis.host', + REDIS_SERVER_PORT: ['redis.port', 'int'], + REDIS_SERVER_USER: 'redis.username', + REDIS_SERVER_PASSWORD: 'redis.password', + REDIS_SERVER_DATABASE: ['redis.database', 'int'], + DOC_MERGE_INTERVAL: ['doc.manager.updatePollInterval', 'int'], + DOC_MERGE_USE_JWST_CODEC: [ + 'doc.manager.experimentalMergeWithJwstCodec', + 'boolean', + ], } satisfies AFFiNEConfig['ENV_MAP'], + affineEnv: 'dev', + get affine() { + const env = this.affineEnv; + return { + canary: env === 'dev', + beta: env === 'beta', + stable: env === 'production', + }; + }, env: process.env.NODE_ENV ?? 'development', - get prod() { - return this.env === 'production'; - }, - get dev() { - return this.env === 'development'; - }, - get test() { - return this.env === 'test'; + get node() { + const env = this.env; + return { + prod: env === 'production', + dev: env === 'development', + test: env === 'test', + }; }, get deploy() { - return !this.dev && !this.test; + return !this.node.dev && !this.node.test; }, https: false, host: 'localhost', @@ -91,7 +114,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { url: '', }, get origin() { - return this.dev + return this.node.dev ? 'http://localhost:8080' : `${this.https ? 'https' : 'http'}://${this.host}${ this.host === 'localhost' ? `:${this.port}` : '' @@ -124,6 +147,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { email: { server: 'smtp.gmail.com', port: 465, + login: '', sender: '', password: '', }, @@ -140,6 +164,20 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { path: join(homedir(), '.affine-storage'), }, }, + redis: { + enabled: false, + host: '127.0.0.1', + port: 6379, + username: '', + password: '', + database: 0, + }, + doc: { + manager: { + updatePollInterval: 3000, + experimentalMergeWithJwstCodec: false, + }, + }, } satisfies AFFiNEConfig; applyEnvToConfig(defaultConfig); diff --git a/apps/server/src/constants.ts b/apps/server/src/constants.ts new file mode 100644 index 0000000000..5c0677a3ab --- /dev/null +++ b/apps/server/src/constants.ts @@ -0,0 +1,3 @@ +export const OPERATION_NAME = 'x-operation-name'; + +export const REQUEST_ID = 'x-request-id'; diff --git a/apps/server/src/graphql.module.ts b/apps/server/src/graphql.module.ts index 445e7c2aff..04bc1034dd 100644 --- a/apps/server/src/graphql.module.ts +++ b/apps/server/src/graphql.module.ts @@ -2,17 +2,20 @@ import type { ApolloDriverConfig } from '@nestjs/apollo'; import { ApolloDriver } from '@nestjs/apollo'; import { Global, Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; +import { Request, Response } from 'express'; import { join } from 'path'; import { fileURLToPath } from 'url'; import { Config } from './config'; +import { GQLLoggerPlugin } from './graphql/logger-plugin'; +import { Metrics } from './metrics/metrics'; @Global() @Module({ imports: [ GraphQLModule.forRootAsync({ driver: ApolloDriver, - useFactory: (config: Config) => { + useFactory: (config: Config, metrics: Metrics) => { return { ...config.graphql, path: `${config.path}/graphql`, @@ -24,9 +27,14 @@ import { Config } from './config'; '..', 'schema.gql' ), + context: ({ req, res }: { req: Request; res: Response }) => ({ + req, + res, + }), + plugins: [new GQLLoggerPlugin(metrics)], }; }, - inject: [Config], + inject: [Config, Metrics], }), ], }) diff --git a/apps/server/src/graphql/logger-plugin.ts b/apps/server/src/graphql/logger-plugin.ts new file mode 100644 index 0000000000..b59cdaf50d --- /dev/null +++ b/apps/server/src/graphql/logger-plugin.ts @@ -0,0 +1,60 @@ +import { + ApolloServerPlugin, + GraphQLRequestContext, + GraphQLRequestListener, +} from '@apollo/server'; +import { Plugin } from '@nestjs/apollo'; +import { Logger } from '@nestjs/common'; +import { Response } from 'express'; + +import { OPERATION_NAME, REQUEST_ID } from '../constants'; +import { Metrics } from '../metrics/metrics'; +import { ReqContext } from '../types'; + +@Plugin() +export class GQLLoggerPlugin implements ApolloServerPlugin { + protected logger = new Logger(GQLLoggerPlugin.name); + + constructor(private readonly metrics: Metrics) {} + + requestDidStart( + reqContext: GraphQLRequestContext + ): Promise>> { + const res = reqContext.contextValue.req.res as Response; + const operation = reqContext.request.operationName; + const headers = reqContext.request.http?.headers; + const requestId = headers + ? headers.get(`${REQUEST_ID}`) + : 'Unknown Request ID'; + const operationName = headers + ? headers.get(`${OPERATION_NAME}`) + : 'Unknown Operation Name'; + + this.metrics.gqlRequest(1, { operation }); + const timer = this.metrics.gqlTimer({ operation }); + + const requestInfo = `${REQUEST_ID}: ${requestId}, ${OPERATION_NAME}: ${operationName}`; + + return Promise.resolve({ + willSendResponse: () => { + const costInMilliseconds = timer() * 1000; + res.setHeader( + 'Server-Timing', + `gql;dur=${costInMilliseconds};desc="GraphQL"` + ); + this.logger.log(requestInfo); + return Promise.resolve(); + }, + didEncounterErrors: () => { + this.metrics.gqlError(1, { operation }); + const costInMilliseconds = timer() * 1000; + res.setHeader( + 'Server-Timing', + `gql;dur=${costInMilliseconds};desc="GraphQL ${operation}"` + ); + this.logger.error(`${requestInfo}, query: ${reqContext.request.query}`); + return Promise.resolve(); + }, + }); + } +} diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 2c506a5795..487580916f 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,6 +1,22 @@ /// +import { MetricExporter } from '@google-cloud/opentelemetry-cloud-monitoring-exporter'; +import { TraceExporter } from '@google-cloud/opentelemetry-cloud-trace-exporter'; import { NestFactory } from '@nestjs/core'; import type { NestExpressApplication } from '@nestjs/platform-express'; +import { + CompositePropagator, + W3CBaggagePropagator, + W3CTraceContextPropagator, +} from '@opentelemetry/core'; +import gql from '@opentelemetry/instrumentation-graphql'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import ioredis from '@opentelemetry/instrumentation-ioredis'; +import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; +import socketIO from '@opentelemetry/instrumentation-socket.io'; +import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'; +import { NodeSDK } from '@opentelemetry/sdk-node'; +import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node'; +import { PrismaInstrumentation } from '@prisma/instrumentation'; import cookieParser from 'cookie-parser'; import { static as staticMiddleware } from 'express'; // @ts-expect-error graphql-upload is not typed @@ -8,19 +24,47 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import { AppModule } from './app'; import { Config } from './config'; +import { serverTimingAndCache } from './middleware/timing'; +import { RedisIoAdapter } from './modules/sync/redis-adapter'; + +const { NODE_ENV } = process.env; + +if (NODE_ENV === 'production') { + const traceExporter = new TraceExporter(); + const tracing = new NodeSDK({ + traceExporter, + metricReader: new PeriodicExportingMetricReader({ + exporter: new MetricExporter(), + }), + spanProcessor: new BatchSpanProcessor(traceExporter), + textMapPropagator: new CompositePropagator({ + propagators: [ + new W3CBaggagePropagator(), + new W3CTraceContextPropagator(), + ], + }), + instrumentations: [ + new NestInstrumentation(), + new ioredis.IORedisInstrumentation(), + new socketIO.SocketIoInstrumentation({ traceReserved: true }), + new gql.GraphQLInstrumentation({ mergeItems: true }), + new HttpInstrumentation(), + new PrismaInstrumentation(), + ], + serviceName: 'affine-cloud', + }); + + tracing.start(); +} const app = await NestFactory.create(AppModule, { - cors: { - origin: - process.env.AFFINE_ENV === 'preview' - ? ['https://affine-preview.vercel.app'] - : ['http://localhost:8080'], - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['x-operation-name', 'x-definition-name'], - }, + cors: true, bodyParser: true, + logger: NODE_ENV === 'production' ? ['log'] : ['verbose'], }); +app.use(serverTimingAndCache); + app.use( graphqlUploadExpress({ maxFileSize: 10 * 1024 * 1024, @@ -39,6 +83,18 @@ if (!config.objectStorage.r2.enabled) { app.use('/assets', staticMiddleware(config.objectStorage.fs.path)); } +if (config.redis.enabled) { + const redisIoAdapter = new RedisIoAdapter(app); + await redisIoAdapter.connectToRedis( + config.redis.host, + config.redis.port, + config.redis.username, + config.redis.password, + config.redis.database + ); + app.useWebSocketAdapter(redisIoAdapter); +} + await app.listen(port, host); console.log(`Listening on http://${host}:${port}`); diff --git a/apps/server/src/metrics/controller.ts b/apps/server/src/metrics/controller.ts new file mode 100644 index 0000000000..c130cf9005 --- /dev/null +++ b/apps/server/src/metrics/controller.ts @@ -0,0 +1,18 @@ +import { Controller, Get, Res } from '@nestjs/common'; +import type { Response } from 'express'; +import { register } from 'prom-client'; + +import { PrismaService } from '../prisma'; + +@Controller() +export class MetricsController { + constructor(private readonly prisma: PrismaService) {} + + @Get('/metrics') + async index(@Res() res: Response): Promise { + res.header('Content-Type', register.contentType); + const prismaMetrics = await this.prisma.$metrics.prometheus(); + const appMetrics = await register.metrics(); + res.send(appMetrics + prismaMetrics); + } +} diff --git a/apps/server/src/metrics/index.ts b/apps/server/src/metrics/index.ts new file mode 100644 index 0000000000..7828b3b980 --- /dev/null +++ b/apps/server/src/metrics/index.ts @@ -0,0 +1,12 @@ +import { Global, Module } from '@nestjs/common'; + +import { MetricsController } from '../metrics/controller'; +import { Metrics } from './metrics'; + +@Global() +@Module({ + providers: [Metrics], + exports: [Metrics], + controllers: [MetricsController], +}) +export class MetricsModule {} diff --git a/apps/server/src/metrics/metrics.ts b/apps/server/src/metrics/metrics.ts new file mode 100644 index 0000000000..1de9a1833a --- /dev/null +++ b/apps/server/src/metrics/metrics.ts @@ -0,0 +1,25 @@ +import { Injectable, OnModuleDestroy } from '@nestjs/common'; +import { register } from 'prom-client'; + +import { metricsCreator } from './utils'; + +@Injectable() +export class Metrics implements OnModuleDestroy { + onModuleDestroy(): void { + register.clear(); + } + + socketIOEventCounter = metricsCreator.counter('socket_io_counter', ['event']); + socketIOEventTimer = metricsCreator.timer('socket_io_timer', ['event']); + socketIOConnectionGauge = metricsCreator.gauge( + 'socket_io_connection_counter' + ); + + gqlRequest = metricsCreator.counter('gql_request', ['operation']); + gqlError = metricsCreator.counter('gql_error', ['operation']); + gqlTimer = metricsCreator.timer('gql_timer', ['operation']); + + jwstCodecMerge = metricsCreator.counter('jwst_codec_merge'); + jwstCodecDidnotMatch = metricsCreator.counter('jwst_codec_didnot_match'); + jwstCodecFail = metricsCreator.counter('jwst_codec_fail'); +} diff --git a/apps/server/src/metrics/utils.ts b/apps/server/src/metrics/utils.ts new file mode 100644 index 0000000000..f2fb599e0f --- /dev/null +++ b/apps/server/src/metrics/utils.ts @@ -0,0 +1,73 @@ +import { Counter, Gauge, Summary } from 'prom-client'; + +type LabelValues = Partial>; +type MetricsCreator = ( + value: number, + labels: LabelValues +) => void; +type TimerMetricsCreator = ( + labels: LabelValues +) => () => number; + +export const metricsCreatorGenerator = () => { + const counterCreator = ( + name: string, + labelNames?: T[] + ): MetricsCreator => { + const counter = new Counter({ + name, + help: name, + ...(labelNames ? { labelNames } : {}), + }); + + return (value: number, labels: LabelValues) => { + counter.inc(labels, value); + }; + }; + + const gaugeCreator = ( + name: string, + labelNames?: T[] + ): MetricsCreator => { + const gauge = new Gauge({ + name, + help: name, + ...(labelNames ? { labelNames } : {}), + }); + + return (value: number, labels: LabelValues) => { + gauge.set(labels, value); + }; + }; + + const timerCreator = ( + name: string, + labelNames?: T[] + ): TimerMetricsCreator => { + const summary = new Summary({ + name, + help: name, + ...(labelNames ? { labelNames } : {}), + }); + + return (labels: LabelValues) => { + const now = process.hrtime(); + + return () => { + const delta = process.hrtime(now); + const value = delta[0] + delta[1] / 1e9; + + summary.observe(labels, value); + return value; + }; + }; + }; + + return { + counter: counterCreator, + gauge: gaugeCreator, + timer: timerCreator, + }; +}; + +export const metricsCreator = metricsCreatorGenerator(); diff --git a/apps/server/src/middleware/timing.ts b/apps/server/src/middleware/timing.ts new file mode 100644 index 0000000000..b0f3eaffd7 --- /dev/null +++ b/apps/server/src/middleware/timing.ts @@ -0,0 +1,27 @@ +import { NextFunction, Request, Response } from 'express'; +import onHeaders from 'on-headers'; + +export const serverTimingAndCache = ( + req: Request, + res: Response, + next: NextFunction +) => { + req.res = res; + const now = process.hrtime(); + + onHeaders(res, () => { + const delta = process.hrtime(now); + const costInMilliseconds = (delta[0] + delta[1] / 1e9) * 1000; + + const serverTiming = res.getHeader('Server-Timing') as string | undefined; + const serverTimingValue = `${ + serverTiming ? `${serverTiming}, ` : '' + }total;dur=${costInMilliseconds}`; + + res.setHeader('Server-Timing', serverTimingValue); + }); + + res.setHeader('Cache-Control', 'max-age=0, private, must-revalidate'); + + next(); +}; diff --git a/apps/server/src/modules/auth/guard.ts b/apps/server/src/modules/auth/guard.ts index 2082c8e34f..8abb414010 100644 --- a/apps/server/src/modules/auth/guard.ts +++ b/apps/server/src/modules/auth/guard.ts @@ -1,8 +1,18 @@ import type { CanActivate, ExecutionContext } from '@nestjs/common'; -import { createParamDecorator, Injectable, UseGuards } from '@nestjs/common'; +import { + createParamDecorator, + Inject, + Injectable, + SetMetadata, + UseGuards, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import type { NextAuthOptions } from 'next-auth'; +import { AuthHandler } from 'next-auth/core'; import { PrismaService } from '../../prisma'; import { getRequestResponseFromContext } from '../../utils/nestjs'; +import { NextAuthOptionsProvide } from './next-auth-options'; import { AuthService } from './service'; export function getUserFromContext(context: ExecutionContext) { @@ -42,26 +52,71 @@ export const CurrentUser = createParamDecorator( @Injectable() class AuthGuard implements CanActivate { constructor( + @Inject(NextAuthOptionsProvide) + private readonly nextAuthOptions: NextAuthOptions, private auth: AuthService, - private prisma: PrismaService + private prisma: PrismaService, + private readonly reflector: Reflector ) {} async canActivate(context: ExecutionContext) { - const { req } = getRequestResponseFromContext(context); + const { req, res } = getRequestResponseFromContext(context); const token = req.headers.authorization; - if (!token) { - return false; - } - const [type, jwt] = token.split(' ') ?? []; - if (type === 'Bearer') { - const claims = await this.auth.verify(jwt); - req.user = await this.prisma.user.findUnique({ - where: { id: claims.id }, + // api is public + const isPublic = this.reflector.get( + 'isPublic', + context.getHandler() + ); + // api can be public, but if user is logged in, we can get user info + const isPublicable = this.reflector.get( + 'isPublicable', + context.getHandler() + ); + + if (isPublic) { + return true; + } else if (!token) { + const session = await AuthHandler({ + req: { + cookies: req.cookies, + action: 'session', + method: 'GET', + headers: req.headers, + }, + options: this.nextAuthOptions, }); - return !!req.user; - } + const { body = {}, cookies, status = 200 } = session; + if (!body && !isPublicable) { + return false; + } + + // @ts-expect-error body is user here + req.user = body.user; + if (cookies && res) { + for (const cookie of cookies) { + res.cookie(cookie.name, cookie.value, cookie.options); + } + } + + return Boolean( + status === 200 && + typeof body !== 'string' && + // ignore body if api is publicable + (Object.keys(body).length || isPublicable) + ); + } else { + const [type, jwt] = token.split(' ') ?? []; + + if (type === 'Bearer') { + const claims = await this.auth.verify(jwt); + req.user = await this.prisma.user.findUnique({ + where: { id: claims.id }, + }); + return !!req.user; + } + } return false; } } @@ -85,3 +140,8 @@ class AuthGuard implements CanActivate { export const Auth = () => { return UseGuards(AuthGuard); }; + +// api is public accessible +export const Public = () => SetMetadata('isPublic', true); +// api is public accessible, but if user is logged in, we can get user info +export const Publicable = () => SetMetadata('isPublicable', true); diff --git a/apps/server/src/modules/auth/index.ts b/apps/server/src/modules/auth/index.ts index df99537183..f8c284d870 100644 --- a/apps/server/src/modules/auth/index.ts +++ b/apps/server/src/modules/auth/index.ts @@ -1,13 +1,21 @@ import { Global, Module } from '@nestjs/common'; +import { MAILER, MailService } from './mailer'; import { NextAuthController } from './next-auth.controller'; +import { NextAuthOptionsProvider } from './next-auth-options'; import { AuthResolver } from './resolver'; import { AuthService } from './service'; @Global() @Module({ - providers: [AuthService, AuthResolver], - exports: [AuthService], + providers: [ + AuthService, + AuthResolver, + NextAuthOptionsProvider, + MAILER, + MailService, + ], + exports: [AuthService, NextAuthOptionsProvider, MailService], controllers: [NextAuthController], }) export class AuthModule {} diff --git a/apps/server/src/modules/auth/mailer/index.ts b/apps/server/src/modules/auth/mailer/index.ts new file mode 100644 index 0000000000..9c53bf57ca --- /dev/null +++ b/apps/server/src/modules/auth/mailer/index.ts @@ -0,0 +1,2 @@ +export { MailService } from './mail.service'; +export { MAILER } from './mailer'; diff --git a/apps/server/src/modules/auth/mailer/mail.service.ts b/apps/server/src/modules/auth/mailer/mail.service.ts new file mode 100644 index 0000000000..5c9505d364 --- /dev/null +++ b/apps/server/src/modules/auth/mailer/mail.service.ts @@ -0,0 +1,130 @@ +import { Inject, Injectable } from '@nestjs/common'; + +import { Config } from '../../../config'; +import { + MAILER_SERVICE, + type MailerService, + type Options, + type Response, +} from './mailer'; +import { emailTemplate } from './template'; +@Injectable() +export class MailService { + constructor( + @Inject(MAILER_SERVICE) private readonly mailer: MailerService, + private readonly config: Config + ) {} + + async sendMail(options: Options): Promise { + return this.mailer.sendMail(options); + } + + hasConfigured() { + return ( + !!this.config.auth.email.login && + !!this.config.auth.email.password && + !!this.config.auth.email.sender + ); + } + + async sendInviteEmail( + to: string, + inviteId: string, + invitationInfo: { + workspace: { + id: string; + name: string; + avatar: string; + }; + user: { + avatar: string; + name: string; + }; + } + ) { + console.log('invitationInfo', invitationInfo); + + const buttonUrl = `${this.config.baseUrl}/invite/${inviteId}`; + const workspaceAvatar = invitationInfo.workspace.avatar; + + const content = ` + ${invitationInfo.user.name} + invited you to join + + ${invitationInfo.workspace.name}`; + + const html = emailTemplate({ + title: 'You are invited!', + content, + buttonContent: 'Accept & Join', + buttonUrl, + }); + + return this.sendMail({ + from: this.config.auth.email.sender, + to, + subject: `Invitation to workspace`, + html, + attachments: [ + { + cid: 'workspaceAvatar', + filename: 'image.png', + content: workspaceAvatar, + encoding: 'base64', + }, + ], + }); + } + async sendChangePasswordEmail(to: string, url: string) { + const html = ` +

Change password

+

Click button to open change password page

+ ${url} + `; + return this.sendMail({ + from: this.config.auth.email.sender, + to, + subject: `Change password`, + html, + }); + } + + async sendSetPasswordEmail(to: string, url: string) { + const html = ` +

Set password

+

Click button to open set password page

+ ${url} + `; + return this.sendMail({ + from: this.config.auth.email.sender, + to, + subject: `Change password`, + html, + }); + } + async sendChangeEmail(to: string, url: string) { + const html = ` +

Change Email

+

Click button to open change email page

+ ${url} + `; + return this.sendMail({ + from: this.config.auth.email.sender, + to, + subject: `Change password`, + html, + }); + } +} diff --git a/apps/server/src/modules/auth/mailer/mailer.ts b/apps/server/src/modules/auth/mailer/mailer.ts new file mode 100644 index 0000000000..6df367f5e6 --- /dev/null +++ b/apps/server/src/modules/auth/mailer/mailer.ts @@ -0,0 +1,27 @@ +import { FactoryProvider } from '@nestjs/common'; +import { createTransport, Transporter } from 'nodemailer'; +import SMTPTransport from 'nodemailer/lib/smtp-transport'; + +import { Config } from '../../../config'; + +export const MAILER_SERVICE = Symbol('MAILER_SERVICE'); + +export type MailerService = Transporter; +export type Response = SMTPTransport.SentMessageInfo; +export type Options = SMTPTransport.Options; + +export const MAILER: FactoryProvider< + Transporter +> = { + provide: MAILER_SERVICE, + useFactory: (config: Config) => { + return createTransport({ + service: 'gmail', + auth: { + user: config.auth.email.login, + pass: config.auth.email.password, + }, + }); + }, + inject: [Config], +}; diff --git a/apps/server/src/modules/auth/mailer/template.ts b/apps/server/src/modules/auth/mailer/template.ts new file mode 100644 index 0000000000..115ac2ff70 --- /dev/null +++ b/apps/server/src/modules/auth/mailer/template.ts @@ -0,0 +1,195 @@ +export const emailTemplate = ({ + title, + content, + buttonContent, + buttonUrl, +}: { + title: string; + content: string; + buttonContent: string; + buttonUrl: string; +}) => { + return ` + + + + + + + + + + + + + +
+ + AFFiNE log + +
${title}
${content}
+ + + + +
+ ${buttonContent} +
+
+ + + + + + + + + + +
+ + + + + + + + + +
+ AFFiNE github link + + + AFFiNE twitter link + + + AFFiNE discord link + + AFFiNE youtube link + + AFFiNE telegram link + + AFFiNE reddit link +
+
+ One hyper-fused platform for wildly creative minds +
+ Copyrightcopyright2023 Toeverything +
+ `; +}; diff --git a/apps/server/src/modules/auth/next-auth-options.ts b/apps/server/src/modules/auth/next-auth-options.ts new file mode 100644 index 0000000000..051b1f2ce2 --- /dev/null +++ b/apps/server/src/modules/auth/next-auth-options.ts @@ -0,0 +1,501 @@ +import { randomUUID } from 'node:crypto'; + +import { PrismaAdapter } from '@auth/prisma-adapter'; +import { BadRequestException, FactoryProvider, Logger } from '@nestjs/common'; +import { verify } from '@node-rs/argon2'; +import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken'; +import { NextAuthOptions } from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; +import Email, { + type SendVerificationRequestParams, +} from 'next-auth/providers/email'; +import Github from 'next-auth/providers/github'; +import Google from 'next-auth/providers/google'; + +import { Config } from '../../config'; +import { PrismaService } from '../../prisma'; +import { NewFeaturesKind } from '../users/types'; +import { MailService } from './mailer'; +import { getUtcTimestamp, UserClaim } from './service'; + +export const NextAuthOptionsProvide = Symbol('NextAuthOptions'); + +function getSchemaFromCallbackUrl(origin: string, callbackUrl: string) { + const { searchParams } = new URL(callbackUrl, origin); + return searchParams.has('schema') ? searchParams.get('schema') : null; +} + +function wrapUrlWithSchema(url: string, schema: string | null) { + if (schema) { + return `${schema}://open-url?${url}`; + } + return url; +} + +export const NextAuthOptionsProvider: FactoryProvider = { + provide: NextAuthOptionsProvide, + useFactory(config: Config, prisma: PrismaService, mailer: MailService) { + const logger = new Logger('NextAuth'); + const prismaAdapter = PrismaAdapter(prisma); + // createUser exists in the adapter + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const createUser = prismaAdapter.createUser!.bind(prismaAdapter); + prismaAdapter.createUser = async data => { + const userData = { + name: data.name, + email: data.email, + avatarUrl: '', + emailVerified: data.emailVerified, + }; + if (data.email && !data.name) { + userData.name = data.email.split('@')[0]; + } + if (data.image) { + userData.avatarUrl = data.image; + } + return createUser(userData); + }; + // getUser exists in the adapter + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const getUser = prismaAdapter.getUser!.bind(prismaAdapter)!; + prismaAdapter.getUser = async id => { + const result = await getUser(id); + if (result) { + // @ts-expect-error Third part library type mismatch + result.image = result.avatarUrl; + // @ts-expect-error Third part library type mismatch + result.hasPassword = Boolean(result.password); + } + return result; + }; + const nextAuthOptions: NextAuthOptions = { + providers: [ + // @ts-expect-error esm interop issue + Email.default({ + server: { + host: config.auth.email.server, + port: config.auth.email.port, + auth: { + user: config.auth.email.login, + pass: config.auth.email.password, + }, + }, + from: config.auth.email.sender, + async sendVerificationRequest(params: SendVerificationRequestParams) { + const { identifier, url, provider } = params; + const { host, searchParams, origin } = new URL(url); + const callbackUrl = searchParams.get('callbackUrl') || ''; + if (!callbackUrl) { + throw new Error('callbackUrl is not set'); + } + const schema = getSchemaFromCallbackUrl(origin, callbackUrl); + const wrappedUrl = wrapUrlWithSchema(url, schema); + // hack: check if link is opened via desktop + const result = await mailer.sendMail({ + to: identifier, + from: provider.from, + subject: `Sign in to ${host}`, + text: text({ url: wrappedUrl, host }), + html: html({ url: wrappedUrl, host }), + }); + logger.log( + `send verification email success: ${result.accepted.join(', ')}` + ); + const failed = result.rejected + .concat(result.pending) + .filter(Boolean); + if (failed.length) { + throw new Error(`Email (${failed.join(', ')}) could not be sent`); + } + }, + }), + ], + // @ts-expect-error Third part library type mismatch + adapter: prismaAdapter, + debug: !config.node.prod, + session: { + strategy: config.node.prod ? 'database' : 'jwt', + }, + // @ts-expect-error Third part library type mismatch + logger: console, + }; + + nextAuthOptions.providers.push( + // @ts-expect-error esm interop issue + Credentials.default({ + name: 'Password', + credentials: { + email: { + label: 'Email', + type: 'text', + placeholder: 'torvalds@osdl.org', + }, + password: { label: 'Password', type: 'password' }, + }, + async authorize( + credentials: + | Record<'email' | 'password' | 'hashedPassword', string> + | undefined + ) { + if (!credentials) { + return null; + } + const { password, hashedPassword } = credentials; + if (!password || !hashedPassword) { + return null; + } + if (!(await verify(hashedPassword, password))) { + return null; + } + return credentials; + }, + }) + ); + + if (config.auth.oauthProviders.github) { + nextAuthOptions.providers.push( + // @ts-expect-error esm interop issue + Github.default({ + clientId: config.auth.oauthProviders.github.clientId, + clientSecret: config.auth.oauthProviders.github.clientSecret, + allowDangerousEmailAccountLinking: true, + }) + ); + } + + if (config.auth.oauthProviders.google) { + nextAuthOptions.providers.push( + // @ts-expect-error esm interop issue + Google.default({ + clientId: config.auth.oauthProviders.google.clientId, + clientSecret: config.auth.oauthProviders.google.clientSecret, + checks: 'nonce', + allowDangerousEmailAccountLinking: true, + }) + ); + } + + nextAuthOptions.jwt = { + encode: async ({ token, maxAge }) => { + if (!token?.email) { + throw new BadRequestException('Missing email in jwt token'); + } + const user = await prisma.user.findFirstOrThrow({ + where: { + email: token.email, + }, + }); + const now = getUtcTimestamp(); + return sign( + { + data: { + id: user.id, + name: user.name, + email: user.email, + emailVerified: user.emailVerified?.toISOString(), + picture: user.avatarUrl, + createdAt: user.createdAt.toISOString(), + hasPassword: Boolean(user.password), + }, + iat: now, + exp: now + (maxAge ?? config.auth.accessTokenExpiresIn), + iss: config.serverId, + sub: user.id, + aud: user.name, + jti: randomUUID({ + disableEntropyCache: true, + }), + }, + config.auth.privateKey, + { + algorithm: Algorithm.ES256, + } + ); + }, + decode: async ({ token }) => { + if (!token) { + return null; + } + const { name, email, emailVerified, id, picture, hasPassword } = ( + await jwtVerify(token, config.auth.publicKey, { + algorithms: [Algorithm.ES256], + iss: [config.serverId], + leeway: config.auth.leeway, + requiredSpecClaims: ['exp', 'iat', 'iss', 'sub'], + }) + ).data as Omit & { + picture: string | undefined; + }; + return { + name, + email, + emailVerified, + picture, + sub: id, + id, + hasPassword, + }; + }, + }; + nextAuthOptions.secret ??= config.auth.nextAuthSecret; + + nextAuthOptions.callbacks = { + session: async ({ session, user, token }) => { + if (session.user) { + if (user) { + // @ts-expect-error Third part library type mismatch + session.user.id = user.id; + // @ts-expect-error Third part library type mismatch + session.user.image = user.image ?? user.avatarUrl; + // @ts-expect-error Third part library type mismatch + session.user.emailVerified = user.emailVerified; + // @ts-expect-error Third part library type mismatch + session.user.hasPassword = Boolean(user.password); + } else { + // technically the sub should be the same as id + // @ts-expect-error Third part library type mismatch + session.user.id = token.sub; + // @ts-expect-error Third part library type mismatch + session.user.emailVerified = token.emailVerified; + // @ts-expect-error Third part library type mismatch + session.user.hasPassword = token.hasPassword; + } + if (token && token.picture) { + session.user.image = token.picture; + } + } + return session; + }, + signIn: async ({ profile }) => { + if (!config.affine.beta || !config.node.prod) { + return true; + } + if (profile?.email) { + return await prisma.newFeaturesWaitingList + .findUnique({ + where: { + email: profile.email, + type: NewFeaturesKind.EarlyAccess, + }, + }) + .then(user => !!user) + .catch(() => false); + } + return false; + }, + redirect({ url }) { + return url; + }, + }; + return nextAuthOptions; + }, + inject: [Config, PrismaService, MailService], +}; + +/** + * Email HTML body + * Insert invisible space into domains from being turned into a hyperlink by email + * clients like Outlook and Apple mail, as this is confusing because it seems + * like they are supposed to click on it to sign in. + * + * @note We don't add the email address to avoid needing to escape it, if you do, remember to sanitize it! + */ +function html(params: { url: string; host: string }) { + const { url } = params; + + return ` + + + + + + + + + + + + + + +
+ + AFFiNE log + +
+ Verify your new email for AFFiNE +
+ You recently requested to change the email address associated with your + AFFiNe account. To complete this process, please click on the + verification link below. +
+ + + + +
+ Verify your new email address +
+
+ + + + + + + + + + +
+ + + + + + + + + +
+ AFFiNE github link + + + AFFiNE twitter link + + + AFFiNE discord link + + AFFiNE youtube link + + AFFiNE telegram link + + AFFiNE reddit link +
+
+ One hyper-fused platform for wildly creative minds +
+ Copyrightcopyright2023 Toeverything +
+ + +`; +} + +/** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */ +function text({ url, host }: { url: string; host: string }) { + return `Sign in to ${host}\n${url}\n\n`; +} diff --git a/apps/server/src/modules/auth/next-auth.controller.ts b/apps/server/src/modules/auth/next-auth.controller.ts index 70c2ca624b..51ec0727fe 100644 --- a/apps/server/src/modules/auth/next-auth.controller.ts +++ b/apps/server/src/modules/auth/next-auth.controller.ts @@ -1,141 +1,41 @@ -import { randomUUID } from 'node:crypto'; - -import { PrismaAdapter } from '@auth/prisma-adapter'; import { All, BadRequestException, Controller, + Inject, Next, + NotFoundException, Query, Req, Res, } from '@nestjs/common'; -import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken'; +import { hash, verify } from '@node-rs/argon2'; +import type { User } from '@prisma/client'; import type { NextFunction, Request, Response } from 'express'; -import type { AuthAction, AuthOptions } from 'next-auth'; +import { pick } from 'lodash-es'; +import type { AuthAction, NextAuthOptions } from 'next-auth'; import { AuthHandler } from 'next-auth/core'; -import Email from 'next-auth/providers/email'; -import Github from 'next-auth/providers/github'; -import Google from 'next-auth/providers/google'; import { Config } from '../../config'; import { PrismaService } from '../../prisma/service'; -import { getUtcTimestamp, type UserClaim } from './service'; +import { NextAuthOptionsProvide } from './next-auth-options'; +import { AuthService } from './service'; const BASE_URL = '/api/auth/'; @Controller(BASE_URL) export class NextAuthController { - private readonly nextAuthOptions: AuthOptions; + private readonly callbackSession; constructor( readonly config: Config, - readonly prisma: PrismaService + readonly prisma: PrismaService, + private readonly authService: AuthService, + @Inject(NextAuthOptionsProvide) + private readonly nextAuthOptions: NextAuthOptions ) { - const prismaAdapter = PrismaAdapter(prisma); - // createUser exists in the adapter // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const createUser = prismaAdapter.createUser!.bind(prismaAdapter); - prismaAdapter.createUser = async data => { - if (data.email && !data.name) { - data.name = data.email.split('@')[0]; - } - return createUser(data); - }; - this.nextAuthOptions = { - providers: [ - // @ts-expect-error esm interop issue - Email.default({ - server: { - host: config.auth.email.server, - port: config.auth.email.port, - auth: { - user: config.auth.email.sender, - pass: config.auth.email.password, - }, - }, - from: `AFFiNE `, - }), - ], - // @ts-expect-error Third part library type mismatch - adapter: prismaAdapter, - debug: !config.prod, - }; - - if (config.auth.oauthProviders.github) { - this.nextAuthOptions.providers.push( - // @ts-expect-error esm interop issue - Github.default({ - clientId: config.auth.oauthProviders.github.clientId, - clientSecret: config.auth.oauthProviders.github.clientSecret, - }) - ); - } - - if (config.auth.oauthProviders.google) { - this.nextAuthOptions.providers.push( - // @ts-expect-error esm interop issue - Google.default({ - clientId: config.auth.oauthProviders.google.clientId, - clientSecret: config.auth.oauthProviders.google.clientSecret, - }) - ); - } - - this.nextAuthOptions.jwt = { - encode: async ({ token, maxAge }) => { - if (!token?.email) { - throw new BadRequestException('Missing email in jwt token'); - } - const user = await this.prisma.user.findFirstOrThrow({ - where: { - email: token.email, - }, - }); - const now = getUtcTimestamp(); - return sign( - { - data: { - id: user.id, - name: user.name, - email: user.email, - createdAt: user.createdAt.toISOString(), - }, - iat: now, - exp: now + (maxAge ?? config.auth.accessTokenExpiresIn), - iss: this.config.serverId, - sub: user.id, - aud: user.name, - jti: randomUUID({ - disableEntropyCache: true, - }), - }, - this.config.auth.privateKey, - { - algorithm: Algorithm.ES256, - } - ); - }, - decode: async ({ token }) => { - if (!token) { - return null; - } - const { name, email, id } = ( - await jwtVerify(token, this.config.auth.publicKey, { - algorithms: [Algorithm.ES256], - iss: [this.config.serverId], - leeway: this.config.auth.leeway, - requiredSpecClaims: ['exp', 'iat', 'iss', 'sub'], - }) - ).data as UserClaim; - return { - name, - email, - sub: id, - }; - }, - }; - this.nextAuthOptions.secret ??= config.auth.nextAuthSecret; + this.callbackSession = nextAuthOptions.callbacks!.session; } @All('*') @@ -145,25 +45,69 @@ export class NextAuthController { @Query() query: Record, @Next() next: NextFunction ) { - const nextauth = req.url // start with request url + const [action, providerId] = req.url // start with request url .slice(BASE_URL.length) // make relative to baseUrl .replace(/\?.*/, '') // remove query part, use only path part - .split('/') as AuthAction[]; // as array of strings; + .split('/') as [AuthAction, string]; // as array of strings; + if (providerId === 'credentials') { + const { email } = req.body; + if (email) { + const user = await this.prisma.user.findFirst({ + where: { + email, + }, + }); + if (!user) { + req.statusCode = 401; + req.statusMessage = 'User not found'; + req.body = null; + throw new NotFoundException(`User not found`); + } else { + req.body = { + ...req.body, + name: user.name, + email: user.email, + image: user.avatarUrl, + hashedPassword: user.password, + }; + } + } + } + const options = this.nextAuthOptions; + if (req.method === 'POST' && action === 'session') { + if (typeof req.body !== 'object' || typeof req.body.data !== 'object') { + throw new BadRequestException(`Invalid new session data`); + } + const user = await this.updateSession(req, req.body.data); + // callbacks.session existed + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + options.callbacks!.session = ({ session }) => { + return { + user: { + ...pick(user, 'id', 'name', 'email'), + image: user.avatarUrl, + hasPassword: !!user.password, + }, + expires: session.expires, + }; + }; + } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + options.callbacks!.session = this.callbackSession; + } const { status, headers, body, redirect, cookies } = await AuthHandler({ req: { body: req.body, query: query, method: req.method, - action: nextauth[0], - providerId: nextauth[1], - error: query.error ?? nextauth[1], + action, + providerId, + error: query.error ?? providerId, cookies: req.cookies, }, - options: this.nextAuthOptions, + options, }); - if (status) { - res.status(status); - } + if (headers) { for (const { key, value } of headers) { res.setHeader(key, value); @@ -174,8 +118,32 @@ export class NextAuthController { res.cookie(cookie.name, cookie.value, cookie.options); } } + + if (redirect?.endsWith('api/auth/error?error=AccessDenied')) { + res.redirect('https://community.affine.pro/c/insider-general/'); + return; + } + + if (status) { + res.status(status); + } + if (redirect) { - res.redirect(redirect); + console.log(providerId, action, req.headers); + if (providerId === 'credentials') { + res.send(JSON.stringify({ ok: true, url: redirect })); + } else if ( + action === 'callback' || + action === 'error' || + (providerId !== 'credentials' && + // login in the next-auth page, /api/auth/signin, auto redirect. + // otherwise, return the json value to allow frontend to handle the redirect. + req.headers?.referer?.includes?.('/api/auth/signin')) + ) { + res.redirect(redirect); + } else { + res.json({ url: redirect }); + } } else if (typeof body === 'string') { res.send(body); } else if (body && typeof body === 'object') { @@ -184,4 +152,91 @@ export class NextAuthController { next(); } } + + private async updateSession( + req: Request, + newSession: Partial> & { oldPassword?: string } + ): Promise { + const { name, email, password, oldPassword } = newSession; + if (!name && !email && !password) { + throw new BadRequestException(`Invalid new session data`); + } + if (password) { + const user = await this.getUserFromRequest(req); + const { password: userPassword } = user; + if (!oldPassword) { + if (userPassword) { + throw new BadRequestException( + `Old password is required to update password` + ); + } + } else { + if (!userPassword) { + throw new BadRequestException(`No existed password`); + } + if (await verify(userPassword, oldPassword)) { + await this.prisma.user.update({ + where: { + id: user.id, + }, + data: { + ...pick(newSession, 'email', 'name'), + password: await hash(password), + }, + }); + } + } + return user; + } else { + const user = await this.getUserFromRequest(req); + return this.prisma.user.update({ + where: { + id: user.id, + }, + data: pick(newSession, 'name', 'email'), + }); + } + } + + private async getUserFromRequest(req: Request): Promise { + const token = req.headers.authorization; + if (!token) { + const session = await AuthHandler({ + req: { + cookies: req.cookies, + action: 'session', + method: 'GET', + headers: req.headers, + }, + options: this.nextAuthOptions, + }); + + const { body } = session; + // @ts-expect-error check if body.user exists + if (body && body.user && body.user.id) { + const user = await this.prisma.user.findUnique({ + where: { + // @ts-expect-error body.user.id exists + id: body.user.id, + }, + }); + if (user) { + return user; + } + } + } else { + const [type, jwt] = token.split(' ') ?? []; + + if (type === 'Bearer') { + const claims = await this.authService.verify(jwt); + const user = await this.prisma.user.findUnique({ + where: { id: claims.id }, + }); + if (user) { + return user; + } + } + } + throw new BadRequestException(`User not found`); + } } diff --git a/apps/server/src/modules/auth/resolver.ts b/apps/server/src/modules/auth/resolver.ts index 009aa599c7..50a7b1e21b 100644 --- a/apps/server/src/modules/auth/resolver.ts +++ b/apps/server/src/modules/auth/resolver.ts @@ -11,6 +11,7 @@ import { } from '@nestjs/graphql'; import type { Request } from 'express'; +import { Config } from '../../config'; import { UserType } from '../users/resolver'; import { CurrentUser } from './guard'; import { AuthService } from './service'; @@ -26,7 +27,10 @@ export class TokenType { @Resolver(() => UserType) export class AuthResolver { - constructor(private auth: AuthService) {} + constructor( + private readonly config: Config, + private auth: AuthService + ) {} @ResolveField(() => TokenType) token(@CurrentUser() currentUser: UserType, @Parent() user: UserType) { @@ -41,13 +45,13 @@ export class AuthResolver { } @Mutation(() => UserType) - async register( + async signUp( @Context() ctx: { req: Request }, @Args('name') name: string, @Args('email') email: string, @Args('password') password: string ) { - const user = await this.auth.register(name, email, password); + const user = await this.auth.signUp(name, email, password); ctx.req.user = user; return user; } @@ -62,4 +66,56 @@ export class AuthResolver { ctx.req.user = user; return user; } + + @Mutation(() => UserType) + async changePassword( + @Context() ctx: { req: Request }, + @Args('id') id: string, + @Args('newPassword') newPassword: string + ) { + const user = await this.auth.changePassword(id, newPassword); + ctx.req.user = user; + return user; + } + + @Mutation(() => UserType) + async changeEmail( + @Context() ctx: { req: Request }, + @Args('id') id: string, + @Args('email') email: string + ) { + const user = await this.auth.changeEmail(id, email); + ctx.req.user = user; + return user; + } + + @Mutation(() => Boolean) + async sendChangePasswordEmail( + @Args('email') email: string, + @Args('callbackUrl') callbackUrl: string + ) { + const url = `${this.config.baseUrl}${callbackUrl}`; + const res = await this.auth.sendChangePasswordEmail(email, url); + return !res.rejected.length; + } + + @Mutation(() => Boolean) + async sendSetPasswordEmail( + @Args('email') email: string, + @Args('callbackUrl') callbackUrl: string + ) { + const url = `${this.config.baseUrl}${callbackUrl}`; + const res = await this.auth.sendSetPasswordEmail(email, url); + return !res.rejected.length; + } + + @Mutation(() => Boolean) + async sendChangeEmail( + @Args('email') email: string, + @Args('callbackUrl') callbackUrl: string + ) { + const url = `${this.config.baseUrl}${callbackUrl}`; + const res = await this.auth.sendChangeEmail(email, url); + return !res.rejected.length; + } } diff --git a/apps/server/src/modules/auth/service.ts b/apps/server/src/modules/auth/service.ts index c61160d940..7ac7828ddb 100644 --- a/apps/server/src/modules/auth/service.ts +++ b/apps/server/src/modules/auth/service.ts @@ -12,8 +12,14 @@ import type { User } from '@prisma/client'; import { Config } from '../../config'; import { PrismaService } from '../../prisma'; +import { MailService } from './mailer'; -export type UserClaim = Pick; +export type UserClaim = Pick< + User, + 'id' | 'name' | 'email' | 'emailVerified' | 'createdAt' | 'avatarUrl' +> & { + hasPassword?: boolean; +}; export const getUtcTimestamp = () => Math.floor(new Date().getTime() / 1000); @@ -21,7 +27,8 @@ export const getUtcTimestamp = () => Math.floor(new Date().getTime() / 1000); export class AuthService { constructor( private config: Config, - private prisma: PrismaService + private prisma: PrismaService, + private mailer: MailService ) {} sign(user: UserClaim) { @@ -32,6 +39,9 @@ export class AuthService { id: user.id, name: user.name, email: user.email, + emailVerified: user.emailVerified?.toISOString(), + image: user.avatarUrl, + hasPassword: Boolean(user.hasPassword), createdAt: user.createdAt.toISOString(), }, iat: now, @@ -58,6 +68,9 @@ export class AuthService { id: user.id, name: user.name, email: user.email, + emailVerified: user.emailVerified?.toISOString(), + image: user.avatarUrl, + hasPassword: Boolean(user.hasPassword), createdAt: user.createdAt.toISOString(), }, exp: now + this.config.auth.refreshTokenExpiresIn, @@ -78,7 +91,7 @@ export class AuthService { async verify(token: string) { try { - return ( + const data = ( await jwtVerify(token, this.config.auth.publicKey, { algorithms: [Algorithm.ES256], iss: [this.config.serverId], @@ -86,6 +99,12 @@ export class AuthService { requiredSpecClaims: ['exp', 'iat', 'iss', 'sub'], }) ).data as UserClaim; + + return { + ...data, + emailVerified: data.emailVerified ? new Date(data.emailVerified) : null, + createdAt: new Date(data.createdAt), + }; } catch (e) { throw new UnauthorizedException('Invalid token'); } @@ -119,7 +138,7 @@ export class AuthService { return user; } - async register(name: string, email: string, password: string): Promise { + async signUp(name: string, email: string, password: string): Promise { const user = await this.prisma.user.findFirst({ where: { email, @@ -140,4 +159,96 @@ export class AuthService { }, }); } + + async createAnonymousUser(email: string): Promise { + const user = await this.prisma.user.findFirst({ + where: { + email, + }, + }); + + if (user) { + throw new BadRequestException('Email already exists'); + } + + return this.prisma.user.create({ + data: { + name: 'Unnamed', + email, + }, + }); + } + + async getUserByEmail(email: string): Promise { + return this.prisma.user.findUnique({ + where: { + email, + }, + }); + } + + async isUserHasPassword(email: string): Promise { + const user = await this.prisma.user.findFirst({ + where: { + email, + }, + }); + if (!user) { + throw new BadRequestException('Invalid email'); + } + return Boolean(user.password); + } + + async changePassword(id: string, newPassword: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { + id, + }, + }); + + if (!user) { + throw new BadRequestException('Invalid email'); + } + + const hashedPassword = await hash(newPassword); + + return this.prisma.user.update({ + where: { + id, + }, + data: { + password: hashedPassword, + }, + }); + } + async changeEmail(id: string, newEmail: string): Promise { + const user = await this.prisma.user.findUnique({ + where: { + id, + }, + }); + + if (!user) { + throw new BadRequestException('Invalid email'); + } + + return this.prisma.user.update({ + where: { + id, + }, + data: { + email: newEmail, + }, + }); + } + + async sendChangePasswordEmail(email: string, callbackUrl: string) { + return this.mailer.sendChangePasswordEmail(email, callbackUrl); + } + async sendSetPasswordEmail(email: string, callbackUrl: string) { + return this.mailer.sendSetPasswordEmail(email, callbackUrl); + } + async sendChangeEmail(email: string, callbackUrl: string) { + return this.mailer.sendChangeEmail(email, callbackUrl); + } } diff --git a/apps/server/src/modules/doc/index.ts b/apps/server/src/modules/doc/index.ts new file mode 100644 index 0000000000..806d3f157f --- /dev/null +++ b/apps/server/src/modules/doc/index.ts @@ -0,0 +1,42 @@ +import { DynamicModule } from '@nestjs/common'; + +import { DocManager } from './manager'; +import { RedisDocManager } from './redis-manager'; + +export class DocModule { + /** + * @param automation whether enable update merging automation logic + */ + private static defModule(automation = true): DynamicModule { + return { + module: DocModule, + providers: [ + { + provide: 'DOC_MANAGER_AUTOMATION', + useValue: automation, + }, + { + provide: DocManager, + useClass: globalThis.AFFiNE.redis.enabled + ? RedisDocManager + : DocManager, + }, + ], + exports: [DocManager], + }; + } + + static forRoot() { + return this.defModule(); + } + + static forSync(): DynamicModule { + return this.defModule(false); + } + + static forFeature(): DynamicModule { + return this.defModule(false); + } +} + +export { DocManager }; diff --git a/apps/server/src/modules/doc/manager.ts b/apps/server/src/modules/doc/manager.ts new file mode 100644 index 0000000000..68b18ebafd --- /dev/null +++ b/apps/server/src/modules/doc/manager.ts @@ -0,0 +1,351 @@ +import { + Inject, + Injectable, + Logger, + OnModuleDestroy, + OnModuleInit, +} from '@nestjs/common'; +import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs'; + +import { Config } from '../../config'; +import { Metrics } from '../../metrics/metrics'; +import { PrismaService } from '../../prisma'; +import { mergeUpdatesInApplyWay as jwstMergeUpdates } from '../../storage'; + +function compare(yBinary: Buffer, jwstBinary: Buffer, strict = false): boolean { + if (yBinary.equals(jwstBinary)) { + return true; + } + + if (strict) { + return false; + } + + const doc = new Doc(); + applyUpdate(doc, jwstBinary); + + const yBinary2 = Buffer.from(encodeStateAsUpdate(doc)); + + return compare(yBinary, yBinary2, true); +} + +/** + * Since we can't directly save all client updates into database, in which way the database will overload, + * we need to buffer the updates and merge them to reduce db write. + * + * And also, if a new client join, it would be nice to see the latest doc asap, + * so we need to at least store a snapshot of the doc and return quickly, + * along side all the updates that have not been applies to that snapshot(timestamp). + * + * @see [RedisUpdateManager](./redis-manager.ts) - redis backed manager + */ +@Injectable() +export class DocManager implements OnModuleInit, OnModuleDestroy { + protected logger = new Logger(DocManager.name); + private job: NodeJS.Timeout | null = null; + private busy = false; + + constructor( + protected readonly db: PrismaService, + @Inject('DOC_MANAGER_AUTOMATION') + protected readonly automation: boolean, + protected readonly config: Config, + protected readonly metrics: Metrics + ) {} + + onModuleInit() { + if (this.automation) { + this.logger.log('Use Database'); + this.setup(); + } + } + + onModuleDestroy() { + this.destroy(); + } + + protected recoverDoc(...updates: Buffer[]): Doc { + const doc = new Doc(); + + updates.forEach(update => { + applyUpdate(doc, update); + }); + + return doc; + } + + protected yjsMergeUpdates(...updates: Buffer[]): Buffer { + const doc = this.recoverDoc(...updates); + + return Buffer.from(encodeStateAsUpdate(doc)); + } + + protected mergeUpdates(guid: string, ...updates: Buffer[]): Buffer { + const yjsResult = this.yjsMergeUpdates(...updates); + this.metrics.jwstCodecMerge(1, {}); + let log = false; + if (this.config.doc.manager.experimentalMergeWithJwstCodec) { + try { + const jwstResult = jwstMergeUpdates(updates); + if (!compare(yjsResult, jwstResult)) { + this.metrics.jwstCodecDidnotMatch(1, {}); + this.logger.warn( + `jwst codec result doesn't match yjs codec result for: ${guid}` + ); + log = true; + if (this.config.node.dev) { + this.logger.warn(`Expected:\n ${yjsResult.toString('hex')}`); + this.logger.warn(`Result:\n ${jwstResult.toString('hex')}`); + } + } + } catch (e) { + this.metrics.jwstCodecFail(1, {}); + this.logger.warn(`jwst apply update failed for :${guid}`, e); + log = true; + } finally { + if (log) { + this.logger.warn( + 'Updates:', + updates.map(u => u.toString('hex')) + ); + } + } + } + + return yjsResult; + } + + /** + * setup pending update processing loop + */ + setup() { + this.job = setInterval(() => { + if (!this.busy) { + this.busy = true; + this.apply() + .catch(() => { + /* we handle all errors in work itself */ + }) + .finally(() => { + this.busy = false; + }); + } + }, this.config.doc.manager.updatePollInterval); + + this.logger.log('Automation started'); + if (this.config.doc.manager.experimentalMergeWithJwstCodec) { + this.logger.warn( + 'Experimental feature enabled: merge updates with jwst codec is enabled' + ); + } + } + + /** + * stop pending update processing loop + */ + destroy() { + if (this.job) { + clearInterval(this.job); + this.job = null; + this.logger.log('Automation stopped'); + } + } + + /** + * add update to manager for later processing like fast merging. + */ + async push(workspaceId: string, guid: string, update: Buffer) { + await this.db.update.create({ + data: { + workspaceId, + id: guid, + blob: update, + }, + }); + + this.logger.verbose( + `pushed update for workspace: ${workspaceId}, guid: ${guid}` + ); + } + + /** + * get the snapshot of the doc we've seen. + */ + async getSnapshot( + workspaceId: string, + guid: string + ): Promise { + const snapshot = await this.db.snapshot.findFirst({ + where: { + workspaceId, + id: guid, + }, + }); + + return snapshot?.blob; + } + + /** + * get pending updates + */ + async getUpdates(workspaceId: string, guid: string): Promise { + const updates = await this.db.update.findMany({ + where: { + workspaceId, + id: guid, + }, + }); + + return updates.map(update => update.blob); + } + + /** + * get the latest doc with all update applied. + * + * latest = snapshot + updates + */ + async getLatest(workspaceId: string, guid: string): Promise { + const snapshot = await this.getSnapshot(workspaceId, guid); + const updates = await this.getUpdates(workspaceId, guid); + + if (updates.length) { + if (snapshot) { + return this.recoverDoc(snapshot, ...updates); + } else { + return this.recoverDoc(...updates); + } + } + + if (snapshot) { + return this.recoverDoc(snapshot); + } + + return undefined; + } + + /** + * get the latest doc and convert it to update binary + */ + async getLatestUpdate( + workspaceId: string, + guid: string + ): Promise { + const doc = await this.getLatest(workspaceId, guid); + + return doc ? Buffer.from(encodeStateAsUpdate(doc)) : undefined; + } + + /** + * apply pending updates to snapshot + */ + async apply() { + const updates = await this.db + .$transaction(async db => { + // find the first update and batch process updates with same id + const first = await db.update.findFirst({ + orderBy: { + createdAt: 'asc', + }, + }); + + // no pending updates + if (!first) { + return; + } + + const { id, workspaceId } = first; + const updates = await db.update.findMany({ + where: { + id, + workspaceId, + }, + }); + + // no pending updates + if (!updates.length) { + return; + } + + // remove update that will be merged later + await db.update.deleteMany({ + where: { + id, + workspaceId, + }, + }); + + return updates; + }) + .catch( + // transaction failed, it's safe to ignore + e => { + this.logger.error('Failed to fetch updates', e); + } + ); + + // we put update merging logic outside transaction will make the processing more complex, + // but it's better to do so, since the merging may takes a lot of time, + // which may slow down the whole db. + if (!updates?.length) { + return; + } + + const { id, workspaceId } = updates[0]; + + this.logger.verbose( + `applying ${updates.length} updates for workspace: ${workspaceId}, guid: ${id}` + ); + + try { + const snapshot = await this.db.snapshot.findFirst({ + where: { + workspaceId, + id, + }, + }); + + // merge updates + const merged = snapshot + ? this.mergeUpdates(id, snapshot.blob, ...updates.map(u => u.blob)) + : this.mergeUpdates(id, ...updates.map(u => u.blob)); + + // save snapshot + await this.upsert(workspaceId, id, merged); + } catch (e) { + // failed to merge updates, put them back + this.logger.error('Failed to merge updates', e); + + await this.db.update + .createMany({ + data: updates.map(u => ({ + id: u.id, + workspaceId: u.workspaceId, + blob: u.blob, + })), + }) + .catch(e => { + // failed to recover, fallback TBD + this.logger.error('Fetal: failed to put updates back to db', e); + }); + } + } + + protected async upsert(workspaceId: string, guid: string, blob: Buffer) { + return this.db.snapshot.upsert({ + where: { + id_workspaceId: { + id: guid, + workspaceId, + }, + }, + create: { + id: guid, + workspaceId, + blob, + }, + update: { + blob, + }, + }); + } +} diff --git a/apps/server/src/modules/doc/redis-manager.ts b/apps/server/src/modules/doc/redis-manager.ts new file mode 100644 index 0000000000..0cb964759f --- /dev/null +++ b/apps/server/src/modules/doc/redis-manager.ts @@ -0,0 +1,150 @@ +import { Inject, Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; + +import { Config } from '../../config'; +import { Metrics } from '../../metrics/metrics'; +import { PrismaService } from '../../prisma'; +import { DocManager } from './manager'; + +function makeKey(prefix: string) { + return (parts: TemplateStringsArray, ...args: any[]) => { + return parts.reduce((prev, curr, i) => { + return prev + curr + (args[i] || ''); + }, prefix); + }; +} + +const pending = 'um_pending:'; +const updates = makeKey('um_u:'); +const lock = makeKey('um_l:'); + +const pushUpdateLua = ` + redis.call('sadd', KEYS[1], ARGV[1]) + redis.call('rpush', KEYS[2], ARGV[2]) +`; + +@Injectable() +export class RedisDocManager extends DocManager { + private readonly redis: Redis; + + constructor( + protected override readonly db: PrismaService, + @Inject('DOC_MANAGER_AUTOMATION') + protected override readonly automation: boolean, + protected override readonly config: Config, + protected override readonly metrics: Metrics + ) { + super(db, automation, config, metrics); + this.redis = new Redis(config.redis); + this.redis.defineCommand('pushDocUpdate', { + numberOfKeys: 2, + lua: pushUpdateLua, + }); + } + + override onModuleInit(): void { + if (this.automation) { + this.logger.log('Use Redis'); + this.setup(); + } + } + + override async push(workspaceId: string, guid: string, update: Buffer) { + try { + const key = `${workspaceId}:${guid}`; + + // @ts-expect-error custom command + this.redis.pushDocUpdate(pending, updates`${key}`, key, update); + + this.logger.verbose( + `pushed update for workspace: ${workspaceId}, guid: ${guid}` + ); + } catch (e) { + return await super.push(workspaceId, guid, update); + } + } + + override async getUpdates( + workspaceId: string, + guid: string + ): Promise { + try { + return this.redis.lrangeBuffer(updates`${workspaceId}:${guid}`, 0, -1); + } catch (e) { + return super.getUpdates(workspaceId, guid); + } + } + + override async apply(): Promise { + // incase some update fallback to db + await super.apply(); + + const pendingDoc = await this.redis.spop(pending).catch(() => null); // safe + + if (!pendingDoc) { + return; + } + + const updateKey = updates`${pendingDoc}`; + const lockKey = lock`${pendingDoc}`; + const splitAt = pendingDoc.indexOf(':'); + const workspaceId = pendingDoc.substring(0, splitAt); + const id = pendingDoc.substring(splitAt + 1); + + // acquire the lock + const lockResult = await this.redis + .set( + lockKey, + '1', + 'EX', + // 10mins, incase progress exit in between lock require & release, which is a rare. + // if the lock is really hold more then 10mins, we should check the merge logic correctness + 600, + 'NX' + ) + .catch(() => null); // safe; + + if (!lockResult) { + return; + } + + try { + // fetch pending updates + const updates = await this.redis + .lrangeBuffer(updateKey, 0, -1) + .catch(() => []); // safe + + if (!updates.length) { + return; + } + + this.logger.verbose( + `applying ${updates.length} updates for workspace: ${workspaceId}, guid: ${id}` + ); + + const snapshot = await this.getSnapshot(workspaceId, id); + + // merge + const blob = snapshot + ? this.mergeUpdates(id, snapshot, ...updates) + : this.mergeUpdates(id, ...updates); + + // update snapshot + + await this.upsert(workspaceId, id, blob); + + // delete merged updates + await this.redis + .ltrim(updateKey, updates.length, -1) + // safe, fallback to mergeUpdates + .catch(e => { + this.logger.error('Failed to remove merged updates from Redis', e); + }); + } catch (e) { + this.logger.error('Failed to merge updates with snapshot', e); + await this.redis.sadd(pending, `${workspaceId}:${id}`).catch(() => null); // safe + } finally { + await this.redis.del(lockKey); + } + } +} diff --git a/apps/server/src/modules/index.ts b/apps/server/src/modules/index.ts index 1daa68cc3e..c11e3428b1 100644 --- a/apps/server/src/modules/index.ts +++ b/apps/server/src/modules/index.ts @@ -1,5 +1,40 @@ +import { DynamicModule, Type } from '@nestjs/common'; + +import { GqlModule } from '../graphql.module'; import { AuthModule } from './auth'; +import { DocModule } from './doc'; +import { SyncModule } from './sync'; import { UsersModule } from './users'; import { WorkspaceModule } from './workspaces'; -export const BusinessModules = [AuthModule, WorkspaceModule, UsersModule]; +const { SERVER_FLAVOR } = process.env; + +const BusinessModules: (Type | DynamicModule)[] = []; + +switch (SERVER_FLAVOR) { + case 'sync': + BusinessModules.push(SyncModule, DocModule.forSync()); + break; + case 'graphql': + BusinessModules.push( + GqlModule, + WorkspaceModule, + UsersModule, + AuthModule, + DocModule.forRoot() + ); + break; + case 'allinone': + default: + BusinessModules.push( + GqlModule, + WorkspaceModule, + UsersModule, + AuthModule, + SyncModule, + DocModule.forRoot() + ); + break; +} + +export { BusinessModules }; diff --git a/apps/server/src/modules/storage/fs.ts b/apps/server/src/modules/storage/fs.ts index 2d6cc967ba..af82527d8a 100644 --- a/apps/server/src/modules/storage/fs.ts +++ b/apps/server/src/modules/storage/fs.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto'; import { createWriteStream } from 'node:fs'; import { mkdir } from 'node:fs/promises'; import { join } from 'node:path'; @@ -14,10 +15,16 @@ export class FSService { async writeFile(key: string, file: FileUpload) { const dest = this.config.objectStorage.fs.path; + const fileName = `${key}-${randomUUID()}`; + const prefix = this.config.node.dev + ? `${this.config.https ? 'https' : 'http'}://${this.config.host}:${ + this.config.port + }` + : ''; await mkdir(dest, { recursive: true }); - const destFile = join(dest, key); + const destFile = join(dest, fileName); await pipeline(file.createReadStream(), createWriteStream(destFile)); - return `/assets/${destFile}`; + return `${prefix}/assets/${fileName}`; } } diff --git a/apps/server/src/modules/storage/storage.service.ts b/apps/server/src/modules/storage/storage.service.ts index f923ca8352..7bdbfc9be1 100644 --- a/apps/server/src/modules/storage/storage.service.ts +++ b/apps/server/src/modules/storage/storage.service.ts @@ -1,5 +1,8 @@ import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { Inject, Injectable } from '@nestjs/common'; +import { crc32 } from '@node-rs/crc32'; +import { fileTypeFromBuffer } from 'file-type'; +import { getStreamAsBuffer } from 'get-stream'; import { Config } from '../../config'; import { FileUpload } from '../../types'; @@ -16,14 +19,21 @@ export class StorageService { async uploadFile(key: string, file: FileUpload) { if (this.config.objectStorage.r2.enabled) { + const readableFile = file.createReadStream(); + const fileBuffer = await getStreamAsBuffer(readableFile); + const mime = (await fileTypeFromBuffer(fileBuffer))?.mime; + const crc32Value = crc32(fileBuffer); + const keyWithCrc32 = `${crc32Value}-${key}`; await this.s3.send( new PutObjectCommand({ - Body: file.createReadStream(), + Body: fileBuffer, Bucket: this.config.objectStorage.r2.bucket, - Key: key, + Key: keyWithCrc32, + ContentLength: fileBuffer.length, + ContentType: mime, }) ); - return `https://avatar.affineassets.com/${key}`; + return `https://avatar.affineassets.com/${keyWithCrc32}`; } else { return this.fs.writeFile(key, file); } diff --git a/apps/server/src/modules/sync/events/events.gateway.ts b/apps/server/src/modules/sync/events/events.gateway.ts new file mode 100644 index 0000000000..082fe82bd4 --- /dev/null +++ b/apps/server/src/modules/sync/events/events.gateway.ts @@ -0,0 +1,153 @@ +import { + ConnectedSocket, + MessageBody, + OnGatewayConnection, + OnGatewayDisconnect, + SubscribeMessage, + WebSocketGateway, + WebSocketServer, +} from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; +import { encodeStateAsUpdate, encodeStateVector } from 'yjs'; + +import { Metrics } from '../../../metrics/metrics'; +import { trimGuid } from '../../../utils/doc'; +import { DocManager } from '../../doc'; + +@WebSocketGateway({ + cors: process.env.NODE_ENV !== 'production', +}) +export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect { + private connectionCount = 0; + + constructor( + private readonly docManager: DocManager, + private readonly metric: Metrics + ) {} + + @WebSocketServer() + server!: Server; + + handleConnection() { + this.connectionCount++; + this.metric.socketIOConnectionGauge(this.connectionCount, {}); + } + + handleDisconnect() { + this.connectionCount--; + this.metric.socketIOConnectionGauge(this.connectionCount, {}); + } + + @SubscribeMessage('client-handshake') + async handleClientHandShake( + @MessageBody() workspaceId: string, + @ConnectedSocket() client: Socket + ) { + this.metric.socketIOEventCounter(1, { event: 'client-handshake' }); + const endTimer = this.metric.socketIOEventTimer({ + event: 'client-handshake', + }); + await client.join(workspaceId); + endTimer(); + } + + @SubscribeMessage('client-leave') + async handleClientLeave( + @MessageBody() workspaceId: string, + @ConnectedSocket() client: Socket + ) { + this.metric.socketIOEventCounter(1, { event: 'client-leave' }); + const endTimer = this.metric.socketIOEventTimer({ + event: 'client-leave', + }); + await client.leave(workspaceId); + endTimer(); + } + + @SubscribeMessage('client-update') + async handleClientUpdate( + @MessageBody() + message: { + workspaceId: string; + guid: string; + update: string; + }, + @ConnectedSocket() client: Socket + ) { + this.metric.socketIOEventCounter(1, { event: 'client-update' }); + const endTimer = this.metric.socketIOEventTimer({ event: 'client-update' }); + const update = Buffer.from(message.update, 'base64'); + client.to(message.workspaceId).emit('server-update', message); + const guid = trimGuid(message.workspaceId, message.guid); + + await this.docManager.push(message.workspaceId, guid, update); + endTimer(); + } + + @SubscribeMessage('doc-load') + async loadDoc( + @MessageBody() + message: { + workspaceId: string; + guid: string; + stateVector?: string; + targetClientId?: number; + } + ): Promise<{ missing: string; state?: string } | false> { + this.metric.socketIOEventCounter(1, { event: 'doc-load' }); + const endTimer = this.metric.socketIOEventTimer({ event: 'doc-load' }); + const guid = trimGuid(message.workspaceId, message.guid); + const doc = await this.docManager.getLatest(message.workspaceId, guid); + + if (!doc) { + endTimer(); + return false; + } + + const missing = Buffer.from( + encodeStateAsUpdate( + doc, + message.stateVector + ? Buffer.from(message.stateVector, 'base64') + : undefined + ) + ).toString('base64'); + const state = Buffer.from(encodeStateVector(doc)).toString('base64'); + + endTimer(); + return { + missing, + state, + }; + } + + @SubscribeMessage('awareness-init') + async handleInitAwareness( + @MessageBody() workspaceId: string, + @ConnectedSocket() client: Socket + ) { + this.metric.socketIOEventCounter(1, { event: 'awareness-init' }); + const endTimer = this.metric.socketIOEventTimer({ + event: 'init-awareness', + }); + client.to(workspaceId).emit('new-client-awareness-init'); + endTimer(); + } + + @SubscribeMessage('awareness-update') + async handleHelpGatheringAwareness( + @MessageBody() message: { workspaceId: string; awarenessUpdate: string }, + @ConnectedSocket() client: Socket + ) { + this.metric.socketIOEventCounter(1, { event: 'awareness-update' }); + const endTimer = this.metric.socketIOEventTimer({ + event: 'awareness-update', + }); + client.to(message.workspaceId).emit('server-awareness-broadcast', { + ...message, + }); + + endTimer(); + return 'ack'; + } +} diff --git a/apps/server/src/modules/sync/events/events.module.ts b/apps/server/src/modules/sync/events/events.module.ts new file mode 100644 index 0000000000..2d61c910c3 --- /dev/null +++ b/apps/server/src/modules/sync/events/events.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; + +import { DocModule } from '../../doc'; +import { EventsGateway } from './events.gateway'; +import { WorkspaceService } from './workspace'; + +@Module({ + imports: [DocModule.forFeature()], + providers: [EventsGateway, WorkspaceService], +}) +export class EventsModule {} diff --git a/apps/server/src/modules/sync/events/workspace.ts b/apps/server/src/modules/sync/events/workspace.ts new file mode 100644 index 0000000000..669cc41b6c --- /dev/null +++ b/apps/server/src/modules/sync/events/workspace.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { Doc, encodeStateAsUpdate } from 'yjs'; + +import { DocManager } from '../../doc'; +import { assertExists } from '../utils'; + +@Injectable() +export class WorkspaceService { + constructor(private readonly docManager: DocManager) {} + + async getDocsFromWorkspaceId(workspaceId: string): Promise< + Array<{ + guid: string; + update: Buffer; + }> + > { + const docs: Array<{ + guid: string; + update: Buffer; + }> = []; + const queue: Array<[string, Doc]> = []; + // Workspace Doc's guid is the same as workspaceId. This is achieved by when creating a new workspace, the doc guid + // is manually set to workspaceId. + const doc = await this.docManager.getLatest(workspaceId, workspaceId); + if (doc) { + queue.push([workspaceId, doc]); + } + + while (queue.length > 0) { + const head = queue.pop(); + assertExists(head); + const [guid, doc] = head; + docs.push({ + guid: guid, + update: Buffer.from(encodeStateAsUpdate(doc)), + }); + + for (const { guid } of doc.subdocs) { + const subDoc = await this.docManager.getLatest(workspaceId, guid); + if (subDoc) { + queue.push([guid, subDoc]); + } + } + } + + return docs; + } +} diff --git a/apps/server/src/modules/sync/index.ts b/apps/server/src/modules/sync/index.ts new file mode 100644 index 0000000000..78553c2b69 --- /dev/null +++ b/apps/server/src/modules/sync/index.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; + +import { EventsModule } from './events/events.module'; + +@Module({ + imports: [EventsModule], +}) +export class SyncModule {} diff --git a/apps/server/src/modules/sync/redis-adapter.ts b/apps/server/src/modules/sync/redis-adapter.ts new file mode 100644 index 0000000000..312a0c1d7e --- /dev/null +++ b/apps/server/src/modules/sync/redis-adapter.ts @@ -0,0 +1,37 @@ +import { IoAdapter } from '@nestjs/platform-socket.io'; +import { createAdapter } from '@socket.io/redis-adapter'; +import { Redis } from 'ioredis'; +import { ServerOptions } from 'socket.io'; + +export class RedisIoAdapter extends IoAdapter { + private adapterConstructor: ReturnType | undefined; + + async connectToRedis( + host: string, + port: number, + username: string, + password: string, + db: number + ): Promise { + const pubClient = new Redis(port, host, { + username, + password, + db, + }); + pubClient.on('error', err => { + console.error(err); + }); + const subClient = pubClient.duplicate(); + subClient.on('error', err => { + console.error(err); + }); + + this.adapterConstructor = createAdapter(pubClient, subClient); + } + + override createIOServer(port: number, options?: ServerOptions): any { + const server = super.createIOServer(port, options); + server.adapter(this.adapterConstructor); + return server; + } +} diff --git a/apps/server/src/modules/sync/utils.ts b/apps/server/src/modules/sync/utils.ts new file mode 100644 index 0000000000..ca13e99719 --- /dev/null +++ b/apps/server/src/modules/sync/utils.ts @@ -0,0 +1,11 @@ +export function assertExists( + val: T | null | undefined, + message: string | Error = 'val does not exist' +): asserts val is T { + if (val === null || val === undefined) { + if (message instanceof Error) { + throw message; + } + throw new Error(message); + } +} diff --git a/apps/server/src/modules/users/resolver.ts b/apps/server/src/modules/users/resolver.ts index 3a90aeb2ec..c2f25b8638 100644 --- a/apps/server/src/modules/users/resolver.ts +++ b/apps/server/src/modules/users/resolver.ts @@ -1,4 +1,8 @@ -import { BadRequestException } from '@nestjs/common'; +import { + BadRequestException, + ForbiddenException, + HttpException, +} from '@nestjs/common'; import { Args, Field, @@ -6,16 +10,23 @@ import { Mutation, ObjectType, Query, + registerEnumType, Resolver, } from '@nestjs/graphql'; import type { User } from '@prisma/client'; // @ts-expect-error graphql-upload is not typed import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; +import { Config } from '../../config'; import { PrismaService } from '../../prisma/service'; import type { FileUpload } from '../../types'; -import { Auth } from '../auth/guard'; +import { Auth, CurrentUser, Public } from '../auth/guard'; import { StorageService } from '../storage/storage.service'; +import { NewFeaturesKind } from './types'; + +registerEnumType(NewFeaturesKind, { + name: 'NewFeaturesKind', +}); @ObjectType() export class UserType implements Partial { @@ -28,11 +39,34 @@ export class UserType implements Partial { @Field({ description: 'User email' }) email!: string; - @Field({ description: 'User avatar url', nullable: true }) - avatarUrl!: string; + @Field(() => String, { description: 'User avatar url', nullable: true }) + avatarUrl: string | null = null; + + @Field(() => Date, { description: 'User email verified', nullable: true }) + emailVerified: Date | null = null; @Field({ description: 'User created date', nullable: true }) createdAt!: Date; + + @Field(() => Boolean, { + description: 'User password has been set', + nullable: true, + }) + hasPassword?: boolean; +} + +@ObjectType() +export class DeleteAccount { + @Field() + success!: boolean; +} + +@ObjectType() +export class AddToNewFeaturesWaitingList { + @Field() + email!: string; + @Field(() => NewFeaturesKind, { description: 'New features kind' }) + type!: NewFeaturesKind; } @Auth() @@ -40,17 +74,59 @@ export class UserType implements Partial { export class UserResolver { constructor( private readonly prisma: PrismaService, - private readonly storage: StorageService + private readonly storage: StorageService, + private readonly config: Config ) {} + @Query(() => UserType, { + name: 'currentUser', + description: 'Get current user', + }) + async currentUser(@CurrentUser() user: User) { + return { + id: user.id, + name: user.name, + email: user.email, + emailVerified: user.emailVerified, + avatarUrl: user.avatarUrl, + createdAt: user.createdAt, + hasPassword: !!user.password, + }; + } + @Query(() => UserType, { name: 'user', description: 'Get user by email', + nullable: true, }) + @Public() async user(@Args('email') email: string) { - return this.prisma.user.findUnique({ - where: { email }, - }); + if (this.config.node.prod && this.config.affine.beta) { + const hasEarlyAccess = await this.prisma.newFeaturesWaitingList + .findUnique({ + where: { email, type: NewFeaturesKind.EarlyAccess }, + }) + .catch(() => false); + if (!hasEarlyAccess) { + return new HttpException( + `You don't have early access permission\nVisit https://community.affine.pro/c/insider-general/ for more information`, + 401 + ); + } + } + // TODO: need to limit a user can only get another user witch is in the same workspace + const user = await this.prisma.user + .findUnique({ + where: { email }, + }) + .catch(() => { + return null; + }); + if (user?.password) { + const userResponse: UserType = user; + userResponse.hasPassword = true; + } + return user; } @Mutation(() => UserType, { @@ -72,4 +148,45 @@ export class UserResolver { data: { avatarUrl: url }, }); } + + @Mutation(() => DeleteAccount) + async deleteAccount(@CurrentUser() user: UserType): Promise { + await this.prisma.user.delete({ + where: { + id: user.id, + }, + }); + await this.prisma.session.deleteMany({ + where: { + userId: user.id, + }, + }); + return { + success: true, + }; + } + + @Mutation(() => AddToNewFeaturesWaitingList) + async addToNewFeaturesWaitingList( + @CurrentUser() user: UserType, + @Args('type', { + type: () => NewFeaturesKind, + }) + type: NewFeaturesKind, + @Args('email') email: string + ): Promise { + if (!user.email.endsWith('@toeverything.info')) { + throw new ForbiddenException('You are not allowed to do this'); + } + await this.prisma.newFeaturesWaitingList.create({ + data: { + email, + type, + }, + }); + return { + email, + type, + }; + } } diff --git a/apps/server/src/modules/users/types.ts b/apps/server/src/modules/users/types.ts new file mode 100644 index 0000000000..565ff3827d --- /dev/null +++ b/apps/server/src/modules/users/types.ts @@ -0,0 +1,3 @@ +export enum NewFeaturesKind { + EarlyAccess, +} diff --git a/apps/server/src/modules/workspaces/controller.ts b/apps/server/src/modules/workspaces/controller.ts index b29a4d412f..bcc4e87b4d 100644 --- a/apps/server/src/modules/workspaces/controller.ts +++ b/apps/server/src/modules/workspaces/controller.ts @@ -1,6 +1,7 @@ import type { Storage } from '@affine/storage'; import { Controller, + ForbiddenException, Get, Inject, NotFoundException, @@ -8,20 +9,33 @@ import { Res, } from '@nestjs/common'; import type { Response } from 'express'; +import format from 'pretty-time'; import { StorageProvide } from '../../storage'; +import { trimGuid } from '../../utils/doc'; +import { Auth, CurrentUser, Publicable } from '../auth'; +import { DocManager } from '../doc'; +import { UserType } from '../users'; +import { PermissionService } from './permission'; @Controller('/api/workspaces') export class WorkspacesController { - constructor(@Inject(StorageProvide) private readonly storage: Storage) {} + constructor( + @Inject(StorageProvide) private readonly storage: Storage, + private readonly permission: PermissionService, + private readonly docManager: DocManager + ) {} + // get workspace blob + // + // NOTE: because graphql can't represent a File, so we have to use REST API to get blob @Get('/:id/blobs/:name') async blob( @Param('id') workspaceId: string, @Param('name') name: string, @Res() res: Response ) { - const blob = await this.storage.blob(workspaceId, name); + const blob = await this.storage.getBlob(workspaceId, name); if (!blob) { throw new NotFoundException('Blob not found'); @@ -33,4 +47,34 @@ export class WorkspacesController { res.send(blob.data); } + + // get doc binary + @Get('/:id/docs/:guid') + @Auth() + @Publicable() + async doc( + @CurrentUser() user: UserType | undefined, + @Param('id') ws: string, + @Param('guid') guid: string, + @Res() res: Response + ) { + const start = process.hrtime(); + const id = trimGuid(ws, guid); + if ( + // if a user has the permission + !(await this.permission.isAccessible(ws, id, user?.id)) + ) { + throw new ForbiddenException('Permission denied'); + } + + const update = await this.docManager.getLatestUpdate(ws, id); + + if (!update) { + throw new NotFoundException('Doc not found'); + } + + res.setHeader('content-type', 'application/octet-stream'); + res.send(update); + console.info('workspaces doc api: ', format(process.hrtime(start))); + } } diff --git a/apps/server/src/modules/workspaces/index.ts b/apps/server/src/modules/workspaces/index.ts index 61cb69da46..6ea516e1dc 100644 --- a/apps/server/src/modules/workspaces/index.ts +++ b/apps/server/src/modules/workspaces/index.ts @@ -1,12 +1,15 @@ import { Module } from '@nestjs/common'; +import { DocModule } from '../doc'; import { WorkspacesController } from './controller'; import { PermissionService } from './permission'; import { WorkspaceResolver } from './resolver'; @Module({ - providers: [WorkspaceResolver, PermissionService, WorkspacesController], + imports: [DocModule.forFeature()], + controllers: [WorkspacesController], + providers: [WorkspaceResolver, PermissionService], exports: [PermissionService], }) export class WorkspaceModule {} -export { WorkspaceType } from './resolver'; +export { InvitationType, WorkspaceType } from './resolver'; diff --git a/apps/server/src/modules/workspaces/permission.ts b/apps/server/src/modules/workspaces/permission.ts index 4b9825fb98..2c2b82c06c 100644 --- a/apps/server/src/modules/workspaces/permission.ts +++ b/apps/server/src/modules/workspaces/permission.ts @@ -12,6 +12,7 @@ export class PermissionService { const data = await this.prisma.userWorkspacePermission.findFirst({ where: { workspaceId: ws, + subPageId: null, userId: user, accepted: true, }, @@ -20,6 +21,38 @@ export class PermissionService { return data?.type as Permission; } + async isAccessible(ws: string, id: string, user?: string): Promise { + if (user) { + return await this.tryCheck(ws, user); + } else { + // check if this is a public workspace + const count = await this.prisma.workspace.count({ + where: { id: ws, public: true }, + }); + if (count > 0) { + return true; + } + + // check whether this is a public subpage + const workspace = await this.prisma.userWorkspacePermission.findMany({ + where: { + workspaceId: ws, + userId: null, + }, + }); + const subpages = workspace + .map(ws => ws.subPageId) + .filter((v): v is string => !!v); + if (subpages.length > 0 && ws === id) { + // rootDoc is always accessible when there is a public subpage + return true; + } else { + // check if this is a public subpage + return subpages.map(page => `space:${page}`).includes(id); + } + } + } + async check( ws: string, user: string, @@ -35,9 +68,21 @@ export class PermissionService { user: string, permission: Permission = Permission.Read ) { + // If the permission is read, we should check if the workspace is public + if (permission === Permission.Read) { + const data = await this.prisma.workspace.count({ + where: { id: ws, public: true }, + }); + + if (data > 0) { + return true; + } + } + const data = await this.prisma.userWorkspacePermission.count({ where: { workspaceId: ws, + subPageId: null, userId: user, accepted: true, type: { @@ -46,30 +91,18 @@ export class PermissionService { }, }); - if (data > 0) { - return true; - } - - // If the permission is read, we should check if the workspace is public - if (permission === Permission.Read) { - const data = await this.prisma.workspace.count({ - where: { id: ws, public: true }, - }); - - return data > 0; - } - - return false; + return data > 0; } async grant( ws: string, user: string, permission: Permission = Permission.Read - ) { + ): Promise { const data = await this.prisma.userWorkspacePermission.findFirst({ where: { workspaceId: ws, + subPageId: null, userId: user, accepted: true, }, @@ -105,22 +138,40 @@ export class PermissionService { ].filter(Boolean) as Prisma.PrismaPromise[] ); - return p; + return p.id; } - return this.prisma.userWorkspacePermission.create({ - data: { + return this.prisma.userWorkspacePermission + .create({ + data: { + workspaceId: ws, + subPageId: null, + userId: user, + type: permission, + }, + }) + .then(p => p.id); + } + + async acceptById(ws: string, id: string) { + const result = await this.prisma.userWorkspacePermission.updateMany({ + where: { + id, workspaceId: ws, - userId: user, - type: permission, + }, + data: { + accepted: true, }, }); + + return result.count > 0; } async accept(ws: string, user: string) { const result = await this.prisma.userWorkspacePermission.updateMany({ where: { workspaceId: ws, + subPageId: null, userId: user, accepted: false, }, @@ -136,6 +187,67 @@ export class PermissionService { const result = await this.prisma.userWorkspacePermission.deleteMany({ where: { workspaceId: ws, + subPageId: null, + userId: user, + type: { + // We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading + not: Permission.Owner, + }, + }, + }); + + return result.count > 0; + } + + async isPageAccessible(ws: string, page: string, user?: string) { + const data = await this.prisma.userWorkspacePermission.findFirst({ + where: { + workspaceId: ws, + subPageId: page, + userId: user, + }, + }); + + return data?.accepted || false; + } + + async grantPage( + ws: string, + page: string, + user?: string, + permission: Permission = Permission.Read + ) { + const data = await this.prisma.userWorkspacePermission.findFirst({ + where: { + workspaceId: ws, + subPageId: page, + userId: user, + }, + }); + + if (data) { + return data.accepted; + } + + return this.prisma.userWorkspacePermission + .create({ + data: { + workspaceId: ws, + subPageId: page, + userId: user, + // if provide user id, user need to accept the invitation + accepted: user ? false : true, + type: permission, + }, + }) + .then(ret => ret.accepted); + } + + async revokePage(ws: string, page: string, user?: string) { + const result = await this.prisma.userWorkspacePermission.deleteMany({ + where: { + workspaceId: ws, + subPageId: page, userId: user, type: { // We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading diff --git a/apps/server/src/modules/workspaces/resolver.ts b/apps/server/src/modules/workspaces/resolver.ts index aae264327d..cd2cfe27f1 100644 --- a/apps/server/src/modules/workspaces/resolver.ts +++ b/apps/server/src/modules/workspaces/resolver.ts @@ -8,6 +8,7 @@ import { Int, Mutation, ObjectType, + OmitType, Parent, PartialType, PickType, @@ -19,20 +20,43 @@ import { import type { User, Workspace } from '@prisma/client'; // @ts-expect-error graphql-upload is not typed import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'; +import { applyUpdate, Doc } from 'yjs'; import { PrismaService } from '../../prisma'; import { StorageProvide } from '../../storage'; import type { FileUpload } from '../../types'; -import { Auth, CurrentUser } from '../auth'; +import { Auth, CurrentUser, Public } from '../auth'; +import { MailService } from '../auth/mailer'; +import { AuthService } from '../auth/service'; import { UserType } from '../users/resolver'; import { PermissionService } from './permission'; import { Permission } from './types'; +import { defaultWorkspaceAvatar } from './utils'; registerEnumType(Permission, { name: 'Permission', description: 'User permission in workspace', }); +@ObjectType() +export class InviteUserType extends OmitType( + PartialType(UserType), + ['id'], + ObjectType +) { + @Field(() => ID) + id!: string; + + @Field(() => Permission, { description: 'User permission in workspace' }) + permission!: Permission; + + @Field({ description: 'Invite id' }) + inviteId!: string; + + @Field({ description: 'User accepted' }) + accepted!: boolean; +} + @ObjectType() export class WorkspaceType implements Partial { @Field(() => ID) @@ -43,6 +67,34 @@ export class WorkspaceType implements Partial { @Field({ description: 'Workspace created date' }) createdAt!: Date; + + @Field(() => [InviteUserType], { + description: 'Members of workspace', + }) + members!: InviteUserType[]; +} + +@ObjectType() +export class InvitationWorkspaceType { + @Field(() => ID) + id!: string; + + @Field({ description: 'Workspace name' }) + name!: string; + + @Field(() => String, { + // nullable: true, + description: 'Base64 encoded avatar', + }) + avatar!: string; +} + +@ObjectType() +export class InvitationType { + @Field({ description: 'Workspace information' }) + workspace!: InvitationWorkspaceType; + @Field({ description: 'User information' }) + user!: UserType; } @InputType() @@ -59,6 +111,8 @@ export class UpdateWorkspaceInput extends PickType( @Resolver(() => WorkspaceType) export class WorkspaceResolver { constructor( + private readonly auth: AuthService, + private readonly mailer: MailService, private readonly prisma: PrismaService, private readonly permissionProvider: PermissionService, @Inject(StorageProvide) private readonly storage: Storage @@ -69,7 +123,7 @@ export class WorkspaceResolver { complexity: 2, }) async permission( - @CurrentUser() user: User, + @CurrentUser() user: UserType, @Parent() workspace: WorkspaceType ) { // may applied in workspaces query @@ -99,6 +153,20 @@ export class WorkspaceResolver { }); } + @ResolveField(() => [String], { + description: 'Shared pages of workspace', + complexity: 2, + }) + async sharedPages(@Parent() workspace: WorkspaceType) { + const data = await this.prisma.userWorkspacePermission.findMany({ + where: { + workspaceId: workspace.id, + }, + }); + + return data.map(item => item.subPageId).filter(Boolean); + } + @ResolveField(() => UserType, { description: 'Owner of workspace', complexity: 2, @@ -117,27 +185,46 @@ export class WorkspaceResolver { return data.user; } - @ResolveField(() => [UserType], { + @ResolveField(() => [InviteUserType], { description: 'Members of workspace', complexity: 2, }) - async members( - @CurrentUser() user: UserType, - @Parent() workspace: WorkspaceType - ) { + async members(@Parent() workspace: WorkspaceType) { const data = await this.prisma.userWorkspacePermission.findMany({ where: { workspaceId: workspace.id, - accepted: true, - userId: { - not: user.id, - }, }, include: { user: true, }, }); - return data.map(({ user }) => user); + return data.map(({ id, accepted, type, user }) => ({ + ...user, + permission: type, + inviteId: id, + accepted, + })); + } + + @Query(() => Boolean, { + description: 'Get is owner of workspace', + complexity: 2, + }) + async isOwner( + @CurrentUser() user: UserType, + @Args('workspaceId') workspaceId: string + ) { + const data = await this.prisma.userWorkspacePermission.findFirst({ + where: { + workspaceId, + type: Permission.Owner, + }, + include: { + user: true, + }, + }); + + return data?.user?.id === user.id; } @Query(() => [WorkspaceType], { @@ -163,6 +250,22 @@ export class WorkspaceResolver { }); } + @Query(() => WorkspaceType, { + description: 'Get public workspace by id', + }) + @Public() + async publicWorkspace(@Args('id') id: string) { + const workspace = await this.prisma.workspace.findUnique({ + where: { id }, + }); + + if (workspace?.public) { + return workspace; + } + + throw new NotFoundException("Workspace doesn't exist"); + } + @Query(() => WorkspaceType, { description: 'Get workspace by id', }) @@ -181,7 +284,7 @@ export class WorkspaceResolver { description: 'Create a new workspace', }) async createWorkspace( - @CurrentUser() user: User, + @CurrentUser() user: UserType, @Args({ name: 'init', type: () => GraphQLUpload }) update: FileUpload ) { @@ -215,7 +318,13 @@ export class WorkspaceResolver { }, }); - await this.storage.createWorkspace(workspace.id, buffer); + await this.prisma.snapshot.create({ + data: { + id: workspace.id, + workspaceId: workspace.id, + blob: buffer, + }, + }); return workspace; } @@ -224,11 +333,11 @@ export class WorkspaceResolver { description: 'Update workspace', }) async updateWorkspace( - @CurrentUser() user: User, + @CurrentUser() user: UserType, @Args({ name: 'input', type: () => UpdateWorkspaceInput }) { id, ...updates }: UpdateWorkspaceInput ) { - await this.permissionProvider.check('id', user.id, Permission.Admin); + await this.permissionProvider.check(id, user.id, Permission.Admin); return this.prisma.workspace.update({ where: { @@ -239,7 +348,7 @@ export class WorkspaceResolver { } @Mutation(() => Boolean) - async deleteWorkspace(@CurrentUser() user: User, @Args('id') id: string) { + async deleteWorkspace(@CurrentUser() user: UserType, @Args('id') id: string) { await this.permissionProvider.check(id, user.id, Permission.Owner); await this.prisma.workspace.delete({ @@ -248,25 +357,30 @@ export class WorkspaceResolver { }, }); - await this.prisma.userWorkspacePermission.deleteMany({ - where: { - workspaceId: id, - }, - }); - - // TODO: - // delete all related data, like websocket connections, blobs, etc. - await this.storage.deleteWorkspace(id); + await this.prisma.$transaction([ + this.prisma.update.deleteMany({ + where: { + workspaceId: id, + }, + }), + this.prisma.snapshot.deleteMany({ + where: { + workspaceId: id, + }, + }), + ]); return true; } - @Mutation(() => Boolean) + @Mutation(() => String) async invite( - @CurrentUser() user: User, + @CurrentUser() user: UserType, @Args('workspaceId') workspaceId: string, @Args('email') email: string, - @Args('permission', { type: () => Permission }) permission: Permission + @Args('permission', { type: () => Permission }) permission: Permission, + // TODO: add rate limit + @Args('sendInviteMail', { nullable: true }) sendInviteMail: boolean ) { await this.permissionProvider.check(workspaceId, user.id, Permission.Admin); @@ -280,18 +394,122 @@ export class WorkspaceResolver { }, }); - if (!target) { - throw new NotFoundException("User doesn't exist"); + if (target) { + const originRecord = await this.prisma.userWorkspacePermission.findFirst({ + where: { + workspaceId, + userId: target.id, + }, + }); + + if (originRecord) { + return originRecord.id; + } + + const inviteId = await this.permissionProvider.grant( + workspaceId, + target.id, + permission + ); + if (sendInviteMail) { + const inviteInfo = await this.getInviteInfo(inviteId); + + await this.mailer.sendInviteEmail(email, inviteId, { + workspace: { + id: inviteInfo.workspace.id, + name: inviteInfo.workspace.name, + avatar: inviteInfo.workspace.avatar, + }, + user: { + avatar: inviteInfo.user?.avatarUrl || '', + name: inviteInfo.user?.name || '', + }, + }); + } + return inviteId; + } else { + const user = await this.auth.createAnonymousUser(email); + const inviteId = await this.permissionProvider.grant( + workspaceId, + user.id, + permission + ); + if (sendInviteMail) { + const inviteInfo = await this.getInviteInfo(inviteId); + + await this.mailer.sendInviteEmail(email, inviteId, { + workspace: { + id: inviteInfo.workspace.id, + name: inviteInfo.workspace.name, + avatar: inviteInfo.workspace.avatar, + }, + user: { + avatar: inviteInfo.user?.avatarUrl || '', + name: inviteInfo.user?.name || '', + }, + }); + } + return inviteId; + } + } + + @Public() + @Query(() => InvitationType, { + description: 'Update workspace', + }) + async getInviteInfo(@Args('inviteId') inviteId: string) { + const permission = + await this.prisma.userWorkspacePermission.findUniqueOrThrow({ + where: { + id: inviteId, + }, + }); + + const snapshot = await this.prisma.snapshot.findFirstOrThrow({ + where: { + id: permission.workspaceId, + workspaceId: permission.workspaceId, + }, + }); + + const doc = new Doc(); + + applyUpdate(doc, new Uint8Array(snapshot.blob)); + const metaJSON = doc.getMap('meta').toJSON(); + + const owner = await this.prisma.userWorkspacePermission.findFirstOrThrow({ + where: { + workspaceId: permission.workspaceId, + type: Permission.Owner, + }, + include: { + user: true, + }, + }); + + let avatar = ''; + + if (metaJSON.avatar) { + const avatarBlob = await this.storage.getBlob( + permission.workspaceId, + metaJSON.avatar + ); + avatar = avatarBlob?.data.toString('base64') || ''; } - await this.permissionProvider.grant(workspaceId, target.id, permission); - - return true; + return { + workspace: { + name: metaJSON.name || '', + avatar: avatar || defaultWorkspaceAvatar, + id: permission.workspaceId, + }, + user: owner.user, + }; } @Mutation(() => Boolean) async revoke( - @CurrentUser() user: User, + @CurrentUser() user: UserType, @Args('workspaceId') workspaceId: string, @Args('userId') userId: string ) { @@ -300,9 +518,18 @@ export class WorkspaceResolver { return this.permissionProvider.revoke(workspaceId, userId); } + @Mutation(() => Boolean) + @Public() + async acceptInviteById( + @Args('workspaceId') workspaceId: string, + @Args('inviteId') inviteId: string + ) { + return this.permissionProvider.acceptById(workspaceId, inviteId); + } + @Mutation(() => Boolean) async acceptInvite( - @CurrentUser() user: User, + @CurrentUser() user: UserType, @Args('workspaceId') workspaceId: string ) { return this.permissionProvider.accept(workspaceId, user.id); @@ -310,7 +537,7 @@ export class WorkspaceResolver { @Mutation(() => Boolean) async leaveWorkspace( - @CurrentUser() user: User, + @CurrentUser() user: UserType, @Args('workspaceId') workspaceId: string ) { await this.permissionProvider.check(workspaceId, user.id); @@ -318,14 +545,48 @@ export class WorkspaceResolver { return this.permissionProvider.revoke(workspaceId, user.id); } + @Mutation(() => Boolean) + async sharePage( + @CurrentUser() user: UserType, + @Args('workspaceId') workspaceId: string, + @Args('pageId') pageId: string + ) { + await this.permissionProvider.check(workspaceId, user.id, Permission.Admin); + + return this.permissionProvider.grantPage(workspaceId, pageId); + } + + @Mutation(() => Boolean) + async revokePage( + @CurrentUser() user: UserType, + @Args('workspaceId') workspaceId: string, + @Args('pageId') pageId: string + ) { + await this.permissionProvider.check(workspaceId, user.id, Permission.Admin); + + return this.permissionProvider.revokePage(workspaceId, pageId); + } + + @Query(() => [String], { + description: 'List blobs of workspace', + }) + async listBlobs( + @CurrentUser() user: UserType, + @Args('workspaceId') workspaceId: string + ) { + await this.permissionProvider.check(workspaceId, user.id); + + return this.storage.listBlobs(workspaceId); + } + @Mutation(() => String) - async uploadBlob( - @CurrentUser() user: User, + async setBlob( + @CurrentUser() user: UserType, @Args('workspaceId') workspaceId: string, @Args({ name: 'blob', type: () => GraphQLUpload }) blob: FileUpload ) { - await this.permissionProvider.check(workspaceId, user.id); + await this.permissionProvider.check(workspaceId, user.id, Permission.Write); const buffer = await new Promise((resolve, reject) => { const stream = blob.createReadStream(); @@ -341,4 +602,15 @@ export class WorkspaceResolver { return this.storage.uploadBlob(workspaceId, buffer); } + + @Mutation(() => Boolean) + async deleteBlob( + @CurrentUser() user: UserType, + @Args('workspaceId') workspaceId: string, + @Args('hash') hash: string + ) { + await this.permissionProvider.check(workspaceId, user.id); + + return this.storage.deleteBlob(workspaceId, hash); + } } diff --git a/apps/server/src/modules/workspaces/utils.ts b/apps/server/src/modules/workspaces/utils.ts new file mode 100644 index 0000000000..366c500278 --- /dev/null +++ b/apps/server/src/modules/workspaces/utils.ts @@ -0,0 +1,2 @@ +export const defaultWorkspaceAvatar = + 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQtSURBVHgBfVa9jhxFEK6q7rkf+4T2AgdIIC0ZoXkBuNQJtngBuIzs1hIRye1FhL438D0CRgKRGUeE6wwkhHYlkE2AtGdkbN/MdJe/qu7Z27PWnnG5Znq7v/rqd47pHddkNh/918tR1/FBamXc9zxOPVFKfJ4yP86qD1LD3/986/3F2zB40+LXv83HrHq/6+gAoNS1kF4odUz2nhJRTkI5E6mD6Bk1crLJkLy5cHc+P4ohzxLng8RKLqKUq6hkUtBSe8Zvdmfir7TT2a0fnkzeaeCbv/44ztSfZskjP2ygVRM0mbYTpgHMMMS8CsIIj/c+//Hp8UYD3z758whQUwdeEwPjAZQLqJhI0VxB2MVco+kXP/0zuZKD6dP5uM397ELzqEtMba/UJ4t7iXeq8U94z52Q+js09qjlIXMxAEsRDJpI59dVPzlDTooHko7BdlR2FcYmAtbGMmAt2mFI4yDQkIjtEQkxUAMKAPD9SiOK4b578N0S7Nt+fqFKbTbmRD1YGXurEmdtnjjz4kFuIV0gtWewV62hMHBY2gpEOw3Rnmztx9jnO72xzTV/YkzgNmgkiypeYJdCLjonqyAAg7VCshVpjTbD08HbxrySdhKxcDvoJTA5gLvpeXVQ+K340WKea9UkNeZVqGSba/IbF6athj+LUeRmRCyiAVnlAKhJJQfmugGZ28ZWna24RGzwNUNUqpWGf6HkajvAgNA4NsSjHgcb9obx+k5c3DUttcwd3NcHxpVurXQ2d4MZACGw9TwEHsdtbEwytL1xywAGcxavjoH1quLVywuGi+aBhFWexRilFSwK0QzgdUdkkVMeKw4wijrgxjzz2CefCRZn+21ViOWW4Ym9nNnyFLMbMS8ivNhGP8RdlgUojBkuBLDpEPi+5LpWiDURgFkKOIIckJTgN/sZ84KtKkKpDnsOZiTQ47jD4ZGwHghbw6AXIL3lo5Zg6Tp2AwIAyYJ8BRzGfmfPl6kI7HOLUdN2LIg+4IfL5SiFdvkK4blI6h50qda7jQI0CUMLdEhFIkqtQciMvXsgpaZ1pWtVUfrIa+TX5/8+RBcftAhTa91r8ycXA5ZxBqhAh2zgVagUAddxMkxfF/JxfvbpB+8d2jhBtsPhtuqsE0HJlhxYeHKdkCU8xUCos8dmkDdnGaOlJ1yy9dM52J2spqldvz9fTgB4z+aQd2kqjUY2KU2s4dTT7ezD0AqDAbvZiKF/VO9+fGPv9IoBu+b/P5ti6djDY+JlSg4ug1jc6fJbMAx9/3b4CNGTD/evT698D9avv188m4gKvko8MiMeJC3jmOvU9MSuHXZohAVpOrmxd+10HW/jR3/58uU45TRFt35ZR2XpY61DzW+tH3z/7xdM8sP93d3Fm1gbDawbEtU7CMtt/JVxEw01Kh7RAmoBE4+u7eycYv38bRivAZbdHBtPrwOHAAAAAElFTkSuQmCC'; diff --git a/apps/server/src/prisma/service.ts b/apps/server/src/prisma/service.ts index 60e0875cf6..25ac6b92f4 100644 --- a/apps/server/src/prisma/service.ts +++ b/apps/server/src/prisma/service.ts @@ -1,18 +1,17 @@ -import type { INestApplication, OnModuleInit } from '@nestjs/common'; +import type { OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { Injectable } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() -export class PrismaService extends PrismaClient implements OnModuleInit { +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ async onModuleInit() { await this.$connect(); } - async enableShutdownHooks(app: INestApplication) { - process.on('beforeExit', () => { - app.close().catch(e => { - console.error(e); - }); - }); + async onModuleDestroy(): Promise { + await this.$disconnect(); } } diff --git a/apps/server/src/schema.gql b/apps/server/src/schema.gql index d0600020bc..ec7e75f386 100644 --- a/apps/server/src/schema.gql +++ b/apps/server/src/schema.gql @@ -5,25 +5,23 @@ type UserType { id: ID! - """ - User name - """ + """User name""" name: String! - """ - User email - """ + """User email""" email: String! - """ - User avatar url - """ + """User avatar url""" avatarUrl: String - """ - User created date - """ + """User email verified""" + emailVerified: DateTime + + """User created date""" createdAt: DateTime + + """User password has been set""" + hasPassword: Boolean token: TokenType! } @@ -32,48 +30,57 @@ A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date """ scalar DateTime +type DeleteAccount { + success: Boolean! +} + +type AddToNewFeaturesWaitingList { + email: String! + + """New features kind""" + type: NewFeaturesKind! +} + +enum NewFeaturesKind { + EarlyAccess +} + type TokenType { token: String! refresh: String! } -type WorkspaceType { +type InviteUserType { + """User name""" + name: String + + """User email""" + email: String + + """User avatar url""" + avatarUrl: String + + """User email verified""" + emailVerified: DateTime + + """User created date""" + createdAt: DateTime + + """User password has been set""" + hasPassword: Boolean id: ID! - """ - is Public workspace - """ - public: Boolean! - - """ - Workspace created date - """ - createdAt: DateTime! - - """ - Permission of current signed in user in workspace - """ + """User permission in workspace""" permission: Permission! - """ - member count of workspace - """ - memberCount: Int! + """Invite id""" + inviteId: String! - """ - Owner of workspace - """ - owner: UserType! - - """ - Members of workspace - """ - members: [UserType!]! + """User accepted""" + accepted: Boolean! } -""" -User permission in workspace -""" +"""User permission in workspace""" enum Permission { Read Write @@ -81,62 +88,110 @@ enum Permission { Owner } +type WorkspaceType { + id: ID! + + """is Public workspace""" + public: Boolean! + + """Workspace created date""" + createdAt: DateTime! + + """Members of workspace""" + members: [InviteUserType!]! + + """Permission of current signed in user in workspace""" + permission: Permission! + + """member count of workspace""" + memberCount: Int! + + """Shared pages of workspace""" + sharedPages: [String!]! + + """Owner of workspace""" + owner: UserType! +} + +type InvitationWorkspaceType { + id: ID! + + """Workspace name""" + name: String! + + """Base64 encoded avatar""" + avatar: String! +} + +type InvitationType { + """Workspace information""" + workspace: InvitationWorkspaceType! + + """User information""" + user: UserType! +} + type Query { - """ - Get all accessible workspaces for current user - """ + """Get is owner of workspace""" + isOwner(workspaceId: String!): Boolean! + + """Get all accessible workspaces for current user""" workspaces: [WorkspaceType!]! - """ - Get workspace by id - """ + """Get public workspace by id""" + publicWorkspace(id: String!): WorkspaceType! + + """Get workspace by id""" workspace(id: String!): WorkspaceType! - """ - Get user by email - """ - user(email: String!): UserType! + """Update workspace""" + getInviteInfo(inviteId: String!): InvitationType! + + """List blobs of workspace""" + listBlobs(workspaceId: String!): [String!]! + + """Get current user""" + currentUser: UserType! + + """Get user by email""" + user(email: String!): UserType } type Mutation { - register(name: String!, email: String!, password: String!): UserType! - signIn(email: String!, password: String!): UserType! - - """ - Create a new workspace - """ + """Create a new workspace""" createWorkspace(init: Upload!): WorkspaceType! - """ - Update workspace - """ + """Update workspace""" updateWorkspace(input: UpdateWorkspaceInput!): WorkspaceType! deleteWorkspace(id: String!): Boolean! - invite( - workspaceId: String! - email: String! - permission: Permission! - ): Boolean! + invite(workspaceId: String!, email: String!, permission: Permission!, sendInviteMail: Boolean): String! revoke(workspaceId: String!, userId: String!): Boolean! + acceptInviteById(workspaceId: String!, inviteId: String!): Boolean! acceptInvite(workspaceId: String!): Boolean! leaveWorkspace(workspaceId: String!): Boolean! - uploadBlob(workspaceId: String!, blob: Upload!): String! + sharePage(workspaceId: String!, pageId: String!): Boolean! + revokePage(workspaceId: String!, pageId: String!): Boolean! + setBlob(workspaceId: String!, blob: Upload!): String! + deleteBlob(workspaceId: String!, hash: String!): Boolean! - """ - Upload user avatar - """ + """Upload user avatar""" uploadAvatar(id: String!, avatar: Upload!): UserType! + deleteAccount: DeleteAccount! + addToNewFeaturesWaitingList(type: NewFeaturesKind!, email: String!): AddToNewFeaturesWaitingList! + signUp(name: String!, email: String!, password: String!): UserType! + signIn(email: String!, password: String!): UserType! + changePassword(id: String!, newPassword: String!): UserType! + changeEmail(id: String!, email: String!): UserType! + sendChangePasswordEmail(email: String!, callbackUrl: String!): Boolean! + sendSetPasswordEmail(email: String!, callbackUrl: String!): Boolean! + sendChangeEmail(email: String!, callbackUrl: String!): Boolean! } -""" -The `Upload` scalar type represents a file upload. -""" +"""The `Upload` scalar type represents a file upload.""" scalar Upload input UpdateWorkspaceInput { - """ - is Public workspace - """ + """is Public workspace""" public: Boolean id: ID! -} +} \ No newline at end of file diff --git a/apps/server/src/storage/index.ts b/apps/server/src/storage/index.ts index 22525bf0cd..c062662e0f 100644 --- a/apps/server/src/storage/index.ts +++ b/apps/server/src/storage/index.ts @@ -1,28 +1,25 @@ import { createRequire } from 'node:module'; -import type { Storage } from '@affine/storage'; import { type DynamicModule, type FactoryProvider } from '@nestjs/common'; import { Config } from '../config'; export const StorageProvide = Symbol('Storage'); -const require = createRequire(import.meta.url); +let storageModule: typeof import('@affine/storage'); +try { + storageModule = await import('@affine/storage'); +} catch { + const require = createRequire(import.meta.url); + storageModule = require('../../storage.node'); +} export class StorageModule { static forRoot(): DynamicModule { const storageProvider: FactoryProvider = { provide: StorageProvide, useFactory: async (config: Config) => { - let StorageFactory: typeof Storage; - try { - // dev mode - StorageFactory = (await import('@affine/storage')).Storage; - } catch { - // In docker - StorageFactory = require('../../storage.node').Storage; - } - return StorageFactory.connect(config.db.url); + return storageModule.Storage.connect(config.db.url); }, inject: [Config], }; @@ -35,3 +32,5 @@ export class StorageModule { }; } } + +export const mergeUpdatesInApplyWay = storageModule.mergeUpdatesInApplyWay; diff --git a/apps/server/src/tests/app.e2e.ts b/apps/server/src/tests/app.e2e.ts index a74d2b9096..41eb7206a4 100644 --- a/apps/server/src/tests/app.e2e.ts +++ b/apps/server/src/tests/app.e2e.ts @@ -30,6 +30,7 @@ describe('AppModule', () => { password: await hash('123456'), }, }); + await client.$disconnect(); }); beforeEach(async () => { diff --git a/apps/server/src/tests/auth.spec.ts b/apps/server/src/tests/auth.spec.ts index feb72d0aa4..dc36fd2e66 100644 --- a/apps/server/src/tests/auth.spec.ts +++ b/apps/server/src/tests/auth.spec.ts @@ -1,17 +1,19 @@ /// -import { ok } from 'node:assert'; -import { beforeEach, test } from 'node:test'; +import { equal } from 'node:assert'; +import { afterEach, beforeEach, test } from 'node:test'; -import { Test } from '@nestjs/testing'; +import { Test, TestingModule } from '@nestjs/testing'; import { PrismaClient } from '@prisma/client'; import { ConfigModule } from '../config'; import { GqlModule } from '../graphql.module'; +import { MetricsModule } from '../metrics'; import { AuthModule } from '../modules/auth'; import { AuthService } from '../modules/auth/service'; import { PrismaModule } from '../prisma'; let auth: AuthService; +let module: TestingModule; // cleanup database before each test beforeEach(async () => { @@ -21,7 +23,7 @@ beforeEach(async () => { }); beforeEach(async () => { - const module = await Test.createTestingModule({ + module = await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ auth: { @@ -33,37 +35,50 @@ beforeEach(async () => { PrismaModule, GqlModule, AuthModule, + MetricsModule, ], }).compile(); auth = module.get(AuthService); }); +afterEach(async () => { + await module.close(); +}); + test('should be able to register and signIn', async () => { - await auth.register('Alex Yang', 'alexyang@example.org', '123456'); + await auth.signUp('Alex Yang', 'alexyang@example.org', '123456'); await auth.signIn('alexyang@example.org', '123456'); }); test('should be able to verify', async () => { - await auth.register('Alex Yang', 'alexyang@example.org', '123456'); + await auth.signUp('Alex Yang', 'alexyang@example.org', '123456'); await auth.signIn('alexyang@example.org', '123456'); + const date = new Date(); + const user = { id: '1', name: 'Alex Yang', email: 'alexyang@example.org', - createdAt: new Date(), + emailVerified: date, + createdAt: date, + avatarUrl: '', }; { const token = await auth.sign(user); const claim = await auth.verify(token); - ok(claim.id === '1'); - ok(claim.name === 'Alex Yang'); - ok(claim.email === 'alexyang@example.org'); + equal(claim.id, '1'); + equal(claim.name, 'Alex Yang'); + equal(claim.email, 'alexyang@example.org'); + equal(claim.emailVerified?.toISOString(), date.toISOString()); + equal(claim.createdAt.toISOString(), date.toISOString()); } { const token = await auth.refresh(user); const claim = await auth.verify(token); - ok(claim.id === '1'); - ok(claim.name === 'Alex Yang'); - ok(claim.email === 'alexyang@example.org'); + equal(claim.id, '1'); + equal(claim.name, 'Alex Yang'); + equal(claim.email, 'alexyang@example.org'); + equal(claim.emailVerified?.toISOString(), date.toISOString()); + equal(claim.createdAt.toISOString(), date.toISOString()); } }); diff --git a/apps/server/src/tests/doc.spec.ts b/apps/server/src/tests/doc.spec.ts new file mode 100644 index 0000000000..00865c921b --- /dev/null +++ b/apps/server/src/tests/doc.spec.ts @@ -0,0 +1,158 @@ +import { deepEqual, equal, ok } from 'node:assert'; +import { afterEach, beforeEach, mock, test } from 'node:test'; + +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { register } from 'prom-client'; +import * as Sinon from 'sinon'; +import { Doc as YDoc, encodeStateAsUpdate } from 'yjs'; + +import { Config, ConfigModule } from '../config'; +import { MetricsModule } from '../metrics'; +import { DocManager, DocModule } from '../modules/doc'; +import { PrismaModule, PrismaService } from '../prisma'; +import { flushDB } from './utils'; + +const createModule = () => { + return Test.createTestingModule({ + imports: [ + PrismaModule, + MetricsModule, + ConfigModule.forRoot(), + DocModule.forRoot(), + ], + }).compile(); +}; + +test('Doc Module', async t => { + let app: INestApplication; + let m: TestingModule; + let timer: Sinon.SinonFakeTimers; + + // cleanup database before each test + beforeEach(async () => { + timer = Sinon.useFakeTimers({ + toFake: ['setInterval'], + }); + await flushDB(); + m = await createModule(); + app = m.createNestApplication(); + app.enableShutdownHooks(); + await app.init(); + }); + + afterEach(async () => { + await app.close(); + timer.restore(); + }); + + await t.test('should setup update poll interval', async () => { + register.clear(); + const m = await createModule(); + const manager = m.get(DocManager); + const fake = mock.method(manager, 'setup'); + + await m.createNestApplication().init(); + + equal(fake.mock.callCount(), 1); + // @ts-expect-error private member + ok(manager.job); + }); + + await t.test('should be able to stop poll', async () => { + const manager = m.get(DocManager); + const fake = mock.method(manager, 'destroy'); + + await app.close(); + + equal(fake.mock.callCount(), 1); + // @ts-expect-error private member + equal(manager.job, null); + }); + + await t.test('should poll when intervel due', async () => { + const manager = m.get(DocManager); + const interval = m.get(Config).doc.manager.updatePollInterval; + + let resolve: any; + const fake = mock.method(manager, 'apply', () => { + return new Promise(_resolve => { + resolve = _resolve; + }); + }); + + timer.tick(interval); + equal(fake.mock.callCount(), 1); + + // busy + timer.tick(interval); + // @ts-expect-error private member + equal(manager.busy, true); + equal(fake.mock.callCount(), 1); + + resolve(); + await timer.tickAsync(1); + + // @ts-expect-error private member + equal(manager.busy, false); + timer.tick(interval); + equal(fake.mock.callCount(), 2); + }); + + await t.test('should merge update when intervel due', async () => { + const db = m.get(PrismaService); + const manager = m.get(DocManager); + + const doc = new YDoc(); + const text = doc.getText('content'); + text.insert(0, 'hello'); + const update = encodeStateAsUpdate(doc); + + const ws = await db.workspace.create({ + data: { + id: '1', + public: false, + }, + }); + + await db.update.createMany({ + data: [ + { + id: '1', + workspaceId: '1', + blob: Buffer.from([0, 0]), + }, + { + id: '1', + workspaceId: '1', + blob: Buffer.from(update), + }, + ], + }); + + await manager.apply(); + + deepEqual(await manager.getLatestUpdate(ws.id, '1'), update); + + let appendUpdate = Buffer.from([]); + doc.on('update', update => { + appendUpdate = Buffer.from(update); + }); + text.insert(5, 'world'); + + await db.update.create({ + data: { + workspaceId: ws.id, + id: '1', + blob: appendUpdate, + }, + }); + + await manager.apply(); + + deepEqual( + await manager.getLatestUpdate(ws.id, '1'), + encodeStateAsUpdate(doc) + ); + }); +}); diff --git a/apps/server/src/tests/mailer.spec.ts b/apps/server/src/tests/mailer.spec.ts new file mode 100644 index 0000000000..f641f69fa7 --- /dev/null +++ b/apps/server/src/tests/mailer.spec.ts @@ -0,0 +1,86 @@ +import { ok } from 'node:assert'; +import { afterEach, beforeEach, describe, it } from 'node:test'; + +import type { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { PrismaClient } from '@prisma/client'; +// @ts-expect-error graphql-upload is not typed +import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; + +import { AppModule } from '../app'; +import { MailService } from '../modules/auth/mailer'; +import { createWorkspace, getInviteInfo, inviteUser, signUp } from './utils'; + +describe('Mail Module', () => { + let app: INestApplication; + + const client = new PrismaClient(); + + let mail: MailService; + + // cleanup database before each test + beforeEach(async () => { + await client.$connect(); + await client.user.deleteMany({}); + await client.snapshot.deleteMany({}); + await client.update.deleteMany({}); + await client.workspace.deleteMany({}); + await client.$disconnect(); + }); + + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = module.createNestApplication(); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + await app.init(); + + mail = module.get(MailService); + }); + + afterEach(async () => { + await app.close(); + }); + + it('should send invite email', async () => { + if (mail.hasConfigured()) { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + const inviteId = await inviteUser( + app, + u1.token.token, + workspace.id, + u2.email, + 'Admin' + ); + + const inviteInfo = await getInviteInfo(app, u1.token.token, inviteId); + + const resp = await mail.sendInviteEmail( + 'production@toeverything.info', + inviteId, + { + workspace: { + id: inviteInfo.workspace.id, + name: inviteInfo.workspace.name, + avatar: '', + }, + user: { + avatar: inviteInfo.user?.avatarUrl || '', + name: inviteInfo.user?.name || '', + }, + } + ); + + ok(resp.accepted.length === 1, 'failed to send invite email'); + } + }); +}); diff --git a/apps/server/src/tests/prometheus-metrics.spec.ts b/apps/server/src/tests/prometheus-metrics.spec.ts new file mode 100644 index 0000000000..c4feddd58a --- /dev/null +++ b/apps/server/src/tests/prometheus-metrics.spec.ts @@ -0,0 +1,61 @@ +import { ok } from 'node:assert'; +import { afterEach, beforeEach, test } from 'node:test'; + +import { Test, TestingModule } from '@nestjs/testing'; +import { register } from 'prom-client'; + +import { MetricsModule } from '../metrics'; +import { Metrics } from '../metrics/metrics'; +import { PrismaModule } from '../prisma'; + +let metrics: Metrics; +let module: TestingModule; + +beforeEach(async () => { + module = await Test.createTestingModule({ + imports: [MetricsModule, PrismaModule], + }).compile(); + + metrics = module.get(Metrics); +}); + +afterEach(async () => { + await module.close(); +}); + +test('should be able to increment counter', async () => { + metrics.socketIOEventCounter(1, { event: 'client-handshake' }); + const socketIOCounterMetric = + await register.getSingleMetric('socket_io_counter'); + ok(socketIOCounterMetric); + + ok( + JSON.stringify((await socketIOCounterMetric.get()).values) === + '[{"value":1,"labels":{"event":"client-handshake"}}]' + ); +}); + +test('should be able to timer', async () => { + const endTimer = metrics.socketIOEventTimer({ event: 'client-handshake' }); + await new Promise(resolve => setTimeout(resolve, 50)); + endTimer(); + + const endTimer2 = metrics.socketIOEventTimer({ event: 'client-handshake' }); + await new Promise(resolve => setTimeout(resolve, 100)); + endTimer2(); + + const socketIOTimerMetric = await register.getSingleMetric('socket_io_timer'); + ok(socketIOTimerMetric); + + const observations = (await socketIOTimerMetric.get()).values; + + for (const observation of observations) { + if ( + observation.labels.event === 'client-handshake' && + 'quantile' in observation.labels + ) { + ok(observation.value >= 0.05); + ok(observation.value <= 0.15); + } + } +}); diff --git a/apps/server/src/tests/user.spec.ts b/apps/server/src/tests/user.spec.ts new file mode 100644 index 0000000000..8b468fb4c2 --- /dev/null +++ b/apps/server/src/tests/user.spec.ts @@ -0,0 +1,77 @@ +import { ok } from 'node:assert'; +import { afterEach, beforeEach, describe, it } from 'node:test'; + +import type { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { PrismaClient } from '@prisma/client'; +// @ts-expect-error graphql-upload is not typed +import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; +import request from 'supertest'; + +import { AppModule } from '../app'; +import { currentUser, signUp } from './utils'; + +describe('User Module', () => { + let app: INestApplication; + + // cleanup database before each test + beforeEach(async () => { + const client = new PrismaClient(); + await client.$connect(); + await client.user.deleteMany({}); + await client.$disconnect(); + }); + + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = module.createNestApplication(); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + await app.init(); + }); + + afterEach(async () => { + await app.close(); + }); + + it('should register a user', async () => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); + ok(typeof user.id === 'string', 'user.id is not a string'); + ok(user.name === 'u1', 'user.name is not valid'); + ok(user.email === 'u1@affine.pro', 'user.email is not valid'); + }); + + it('should get current user', async () => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); + const currUser = await currentUser(app, user.token.token); + ok(currUser.id === user.id, 'user.id is not valid'); + ok(currUser.name === user.name, 'user.name is not valid'); + ok(currUser.email === user.email, 'user.email is not valid'); + ok(currUser.hasPassword, 'currUser.hasPassword is not valid'); + }); + + it('should be able to delete user', async () => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); + await request(app.getHttpServer()) + .post('/graphql') + .auth(user.token.token, { type: 'bearer' }) + .send({ + query: ` + mutation { + deleteAccount { + success + } + } + `, + }) + .expect(200); + const current = await currentUser(app, user.token.token); + ok(current == null); + }); +}); diff --git a/apps/server/src/tests/utils.ts b/apps/server/src/tests/utils.ts new file mode 100644 index 0000000000..9dad573481 --- /dev/null +++ b/apps/server/src/tests/utils.ts @@ -0,0 +1,465 @@ +import type { INestApplication, LoggerService } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { PrismaClient } from '@prisma/client'; +// @ts-expect-error graphql-upload is not typed +import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; +import request from 'supertest'; + +import { AppModule } from '../app'; +import type { TokenType } from '../modules/auth'; +import type { UserType } from '../modules/users'; +import type { InvitationType, WorkspaceType } from '../modules/workspaces'; + +export class NestDebugLogger implements LoggerService { + log(message: string): any { + console.log(message); + } + + error(message: string, trace: string): any { + console.error(message, trace); + } + + warn(message: string): any { + console.warn(message); + } + + debug(message: string): any { + console.debug(message); + } + + verbose(message: string): any { + console.log(message); + } +} + +const gql = '/graphql'; + +async function signUp( + app: INestApplication, + name: string, + email: string, + password: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + signUp(name: "${name}", email: "${email}", password: "${password}") { + id, name, email, token { token } + } + } + `, + }) + .expect(200); + return res.body.data.signUp; +} + +async function currentUser(app: INestApplication, token: string) { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + query { + currentUser { + id, name, email, emailVerified, avatarUrl, createdAt, hasPassword + } + } + `, + }) + .expect(200); + return res.body?.data?.currentUser; +} + +async function createWorkspace( + app: INestApplication, + token: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .field( + 'operations', + JSON.stringify({ + name: 'createWorkspace', + query: `mutation createWorkspace($init: Upload!) { + createWorkspace(init: $init) { + id + } + }`, + variables: { init: null }, + }) + ) + .field('map', JSON.stringify({ '0': ['variables.init'] })) + .attach('0', Buffer.from([0, 0]), 'init.data') + .expect(200); + return res.body.data.createWorkspace; +} + +export async function getWorkspaceSharedPages( + app: INestApplication, + token: string, + workspaceId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + query { + workspace(id: "${workspaceId}") { + sharedPages + } + } + `, + }) + .expect(200); + return res.body.data.workspace.sharedPages; +} + +async function getWorkspace( + app: INestApplication, + token: string, + workspaceId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + query { + workspace(id: "${workspaceId}") { + id, members { id, name, email, permission, inviteId } + } + } + `, + }) + .expect(200); + return res.body.data.workspace; +} + +async function getPublicWorkspace( + app: INestApplication, + workspaceId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + query { + publicWorkspace(id: "${workspaceId}") { + id + } + } + `, + }) + .expect(200); + return res.body.data.publicWorkspace; +} + +async function updateWorkspace( + app: INestApplication, + token: string, + workspaceId: string, + isPublic: boolean +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + updateWorkspace(input: { id: "${workspaceId}", public: ${isPublic} }) { + public + } + } + `, + }) + .expect(200); + return res.body.data.updateWorkspace.public; +} + +async function inviteUser( + app: INestApplication, + token: string, + workspaceId: string, + email: string, + permission: string, + sendInviteMail = false +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + invite(workspaceId: "${workspaceId}", email: "${email}", permission: ${permission}, sendInviteMail: ${sendInviteMail}) + } + `, + }) + .expect(200); + return res.body.data.invite; +} + +async function acceptInviteById( + app: INestApplication, + workspaceId: string, + inviteId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + acceptInviteById(workspaceId: "${workspaceId}", inviteId: "${inviteId}") + } + `, + }) + .expect(200); + return res.body.data.acceptInviteById; +} + +async function acceptInvite( + app: INestApplication, + token: string, + workspaceId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + acceptInvite(workspaceId: "${workspaceId}") + } + `, + }) + .expect(200); + return res.body.data.acceptInvite; +} + +async function leaveWorkspace( + app: INestApplication, + token: string, + workspaceId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + leaveWorkspace(workspaceId: "${workspaceId}") + } + `, + }) + .expect(200); + return res.body.data.leaveWorkspace; +} + +async function revokeUser( + app: INestApplication, + token: string, + workspaceId: string, + userId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + revoke(workspaceId: "${workspaceId}", userId: "${userId}") + } + `, + }) + .expect(200); + return res.body.data.revoke; +} + +async function sharePage( + app: INestApplication, + token: string, + workspaceId: string, + pageId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + sharePage(workspaceId: "${workspaceId}", pageId: "${pageId}") + } + `, + }) + .expect(200); + return res.body.errors?.[0]?.message || res.body.data?.sharePage; +} + +async function revokePage( + app: INestApplication, + token: string, + workspaceId: string, + pageId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + revokePage(workspaceId: "${workspaceId}", pageId: "${pageId}") + } + `, + }) + .expect(200); + return res.body.errors?.[0]?.message || res.body.data?.revokePage; +} + +async function listBlobs( + app: INestApplication, + token: string, + workspaceId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + query { + listBlobs(workspaceId: "${workspaceId}") + } + `, + }) + .expect(200); + return res.body.data.listBlobs; +} + +async function setBlob( + app: INestApplication, + token: string, + workspaceId: string, + buffer: Buffer +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .field( + 'operations', + JSON.stringify({ + name: 'setBlob', + query: `mutation setBlob($blob: Upload!) { + setBlob(workspaceId: "${workspaceId}", blob: $blob) + }`, + variables: { blob: null }, + }) + ) + .field('map', JSON.stringify({ '0': ['variables.blob'] })) + .attach('0', buffer, 'blob.data') + .expect(200); + return res.body.data.setBlob; +} + +async function flushDB() { + const client = new PrismaClient(); + await client.$connect(); + const result: { tablename: string }[] = + await client.$queryRaw`SELECT tablename + FROM pg_catalog.pg_tables + WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'`; + + // remove all table data + await client.$executeRawUnsafe( + `TRUNCATE TABLE ${result + .map(({ tablename }) => tablename) + .filter(name => !name.includes('migrations')) + .join(', ')}` + ); + + await client.$disconnect(); +} + +async function createTestApp() { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + const app = module.createNestApplication(); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + await app.init(); + return app; +} + +async function getInviteInfo( + app: INestApplication, + token: string, + inviteId: string +): Promise { + const res = await request(app.getHttpServer()) + .post(gql) + .auth(token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + query { + getInviteInfo(inviteId: "${inviteId}") { + workspace { + id + name + avatar + } + user { + id + name + avatarUrl + } + } + } + `, + }) + .expect(200); + return res.body.data.workspace; +} + +export { + acceptInvite, + acceptInviteById, + createTestApp, + createWorkspace, + currentUser, + flushDB, + getInviteInfo, + getPublicWorkspace, + getWorkspace, + inviteUser, + leaveWorkspace, + listBlobs, + revokePage, + revokeUser, + setBlob, + sharePage, + signUp, + updateWorkspace, +}; diff --git a/apps/server/src/tests/workspace-blobs.spec.ts b/apps/server/src/tests/workspace-blobs.spec.ts new file mode 100644 index 0000000000..3295f51a26 --- /dev/null +++ b/apps/server/src/tests/workspace-blobs.spec.ts @@ -0,0 +1,70 @@ +import { deepEqual, ok } from 'node:assert'; +import { afterEach, beforeEach, describe, it } from 'node:test'; + +import type { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { PrismaClient } from '@prisma/client'; +// @ts-expect-error graphql-upload is not typed +import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; +import request from 'supertest'; + +import { AppModule } from '../app'; +import { createWorkspace, listBlobs, setBlob, signUp } from './utils'; + +describe('Workspace Module - Blobs', () => { + let app: INestApplication; + + const client = new PrismaClient(); + + // cleanup database before each test + beforeEach(async () => { + await client.$connect(); + await client.user.deleteMany({}); + await client.snapshot.deleteMany({}); + await client.update.deleteMany({}); + await client.workspace.deleteMany({}); + await client.$disconnect(); + }); + + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = module.createNestApplication(); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + await app.init(); + }); + + afterEach(async () => { + await app.close(); + }); + + it('should list blobs', async () => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + const blobs = await listBlobs(app, u1.token.token, workspace.id); + ok(blobs.length === 0, 'failed to list blobs'); + + const buffer = Buffer.from([0, 0]); + const hash = await setBlob(app, u1.token.token, workspace.id, buffer); + + const ret = await listBlobs(app, u1.token.token, workspace.id); + ok(ret.length === 1, 'failed to list blobs'); + ok(ret[0] === hash, 'failed to list blobs'); + const server = app.getHttpServer(); + + const token = u1.token.token; + const response = await request(server) + .get(`/api/workspaces/${workspace.id}/blobs/${hash}`) + .auth(token, { type: 'bearer' }) + .buffer(); + + deepEqual(response.body, buffer, 'failed to get blob'); + }); +}); diff --git a/apps/server/src/tests/workspace-invite.spec.ts b/apps/server/src/tests/workspace-invite.spec.ts new file mode 100644 index 0000000000..010239d76f --- /dev/null +++ b/apps/server/src/tests/workspace-invite.spec.ts @@ -0,0 +1,189 @@ +import { ok } from 'node:assert'; +import { afterEach, beforeEach, describe, it } from 'node:test'; + +import type { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { PrismaClient } from '@prisma/client'; +// @ts-expect-error graphql-upload is not typed +import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; + +import { AppModule } from '../app'; +import { MailService } from '../modules/auth/mailer'; +import { AuthService } from '../modules/auth/service'; +import { + acceptInvite, + acceptInviteById, + createWorkspace, + getWorkspace, + inviteUser, + leaveWorkspace, + revokeUser, + signUp, +} from './utils'; + +describe('Workspace Module - invite', () => { + let app: INestApplication; + + const client = new PrismaClient(); + + let auth: AuthService; + let mail: MailService; + + // cleanup database before each test + beforeEach(async () => { + await client.$connect(); + await client.user.deleteMany({}); + await client.snapshot.deleteMany({}); + await client.update.deleteMany({}); + await client.workspace.deleteMany({}); + await client.$disconnect(); + }); + + beforeEach(async () => { + const module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = module.createNestApplication(); + app.use( + graphqlUploadExpress({ + maxFileSize: 10 * 1024 * 1024, + maxFiles: 5, + }) + ); + await app.init(); + + auth = module.get(AuthService); + mail = module.get(MailService); + }); + + afterEach(async () => { + await app.close(); + }); + + it('should invite a user', async () => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + + const invite = await inviteUser( + app, + u1.token.token, + workspace.id, + u2.email, + 'Admin' + ); + ok(!!invite, 'failed to invite user'); + }); + + it('should accept an invite', async () => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); + + const accept = await acceptInvite(app, u2.token.token, workspace.id); + ok(accept === true, 'failed to accept invite'); + + const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); + const currMember = currWorkspace.members.find(u => u.email === u2.email); + ok(currMember !== undefined, 'failed to invite user'); + ok(currMember.id === u2.id, 'failed to invite user'); + ok(!currMember.accepted, 'failed to invite user'); + }); + + it('should leave a workspace', async () => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); + await acceptInvite(app, u2.token.token, workspace.id); + + const leave = await leaveWorkspace(app, u2.token.token, workspace.id); + ok(leave === true, 'failed to leave workspace'); + }); + + it('should revoke a user', async () => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); + + const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); + ok(currWorkspace.members.length === 2, 'failed to invite user'); + + const revoke = await revokeUser(app, u1.token.token, workspace.id, u2.id); + ok(revoke === true, 'failed to revoke user'); + }); + + it('should create user if not exist', async () => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + + await inviteUser( + app, + u1.token.token, + workspace.id, + 'u2@affine.pro', + 'Admin' + ); + + const user = await auth.getUserByEmail('u2@affine.pro'); + ok(user !== undefined, 'failed to create user'); + ok(user?.name === 'Unnamed', 'failed to create user'); + }); + + it('should invite a user by link', async () => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + + const invite = await inviteUser( + app, + u1.token.token, + workspace.id, + u2.email, + 'Admin' + ); + + const accept = await acceptInviteById(app, workspace.id, invite); + ok(accept === true, 'failed to accept invite'); + + const invite1 = await inviteUser( + app, + u1.token.token, + workspace.id, + u2.email, + 'Admin' + ); + + ok(invite === invite1, 'repeat the invitation must return same id'); + + const currWorkspace = await getWorkspace(app, u1.token.token, workspace.id); + const currMember = currWorkspace.members.find(u => u.email === u2.email); + ok(currMember !== undefined, 'failed to invite user'); + ok(currMember.inviteId === invite, 'failed to check invite id'); + }); + + it('should send invite email', async () => { + if (mail.hasConfigured()) { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'test', 'production@toeverything.info', '1'); + + const workspace = await createWorkspace(app, u1.token.token); + await inviteUser( + app, + u1.token.token, + workspace.id, + u2.email, + 'Admin', + true + ); + } + }); +}); diff --git a/apps/server/src/tests/workspace.spec.ts b/apps/server/src/tests/workspace.spec.ts index c590ff2332..3035fbab15 100644 --- a/apps/server/src/tests/workspace.spec.ts +++ b/apps/server/src/tests/workspace.spec.ts @@ -1,4 +1,4 @@ -import { ok } from 'node:assert'; +import { deepEqual, ok, rejects } from 'node:assert'; import { afterEach, beforeEach, describe, it } from 'node:test'; import type { INestApplication } from '@nestjs/common'; @@ -9,20 +9,30 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'; import request from 'supertest'; import { AppModule } from '../app'; -import type { TokenType } from '../modules/auth'; -import type { UserType } from '../modules/users'; -import type { WorkspaceType } from '../modules/workspaces'; - -const gql = '/graphql'; +import { + acceptInvite, + createWorkspace, + getPublicWorkspace, + getWorkspaceSharedPages, + inviteUser, + revokePage, + sharePage, + signUp, + updateWorkspace, +} from './utils'; describe('Workspace Module', () => { let app: INestApplication; + const client = new PrismaClient(); + // cleanup database before each test beforeEach(async () => { - const client = new PrismaClient(); await client.$connect(); await client.user.deleteMany({}); + await client.update.deleteMany({}); + await client.snapshot.deleteMany({}); + await client.workspace.deleteMany({}); await client.$disconnect(); }); @@ -44,183 +54,177 @@ describe('Workspace Module', () => { await app.close(); }); - async function registerUser( - name: string, - email: string, - password: string - ): Promise { - const res = await request(app.getHttpServer()) - .post(gql) - .send({ - query: ` - mutation { - register(name: "${name}", email: "${email}", password: "${password}") { - id, name, email, token { token } - } - } - `, - }) - .expect(200); - return res.body.data.register; - } - - async function createWorkspace(token: string): Promise { - const res = await request(app.getHttpServer()) - .post(gql) - .auth(token, { type: 'bearer' }) - .field( - 'operations', - JSON.stringify({ - name: 'createWorkspace', - query: `mutation createWorkspace($init: Upload!) { - createWorkspace(init: $init) { - id - } - }`, - variables: { init: null }, - }) - ) - .field('map', JSON.stringify({ '0': ['variables.init'] })) - .attach('0', Buffer.from([0, 0]), 'init.data') - .expect(200); - return res.body.data.createWorkspace; - } - - async function inviteUser( - token: string, - workspaceId: string, - email: string, - permission: string - ): Promise { - const res = await request(app.getHttpServer()) - .post(gql) - .auth(token, { type: 'bearer' }) - .send({ - query: ` - mutation { - invite(workspaceId: "${workspaceId}", email: "${email}", permission: ${permission}) - } - `, - }) - .expect(200); - return res.body.data.invite; - } - - async function acceptInvite( - token: string, - workspaceId: string - ): Promise { - const res = await request(app.getHttpServer()) - .post(gql) - .auth(token, { type: 'bearer' }) - .send({ - query: ` - mutation { - acceptInvite(workspaceId: "${workspaceId}") - } - `, - }) - .expect(200); - return res.body.data.acceptInvite; - } - - async function leaveWorkspace( - token: string, - workspaceId: string - ): Promise { - const res = await request(app.getHttpServer()) - .post(gql) - .auth(token, { type: 'bearer' }) - .send({ - query: ` - mutation { - leaveWorkspace(workspaceId: "${workspaceId}") - } - `, - }) - .expect(200); - return res.body.data.leaveWorkspace; - } - - async function revokeUser( - token: string, - workspaceId: string, - userId: string - ): Promise { - const res = await request(app.getHttpServer()) - .post(gql) - .auth(token, { type: 'bearer' }) - .send({ - query: ` - mutation { - revoke(workspaceId: "${workspaceId}", userId: "${userId}") - } - `, - }) - .expect(200); - return res.body.data.revoke; - } - it('should register a user', async () => { - const user = await registerUser('u1', 'u1@affine.pro', '123456'); + const user = await signUp(app, 'u1', 'u1@affine.pro', '123456'); ok(typeof user.id === 'string', 'user.id is not a string'); ok(user.name === 'u1', 'user.name is not valid'); ok(user.email === 'u1@affine.pro', 'user.email is not valid'); }); it('should create a workspace', async () => { - const user = await registerUser('u1', 'u1@affine.pro', '1'); + const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); - const workspace = await createWorkspace(user.token.token); + const workspace = await createWorkspace(app, user.token.token); ok(typeof workspace.id === 'string', 'workspace.id is not a string'); }); - it('should invite a user', async () => { - const u1 = await registerUser('u1', 'u1@affine.pro', '1'); - const u2 = await registerUser('u2', 'u2@affine.pro', '1'); + it('should can publish workspace', async () => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const workspace = await createWorkspace(app, user.token.token); - const workspace = await createWorkspace(u1.token.token); - - const invite = await inviteUser( - u1.token.token, + const isPublic = await updateWorkspace( + app, + user.token.token, workspace.id, - u2.email, - 'Admin' + true ); - ok(invite === true, 'failed to invite user'); + ok(isPublic === true, 'failed to publish workspace'); + + const isPrivate = await updateWorkspace( + app, + user.token.token, + workspace.id, + false + ); + ok(isPrivate === false, 'failed to unpublish workspace'); }); - it('should accept an invite', async () => { - const u1 = await registerUser('u1', 'u1@affine.pro', '1'); - const u2 = await registerUser('u2', 'u2@affine.pro', '1'); + it('should can read published workspace', async () => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const workspace = await createWorkspace(app, user.token.token); - const workspace = await createWorkspace(u1.token.token); - await inviteUser(u1.token.token, workspace.id, u2.email, 'Admin'); + rejects( + getPublicWorkspace(app, 'not_exists_ws'), + 'must not get not exists workspace' + ); + rejects( + getPublicWorkspace(app, workspace.id), + 'must not get private workspace' + ); - const accept = await acceptInvite(u2.token.token, workspace.id); - ok(accept === true, 'failed to accept invite'); + await updateWorkspace(app, user.token.token, workspace.id, true); + + const publicWorkspace = await getPublicWorkspace(app, workspace.id); + ok(publicWorkspace.id === workspace.id, 'failed to get public workspace'); }); - it('should leave a workspace', async () => { - const u1 = await registerUser('u1', 'u1@affine.pro', '1'); - const u2 = await registerUser('u2', 'u2@affine.pro', '1'); + it('should share a page', async () => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '1'); - const workspace = await createWorkspace(u1.token.token); - await inviteUser(u1.token.token, workspace.id, u2.email, 'Admin'); - await acceptInvite(u2.token.token, workspace.id); + const workspace = await createWorkspace(app, u1.token.token); - const leave = await leaveWorkspace(u2.token.token, workspace.id); - ok(leave === true, 'failed to leave workspace'); + const share = await sharePage(app, u1.token.token, workspace.id, 'page1'); + ok(share === true, 'failed to share page'); + const pages = await getWorkspaceSharedPages( + app, + u1.token.token, + workspace.id + ); + ok(pages.length === 1, 'failed to get shared pages'); + ok(pages[0] === 'page1', 'failed to get shared page: page1'); + + const msg1 = await sharePage(app, u2.token.token, workspace.id, 'page2'); + ok(msg1 === 'Permission denied', 'unauthorized user can share page'); + const msg2 = await revokePage( + app, + u2.token.token, + 'not_exists_ws', + 'page2' + ); + ok(msg2 === 'Permission denied', 'unauthorized user can share page'); + + await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); + await acceptInvite(app, u2.token.token, workspace.id); + const invited = await sharePage(app, u2.token.token, workspace.id, 'page2'); + ok(invited === true, 'failed to share page'); + + const revoke = await revokePage(app, u1.token.token, workspace.id, 'page1'); + ok(revoke === true, 'failed to revoke page'); + const pages2 = await getWorkspaceSharedPages( + app, + u1.token.token, + workspace.id + ); + ok(pages2.length === 1, 'failed to get shared pages'); + ok(pages2[0] === 'page2', 'failed to get shared page: page2'); + + const msg3 = await revokePage(app, u1.token.token, workspace.id, 'page3'); + ok(msg3 === false, 'can revoke non-exists page'); + + const msg4 = await revokePage(app, u1.token.token, workspace.id, 'page2'); + ok(msg4 === true, 'failed to revoke page'); + const page3 = await getWorkspaceSharedPages( + app, + u1.token.token, + workspace.id + ); + ok(page3.length === 0, 'failed to get shared pages'); }); - it('should revoke a user', async () => { - const u1 = await registerUser('u1', 'u1@affine.pro', '1'); - const u2 = await registerUser('u2', 'u2@affine.pro', '1'); + it('should can get workspace doc', async () => { + const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const u2 = await signUp(app, 'u2', 'u2@affine.pro', '2'); + const workspace = await createWorkspace(app, u1.token.token); - const workspace = await createWorkspace(u1.token.token); - await inviteUser(u1.token.token, workspace.id, u2.email, 'Admin'); + const res1 = await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .auth(u1.token.token, { type: 'bearer' }) + .expect(200) + .type('application/octet-stream'); - const revoke = await revokeUser(u1.token.token, workspace.id, u2.id); - ok(revoke === true, 'failed to revoke user'); + deepEqual( + res1.body, + Buffer.from([0, 0]), + 'failed to get doc with u1 token' + ); + + await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .expect(403); + await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .auth(u2.token.token, { type: 'bearer' }) + .expect(403); + + await inviteUser(app, u1.token.token, workspace.id, u2.email, 'Admin'); + await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .auth(u2.token.token, { type: 'bearer' }) + .expect(403); + + await acceptInvite(app, u2.token.token, workspace.id); + const res2 = await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .auth(u2.token.token, { type: 'bearer' }) + .expect(200) + .type('application/octet-stream'); + + deepEqual( + res2.body, + Buffer.from([0, 0]), + 'failed to get doc with u2 token' + ); + }); + + it('should be able to get public workspace doc', async () => { + const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); + const workspace = await createWorkspace(app, user.token.token); + + const isPublic = await updateWorkspace( + app, + user.token.token, + workspace.id, + true + ); + + ok(isPublic === true, 'failed to publish workspace'); + + const res = await request(app.getHttpServer()) + .get(`/api/workspaces/${workspace.id}/docs/${workspace.id}`) + .expect(200) + .type('application/octet-stream'); + + deepEqual(res.body, Buffer.from([0, 0]), 'failed to get public doc'); }); }); diff --git a/apps/server/src/types.ts b/apps/server/src/types.ts index db36b4dd55..249d2b722b 100644 --- a/apps/server/src/types.ts +++ b/apps/server/src/types.ts @@ -6,3 +6,9 @@ export interface FileUpload { encoding: string; createReadStream: () => Readable; } + +export interface ReqContext { + req: Express.Request & { + res: Express.Response; + }; +} diff --git a/apps/server/src/utils/doc.ts b/apps/server/src/utils/doc.ts new file mode 100644 index 0000000000..b3e87bc516 --- /dev/null +++ b/apps/server/src/utils/doc.ts @@ -0,0 +1,7 @@ +export function trimGuid(ws: string, guid: string) { + if (guid.startsWith(`${ws}:space:`)) { + return guid.substring(ws.length + 1); + } + + return guid; +} diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 3bb4caa6c2..6b3c3286c3 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -7,6 +7,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "moduleResolution": "Bundler", + "allowSyntheticDefaultImports": true, "isolatedModules": false, "resolveJsonModule": true, "types": ["node"], diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts index 8d863e0cf5..677ab6b00e 100644 --- a/apps/storybook/.storybook/main.ts +++ b/apps/storybook/.storybook/main.ts @@ -40,10 +40,16 @@ export default { vanillaExtractPlugin(), tsconfigPaths({ root: fileURLToPath(new URL('../../../', import.meta.url)), + ignoreConfigErrors: true, }), ], define: { 'process.env': {}, + 'process.env.COVERAGE': JSON.stringify(!!process.env.COVERAGE), + 'process.env.SHOULD_REPORT_TRACE': `${Boolean( + process.env.SHOULD_REPORT_TRACE + )}`, + 'process.env.TRACE_REPORT_ENDPOINT': `"${process.env.TRACE_REPORT_ENDPOINT}"`, runtimeConfig: getRuntimeConfig({ distribution: 'browser', mode: 'development', diff --git a/apps/storybook/.storybook/preview.tsx b/apps/storybook/.storybook/preview.tsx index c42a947695..3449ecf49e 100644 --- a/apps/storybook/.storybook/preview.tsx +++ b/apps/storybook/.storybook/preview.tsx @@ -3,6 +3,10 @@ import '@affine/component/theme/global.css'; import '@affine/component/theme/theme.css'; import '@toeverything/components/style.css'; import { createI18n } from '@affine/i18n'; +import MockSessionContext, { + mockAuthStates, + // @ts-ignore +} from '@tomfreudenberg/next-auth-mock'; import { ThemeProvider, useTheme } from 'next-themes'; import { useDarkMode } from 'storybook-dark-mode'; import { AffineContext } from '@affine/component/context'; @@ -24,6 +28,51 @@ export const parameters = { }, }; +const SB_PARAMETER_KEY = 'nextAuthMock'; +export const mockAuthPreviewToolbarItem = ({ + name = 'mockAuthState', + description = 'Set authentication state', + defaultValue = null, + icon = 'user', + items = mockAuthStates, +} = {}) => { + return { + mockAuthState: { + name, + description, + defaultValue, + toolbar: { + icon, + items: Object.keys(items).map(e => ({ + value: e, + title: items[e].title, + })), + }, + }, + }; +}; + +export const withMockAuth: Decorator = (Story, context) => { + // Set a session value for mocking + const session = (() => { + // Allow overwrite of session value by parameter in story + const paramValue = context?.parameters[SB_PARAMETER_KEY]; + if (typeof paramValue?.session === 'string') { + return mockAuthStates[paramValue.session]?.session; + } else { + return paramValue?.session + ? paramValue.session + : mockAuthStates[context.globals.mockAuthState]?.session; + } + })(); + + return ( + + + + ); +}; + const i18n = createI18n(); const withI18n: Decorator = (Story, context) => { const locale = context.globals.locale; @@ -91,4 +140,4 @@ const withContextDecorator: Decorator = (Story, context) => { ); }; -export const decorators = [withContextDecorator, withI18n]; +export const decorators = [withContextDecorator, withI18n, withMockAuth]; diff --git a/apps/storybook/package.json b/apps/storybook/package.json index b3b710bf1e..09060f3629 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -38,6 +38,7 @@ "@blocksuite/icons": "^2.1.31", "@blocksuite/lit": "0.0.0-20230827224823-81f8728e-nightly", "@blocksuite/store": "0.0.0-20230827224823-81f8728e-nightly", + "@tomfreudenberg/next-auth-mock": "^0.5.6", "chromatic": "^6.24.0", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/apps/storybook/src/stories/share-menu.stories.tsx b/apps/storybook/src/stories/share-menu.stories.tsx index 8032202b7f..bce860f6bd 100644 --- a/apps/storybook/src/stories/share-menu.stories.tsx +++ b/apps/storybook/src/stories/share-menu.stories.tsx @@ -24,6 +24,20 @@ export default { }, } satisfies Meta; +const sharePageMap = new Map([]); +// todo: use a real hook +const useIsSharedPage = ( + _workspaceId: string, + pageId: string +): [isSharePage: boolean, setIsSharePage: (enable: boolean) => void] => { + const [isShared, setIsShared] = useState(sharePageMap.get(pageId) ?? false); + const togglePagePublic = (enable: boolean) => { + setIsShared(enable); + sharePageMap.set(pageId, enable); + }; + return [isShared, togglePagePublic]; +}; + async function initPage(page: Page) { await page.waitForLoaded(); // Add page block and surface block at root level @@ -74,11 +88,10 @@ export const Basic: StoryFn = () => { return ( ); }; @@ -105,11 +118,10 @@ export const AffineBasic: StoryFn = () => { return ( ); }; diff --git a/apps/storybook/tsconfig.node.json b/apps/storybook/tsconfig.node.json index 2c5839ca72..0f7e4b10a8 100644 --- a/apps/storybook/tsconfig.node.json +++ b/apps/storybook/tsconfig.node.json @@ -10,7 +10,7 @@ "noEmit": false, "outDir": "lib/.storybook" }, - "include": [".storybook/**/*"], + "include": [".storybook"], "exclude": ["lib"], "references": [ { "path": "../../apps/core" }, diff --git a/nx.json b/nx.json index fb81890245..9de2ef9eab 100644 --- a/nx.json +++ b/nx.json @@ -36,6 +36,7 @@ "{workspaceRoot}/packages/i18n/src/i18n-generated.ts" ], "inputs": [ + "{workspaceRoot}/i18n/**/*", "{workspaceRoot}/infra/**/*", "{workspaceRoot}/sdk/**/*", { diff --git a/package.json b/package.json index 6749fd9563..0523f30717 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "tests/affine-legacy/*", "tests/affine-local", "tests/affine-plugin", - "tests/affine-prototype" + "tests/affine-prototype", + "tests/affine-cloud" ], "engines": { "node": ">=18.16.1 <19.0.0" @@ -26,8 +27,8 @@ "build": "yarn nx build @affine/core", "build:electron": "yarn nx build @affine/electron", "build:storage": "yarn nx run-many -t build -p @affine/storage", - "build:infra": "yarn nx run-many -t build -p infra sdk", - "build:plugins": "yarn nx run-many -t build --projects=tag:plugin", + "build:infra": "yarn nx run-many -t build --projects=tag:infra", + "build:plugins": "yarn nx run-many -t build --projects=tag:plugin", "build:storybook": "yarn nx build @affine/storybook", "start:web-static": "yarn workspace @affine/core static-server", "start:storybook": "yarn exec serve apps/storybook/storybook-static -l 6006", diff --git a/packages/cli/src/bin/build-core.ts b/packages/cli/src/bin/build-core.ts index cff680a081..5a35d0a42e 100644 --- a/packages/cli/src/bin/build-core.ts +++ b/packages/cli/src/bin/build-core.ts @@ -7,16 +7,21 @@ import { buildI18N } from '../util/i18n.js'; const cwd = path.resolve(projectRoot, 'apps', 'core'); +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +const buildType = process.env.BUILD_TYPE_OVERRIDE || process.env.BUILD_TYPE; + const getChannel = () => { - switch (process.env.BUILD_TYPE) { + switch (buildType) { case 'canary': case 'beta': case 'stable': case 'internal': - return process.env.BUILD_TYPE; + return buildType; + case '': + throw new Error('BUILD_TYPE is not set'); default: { throw new Error( - 'BUILD_TYPE must be one of canary, beta, stable, internal' + `BUILD_TYPE must be one of canary, beta, stable, internal, received [${buildType}]` ); } } diff --git a/packages/component/package.json b/packages/component/package.json index e28e240865..183236d5f1 100644 --- a/packages/component/package.json +++ b/packages/component/package.json @@ -38,6 +38,7 @@ "@toeverything/hooks": "workspace:*", "@toeverything/theme": "^0.7.12", "@vanilla-extract/dynamic": "^2.0.3", + "check-password-strength": "^2.0.7", "clsx": "^2.0.0", "dayjs": "^1.11.9", "jotai": "^2.4.0", diff --git a/packages/component/src/components/auth-components/auth-content.tsx b/packages/component/src/components/auth-components/auth-content.tsx new file mode 100644 index 0000000000..1abefea527 --- /dev/null +++ b/packages/component/src/components/auth-components/auth-content.tsx @@ -0,0 +1,14 @@ +import clsx from 'clsx'; +import type { FC, HTMLAttributes, PropsWithChildren } from 'react'; + +import { authContent } from './share.css'; + +export const AuthContent: FC< + PropsWithChildren & HTMLAttributes +> = ({ children, className, ...otherProps }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/packages/component/src/components/auth-components/auth-input.tsx b/packages/component/src/components/auth-components/auth-input.tsx new file mode 100644 index 0000000000..2e49c40089 --- /dev/null +++ b/packages/component/src/components/auth-components/auth-input.tsx @@ -0,0 +1,49 @@ +import clsx from 'clsx'; +import type { FC } from 'react'; + +import { Input, type InputProps } from '../../ui/input'; +import { authInputWrapper, formHint } from './share.css'; +export type AuthInputProps = InputProps & { + label?: string; + error?: boolean; + errorHint?: string; + withoutHint?: boolean; + onEnter?: () => void; +}; +export const AuthInput: FC = ({ + label, + error, + errorHint, + withoutHint = false, + onEnter, + ...inputProps +}) => { + return ( +
+ {label ? : null} + { + if (e.key === 'Enter') { + onEnter?.(); + } + }} + {...inputProps} + /> + {error && errorHint && !withoutHint ? ( +
+ {errorHint} +
+ ) : null} +
+ ); +}; diff --git a/packages/component/src/components/auth-components/auth-page-container.tsx b/packages/component/src/components/auth-components/auth-page-container.tsx new file mode 100644 index 0000000000..10f6d29ba6 --- /dev/null +++ b/packages/component/src/components/auth-components/auth-page-container.tsx @@ -0,0 +1,32 @@ +import type { FC, PropsWithChildren, ReactNode } from 'react'; + +import { Empty } from '../../ui/empty'; +import { Wrapper } from '../../ui/layout'; +import { Logo } from './logo'; +import { authPageContainer } from './share.css'; + +export const AuthPageContainer: FC< + PropsWithChildren<{ title?: ReactNode; subtitle?: ReactNode }> +> = ({ children, title, subtitle }) => { + return ( +
+ + + +
+
+

{title}

+

{subtitle}

+ {children} +
+ +
+
+ ); +}; diff --git a/packages/component/src/components/auth-components/back-button.tsx b/packages/component/src/components/auth-components/back-button.tsx new file mode 100644 index 0000000000..4708a70b3a --- /dev/null +++ b/packages/component/src/components/auth-components/back-button.tsx @@ -0,0 +1,24 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { ArrowLeftSmallIcon } from '@blocksuite/icons'; +import { Button, type ButtonProps } from '@toeverything/components/button'; +import { type FC } from 'react'; + +export const BackButton: FC = props => { + const t = useAFFiNEI18N(); + return ( + + ); +}; diff --git a/packages/component/src/components/auth-components/change-email-page.tsx b/packages/component/src/components/auth-components/change-email-page.tsx new file mode 100644 index 0000000000..f48778a4ee --- /dev/null +++ b/packages/component/src/components/auth-components/change-email-page.tsx @@ -0,0 +1,89 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button } from '@toeverything/components/button'; +import type { FC } from 'react'; +import { useCallback, useState } from 'react'; + +import { AuthInput } from './auth-input'; +import { AuthPageContainer } from './auth-page-container'; +import { emailRegex } from './utils'; +type User = { + id: string; + name: string; + email: string; + image: string; +}; + +export const ChangeEmailPage: FC<{ + user: User; + onChangeEmail: (email: string) => Promise; + onOpenAffine: () => void; +}> = ({ onChangeEmail: propsOnChangeEmail, onOpenAffine }) => { + const t = useAFFiNEI18N(); + const [hasSetUp, setHasSetUp] = useState(false); + const [email, setEmail] = useState(''); + const [isValidEmail, setIsValidEmail] = useState(true); + const [loading, setLoading] = useState(false); + const onContinue = useCallback( + () => + void (async () => { + if (!emailRegex.test(email)) { + setIsValidEmail(false); + return; + } + setIsValidEmail(true); + setLoading(true); + + const setup = await propsOnChangeEmail(email); + + setLoading(false); + setHasSetUp(setup); + })(), + [email, propsOnChangeEmail] + ); + const onEmailChange = useCallback((value: string) => { + setEmail(value); + }, []); + return ( + + {hasSetUp ? ( + + ) : ( + <> + + + + )} + + ); +}; diff --git a/packages/component/src/components/auth-components/change-password-page.tsx b/packages/component/src/components/auth-components/change-password-page.tsx new file mode 100644 index 0000000000..4b0b62a554 --- /dev/null +++ b/packages/component/src/components/auth-components/change-password-page.tsx @@ -0,0 +1,58 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button } from '@toeverything/components/button'; +import type { FC } from 'react'; +import { useCallback, useState } from 'react'; + +import { AuthPageContainer } from './auth-page-container'; +import { SetPassword } from './set-password'; +type User = { + id: string; + name: string; + email: string; + image: string; +}; + +export const ChangePasswordPage: FC<{ + user: User; + onSetPassword: (password: string) => void; + onOpenAffine: () => void; +}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => { + const t = useAFFiNEI18N(); + const [hasSetUp, setHasSetUp] = useState(false); + + const onSetPassword = useCallback( + (passWord: string) => { + propsOnSetPassword(passWord); + setHasSetUp(true); + }, + [propsOnSetPassword] + ); + + return ( + + {t['com.affine.auth.page.sent.email.subtitle']()} + {email} + + ) + } + > + {hasSetUp ? ( + + ) : ( + + )} + + ); +}; diff --git a/packages/component/src/components/auth-components/index.tsx b/packages/component/src/components/auth-components/index.tsx new file mode 100644 index 0000000000..cfeecce644 --- /dev/null +++ b/packages/component/src/components/auth-components/index.tsx @@ -0,0 +1,14 @@ +export * from './auth-content'; +export * from './auth-input'; +export * from './auth-page-container'; +export * from './back-button'; +export * from './change-email-page'; +export * from './change-password-page'; +export * from './modal'; +export * from './modal-header'; +export * from './password-input'; +export * from './resend-button'; +export * from './set-password-page'; +export * from './sign-in-page-container'; +export * from './sign-in-success-page'; +export * from './sign-up-page'; diff --git a/packages/component/src/components/auth-components/logo.tsx b/packages/component/src/components/auth-components/logo.tsx new file mode 100644 index 0000000000..6b11ad2705 --- /dev/null +++ b/packages/component/src/components/auth-components/logo.tsx @@ -0,0 +1,18 @@ +export const Logo = () => { + return ( + + + + + + + + + ); +}; diff --git a/packages/component/src/components/auth-components/modal-header.tsx b/packages/component/src/components/auth-components/modal-header.tsx new file mode 100644 index 0000000000..3cd90eaa99 --- /dev/null +++ b/packages/component/src/components/auth-components/modal-header.tsx @@ -0,0 +1,18 @@ +import { Logo1Icon } from '@blocksuite/icons'; +import type { FC } from 'react'; + +import { modalHeaderWrapper } from './share.css'; +export const ModalHeader: FC<{ + title: string; + subTitle: string; +}> = ({ title, subTitle }) => { + return ( +
+

+ + {title} +

+

{subTitle}

+
+ ); +}; diff --git a/packages/component/src/components/auth-components/modal.tsx b/packages/component/src/components/auth-components/modal.tsx new file mode 100644 index 0000000000..b4fef6b923 --- /dev/null +++ b/packages/component/src/components/auth-components/modal.tsx @@ -0,0 +1,43 @@ +import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; +import type { FC, PropsWithChildren } from 'react'; +import { useCallback } from 'react'; + +export type AuthModalProps = { + open: boolean; + setOpen: (value: boolean) => void; +}; + +export const AuthModal: FC> = ({ + children, + open, + setOpen, +}) => { + const handleClose = useCallback(() => { + setOpen(false); + }, [setOpen]); + + return ( + + + + {children} + + + ); +}; diff --git a/packages/component/src/components/auth-components/password-input/error.tsx b/packages/component/src/components/auth-components/password-input/error.tsx new file mode 100644 index 0000000000..4ce9b48de4 --- /dev/null +++ b/packages/component/src/components/auth-components/password-input/error.tsx @@ -0,0 +1,30 @@ +export const ErrorIcon = () => { + return ( + + + + + + + + + + ); +}; diff --git a/packages/component/src/components/auth-components/password-input/index.tsx b/packages/component/src/components/auth-components/password-input/index.tsx new file mode 100644 index 0000000000..048e0f497c --- /dev/null +++ b/packages/component/src/components/auth-components/password-input/index.tsx @@ -0,0 +1,100 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { passwordStrength } from 'check-password-strength'; +import { type FC, useEffect } from 'react'; +import { useCallback, useState } from 'react'; + +import { Input, type InputProps } from '../../../ui/input'; +import { ErrorIcon } from './error'; +import { SuccessIcon } from './success'; +import { Tag } from './tag'; + +export type Status = 'weak' | 'medium' | 'strong' | 'maximum'; + +export const PasswordInput: FC< + InputProps & { + onPass: (password: string) => void; + onPrevent: () => void; + } +> = ({ onPass, onPrevent, ...inputProps }) => { + const t = useAFFiNEI18N(); + + const [status, setStatus] = useState(null); + const [confirmStatus, setConfirmStatus] = useState< + 'success' | 'error' | null + >(null); + + const [password, setPassWord] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + + const onPasswordChange = useCallback((value: string) => { + setPassWord(value); + if (!value) { + return setStatus(null); + } + if (value.length > 20) { + return setStatus('maximum'); + } + switch (passwordStrength(value).id) { + case 0: + case 1: + setStatus('weak'); + break; + case 2: + setStatus('medium'); + break; + case 3: + setStatus('strong'); + break; + } + }, []); + + const onConfirmPasswordChange = useCallback((value: string) => { + setConfirmPassword(value); + }, []); + + useEffect(() => { + if (password === confirmPassword) { + setConfirmStatus('success'); + } else { + setConfirmStatus('error'); + } + }, [confirmPassword, password]); + + useEffect(() => { + if (confirmStatus === 'success' && password.length > 7) { + onPass(password); + } else { + onPrevent(); + } + }, [confirmStatus, onPass, onPrevent, password]); + + return ( + <> + : null} + {...inputProps} + /> + + ) : ( + + ) + ) : null + } + {...inputProps} + /> + + ); +}; diff --git a/packages/component/src/components/auth-components/password-input/style.css.ts b/packages/component/src/components/auth-components/password-input/style.css.ts new file mode 100644 index 0000000000..389a0065cd --- /dev/null +++ b/packages/component/src/components/auth-components/password-input/style.css.ts @@ -0,0 +1,28 @@ +import { style } from '@vanilla-extract/css'; + +export const tag = style({ + padding: '0 15px', + height: 20, + lineHeight: '20px', + borderRadius: 10, + fontSize: 'var(--affine-font-xs)', + + selectors: { + '&.weak': { + backgroundColor: 'var(--affine-tag-red)', + color: 'var(--affine-error-color)', + }, + '&.medium': { + backgroundColor: 'var(--affine-tag-orange)', + color: 'var(--affine-warning-color)', + }, + '&.strong': { + backgroundColor: 'var(--affine-tag-green)', + color: 'var(--affine-success-color)', + }, + '&.maximum': { + backgroundColor: 'var(--affine-tag-red)', + color: 'var(--affine-error-color)', + }, + }, +}); diff --git a/packages/component/src/components/auth-components/password-input/success.tsx b/packages/component/src/components/auth-components/password-input/success.tsx new file mode 100644 index 0000000000..d9242efebe --- /dev/null +++ b/packages/component/src/components/auth-components/password-input/success.tsx @@ -0,0 +1,28 @@ +export const SuccessIcon = () => { + return ( + + + + + + + ); +}; diff --git a/packages/component/src/components/auth-components/password-input/tag.tsx b/packages/component/src/components/auth-components/password-input/tag.tsx new file mode 100644 index 0000000000..ecf6673d0d --- /dev/null +++ b/packages/component/src/components/auth-components/password-input/tag.tsx @@ -0,0 +1,28 @@ +import clsx from 'clsx'; +import { type FC, useMemo } from 'react'; + +import type { Status } from './index'; +import { tag } from './style.css'; +export const Tag: FC<{ status: Status }> = ({ status }) => { + const textMap = useMemo<{ [K in Status]: string }>(() => { + return { + weak: 'Weak', + medium: 'Medium', + strong: 'Strong', + maximum: 'Maximum', + }; + }, []); + + return ( +
+ {textMap[status]} +
+ ); +}; diff --git a/packages/component/src/components/auth-components/resend-button.tsx b/packages/component/src/components/auth-components/resend-button.tsx new file mode 100644 index 0000000000..5e1b8fa0cd --- /dev/null +++ b/packages/component/src/components/auth-components/resend-button.tsx @@ -0,0 +1,76 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button } from '@toeverything/components/button'; +import { type FC, useCallback, useEffect, useState } from 'react'; + +import { resendButtonWrapper } from './share.css'; + +const formatTime = (time: number): string => { + const minutes = Math.floor(time / 60); + const seconds = time % 60; + + const formattedMinutes = minutes.toString().padStart(2, '0'); + const formattedSeconds = seconds.toString().padStart(2, '0'); + + return `${formattedMinutes}:${formattedSeconds}`; +}; +const CountDown: FC<{ + seconds: number; + onEnd?: () => void; +}> = ({ seconds, onEnd }) => { + const [timeLeft, setTimeLeft] = useState(seconds); + + useEffect(() => { + if (timeLeft === 0) { + onEnd?.(); + return; + } + + const intervalId = setInterval(() => { + setTimeLeft(timeLeft - 1); + + if (timeLeft - 1 === 0) { + clearInterval(intervalId); + } + }, 1000); + + return () => clearInterval(intervalId); + }, [onEnd, timeLeft]); + + return ( +
{formatTime(timeLeft)}
+ ); +}; + +export const ResendButton: FC<{ + onClick: () => void; + countDownSeconds?: number; +}> = ({ onClick, countDownSeconds = 60 }) => { + const t = useAFFiNEI18N(); + const [canResend, setCanResend] = useState(false); + + const onButtonClick = useCallback(() => { + onClick(); + setCanResend(false); + }, [onClick]); + + const onCountDownEnd = useCallback(() => { + setCanResend(true); + }, [setCanResend]); + + return ( +
+ {canResend ? ( + + ) : ( + <> + + {t['com.affine.auth.sign.auth.code.on.resend.hint']()} + + + + )} +
+ ); +}; diff --git a/packages/component/src/components/auth-components/set-password-page.tsx b/packages/component/src/components/auth-components/set-password-page.tsx new file mode 100644 index 0000000000..7eea1f4e0a --- /dev/null +++ b/packages/component/src/components/auth-components/set-password-page.tsx @@ -0,0 +1,60 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button } from '@toeverything/components/button'; +import type { FC } from 'react'; +import { useCallback, useState } from 'react'; + +import { AuthPageContainer } from './auth-page-container'; +import { SetPassword } from './set-password'; + +type User = { + id: string; + name: string; + email: string; + image: string; +}; + +export const SetPasswordPage: FC<{ + user: User; + onSetPassword: (password: string) => void; + onOpenAffine: () => void; +}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => { + const t = useAFFiNEI18N(); + const [hasSetUp, setHasSetUp] = useState(false); + + const onSetPassword = useCallback( + (passWord: string) => { + propsOnSetPassword(passWord); + setHasSetUp(true); + }, + [propsOnSetPassword] + ); + + return ( + + {t['com.affine.auth.page.sent.email.subtitle']()} + {email} + + ) + } + > +

This is set page

+ {hasSetUp ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/packages/component/src/components/auth-components/set-password.tsx b/packages/component/src/components/auth-components/set-password.tsx new file mode 100644 index 0000000000..579950d1e0 --- /dev/null +++ b/packages/component/src/components/auth-components/set-password.tsx @@ -0,0 +1,50 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button } from '@toeverything/components/button'; +import { type FC, useCallback, useRef, useState } from 'react'; + +import { Wrapper } from '../../ui/layout'; +import { PasswordInput } from './password-input'; + +export const SetPassword: FC<{ + showLater?: boolean; + onLater?: () => void; + onSetPassword: (password: string) => void; +}> = ({ onLater, onSetPassword, showLater = false }) => { + const t = useAFFiNEI18N(); + + const [passwordPass, setPasswordPass] = useState(false); + const passwordRef = useRef(''); + + return ( + <> + + { + setPasswordPass(true); + passwordRef.current = password; + }, [])} + onPrevent={useCallback(() => { + setPasswordPass(false); + }, [])} + /> + + + {showLater ? ( + + ) : null} + + ); +}; diff --git a/packages/component/src/components/auth-components/share.css.ts b/packages/component/src/components/auth-components/share.css.ts new file mode 100644 index 0000000000..0f5d8bcc5d --- /dev/null +++ b/packages/component/src/components/auth-components/share.css.ts @@ -0,0 +1,180 @@ +import { globalStyle, keyframes, style } from '@vanilla-extract/css'; + +export const modalHeaderWrapper = style({}); + +globalStyle(`${modalHeaderWrapper} .logo`, { + fontSize: 'var(--affine-font-h-3)', + fontWeight: 600, + color: 'var(--affine-blue)', + marginRight: '6px', + verticalAlign: 'middle', +}); + +globalStyle(`${modalHeaderWrapper} > p:first-of-type`, { + fontSize: 'var(--affine-font-h-5)', + fontWeight: 600, + marginBottom: '4px', + lineHeight: '28px', + display: 'flex', + alignItems: 'center', +}); +globalStyle(`${modalHeaderWrapper} > p:last-of-type`, { + fontSize: 'var(--affine-font-h-4)', + fontWeight: 600, + lineHeight: '28px', +}); + +export const authInputWrapper = style({ + paddingBottom: '30px', + position: 'relative', + selectors: { + '&.without-hint': { + paddingBottom: '20px', + }, + }, +}); + +globalStyle(`${authInputWrapper} label`, { + display: 'block', + color: 'var(--light-text-color-text-secondary-color, #8E8D91)', + marginBottom: '4px', + fontSize: 'var(--affine-font-sm)', + fontWeight: 600, + lineHeight: '22px', +}); +export const formHint = style({ + fontSize: 'var(--affine-font-sm)', + position: 'absolute', + bottom: '4px', + left: 0, + lineHeight: '22px', + selectors: { + '&.error': { + color: 'var(--affine-error-color)', + }, + '&.warning': { + color: 'var(--affine-warning-color)', + }, + }, +}); +const rotate = keyframes({ + '0%': { transform: 'rotate(0deg)' }, + '50%': { transform: 'rotate(180deg)' }, + '100%': { transform: 'rotate(360deg)' }, +}); +export const loading = style({ + width: '15px', + height: '15px', + position: 'relative', + borderRadius: '50%', + overflow: 'hidden', + backgroundColor: 'var(--affine-border-color)', + selectors: { + '&::after': { + content: '""', + width: '12px', + height: '12px', + position: 'absolute', + left: '0', + right: '0', + top: '0', + bottom: '0', + margin: 'auto', + backgroundColor: '#fff', + zIndex: 2, + borderRadius: '50%', + }, + '&::before': { + content: '""', + width: '20px', + height: '20px', + backgroundColor: 'var(--affine-blue)', + position: 'absolute', + left: '50%', + bottom: '50%', + zIndex: '1', + transformOrigin: 'left bottom', + animation: `${rotate} 1.5s infinite linear`, + }, + }, +}); + +export const authContent = style({ + fontSize: 'var(--affine-font-base)', + lineHeight: 'var(--affine-font-h-3)', + marginTop: '30px', +}); +globalStyle(`${authContent} a`, { + color: 'var(--affine-link-color)', +}); + +export const authCodeContainer = style({ + paddingBottom: '40px', + position: 'relative', +}); +export const authCodeWrapper = style({ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', +}); + +export const authCodeErrorMessage = style({ + color: 'var(--affine-error-color)', + fontSize: 'var(--affine-font-sm)', + textAlign: 'center', + lineHeight: '1.5', + position: 'absolute', + left: 0, + right: 0, + bottom: 5, + margin: 'auto', +}); + +export const resendButtonWrapper = style({ + height: 32, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginTop: 30, +}); + +globalStyle(`${resendButtonWrapper} .resend-code-hint`, { + fontWeight: 600, + fontSize: 'var(--affine-font-sm)', + marginRight: 8, +}); + +export const authPageContainer = style({ + height: '100vh', + width: '100vw', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: 'var(--affine-font-base)', +}); +globalStyle(`${authPageContainer} .wrapper`, { + display: 'flex', + alignItems: 'center', +}); +globalStyle(`${authPageContainer} .content`, { + maxWidth: '700px', + minWidth: '550px', +}); + +globalStyle(`${authPageContainer} .title`, { + fontSize: 'var(--affine-font-title)', + fontWeight: 600, + marginBottom: '28px', +}); + +globalStyle(`${authPageContainer} .subtitle`, { + marginBottom: '28px', +}); +globalStyle(`${authPageContainer} a`, { + color: 'var(--affine-link-color)', +}); + +export const signInPageContainer = style({ + width: '400px', + margin: '205px auto 0', +}); diff --git a/packages/component/src/components/auth-components/sign-in-page-container.tsx b/packages/component/src/components/auth-components/sign-in-page-container.tsx new file mode 100644 index 0000000000..243205debf --- /dev/null +++ b/packages/component/src/components/auth-components/sign-in-page-container.tsx @@ -0,0 +1,6 @@ +import type { PropsWithChildren } from 'react'; + +import { signInPageContainer } from './share.css'; +export const SignInPageContainer = ({ children }: PropsWithChildren) => { + return
{children}
; +}; diff --git a/packages/component/src/components/auth-components/sign-in-success-page.tsx b/packages/component/src/components/auth-components/sign-in-success-page.tsx new file mode 100644 index 0000000000..807269d8a1 --- /dev/null +++ b/packages/component/src/components/auth-components/sign-in-success-page.tsx @@ -0,0 +1,21 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button } from '@toeverything/components/button'; +import type { FC } from 'react'; + +import { AuthPageContainer } from './auth-page-container'; + +export const SignInSuccessPage: FC<{ + onOpenAffine: () => void; +}> = ({ onOpenAffine }) => { + const t = useAFFiNEI18N(); + return ( + + + + ); +}; diff --git a/packages/component/src/components/auth-components/sign-up-page.tsx b/packages/component/src/components/auth-components/sign-up-page.tsx new file mode 100644 index 0000000000..fe0bd8730f --- /dev/null +++ b/packages/component/src/components/auth-components/sign-up-page.tsx @@ -0,0 +1,65 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button } from '@toeverything/components/button'; +import type { FC } from 'react'; +import { useCallback, useState } from 'react'; + +import { AuthPageContainer } from './auth-page-container'; +import { SetPassword } from './set-password'; +type User = { + id: string; + name: string; + email: string; + image: string; +}; + +export const SignUpPage: FC<{ + user: User; + onSetPassword: (password: string) => void; + onOpenAffine: () => void; +}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => { + const t = useAFFiNEI18N(); + const [hasSetUp, setHasSetUp] = useState(false); + + const onSetPassword = useCallback( + (passWord: string) => { + propsOnSetPassword(passWord); + setHasSetUp(true); + }, + [propsOnSetPassword] + ); + const onLater = useCallback(() => { + setHasSetUp(true); + }, []); + + return ( + + {t['com.affine.auth.page.sent.email.subtitle']()} + {email} + + ) + } + > + {hasSetUp ? ( + + ) : ( + + )} + + ); +}; diff --git a/packages/component/src/components/auth-components/utils.ts b/packages/component/src/components/auth-components/utils.ts new file mode 100644 index 0000000000..466a8ec8ea --- /dev/null +++ b/packages/component/src/components/auth-components/utils.ts @@ -0,0 +1,2 @@ +export const emailRegex = + /^(?:(?:[^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(?:(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|((?:[a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; diff --git a/packages/component/src/components/card/workspace-card/index.tsx b/packages/component/src/components/card/workspace-card/index.tsx index 0ee7ec8454..311e0be988 100644 --- a/packages/component/src/components/card/workspace-card/index.tsx +++ b/packages/component/src/components/card/workspace-card/index.tsx @@ -12,6 +12,7 @@ import { StyledSettingLink, StyledWorkspaceInfo, StyledWorkspaceTitle, + StyledWorkspaceTitleArea, } from './styles'; export interface WorkspaceTypeProps { @@ -70,25 +71,29 @@ export const WorkspaceCard = ({ - {name} - + + {name} + + { + e.stopPropagation(); + onSettingClick(meta.id); + }} + withoutHoverStyle={true} + > + + + {/* {meta.flavour === WorkspaceFlavour.LOCAL && (

- {t['Available Offline']()} +

+ )} */} +
- { - e.stopPropagation(); - onSettingClick(meta.id); - }} - > - - ); }; diff --git a/packages/component/src/components/card/workspace-card/styles.ts b/packages/component/src/components/card/workspace-card/styles.ts index 1aea2c2c55..649d9152b3 100644 --- a/packages/component/src/components/card/workspace-card/styles.ts +++ b/packages/component/src/components/card/workspace-card/styles.ts @@ -104,3 +104,10 @@ export const StyledWorkspaceType = styled('p')(() => { fontSize: 10, }; }); + +export const StyledWorkspaceTitleArea = styled('div')(() => { + return { + display: 'flex', + justifyContent: 'space-between', + }; +}); diff --git a/packages/component/src/components/member-components/accept-invite-page.tsx b/packages/component/src/components/member-components/accept-invite-page.tsx new file mode 100644 index 0000000000..80af002417 --- /dev/null +++ b/packages/component/src/components/member-components/accept-invite-page.tsx @@ -0,0 +1,51 @@ +import { AuthPageContainer } from '@affine/component/auth-components'; +import { type GetInviteInfoQuery } from '@affine/graphql'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Avatar } from '@toeverything/components/avatar'; +import { Button } from '@toeverything/components/button'; + +import { FlexWrapper } from '../../ui/layout'; +import * as styles from './styles.css'; +export const AcceptInvitePage = ({ + onOpenWorkspace, + inviteInfo, +}: { + onOpenWorkspace: () => void; + inviteInfo: GetInviteInfoQuery['getInviteInfo']; +}) => { + const t = useAFFiNEI18N(); + return ( + + + {inviteInfo.user.name} + {t['invited you to join']()} + + {inviteInfo.workspace.name} + + } + > + + + ); +}; diff --git a/packages/component/src/components/member-components/index.tsx b/packages/component/src/components/member-components/index.tsx new file mode 100644 index 0000000000..bb5d6d82c0 --- /dev/null +++ b/packages/component/src/components/member-components/index.tsx @@ -0,0 +1,2 @@ +export * from './accept-invite-page'; +export * from './invite-modal'; diff --git a/packages/component/src/components/member-components/invite-modal.tsx b/packages/component/src/components/member-components/invite-modal.tsx new file mode 100644 index 0000000000..e0531e2c2f --- /dev/null +++ b/packages/component/src/components/member-components/invite-modal.tsx @@ -0,0 +1,147 @@ +import { Permission } from '@affine/graphql'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button } from '@toeverything/components/button'; +import { useCallback, useEffect, useState } from 'react'; + +import { Modal, ModalCloseButton, ModalWrapper } from '../../ui/modal'; +import { AuthInput } from '..//auth-components'; +import { emailRegex } from '..//auth-components/utils'; +import * as styles from './styles.css'; + +export interface InviteModalProps { + open: boolean; + setOpen: (value: boolean) => void; + onConfirm: (params: { email: string; permission: Permission }) => void; + isMutating: boolean; +} + +const PermissionMenu = ({ + currentPermission, + onChange, +}: { + currentPermission: Permission; + onChange: (permission: Permission) => void; +}) => { + console.log('currentPermission', currentPermission); + console.log('onChange', onChange); + + return null; + // return ( + // + // {Object.entries(Permission).map(([permission]) => { + // return ( + // { + // onChange(permission as Permission); + // }} + // > + // {permission} + // + // ); + // })} + // + // } + // > + // + // {currentPermission} + // + // + // ); +}; + +export const InviteModal = ({ + open, + setOpen, + onConfirm, + isMutating, +}: InviteModalProps) => { + const t = useAFFiNEI18N(); + const [inviteEmail, setInviteEmail] = useState(''); + const [permission, setPermission] = useState(Permission.Write); + const [isValidEmail, setIsValidEmail] = useState(true); + + const handleCancel = useCallback(() => { + setOpen(false); + }, [setOpen]); + + const handleConfirm = useCallback(() => { + if (!emailRegex.test(inviteEmail)) { + setIsValidEmail(false); + return; + } + setIsValidEmail(true); + + onConfirm({ + email: inviteEmail, + permission, + }); + }, [inviteEmail, onConfirm, permission]); + + useEffect(() => { + if (!open) { + setInviteEmail(''); + setIsValidEmail(true); + } + }, [open]); + + return ( + + + + +
{t['Invite Members']()}
+
+ {t['Invite Members Message']()} + {/*TODO: check email & add placeholder*/} + + } + /> +
+
+ + +
+
+
+ ); +}; diff --git a/packages/component/src/components/member-components/styles.css.tsx b/packages/component/src/components/member-components/styles.css.tsx new file mode 100644 index 0000000000..5bcc77ffea --- /dev/null +++ b/packages/component/src/components/member-components/styles.css.tsx @@ -0,0 +1,23 @@ +import { style } from '@vanilla-extract/css'; + +export const inviteModalTitle = style({ + fontWeight: '600', + fontSize: 'var(--affine-font-h-6)', + marginBottom: '20px', +}); + +export const inviteModalContent = style({ + marginBottom: '10px', +}); + +export const inviteModalButtonContainer = style({ + display: 'flex', + justifyContent: 'flex-end', + // marginTop: 10, +}); + +export const inviteName = style({ + marginLeft: '4px', + marginRight: '10px', + color: 'var(--affine-black)', +}); diff --git a/packages/component/src/components/notification-center/index.tsx b/packages/component/src/components/notification-center/index.tsx index 1d56f5388d..3a1c79db2c 100644 --- a/packages/component/src/components/notification-center/index.tsx +++ b/packages/component/src/components/notification-center/index.tsx @@ -233,6 +233,7 @@ function NotificationCard(props: NotificationCardProps): ReactElement { data-index={index} data-front={isFront} data-expanded={expand} + data-testid="affine-notification" onMouseEnter={() => { setExpand(true); }} diff --git a/packages/component/src/components/page-list/operation-menu-items/export.tsx b/packages/component/src/components/page-list/operation-menu-items/export.tsx index b6595d5d50..61b7299ad4 100644 --- a/packages/component/src/components/page-list/operation-menu-items/export.tsx +++ b/packages/component/src/components/page-list/operation-menu-items/export.tsx @@ -16,13 +16,22 @@ import { Menu, MenuItem } from '../../..'; import { getContentParser } from './get-content-parser'; import type { CommonMenuItemProps } from './types'; +type ExportMenuItemProps = { + iconSize?: number; + gap?: string; + fontSize?: string; +}; const MenuItemStyle = { padding: '4px 12px', }; export const ExportToPdfMenuItem = ({ onSelect, -}: CommonMenuItemProps<{ type: 'pdf' }>) => { + style = MenuItemStyle, + iconSize, + gap, + fontSize, +}: CommonMenuItemProps<{ type: 'pdf' }> & ExportMenuItemProps) => { const t = useAFFiNEI18N(); const { currentEditor } = globalThis; const setPushNotification = useSetAtom(pushNotificationAtom); @@ -82,7 +91,10 @@ export const ExportToPdfMenuItem = ({ data-testid="export-to-pdf" onClick={onClickDownloadPDF} icon={} - style={MenuItemStyle} + style={style} + iconSize={iconSize} + gap={gap} + fontSize={fontSize} > {t['Export to PDF']()} @@ -91,7 +103,11 @@ export const ExportToPdfMenuItem = ({ export const ExportToHtmlMenuItem = ({ onSelect, -}: CommonMenuItemProps<{ type: 'html' }>) => { + style = MenuItemStyle, + iconSize, + gap, + fontSize, +}: CommonMenuItemProps<{ type: 'html' }> & ExportMenuItemProps) => { const t = useAFFiNEI18N(); const { currentEditor } = globalThis; const setPushNotification = useSetAtom(pushNotificationAtom); @@ -126,7 +142,10 @@ export const ExportToHtmlMenuItem = ({ data-testid="export-to-html" onClick={onClickExportHtml} icon={} - style={MenuItemStyle} + style={style} + iconSize={iconSize} + gap={gap} + fontSize={fontSize} > {t['Export to HTML']()} @@ -136,7 +155,11 @@ export const ExportToHtmlMenuItem = ({ export const ExportToPngMenuItem = ({ onSelect, -}: CommonMenuItemProps<{ type: 'png' }>) => { + style = MenuItemStyle, + iconSize, + gap, + fontSize, +}: CommonMenuItemProps<{ type: 'png' }> & ExportMenuItemProps) => { const t = useAFFiNEI18N(); const { currentEditor } = globalThis; const setPushNotification = useSetAtom(pushNotificationAtom); @@ -173,7 +196,10 @@ export const ExportToPngMenuItem = ({ data-testid="export-to-png" onClick={onClickDownloadPNG} icon={} - style={MenuItemStyle} + style={style} + iconSize={iconSize} + gap={gap} + fontSize={fontSize} > {t['Export to PNG']()} @@ -183,7 +209,11 @@ export const ExportToPngMenuItem = ({ export const ExportToMarkdownMenuItem = ({ onSelect, -}: CommonMenuItemProps<{ type: 'markdown' }>) => { + style = MenuItemStyle, + iconSize, + gap, + fontSize, +}: CommonMenuItemProps<{ type: 'markdown' }> & ExportMenuItemProps) => { const t = useAFFiNEI18N(); const { currentEditor } = globalThis; const setPushNotification = useSetAtom(pushNotificationAtom); @@ -218,7 +248,10 @@ export const ExportToMarkdownMenuItem = ({ data-testid="export-to-markdown" onClick={onClickExportMarkdown} icon={} - style={MenuItemStyle} + style={style} + iconSize={iconSize} + gap={gap} + fontSize={fontSize} > {t['Export to Markdown']()} @@ -253,7 +286,7 @@ export const Export = ({ data-testid="export-menu" icon={} endIcon={} - style={{ padding: '4px 12px' }} + style={MenuItemStyle} onClick={e => { e.stopPropagation(); onItemClick?.(); diff --git a/packages/component/src/components/setting-components/setting-row.tsx b/packages/component/src/components/setting-components/setting-row.tsx index 33297d962a..4a6dc3f65f 100644 --- a/packages/component/src/components/setting-components/setting-row.tsx +++ b/packages/component/src/components/setting-components/setting-row.tsx @@ -3,14 +3,15 @@ import type { CSSProperties, PropsWithChildren, ReactNode } from 'react'; import { settingRow } from './share.css'; -type SettingRowProps = { +export type SettingRowProps = PropsWithChildren<{ name: ReactNode; desc: ReactNode; style?: CSSProperties; onClick?: () => void; spreadCol?: boolean; 'data-testid'?: string; -}; + disabled?: boolean; +}>; export const SettingRow = ({ name, @@ -19,12 +20,14 @@ export const SettingRow = ({ onClick, style, spreadCol = true, + disabled = false, ...props }: PropsWithChildren) => { return (
- - - {t['Disable Public Link ?']()} - - - {t['Disable Public Link Description']()} - - - - - - - + +
+ {t['Disable Public Link']()} + + + +
+ + {t['Disable Public Link Description']()} + +
+ +
+
+ +
+
+
+
); }; diff --git a/packages/component/src/components/share-menu/disable-public-link/style.ts b/packages/component/src/components/share-menu/disable-public-link/style.ts index c34fccd34c..936119e85c 100644 --- a/packages/component/src/components/share-menu/disable-public-link/style.ts +++ b/packages/component/src/components/share-menu/disable-public-link/style.ts @@ -1,42 +1,35 @@ import { styled } from '../../..'; -export const StyledModalWrapper = styled('div')(() => { - return { - position: 'relative', - padding: '0px', - width: '560px', - background: 'var(--affine-white)', - borderRadius: '12px', - // height: '312px', - }; +export const Header = styled('div')({ + display: 'flex', + justifyContent: 'space-between', + paddingRight: '20px', + paddingTop: '20px', + paddingLeft: '24px', + alignItems: 'center', }); -export const StyledModalHeader = styled('div')(() => { - return { - margin: '44px 0px 12px 0px', - width: '560px', - fontWeight: '600', - fontSize: 'var(--affine-font-h6)', - textAlign: 'center', - }; +export const Content = styled('div')({ + padding: '12px 24px 20px 24px', }); -export const StyledTextContent = styled('div')(() => { - return { - margin: 'auto', - width: '560px', - padding: '0px 84px', - fontWeight: '400', - fontSize: 'var(--affine-font-base)', - textAlign: 'center', - }; +export const Title = styled('div')({ + fontSize: 'var(--affine-font-h6)', + lineHeight: '26px', + fontWeight: 600, }); -export const StyledButtonContent = styled('div')(() => { +export const StyleTips = styled('div')(() => { + return { + userSelect: 'none', + marginBottom: '20px', + }; +}); +export const ButtonContainer = styled('div')(() => { return { - margin: '32px 0', display: 'flex', - flexDirection: 'row', - justifyContent: 'center', + justifyContent: 'flex-end', + gap: '20px', + paddingTop: '20px', }; }); diff --git a/packages/component/src/components/share-menu/export.tsx b/packages/component/src/components/share-menu/export.tsx deleted file mode 100644 index 476b2bb0a2..0000000000 --- a/packages/component/src/components/share-menu/export.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useAFFiNEI18N } from '@affine/i18n/hooks'; - -import { - ExportToHtmlMenuItem, - ExportToMarkdownMenuItem, - ExportToPdfMenuItem, - ExportToPngMenuItem, -} from '../page-list/operation-menu-items/export'; -import { actionsStyle, descriptionStyle, menuItemStyle } from './index.css'; -// import type { ShareMenuProps } from './share-menu'; - -export const Export = () => { - const t = useAFFiNEI18N(); - return ( -
-
- {t['Export Shared Pages Description']()} -
-
- - - - -
-
- ); -}; diff --git a/packages/component/src/components/share-menu/index.css.ts b/packages/component/src/components/share-menu/index.css.ts index bf9d585e7a..a0547c0a9b 100644 --- a/packages/component/src/components/share-menu/index.css.ts +++ b/packages/component/src/components/share-menu/index.css.ts @@ -1,16 +1,5 @@ import { style } from '@vanilla-extract/css'; -export const tabStyle = style({ - display: 'flex', - flex: '1', - width: '100%', - padding: '0 10px', - margin: '0', - justifyContent: 'center', - alignItems: 'center', - position: 'relative', -}); - export const menuItemStyle = style({ padding: '4px 18px', paddingBottom: '16px', @@ -20,9 +9,9 @@ export const menuItemStyle = style({ export const descriptionStyle = style({ wordWrap: 'break-word', // wordBreak: 'break-all', - fontSize: '16px', - marginTop: '16px', - marginBottom: '16px', + fontSize: 'var(--affine-font-xs)', + lineHeight: '20px', + color: 'var(--affine-text-secondary-color)', }); export const buttonStyle = style({ @@ -42,13 +31,103 @@ export const containerStyle = style({ display: 'flex', width: '100%', flexDirection: 'column', + gap: '8px', }); + export const indicatorContainerStyle = style({ position: 'relative', }); + export const inputButtonRowStyle = style({ display: 'flex', justifyContent: 'center', alignItems: 'center', marginTop: '16px', }); + +export const titleContainerStyle = style({ + display: 'flex', + alignItems: 'center', + gap: '4px', + fontSize: 'var(--affine-font-sm)', + fontWeight: 600, + lineHeight: '22px', + padding: '0 4px', +}); +export const subTitleStyle = style({ + fontSize: 'var(--affine-font-sm)', + fontWeight: 500, + lineHeight: '22px', +}); + +export const columnContainerStyle = style({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + padding: '0 4px', + width: '100%', + gap: '12px', +}); + +export const rowContainerStyle = style({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + gap: '12px', + padding: '4px', +}); + +export const radioButtonGroup = style({ + display: 'flex', + justifyContent: 'flex-end', + padding: '2px', + minWidth: '154px', + maxWidth: '250px', +}); + +export const radioButton = style({ + color: 'var(--affine-text-secondary-color)', + selectors: { + '&[data-state="checked"]': { + color: 'var(--affine-text-primary-color)', + }, + }, +}); +export const spanStyle = style({ + padding: '4px 20px', +}); + +export const disableSharePage = style({ + color: 'var(--affine-error-color)', +}); + +export const localSharePage = style({ + padding: '12px 8px', + display: 'flex', + alignItems: 'center', + borderRadius: '8px', + backgroundColor: 'var(--affine-background-secondary-color)', + minHeight: '108px', + position: 'relative', +}); + +export const cloudSvgContainer = style({ + width: '100%', + height: '100%', + minWidth: '185px', +}); + +export const cloudSvgStyle = style({ + width: '193px', + height: '108px', + position: 'absolute', + bottom: '0', + right: '8px', +}); +export const shareIconStyle = style({ + fontSize: '16px', + color: 'var(--affine-icon-color)', + display: 'flex', + alignItems: 'center', +}); diff --git a/packages/component/src/components/share-menu/index.jotai.ts b/packages/component/src/components/share-menu/index.jotai.ts new file mode 100644 index 0000000000..93980dc4af --- /dev/null +++ b/packages/component/src/components/share-menu/index.jotai.ts @@ -0,0 +1,3 @@ +import { atom } from 'jotai/vanilla'; + +export const enableShareMenuAtom = atom(false); diff --git a/packages/component/src/components/share-menu/index.tsx b/packages/component/src/components/share-menu/index.tsx index 67f29e7ecf..6dabe4f3d1 100644 --- a/packages/component/src/components/share-menu/index.tsx +++ b/packages/component/src/components/share-menu/index.tsx @@ -1,4 +1,3 @@ export * from './disable-public-link'; export * from './share-menu'; -export * from './share-workspace'; export * from './styles'; diff --git a/packages/component/src/components/share-menu/share-export.tsx b/packages/component/src/components/share-menu/share-export.tsx new file mode 100644 index 0000000000..ff4d23ff4e --- /dev/null +++ b/packages/component/src/components/share-menu/share-export.tsx @@ -0,0 +1,48 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; + +import { + ExportToHtmlMenuItem, + ExportToMarkdownMenuItem, + ExportToPdfMenuItem, + ExportToPngMenuItem, +} from '../page-list/operation-menu-items/export'; +import * as styles from './index.css'; +// import type { ShareMenuProps } from './share-menu'; + +export const ShareExport = () => { + const t = useAFFiNEI18N(); + return ( + <> +
+ {t['com.affine.share-menu.ShareViaExport']()} +
+
+ + + + +
+
+
+ {t['com.affine.share-menu.ShareViaExportDescription']()} +
+
+ + ); +}; diff --git a/packages/component/src/components/share-menu/share-menu.tsx b/packages/component/src/components/share-menu/share-menu.tsx index 9eb5fec39d..61dc87f328 100644 --- a/packages/component/src/components/share-menu/share-menu.tsx +++ b/packages/component/src/components/share-menu/share-menu.tsx @@ -1,139 +1,66 @@ import type { AffineCloudWorkspace, + AffineOfficialWorkspace, + AffinePublicWorkspace, LocalWorkspace, } from '@affine/env/workspace'; -import { ExportIcon, PublishIcon, ShareIcon } from '@blocksuite/icons'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { Page } from '@blocksuite/store'; import { Button } from '@toeverything/components/button'; -import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-block-suite-workspace-page-is-public'; -import { type ReactElement, useRef } from 'react'; -import { useCallback, useState } from 'react'; +import { Divider } from '@toeverything/components/divider'; +import { useAtom } from 'jotai'; import { Menu } from '../../ui/menu/menu'; -import { Export } from './export'; -import { containerStyle, indicatorContainerStyle, tabStyle } from './index.css'; +import * as styles from './index.css'; +import { enableShareMenuAtom } from './index.jotai'; +import { ShareExport } from './share-export'; import { SharePage } from './share-page'; -import { ShareWorkspace } from './share-workspace'; -import { StyledIndicator, TabItem } from './styles'; - -type SharePanel = 'SharePage' | 'Export' | 'ShareWorkspace'; -type ShareMenuComponent = (props: T) => ReactElement; - -const MenuItems: Record> = { - SharePage: SharePage, - Export: Export, - ShareWorkspace: ShareWorkspace, -}; - -const tabIcons = { - SharePage: , - Export: , - ShareWorkspace: , -}; export interface ShareMenuProps< - Workspace extends AffineCloudWorkspace | LocalWorkspace = + Workspace extends AffineOfficialWorkspace = | AffineCloudWorkspace - | LocalWorkspace, + | LocalWorkspace + | AffinePublicWorkspace, > { workspace: Workspace; currentPage: Page; - onEnableAffineCloud: (workspace: LocalWorkspace) => void; - onOpenWorkspaceSettings: (workspace: Workspace) => void; - togglePagePublic: (page: Page, isPublic: boolean) => Promise; - toggleWorkspacePublish: ( - workspace: Workspace, - publish: boolean - ) => Promise; -} - -function assertInstanceOf( - obj: T, - type: new (...args: any[]) => U -): asserts obj is U { - if (!(obj instanceof type)) { - throw new Error('Object is not instance of type'); - } + useIsSharedPage: ( + workspaceId: string, + pageId: string + ) => [isSharePage: boolean, setIsSharePage: (enable: boolean) => void]; + onEnableAffineCloud: () => void; + togglePagePublic: () => Promise; } export const ShareMenu = (props: ShareMenuProps) => { - const [activeItem, setActiveItem] = useState('SharePage'); - const [isPublic] = useBlockSuiteWorkspacePageIsPublic(props.currentPage); - const [open, setOpen] = useState(false); - const containerRef = useRef(null); - const indicatorRef = useRef(null); - const startTransaction = useCallback(() => { - if (indicatorRef.current && containerRef.current) { - const indicator = indicatorRef.current; - const activeTabElement = containerRef.current.querySelector( - `[data-tab-key="${activeItem}"]` - ); - assertInstanceOf(activeTabElement, HTMLElement); - requestAnimationFrame(() => { - indicator.style.left = `${activeTabElement.offsetLeft}px`; - indicator.style.width = `${activeTabElement.offsetWidth}px`; - }); - } - }, [activeItem]); - const handleMenuChange = useCallback( - (selectedItem: SharePanel) => { - setActiveItem(selectedItem); - startTransaction(); - }, - [setActiveItem, startTransaction] + const { useIsSharedPage } = props; + const isSharedPage = useIsSharedPage( + props.workspace.id, + props.currentPage.id ); - - const ActiveComponent = MenuItems[activeItem]; - interface ShareMenuProps { - activeItem: SharePanel; - onChangeTab: (selectedItem: SharePanel) => void; - } - const ShareMenu = ({ activeItem, onChangeTab }: ShareMenuProps) => { - const handleButtonClick = (itemName: SharePanel) => { - onChangeTab(itemName); - setActiveItem(itemName); - }; - - return ( -
- {Object.keys(MenuItems).map(item => ( - handleButtonClick(item as SharePanel)} - > - {tabIcons[item as SharePanel]} - {isPublic ? (item === 'SharePage' ? 'SharedPage' : item) : item} - - ))} + const [open, setOpen] = useAtom(enableShareMenuAtom); + const t = useAFFiNEI18N(); + const content = ( +
+ +
+
- ); - }; - const Share = ( - <> - -
- { - indicatorRef.current = ref; - startTransaction(); - }} - /> -
- -
- -
- + +
); return ( { setOpen(false); @@ -144,9 +71,19 @@ export const ShareMenu = (props: ShareMenuProps) => { onClick={() => { setOpen(!open); }} - type={isPublic ? 'primary' : undefined} + type={'plain'} > -
{isPublic ? 'Shared' : 'Share'}
+
+ {isSharedPage + ? t['com.affine.share-menu.sharedButton']() + : t['com.affine.share-menu.shareButton']()} +
); diff --git a/packages/component/src/components/share-menu/share-page.tsx b/packages/component/src/components/share-menu/share-page.tsx index 4f396daeba..e97223dec3 100644 --- a/packages/component/src/components/share-menu/share-page.tsx +++ b/packages/component/src/components/share-menu/share-page.tsx @@ -1,49 +1,90 @@ -import type { LocalWorkspace } from '@affine/env/workspace'; +import { + Menu, + MenuItem, + MenuTrigger, + RadioButton, + RadioButtonGroup, + Switch, +} from '@affine/component'; import { WorkspaceFlavour } from '@affine/env/workspace'; -import { Trans } from '@affine/i18n'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { ArrowRightSmallIcon, WebIcon } from '@blocksuite/icons'; import { Button } from '@toeverything/components/button'; -import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-block-suite-workspace-page-is-public'; import { useState } from 'react'; import { useCallback, useMemo } from 'react'; +import Input from '../../ui/input'; import { toast } from '../../ui/toast'; import { PublicLinkDisableModal } from './disable-public-link'; -import { - descriptionStyle, - inputButtonRowStyle, - menuItemStyle, -} from './index.css'; +import * as styles from './index.css'; import type { ShareMenuProps } from './share-menu'; -import { StyledDisableButton, StyledInput, StyledLinkSpan } from './styles'; + +const CloudSvg = () => ( + + + + + + + + + + + + +); export const LocalSharePage = (props: ShareMenuProps) => { const t = useAFFiNEI18N(); return ( -
-
{t['Shared Pages Description']()}
- -
+ <> +
+
+ +
+ {t['com.affine.share-menu.SharePage']()} +
+
+
+
+ {t['com.affine.share-menu.EnableCloudDescription']()} +
+
+ +
+
+
+ +
+ ); }; export const AffineSharePage = (props: ShareMenuProps) => { - const [isPublic, setIsPublic] = useBlockSuiteWorkspacePageIsPublic( - props.currentPage - ); + const { + workspace: { id: workspaceId }, + currentPage: { id: pageId }, + } = props; + const [isPublic, setIsPublic] = props.useIsSharedPage(workspaceId, pageId); const [showDisable, setShowDisable] = useState(false); const t = useAFFiNEI18N(); const sharingUrl = useMemo(() => { - return `${prefixUrl}public-workspace/${props.workspace.id}/${props.currentPage.id}`; - }, [props.workspace.id, props.currentPage.id]); + return `${runtimeConfig.serverUrlPrefix}/share/${workspaceId}/${pageId}`; + }, [workspaceId, pageId]); const onClickCreateLink = useCallback(() => { setIsPublic(true); }, [setIsPublic]); @@ -65,51 +106,126 @@ export const AffineSharePage = (props: ShareMenuProps) => { }, [setIsPublic]); return ( -
-
- {t['Create Shared Link Description']()} + <> +
+
+ +
+ {t['com.affine.share-menu.SharePage']()}
-
- + {t['com.affine.share-menu.ShareWithLink']()} +
+
+
+ {t['com.affine.share-menu.ShareWithLinkDescription']()} +
+
+
+ - {!isPublic && ( + {isPublic ? ( + + ) : ( - )} - {isPublic && ( - )}
-
- - The entire Workspace is published on the web and can be edited via - { - props.onOpenWorkspaceSettings(props.workspace); - }} - > - Workspace Settings - - . - -
- {isPublic && ( + {runtimeConfig.enableEnhanceShareMode ? ( +
+
+ {t['com.affine.share-menu.ShareMode']()} +
+
+ {}} + > + + {t['Page']()} + + + {t['Edgeless']()} + + +
+
+ ) : null} + {isPublic ? ( <> - setShowDisable(true)}> - {t['Disable Public Link']()} - + {runtimeConfig.enableEnhanceShareMode && ( + <> +
+
Link expires
+
+ Never} + placement="bottom-end" + trigger="click" + > + + Never + + +
+
+
+
+ {'Show "Created with AFFiNE"'} +
+
+ +
+
+
+
+ Search engine indexing +
+
+ +
+
+ + )} +
setShowDisable(true)} + style={{ cursor: 'pointer' }} + > +
+ {t['Disable Public Link']()} +
+ +
{ }} /> - )} -
+ ) : null} + ); }; diff --git a/packages/component/src/components/share-menu/share-workspace.tsx b/packages/component/src/components/share-menu/share-workspace.tsx deleted file mode 100644 index 69e54270fa..0000000000 --- a/packages/component/src/components/share-menu/share-workspace.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import type { - AffineCloudWorkspace, - LocalWorkspace, -} from '@affine/env/workspace'; -import { WorkspaceFlavour } from '@affine/env/workspace'; -import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { Button } from '@toeverything/components/button'; - -import { descriptionStyle, menuItemStyle } from './index.css'; -import type { ShareMenuProps } from './share-menu'; - -const ShareLocalWorkspace = (props: ShareMenuProps) => { - const t = useAFFiNEI18N(); - return ( -
-
- {t['Share Menu Public Workspace Description1']()} -
- -
- ); -}; - -const ShareAffineWorkspace = (props: ShareMenuProps) => { - // fixme: regression - const isPublicWorkspace = false; - const t = useAFFiNEI18N(); - return ( -
-
- {isPublicWorkspace - ? t['Share Menu Public Workspace Description2']() - : t['Share Menu Public Workspace Description1']()} -
- -
- ); -}; - -export const ShareWorkspace = (props: ShareMenuProps) => { - if (props.workspace.flavour === WorkspaceFlavour.LOCAL) { - return ( - )} /> - ); - } else if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) { - return ( - )} - /> - ); - } - throw new Error('Unreachable'); -}; diff --git a/packages/component/src/components/workspace/index.tsx b/packages/component/src/components/workspace/index.tsx index 5754355878..e757c7fce0 100644 --- a/packages/component/src/components/workspace/index.tsx +++ b/packages/component/src/components/workspace/index.tsx @@ -1,20 +1,14 @@ import { clsx } from 'clsx'; -import type { - HTMLAttributes, - PropsWithChildren, - ReactElement, - ReactNode, -} from 'react'; +import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react'; import { AppSidebarFallback } from '../app-sidebar'; import { appStyle, mainContainerStyle, toolStyle } from './index.css'; -export interface WorkspaceRootProps { +export type WorkspaceRootProps = PropsWithChildren<{ resizing?: boolean; useNoisyBackground?: boolean; useBlurBackground?: boolean; - children: ReactNode; -} +}>; export const AppContainer = ({ resizing, @@ -53,7 +47,7 @@ export const MainContainer = ({ {...props} className={clsx(mainContainerStyle, 'main-container', className)} data-is-macos={environment.isDesktop && environment.isMacOs} - data-show-padding={padding} + data-show-padding={!!padding} > {children}
diff --git a/apps/core/public/fonts/inter/Inter-VariableFont_slnt,wght.ttf b/packages/component/src/fonts/inter/Inter-VariableFont_slnt,wght.ttf similarity index 100% rename from apps/core/public/fonts/inter/Inter-VariableFont_slnt,wght.ttf rename to packages/component/src/fonts/inter/Inter-VariableFont_slnt,wght.ttf diff --git a/apps/core/public/fonts/inter/OFL.txt b/packages/component/src/fonts/inter/OFL.txt similarity index 100% rename from apps/core/public/fonts/inter/OFL.txt rename to packages/component/src/fonts/inter/OFL.txt diff --git a/apps/core/public/fonts/kalam/Kalam-Bold.ttf b/packages/component/src/fonts/kalam/Kalam-Bold.ttf similarity index 100% rename from apps/core/public/fonts/kalam/Kalam-Bold.ttf rename to packages/component/src/fonts/kalam/Kalam-Bold.ttf diff --git a/apps/core/public/fonts/kalam/Kalam-Light.ttf b/packages/component/src/fonts/kalam/Kalam-Light.ttf similarity index 100% rename from apps/core/public/fonts/kalam/Kalam-Light.ttf rename to packages/component/src/fonts/kalam/Kalam-Light.ttf diff --git a/apps/core/public/fonts/kalam/Kalam-Regular.ttf b/packages/component/src/fonts/kalam/Kalam-Regular.ttf similarity index 100% rename from apps/core/public/fonts/kalam/Kalam-Regular.ttf rename to packages/component/src/fonts/kalam/Kalam-Regular.ttf diff --git a/apps/core/public/fonts/kalam/OFL.txt b/packages/component/src/fonts/kalam/OFL.txt similarity index 100% rename from apps/core/public/fonts/kalam/OFL.txt rename to packages/component/src/fonts/kalam/OFL.txt diff --git a/apps/core/public/fonts/source-code-pro/OFL.txt b/packages/component/src/fonts/source-code-pro/OFL.txt similarity index 100% rename from apps/core/public/fonts/source-code-pro/OFL.txt rename to packages/component/src/fonts/source-code-pro/OFL.txt diff --git a/apps/core/public/fonts/source-code-pro/SourceCodePro-Italic-VariableFont_wght.ttf b/packages/component/src/fonts/source-code-pro/SourceCodePro-Italic-VariableFont_wght.ttf similarity index 100% rename from apps/core/public/fonts/source-code-pro/SourceCodePro-Italic-VariableFont_wght.ttf rename to packages/component/src/fonts/source-code-pro/SourceCodePro-Italic-VariableFont_wght.ttf diff --git a/apps/core/public/fonts/source-code-pro/SourceCodePro-VariableFont_wght.ttf b/packages/component/src/fonts/source-code-pro/SourceCodePro-VariableFont_wght.ttf similarity index 100% rename from apps/core/public/fonts/source-code-pro/SourceCodePro-VariableFont_wght.ttf rename to packages/component/src/fonts/source-code-pro/SourceCodePro-VariableFont_wght.ttf diff --git a/apps/core/public/fonts/source-serif-4/OFL.txt b/packages/component/src/fonts/source-serif-4/OFL.txt similarity index 100% rename from apps/core/public/fonts/source-serif-4/OFL.txt rename to packages/component/src/fonts/source-serif-4/OFL.txt diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-Bold.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-Bold.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-Bold.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-Bold.ttf diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-BoldItalic.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-BoldItalic.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-BoldItalic.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-BoldItalic.ttf diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-Italic-VariableFont_opsz,wght.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-Italic-VariableFont_opsz,wght.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-Italic-VariableFont_opsz,wght.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-Italic-VariableFont_opsz,wght.ttf diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-Italic.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-Italic.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-Italic.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-Italic.ttf diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-Light.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-Light.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-Light.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-Light.ttf diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-LightItalic.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-LightItalic.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-LightItalic.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-LightItalic.ttf diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-Medium.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-Medium.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-Medium.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-Medium.ttf diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-MediumItalic.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-MediumItalic.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-MediumItalic.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-MediumItalic.ttf diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-Regular.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-Regular.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-Regular.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-Regular.ttf diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-SemiBold.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-SemiBold.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-SemiBold.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-SemiBold.ttf diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-SemiBoldItalic.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-SemiBoldItalic.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-SemiBoldItalic.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-SemiBoldItalic.ttf diff --git a/apps/core/public/fonts/source-serif-4/SourceSerif4-VariableFont_opsz,wght.ttf b/packages/component/src/fonts/source-serif-4/SourceSerif4-VariableFont_opsz,wght.ttf similarity index 100% rename from apps/core/public/fonts/source-serif-4/SourceSerif4-VariableFont_opsz,wght.ttf rename to packages/component/src/fonts/source-serif-4/SourceSerif4-VariableFont_opsz,wght.ttf diff --git a/apps/core/public/fonts/space-mono/OFL.txt b/packages/component/src/fonts/space-mono/OFL.txt similarity index 100% rename from apps/core/public/fonts/space-mono/OFL.txt rename to packages/component/src/fonts/space-mono/OFL.txt diff --git a/apps/core/public/fonts/space-mono/SpaceMono-Bold.ttf b/packages/component/src/fonts/space-mono/SpaceMono-Bold.ttf similarity index 100% rename from apps/core/public/fonts/space-mono/SpaceMono-Bold.ttf rename to packages/component/src/fonts/space-mono/SpaceMono-Bold.ttf diff --git a/apps/core/public/fonts/space-mono/SpaceMono-BoldItalic.ttf b/packages/component/src/fonts/space-mono/SpaceMono-BoldItalic.ttf similarity index 100% rename from apps/core/public/fonts/space-mono/SpaceMono-BoldItalic.ttf rename to packages/component/src/fonts/space-mono/SpaceMono-BoldItalic.ttf diff --git a/apps/core/public/fonts/space-mono/SpaceMono-Italic.ttf b/packages/component/src/fonts/space-mono/SpaceMono-Italic.ttf similarity index 100% rename from apps/core/public/fonts/space-mono/SpaceMono-Italic.ttf rename to packages/component/src/fonts/space-mono/SpaceMono-Italic.ttf diff --git a/apps/core/public/fonts/space-mono/SpaceMono-Regular.ttf b/packages/component/src/fonts/space-mono/SpaceMono-Regular.ttf similarity index 100% rename from apps/core/public/fonts/space-mono/SpaceMono-Regular.ttf rename to packages/component/src/fonts/space-mono/SpaceMono-Regular.ttf diff --git a/packages/component/src/theme/fonts.css b/packages/component/src/theme/fonts.css index 2f936a7832..f944342d6a 100644 --- a/packages/component/src/theme/fonts.css +++ b/packages/component/src/theme/fonts.css @@ -4,7 +4,7 @@ @font-face { font-family: 'Inter'; font-display: swap; - src: url(/fonts/inter/Inter-VariableFont_slnt,wght.ttf); + src: url(../fonts/inter/Inter-VariableFont_slnt,wght.ttf); } /* @@ -14,14 +14,14 @@ font-family: 'Source Code Pro'; font-style: normal; font-display: swap; - src: url(/fonts/source-code-pro/SourceCodePro-VariableFont_wght.ttf); + src: url(../fonts/source-code-pro/SourceCodePro-VariableFont_wght.ttf); } @font-face { font-family: 'Source Code Pro'; font-style: italic; font-display: swap; - src: url(/fonts/source-code-pro/SourceCodePro-Italic-VariableFont_wght.ttf); + src: url(../fonts/source-code-pro/SourceCodePro-Italic-VariableFont_wght.ttf); } /* @@ -32,7 +32,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/kalam/Kalam-Regular.ttf); + src: url(../fonts/kalam/Kalam-Regular.ttf); } @font-face { @@ -40,7 +40,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: url(/fonts/kalam/Kalam-Light.ttf); + src: url(../fonts/kalam/Kalam-Light.ttf); } @font-face { @@ -48,7 +48,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: url(/fonts/kalam/Kalam-Bold.ttf); + src: url(../fonts/kalam/Kalam-Bold.ttf); } /* @@ -59,7 +59,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/source-serif-4/SourceSerif4-Regular.ttf); + src: url(../fonts/source-serif-4/SourceSerif4-Regular.ttf); } @font-face { @@ -67,7 +67,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: url(/fonts/source-serif-4/SourceSerif4-Italic.ttf); + src: url(../fonts/source-serif-4/SourceSerif4-Italic.ttf); } @font-face { @@ -75,7 +75,7 @@ font-style: normal; font-weight: 500; font-display: swap; - src: url(/fonts/source-serif-4/SourceSerif4-Medium.ttf); + src: url(../fonts/source-serif-4/SourceSerif4-Medium.ttf); } @font-face { @@ -83,7 +83,7 @@ font-style: italic; font-weight: 500; font-display: swap; - src: url(/fonts/source-serif-4/SourceSerif4-MediumItalic.ttf); + src: url(../fonts/source-serif-4/SourceSerif4-MediumItalic.ttf); } @font-face { @@ -91,7 +91,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: url(/fonts/source-serif-4/SourceSerif4-SemiBold.ttf); + src: url(../fonts/source-serif-4/SourceSerif4-SemiBold.ttf); } @font-face { @@ -99,7 +99,7 @@ font-style: italic; font-weight: 600; font-display: swap; - src: url(/fonts/source-serif-4/SourceSerif4-SemiBoldItalic.ttf); + src: url(../fonts/source-serif-4/SourceSerif4-SemiBoldItalic.ttf); } @font-face { @@ -107,7 +107,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: url(/fonts/source-serif-4/SourceSerif4-Bold.ttf); + src: url(../fonts/source-serif-4/SourceSerif4-Bold.ttf); } @font-face { @@ -115,7 +115,7 @@ font-style: italic; font-weight: 700; font-display: swap; - src: url(/fonts/source-serif-4/SourceSerif4-BoldItalic.ttf); + src: url(../fonts/source-serif-4/SourceSerif4-BoldItalic.ttf); } /* @@ -126,7 +126,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/space-mono/SpaceMono-Regular.ttf); + src: url(../fonts/space-mono/SpaceMono-Regular.ttf); } @font-face { @@ -134,7 +134,7 @@ font-style: italic; font-weight: 400; font-display: swap; - src: url(/fonts/space-mono/SpaceMono-Italic.ttf); + src: url(../fonts/space-mono/SpaceMono-Italic.ttf); } @font-face { @@ -142,7 +142,7 @@ font-style: normal; font-weight: 700; font-display: swap; - src: url(/fonts/space-mono/SpaceMono-Bold.ttf); + src: url(../fonts/space-mono/SpaceMono-Bold.ttf); } @font-face { @@ -150,5 +150,5 @@ font-style: italic; font-weight: 700; font-display: swap; - src: url(/fonts/space-mono/SpaceMono-BoldItalic.ttf); + src: url(../fonts/space-mono/SpaceMono-BoldItalic.ttf); } diff --git a/packages/component/src/theme/global.css b/packages/component/src/theme/global.css index d701426a2a..a8cadf2405 100644 --- a/packages/component/src/theme/global.css +++ b/packages/component/src/theme/global.css @@ -1,11 +1,13 @@ @import 'react-datepicker/dist/react-datepicker.css'; @import './fonts.css'; + * { -webkit-overflow-scrolling: touch; -webkit-tap-highlight-color: rgba(255, 255, 255, 0); box-sizing: border-box; /*transition: all 0.1s;*/ } + html, body, h1, @@ -40,6 +42,7 @@ menu { margin: 0; padding: 0; } + header, footer, section, @@ -54,15 +57,18 @@ menu, details { display: block; } + table { border-collapse: collapse; border-spacing: 0; } + caption, th { text-align: left; font-weight: normal; } + html, body, fieldset, @@ -71,6 +77,7 @@ iframe, abbr { border: 0; } + i, cite, em, @@ -79,13 +86,16 @@ address, dfn { font-style: normal; } + [hidefocus], summary { outline: 0; } + li { list-style: none; } + h1, h2, h3, @@ -95,32 +105,39 @@ h6, small { font-size: 100%; } + sup, sub { font-size: 83%; } + pre, code, kbd, samp { font-family: inherit; } + q:before, q:after { content: none; } + textarea { overflow: auto; resize: none; } + label, summary { cursor: default; } + a, button { cursor: pointer; } + h1, h2, h3, @@ -131,6 +148,7 @@ strong, b { font-weight: bold; } + del, ins, u, @@ -139,6 +157,7 @@ a, a:hover { text-decoration: none; } + body, textarea, input, @@ -150,9 +169,9 @@ legend { outline: 0; border: 0; font-size: var(--affine-font-base); - line-height: var(--affine-line-height); font-family: var(--affine-font-family); } + body { background: transparent; overflow: hidden; @@ -161,9 +180,12 @@ body { input { border: none; -moz-appearance: none; - -webkit-appearance: none; /*Solve the rounded corners of buttons on ios*/ - border-radius: 0; /*Solve the problem of rounded corners of the input box on ios*/ - outline: medium; /*Remove the default yellow border on mouse click*/ + -webkit-appearance: none; + /*Solve the rounded corners of buttons on ios*/ + border-radius: 0; + /*Solve the problem of rounded corners of the input box on ios*/ + outline: medium; + /*Remove the default yellow border on mouse click*/ background-color: transparent; caret-color: var(--affine-primary-color); } @@ -183,17 +205,24 @@ input[type='number']::-webkit-outer-spin-button { } * { - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE 10+ */ + scrollbar-width: none; + /* Firefox */ + -ms-overflow-style: none; + /* IE 10+ */ } + ::-webkit-scrollbar { - width: 0; /* Chrome Safari */ + width: 0; + /* Chrome Safari */ height: 0; } + .affine-doc-viewport::-webkit-scrollbar { - width: 20px; /* Chrome Safari */ + width: 20px; + /* Chrome Safari */ height: 20px; } + .affine-doc-viewport::-webkit-scrollbar-thumb { border-radius: 12px; background-clip: padding-box; @@ -202,13 +231,16 @@ input[type='number']::-webkit-outer-spin-button { border-color: transparent; transition: all 0.2s; } + .affine-doc-viewport:hover::-webkit-scrollbar-thumb { background-color: var(--affine-divider-color); } + .affine-doc-viewport:hover::-webkit-scrollbar-thumb:hover { background-color: var(--affine-icon-color); border-width: 5px; } + .editor-wrapper { position: relative; padding-right: 0; @@ -233,6 +265,7 @@ input[type='number']::-webkit-outer-spin-button { affine-block-hub { position: unset !important; } + .block-hub-menu-container { position: unset !important; } @@ -260,3 +293,7 @@ textarea, [role='button'] { -webkit-app-region: no-drag; } + +#webpack-dev-server-client-overlay { + -webkit-app-region: no-drag; +} diff --git a/packages/component/src/theme/theme.css.ts b/packages/component/src/theme/theme.css.ts index 299c8f3bbf..24187dc84a 100644 --- a/packages/component/src/theme/theme.css.ts +++ b/packages/component/src/theme/theme.css.ts @@ -5,7 +5,6 @@ globalStyle('body', { color: 'var(--affine-text-primary-color)', fontFamily: 'var(--affine-font-family)', fontSize: 'var(--affine-font-base)', - lineHeight: 'var(--affine-font-height)', }); globalStyle('html', { diff --git a/packages/component/src/ui/button/radio.tsx b/packages/component/src/ui/button/radio.tsx index 92daee1f8a..0ddaef2146 100644 --- a/packages/component/src/ui/button/radio.tsx +++ b/packages/component/src/ui/button/radio.tsx @@ -8,22 +8,27 @@ import { type CSSProperties, forwardRef } from 'react'; import * as styles from './styles.css'; -export const RadioButton = forwardRef( - ({ children, className, ...props }, ref) => { - return ( - (({ children, className, spanStyle, ...props }, ref) => { + return ( + + + {children} + + - {children} - - {children} - - - ); - } -); + {children} + + + ); +}); RadioButton.displayName = 'RadioButton'; export const RadioButtonGroup = forwardRef< diff --git a/packages/component/src/ui/button/style.css.ts b/packages/component/src/ui/button/style.css.ts index 7937490855..b373e35101 100644 --- a/packages/component/src/ui/button/style.css.ts +++ b/packages/component/src/ui/button/style.css.ts @@ -25,18 +25,24 @@ export const button = style({ '&.text-bold': { fontWeight: 600, }, - '&:hover': { + '&:not(.without-hover):hover': { background: 'var(--affine-hover-color)', }, - '&.disabled, &.loading': { + '&.disabled': { opacity: '.4', cursor: 'default', color: 'var(--affine-disable-color)', pointerEvents: 'none', }, - '&.disabled:hover, &.loading:hover': { - background: 'inherit', + '&.loading': { + cursor: 'default', + color: 'var(--affine-disable-color)', + pointerEvents: 'none', }, + '&.disabled:not(.without-hover):hover, &.loading:not(.without-hover):hover': + { + background: 'inherit', + }, '&.block': { display: 'flex', width: '100%' }, @@ -64,6 +70,7 @@ export const button = style({ '&.plain': { color: 'var(--affine-text-primary-color)', borderColor: 'transparent', + background: 'transparent', }, '&.primary': { @@ -72,7 +79,7 @@ export const button = style({ borderColor: 'var(--affine-black-10)', boxShadow: 'var(--affine-button-inner-shadow)', }, - '&.primary:hover': { + '&.primary:not(.without-hover):hover': { background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-primary-color)', }, @@ -80,7 +87,7 @@ export const button = style({ opacity: '.4', cursor: 'default', }, - '&.primary.disabled:hover': { + '&.primary.disabled:not(.without-hover):hover': { background: 'var(--affine-primary-color)', }, @@ -90,7 +97,7 @@ export const button = style({ borderColor: 'var(--affine-black-10)', boxShadow: 'var(--affine-button-inner-shadow)', }, - '&.error:hover': { + '&.error:not(.without-hover):hover': { background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-error-color)', }, @@ -98,7 +105,7 @@ export const button = style({ opacity: '.4', cursor: 'default', }, - '&.error.disabled:hover': { + '&.error.disabled:not(.without-hover):hover': { background: 'var(--affine-error-color)', }, @@ -108,7 +115,7 @@ export const button = style({ borderColor: 'var(--affine-black-10)', boxShadow: 'var(--affine-button-inner-shadow)', }, - '&.warning:hover': { + '&.warning:not(.without-hover):hover': { background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-warning-color)', }, @@ -116,7 +123,7 @@ export const button = style({ opacity: '.4', cursor: 'default', }, - '&.warning.disabled:hover': { + '&.warning.disabled:not(.without-hover):hover': { background: 'var(--affine-warning-color)', }, @@ -126,7 +133,7 @@ export const button = style({ borderColor: 'var(--affine-black-10)', boxShadow: 'var(--affine-button-inner-shadow)', }, - '&.success:hover': { + '&.success:not(.without-hover):hover': { background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-success-color)', }, @@ -134,7 +141,7 @@ export const button = style({ opacity: '.4', cursor: 'default', }, - '&.success.disabled:hover': { + '&.success.disabled:not(.without-hover):hover': { background: 'var(--affine-success-color)', }, @@ -144,7 +151,7 @@ export const button = style({ borderColor: 'var(--affine-black-10)', boxShadow: 'var(--affine-button-inner-shadow)', }, - '&.processing:hover': { + '&.processing:not(.without-hover):hover': { background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-processing-color)', }, @@ -152,7 +159,7 @@ export const button = style({ opacity: '.4', cursor: 'default', }, - '&.processing.disabled:hover': { + '&.processing.disabled:not(.without-hover):hover': { background: 'var(--affine-processing-color)', }, }, @@ -222,18 +229,24 @@ export const iconButton = style({ color: 'var(--affine-primary-color)', }, - '&:hover': { + '&:not(.without-hover):hover': { background: 'var(--affine-hover-color)', }, - '&.disabled, &.loading': { + '&.disabled': { opacity: '.4', cursor: 'default', color: 'var(--affine-disable-color)', pointerEvents: 'none', }, - '&.disabled:hover, &.loading:hover': { - background: 'inherit', + '&.loading': { + cursor: 'default', + color: 'var(--affine-disable-color)', + pointerEvents: 'none', }, + '&.disabled:not(.without-hover):hover, &.loading:not(.without-hover):hover': + { + background: 'inherit', + }, // size '&.large': { @@ -251,6 +264,7 @@ export const iconButton = style({ '&.plain': { color: 'var(--affine-icon-color)', borderColor: 'transparent', + background: 'transparent', }, '&.plain.active': { color: 'var(--affine-primary-color)', @@ -262,7 +276,7 @@ export const iconButton = style({ borderColor: 'var(--affine-black-10)', boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset', }, - '&.primary:hover': { + '&.primary:not(.without-hover):hover': { background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-primary-color)', }, @@ -270,7 +284,7 @@ export const iconButton = style({ opacity: '.4', cursor: 'default', }, - '&.primary.disabled:hover': { + '&.primary.disabled:not(.without-hover):hover': { background: 'var(--affine-primary-color)', }, @@ -280,7 +294,7 @@ export const iconButton = style({ borderColor: 'var(--affine-black-10)', boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset', }, - '&.error:hover': { + '&.error:not(.without-hover):hover': { background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-error-color)', }, @@ -288,7 +302,7 @@ export const iconButton = style({ opacity: '.4', cursor: 'default', }, - '&.error.disabled:hover': { + '&.error.disabled:not(.without-hover):hover': { background: 'var(--affine-error-color)', }, @@ -298,7 +312,7 @@ export const iconButton = style({ borderColor: 'var(--affine-black-10)', boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset', }, - '&.warning:hover': { + '&.warning:not(.without-hover):hover': { background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-warning-color)', }, @@ -306,7 +320,7 @@ export const iconButton = style({ opacity: '.4', cursor: 'default', }, - '&.warning.disabled:hover': { + '&.warning.disabled:not(.without-hover):hover': { background: 'var(--affine-warning-color)', }, @@ -316,7 +330,7 @@ export const iconButton = style({ borderColor: 'var(--affine-black-10)', boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset', }, - '&.success:hover': { + '&.success:not(.without-hover):hover': { background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-success-color)', }, @@ -324,7 +338,7 @@ export const iconButton = style({ opacity: '.4', cursor: 'default', }, - '&.success.disabled:hover': { + '&.success.disabled:not(.without-hover):hover': { background: 'var(--affine-success-color)', }, @@ -334,7 +348,7 @@ export const iconButton = style({ borderColor: 'var(--affine-black-10)', boxShadow: '0px 1px 2px 0px rgba(255, 255, 255, 0.25) inset', }, - '&.processing:hover': { + '&.processing:not(.without-hover):hover': { background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) 100%), var(--affine-processing-color)', }, @@ -342,7 +356,7 @@ export const iconButton = style({ opacity: '.4', cursor: 'default', }, - '&.processing.disabled:hover': { + '&.processing.disabled:not(.without-hover):hover': { background: 'var(--affine-processing-color)', }, }, diff --git a/packages/component/src/ui/button/utils.ts b/packages/component/src/ui/button/utils.ts index 874e1c5b47..1e153d751b 100644 --- a/packages/component/src/ui/button/utils.ts +++ b/packages/component/src/ui/button/utils.ts @@ -8,14 +8,14 @@ export const SIZE_CONFIG = { [SIZE_SMALL]: { iconSize: 16, fontSize: 'var(--affine-font-xs)', - borderRadius: 4, + borderRadius: 8, height: 28, - padding: 6, + padding: 12, }, [SIZE_MIDDLE]: { iconSize: 20, fontSize: 'var(--affine-font-sm)', - borderRadius: 4, + borderRadius: 8, height: 32, padding: 12, }, @@ -24,7 +24,7 @@ export const SIZE_CONFIG = { fontSize: 'var(--affine-font-base)', height: 38, padding: 24, - borderRadius: 4, + borderRadius: 8, }, } as const; @@ -51,6 +51,7 @@ export const getButtonColors = ( color: 'var(--affine-white)', borderColor: 'var(--affine-primary-color)', backgroundBlendMode: 'overlay', + opacity: disabled ? '.4' : '1', '.affine-button-icon': { color: 'var(--affine-white)', }, diff --git a/packages/component/src/ui/input/index.css.ts b/packages/component/src/ui/input/index.css.ts deleted file mode 100644 index 84ca25ad5d..0000000000 --- a/packages/component/src/ui/input/index.css.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { createVar, style } from '@vanilla-extract/css'; - -export const heightVar = createVar('heightVar'); -export const widthVar = createVar('widthVar'); - -export const inputStyle = style({ - vars: { - [heightVar]: 'unset', - [widthVar]: '100%', - }, - width: widthVar, - height: heightVar, - lineHeight: '22px', - padding: '8px 12px', - color: 'var(--affine-text-primary-color)', - border: '1px solid', - borderColor: 'var(--affine-border-color)', // TODO: check out disableColor, - backgroundColor: 'var(--affine-white)', - borderRadius: '10px', - selectors: { - '&[data-no-border="true"]': { - border: 'unset', - }, - '&[data-disabled="true"]': { - color: 'var(--affine-text-disable-color)', - }, - '&::placeholder': { - color: 'var(--affine-placeholder-color)', - }, - '&:focus': { - borderColor: 'var(--affine-primary-color)', - }, - }, -}); diff --git a/packages/component/src/ui/input/input.tsx b/packages/component/src/ui/input/input.tsx index ce03ee73e1..f37bc4dd75 100644 --- a/packages/component/src/ui/input/input.tsx +++ b/packages/component/src/ui/input/input.tsx @@ -1,84 +1,120 @@ import { assignInlineVars } from '@vanilla-extract/dynamic'; import clsx from 'clsx'; -import { useCompositionInput } from 'foxact/use-composition-input'; import type { + ChangeEvent, CSSProperties, + FocusEvent, FocusEventHandler, ForwardedRef, - HTMLAttributes, + InputHTMLAttributes, + KeyboardEvent, KeyboardEventHandler, + ReactNode, } from 'react'; -import { forwardRef, useCallback } from 'react'; +import { forwardRef, useCallback, useState } from 'react'; -import { heightVar, inputStyle, widthVar } from './index.css'; +import { input, inputWrapper, widthVar } from './style.css'; -type InputProps = { - // We don't have `value` props here, - // see https://foxact.skk.moe/use-composition-input - defaultValue?: string | undefined; - placeholder?: string; +export type InputProps = { disabled?: boolean; width?: CSSProperties['width']; - height?: CSSProperties['height']; - maxLength?: number; - minLength?: number; onChange?: (value: string) => void; onBlur?: FocusEventHandler; onKeyDown?: KeyboardEventHandler; noBorder?: boolean; -} & Omit< - HTMLAttributes, - | 'onChange' - | 'value' - | 'defaultValue' - | 'onCompositionStart' - | 'onCompositionEnd' ->; + status?: 'error' | 'success' | 'warning' | 'default'; + size?: 'default' | 'large' | 'extraLarge'; + preFix?: ReactNode; + endFix?: ReactNode; + type?: HTMLInputElement['type']; + inputStyle?: CSSProperties; + onEnter?: () => void; +} & Omit, 'onChange' | 'size'>; export const Input = forwardRef(function Input( { disabled, - defaultValue, - placeholder, - maxLength, - minLength, - height, width, - onChange, + onChange: propsOnChange, noBorder = false, className, + status = 'default', + style = {}, + inputStyle = {}, + size = 'default', + onFocus, + onBlur, + preFix, + endFix, + onEnter, + onKeyDown, ...otherProps }: InputProps, ref: ForwardedRef ) { - const inputProps = useCompositionInput( - useCallback( - (value: string) => { - onChange && onChange(value); - }, - [onChange] - ) - ); + const [isFocus, setIsFocus] = useState(false); return ( - + style={{ + ...assignInlineVars({ + [widthVar]: width ? `${width}px` : '100%', + }), + ...style, + }} + > + {preFix} + ) => { + setIsFocus(true); + onFocus?.(e); + }, + [onFocus] + )} + onBlur={useCallback( + (e: FocusEvent) => { + setIsFocus(false); + onBlur?.(e); + }, + [onBlur] + )} + onChange={useCallback( + (e: ChangeEvent) => { + propsOnChange?.(e.target.value); + }, + [propsOnChange] + )} + onKeyDown={useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + onEnter?.(); + } + onKeyDown?.(e); + }, + [onKeyDown, onEnter] + )} + {...otherProps} + /> + {endFix} +
); }); diff --git a/packages/component/src/ui/input/style.css.ts b/packages/component/src/ui/input/style.css.ts new file mode 100644 index 0000000000..c61135ee4f --- /dev/null +++ b/packages/component/src/ui/input/style.css.ts @@ -0,0 +1,76 @@ +import { createVar, style } from '@vanilla-extract/css'; + +export const widthVar = createVar('widthVar'); + +export const inputWrapper = style({ + vars: { + [widthVar]: '100%', + }, + width: widthVar, + height: 28, + lineHeight: '22px', + padding: '0 10px', + color: 'var(--affine-text-primary-color)', + border: '1px solid', + backgroundColor: 'var(--affine-white)', + borderRadius: 8, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: 'var(--affine-font-base)', + + selectors: { + '&.no-border': { + border: 'unset', + }, + // size + '&.large': { + height: 32, + }, + '&.extra-large': { + height: 40, + fontWeight: 600, + }, + // color + '&.disabled': { + background: 'var(--affine-hover-color)', + }, + '&.error': { + borderColor: 'var(--affine-error-color)', + }, + '&.success': { + borderColor: 'var(--affine-success-color)', + }, + '&.warning': { + borderColor: 'var(--affine-warning-color)', + }, + '&.default': { + borderColor: 'var(--affine-border-color)', + }, + '&.default.focus': { + borderColor: 'var(--affine-primary-color)', + boxShadow: '0px 0px 0px 2px rgba(30, 150, 235, 0.30);', + }, + }, +}); + +export const input = style({ + height: '100%', + width: '0', + flex: 1, + boxSizing: 'border-box', + // prevent default style + WebkitAppearance: 'none', + WebkitTapHighlightColor: 'transparent', + outline: 'none', + border: 'none', + + selectors: { + '&::placeholder': { + color: 'var(--affine-placeholder-color)', + }, + '&:disabled': { + color: 'var(--affine-text-disable-color)', + }, + }, +}); diff --git a/packages/component/src/ui/menu/menu-item.tsx b/packages/component/src/ui/menu/menu-item.tsx index 69777283b6..c7829239dd 100644 --- a/packages/component/src/ui/menu/menu-item.tsx +++ b/packages/component/src/ui/menu/menu-item.tsx @@ -11,20 +11,30 @@ import { export type IconMenuProps = PropsWithChildren<{ icon?: ReactElement; endIcon?: ReactElement; - iconSize?: [number, number]; + iconSize?: number; disabled?: boolean; active?: boolean; disableHover?: boolean; + gap?: string; + fontSize?: string; }> & HTMLAttributes; export const MenuItem = forwardRef( - ({ endIcon, icon, children, ...props }, ref) => { + ({ endIcon, icon, children, gap, fontSize, iconSize, ...props }, ref) => { return ( - {icon && {icon}} - {children} - {endIcon && {endIcon}} + {icon && ( + + {icon} + + )} + {children} + {endIcon && ( + + {endIcon} + + )} ); } diff --git a/packages/component/src/ui/menu/styles.ts b/packages/component/src/ui/menu/styles.ts index 9fc3179481..5b27fb5a24 100644 --- a/packages/component/src/ui/menu/styles.ts +++ b/packages/component/src/ui/menu/styles.ts @@ -23,28 +23,36 @@ export const StyledMenuWrapper = styled(StyledPopperContainer, { }; }); -export const StyledStartIconWrapper = styled('div')(() => { +export const StyledStartIconWrapper = styled('div')<{ + gap?: CSSProperties['gap']; + iconSize?: CSSProperties['fontSize']; +}>(({ gap, iconSize }) => { return { display: 'flex', - marginRight: '12px', - fontSize: '20px', + marginRight: gap ? gap : '12px', + fontSize: iconSize ? iconSize : '20px', color: 'var(--affine-icon-color)', }; }); -export const StyledEndIconWrapper = styled('div')(() => { +export const StyledEndIconWrapper = styled('div')<{ + gap?: CSSProperties['gap']; + iconSize?: CSSProperties['fontSize']; +}>(({ gap, iconSize }) => { return { display: 'flex', - marginLeft: '12px', - fontSize: '20px', + marginLeft: gap ? gap : '12px', + fontSize: iconSize ? iconSize : '20px', color: 'var(--affine-icon-color)', }; }); -export const StyledContent = styled('div')(() => { +export const StyledContent = styled('div')<{ + fontSize?: CSSProperties['fontSize']; +}>(({ fontSize }) => { return { textAlign: 'left', flexGrow: 1, - fontSize: 'var(--affine-font-base)', + fontSize: fontSize ? fontSize : 'var(--affine-font-base)', ...textEllipsis(1), }; }); diff --git a/packages/component/src/ui/modal/confirm-modal.tsx b/packages/component/src/ui/modal/confirm-modal.tsx new file mode 100644 index 0000000000..43041cc74f --- /dev/null +++ b/packages/component/src/ui/modal/confirm-modal.tsx @@ -0,0 +1,72 @@ +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { Button, type ButtonType } from '@toeverything/components/button'; +import { useCallback } from 'react'; + +import { Modal, type ModalProps } from './modal'; +import { ModalCloseButton } from './modal-close-button'; +import { ModalWrapper } from './modal-wrapper'; +import { + StyledModalContent, + StyledModalFooter, + StyledModalTitle, +} from './styles'; + +export interface BaseModalProps + extends Omit { + title?: string; + content?: string; + confirmText?: string; + confirmType?: ButtonType; + onClose: () => void; + onCancel: () => void; + onConfirm: () => void; +} + +export const ConfirmModal = ({ + open, + onClose, + confirmText, + confirmType = 'primary', + onCancel, + onConfirm, + title, + content, +}: BaseModalProps) => { + const t = useAFFiNEI18N(); + + const handleClose = useCallback(() => { + onClose(); + }, [onClose]); + + return ( + + + + {title} + {content} + + + + + + + ); +}; diff --git a/packages/component/src/ui/modal/index.tsx b/packages/component/src/ui/modal/index.tsx index a0eb993d99..819d5d67ea 100644 --- a/packages/component/src/ui/modal/index.tsx +++ b/packages/component/src/ui/modal/index.tsx @@ -1,5 +1,6 @@ import Modal from './modal'; +export * from './confirm-modal'; export * from './modal'; export * from './modal-close-button'; export * from './modal-wrapper'; diff --git a/packages/component/src/ui/modal/modal-close-button.tsx b/packages/component/src/ui/modal/modal-close-button.tsx index 5cdc715349..06b0a38233 100644 --- a/packages/component/src/ui/modal/modal-close-button.tsx +++ b/packages/component/src/ui/modal/modal-close-button.tsx @@ -37,5 +37,3 @@ export const ModalCloseButton = ({ ); }; - -export default ModalCloseButton; diff --git a/packages/component/src/ui/modal/styles.ts b/packages/component/src/ui/modal/styles.ts index 4125284cb9..bfb1b5e8f6 100644 --- a/packages/component/src/ui/modal/styles.ts +++ b/packages/component/src/ui/modal/styles.ts @@ -2,7 +2,6 @@ import { Modal as ModalUnstyled } from '@mui/base/Modal'; import type { CSSProperties } from 'react'; import { styled } from '../../styles'; -import { Wrapper } from '../layout'; export const StyledBackdrop = styled('div')(() => { return { @@ -42,10 +41,25 @@ export const StyledModal = styled(ModalUnstyled, { }; }); -export const StyledWrapper = styled(Wrapper)(() => { +export const StyledModalFooter = styled('div')(() => { return { - width: '100vw', - height: '100vh', - overflow: 'hidden', + marginTop: 40, + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + }; +}); + +export const StyledModalTitle = styled('div')(() => { + return { + fontWeight: 600, + fontSize: 'var(--affine-font-h-6)', + }; +}); +export const StyledModalContent = styled('div')(() => { + return { + fontSize: 'var(--affine-font-base)', + lineHeight: '24px', + marginTop: '12px', }; }); diff --git a/packages/component/src/ui/scrollbar/scrollbar.tsx b/packages/component/src/ui/scrollbar/scrollbar.tsx index c100a74359..6d06d2ca25 100644 --- a/packages/component/src/ui/scrollbar/scrollbar.tsx +++ b/packages/component/src/ui/scrollbar/scrollbar.tsx @@ -10,6 +10,7 @@ export type ScrollableContainerProps = { inTableView?: boolean; className?: string; viewPortClassName?: string; + styles?: React.CSSProperties; }; export const ScrollableContainer = ({ @@ -17,11 +18,13 @@ export const ScrollableContainer = ({ showScrollTopBorder = false, inTableView = false, className, + styles: _styles, viewPortClassName, }: PropsWithChildren) => { const [hasScrollTop, ref] = useHasScrollTop(); return (
= type NewSettingProps = UIBaseProps & { - onDeleteWorkspace: (id: string) => Promise; + onDeleteLocalWorkspace: () => void; + onDeleteCloudWorkspace: () => void; + onLeaveWorkspace: () => void; onTransformWorkspace: < From extends keyof WorkspaceRegistry, To extends keyof WorkspaceRegistry, @@ -170,16 +181,15 @@ export interface WorkspaceUISchema { PageList: FC>; NewSettingsDetail: FC>; Provider: FC; + LoginCard?: FC; } export interface AppEvents { // event there is no workspace - // usually used to initialize workspace plugin + // usually used to initialize workspace adapter 'app:init': () => string[]; - // request to gain access to workspace plugin - 'workspace:access': () => Promise; - // request to revoke access to workspace plugin - 'workspace:revoke': () => Promise; + // event if you have access to workspace adapter + 'app:access': () => Promise; } export interface WorkspaceAdapter { diff --git a/packages/graphql/package.json b/packages/graphql/package.json index daca6eb214..1ece62c558 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -22,6 +22,8 @@ "postinstall": "gql-gen" }, "dependencies": { - "graphql": "^16.8.0" + "@affine/env": "workspace:*", + "graphql": "^16.8.0", + "nanoid": "^4.0.2" } } diff --git a/packages/graphql/src/__tests__/fetcher.spec.ts b/packages/graphql/src/__tests__/fetcher.spec.ts index b14f9c37ff..aaaa6ab254 100644 --- a/packages/graphql/src/__tests__/fetcher.spec.ts +++ b/packages/graphql/src/__tests__/fetcher.spec.ts @@ -1,8 +1,15 @@ +import { nanoid } from 'nanoid'; import type { Mock } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { gqlFetcherFactory } from '../fetcher'; import type { GraphQLQuery } from '../graphql'; +import { + generateRandUTF16Chars, + SPAN_ID_BYTES, + TRACE_ID_BYTES, + TraceReporter, +} from '../utils'; const query: GraphQLQuery = { id: 'query', @@ -51,16 +58,18 @@ describe('GraphQL fetcher', () => { variables: { a: 1, b: '2', c: { d: false } }, }); - expect(fetch.mock.lastCall[1]).toMatchInlineSnapshot(` - { - "body": "{\\"query\\":\\"query { field }\\",\\"variables\\":{\\"a\\":1,\\"b\\":\\"2\\",\\"c\\":{\\"d\\":false}},\\"operationName\\":\\"query\\"}", - "headers": { - "x-definition-name": "query", - "x-operation-name": "query", - }, - "method": "POST", - } - `); + expect(fetch.mock.lastCall[1]).toEqual( + expect.objectContaining({ + body: '{"query":"query { field }","variables":{"a":1,"b":"2","c":{"d":false}},"operationName":"query"}', + headers: expect.objectContaining({ + 'content-type': 'application/json', + 'x-definition-name': 'query', + 'x-operation-name': 'query', + 'x-request-id': expect.any(String), + }), + method: 'POST', + }) + ); }); it('should correctly ignore nil variables', async () => { @@ -110,3 +119,41 @@ describe('GraphQL fetcher', () => { `); }); }); + +describe('Trace Reporter', () => { + const startTime = new Date().toISOString(); + const traceId = generateRandUTF16Chars(TRACE_ID_BYTES); + const spanId = generateRandUTF16Chars(SPAN_ID_BYTES); + const requestId = nanoid(); + + it('spanId, traceId should be right format', () => { + expect( + new RegExp(`^[0-9a-f]{${SPAN_ID_BYTES * 2}}$`).test( + generateRandUTF16Chars(SPAN_ID_BYTES) + ) + ).toBe(true); + expect( + new RegExp(`^[0-9a-f]{${TRACE_ID_BYTES * 2}}$`).test( + generateRandUTF16Chars(TRACE_ID_BYTES) + ) + ).toBe(true); + }); + + it('test createTraceSpan', () => { + const traceSpan = TraceReporter.createTraceSpan( + traceId, + spanId, + requestId, + startTime + ); + expect(traceSpan.startTime).toBe(startTime); + expect( + traceSpan.name === + `projects/{GCP_PROJECT_ID}/traces/${traceId}/spans/${spanId}` + ).toBe(true); + expect(traceSpan.spanId).toBe(spanId); + expect(traceSpan.attributes.attributeMap.requestId.stringValue.value).toBe( + requestId + ); + }); +}); diff --git a/packages/graphql/src/fetcher.ts b/packages/graphql/src/fetcher.ts index f8dd366f5b..c79bbace3f 100644 --- a/packages/graphql/src/fetcher.ts +++ b/packages/graphql/src/fetcher.ts @@ -1,9 +1,18 @@ import type { ExecutionResult } from 'graphql'; import { GraphQLError } from 'graphql'; import { isNil, isObject, merge } from 'lodash-es'; +import { nanoid } from 'nanoid'; import type { GraphQLQuery } from './graphql'; import type { Mutations, Queries } from './schema'; +import { + generateRandUTF16Chars, + SPAN_ID_BYTES, + TRACE_FLAG, + TRACE_ID_BYTES, + TRACE_VERSION, + traceReporter, +} from './utils'; export type NotArray = T extends Array ? never : T; @@ -116,19 +125,24 @@ export function transformToForm(body: RequestBody) { if (body.operationName) { gqlBody.name = body.operationName; } - + const map: Record = {}; + const files: File[] = []; if (body.variables) { let i = 0; Object.entries(body.variables).forEach(([key, value]) => { if (value instanceof File) { - gqlBody.map['0'] = [`variables.${key}`]; - form.append(`${i}`, value); + map['0'] = [`variables.${key}`]; + files[i] = value; i++; } }); } - form.append('operations', JSON.stringify(gqlBody)); + form.set('operations', JSON.stringify(gqlBody)); + form.set('map', JSON.stringify(map)); + for (const [i, file] of files.entries()) { + form.set(`${i}`, file); + } return form; } @@ -159,20 +173,25 @@ export const gqlFetcherFactory = (endpoint: string) => { ): Promise> => { const body = formatRequestBody(options); - const ret = fetch( + const isFormData = body instanceof FormData; + const headers: Record = { + 'x-operation-name': options.query.operationName, + 'x-definition-name': options.query.definitionName, + }; + if (!isFormData) { + headers['content-type'] = 'application/json'; + } + const ret = fetchWithReport( endpoint, merge(options.context, { method: 'POST', - headers: { - 'x-operation-name': options.query.operationName, - 'x-definition-name': options.query.definitionName, - }, - body: body instanceof FormData ? body : JSON.stringify(body), + headers, + body: isFormData ? body : JSON.stringify(body), }) ).then(async res => { - if (res.headers.get('content-type') === 'application/json') { + if (res.headers.get('content-type')?.startsWith('application/json')) { const result = (await res.json()) as ExecutionResult; - if (res.status >= 400) { + if (res.status >= 400 || result.errors) { if (result.errors && result.errors.length > 0) { throw result.errors.map( error => new GraphQLError(error.message, error) @@ -194,3 +213,40 @@ export const gqlFetcherFactory = (endpoint: string) => { return gqlFetch; }; + +export const fetchWithReport = ( + input: RequestInfo | URL, + init?: RequestInit +): Promise => { + const startTime = new Date().toISOString(); + const spanId = generateRandUTF16Chars(SPAN_ID_BYTES); + const traceId = generateRandUTF16Chars(TRACE_ID_BYTES); + const traceparent = `${TRACE_VERSION}-${traceId}-${spanId}-${TRACE_FLAG}`; + init = init || {}; + init.headers = init.headers || new Headers(); + const requestId = nanoid(); + if (init.headers instanceof Headers) { + init.headers.append('x-request-id', requestId); + init.headers.append('traceparent', traceparent); + } else { + const headers = init.headers as Record; + headers['x-request-id'] = requestId; + headers['traceparent'] = traceparent; + } + + if (!traceReporter) { + return fetch(input, init); + } + + return fetch(input, init) + .then(response => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + traceReporter!.cacheTrace(traceId, spanId, requestId, startTime); + return response; + }) + .catch(err => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + traceReporter!.uploadTrace(traceId, spanId, requestId, startTime); + return Promise.reject(err); + }); +}; diff --git a/packages/graphql/src/graphql/blob-delete.gql b/packages/graphql/src/graphql/blob-delete.gql new file mode 100644 index 0000000000..790c326465 --- /dev/null +++ b/packages/graphql/src/graphql/blob-delete.gql @@ -0,0 +1,3 @@ +mutation deleteBlob($workspaceId: String!, $hash: String!) { + deleteBlob(workspaceId: $workspaceId, hash: $hash) +} diff --git a/packages/graphql/src/graphql/blob-list.gql b/packages/graphql/src/graphql/blob-list.gql new file mode 100644 index 0000000000..67a19c718c --- /dev/null +++ b/packages/graphql/src/graphql/blob-list.gql @@ -0,0 +1,3 @@ +query listBlobs($workspaceId: String!) { + listBlobs(workspaceId: $workspaceId) +} diff --git a/packages/graphql/src/graphql/blob-set.gql b/packages/graphql/src/graphql/blob-set.gql new file mode 100644 index 0000000000..3751ecd481 --- /dev/null +++ b/packages/graphql/src/graphql/blob-set.gql @@ -0,0 +1,3 @@ +mutation setBlob($workspaceId: String!, $blob: Upload!) { + setBlob(workspaceId: $workspaceId, blob: $blob) +} diff --git a/packages/graphql/src/graphql/change-email.gql b/packages/graphql/src/graphql/change-email.gql new file mode 100644 index 0000000000..78daad9fd3 --- /dev/null +++ b/packages/graphql/src/graphql/change-email.gql @@ -0,0 +1,8 @@ +mutation changeEmail($id: String!, $newEmail: String!) { + changeEmail(id: $id, email: $newEmail) { + id + name + avatarUrl + email + } +} diff --git a/packages/graphql/src/graphql/change-password.gql b/packages/graphql/src/graphql/change-password.gql new file mode 100644 index 0000000000..bc69c92437 --- /dev/null +++ b/packages/graphql/src/graphql/change-password.gql @@ -0,0 +1,8 @@ +mutation changePassword($id: String!, $newPassword: String!) { + changePassword(id: $id, newPassword: $newPassword) { + id + name + avatarUrl + email + } +} diff --git a/packages/graphql/src/graphql/delete-account.gql b/packages/graphql/src/graphql/delete-account.gql new file mode 100644 index 0000000000..dbb831ca1d --- /dev/null +++ b/packages/graphql/src/graphql/delete-account.gql @@ -0,0 +1,5 @@ +mutation deleteAccount { + deleteAccount { + success + } +} diff --git a/packages/graphql/src/graphql/delete-workspace.gql b/packages/graphql/src/graphql/delete-workspace.gql new file mode 100644 index 0000000000..55edbe2582 --- /dev/null +++ b/packages/graphql/src/graphql/delete-workspace.gql @@ -0,0 +1,3 @@ +mutation deleteWorkspace($id: String!) { + deleteWorkspace(id: $id) +} diff --git a/packages/graphql/src/graphql/get-current-user.gql b/packages/graphql/src/graphql/get-current-user.gql new file mode 100644 index 0000000000..e7d6b7e04a --- /dev/null +++ b/packages/graphql/src/graphql/get-current-user.gql @@ -0,0 +1,10 @@ +query getCurrentUser { + currentUser { + id + name + email + emailVerified + avatarUrl + createdAt + } +} diff --git a/packages/graphql/src/graphql/get-invite-info.gql b/packages/graphql/src/graphql/get-invite-info.gql new file mode 100644 index 0000000000..7cae4450c8 --- /dev/null +++ b/packages/graphql/src/graphql/get-invite-info.gql @@ -0,0 +1,14 @@ +query getInviteInfo($inviteId: String!) { + getInviteInfo(inviteId: $inviteId) { + workspace { + id + name + avatar + } + user { + id + name + avatarUrl + } + } +} diff --git a/packages/graphql/src/graphql/get-is-owner.gql b/packages/graphql/src/graphql/get-is-owner.gql new file mode 100644 index 0000000000..f9de28c683 --- /dev/null +++ b/packages/graphql/src/graphql/get-is-owner.gql @@ -0,0 +1,3 @@ +query getIsOwner($workspaceId: String!) { + isOwner(workspaceId: $workspaceId) +} diff --git a/packages/graphql/src/graphql/get-members-by-workspace-id.gql b/packages/graphql/src/graphql/get-members-by-workspace-id.gql new file mode 100644 index 0000000000..216df27e5e --- /dev/null +++ b/packages/graphql/src/graphql/get-members-by-workspace-id.gql @@ -0,0 +1,14 @@ +query getMembersByWorkspaceId($workspaceId: String!) { + workspace(id: $workspaceId) { + members { + id + name + email + avatarUrl + permission + inviteId + accepted + emailVerified + } + } +} diff --git a/packages/graphql/src/graphql/get-public-workspace.gql b/packages/graphql/src/graphql/get-public-workspace.gql new file mode 100644 index 0000000000..6da347222d --- /dev/null +++ b/packages/graphql/src/graphql/get-public-workspace.gql @@ -0,0 +1,5 @@ +query getPublicWorkspace($id: String!) { + publicWorkspace(id: $id) { + id + } +} diff --git a/packages/graphql/src/graphql/get-user.gql b/packages/graphql/src/graphql/get-user.gql new file mode 100644 index 0000000000..8b9f82e343 --- /dev/null +++ b/packages/graphql/src/graphql/get-user.gql @@ -0,0 +1,9 @@ +query getUser($email: String!) { + user(email: $email) { + id + name + avatarUrl + email + hasPassword + } +} diff --git a/packages/graphql/src/graphql/get-workspace-public-by-id.gql b/packages/graphql/src/graphql/get-workspace-public-by-id.gql new file mode 100644 index 0000000000..fae875856d --- /dev/null +++ b/packages/graphql/src/graphql/get-workspace-public-by-id.gql @@ -0,0 +1,5 @@ +query getWorkspacePublicById($id: String!) { + workspace(id: $id) { + public + } +} diff --git a/packages/graphql/src/graphql/get-workspace-shared-pages.gql b/packages/graphql/src/graphql/get-workspace-shared-pages.gql new file mode 100644 index 0000000000..3d94e0a739 --- /dev/null +++ b/packages/graphql/src/graphql/get-workspace-shared-pages.gql @@ -0,0 +1,5 @@ +query getWorkspaceSharedPages($workspaceId: String!) { + workspace(id: $workspaceId) { + sharedPages + } +} diff --git a/packages/graphql/src/graphql/get-workspace.gql b/packages/graphql/src/graphql/get-workspace.gql new file mode 100644 index 0000000000..8855656a34 --- /dev/null +++ b/packages/graphql/src/graphql/get-workspace.gql @@ -0,0 +1,5 @@ +query getWorkspace($id: String!) { + workspace(id: $id) { + id + } +} diff --git a/packages/graphql/src/graphql/get-workspaces.gql b/packages/graphql/src/graphql/get-workspaces.gql new file mode 100644 index 0000000000..af151ac6c2 --- /dev/null +++ b/packages/graphql/src/graphql/get-workspaces.gql @@ -0,0 +1,5 @@ +query getWorkspaces { + workspaces { + id + } +} diff --git a/packages/graphql/src/graphql/index.ts b/packages/graphql/src/graphql/index.ts index 34fb697581..5bf4d85b92 100644 --- a/packages/graphql/src/graphql/index.ts +++ b/packages/graphql/src/graphql/index.ts @@ -7,6 +7,71 @@ export interface GraphQLQuery { containsFile?: boolean; } +export const deleteBlobMutation = { + id: 'deleteBlobMutation' as const, + operationName: 'deleteBlob', + definitionName: 'deleteBlob', + containsFile: false, + query: ` +mutation deleteBlob($workspaceId: String!, $hash: String!) { + deleteBlob(workspaceId: $workspaceId, hash: $hash) +}`, +}; + +export const listBlobsQuery = { + id: 'listBlobsQuery' as const, + operationName: 'listBlobs', + definitionName: 'listBlobs', + containsFile: false, + query: ` +query listBlobs($workspaceId: String!) { + listBlobs(workspaceId: $workspaceId) +}`, +}; + +export const setBlobMutation = { + id: 'setBlobMutation' as const, + operationName: 'setBlob', + definitionName: 'setBlob', + containsFile: true, + query: ` +mutation setBlob($workspaceId: String!, $blob: Upload!) { + setBlob(workspaceId: $workspaceId, blob: $blob) +}`, +}; + +export const changeEmailMutation = { + id: 'changeEmailMutation' as const, + operationName: 'changeEmail', + definitionName: 'changeEmail', + containsFile: false, + query: ` +mutation changeEmail($id: String!, $newEmail: String!) { + changeEmail(id: $id, email: $newEmail) { + id + name + avatarUrl + email + } +}`, +}; + +export const changePasswordMutation = { + id: 'changePasswordMutation' as const, + operationName: 'changePassword', + definitionName: 'changePassword', + containsFile: false, + query: ` +mutation changePassword($id: String!, $newPassword: String!) { + changePassword(id: $id, newPassword: $newPassword) { + id + name + avatarUrl + email + } +}`, +}; + export const createWorkspaceMutation = { id: 'createWorkspaceMutation' as const, operationName: 'createWorkspace', @@ -22,6 +87,327 @@ mutation createWorkspace($init: Upload!) { }`, }; +export const deleteAccountMutation = { + id: 'deleteAccountMutation' as const, + operationName: 'deleteAccount', + definitionName: 'deleteAccount', + containsFile: false, + query: ` +mutation deleteAccount { + deleteAccount { + success + } +}`, +}; + +export const deleteWorkspaceMutation = { + id: 'deleteWorkspaceMutation' as const, + operationName: 'deleteWorkspace', + definitionName: 'deleteWorkspace', + containsFile: false, + query: ` +mutation deleteWorkspace($id: String!) { + deleteWorkspace(id: $id) +}`, +}; + +export const getCurrentUserQuery = { + id: 'getCurrentUserQuery' as const, + operationName: 'getCurrentUser', + definitionName: 'currentUser', + containsFile: false, + query: ` +query getCurrentUser { + currentUser { + id + name + email + emailVerified + avatarUrl + createdAt + } +}`, +}; + +export const getInviteInfoQuery = { + id: 'getInviteInfoQuery' as const, + operationName: 'getInviteInfo', + definitionName: 'getInviteInfo', + containsFile: false, + query: ` +query getInviteInfo($inviteId: String!) { + getInviteInfo(inviteId: $inviteId) { + workspace { + id + name + avatar + } + user { + id + name + avatarUrl + } + } +}`, +}; + +export const getIsOwnerQuery = { + id: 'getIsOwnerQuery' as const, + operationName: 'getIsOwner', + definitionName: 'isOwner', + containsFile: false, + query: ` +query getIsOwner($workspaceId: String!) { + isOwner(workspaceId: $workspaceId) +}`, +}; + +export const getMembersByWorkspaceIdQuery = { + id: 'getMembersByWorkspaceIdQuery' as const, + operationName: 'getMembersByWorkspaceId', + definitionName: 'workspace', + containsFile: false, + query: ` +query getMembersByWorkspaceId($workspaceId: String!) { + workspace(id: $workspaceId) { + members { + id + name + email + avatarUrl + permission + inviteId + accepted + emailVerified + } + } +}`, +}; + +export const getPublicWorkspaceQuery = { + id: 'getPublicWorkspaceQuery' as const, + operationName: 'getPublicWorkspace', + definitionName: 'publicWorkspace', + containsFile: false, + query: ` +query getPublicWorkspace($id: String!) { + publicWorkspace(id: $id) { + id + } +}`, +}; + +export const getUserQuery = { + id: 'getUserQuery' as const, + operationName: 'getUser', + definitionName: 'user', + containsFile: false, + query: ` +query getUser($email: String!) { + user(email: $email) { + id + name + avatarUrl + email + hasPassword + } +}`, +}; + +export const getWorkspacePublicByIdQuery = { + id: 'getWorkspacePublicByIdQuery' as const, + operationName: 'getWorkspacePublicById', + definitionName: 'workspace', + containsFile: false, + query: ` +query getWorkspacePublicById($id: String!) { + workspace(id: $id) { + public + } +}`, +}; + +export const getWorkspaceSharedPagesQuery = { + id: 'getWorkspaceSharedPagesQuery' as const, + operationName: 'getWorkspaceSharedPages', + definitionName: 'workspace', + containsFile: false, + query: ` +query getWorkspaceSharedPages($workspaceId: String!) { + workspace(id: $workspaceId) { + sharedPages + } +}`, +}; + +export const getWorkspaceQuery = { + id: 'getWorkspaceQuery' as const, + operationName: 'getWorkspace', + definitionName: 'workspace', + containsFile: false, + query: ` +query getWorkspace($id: String!) { + workspace(id: $id) { + id + } +}`, +}; + +export const getWorkspacesQuery = { + id: 'getWorkspacesQuery' as const, + operationName: 'getWorkspaces', + definitionName: 'workspaces', + containsFile: false, + query: ` +query getWorkspaces { + workspaces { + id + } +}`, +}; + +export const leaveWorkspaceMutation = { + id: 'leaveWorkspaceMutation' as const, + operationName: 'leaveWorkspace', + definitionName: 'leaveWorkspace', + containsFile: false, + query: ` +mutation leaveWorkspace($workspaceId: String!) { + leaveWorkspace(workspaceId: $workspaceId) +}`, +}; + +export const revokeMemberPermissionMutation = { + id: 'revokeMemberPermissionMutation' as const, + operationName: 'revokeMemberPermission', + definitionName: 'revoke', + containsFile: false, + query: ` +mutation revokeMemberPermission($workspaceId: String!, $userId: String!) { + revoke(workspaceId: $workspaceId, userId: $userId) +}`, +}; + +export const revokePageMutation = { + id: 'revokePageMutation' as const, + operationName: 'revokePage', + definitionName: 'revokePage', + containsFile: false, + query: ` +mutation revokePage($workspaceId: String!, $pageId: String!) { + revokePage(workspaceId: $workspaceId, pageId: $pageId) +}`, +}; + +export const sendChangeEmailMutation = { + id: 'sendChangeEmailMutation' as const, + operationName: 'sendChangeEmail', + definitionName: 'sendChangeEmail', + containsFile: false, + query: ` +mutation sendChangeEmail($email: String!, $callbackUrl: String!) { + sendChangeEmail(email: $email, callbackUrl: $callbackUrl) +}`, +}; + +export const sendChangePasswordEmailMutation = { + id: 'sendChangePasswordEmailMutation' as const, + operationName: 'sendChangePasswordEmail', + definitionName: 'sendChangePasswordEmail', + containsFile: false, + query: ` +mutation sendChangePasswordEmail($email: String!, $callbackUrl: String!) { + sendChangePasswordEmail(email: $email, callbackUrl: $callbackUrl) +}`, +}; + +export const sendSetPasswordEmailMutation = { + id: 'sendSetPasswordEmailMutation' as const, + operationName: 'sendSetPasswordEmail', + definitionName: 'sendSetPasswordEmail', + containsFile: false, + query: ` +mutation sendSetPasswordEmail($email: String!, $callbackUrl: String!) { + sendSetPasswordEmail(email: $email, callbackUrl: $callbackUrl) +}`, +}; + +export const setRevokePageMutation = { + id: 'setRevokePageMutation' as const, + operationName: 'setRevokePage', + definitionName: 'revokePage', + containsFile: false, + query: ` +mutation setRevokePage($workspaceId: String!, $pageId: String!) { + revokePage(workspaceId: $workspaceId, pageId: $pageId) +}`, +}; + +export const setSharePageMutation = { + id: 'setSharePageMutation' as const, + operationName: 'setSharePage', + definitionName: 'sharePage', + containsFile: false, + query: ` +mutation setSharePage($workspaceId: String!, $pageId: String!) { + sharePage(workspaceId: $workspaceId, pageId: $pageId) +}`, +}; + +export const setWorkspacePublicByIdMutation = { + id: 'setWorkspacePublicByIdMutation' as const, + operationName: 'setWorkspacePublicById', + definitionName: 'updateWorkspace', + containsFile: false, + query: ` +mutation setWorkspacePublicById($id: ID!, $public: Boolean!) { + updateWorkspace(input: {id: $id, public: $public}) { + id + } +}`, +}; + +export const sharePageMutation = { + id: 'sharePageMutation' as const, + operationName: 'sharePage', + definitionName: 'sharePage', + containsFile: false, + query: ` +mutation sharePage($workspaceId: String!, $pageId: String!) { + sharePage(workspaceId: $workspaceId, pageId: $pageId) +}`, +}; + +export const signInMutation = { + id: 'signInMutation' as const, + operationName: 'signIn', + definitionName: 'signIn', + containsFile: false, + query: ` +mutation signIn($email: String!, $password: String!) { + signIn(email: $email, password: $password) { + token { + token + } + } +}`, +}; + +export const signUpMutation = { + id: 'signUpMutation' as const, + operationName: 'signUp', + definitionName: 'signUp', + containsFile: false, + query: ` +mutation signUp($name: String!, $email: String!, $password: String!) { + signUp(name: $name, email: $email, password: $password) { + token { + token + } + } +}`, +}; + export const uploadAvatarMutation = { id: 'uploadAvatarMutation' as const, operationName: 'uploadAvatar', @@ -38,17 +424,40 @@ mutation uploadAvatar($id: String!, $avatar: Upload!) { }`, }; -export const workspaceByIdQuery = { - id: 'workspaceByIdQuery' as const, - operationName: 'workspaceById', - definitionName: 'workspace', +export const inviteByEmailMutation = { + id: 'inviteByEmailMutation' as const, + operationName: 'inviteByEmail', + definitionName: 'invite', containsFile: false, query: ` -query workspaceById($id: String!) { - workspace(id: $id) { - id - public - createdAt - } +mutation inviteByEmail($workspaceId: String!, $email: String!, $permission: Permission!, $sendInviteMail: Boolean) { + invite( + workspaceId: $workspaceId + email: $email + permission: $permission + sendInviteMail: $sendInviteMail + ) +}`, +}; + +export const acceptInviteByInviteIdMutation = { + id: 'acceptInviteByInviteIdMutation' as const, + operationName: 'acceptInviteByInviteId', + definitionName: 'acceptInviteById', + containsFile: false, + query: ` +mutation acceptInviteByInviteId($workspaceId: String!, $inviteId: String!) { + acceptInviteById(workspaceId: $workspaceId, inviteId: $inviteId) +}`, +}; + +export const acceptInviteByWorkspaceIdMutation = { + id: 'acceptInviteByWorkspaceIdMutation' as const, + operationName: 'acceptInviteByWorkspaceId', + definitionName: 'acceptInvite', + containsFile: false, + query: ` +mutation acceptInviteByWorkspaceId($workspaceId: String!) { + acceptInvite(workspaceId: $workspaceId) }`, }; diff --git a/packages/graphql/src/graphql/leave-workspace.gql b/packages/graphql/src/graphql/leave-workspace.gql new file mode 100644 index 0000000000..11c095a0d9 --- /dev/null +++ b/packages/graphql/src/graphql/leave-workspace.gql @@ -0,0 +1,3 @@ +mutation leaveWorkspace($workspaceId: String!) { + leaveWorkspace(workspaceId: $workspaceId) +} diff --git a/packages/graphql/src/graphql/revoke-member-permission.gql b/packages/graphql/src/graphql/revoke-member-permission.gql new file mode 100644 index 0000000000..6eace191cc --- /dev/null +++ b/packages/graphql/src/graphql/revoke-member-permission.gql @@ -0,0 +1,3 @@ +mutation revokeMemberPermission($workspaceId: String!, $userId: String!) { + revoke(workspaceId: $workspaceId, userId: $userId) +} diff --git a/packages/graphql/src/graphql/revoke-page.gql b/packages/graphql/src/graphql/revoke-page.gql new file mode 100644 index 0000000000..df8de2aba0 --- /dev/null +++ b/packages/graphql/src/graphql/revoke-page.gql @@ -0,0 +1,3 @@ +mutation revokePage($workspaceId: String!, $pageId: String!) { + revokePage(workspaceId: $workspaceId, pageId: $pageId) +} diff --git a/packages/graphql/src/graphql/send-change-email.gql b/packages/graphql/src/graphql/send-change-email.gql new file mode 100644 index 0000000000..b9421d15b5 --- /dev/null +++ b/packages/graphql/src/graphql/send-change-email.gql @@ -0,0 +1,3 @@ +mutation sendChangeEmail($email: String!, $callbackUrl: String!) { + sendChangeEmail(email: $email, callbackUrl: $callbackUrl) +} diff --git a/packages/graphql/src/graphql/send-change-password-email.gql b/packages/graphql/src/graphql/send-change-password-email.gql new file mode 100644 index 0000000000..ed99bab15b --- /dev/null +++ b/packages/graphql/src/graphql/send-change-password-email.gql @@ -0,0 +1,3 @@ +mutation sendChangePasswordEmail($email: String!, $callbackUrl: String!) { + sendChangePasswordEmail(email: $email, callbackUrl: $callbackUrl) +} diff --git a/packages/graphql/src/graphql/send-set-password-email.gql b/packages/graphql/src/graphql/send-set-password-email.gql new file mode 100644 index 0000000000..8caaebd989 --- /dev/null +++ b/packages/graphql/src/graphql/send-set-password-email.gql @@ -0,0 +1,3 @@ +mutation sendSetPasswordEmail($email: String!, $callbackUrl: String!) { + sendSetPasswordEmail(email: $email, callbackUrl: $callbackUrl) +} diff --git a/packages/graphql/src/graphql/set-revoke-page.gql b/packages/graphql/src/graphql/set-revoke-page.gql new file mode 100644 index 0000000000..a8185958b5 --- /dev/null +++ b/packages/graphql/src/graphql/set-revoke-page.gql @@ -0,0 +1,3 @@ +mutation setRevokePage($workspaceId: String!, $pageId: String!) { + revokePage(workspaceId: $workspaceId, pageId: $pageId) +} diff --git a/packages/graphql/src/graphql/set-share-page.gql b/packages/graphql/src/graphql/set-share-page.gql new file mode 100644 index 0000000000..8d6fa6d3b7 --- /dev/null +++ b/packages/graphql/src/graphql/set-share-page.gql @@ -0,0 +1,3 @@ +mutation setSharePage($workspaceId: String!, $pageId: String!) { + sharePage(workspaceId: $workspaceId, pageId: $pageId) +} diff --git a/packages/graphql/src/graphql/set-workspace-public-by-id.gql b/packages/graphql/src/graphql/set-workspace-public-by-id.gql new file mode 100644 index 0000000000..e50385f837 --- /dev/null +++ b/packages/graphql/src/graphql/set-workspace-public-by-id.gql @@ -0,0 +1,5 @@ +mutation setWorkspacePublicById($id: ID!, $public: Boolean!) { + updateWorkspace(input: { id: $id, public: $public }) { + id + } +} diff --git a/packages/graphql/src/graphql/share-page.gql b/packages/graphql/src/graphql/share-page.gql new file mode 100644 index 0000000000..26a4259412 --- /dev/null +++ b/packages/graphql/src/graphql/share-page.gql @@ -0,0 +1,3 @@ +mutation sharePage($workspaceId: String!, $pageId: String!) { + sharePage(workspaceId: $workspaceId, pageId: $pageId) +} diff --git a/packages/graphql/src/graphql/sign-in.gql b/packages/graphql/src/graphql/sign-in.gql new file mode 100644 index 0000000000..b43e7dcbee --- /dev/null +++ b/packages/graphql/src/graphql/sign-in.gql @@ -0,0 +1,7 @@ +mutation signIn($email: String!, $password: String!) { + signIn(email: $email, password: $password) { + token { + token + } + } +} diff --git a/packages/graphql/src/graphql/sign-up.gql b/packages/graphql/src/graphql/sign-up.gql new file mode 100644 index 0000000000..a93df61981 --- /dev/null +++ b/packages/graphql/src/graphql/sign-up.gql @@ -0,0 +1,7 @@ +mutation signUp($name: String!, $email: String!, $password: String!) { + signUp(name: $name, email: $email, password: $password) { + token { + token + } + } +} diff --git a/packages/graphql/src/graphql/workspace-intive-by-email.gql b/packages/graphql/src/graphql/workspace-intive-by-email.gql new file mode 100644 index 0000000000..fbe94cb234 --- /dev/null +++ b/packages/graphql/src/graphql/workspace-intive-by-email.gql @@ -0,0 +1,13 @@ +mutation inviteByEmail( + $workspaceId: String! + $email: String! + $permission: Permission! + $sendInviteMail: Boolean +) { + invite( + workspaceId: $workspaceId + email: $email + permission: $permission + sendInviteMail: $sendInviteMail + ) +} diff --git a/packages/graphql/src/graphql/workspace-invite-accept-by-invite-id.gql b/packages/graphql/src/graphql/workspace-invite-accept-by-invite-id.gql new file mode 100644 index 0000000000..1138ddf327 --- /dev/null +++ b/packages/graphql/src/graphql/workspace-invite-accept-by-invite-id.gql @@ -0,0 +1,3 @@ +mutation acceptInviteByInviteId($workspaceId: String!, $inviteId: String!) { + acceptInviteById(workspaceId: $workspaceId, inviteId: $inviteId) +} diff --git a/packages/graphql/src/graphql/workspace-invite-accept-by-workspace-id.gql b/packages/graphql/src/graphql/workspace-invite-accept-by-workspace-id.gql new file mode 100644 index 0000000000..fe715ed663 --- /dev/null +++ b/packages/graphql/src/graphql/workspace-invite-accept-by-workspace-id.gql @@ -0,0 +1,3 @@ +mutation acceptInviteByWorkspaceId($workspaceId: String!) { + acceptInvite(workspaceId: $workspaceId) +} diff --git a/packages/graphql/src/graphql/workspace.gql b/packages/graphql/src/graphql/workspace.gql deleted file mode 100644 index e5bf0f7e8b..0000000000 --- a/packages/graphql/src/graphql/workspace.gql +++ /dev/null @@ -1,7 +0,0 @@ -query workspaceById($id: String!) { - workspace(id: $id) { - id - public - createdAt - } -} diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index 2231ca4669..390f85e077 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -1,3 +1,4 @@ export * from './fetcher'; export * from './graphql'; export * from './schema'; +import '@affine/env/global'; diff --git a/packages/graphql/src/schema.ts b/packages/graphql/src/schema.ts index 516da727c8..9b9842a4fe 100644 --- a/packages/graphql/src/schema.ts +++ b/packages/graphql/src/schema.ts @@ -32,6 +32,10 @@ export interface Scalars { Upload: { input: File; output: File }; } +export enum NewFeaturesKind { + EarlyAccess = 'EarlyAccess', +} + /** User permission in workspace */ export enum Permission { Admin = 'Admin', @@ -46,6 +50,61 @@ export interface UpdateWorkspaceInput { public: InputMaybe; } +export type DeleteBlobMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; + hash: Scalars['String']['input']; +}>; + +export type DeleteBlobMutation = { + __typename?: 'Mutation'; + deleteBlob: boolean; +}; + +export type ListBlobsQueryVariables = Exact<{ + workspaceId: Scalars['String']['input']; +}>; + +export type ListBlobsQuery = { __typename?: 'Query'; listBlobs: Array }; + +export type SetBlobMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; + blob: Scalars['Upload']['input']; +}>; + +export type SetBlobMutation = { __typename?: 'Mutation'; setBlob: string }; + +export type ChangeEmailMutationVariables = Exact<{ + id: Scalars['String']['input']; + newEmail: Scalars['String']['input']; +}>; + +export type ChangeEmailMutation = { + __typename?: 'Mutation'; + changeEmail: { + __typename?: 'UserType'; + id: string; + name: string; + avatarUrl: string | null; + email: string; + }; +}; + +export type ChangePasswordMutationVariables = Exact<{ + id: Scalars['String']['input']; + newPassword: Scalars['String']['input']; +}>; + +export type ChangePasswordMutation = { + __typename?: 'Mutation'; + changePassword: { + __typename?: 'UserType'; + id: string; + name: string; + avatarUrl: string | null; + email: string; + }; +}; + export type CreateWorkspaceMutationVariables = Exact<{ init: Scalars['Upload']['input']; }>; @@ -60,6 +119,270 @@ export type CreateWorkspaceMutation = { }; }; +export type DeleteAccountMutationVariables = Exact<{ [key: string]: never }>; + +export type DeleteAccountMutation = { + __typename?: 'Mutation'; + deleteAccount: { __typename?: 'DeleteAccount'; success: boolean }; +}; + +export type DeleteWorkspaceMutationVariables = Exact<{ + id: Scalars['String']['input']; +}>; + +export type DeleteWorkspaceMutation = { + __typename?: 'Mutation'; + deleteWorkspace: boolean; +}; + +export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never }>; + +export type GetCurrentUserQuery = { + __typename?: 'Query'; + currentUser: { + __typename?: 'UserType'; + id: string; + name: string; + email: string; + emailVerified: string | null; + avatarUrl: string | null; + createdAt: string | null; + }; +}; + +export type GetInviteInfoQueryVariables = Exact<{ + inviteId: Scalars['String']['input']; +}>; + +export type GetInviteInfoQuery = { + __typename?: 'Query'; + getInviteInfo: { + __typename?: 'InvitationType'; + workspace: { + __typename?: 'InvitationWorkspaceType'; + id: string; + name: string; + avatar: string; + }; + user: { + __typename?: 'UserType'; + id: string; + name: string; + avatarUrl: string | null; + }; + }; +}; + +export type GetIsOwnerQueryVariables = Exact<{ + workspaceId: Scalars['String']['input']; +}>; + +export type GetIsOwnerQuery = { __typename?: 'Query'; isOwner: boolean }; + +export type GetMembersByWorkspaceIdQueryVariables = Exact<{ + workspaceId: Scalars['String']['input']; +}>; + +export type GetMembersByWorkspaceIdQuery = { + __typename?: 'Query'; + workspace: { + __typename?: 'WorkspaceType'; + members: Array<{ + __typename?: 'InviteUserType'; + id: string; + name: string | null; + email: string | null; + avatarUrl: string | null; + permission: Permission; + inviteId: string; + accepted: boolean; + emailVerified: string | null; + }>; + }; +}; + +export type GetPublicWorkspaceQueryVariables = Exact<{ + id: Scalars['String']['input']; +}>; + +export type GetPublicWorkspaceQuery = { + __typename?: 'Query'; + publicWorkspace: { __typename?: 'WorkspaceType'; id: string }; +}; + +export type GetUserQueryVariables = Exact<{ + email: Scalars['String']['input']; +}>; + +export type GetUserQuery = { + __typename?: 'Query'; + user: { + __typename?: 'UserType'; + id: string; + name: string; + avatarUrl: string | null; + email: string; + hasPassword: boolean | null; + } | null; +}; + +export type GetWorkspacePublicByIdQueryVariables = Exact<{ + id: Scalars['String']['input']; +}>; + +export type GetWorkspacePublicByIdQuery = { + __typename?: 'Query'; + workspace: { __typename?: 'WorkspaceType'; public: boolean }; +}; + +export type GetWorkspaceSharedPagesQueryVariables = Exact<{ + workspaceId: Scalars['String']['input']; +}>; + +export type GetWorkspaceSharedPagesQuery = { + __typename?: 'Query'; + workspace: { __typename?: 'WorkspaceType'; sharedPages: Array }; +}; + +export type GetWorkspaceQueryVariables = Exact<{ + id: Scalars['String']['input']; +}>; + +export type GetWorkspaceQuery = { + __typename?: 'Query'; + workspace: { __typename?: 'WorkspaceType'; id: string }; +}; + +export type GetWorkspacesQueryVariables = Exact<{ [key: string]: never }>; + +export type GetWorkspacesQuery = { + __typename?: 'Query'; + workspaces: Array<{ __typename?: 'WorkspaceType'; id: string }>; +}; + +export type LeaveWorkspaceMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; +}>; + +export type LeaveWorkspaceMutation = { + __typename?: 'Mutation'; + leaveWorkspace: boolean; +}; + +export type RevokeMemberPermissionMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; + userId: Scalars['String']['input']; +}>; + +export type RevokeMemberPermissionMutation = { + __typename?: 'Mutation'; + revoke: boolean; +}; + +export type RevokePageMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; + pageId: Scalars['String']['input']; +}>; + +export type RevokePageMutation = { + __typename?: 'Mutation'; + revokePage: boolean; +}; + +export type SendChangeEmailMutationVariables = Exact<{ + email: Scalars['String']['input']; + callbackUrl: Scalars['String']['input']; +}>; + +export type SendChangeEmailMutation = { + __typename?: 'Mutation'; + sendChangeEmail: boolean; +}; + +export type SendChangePasswordEmailMutationVariables = Exact<{ + email: Scalars['String']['input']; + callbackUrl: Scalars['String']['input']; +}>; + +export type SendChangePasswordEmailMutation = { + __typename?: 'Mutation'; + sendChangePasswordEmail: boolean; +}; + +export type SendSetPasswordEmailMutationVariables = Exact<{ + email: Scalars['String']['input']; + callbackUrl: Scalars['String']['input']; +}>; + +export type SendSetPasswordEmailMutation = { + __typename?: 'Mutation'; + sendSetPasswordEmail: boolean; +}; + +export type SetRevokePageMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; + pageId: Scalars['String']['input']; +}>; + +export type SetRevokePageMutation = { + __typename?: 'Mutation'; + revokePage: boolean; +}; + +export type SetSharePageMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; + pageId: Scalars['String']['input']; +}>; + +export type SetSharePageMutation = { + __typename?: 'Mutation'; + sharePage: boolean; +}; + +export type SetWorkspacePublicByIdMutationVariables = Exact<{ + id: Scalars['ID']['input']; + public: Scalars['Boolean']['input']; +}>; + +export type SetWorkspacePublicByIdMutation = { + __typename?: 'Mutation'; + updateWorkspace: { __typename?: 'WorkspaceType'; id: string }; +}; + +export type SharePageMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; + pageId: Scalars['String']['input']; +}>; + +export type SharePageMutation = { __typename?: 'Mutation'; sharePage: boolean }; + +export type SignInMutationVariables = Exact<{ + email: Scalars['String']['input']; + password: Scalars['String']['input']; +}>; + +export type SignInMutation = { + __typename?: 'Mutation'; + signIn: { + __typename?: 'UserType'; + token: { __typename?: 'TokenType'; token: string }; + }; +}; + +export type SignUpMutationVariables = Exact<{ + name: Scalars['String']['input']; + email: Scalars['String']['input']; + password: Scalars['String']['input']; +}>; + +export type SignUpMutation = { + __typename?: 'Mutation'; + signUp: { + __typename?: 'UserType'; + token: { __typename?: 'TokenType'; token: string }; + }; +}; + export type UploadAvatarMutationVariables = Exact<{ id: Scalars['String']['input']; avatar: Scalars['Upload']['input']; @@ -76,34 +399,204 @@ export type UploadAvatarMutation = { }; }; -export type WorkspaceByIdQueryVariables = Exact<{ - id: Scalars['String']['input']; +export type InviteByEmailMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; + email: Scalars['String']['input']; + permission: Permission; + sendInviteMail: InputMaybe; }>; -export type WorkspaceByIdQuery = { - __typename?: 'Query'; - workspace: { - __typename?: 'WorkspaceType'; - id: string; - public: boolean; - createdAt: string; - }; +export type InviteByEmailMutation = { __typename?: 'Mutation'; invite: string }; + +export type AcceptInviteByInviteIdMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; + inviteId: Scalars['String']['input']; +}>; + +export type AcceptInviteByInviteIdMutation = { + __typename?: 'Mutation'; + acceptInviteById: boolean; }; -export type Queries = { - name: 'workspaceByIdQuery'; - variables: WorkspaceByIdQueryVariables; - response: WorkspaceByIdQuery; +export type AcceptInviteByWorkspaceIdMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; +}>; + +export type AcceptInviteByWorkspaceIdMutation = { + __typename?: 'Mutation'; + acceptInvite: boolean; }; +export type Queries = + | { + name: 'listBlobsQuery'; + variables: ListBlobsQueryVariables; + response: ListBlobsQuery; + } + | { + name: 'getCurrentUserQuery'; + variables: GetCurrentUserQueryVariables; + response: GetCurrentUserQuery; + } + | { + name: 'getInviteInfoQuery'; + variables: GetInviteInfoQueryVariables; + response: GetInviteInfoQuery; + } + | { + name: 'getIsOwnerQuery'; + variables: GetIsOwnerQueryVariables; + response: GetIsOwnerQuery; + } + | { + name: 'getMembersByWorkspaceIdQuery'; + variables: GetMembersByWorkspaceIdQueryVariables; + response: GetMembersByWorkspaceIdQuery; + } + | { + name: 'getPublicWorkspaceQuery'; + variables: GetPublicWorkspaceQueryVariables; + response: GetPublicWorkspaceQuery; + } + | { + name: 'getUserQuery'; + variables: GetUserQueryVariables; + response: GetUserQuery; + } + | { + name: 'getWorkspacePublicByIdQuery'; + variables: GetWorkspacePublicByIdQueryVariables; + response: GetWorkspacePublicByIdQuery; + } + | { + name: 'getWorkspaceSharedPagesQuery'; + variables: GetWorkspaceSharedPagesQueryVariables; + response: GetWorkspaceSharedPagesQuery; + } + | { + name: 'getWorkspaceQuery'; + variables: GetWorkspaceQueryVariables; + response: GetWorkspaceQuery; + } + | { + name: 'getWorkspacesQuery'; + variables: GetWorkspacesQueryVariables; + response: GetWorkspacesQuery; + }; + export type Mutations = + | { + name: 'deleteBlobMutation'; + variables: DeleteBlobMutationVariables; + response: DeleteBlobMutation; + } + | { + name: 'setBlobMutation'; + variables: SetBlobMutationVariables; + response: SetBlobMutation; + } + | { + name: 'changeEmailMutation'; + variables: ChangeEmailMutationVariables; + response: ChangeEmailMutation; + } + | { + name: 'changePasswordMutation'; + variables: ChangePasswordMutationVariables; + response: ChangePasswordMutation; + } | { name: 'createWorkspaceMutation'; variables: CreateWorkspaceMutationVariables; response: CreateWorkspaceMutation; } + | { + name: 'deleteAccountMutation'; + variables: DeleteAccountMutationVariables; + response: DeleteAccountMutation; + } + | { + name: 'deleteWorkspaceMutation'; + variables: DeleteWorkspaceMutationVariables; + response: DeleteWorkspaceMutation; + } + | { + name: 'leaveWorkspaceMutation'; + variables: LeaveWorkspaceMutationVariables; + response: LeaveWorkspaceMutation; + } + | { + name: 'revokeMemberPermissionMutation'; + variables: RevokeMemberPermissionMutationVariables; + response: RevokeMemberPermissionMutation; + } + | { + name: 'revokePageMutation'; + variables: RevokePageMutationVariables; + response: RevokePageMutation; + } + | { + name: 'sendChangeEmailMutation'; + variables: SendChangeEmailMutationVariables; + response: SendChangeEmailMutation; + } + | { + name: 'sendChangePasswordEmailMutation'; + variables: SendChangePasswordEmailMutationVariables; + response: SendChangePasswordEmailMutation; + } + | { + name: 'sendSetPasswordEmailMutation'; + variables: SendSetPasswordEmailMutationVariables; + response: SendSetPasswordEmailMutation; + } + | { + name: 'setRevokePageMutation'; + variables: SetRevokePageMutationVariables; + response: SetRevokePageMutation; + } + | { + name: 'setSharePageMutation'; + variables: SetSharePageMutationVariables; + response: SetSharePageMutation; + } + | { + name: 'setWorkspacePublicByIdMutation'; + variables: SetWorkspacePublicByIdMutationVariables; + response: SetWorkspacePublicByIdMutation; + } + | { + name: 'sharePageMutation'; + variables: SharePageMutationVariables; + response: SharePageMutation; + } + | { + name: 'signInMutation'; + variables: SignInMutationVariables; + response: SignInMutation; + } + | { + name: 'signUpMutation'; + variables: SignUpMutationVariables; + response: SignUpMutation; + } | { name: 'uploadAvatarMutation'; variables: UploadAvatarMutationVariables; response: UploadAvatarMutation; + } + | { + name: 'inviteByEmailMutation'; + variables: InviteByEmailMutationVariables; + response: InviteByEmailMutation; + } + | { + name: 'acceptInviteByInviteIdMutation'; + variables: AcceptInviteByInviteIdMutationVariables; + response: AcceptInviteByInviteIdMutation; + } + | { + name: 'acceptInviteByWorkspaceIdMutation'; + variables: AcceptInviteByWorkspaceIdMutationVariables; + response: AcceptInviteByWorkspaceIdMutation; }; diff --git a/packages/graphql/src/utils.ts b/packages/graphql/src/utils.ts new file mode 100644 index 0000000000..9ca47f2137 --- /dev/null +++ b/packages/graphql/src/utils.ts @@ -0,0 +1,174 @@ +export const SPAN_ID_BYTES = 8; +export const TRACE_ID_BYTES = 16; +export const TRACE_VERSION = '00'; +export const TRACE_FLAG = '01'; + +const BytesBuffer = Array(32); + +type TraceSpan = { + name: string; + spanId: string; + displayName: { + value: string; + truncatedByteCount: number; + }; + startTime: string; + endTime: string; + attributes: { + attributeMap: { + requestId: { + stringValue: { + value: string; + truncatedByteCount: number; + }; + }; + }; + droppedAttributesCount: number; + }; +}; + +/** + * inspired by open-telemetry/opentelemetry-js + */ +export function generateRandUTF16Chars(bytes: number) { + for (let i = 0; i < bytes * 2; i++) { + BytesBuffer[i] = Math.floor(Math.random() * 16) + 48; + // valid hex characters in the range 48-57 and 97-102 + if (BytesBuffer[i] >= 58) { + BytesBuffer[i] += 39; + } + } + + return String.fromCharCode(...BytesBuffer.slice(0, bytes * 2)); +} + +export class TraceReporter { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + static traceReportEndpoint = process.env.TRACE_REPORT_ENDPOINT!; + static shouldReportTrace = process.env.SHOULD_REPORT_TRACE; + + private spansCache = new Array(); + private reportIntervalId: number | undefined | NodeJS.Timeout; + private reportInterval = 60_000; + + private static instance: TraceReporter; + + public static getInstance(): TraceReporter { + if (!TraceReporter.instance) { + const instance = (TraceReporter.instance = new TraceReporter()); + instance.initTraceReport(); + } + + return TraceReporter.instance; + } + + public cacheTrace( + traceId: string, + spanId: string, + requestId: string, + startTime: string + ) { + const span = TraceReporter.createTraceSpan( + traceId, + spanId, + requestId, + startTime + ); + this.spansCache.push(span); + if (this.spansCache.length <= 1) { + this.initTraceReport(); + } + } + + public uploadTrace( + traceId: string, + spanId: string, + requestId: string, + startTime: string + ) { + const span = TraceReporter.createTraceSpan( + traceId, + spanId, + requestId, + startTime + ); + TraceReporter.reportToTraceEndpoint(JSON.stringify({ spans: [span] })); + } + + public static reportToTraceEndpoint(payload: string): void { + if (typeof navigator !== 'undefined') { + navigator.sendBeacon(TraceReporter.traceReportEndpoint, payload); + } else { + fetch(TraceReporter.traceReportEndpoint, { + method: 'POST', + mode: 'cors', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', + }, + body: payload, + }).catch(console.warn); + } + } + + public static createTraceSpan( + traceId: string, + spanId: string, + requestId: string, + startTime: string + ): TraceSpan { + return { + name: `projects/{GCP_PROJECT_ID}/traces/${traceId}/spans/${spanId}`, + spanId, + displayName: { + value: 'fetch', + truncatedByteCount: 0, + }, + startTime, + endTime: new Date().toISOString(), + attributes: { + attributeMap: { + requestId: { + stringValue: { + value: requestId, + truncatedByteCount: 0, + }, + }, + }, + droppedAttributesCount: 0, + }, + }; + } + + private initTraceReport = () => { + if (!this.reportIntervalId && TraceReporter.shouldReportTrace) { + if (typeof window !== 'undefined') { + this.reportIntervalId = window.setInterval( + this.reportHandler, + this.reportInterval + ); + } else { + this.reportIntervalId = setInterval( + this.reportHandler, + this.reportInterval + ); + } + } + }; + + private reportHandler = () => { + if (this.spansCache.length <= 0) { + clearInterval(this.reportIntervalId); + this.reportIntervalId = undefined; + return; + } + TraceReporter.reportToTraceEndpoint( + JSON.stringify({ spans: [...this.spansCache] }) + ); + this.spansCache = []; + }; +} + +export const traceReporter = !process.env.SHOULD_REPORT_TRACE + ? null + : TraceReporter.getInstance(); diff --git a/packages/graphql/tsconfig.json b/packages/graphql/tsconfig.json index 8034f93ce4..a28130cf02 100644 --- a/packages/graphql/tsconfig.json +++ b/packages/graphql/tsconfig.json @@ -5,5 +5,10 @@ "composite": true, "noEmit": false, "outDir": "lib" - } + }, + "references": [ + { + "path": "../env" + } + ] } diff --git a/packages/hooks/src/__tests__/index.spec.ts b/packages/hooks/src/__tests__/index.spec.ts index ebe511676b..612f67eea1 100644 --- a/packages/hooks/src/__tests__/index.spec.ts +++ b/packages/hooks/src/__tests__/index.spec.ts @@ -14,7 +14,6 @@ import { beforeEach } from 'vitest'; import { useBlockSuitePagePreview } from '../use-block-suite-page-preview'; import { useBlockSuiteWorkspaceName } from '../use-block-suite-workspace-name'; -import { useBlockSuiteWorkspacePageIsPublic } from '../use-block-suite-workspace-page-is-public'; import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title'; let blockSuiteWorkspace: BlockSuiteWorkspace; @@ -66,19 +65,6 @@ describe('useBlockSuiteWorkspacePageTitle', () => { }); }); -describe('useBlockSuiteWorkspacePageIsPublic', () => { - test('basic', async () => { - const page = blockSuiteWorkspace.getPage('page0') as Page; - expect(page).not.toBeNull(); - const hook = renderHook(() => useBlockSuiteWorkspacePageIsPublic(page)); - expect(hook.result.current[0]).toBe(false); - hook.result.current[1](true); - expect(page.meta.isPublic).toBe(true); - hook.rerender(); - expect(hook.result.current[0]).toBe(true); - }); -}); - describe('useBlockSuitePagePreview', () => { test('basic', async () => { const page = blockSuiteWorkspace.getPage('page0') as Page; diff --git a/packages/hooks/src/use-block-suite-workspace-page-is-public.ts b/packages/hooks/src/use-block-suite-workspace-page-is-public.ts deleted file mode 100644 index cdfcf371fd..0000000000 --- a/packages/hooks/src/use-block-suite-workspace-page-is-public.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { assertExists } from '@blocksuite/global/utils'; -import type { Page } from '@blocksuite/store'; -import type { Atom, WritableAtom } from 'jotai'; -import { atom, useAtom } from 'jotai'; - -const weakMap = new WeakMap< - Page, - WritableAtom & Atom ->(); - -export function useBlockSuiteWorkspacePageIsPublic(page: Page) { - if (!weakMap.has(page)) { - const baseAtom = atom(page.meta.isPublic ?? false); - const writableAtom = atom( - get => get(baseAtom), - (_, set, isPublic: boolean) => { - page.workspace.setPageMeta(page.id, { - isPublic, - }); - set(baseAtom, isPublic); - } - ); - baseAtom.onMount = set => { - const disposable = page.workspace.meta.pageMetasUpdated.on(() => { - set(page.meta.isPublic ?? false); - }); - return () => { - disposable.dispose(); - }; - }; - weakMap.set(page, writableAtom); - } - const isPublicAtom = weakMap.get(page); - assertExists(isPublicAtom); - return useAtom(isPublicAtom); -} diff --git a/packages/i18n/project.json b/packages/i18n/project.json index 42e3e95c77..17cce58df6 100644 --- a/packages/i18n/project.json +++ b/packages/i18n/project.json @@ -11,5 +11,6 @@ "command": "node ./build.mjs" } } - } + }, + "tags": ["infra"] } diff --git a/packages/i18n/src/resources/en.json b/packages/i18n/src/resources/en.json index 4fa723916c..f2aae774ae 100644 --- a/packages/i18n/src/resources/en.json +++ b/packages/i18n/src/resources/en.json @@ -151,7 +151,13 @@ "Inline code": "Inline code", "Invite": "Invite", "Invite Members": "Invite Members", + "invited you to join": "invited you to join", "Invite placeholder": "Search mail (Gmail support only)", + "Invite Members Message": "Invited members will collaborate with you in current Workspace", + "Visit Workspace": "Visit Workspace", + "Successfully joined!": "Successfully joined!", + "Invitation sent": "Invitation sent", + "Invitation sent hint": "Invited members have been notified with email to join this Workspace.", "It takes up little space on your device": "It takes up little space on your device.", "It takes up little space on your device.": "It takes up little space on your device.", "It takes up more space on your device": "It takes up more space on your device.", @@ -162,6 +168,7 @@ "Keyboard Shortcuts": "Keyboard shortcuts", "Leave": "Leave", "Leave Workspace": "Leave Workspace", + "Leave Workspace hint": "After you leave, you will not be able to access content within this workspace.", "Leave Workspace Description": "After you leave, you will no longer be able to access the contents of this workspace.", "Link": "Hyperlink (with selected text)", "Loading": "Loading...", @@ -474,9 +481,11 @@ "com.affine.workspace.cloud.join": "Join Workspace", "com.affine.workspace.cloud.account.settings": "Account Settings", "com.affine.workspace.cloud.account.logout": "Log Out", - "com.affine.workspace.cloud.sync": "Cloud sync", + "com.affine.workspace.cloud": "Cloud Workspaces", "com.affine.workspace.cloud.auth": "Sign up/ Sign in", + "com.affine.workspace.cloud.description": "Sync with AFFiNE Cloud", "com.affine.workspace.local.import": "Import Workspace", + "com.affine.workspace.local": "Local Workspaces", "core": "core", "dark": "Dark", "emptyAllPages": "Click on the <1>$t(New Page) button to create your first page.", @@ -502,5 +511,15 @@ "Create a collection": "Create a collection", "Filters": "Filters", "Untitled Collection": "Untitled Collection", - "Add Filter": "Add Filter" + "Add Filter": "Add Filter", + "com.affine.share-menu.shareButton": "Share", + "com.affine.share-menu.sharedButton": "Shared", + "com.affine.share-menu.SharePage": "Share Page", + "com.affine.share-menu.SharedPage": "Shared Page", + "com.affine.share-menu.EnableCloudDescription": "Sharing page publicly requires AFFiNE Cloud service.", + "com.affine.share-menu.ShareViaExport": "Share via Export", + "com.affine.share-menu.ShareViaExportDescription": "Download a static copy of your page to share with others.", + "com.affine.share-menu.ShareWithLink": "Share with link", + "com.affine.share-menu.ShareWithLinkDescription": "Create a link you can easily share with anyone. The visitors will open your page in the form od a document", + "com.affine.share-menu.ShareMode": "Share mode" } diff --git a/packages/infra/project.json b/packages/infra/project.json index 391313f58d..e4828ee152 100644 --- a/packages/infra/project.json +++ b/packages/infra/project.json @@ -13,5 +13,6 @@ }, "outputs": ["{projectRoot}/dist"] } - } + }, + "tags": ["infra"] } diff --git a/packages/infra/src/preload/electron.ts b/packages/infra/src/preload/electron.ts index 60229a1ee5..25e398c75c 100644 --- a/packages/infra/src/preload/electron.ts +++ b/packages/infra/src/preload/electron.ts @@ -3,6 +3,7 @@ import { AsyncCall, type EventBasedChannel } from 'async-call-rpc'; import type { app, dialog, shell } from 'electron'; import { ipcRenderer } from 'electron'; import { Subject } from 'rxjs'; +import { z } from 'zod'; export interface ExposedMeta { handlers: [string, string[]][]; @@ -48,8 +49,17 @@ export function getElectronAPIs() { }; } +// todo: remove duplicated codes +const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']); +const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase(); +const buildType = ReleaseTypeSchema.parse(envBuildType); +const isDev = process.env.NODE_ENV === 'development'; +let schema = buildType === 'stable' ? 'affine' : `affine-${envBuildType}`; +schema = isDev ? 'affine-dev' : schema; + export const appInfo = { electron: true, + schema, }; function getMainAPIs() { @@ -142,7 +152,7 @@ const createMessagePortChannel = (port: MessagePort): EventBasedChannel => { function getHelperAPIs() { const events$ = new Subject<{ channel: string; args: any[] }>(); - const meta: ExposedMeta = (() => { + const meta: ExposedMeta | null = (() => { const val = process.argv .find(arg => arg.startsWith('--helper-exposed-meta=')) ?.split('=')[1]; @@ -211,7 +221,10 @@ function getHelperAPIs() { return [helperHandlers, helperEvents]; }; - const [apis, events] = setup(meta); - - return { apis, events }; + if (meta) { + const [apis, events] = setup(meta); + return { apis, events }; + } else { + return { apis: {}, events: {} }; + } } diff --git a/packages/infra/src/type.ts b/packages/infra/src/type.ts index 22d635d1dd..68358af17e 100644 --- a/packages/infra/src/type.ts +++ b/packages/infra/src/type.ts @@ -173,6 +173,7 @@ export type UIHandlers = { handleMinimizeApp: () => Promise; handleMaximizeApp: () => Promise; handleCloseApp: () => Promise; + handleFinishLogin: () => Promise; getGoogleOauthCode: () => Promise; }; @@ -265,9 +266,14 @@ export interface WorkspaceEvents { ) => () => void; } +export interface UIEvents { + onFinishLogin: (fn: () => void) => () => void; +} + export interface EventMap { updater: UpdaterEvents; applicationMenu: ApplicationMenuEvents; db: DBEvents; + ui: UIEvents; workspace: WorkspaceEvents; } diff --git a/packages/infra/vite.config.ts b/packages/infra/vite.config.ts index f1e6c63050..5dcd9d8568 100644 --- a/packages/infra/vite.config.ts +++ b/packages/infra/vite.config.ts @@ -1,8 +1,8 @@ import { resolve } from 'node:path'; import { fileURLToPath } from 'url'; +import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; -import { defineConfig } from 'vitest/config'; const root = fileURLToPath(new URL('.', import.meta.url)); diff --git a/packages/native/Cargo.toml b/packages/native/Cargo.toml index c63510021c..c0d573ea68 100644 --- a/packages/native/Cargo.toml +++ b/packages/native/Cargo.toml @@ -23,8 +23,9 @@ once_cell = "1" parking_lot = "0.12" serde = "1" serde_json = "1" -sqlx = { version = "0.7.0-alpha.3", default-features = false, features = [ +sqlx = { version = "0.7.1", default-features = false, features = [ "sqlite", + "migrate", "runtime-tokio", "tls-rustls", "chrono", @@ -41,7 +42,7 @@ uuid = { version = "1", default-features = false, features = [ affine_schema = { path = "./schema" } dotenv = "0.15" napi-build = "2" -sqlx = { version = "0.7.0-alpha.3", default-features = false, features = [ +sqlx = { version = "0.7.1", default-features = false, features = [ "sqlite", "runtime-tokio", "tls-rustls", diff --git a/packages/sdk/project.json b/packages/sdk/project.json index 009aa04101..28ec6c041c 100644 --- a/packages/sdk/project.json +++ b/packages/sdk/project.json @@ -17,5 +17,6 @@ "watch": true } } - } + }, + "tags": ["infra"] } diff --git a/packages/storage/Cargo.lock b/packages/storage/Cargo.lock deleted file mode 100644 index 3775cc4851..0000000000 --- a/packages/storage/Cargo.lock +++ /dev/null @@ -1,4428 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "affine_storage" -version = "1.0.0" -dependencies = [ - "jwst", - "jwst-storage", - "napi", - "napi-build", - "napi-derive", - "yrs", -] - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom 0.2.10", - "once_cell", - "version_check", -] - -[[package]] -name = "ahash" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "aho-corasick" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" -dependencies = [ - "memchr", -] - -[[package]] -name = "aliasable" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" - -[[package]] -name = "allocator-api2" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" - -[[package]] -name = "arbitrary" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "arc-swap" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" - -[[package]] -name = "arrayvec" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" - -[[package]] -name = "async-compat" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b48b4ff0c2026db683dea961cd8ea874737f56cffca86fa84415eaddc51c00d" -dependencies = [ - "futures-core", - "futures-io", - "once_cell", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "async-stream" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "async-trait" -version = "0.1.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "atoi" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" -dependencies = [ - "num-traits", -] - -[[package]] -name = "atomic_refcell" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79d6dc922a2792b006573f60b2648076355daeae5ce9cb59507e5908c9625d31" - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backon" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a6197b2120bb2185a267f6515038558b019e92b832bb0320e96d66268dcf9" -dependencies = [ - "fastrand", - "futures-core", - "pin-project", - "tokio", -] - -[[package]] -name = "bae" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b8de67cc41132507eeece2584804efcb15f85ba516e34c944b7667f480397a" -dependencies = [ - "heck 0.3.3", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" - -[[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bigdecimal" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" - -[[package]] -name = "bitpacking" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" -dependencies = [ - "crunchy", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "borsh" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" -dependencies = [ - "borsh-derive", - "hashbrown 0.13.2", -] - -[[package]] -name = "borsh-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" -dependencies = [ - "borsh-derive-internal", - "borsh-schema-derive-internal", - "proc-macro-crate", - "proc-macro2", - "syn 1.0.109", -] - -[[package]] -name = "borsh-derive-internal" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "borsh-schema-derive-internal" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bumpalo" -version = "3.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" - -[[package]] -name = "bytecheck" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bytemuck" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "cang-jie" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1238ed330d627f47a2309023a7425a4c73cd586ccbda77151e18ece8f9495b92" -dependencies = [ - "jieba-rs", - "log", - "tantivy", -] - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cedarwood" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d910bedd62c24733263d0bed247460853c9d22e8956bd4cd964302095e04e90" -dependencies = [ - "smallvec", -] - -[[package]] -name = "census" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fafee10a5dd1cffcb5cc560e0d0df8803d7355a2b12272e3557dee57314cb6e" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "time 0.1.45", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "clap" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "bitflags 1.3.2", - "clap_derive", - "clap_lex", - "indexmap", - "once_cell", - "textwrap", -] - -[[package]] -name = "clap_derive" -version = "3.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" -dependencies = [ - "heck 0.4.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "combine" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" -dependencies = [ - "memchr", -] - -[[package]] -name = "const-oid" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" - -[[package]] -name = "const-random" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368a7a772ead6ce7e1de82bfb04c485f3db8ec744f72925af5735e29a22cc18e" -dependencies = [ - "const-random-macro", - "proc-macro-hack", -] - -[[package]] -name = "const-random-macro" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d7d6ab3c3a2282db210df5f02c4dab6e0a7057af0fb7ebd4070f30fe05c0ddb" -dependencies = [ - "getrandom 0.2.10", - "once_cell", - "proc-macro-hack", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - -[[package]] -name = "cpufeatures" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "ctor" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1586fa608b1dab41f667475b4a41faec5ba680aee428bfa5de4ea520fdc6e901" -dependencies = [ - "quote", - "syn 2.0.22", -] - -[[package]] -name = "dashmap" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" -dependencies = [ - "cfg-if", - "hashbrown 0.12.3", - "lock_api", - "once_cell", - "parking_lot_core 0.9.8", -] - -[[package]] -name = "der" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "derive_arbitrary" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dlv-list" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d529fd73d344663edfd598ccb3f344e46034db51ebd103518eae34338248ad73" -dependencies = [ - "const-random", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "downcast-rs" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" - -[[package]] -name = "dyn-clone" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "encoding_rs" -version = "0.8.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "enum-iterator" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7add3873b5dd076766ee79c8e406ad1a472c385476b9e38849f8eec24f1be689" -dependencies = [ - "enum-iterator-derive", -] - -[[package]] -name = "enum-iterator-derive" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eecf8589574ce9b895052fa12d69af7a233f99e6107f5cb8dd1044f2a17bfdcb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "errno" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" -dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "exr" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279d3efcc55e19917fff7ab3ddd6c14afb6a90881a0078465196fe2f99d08c56" -dependencies = [ - "bit_field", - "flume", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - -[[package]] -name = "fail" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5e43d0f78a42ad591453aedb1d7ae631ce7ee445c7643691055a9ed8d3b01c" -dependencies = [ - "log", - "once_cell", - "rand 0.8.5", -] - -[[package]] -name = "fastdivide" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25c7df09945d65ea8d70b3321547ed414bbc540aad5bac6883d021b970f35b04" - -[[package]] -name = "fastfield_codecs" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374a3a53c1bd5fb31b10084229290eafb0a05f260ec90f1f726afffda4877a8a" -dependencies = [ - "fastdivide", - "itertools", - "log", - "ownedbytes", - "tantivy-bitpacker", - "tantivy-common", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fdeflate" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "flagset" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda653ca797810c02f7ca4b804b40b8b95ae046eb989d356bce17919a8c25499" - -[[package]] -name = "flate2" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "flume" -version = "0.10.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" -dependencies = [ - "futures-core", - "futures-sink", - "nanorand", - "pin-project", - "spin 0.9.8", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" - -[[package]] -name = "futures-executor" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-intrusive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot 0.11.2", -] - -[[package]] -name = "futures-io" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" - -[[package]] -name = "futures-macro" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "futures-sink" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" - -[[package]] -name = "futures-task" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" - -[[package]] -name = "futures-timer" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" - -[[package]] -name = "futures-util" -version = "0.3.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getset" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "gif" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "git2" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc" -dependencies = [ - "bitflags 1.3.2", - "libc", - "libgit2-sys", - "log", - "url", -] - -[[package]] -name = "governor" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5" -dependencies = [ - "cfg-if", - "dashmap", - "futures", - "futures-timer", - "no-std-compat", - "nonzero_ext", - "parking_lot 0.12.1", - "quanta", - "rand 0.8.5", - "smallvec", -] - -[[package]] -name = "h2" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "half" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" -dependencies = [ - "crunchy", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.6", -] - -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash 0.8.3", -] - -[[package]] -name = "hashbrown" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" -dependencies = [ - "ahash 0.8.3", - "allocator-api2", -] - -[[package]] -name = "hashlink" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" -dependencies = [ - "hashbrown 0.14.0", -] - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "home" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" -dependencies = [ - "windows-sys 0.48.0", -] - -[[package]] -name = "htmlescape" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" - -[[package]] -name = "http" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "hyper" -version = "0.14.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" -dependencies = [ - "http", - "hyper", - "rustls 0.21.2", - "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "image" -version = "0.24.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "exr", - "gif", - "jpeg-decoder", - "num-rational", - "num-traits", - "png", - "qoi", - "tiff", - "webp", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.48.0", -] - -[[package]] -name = "ipnet" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" - -[[package]] -name = "jieba-rs" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37228e06c75842d1097432d94d02f37fe3ebfca9791c2e8fef6e9db17ed128c1" -dependencies = [ - "cedarwood", - "fxhash", - "hashbrown 0.12.3", - "lazy_static", - "phf", - "phf_codegen", - "regex", -] - -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", -] - -[[package]] -name = "jpeg-decoder" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" -dependencies = [ - "rayon", -] - -[[package]] -name = "js-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "jwst" -version = "0.1.1" -source = "git+https://github.com/toeverything/OctoBase.git?branch=master#5e0d5e0cc65ea46f326fdde12658bfac59b38c9f" -dependencies = [ - "async-trait", - "base64 0.21.2", - "bytes", - "cang-jie", - "chrono", - "convert_case", - "futures", - "jwst-codec", - "lib0", - "nanoid", - "schemars", - "serde", - "serde_json", - "tantivy", - "thiserror", - "tokio", - "tracing", - "type-map", - "utoipa", - "vergen", - "yrs", -] - -[[package]] -name = "jwst-codec" -version = "0.1.0" -source = "git+https://github.com/toeverything/OctoBase.git?branch=master#5e0d5e0cc65ea46f326fdde12658bfac59b38c9f" -dependencies = [ - "arbitrary", - "bitvec", - "byteorder", - "jwst-logger", - "nanoid", - "nom", - "ordered-float", - "rand 0.8.5", - "serde_json", - "thiserror", -] - -[[package]] -name = "jwst-logger" -version = "0.1.0" -source = "git+https://github.com/toeverything/OctoBase.git?branch=master#5e0d5e0cc65ea46f326fdde12658bfac59b38c9f" -dependencies = [ - "chrono", - "nu-ansi-term", - "tracing", - "tracing-log", - "tracing-stackdriver", - "tracing-subscriber", -] - -[[package]] -name = "jwst-storage" -version = "0.1.0" -source = "git+https://github.com/toeverything/OctoBase.git?branch=master#5e0d5e0cc65ea46f326fdde12658bfac59b38c9f" -dependencies = [ - "anyhow", - "async-trait", - "bytes", - "chrono", - "futures", - "governor", - "image", - "jwst", - "jwst-codec", - "jwst-logger", - "jwst-storage-migration", - "lib0", - "opendal", - "path-ext", - "sea-orm", - "sea-orm-migration", - "sha2", - "thiserror", - "tokio", - "tokio-util", - "url", - "yrs", -] - -[[package]] -name = "jwst-storage-migration" -version = "0.1.0" -source = "git+https://github.com/toeverything/OctoBase.git?branch=master#5e0d5e0cc65ea46f326fdde12658bfac59b38c9f" -dependencies = [ - "sea-orm-migration", - "tokio", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -dependencies = [ - "spin 0.5.2", -] - -[[package]] -name = "lebe" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" - -[[package]] -name = "levenshtein_automata" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" - -[[package]] -name = "lib0" -version = "0.16.5" -source = "git+https://github.com/toeverything/y-crdt?rev=a700f09#a700f0990a993f905531f7acf589c6a736bb7429" -dependencies = [ - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "libc" -version = "0.2.147" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" - -[[package]] -name = "libgit2-sys" -version = "0.14.2+1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libm" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" - -[[package]] -name = "libsqlite3-sys" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libwebp-sys" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439fd1885aa28937e7edcd68d2e793cb4a22f8733460d2519fbafd2b215672bf" -dependencies = [ - "cc", -] - -[[package]] -name = "libz-sys" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "lock_api" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator", - "pin-utils", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "lru" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" -dependencies = [ - "hashbrown 0.12.3", -] - -[[package]] -name = "lz4_flex" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a8cbbb2831780bc3b9c15a41f5b49222ef756b6730a95f3decfdd15903eb5a3" - -[[package]] -name = "mach" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" -dependencies = [ - "libc", -] - -[[package]] -name = "matchers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "md-5" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" -dependencies = [ - "digest", -] - -[[package]] -name = "measure_time" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" -dependencies = [ - "instant", - "log", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memmap2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" -dependencies = [ - "adler", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" -dependencies = [ - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", -] - -[[package]] -name = "murmurhash32" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d736ff882f0e85fe9689fb23db229616c4c00aee2b3ac282f666d8f20eb25d4a" -dependencies = [ - "byteorder", -] - -[[package]] -name = "nanoid" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" -dependencies = [ - "rand 0.8.5", -] - -[[package]] -name = "nanorand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -dependencies = [ - "getrandom 0.2.10", -] - -[[package]] -name = "napi" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ede2d12cd6fce44da537a4be1f5510c73be2506c2e32dfaaafd1f36968f3a0e" -dependencies = [ - "bitflags 2.3.3", - "ctor", - "napi-derive", - "napi-sys", - "once_cell", - "tokio", -] - -[[package]] -name = "napi-build" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e" - -[[package]] -name = "napi-derive" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da1c6a8fa84d549aa8708fcd062372bf8ec6e849de39016ab921067d21bde367" -dependencies = [ - "cfg-if", - "convert_case", - "napi-derive-backend", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "napi-derive-backend" -version = "1.0.52" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20bbc7c69168d06a848f925ec5f0e0997f98e8c8d4f2cc30157f0da51c009e17" -dependencies = [ - "convert_case", - "once_cell", - "proc-macro2", - "quote", - "regex", - "semver", - "syn 1.0.109", -] - -[[package]] -name = "napi-sys" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3" -dependencies = [ - "libloading", -] - -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-bigint" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint-dig" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" -dependencies = [ - "hermit-abi 0.2.6", - "libc", -] - -[[package]] -name = "once_cell" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" - -[[package]] -name = "oneshot" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc22d22931513428ea6cc089e942d38600e3d00976eef8c86de6b8a3aadec6eb" -dependencies = [ - "loom", -] - -[[package]] -name = "opendal" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37de9fe637d53550bf3f76d5c731f69cb6f9685ada6afd390ada98994a3f91" -dependencies = [ - "anyhow", - "async-compat", - "async-trait", - "backon", - "base64 0.21.2", - "bytes", - "chrono", - "flagset", - "futures", - "http", - "hyper", - "log", - "md-5", - "once_cell", - "parking_lot 0.12.1", - "percent-encoding", - "pin-project", - "quick-xml 0.27.1", - "reqsign", - "reqwest", - "serde", - "serde_json", - "tokio", - "uuid", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "ordered-float" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc2dbde8f8a79f2102cc474ceb0ad68e3b80b85289ea62389b60e66777e4213" -dependencies = [ - "arbitrary", - "num-traits", -] - -[[package]] -name = "ordered-multimap" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" -dependencies = [ - "dlv-list", - "hashbrown 0.13.2", -] - -[[package]] -name = "os_str_bytes" -version = "6.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" - -[[package]] -name = "ouroboros" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db" -dependencies = [ - "aliasable", - "ouroboros_macro", -] - -[[package]] -name = "ouroboros_macro" -version = "0.15.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7" -dependencies = [ - "Inflector", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "ownedbytes" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e957eaa64a299f39755416e5b3128c505e9d63a91d0453771ad2ccd3907f8db" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.8", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.3.5", - "smallvec", - "windows-targets", -] - -[[package]] -name = "paste" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" - -[[package]] -name = "path-ext" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8285c3c3c3085f8819bdcebc9c7e783851527f34974d7d283ced36c977ae812" -dependencies = [ - "walkdir", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - -[[package]] -name = "png" -version = "0.17.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - -[[package]] -name = "proc-macro2" -version = "1.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "quanta" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20afe714292d5e879d8b12740aa223c6a88f118af41870e8b6196e39a02238a8" -dependencies = [ - "crossbeam-utils", - "libc", - "mach", - "once_cell", - "raw-cpuid", - "wasi 0.10.0+wasi-snapshot-preview1", - "web-sys", - "winapi", -] - -[[package]] -name = "quick-xml" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quick-xml" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quote" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom 0.1.16", - "libc", - "rand_chacha 0.2.2", - "rand_core 0.5.1", - "rand_hc", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" -dependencies = [ - "ppv-lite86", - "rand_core 0.5.1", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" -dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.10", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core 0.5.1", -] - -[[package]] -name = "raw-cpuid" -version = "10.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "rayon" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom 0.2.10", - "redox_syscall 0.2.16", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" -dependencies = [ - "aho-corasick 1.0.2", - "memchr", - "regex-syntax 0.7.2", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" - -[[package]] -name = "rend" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581008d2099240d37fb08d77ad713bcaec2c4d89d50b5b21a8bb1996bbab68ab" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "reqsign" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cb65eb3405f9c2de5c18bfc37338d6bbdb2c35eb8eb0e946208cbb564e4833" -dependencies = [ - "anyhow", - "async-trait", - "base64 0.21.2", - "chrono", - "form_urlencoded", - "hex", - "hmac", - "home", - "http", - "log", - "once_cell", - "percent-encoding", - "quick-xml 0.28.2", - "rand 0.8.5", - "reqwest", - "rsa", - "rust-ini", - "serde", - "serde_json", - "sha1", - "sha2", -] - -[[package]] -name = "reqwest" -version = "0.11.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" -dependencies = [ - "base64 0.21.2", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-rustls", - "ipnet", - "js-sys", - "log", - "mime", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls 0.21.2", - "rustls-native-certs", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-rustls 0.24.1", - "tokio-util", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "winreg", -] - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted", - "web-sys", - "winapi", -] - -[[package]] -name = "rkyv" -version = "0.7.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" -dependencies = [ - "bitvec", - "bytecheck", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rsa" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" -dependencies = [ - "byteorder", - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-iter", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rust-ini" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - -[[package]] -name = "rust-stemmers" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "rust_decimal" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0446843641c69436765a35a5a77088e28c2e6a12da93e84aa3ab1cd4aa5a042" -dependencies = [ - "arrayvec", - "borsh", - "bytecheck", - "byteorder", - "bytes", - "num-traits", - "rand 0.8.5", - "rkyv", - "serde", - "serde_json", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.37.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustls" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" -dependencies = [ - "log", - "ring", - "sct", - "webpki", -] - -[[package]] -name = "rustls" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" -dependencies = [ - "log", - "ring", - "rustls-webpki", - "sct", -] - -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" -dependencies = [ - "base64 0.21.2", -] - -[[package]] -name = "rustls-webpki" -version = "0.100.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" - -[[package]] -name = "ryu" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" -dependencies = [ - "windows-sys 0.42.0", -] - -[[package]] -name = "schemars" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" -dependencies = [ - "dyn-clone", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 1.0.109", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "sct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "sea-orm" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fade86e8d41fd1a4721f84cb834f4ca2783f973cc30e6212b7fafc134f169214" -dependencies = [ - "async-stream", - "async-trait", - "bigdecimal", - "chrono", - "futures", - "log", - "ouroboros", - "rust_decimal", - "sea-orm-macros", - "sea-query", - "sea-query-binder", - "sea-strum", - "serde", - "serde_json", - "sqlx", - "thiserror", - "time 0.3.22", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "sea-orm-cli" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbf34a2caf70c2e3be9bb1e674e9540f6dfd7c8f40f6f05daf3b9740e476005" -dependencies = [ - "chrono", - "clap", - "dotenvy", - "regex", - "sea-schema", - "tracing", - "tracing-subscriber", - "url", -] - -[[package]] -name = "sea-orm-macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28936f26d62234ff0be16f80115dbdeb3237fe9c25cf18fbcd1e3b3592360f20" -dependencies = [ - "bae", - "heck 0.3.3", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "sea-orm-migration" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278d3adfd0832b6ffc17d3cfbc574d3695a5c1b38814e0bc8ac238d33f3d87cf" -dependencies = [ - "async-trait", - "clap", - "dotenvy", - "futures", - "sea-orm", - "sea-orm-cli", - "sea-schema", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "sea-query" -version = "0.28.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbab99b8cd878ab7786157b7eb8df96333a6807cc6e45e8888c85b51534b401a" -dependencies = [ - "bigdecimal", - "chrono", - "rust_decimal", - "sea-query-derive", - "serde_json", - "time 0.3.22", - "uuid", -] - -[[package]] -name = "sea-query-binder" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cea85029985b40dfbf18318d85fe985c04db7c1b4e5e8e0a0a0cdff5f1e30f9" -dependencies = [ - "bigdecimal", - "chrono", - "rust_decimal", - "sea-query", - "serde_json", - "sqlx", - "time 0.3.22", - "uuid", -] - -[[package]] -name = "sea-query-derive" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63f62030c60f3a691f5fe251713b4e220b306e50a71e1d6f9cce1f24bb781978" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 1.0.109", - "thiserror", -] - -[[package]] -name = "sea-schema" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb2940bb5a10bc6cd05b450ce6cd3993e27fddd7eface2becb97fc5af3a040e" -dependencies = [ - "futures", - "sea-query", - "sea-schema-derive", -] - -[[package]] -name = "sea-schema-derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56821b7076f5096b8f726e2791ad255a99c82498e08ec477a65a96c461ff1927" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "sea-strum" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391d06a6007842cfe79ac6f7f53911b76dfd69fc9a6769f1cf6569d12ce20e1b" -dependencies = [ - "sea-strum_macros", -] - -[[package]] -name = "sea-strum_macros" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b4397b825df6ccf1e98bcdabef3bbcfc47ff5853983467850eeab878384f21" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "rustversion", - "syn 1.0.109", -] - -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - -[[package]] -name = "security-framework" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" - -[[package]] -name = "serde" -version = "1.0.164" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.164" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "serde_derive_internals" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "serde_json" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "signature" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "simd-adler32" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" - -[[package]] -name = "simdutf8" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" - -[[package]] -name = "siphasher" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallstr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f" -dependencies = [ - "smallvec", -] - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "spki" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "sqlformat" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" -dependencies = [ - "itertools", - "nom", - "unicode_categories", -] - -[[package]] -name = "sqlx" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" -dependencies = [ - "sqlx-core", - "sqlx-macros", -] - -[[package]] -name = "sqlx-core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" -dependencies = [ - "ahash 0.7.6", - "atoi", - "base64 0.13.1", - "bigdecimal", - "bitflags 1.3.2", - "byteorder", - "bytes", - "chrono", - "crossbeam-queue", - "dirs", - "dotenvy", - "either", - "event-listener", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "hashlink", - "hex", - "hkdf", - "hmac", - "indexmap", - "itoa", - "libc", - "libsqlite3-sys", - "log", - "md-5", - "memchr", - "num-bigint", - "once_cell", - "paste", - "percent-encoding", - "rand 0.8.5", - "rust_decimal", - "rustls 0.20.8", - "rustls-pemfile", - "serde", - "serde_json", - "sha1", - "sha2", - "smallvec", - "sqlformat", - "sqlx-rt", - "stringprep", - "thiserror", - "time 0.3.22", - "tokio-stream", - "url", - "uuid", - "webpki-roots", - "whoami", -] - -[[package]] -name = "sqlx-macros" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" -dependencies = [ - "dotenvy", - "either", - "heck 0.4.1", - "once_cell", - "proc-macro2", - "quote", - "serde_json", - "sqlx-core", - "sqlx-rt", - "syn 1.0.109", - "url", -] - -[[package]] -name = "sqlx-rt" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" -dependencies = [ - "once_cell", - "tokio", - "tokio-rustls 0.23.4", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "stringprep" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "subtle" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tantivy" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb26a6b22c84d8be41d99a14016d6f04d30d8d31a2ea411a8ab553af5cc490d" -dependencies = [ - "aho-corasick 0.7.20", - "arc-swap", - "async-trait", - "base64 0.13.1", - "bitpacking", - "byteorder", - "census", - "crc32fast", - "crossbeam-channel", - "downcast-rs", - "fail", - "fastdivide", - "fastfield_codecs", - "fs2", - "htmlescape", - "itertools", - "levenshtein_automata", - "log", - "lru", - "lz4_flex", - "measure_time", - "memmap2", - "murmurhash32", - "num_cpus", - "once_cell", - "oneshot", - "ownedbytes", - "rayon", - "regex", - "rust-stemmers", - "rustc-hash", - "serde", - "serde_json", - "smallvec", - "stable_deref_trait", - "tantivy-bitpacker", - "tantivy-common", - "tantivy-fst", - "tantivy-query-grammar", - "tempfile", - "thiserror", - "time 0.3.22", - "uuid", - "winapi", -] - -[[package]] -name = "tantivy-bitpacker" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e71a0c95b82d4292b097a09b989a6380d28c3a86800c841a2d03bae1fc8b9fa6" - -[[package]] -name = "tantivy-common" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14fef4182bb60df9a4b92cd8ecab39ba2e50a05542934af17eef1f49660705cb" -dependencies = [ - "byteorder", - "ownedbytes", -] - -[[package]] -name = "tantivy-fst" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" -dependencies = [ - "byteorder", - "regex-syntax 0.6.29", - "utf8-ranges", -] - -[[package]] -name = "tantivy-query-grammar" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343e3ada4c1c480953f6960f8a21ce9c76611480ffdd4f4e230fdddce0fc5331" -dependencies = [ - "combine", - "once_cell", - "regex", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" -dependencies = [ - "autocfg", - "cfg-if", - "fastrand", - "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.48.0", -] - -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - -[[package]] -name = "thiserror" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "tiff" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" -dependencies = [ - "flate2", - "jpeg-decoder", - "weezl", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" -dependencies = [ - "itoa", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" - -[[package]] -name = "time-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" -dependencies = [ - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" -dependencies = [ - "autocfg", - "bytes", - "libc", - "mio", - "num_cpus", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-macros" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.8", - "tokio", - "webpki", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.2", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "tracing-core" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" -dependencies = [ - "lazy_static", - "log", - "tracing-core", -] - -[[package]] -name = "tracing-serde" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-stackdriver" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff9dd91761e07727176a3dd3a1d64bbb577ea656b7b82fa4be4021832674c49" -dependencies = [ - "Inflector", - "serde", - "serde_json", - "thiserror", - "time 0.3.22", - "tracing-core", - "tracing-subscriber", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", -] - -[[package]] -name = "try-lock" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" - -[[package]] -name = "type-map" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" -dependencies = [ - "rustc-hash", -] - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - -[[package]] -name = "unicode-ident" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" - -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "url" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf8-ranges" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" - -[[package]] -name = "utoipa" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ae74ef183fae36d650f063ae7bde1cacbe1cd7e72b617cbe1e985551878b98" -dependencies = [ - "indexmap", - "serde", - "serde_json", - "utoipa-gen", -] - -[[package]] -name = "utoipa-gen" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ea8ac818da7e746a63285594cce8a96f5e00ee31994e655bd827569cb8b137b" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.22", -] - -[[package]] -name = "uuid" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" -dependencies = [ - "getrandom 0.2.10", - "serde", -] - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "vergen" -version = "7.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21b881cd6636ece9735721cf03c1fe1e774fe258683d084bb2812ab67435749" -dependencies = [ - "anyhow", - "cfg-if", - "enum-iterator", - "getset", - "git2", - "rustc_version", - "rustversion", - "thiserror", - "time 0.3.22", -] - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.22", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.22", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" - -[[package]] -name = "wasm-streams" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webp" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf022f821f166079a407d000ab57e84de020e66ffbbf4edde999bc7d6e371cae" -dependencies = [ - "libwebp-sys", -] - -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - -[[package]] -name = "weezl" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" - -[[package]] -name = "whoami" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" -dependencies = [ - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" -dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "yrs" -version = "0.16.5" -source = "git+https://github.com/toeverything/y-crdt?rev=a700f09#a700f0990a993f905531f7acf589c6a736bb7429" -dependencies = [ - "atomic_refcell", - "lib0", - "rand 0.7.3", - "smallstr", - "smallvec", - "thiserror", -] - -[[package]] -name = "zeroize" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] diff --git a/packages/storage/Cargo.toml b/packages/storage/Cargo.toml index 15f0b583d2..1035b21e52 100644 --- a/packages/storage/Cargo.toml +++ b/packages/storage/Cargo.toml @@ -3,15 +3,13 @@ name = "affine_storage" version = "1.0.0" edition = "2021" -# used to avoid sys dep conflict sqlx -> libsqlite-sys -[workspace] - [lib] crate-type = ["cdylib"] [dependencies] -jwst = { git = "https://github.com/toeverything/OctoBase.git", branch = "master" } -jwst-storage = { git = "https://github.com/toeverything/OctoBase.git", branch = "master" } +jwst = { git = "https://github.com/toeverything/OctoBase.git" } +jwst-codec = { git = "https://github.com/toeverything/OctoBase.git" } +jwst-storage = { git = "https://github.com/toeverything/OctoBase.git" } napi = { version = "2", default-features = false, features = [ "napi5", "async", @@ -21,7 +19,3 @@ yrs = { version = "0.16.5" } [build-dependencies] napi-build = "2" - -[patch.crates-io] -lib0 = { git = "https://github.com/toeverything/y-crdt", rev = "a700f09" } -yrs = { git = "https://github.com/toeverything/y-crdt", rev = "a700f09" } diff --git a/packages/storage/__tests__/storage.spec.js b/packages/storage/__tests__/storage.spec.js index 2634044688..56d5b61c06 100644 --- a/packages/storage/__tests__/storage.spec.js +++ b/packages/storage/__tests__/storage.spec.js @@ -142,13 +142,16 @@ describe('Test jwst storage binding', () => { }); test('should be able to store blob', async () => { - let workspace = await storage.createWorkspace('test-workspace', init); + let workspace = await storage.createWorkspace('test-workspace'); + await storage.sync(workspace.id, workspace.doc.guid, init); const blobId = await storage.uploadBlob(workspace.id, Buffer.from([1])); assert(blobId !== null); - let blob = await storage.blob(workspace.id, blobId); + let list = await storage.listBlobs(workspace.id); + assert.deepEqual(list, [blobId]); + let blob = await storage.getBlob(workspace.id, blobId); assert.deepEqual(blob.data, Buffer.from([1])); assert.strictEqual(blob.size, 1); assert.equal(blob.contentType, 'application/octet-stream'); diff --git a/packages/storage/index.d.ts b/packages/storage/index.d.ts index 0b7833732c..ff780a3863 100644 --- a/packages/storage/index.d.ts +++ b/packages/storage/index.d.ts @@ -1,44 +1,24 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ -export class Doc { - get guid(): string; -} - export class Storage { /** Create a storage instance and establish connection to persist store. */ static connect( database: string, debugOnlyAutoMigrate?: boolean | undefined | null ): Promise; - /** Get a workspace by id */ - getWorkspace(workspaceId: string): Promise; - /** Create a new workspace with a init update. */ - createWorkspace(workspaceId: string, init: Buffer): Promise; - /** Delete a workspace. */ - deleteWorkspace(workspaceId: string): Promise; - /** Sync doc updates. */ - sync(workspaceId: string, guid: string, update: Buffer): Promise; - /** Sync doc update with doc guid encoded. */ - syncWithGuid(workspaceId: string, update: Buffer): Promise; - /** Load doc as update buffer. */ - load(guid: string): Promise; + /** List all blobs in a workspace. */ + listBlobs(workspaceId?: string | undefined | null): Promise>; /** Fetch a workspace blob. */ - blob(workspaceId: string, name: string): Promise; + getBlob(workspaceId: string, name: string): Promise; /** Upload a blob into workspace storage. */ uploadBlob(workspaceId: string, blob: Buffer): Promise; + /** Delete a blob from workspace storage. */ + deleteBlob(workspaceId: string, hash: string): Promise; /** Workspace size taken by blobs. */ blobsSize(workspaceId: string): Promise; } -export class Workspace { - get doc(): Doc; - isEmpty(): boolean; - get id(): string; - get clientId(): string; - search(query: string): Array; -} - export interface Blob { contentType: string; lastModified: string; @@ -46,7 +26,5 @@ export interface Blob { data: Buffer; } -export interface SearchResult { - blockId: string; - score: number; -} +/** Merge updates in form like `Y.applyUpdate(doc, update)` way and return the result binary. */ +export function mergeUpdatesInApplyWay(updates: Array): Buffer; diff --git a/packages/storage/index.js b/packages/storage/index.js index d05ffda8ca..900771cb75 100644 --- a/packages/storage/index.js +++ b/packages/storage/index.js @@ -6,5 +6,4 @@ const require = createRequire(import.meta.url); const binding = require('./storage.node'); export const Storage = binding.Storage; -export const Workspace = binding.Workspace; -export const Document = binding.Doc; +export const mergeUpdatesInApplyWay = binding.mergeUpdatesInApplyWay; diff --git a/packages/storage/project.json b/packages/storage/project.json index e06161aba6..5b627984f6 100644 --- a/packages/storage/project.json +++ b/packages/storage/project.json @@ -17,6 +17,9 @@ }, { "runtime": "node -v" + }, + { + "runtime": "clang --version" } ], "outputs": ["{projectRoot}/*.node", "{workspaceRoot}/*.node"] diff --git a/packages/storage/src/lib.rs b/packages/storage/src/lib.rs index a0a4442b72..ea1afb5405 100644 --- a/packages/storage/src/lib.rs +++ b/packages/storage/src/lib.rs @@ -6,9 +6,9 @@ use std::{ path::PathBuf, }; -use jwst::{BlobStorage, SearchResult as JwstSearchResult, Workspace as JwstWorkspace, DocStorage}; -use jwst_storage::{JwstStorage, JwstStorageError}; -use yrs::{Doc as YDoc, ReadTxn, StateVector, Transact}; +use jwst::BlobStorage; +use jwst_codec::Doc; +use jwst_storage::{BlobStorageType, JwstStorage, JwstStorageError}; use napi::{bindgen_prelude::*, Error, Result, Status}; @@ -57,17 +57,7 @@ macro_rules! napi_wrap { }; } -napi_wrap!( - (Storage, JwstStorage), - (Workspace, JwstWorkspace), - (Doc, YDoc) -); - -fn to_update_v1(doc: &YDoc) -> Result { - let trx = doc.transact(); - - map_err!(trx.encode_state_as_update_v1(&StateVector::default())).map(|update| update.into()) -} +napi_wrap!((Storage, JwstStorage)); #[napi(object)] pub struct Blob { @@ -77,37 +67,22 @@ pub struct Blob { pub data: Buffer, } -#[napi(object)] -pub struct SearchResult { - pub block_id: String, - pub score: f64, -} - -impl From for SearchResult { - fn from(r: JwstSearchResult) -> Self { - Self { - block_id: r.block_id, - score: r.score as f64, - } - } -} - #[napi] impl Storage { /// Create a storage instance and establish connection to persist store. #[napi] pub async fn connect(database: String, debug_only_auto_migrate: Option) -> Result { let inner = match if cfg!(debug_assertions) && debug_only_auto_migrate.unwrap_or(false) { - JwstStorage::new_with_migration(&database).await + JwstStorage::new_with_migration(&database, BlobStorageType::DB).await } else { - JwstStorage::new(&database).await + JwstStorage::new(&database, BlobStorageType::DB).await } { Ok(storage) => storage, Err(JwstStorageError::Db(e)) => { return Err(Error::new( Status::GenericFailure, format!("failed to connect to database: {}", e), - )) + )); } Err(e) => return Err(Error::new(Status::GenericFailure, e.to_string())), }; @@ -115,71 +90,15 @@ impl Storage { Ok(inner.into()) } - /// Get a workspace by id + /// List all blobs in a workspace. #[napi] - pub async fn get_workspace(&self, workspace_id: String) -> Result> { - match self.0.get_workspace(workspace_id).await { - Ok(w) => Ok(Some(w.into())), - Err(JwstStorageError::WorkspaceNotFound(_)) => Ok(None), - Err(e) => Err(Error::new(Status::GenericFailure, e.to_string())), - } - } - - /// Create a new workspace with a init update. - #[napi] - pub async fn create_workspace(&self, workspace_id: String, init: Buffer) -> Result { - if map_err!(self.0.docs().detect_workspace(&workspace_id).await)? { - return Err(Error::new( - Status::GenericFailure, - format!("Workspace {} already exists", workspace_id), - )); - } - - let workspace = map_err!(self.0.create_workspace(workspace_id).await)?; - - let init = init.as_ref(); - let guid = workspace.doc_guid().to_string(); - map_err!(self.docs().update_doc(workspace.id(), guid, init).await)?; - - Ok(workspace.into()) - } - - /// Delete a workspace. - #[napi] - pub async fn delete_workspace(&self, workspace_id: String) -> Result<()> { - map_err!(self.docs().delete_workspace(&workspace_id).await)?; - map_err!(self.blobs().delete_workspace(workspace_id).await) - } - - /// Sync doc updates. - #[napi] - pub async fn sync(&self, workspace_id: String, guid: String, update: Buffer) -> Result<()> { - let update = update.as_ref(); - map_err!(self.docs().update_doc(workspace_id, guid, update).await) - } - - /// Sync doc update with doc guid encoded. - #[napi] - pub async fn sync_with_guid(&self, workspace_id: String, update: Buffer) -> Result<()> { - let update = update.as_ref(); - map_err!(self.docs().update_doc_with_guid(workspace_id, update).await) - } - - /// Load doc as update buffer. - #[napi] - pub async fn load(&self, guid: String) -> Result> { - self.ensure_exists(&guid).await?; - - if let Some(doc) = map_err!(self.docs().get_doc(guid).await)? { - Ok(Some(to_update_v1(&doc)?)) - } else { - Ok(None) - } + pub async fn list_blobs(&self, workspace_id: Option) -> Result> { + map_err!(self.blobs().list_blobs(workspace_id).await) } /// Fetch a workspace blob. #[napi] - pub async fn blob(&self, workspace_id: String, name: String) -> Result> { + pub async fn get_blob(&self, workspace_id: String, name: String) -> Result> { let (id, params) = { let path = PathBuf::from(name.clone()); let ext = path @@ -217,68 +136,28 @@ impl Storage { map_err!(self.blobs().put_blob(Some(workspace_id), blob).await) } + /// Delete a blob from workspace storage. + #[napi] + pub async fn delete_blob(&self, workspace_id: String, hash: String) -> Result { + map_err!(self.blobs().delete_blob(Some(workspace_id), hash).await) + } + /// Workspace size taken by blobs. #[napi] pub async fn blobs_size(&self, workspace_id: String) -> Result { map_err!(self.blobs().get_blobs_size(workspace_id).await) } - - async fn ensure_exists(&self, guid: &str) -> Result<()> { - if map_err!(self.docs().detect_doc(guid).await)? { - Ok(()) - } else { - Err(Error::new( - Status::GenericFailure, - format!("Doc {} not exists", guid), - )) - } - } } -#[napi] -impl Workspace { - #[napi(getter)] - pub fn doc(&self) -> Doc { - self.0.doc().into() +/// Merge updates in form like `Y.applyUpdate(doc, update)` way and return the result binary. +#[napi(catch_unwind)] +pub fn merge_updates_in_apply_way(updates: Vec) -> Result { + let mut doc = Doc::default(); + for update in updates { + map_err!(doc.apply_update_from_binary(update.as_ref().to_vec()))?; } - #[napi] - #[inline] - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } + let buf = map_err!(doc.encode_update_v1())?; - #[napi(getter)] - #[inline] - pub fn id(&self) -> String { - self.0.id() - } - - #[napi(getter)] - #[inline] - pub fn client_id(&self) -> String { - self.0.client_id().to_string() - } - - #[napi] - pub fn search(&self, query: String) -> Result> { - // TODO: search in all subdocs - let result = map_err!(self.0.search(&query))?; - - Ok( - result - .into_inner() - .into_iter() - .map(Into::into) - .collect::>(), - ) - } -} - -#[napi] -impl Doc { - #[napi(getter)] - pub fn guid(&self) -> String { - self.0.guid().to_string() - } + Ok(buf.into()) } diff --git a/packages/workers/src/index.ts b/packages/workers/src/index.ts index 26da5810ef..49ded44787 100644 --- a/packages/workers/src/index.ts +++ b/packages/workers/src/index.ts @@ -1,4 +1,9 @@ -const ALLOW_ORIGIN = ['https://affine.pro', 'https://affine.fail']; +const ALLOW_ORIGIN = [ + 'https://affine.pro', + 'https://app.affine.pro', + 'https://ambassador.affine.pro', + 'https://affine.fail', +]; function isString(s: any): boolean { return typeof s === 'string' || s instanceof String; diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 9bdc9ff637..6e3102ef2e 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -7,6 +7,7 @@ "./type": "./src/type.ts", "./migration": "./src/migration/index.ts", "./local/crud": "./src/local/crud.ts", + "./affine/gql": "./src/affine/gql.ts", "./providers": "./src/providers/index.ts" }, "peerDependencies": { @@ -17,6 +18,8 @@ "@affine-test/fixtures": "workspace:*", "@affine/debug": "workspace:*", "@affine/env": "workspace:*", + "@affine/graphql": "workspace:*", + "@affine/y-provider": "workspace:*", "@toeverything/hooks": "workspace:*", "@toeverything/y-indexeddb": "workspace:*", "async-call-rpc": "^6.3.1", @@ -26,6 +29,8 @@ "lib0": "^0.2.83", "react": "18.2.0", "react-dom": "18.2.0", + "socket.io-client": "^4.7.1", + "swr": "^2.2.1", "y-protocols": "^1.0.5", "yjs": "^13.6.7", "zod": "^3.22.2" diff --git a/apps/core/src/shared/__tests__/gql.spec.tsx b/packages/workspace/src/affine/__tests__/gql.spec.tsx similarity index 100% rename from apps/core/src/shared/__tests__/gql.spec.tsx rename to packages/workspace/src/affine/__tests__/gql.spec.tsx diff --git a/apps/core/src/shared/gql.ts b/packages/workspace/src/affine/gql.ts similarity index 74% rename from apps/core/src/shared/gql.ts rename to packages/workspace/src/affine/gql.ts index 5156f67cf2..6a6a30b738 100644 --- a/apps/core/src/shared/gql.ts +++ b/packages/workspace/src/affine/gql.ts @@ -1,3 +1,4 @@ +import { setupGlobal } from '@affine/env/global'; import type { GraphQLQuery, MutationOptions, @@ -7,7 +8,7 @@ import type { } from '@affine/graphql'; import { gqlFetcherFactory } from '@affine/graphql'; import type { GraphQLError } from 'graphql'; -import type { SWRConfiguration, SWRResponse } from 'swr'; +import type { Key, SWRConfiguration, SWRResponse } from 'swr'; import useSWR from 'swr'; import type { SWRMutationConfiguration, @@ -15,7 +16,11 @@ import type { } from 'swr/mutation'; import useSWRMutation from 'swr/mutation'; -const fetcher = gqlFetcherFactory(prefixUrl + '/graphql'); +setupGlobal(); + +export const fetcher = gqlFetcherFactory( + runtimeConfig.serverUrlPrefix + '/graphql' +); /** * A `useSWR` wrapper for sending graphql queries @@ -37,7 +42,13 @@ const fetcher = gqlFetcherFactory(prefixUrl + '/graphql'); */ export function useQuery( options: QueryOptions -): SWRResponse, GraphQLError | GraphQLError[]>; +): SWRResponse< + QueryResponse, + GraphQLError | GraphQLError[], + { + suspense: true; + } +>; export function useQuery( options: QueryOptions, config: Omit< @@ -48,13 +59,19 @@ export function useQuery( >, 'fetcher' > -): SWRResponse, GraphQLError | GraphQLError[]>; +): SWRResponse< + QueryResponse, + GraphQLError | GraphQLError[], + { + suspense: true; + } +>; export function useQuery( options: QueryOptions, config?: any ) { return useSWR( - () => [options.query.id, options.variables], + () => ['cloud', options.query.id, options.variables], () => fetcher(options), config ); @@ -74,21 +91,21 @@ export function useQuery( * * trigger({ name: 'John Doe' }) */ -export function useMutation( +export function useMutation( options: Omit, 'variables'> ): SWRMutationResponse< QueryResponse, GraphQLError | GraphQLError[], - string, + K, QueryVariables >; -export function useMutation( +export function useMutation( options: Omit, 'variables'>, config: Omit< SWRMutationConfiguration< QueryResponse, GraphQLError | GraphQLError[], - string, + K, QueryVariables >, 'fetcher' @@ -96,7 +113,7 @@ export function useMutation( ): SWRMutationResponse< QueryResponse, GraphQLError | GraphQLError[], - string, + K, QueryVariables >; export function useMutation( @@ -104,8 +121,8 @@ export function useMutation( config?: any ) { return useSWRMutation( - options.mutation.id, - (_: string, { arg }: { arg: any }) => + () => ['cloud', options.mutation.id], + (_: unknown[], { arg }: { arg: any }) => fetcher({ ...options, query: options.mutation, variables: arg }), config ); diff --git a/packages/workspace/src/affine/index.ts b/packages/workspace/src/affine/index.ts new file mode 100644 index 0000000000..9d6d47cb5e --- /dev/null +++ b/packages/workspace/src/affine/index.ts @@ -0,0 +1,184 @@ +import type { DatasourceDocAdapter } from '@affine/y-provider'; +import type { Socket } from 'socket.io-client'; +import { Manager } from 'socket.io-client'; +import { + applyAwarenessUpdate, + type Awareness, + encodeAwarenessUpdate, +} from 'y-protocols/awareness'; +import type { Doc } from 'yjs'; + +import { + type AwarenessChanges, + base64ToUint8Array, + uint8ArrayToBase64, +} from './utils'; + +let ioManager: Manager | null = null; +// use lazy initialization to avoid global side effect +function getIoManager(): Manager { + if (ioManager) { + return ioManager; + } + ioManager = new Manager(runtimeConfig.serverUrlPrefix + '/', { + autoConnect: false, + }); + return ioManager; +} + +export const createAffineDataSource = ( + id: string, + rootDoc: Doc, + awareness: Awareness +) => { + if (id !== rootDoc.guid) { + console.warn('important!! please use doc.guid as roomName'); + } + + const socket = getIoManager().socket('/'); + + return { + get socket() { + return socket; + }, + queryDocState: async (guid, options) => { + const stateVector = options?.stateVector + ? await uint8ArrayToBase64(options.stateVector) + : undefined; + + return new Promise((resolve, reject) => { + socket.emit( + 'doc-load', + { + workspaceId: rootDoc.guid, + guid, + stateVector, + }, + (docState: Error | { missing: string; state: string } | null) => { + if (docState instanceof Error) { + reject(docState); + return; + } + + resolve( + docState + ? { + missing: base64ToUint8Array(docState.missing), + state: docState.state + ? base64ToUint8Array(docState.state) + : undefined, + } + : false + ); + } + ); + }); + }, + sendDocUpdate: async (guid: string, update: Uint8Array) => { + socket.emit('client-update', { + workspaceId: rootDoc.guid, + guid, + update: await uint8ArrayToBase64(update), + }); + + return Promise.resolve(); + }, + onDocUpdate: callback => { + socket.on('connect', () => { + socket.emit('client-handshake', rootDoc.guid); + }); + const onUpdate = async (message: { + workspaceId: string; + guid: string; + update: string; + }) => { + if (message.workspaceId === rootDoc.guid) { + callback(message.guid, base64ToUint8Array(message.update)); + } + }; + socket.on('server-update', onUpdate); + const destroyAwareness = setupAffineAwareness(socket, rootDoc, awareness); + + socket.connect(); + return () => { + socket.emit('client-leave', rootDoc.guid); + socket.off('server-update', onUpdate); + destroyAwareness(); + socket.disconnect(); + }; + }, + } satisfies DatasourceDocAdapter & { readonly socket: Socket }; +}; + +function setupAffineAwareness( + conn: Socket, + rootDoc: Doc, + awareness: Awareness +) { + const awarenessBroadcast = ({ + workspaceId, + awarenessUpdate, + }: { + workspaceId: string; + awarenessUpdate: string; + }) => { + if (workspaceId !== rootDoc.guid) { + return; + } + + applyAwarenessUpdate( + awareness, + base64ToUint8Array(awarenessUpdate), + 'server' + ); + }; + + const awarenessUpdate = (changes: AwarenessChanges, origin: unknown) => { + if (origin === 'server') { + return; + } + + const changedClients = Object.values(changes).reduce((res, cur) => [ + ...res, + ...cur, + ]); + + const update = encodeAwarenessUpdate(awareness, changedClients); + uint8ArrayToBase64(update) + .then(encodedUpdate => { + conn.emit('awareness-update', { + workspaceId: rootDoc.guid, + awarenessUpdate: encodedUpdate, + }); + }) + .catch(err => console.error(err)); + }; + + const newClientAwarenessInitHandler = () => { + const awarenessUpdate = encodeAwarenessUpdate(awareness, [ + awareness.clientID, + ]); + uint8ArrayToBase64(awarenessUpdate) + .then(encodedAwarenessUpdate => { + conn.emit('awareness-update', { + guid: rootDoc.guid, + awarenessUpdate: encodedAwarenessUpdate, + }); + }) + .catch(err => console.error(err)); + }; + + conn.on('server-awareness-broadcast', awarenessBroadcast); + conn.on('new-client-awareness-init', newClientAwarenessInitHandler); + awareness.on('update', awarenessUpdate); + + conn.on('connect', () => { + conn.emit('awareness-init', rootDoc.guid); + }); + + return () => { + awareness.off('update', awarenessUpdate); + conn.off('server-awareness-broadcast', awarenessBroadcast); + conn.off('new-client-awareness-init', newClientAwarenessInitHandler); + }; +} diff --git a/packages/workspace/src/affine/utils.ts b/packages/workspace/src/affine/utils.ts new file mode 100644 index 0000000000..7c37f9c043 --- /dev/null +++ b/packages/workspace/src/affine/utils.ts @@ -0,0 +1,45 @@ +import type { Doc as YDoc } from 'yjs'; + +export type SubdocEvent = { + loaded: Set; + removed: Set; + added: Set; +}; + +export type UpdateHandler = (update: Uint8Array, origin: unknown) => void; +export type SubdocsHandler = (event: SubdocEvent) => void; +export type DestroyHandler = () => void; + +export type AwarenessChanges = Record< + 'added' | 'updated' | 'removed', + number[] +>; + +export function uint8ArrayToBase64(array: Uint8Array): Promise { + return new Promise(resolve => { + // Create a blob from the Uint8Array + const blob = new Blob([array]); + + const reader = new FileReader(); + reader.onload = function () { + const dataUrl = reader.result as string | null; + if (!dataUrl) { + resolve(''); + return; + } + // The result includes the `data:` URL prefix and the MIME type. We only want the Base64 data + const base64 = dataUrl.split(',')[1]; + resolve(base64); + }; + + reader.readAsDataURL(blob); + }); +} + +export function base64ToUint8Array(base64: string) { + const binaryString = atob(base64); + const binaryArray = binaryString.split('').map(function (char) { + return char.charCodeAt(0); + }); + return new Uint8Array(binaryArray); +} diff --git a/packages/workspace/src/atom.ts b/packages/workspace/src/atom.ts index 76320ba498..6464347e5d 100644 --- a/packages/workspace/src/atom.ts +++ b/packages/workspace/src/atom.ts @@ -1,7 +1,11 @@ import type { WorkspaceAdapter } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace'; import type { BlockHub } from '@blocksuite/blocks'; -import { assertExists } from '@blocksuite/global/utils'; +import { assertEquals, assertExists } from '@blocksuite/global/utils'; +import { + currentPageIdAtom, + currentWorkspaceIdAtom, +} from '@toeverything/infra/atom'; import { WorkspaceVersion } from '@toeverything/infra/blocksuite'; import { atom } from 'jotai'; import { z } from 'zod'; @@ -58,7 +62,7 @@ export const workspaceAdaptersAtom = atom< /** * root workspaces atom * this atom stores the metadata of all workspaces, - * which is `id` and `flavor`, that is enough to load the real workspace data + * which is `id` and `flavor,` that is enough to load the real workspace data */ const METADATA_STORAGE_KEY = 'jotai-workspaces'; const rootWorkspacesMetadataPrimitiveAtom = atom(async (get, { signal }) => { const WorkspaceAdapters = get(workspaceAdaptersAtom); assertExists(WorkspaceAdapters, 'workspace adapter should be defined'); - const maybeMetadata = get(rootWorkspacesMetadataPrimitiveAtom); - if (maybeMetadata !== null) { - return maybeMetadata; - } + const primitiveMetadata = get(rootWorkspacesMetadataPrimitiveAtom); + assertEquals( + primitiveMetadata, + null, + 'rootWorkspacesMetadataPrimitiveAtom should be null' + ); if (environment.isServer) { // return a promise in SSR to avoid the hydration mismatch @@ -127,6 +133,13 @@ const rootWorkspacesMetadataPromiseAtom = atom< for (const Adapter of Adapters) { const { CRUD, flavour: currentFlavour } = Adapter; + if ( + Adapter.Events['app:access'] && + !(await Adapter.Events['app:access']()) + ) { + // skip the adapter if the user doesn't have access to it + continue; + } try { const item = await CRUD.list(); // remove the metadata that is not in the list @@ -182,7 +195,10 @@ type SetStateAction = Value | ((prev: Value) => Value); export const rootWorkspacesMetadataAtom = atom< Promise, - [SetStateAction], + [ + setStateAction: SetStateAction, + newWorkspaceId?: string, + ], void >( async get => { @@ -192,8 +208,11 @@ export const rootWorkspacesMetadataAtom = atom< } return get(rootWorkspacesMetadataPromiseAtom); }, - async (get, set, action) => { + async (get, set, action, newWorkspaceId) => { const metadataPromise = get(rootWorkspacesMetadataPromiseAtom); + const oldWorkspaceId = get(currentWorkspaceIdAtom); + const oldPageId = get(currentPageIdAtom); + // get metadata set(rootWorkspacesMetadataPrimitiveAtom, async maybeMetadataPromise => { let metadata: RootWorkspaceMetadata[] = @@ -211,6 +230,17 @@ export const rootWorkspacesMetadataAtom = atom< // write back to localStorage rootWorkspaceMetadataArraySchema.parse(metadata); localStorage.setItem(METADATA_STORAGE_KEY, JSON.stringify(metadata)); + + // if the current workspace is deleted, reset the current workspace + if (oldWorkspaceId && metadata.some(x => x.id === oldWorkspaceId)) { + set(currentWorkspaceIdAtom, oldWorkspaceId); + set(currentPageIdAtom, oldPageId); + } + + if (newWorkspaceId) { + set(currentPageIdAtom, null); + set(currentWorkspaceIdAtom, newWorkspaceId); + } return metadata; }); } diff --git a/packages/workspace/src/blob/cloud-blob-storage.ts b/packages/workspace/src/blob/cloud-blob-storage.ts new file mode 100644 index 0000000000..aac8ed6ba6 --- /dev/null +++ b/packages/workspace/src/blob/cloud-blob-storage.ts @@ -0,0 +1,51 @@ +import { + deleteBlobMutation, + fetchWithReport, + listBlobsQuery, + setBlobMutation, +} from '@affine/graphql'; +import type { BlobStorage } from '@blocksuite/store'; + +import { fetcher } from '../affine/gql'; + +export const createCloudBlobStorage = (workspaceId: string): BlobStorage => { + return { + crud: { + get: async key => { + return fetchWithReport( + runtimeConfig.serverUrlPrefix + + `/api/workspaces/${workspaceId}/blobs/${key}` + ).then(res => res.blob()); + }, + set: async (key, value) => { + const result = await fetcher({ + query: setBlobMutation, + variables: { + workspaceId, + blob: new File([value], key), + }, + }); + console.assert(result.setBlob === key, 'Blob hash mismatch'); + return key; + }, + list: async () => { + const result = await fetcher({ + query: listBlobsQuery, + variables: { + workspaceId, + }, + }); + return result.listBlobs; + }, + delete: async (key: string) => { + await fetcher({ + query: deleteBlobMutation, + variables: { + workspaceId, + hash: key, + }, + }); + }, + }, + }; +}; diff --git a/packages/workspace/src/blob/local-static-storage.ts b/packages/workspace/src/blob/local-static-storage.ts index 186d62dd48..7e05bb9de8 100644 --- a/packages/workspace/src/blob/local-static-storage.ts +++ b/packages/workspace/src/blob/local-static-storage.ts @@ -25,10 +25,14 @@ export const createStaticStorage = (): BlobStorage => { get: async (key: string) => { if (key.startsWith('/static/')) { const response = await fetch(key); - return response.blob(); + if (response.ok) { + return response.blob(); + } } else if (predefinedStaticFiles.includes(key)) { const response = await fetch(`/static/${key}.png`); - return response.blob(); + if (response.ok) { + return response.blob(); + } } return null; }, diff --git a/packages/workspace/src/local/crud.ts b/packages/workspace/src/local/crud.ts index 9ed739c64c..498a0837f8 100644 --- a/packages/workspace/src/local/crud.ts +++ b/packages/workspace/src/local/crud.ts @@ -98,26 +98,10 @@ export const CRUD: WorkspaceCRUD = { list: async () => { logger.debug('list'); const storage = getStorage(); - let allWorkspaceIDs: string[] = Array.isArray( - storage.getItem(kStoreKey, []) - ) - ? (storage.getItem(kStoreKey, []) as z.infer) - : []; + const allWorkspaceIDs: string[] = storage.getItem(kStoreKey, []) as z.infer< + typeof schema + >; - // workspaces in desktop - if ( - window.apis && - environment.isDesktop && - runtimeConfig.enableSQLiteProvider - ) { - const desktopIds = (await window.apis.workspace.list()).map(([id]) => id); - // the ids maybe a subset of the local storage - const moreWorkspaces = desktopIds.filter( - id => !allWorkspaceIDs.includes(id) - ); - allWorkspaceIDs = [...allWorkspaceIDs, ...moreWorkspaces]; - storage.setItem(kStoreKey, allWorkspaceIDs); - } const workspaces = ( await Promise.all(allWorkspaceIDs.map(id => CRUD.get(id))) ).filter(item => item !== null) as LocalWorkspace[]; diff --git a/packages/workspace/src/manager/index.ts b/packages/workspace/src/manager/index.ts index 1435cdce8b..a05e638ca5 100644 --- a/packages/workspace/src/manager/index.ts +++ b/packages/workspace/src/manager/index.ts @@ -1,6 +1,7 @@ import { isBrowser, isDesktop } from '@affine/env/constant'; import type { BlockSuiteFeatureFlags } from '@affine/env/global'; import { WorkspaceFlavour } from '@affine/env/workspace'; +import { createAffinePublicProviders } from '@affine/workspace/providers'; import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import type { DocProviderCreator, StoreOptions } from '@blocksuite/store'; import { @@ -13,6 +14,7 @@ import { INTERNAL_BLOCKSUITE_HASH_MAP } from '@toeverything/infra/__internal__/w import type { Doc } from 'yjs'; import type { Transaction } from 'yjs'; +import { createCloudBlobStorage } from '../blob/cloud-blob-storage'; import { createStaticStorage } from '../blob/local-static-storage'; import { createSQLiteStorage } from '../blob/sqlite-blob-storage'; import { createAffineProviders, createLocalProviders } from '../providers'; @@ -95,18 +97,18 @@ export function getOrCreateWorkspace( const idGenerator = Generator.NanoID; const blobStorages: StoreOptions['blobStorages'] = []; - if (flavour === WorkspaceFlavour.AFFINE_CLOUD) { if (isBrowser) { blobStorages.push(createIndexeddbStorage); + blobStorages.push(createCloudBlobStorage); if (isDesktop && runtimeConfig.enableSQLiteProvider) { blobStorages.push(createSQLiteStorage); } + providerCreators.push(...createAffineProviders()); // todo(JimmFly): add support for cloud storage } - providerCreators.push(...createAffineProviders()); - } else { + } else if (flavour === WorkspaceFlavour.LOCAL) { if (isBrowser) { blobStorages.push(createIndexeddbStorage); if (isDesktop && runtimeConfig.enableSQLiteProvider) { @@ -114,6 +116,17 @@ export function getOrCreateWorkspace( } } providerCreators.push(...createLocalProviders()); + } else if (flavour === WorkspaceFlavour.AFFINE_PUBLIC) { + if (isBrowser) { + blobStorages.push(createIndexeddbStorage); + if (isDesktop && runtimeConfig.enableSQLiteProvider) { + blobStorages.push(createSQLiteStorage); + } + } + blobStorages.push(createCloudBlobStorage); + providerCreators.push(...createAffinePublicProviders()); + } else { + throw new Error('unsupported flavour'); } blobStorages.push(createStaticStorage); diff --git a/packages/workspace/src/providers/__tests__/socketio-provider.spec.ts b/packages/workspace/src/providers/__tests__/socketio-provider.spec.ts new file mode 100644 index 0000000000..7a155bd24a --- /dev/null +++ b/packages/workspace/src/providers/__tests__/socketio-provider.spec.ts @@ -0,0 +1,103 @@ +/** + * @vitest-environment happy-dom + */ +import 'fake-indexeddb/auto'; + +import type { AffineSocketIOProvider } from '@affine/env/workspace'; +import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; +import { Schema, Workspace } from '@blocksuite/store'; +import { describe, expect, test } from 'vitest'; +import * as awarenessProtocol from 'y-protocols/awareness'; +import { Doc } from 'yjs'; + +import { createAffineSocketIOProvider } from '..'; + +const schema = new Schema(); + +schema.register(AffineSchemas).register(__unstableSchemas); + +describe('sockio provider', () => { + test.skip('test storage', async () => { + const workspaceId = 'test-storage-ws'; + { + const workspace = new Workspace({ + id: workspaceId, + isSSR: true, + schema, + }); + const provider = createAffineSocketIOProvider( + workspace.id, + workspace.doc, + { + awareness: workspace.awarenessStore.awareness, + } + ) as AffineSocketIOProvider; + provider.connect(); + const page = workspace.createPage({ + id: 'page', + }); + + await page.waitForLoaded(); + page.addBlock('affine:page', { + title: new page.Text('123123'), + }); + + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + { + const workspace = new Workspace({ + id: workspaceId, + isSSR: true, + schema, + }); + const provider = createAffineSocketIOProvider( + workspace.id, + workspace.doc, + { + awareness: workspace.awarenessStore.awareness, + } + ) as AffineSocketIOProvider; + + provider.connect(); + + await new Promise(resolve => setTimeout(resolve, 1000)); + const page = workspace.getPage('page')!; + await page.waitForLoaded(); + const block = page.getBlockByFlavour('affine:page'); + expect(block[0].flavour).toEqual('affine:page'); + } + }); + + test.skip('test collaboration', async () => { + const workspaceId = 'test-collboration-ws'; + { + const doc = new Doc({ guid: workspaceId }); + const provider = createAffineSocketIOProvider(doc.guid, doc, { + awareness: new awarenessProtocol.Awareness(doc), + }) as AffineSocketIOProvider; + + const doc2 = new Doc({ guid: workspaceId }); + const provider2 = createAffineSocketIOProvider(doc2.guid, doc2, { + awareness: new awarenessProtocol.Awareness(doc2), + }) as AffineSocketIOProvider; + + provider.connect(); + provider2.connect(); + + await new Promise(resolve => setTimeout(resolve, 500)); + + const subdoc = new Doc(); + const folder = doc.getMap(); + folder.set('subDoc', subdoc); + subdoc.getText().insert(0, 'subDoc content'); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + expect( + (doc2.getMap().get('subDoc') as Doc).getText().toJSON(), + 'subDoc content' + ); + } + }); +}); diff --git a/packages/workspace/src/providers/cloud/index.ts b/packages/workspace/src/providers/cloud/index.ts new file mode 100644 index 0000000000..d4895a9bff --- /dev/null +++ b/packages/workspace/src/providers/cloud/index.ts @@ -0,0 +1,88 @@ +import { DebugLogger } from '@affine/debug'; +import { fetchWithReport } from '@affine/graphql'; +import type { ActiveDocProvider, DocProviderCreator } from '@blocksuite/store'; +import { Workspace } from '@blocksuite/store'; +import type { Doc } from 'yjs'; + +const Y = Workspace.Y; + +const logger = new DebugLogger('affine:cloud'); + +export async function downloadBinaryFromCloud( + rootGuid: string, + pageGuid: string +) { + const response = await fetchWithReport( + runtimeConfig.serverUrlPrefix + + `/api/workspaces/${rootGuid}/docs/${pageGuid}` + ); + if (response.ok) { + return response.arrayBuffer(); + } + return false; +} + +async function downloadBinary(rootGuid: string, doc: Doc) { + const buffer = await downloadBinaryFromCloud(rootGuid, doc.guid); + if (buffer) { + Y.applyUpdate(doc, new Uint8Array(buffer), 'affine-cloud'); + } +} + +export const createCloudDownloadProvider: DocProviderCreator = ( + id, + doc +): ActiveDocProvider => { + let _resolve: () => void; + let _reject: (error: unknown) => void; + const promise = new Promise((resolve, reject) => { + _resolve = resolve; + _reject = reject; + }); + + return { + flavour: 'affine-cloud-download', + active: true, + sync() { + downloadBinary(id, doc) + .then(() => { + logger.info(`Downloaded ${id}`); + _resolve(); + }) + .catch(_reject); + }, + get whenReady() { + return promise; + }, + }; +}; + +export const createMergeCloudSnapshotProvider: DocProviderCreator = ( + id, + doc +): ActiveDocProvider => { + let _resolve: () => void; + const promise = new Promise(resolve => { + _resolve = resolve; + }); + + return { + flavour: 'affine-cloud-merge-snapshot', + active: true, + sync() { + downloadBinary(id, doc) + .then(() => { + logger.info(`Downloaded ${id}`); + _resolve(); + }) + // ignore error + .catch(e => { + console.error(e); + _resolve(); + }); + }, + get whenReady() { + return promise; + }, + }; +}; diff --git a/packages/workspace/src/providers/index.ts b/packages/workspace/src/providers/index.ts index 5d3fb549a6..bad631530d 100644 --- a/packages/workspace/src/providers/index.ts +++ b/packages/workspace/src/providers/index.ts @@ -1,8 +1,10 @@ import { DebugLogger } from '@affine/debug'; import type { + AffineSocketIOProvider, LocalIndexedDBBackgroundProvider, LocalIndexedDBDownloadProvider, } from '@affine/env/workspace'; +import { createLazyProvider } from '@affine/y-provider'; import { assertExists } from '@blocksuite/global/utils'; import type { DocProviderCreator } from '@blocksuite/store'; import { Workspace } from '@blocksuite/store'; @@ -13,6 +15,12 @@ import { } from '@toeverything/y-indexeddb'; import type { Doc } from 'yjs'; +import { createAffineDataSource } from '../affine'; +import { + createCloudDownloadProvider, + createMergeCloudSnapshotProvider, + downloadBinaryFromCloud, +} from './cloud'; import { createSQLiteDBDownloadProvider, createSQLiteProvider, @@ -21,6 +29,18 @@ import { const Y = Workspace.Y; const logger = new DebugLogger('indexeddb-provider'); +const createAffineSocketIOProvider: DocProviderCreator = ( + id, + doc, + { awareness } +): AffineSocketIOProvider => { + const dataSource = createAffineDataSource(id, doc, awareness); + return { + flavour: 'affine-socket-io', + ...createLazyProvider(doc, dataSource), + }; +}; + const createIndexedDBBackgroundProvider: DocProviderCreator = ( id, blockSuiteWorkspace @@ -72,6 +92,7 @@ const createIndexedDBDownloadProvider: DocProviderCreator = ( Y.applyUpdate(doc, binary, indexedDBDownloadOrigin); } } + return { flavour: 'local-indexeddb', active: true, @@ -89,11 +110,13 @@ const createIndexedDBDownloadProvider: DocProviderCreator = ( }; export { + createAffineSocketIOProvider, createBroadcastChannelProvider, createIndexedDBBackgroundProvider, createIndexedDBDownloadProvider, createSQLiteDBDownloadProvider, createSQLiteProvider, + downloadBinaryFromCloud, }; export const createLocalProviders = (): DocProviderCreator[] => { @@ -116,9 +139,16 @@ export const createLocalProviders = (): DocProviderCreator[] => { export const createAffineProviders = (): DocProviderCreator[] => { return ( [ + ...createLocalProviders(), runtimeConfig.enableBroadcastChannelProvider && createBroadcastChannelProvider, + runtimeConfig.enableCloud && createAffineSocketIOProvider, + runtimeConfig.enableCloud && createMergeCloudSnapshotProvider, createIndexedDBDownloadProvider, ] as DocProviderCreator[] ).filter(v => Boolean(v)); }; + +export const createAffinePublicProviders = (): DocProviderCreator[] => { + return [createCloudDownloadProvider]; +}; diff --git a/packages/workspace/src/providers/sqlite-providers.ts b/packages/workspace/src/providers/sqlite-providers.ts index 0363a2a06f..9b4df6caa9 100644 --- a/packages/workspace/src/providers/sqlite-providers.ts +++ b/packages/workspace/src/providers/sqlite-providers.ts @@ -24,10 +24,18 @@ const createDatasource = (workspaceId: string): DatasourceDocAdapter => { return { queryDocState: async guid => { - return window.apis.db.getDocAsUpdates( + const update = await window.apis.db.getDocAsUpdates( workspaceId, workspaceId === guid ? undefined : guid ); + + if (update) { + return { + missing: update, + }; + } + + return false; }, sendDocUpdate: async (guid, update) => { return window.apis.db.applyDocUpdate( diff --git a/packages/workspace/tsconfig.json b/packages/workspace/tsconfig.json index 2cd5443c4f..42c087e870 100644 --- a/packages/workspace/tsconfig.json +++ b/packages/workspace/tsconfig.json @@ -8,9 +8,11 @@ "references": [ { "path": "../../tests/fixtures" }, { "path": "../y-indexeddb" }, + { "path": "../y-provider" }, { "path": "../env" }, { "path": "../debug" }, { "path": "../hooks" }, - { "path": "../infra" } + { "path": "../infra" }, + { "path": "../graphql" } ] } diff --git a/packages/y-indexeddb/src/provider.ts b/packages/y-indexeddb/src/provider.ts index 859d31877a..31a17b8905 100644 --- a/packages/y-indexeddb/src/provider.ts +++ b/packages/y-indexeddb/src/provider.ts @@ -6,7 +6,7 @@ import { import { assertExists } from '@blocksuite/global/utils'; import { openDB } from 'idb'; import type { Doc } from 'yjs'; -import { diffUpdate } from 'yjs'; +import { diffUpdate, encodeStateVectorFromUpdate } from 'yjs'; import { type BlockSuiteBinaryDB, @@ -50,11 +50,11 @@ const createDatasource = ({ const { updates } = data; const update = mergeUpdates(updates.map(({ update }) => update)); - const diff = options?.stateVector + const missing = options?.stateVector ? diffUpdate(update, options?.stateVector) : update; - return diff; + return { missing, state: encodeStateVectorFromUpdate(update) }; } catch (err: any) { if (!err.message?.includes('The database connection is closing.')) { throw err; diff --git a/packages/y-provider/src/__tests__/index.spec.ts b/packages/y-provider/src/__tests__/index.spec.ts index 30b21a29f7..84b623917b 100644 --- a/packages/y-provider/src/__tests__/index.spec.ts +++ b/packages/y-provider/src/__tests__/index.spec.ts @@ -1,7 +1,7 @@ import { setTimeout } from 'node:timers/promises'; import { describe, expect, test, vi } from 'vitest'; -import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs'; +import { applyUpdate, Doc, encodeStateAsUpdate, encodeStateVector } from 'yjs'; import { createLazyProvider } from '../lazy-provider'; import type { DatasourceDocAdapter } from '../types'; @@ -36,7 +36,10 @@ const createMemoryDatasource = (rootDoc: Doc) => { if (!subdoc) { return false; } - return encodeStateAsUpdate(subdoc, options?.stateVector); + return { + missing: encodeStateAsUpdate(subdoc, options?.stateVector), + state: encodeStateVector(subdoc), + }; }, sendDocUpdate: async (guid, update) => { const subdoc = getDoc(rootDoc, guid); diff --git a/packages/y-provider/src/lazy-provider.ts b/packages/y-provider/src/lazy-provider.ts index 670d2e0cf2..fb686edac8 100644 --- a/packages/y-provider/src/lazy-provider.ts +++ b/packages/y-provider/src/lazy-provider.ts @@ -4,7 +4,6 @@ import { type Doc, encodeStateAsUpdate, encodeStateVector, - encodeStateVectorFromUpdate, } from 'yjs'; import type { DatasourceDocAdapter, StatusAdapter } from './types'; @@ -113,20 +112,22 @@ export const createLazyProvider = ( pendingMap.set(guid, []); if (remoteUpdate) { - applyUpdate(doc, remoteUpdate, origin); + applyUpdate(doc, remoteUpdate.missing, origin); } - const sv = remoteUpdate - ? encodeStateVectorFromUpdate(remoteUpdate) - : undefined; - if (!connected) { return; } + // perf: optimize me // it is possible the doc is only in memory but not yet in the datasource // we need to send the whole update to the datasource - await datasource.sendDocUpdate(guid, encodeStateAsUpdate(doc, sv)); + await datasource.sendDocUpdate( + guid, + encodeStateAsUpdate(doc, remoteUpdate ? remoteUpdate.state : undefined) + ); + + doc.emit('sync', []); } /** @@ -159,7 +160,11 @@ export const createLazyProvider = ( }); }; - const subdocsHandler = (event: { loaded: Set; removed: Set }) => { + const subdocsHandler = (event: { + loaded: Set; + removed: Set; + added: Set; + }) => { event.loaded.forEach(subdoc => { connectDoc(subdoc).catch(console.error); }); @@ -293,6 +298,7 @@ export const createLazyProvider = ( return { get status() { + console.log('currentStatus', currentStatus); return currentStatus; }, subscribeStatusChange(cb: () => void) { diff --git a/packages/y-provider/src/types.ts b/packages/y-provider/src/types.ts index f997e230c5..82ef979da2 100644 --- a/packages/y-provider/src/types.ts +++ b/packages/y-provider/src/types.ts @@ -18,21 +18,39 @@ export interface StatusAdapter { subscribeStatusChange(onStatusChange: () => void): () => void; } +export interface DocState { + /** + * The missing structs of client queries with self state. + */ + missing: Uint8Array; + + /** + * The full state of remote, used to prepare for diff sync. + */ + state?: Uint8Array; +} + export interface DatasourceDocAdapter extends Partial { - // request diff update from other clients + /** + * request diff update from other clients + */ queryDocState: ( guid: string, options?: { stateVector?: Uint8Array; targetClientId?: number; } - ) => Promise; + ) => Promise; - // send update to the datasource + /** + * send update to the datasource + */ sendDocUpdate: (guid: string, update: Uint8Array) => Promise; - // listen to update from the datasource. Returns a function to unsubscribe. - // this is optional because some datasource might not support it + /** + * listen to update from the datasource. Returns a function to unsubscribe. + * this is optional because some datasource might not support it + */ onDocUpdate?( callback: (guid: string, update: Uint8Array) => void ): () => void; diff --git a/plugins/copilot/src/UI/debug-content.tsx b/plugins/copilot/src/UI/debug-content.tsx index 24b2c080aa..1df626ca8b 100644 --- a/plugins/copilot/src/UI/debug-content.tsx +++ b/plugins/copilot/src/UI/debug-content.tsx @@ -13,7 +13,6 @@ export const DebugContent = (): ReactElement => { { diff --git a/plugins/outline/src/atom.ts b/plugins/outline/src/atom.ts new file mode 100644 index 0000000000..3a2f13a39f --- /dev/null +++ b/plugins/outline/src/atom.ts @@ -0,0 +1,5 @@ +import { atom } from 'jotai'; + +export const blocksuiteRootAtom = atom(() => + document.querySelector('block-suite-root') +); diff --git a/scripts/check-version.mjs b/scripts/check-version.mjs index 5f258aa7fb..5bf95ef554 100644 --- a/scripts/check-version.mjs +++ b/scripts/check-version.mjs @@ -1,4 +1,4 @@ -import semver from 'semver'; +const semver = await import('../apps/server/node_modules/semver/index.js'); import packageJson from '../package.json' assert { type: 'json' }; diff --git a/tests/affine-cloud/e2e/basic.spec.ts b/tests/affine-cloud/e2e/basic.spec.ts new file mode 100644 index 0000000000..3100d0a709 --- /dev/null +++ b/tests/affine-cloud/e2e/basic.spec.ts @@ -0,0 +1,62 @@ +import { test } from '@affine-test/kit/playwright'; +import { + createRandomUser, + deleteUser, + getLoginCookie, +} from '@affine-test/kit/utils/cloud'; +import { openHomePage } from '@affine-test/kit/utils/load-page'; +import { waitEditorLoad } from '@affine-test/kit/utils/page-logic'; +import { clickSideBarCurrentWorkspaceBanner } from '@affine-test/kit/utils/sidebar'; +import { expect } from '@playwright/test'; + +let user: { + name: string; + email: string; + password: string; +}; + +test.beforeEach(async () => { + user = await createRandomUser(); +}); + +test.afterEach(async () => { + // if you want to keep the user in the database for debugging, + // comment this line + await deleteUser(user.email); +}); + +test('server exist', async ({ page }) => { + await openHomePage(page); + await waitEditorLoad(page); + + const json = await (await fetch('http://localhost:3010')).json(); + expect(json.message).toMatch(/^AFFiNE GraphQL server/); +}); + +test('enable cloud success', async ({ page, context }) => { + await page.goto('http://localhost:8080'); + await page.waitForSelector('v-line'); + + await clickSideBarCurrentWorkspaceBanner(page); + await page.getByTestId('cloud-signin-button').click({ + delay: 200, + }); + await page.getByPlaceholder('Enter your email address').type(user.email, { + delay: 50, + }); + await page.getByTestId('continue-login-button').click({ + delay: 200, + }); + await page.getByTestId('sign-in-with-password').click({ + delay: 200, + }); + await page.getByTestId('password-input').type('123456', { + delay: 50, + }); + expect(await getLoginCookie(context)).toBeUndefined(); + await page.getByTestId('sign-in-button').click(); + await page.waitForTimeout(1000); + await page.reload(); + await waitEditorLoad(page); + expect(await getLoginCookie(context)).toBeTruthy(); +}); diff --git a/tests/affine-cloud/e2e/login.spec.ts b/tests/affine-cloud/e2e/login.spec.ts new file mode 100644 index 0000000000..90596b79bc --- /dev/null +++ b/tests/affine-cloud/e2e/login.spec.ts @@ -0,0 +1,17 @@ +import { test } from '@affine-test/kit/playwright'; +import { openHomePage } from '@affine-test/kit/utils/load-page'; +import { waitEditorLoad } from '@affine-test/kit/utils/page-logic'; +import { clickSideBarCurrentWorkspaceBanner } from '@affine-test/kit/utils/sidebar'; +import { expect } from '@playwright/test'; + +test.describe('login', () => { + test('can open login modal in workspace list', async ({ page }) => { + await openHomePage(page); + await waitEditorLoad(page); + await clickSideBarCurrentWorkspaceBanner(page); + await page.getByTestId('cloud-signin-button').click({ + delay: 200, + }); + await expect(page.getByTestId('auth-modal')).toBeVisible(); + }); +}); diff --git a/tests/affine-cloud/package.json b/tests/affine-cloud/package.json new file mode 100644 index 0000000000..312ac0915e --- /dev/null +++ b/tests/affine-cloud/package.json @@ -0,0 +1,12 @@ +{ + "name": "@affine-test/affine-cloud", + "private": true, + "scripts": { + "e2e": "yarn playwright test" + }, + "devDependencies": { + "@affine-test/fixtures": "workspace:*", + "@affine-test/kit": "workspace:*", + "@playwright/test": "^1.37.0" + } +} diff --git a/tests/affine-cloud/playwright.config.ts b/tests/affine-cloud/playwright.config.ts new file mode 100644 index 0000000000..f88b60cea4 --- /dev/null +++ b/tests/affine-cloud/playwright.config.ts @@ -0,0 +1,64 @@ +import type { + PlaywrightTestConfig, + PlaywrightWorkerOptions, +} from '@playwright/test'; + +const config: PlaywrightTestConfig = { + testDir: './e2e', + fullyParallel: !process.env.CI, + timeout: process.env.CI ? 50_000 : 30_000, + use: { + baseURL: 'http://localhost:8081/', + browserName: + (process.env.BROWSER as PlaywrightWorkerOptions['browserName']) ?? + 'chromium', + permissions: ['clipboard-read', 'clipboard-write'], + viewport: { width: 1440, height: 800 }, + actionTimeout: 5 * 1000, + locale: 'en-US', + trace: 'on-first-retry', + video: 'on-first-retry', + }, + forbidOnly: !!process.env.CI, + workers: process.env.CI ? 1 : 4, + retries: 1, + reporter: process.env.CI ? 'github' : 'list', + webServer: [ + // Intentionally not building the web, reminds you to run it by yourself. + { + command: 'yarn -T run start:web-static', + port: 8080, + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + env: { + COVERAGE: process.env.COVERAGE || 'false', + }, + }, + { + command: 'yarn workspace @affine/server start', + port: 3010, + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { + DATABASE_URL: + process.env.DATABASE_URL ?? + 'postgresql://affine@localhost:5432/affine', + NODE_ENV: 'development', + AFFINE_ENV: process.env.AFFINE_ENV ?? 'dev', + DEBUG: 'affine:*', + FORCE_COLOR: 'true', + DEBUG_COLORS: 'true', + NEXTAUTH_URL: 'http://localhost:8080', + OAUTH_EMAIL_SENDER: 'noreply@toeverything.info', + }, + }, + ], +}; + +if (process.env.CI) { + config.retries = 3; +} + +export default config; diff --git a/tests/affine-cloud/tsconfig.json b/tests/affine-cloud/tsconfig.json new file mode 100644 index 0000000000..c7587f158b --- /dev/null +++ b/tests/affine-cloud/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "esModuleInterop": true, + "outDir": "lib" + }, + "include": ["e2e"], + "references": [ + { + "path": "../kit" + }, + { + "path": "../fixtures" + } + ] +} diff --git a/tests/affine-legacy/0.7.0-canary.18/e2e/basic.spec.ts b/tests/affine-legacy/0.7.0-canary.18/e2e/basic.spec.ts index 3da3e12cd8..28650bf2db 100644 --- a/tests/affine-legacy/0.7.0-canary.18/e2e/basic.spec.ts +++ b/tests/affine-legacy/0.7.0-canary.18/e2e/basic.spec.ts @@ -1,6 +1,7 @@ import { resolve } from 'node:path'; -import { expect, test } from '@playwright/test'; +import { test } from '@affine-test/kit/playwright'; +import { expect } from '@playwright/test'; import express from 'express'; import { createProxyMiddleware } from 'http-proxy-middleware'; diff --git a/tests/affine-legacy/0.7.0-canary.18/tsconfig.json b/tests/affine-legacy/0.7.0-canary.18/tsconfig.json index 0b284713e8..9d9cf8b360 100644 --- a/tests/affine-legacy/0.7.0-canary.18/tsconfig.json +++ b/tests/affine-legacy/0.7.0-canary.18/tsconfig.json @@ -4,5 +4,10 @@ "esModuleInterop": true, "outDir": "lib" }, - "include": ["e2e"] + "include": ["e2e"], + "references": [ + { + "path": "../../kit" + } + ] } diff --git a/tests/affine-local/e2e/local-first-delete-workspace.spec.ts b/tests/affine-local/e2e/local-first-delete-workspace.spec.ts index ae000c1e02..b67b20c1f8 100644 --- a/tests/affine-local/e2e/local-first-delete-workspace.spec.ts +++ b/tests/affine-local/e2e/local-first-delete-workspace.spec.ts @@ -32,7 +32,7 @@ test('Create new workspace, then delete it', async ({ page, workspace }) => { .getByTestId('delete-workspace-input') .type(currentWorkspaceName as string); const promise = page - .getByTestId('affine-toast') + .getByTestId('affine-notification') .waitFor({ state: 'attached' }); await page.getByTestId('delete-workspace-confirm-button').click(); await promise; @@ -46,7 +46,7 @@ test('Create new workspace, then delete it', async ({ page, workspace }) => { expect(currentWorkspace.flavour).toContain('local'); }); - +//FIXME: this test is broken test('Delete last workspace', async ({ page }) => { await openHomePage(page); await waitEditorLoad(page); @@ -60,12 +60,8 @@ test('Delete last workspace', async ({ page }) => { await page .getByTestId('delete-workspace-input') .type(currentWorkspaceName as string); - const promise = page - .getByTestId('affine-toast') - .waitFor({ state: 'attached' }); await page.getByTestId('delete-workspace-confirm-button').click(); - await promise; - await page.reload(); + await openHomePage(page); await expect(page.getByTestId('new-workspace')).toBeVisible(); await page.getByTestId('new-workspace').click(); await page.type('[data-testid="create-workspace-input"]', 'Test Workspace'); diff --git a/tests/affine-local/e2e/local-first-workspace-list.spec.ts b/tests/affine-local/e2e/local-first-workspace-list.spec.ts index af8d7ac727..f3d5bd84e3 100644 --- a/tests/affine-local/e2e/local-first-workspace-list.spec.ts +++ b/tests/affine-local/e2e/local-first-workspace-list.spec.ts @@ -124,8 +124,9 @@ test('create multi workspace in the workspace list', async ({ await page.waitForTimeout(1000); // check workspace list length { - const workspaceCards1 = await page.$$('data-testid=workspace-card'); - expect(workspaceCards1.length).toBe(3); + await page.waitForTimeout(1000); + const workspaceCards = page.getByTestId('workspace-card'); + expect(await workspaceCards.count()).toBe(3); } const workspaceChangePromise = page.evaluate(() => { diff --git a/tests/affine-local/e2e/quick-search.spec.ts b/tests/affine-local/e2e/quick-search.spec.ts index 073a33d218..5f8ce4e0e5 100644 --- a/tests/affine-local/e2e/quick-search.spec.ts +++ b/tests/affine-local/e2e/quick-search.spec.ts @@ -8,8 +8,10 @@ import { } from '@affine-test/kit/utils/page-logic'; import { expect, type Page } from '@playwright/test'; -const openQuickSearchByShortcut = async (page: Page) => +const openQuickSearchByShortcut = async (page: Page) => { await withCtrlOrMeta(page, () => page.keyboard.press('k', { delay: 50 })); + await page.waitForTimeout(500); +}; async function assertTitle(page: Page, text: string) { const edgeless = page.locator('affine-edgeless-page'); diff --git a/tests/affine-local/e2e/router.spec.ts b/tests/affine-local/e2e/router.spec.ts index b8978d14da..39477dd6c8 100644 --- a/tests/affine-local/e2e/router.spec.ts +++ b/tests/affine-local/e2e/router.spec.ts @@ -18,6 +18,6 @@ test('goto not found workspace', async ({ page }) => { // if doesn't wait for timeout, data won't be saved into indexedDB await page.waitForTimeout(1000); await page.goto(new URL('/workspace/invalid/all', coreUrl).toString()); - await page.waitForTimeout(1000); + await page.waitForTimeout(3000); expect(page.url()).toBe(new URL('/404', coreUrl).toString()); }); diff --git a/tests/kit/package.json b/tests/kit/package.json index ccc09b6f7a..72299aba6c 100644 --- a/tests/kit/package.json +++ b/tests/kit/package.json @@ -7,6 +7,7 @@ "./utils/*": "./utils/*.ts" }, "devDependencies": { + "@node-rs/argon2": "^1.5.2", "@playwright/test": "^1.37.1" }, "peerDependencies": { diff --git a/tests/kit/utils/cloud.ts b/tests/kit/utils/cloud.ts new file mode 100644 index 0000000000..e38e24b564 --- /dev/null +++ b/tests/kit/utils/cloud.ts @@ -0,0 +1,54 @@ +import { faker } from '@faker-js/faker'; +import { hash } from '@node-rs/argon2'; +import type { BrowserContext, Cookie } from '@playwright/test'; + +export async function getLoginCookie( + context: BrowserContext +): Promise { + return (await context.cookies()).find( + c => c.name === 'next-auth.session-token' + ); +} + +export async function createRandomUser() { + const user = { + name: faker.internet.userName(), + email: faker.internet.email().toLowerCase(), + password: '123456', + }; + const { + PrismaClient, + // eslint-disable-next-line @typescript-eslint/no-var-requires + } = require('../../../apps/server/node_modules/@prisma/client'); + const client = new PrismaClient(); + await client.$connect(); + await client.user.create({ + data: { + ...user, + emailVerified: new Date(), + password: await hash(user.password), + }, + }); + await client.$disconnect(); + + return client.user.findUnique({ + where: { + email: user.email, + }, + }); +} + +export async function deleteUser(email: string) { + const { + PrismaClient, + // eslint-disable-next-line @typescript-eslint/no-var-requires + } = require('../../../apps/server/node_modules/@prisma/client'); + const client = new PrismaClient(); + await client.$connect(); + await client.user.delete({ + where: { + email, + }, + }); + await client.$disconnect(); +} diff --git a/tsconfig.json b/tsconfig.json index 522a1042ce..ebd9d636be 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,8 @@ "noImplicitOverride": true, "noImplicitReturns": true, "noImplicitThis": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, // noPropertyAccessFromIndexSignature: false, // noUncheckedIndexedAccess: false, "useUnknownInCatchVariables": true, @@ -56,6 +56,7 @@ "paths": { "@affine/core/*": ["./packages/core/src/*"], "@affine/cli/*": ["./packages/cli/src/*"], + "@affine/server/*": ["./apps/server/src/*"], "@affine/component": ["./packages/component/src/index"], "@affine/component/*": [ "./packages/component/src/components/*/index", @@ -175,6 +176,9 @@ { "path": "./tests/affine-legacy/0.7.0-canary.18" }, + { + "path": "./tests/affine-cloud" + }, { "path": "./tests/affine-legacy/0.8.0-canary.7" } diff --git a/yarn.lock b/yarn.lock index a6773ef752..31e137f077 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,6 +53,16 @@ __metadata: languageName: unknown linkType: soft +"@affine-test/affine-cloud@workspace:tests/affine-cloud": + version: 0.0.0-use.local + resolution: "@affine-test/affine-cloud@workspace:tests/affine-cloud" + dependencies: + "@affine-test/fixtures": "workspace:*" + "@affine-test/kit": "workspace:*" + "@playwright/test": ^1.37.0 + languageName: unknown + linkType: soft + "@affine-test/affine-local@workspace:tests/affine-local": version: 0.0.0-use.local resolution: "@affine-test/affine-local@workspace:tests/affine-local" @@ -93,6 +103,7 @@ __metadata: version: 0.0.0-use.local resolution: "@affine-test/kit@workspace:tests/kit" dependencies: + "@node-rs/argon2": ^1.5.2 "@playwright/test": ^1.37.1 peerDependencies: "@playwright/test": "*" @@ -165,6 +176,7 @@ __metadata: "@types/react-dom": ^18.2.7 "@vanilla-extract/css": ^1.13.0 "@vanilla-extract/dynamic": ^2.0.3 + check-password-strength: ^2.0.7 clsx: ^2.0.0 dayjs: ^1.11.9 jotai: ^2.4.0 @@ -226,6 +238,7 @@ __metadata: "@affine/jotai": "workspace:*" "@affine/templates": "workspace:*" "@affine/workspace": "workspace:*" + "@aws-sdk/client-s3": 3.400.0 "@blocksuite/block-std": 0.0.0-20230827224823-81f8728e-nightly "@blocksuite/blocks": 0.0.0-20230827224823-81f8728e-nightly "@blocksuite/editor": 0.0.0-20230827224823-81f8728e-nightly @@ -242,11 +255,13 @@ __metadata: "@mui/material": ^5.14.6 "@perfsee/webpack": ^1.8.4 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.11 + "@radix-ui/react-select": ^1.2.2 "@react-hookz/web": ^23.1.0 "@sentry/webpack-plugin": ^2.7.0 "@svgr/webpack": ^8.1.0 "@swc/core": ^1.3.80 "@toeverything/components": ^0.0.19 + "@types/lodash-es": ^4.17.8 "@types/lodash.debounce": ^4.0.7 "@types/webpack-env": ^1.18.1 async-call-rpc: ^6.3.1 @@ -262,9 +277,12 @@ __metadata: jotai: ^2.4.0 jotai-devtools: ^0.6.2 lit: ^2.8.0 + lodash-es: ^4.17.21 lodash.debounce: ^4.0.8 lottie-web: ^5.12.2 + mime-types: ^2.1.35 mini-css-extract-plugin: ^2.7.6 + next-auth: ^4.22.1 next-themes: ^0.2.1 postcss-loader: ^7.3.3 raw-loader: ^4.0.2 @@ -281,6 +299,7 @@ __metadata: swc-plugin-coverage-instrument: ^0.0.20 swr: 2.2.1 thread-loader: ^4.0.2 + valtio: ^1.10.6 webpack: ^5.88.2 webpack-cli: ^5.1.4 webpack-dev-server: ^4.15.1 @@ -398,6 +417,7 @@ __metadata: version: 0.0.0-use.local resolution: "@affine/graphql@workspace:packages/graphql" dependencies: + "@affine/env": "workspace:*" "@graphql-codegen/add": ^5.0.0 "@graphql-codegen/cli": 5.0.0 "@graphql-codegen/typescript": ^4.0.1 @@ -405,6 +425,7 @@ __metadata: "@types/lodash-es": ^4.17.8 graphql: ^16.8.0 lodash-es: ^4.17.21 + nanoid: ^4.0.2 prettier: ^3.0.2 languageName: unknown linkType: soft @@ -639,43 +660,73 @@ __metadata: "@affine/storage": "workspace:*" "@apollo/server": ^4.9.2 "@auth/prisma-adapter": ^1.0.1 - "@aws-sdk/client-s3": ^3.398.0 + "@aws-sdk/client-s3": ^3.400.0 + "@google-cloud/opentelemetry-cloud-monitoring-exporter": ^0.17.0 + "@google-cloud/opentelemetry-cloud-trace-exporter": ^2.1.0 "@napi-rs/image": ^1.6.1 "@nestjs/apollo": ^12.0.7 "@nestjs/common": ^10.2.1 "@nestjs/core": ^10.2.1 "@nestjs/graphql": ^12.0.8 - "@nestjs/platform-express": ^10.2.1 + "@nestjs/platform-express": ^10.1.3 + "@nestjs/platform-socket.io": ^10.0.5 "@nestjs/testing": ^10.2.1 + "@nestjs/websockets": ^10.0.5 "@node-rs/argon2": ^1.5.2 "@node-rs/crc32": ^1.7.2 "@node-rs/jsonwebtoken": ^0.2.3 + "@opentelemetry/api": ^1.4.1 + "@opentelemetry/instrumentation": ^0.41.1 + "@opentelemetry/instrumentation-graphql": ^0.35.0 + "@opentelemetry/instrumentation-http": ^0.41.1 + "@opentelemetry/instrumentation-ioredis": ^0.35.0 + "@opentelemetry/instrumentation-nestjs-core": ^0.33.0 + "@opentelemetry/instrumentation-socket.io": ^0.34.0 + "@opentelemetry/sdk-metrics": ^1.15.1 + "@opentelemetry/sdk-node": ^0.41.1 + "@opentelemetry/sdk-trace-node": ^1.15.1 "@prisma/client": ^5.2.0 + "@prisma/instrumentation": ^5.0.0 + "@socket.io/redis-adapter": ^8.2.1 "@types/cookie-parser": ^1.4.3 "@types/express": ^4.17.17 "@types/lodash-es": ^4.17.8 "@types/node": ^18.17.11 "@types/nodemailer": ^6.4.9 + "@types/on-headers": ^1.0.0 + "@types/pretty-time": ^1.1.2 + "@types/sinon": ^10.0.15 "@types/supertest": ^2.0.12 + "@types/ws": ^8.5.5 c8: ^8.0.1 cookie-parser: ^1.4.6 dotenv: ^16.3.1 express: ^4.18.2 + file-type: ^18.5.0 + get-stream: ^7.0.1 graphql: ^16.8.0 graphql-type-json: ^0.3.2 graphql-upload: ^16.0.2 + ioredis: ^5.3.2 lodash-es: ^4.17.21 next-auth: 4.22.5 nodemailer: ^6.9.4 nodemon: ^3.0.1 + on-headers: ^1.0.2 parse-duration: ^1.1.0 + pretty-time: ^1.1.0 prisma: ^5.1.1 + prom-client: ^14.2.0 reflect-metadata: ^0.1.13 rxjs: ^7.8.1 semver: ^7.5.4 + sinon: ^15.2.0 + socket.io: ^4.7.1 supertest: ^6.3.3 ts-node: ^10.9.1 typescript: ^5.2.2 + ws: ^8.13.0 + yjs: ^13.6.6 bin: run-test: ./scripts/run-test.ts languageName: unknown @@ -716,6 +767,7 @@ __metadata: "@storybook/react-vite": ^7.3.2 "@storybook/test-runner": ^0.13.0 "@storybook/testing-library": ^0.2.0 + "@tomfreudenberg/next-auth-mock": ^0.5.6 "@vitejs/plugin-react": ^4.0.4 chromatic: ^6.24.0 concurrently: ^8.2.1 @@ -771,6 +823,8 @@ __metadata: "@affine-test/fixtures": "workspace:*" "@affine/debug": "workspace:*" "@affine/env": "workspace:*" + "@affine/graphql": "workspace:*" + "@affine/y-provider": "workspace:*" "@toeverything/hooks": "workspace:*" "@toeverything/y-indexeddb": "workspace:*" "@types/ws": ^8.5.5 @@ -781,6 +835,8 @@ __metadata: lib0: ^0.2.83 react: 18.2.0 react-dom: 18.2.0 + socket.io-client: ^4.7.1 + swr: ^2.2.1 ws: ^8.13.0 y-protocols: ^1.0.5 yjs: ^13.6.7 @@ -1239,9 +1295,9 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/client-s3@npm:^3.398.0": - version: 3.398.0 - resolution: "@aws-sdk/client-s3@npm:3.398.0" +"@aws-sdk/client-s3@npm:3.400.0, @aws-sdk/client-s3@npm:^3.400.0": + version: 3.400.0 + resolution: "@aws-sdk/client-s3@npm:3.400.0" dependencies: "@aws-crypto/sha1-browser": 3.0.0 "@aws-crypto/sha256-browser": 3.0.0 @@ -1250,7 +1306,7 @@ __metadata: "@aws-sdk/credential-provider-node": 3.398.0 "@aws-sdk/middleware-bucket-endpoint": 3.398.0 "@aws-sdk/middleware-expect-continue": 3.398.0 - "@aws-sdk/middleware-flexible-checksums": 3.398.0 + "@aws-sdk/middleware-flexible-checksums": 3.400.0 "@aws-sdk/middleware-host-header": 3.398.0 "@aws-sdk/middleware-location-constraint": 3.398.0 "@aws-sdk/middleware-logger": 3.398.0 @@ -1297,7 +1353,7 @@ __metadata: "@smithy/util-waiter": ^2.0.5 fast-xml-parser: 4.2.5 tslib: ^2.5.0 - checksum: bae0cffec2f442ccdfc8eb38498d4f043a5d5ba61657337f04dec59f3c69b68c6cc4d335395548853bc93fdfb4b75f880603b654722d668cb357705e06b9741d + checksum: ca6b0ed997a83a9dc95e3f148dd968efb5f679058f39d5e062ea6eba7a74a2cda1d3ff7ca8bf6a02ea7e033ed59670a2b7cdbd24afafa36cd0ad8df9fd49b652 languageName: node linkType: hard @@ -1502,9 +1558,9 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-flexible-checksums@npm:3.398.0": - version: 3.398.0 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.398.0" +"@aws-sdk/middleware-flexible-checksums@npm:3.400.0": + version: 3.400.0 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.400.0" dependencies: "@aws-crypto/crc32": 3.0.0 "@aws-crypto/crc32c": 3.0.0 @@ -1514,7 +1570,7 @@ __metadata: "@smithy/types": ^2.2.2 "@smithy/util-utf8": ^2.0.0 tslib: ^2.5.0 - checksum: 83d8f8ef0300e32acb0f39d5cacc2f35460899ec0ee3f137bada638ef0c191ddfe0a0e33ac2ab6c489c05ab744534c1f3cad00ed55cef3a96cb07fab86cac8ea + checksum: d2fab21ebf98b3a072fd1062c7ddff1b93db31d2356813b670f70690093f05518612009dde217e21493630aa0e6b8cb62a5c9fe29c81d84a66dab7ebbde6179a languageName: node linkType: hard @@ -5175,6 +5231,61 @@ __metadata: languageName: node linkType: hard +"@google-cloud/opentelemetry-cloud-monitoring-exporter@npm:^0.17.0": + version: 0.17.0 + resolution: "@google-cloud/opentelemetry-cloud-monitoring-exporter@npm:0.17.0" + dependencies: + "@google-cloud/opentelemetry-resource-util": ^2.1.0 + "@google-cloud/precise-date": ^3.0.1 + "@opentelemetry/semantic-conventions": ^1.0.0 + google-auth-library: ^7.0.0 + googleapis: ^97.0.0 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + "@opentelemetry/core": ^1.0.0 + "@opentelemetry/resources": ^1.0.0 + "@opentelemetry/sdk-metrics": ^1.0.0 + checksum: 230216f708bebbe60fe098ce28cde9374b6baab4caefb73603bcb8b189fda45d3ac691cb8b07b62a8d03a89fc4b34fbaca992e89918e0bb0129ce4f7d3154296 + languageName: node + linkType: hard + +"@google-cloud/opentelemetry-cloud-trace-exporter@npm:^2.1.0": + version: 2.1.0 + resolution: "@google-cloud/opentelemetry-cloud-trace-exporter@npm:2.1.0" + dependencies: + "@google-cloud/opentelemetry-resource-util": ^2.1.0 + "@grpc/grpc-js": ^1.1.8 + "@grpc/proto-loader": ^0.7.0 + google-auth-library: ^7.0.0 + google-proto-files: ^3.0.0 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + "@opentelemetry/core": ^1.0.0 + "@opentelemetry/resources": ^1.0.0 + "@opentelemetry/sdk-trace-base": ^1.0.0 + checksum: 17ba2dbf66cd47a776a0b1546ca68eadd22c5a0f58477b69be1927bc31f9b3596982715e4714f46d4f955d2fce91367f207d2026546485e254a20b2fe0fb8531 + languageName: node + linkType: hard + +"@google-cloud/opentelemetry-resource-util@npm:^2.1.0": + version: 2.1.0 + resolution: "@google-cloud/opentelemetry-resource-util@npm:2.1.0" + dependencies: + gcp-metadata: ^5.0.1 + peerDependencies: + "@opentelemetry/resources": ^1.0.0 + "@opentelemetry/semantic-conventions": ^1.0.0 + checksum: 3b70d42f39e808919e85be7b37cc716816c2a4dae0f2d903d1da40a4804199bbc5368f8f78b026292fddc38e9cecd515caf673925d57c630ed16c1fbe7e96862 + languageName: node + linkType: hard + +"@google-cloud/precise-date@npm:^3.0.1": + version: 3.0.1 + resolution: "@google-cloud/precise-date@npm:3.0.1" + checksum: 5f99a8a67909b4b2b66b580821a96f780f55660e096b3eebeae067b6391f8c904866220aa1c2426b67be5e5567818fc565dd44f60173b4f58a713e8fb0d90705 + languageName: node + linkType: hard + "@graphql-codegen/add@npm:^5.0.0": version: 5.0.0 resolution: "@graphql-codegen/add@npm:5.0.0" @@ -5749,6 +5860,30 @@ __metadata: languageName: node linkType: hard +"@grpc/grpc-js@npm:^1.1.8, @grpc/grpc-js@npm:^1.7.1": + version: 1.9.1 + resolution: "@grpc/grpc-js@npm:1.9.1" + dependencies: + "@grpc/proto-loader": ^0.7.8 + "@types/node": ">=12.12.47" + checksum: eb01e247a5fefb7730a1f934318aee5676390845efcde362bcfdd22c7278f57c2e2646d47e54ce84e75b061c41b0e412c3d4bd7d6ddccbc51e012e9984d28118 + languageName: node + linkType: hard + +"@grpc/proto-loader@npm:^0.7.0, @grpc/proto-loader@npm:^0.7.8": + version: 0.7.9 + resolution: "@grpc/proto-loader@npm:0.7.9" + dependencies: + lodash.camelcase: ^4.3.0 + long: ^5.0.0 + protobufjs: ^7.2.4 + yargs: ^17.7.2 + bin: + proto-loader-gen-types: build/bin/proto-loader-gen-types.js + checksum: 80df62eca98c8ff2bc584f3739d0d432b99b765489359cb2928fa36c699b5c728f633af330279b109cad9b8ec93c744d66bbb683f0d879fe1e51e5c79c95266b + languageName: node + linkType: hard + "@hapi/hoek@npm:^9.0.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" @@ -5790,6 +5925,13 @@ __metadata: languageName: node linkType: hard +"@ioredis/commands@npm:^1.1.1": + version: 1.2.0 + resolution: "@ioredis/commands@npm:1.2.0" + checksum: 9b20225ba36ef3e5caf69b3c0720597c3016cc9b1e157f519ea388f621dd9037177f84cfe7e25c4c32dad7dd90c70ff9123cd411f747e053cf292193c9c461e2 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -7107,19 +7249,33 @@ __metadata: languageName: node linkType: hard -"@nestjs/platform-express@npm:^10.2.1": - version: 10.2.1 - resolution: "@nestjs/platform-express@npm:10.2.1" +"@nestjs/platform-express@npm:^10.1.3": + version: 10.2.2 + resolution: "@nestjs/platform-express@npm:10.2.2" dependencies: body-parser: 1.20.2 cors: 2.8.5 express: 4.18.2 multer: 1.4.4-lts.1 - tslib: 2.6.1 + tslib: 2.6.2 peerDependencies: "@nestjs/common": ^10.0.0 "@nestjs/core": ^10.0.0 - checksum: c69f2ca51f3c252697c983fe67a128174ad2878418b97de9183ce271b1a1f49de90e42ea48c7e6c82fc9c2314c0574a367302fa3b723fd97e38bb9ff8ed86538 + checksum: 9a6e513bed53f73573eeb9a0f87d3c13625ae325b22ad925648f7efd70e8426c59d8d28a87bf63b18b4f77443c71016d760b997027c6714d494e3e54c0b06cff + languageName: node + linkType: hard + +"@nestjs/platform-socket.io@npm:^10.0.5": + version: 10.2.2 + resolution: "@nestjs/platform-socket.io@npm:10.2.2" + dependencies: + socket.io: 4.7.2 + tslib: 2.6.2 + peerDependencies: + "@nestjs/common": ^10.0.0 + "@nestjs/websockets": ^10.0.0 + rxjs: ^7.1.0 + checksum: 789550d3e00e81118a91c74a6aba13c8d605300488a4de3759176ee2bfb057153137643e32f1f0caf4cd0636543f8b56df30250381088b1672081b967b0c87ca languageName: node linkType: hard @@ -7142,6 +7298,26 @@ __metadata: languageName: node linkType: hard +"@nestjs/websockets@npm:^10.0.5": + version: 10.2.2 + resolution: "@nestjs/websockets@npm:10.2.2" + dependencies: + iterare: 1.2.1 + object-hash: 3.0.0 + tslib: 2.6.2 + peerDependencies: + "@nestjs/common": ^10.0.0 + "@nestjs/core": ^10.0.0 + "@nestjs/platform-socket.io": ^10.0.0 + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + "@nestjs/platform-socket.io": + optional: true + checksum: 1c19a4221429165f1d6268ff7bf88c555f88fa0755191d3c59a0d6368554ef371c2a0c2ad1aa8a76d9aea3b5b26acd2fea2b5b9e24aacb2dd3eb1150b2ae5fcb + languageName: node + linkType: hard + "@node-rs/argon2-android-arm-eabi@npm:1.5.2": version: 1.5.2 resolution: "@node-rs/argon2-android-arm-eabi@npm:1.5.2" @@ -7957,6 +8133,376 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/api-logs@npm:0.41.2": + version: 0.41.2 + resolution: "@opentelemetry/api-logs@npm:0.41.2" + dependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 9a33466baa5269df2c9153cf8385b06b957c1abdbae0c1fe3e16183d25fd89f93df58e98efb72994518ae07abb8215b803f67f62bdfb7a5050762cca1a3a3f07 + languageName: node + linkType: hard + +"@opentelemetry/api@npm:1.4.1, @opentelemetry/api@npm:^1.0.0, @opentelemetry/api@npm:^1.4.1": + version: 1.4.1 + resolution: "@opentelemetry/api@npm:1.4.1" + checksum: e783c40d1a518abf9c4c5d65223237c1392cd9a6c53ac6e2c3ef0c05ff7266e3dfc4fd9874316dae0dcb7a97950878deb513bcbadfaad653d48f0215f2a0911b + languageName: node + linkType: hard + +"@opentelemetry/context-async-hooks@npm:1.15.2": + version: 1.15.2 + resolution: "@opentelemetry/context-async-hooks@npm:1.15.2" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: f5e00a9920e14f12a4abd4f855927d7f2fcae3e57048b9853860529608d680e43473216643089c95de95519cf3642f635b178849acafec034cd7174198297295 + languageName: node + linkType: hard + +"@opentelemetry/core@npm:1.15.2": + version: 1.15.2 + resolution: "@opentelemetry/core@npm:1.15.2" + dependencies: + "@opentelemetry/semantic-conventions": 1.15.2 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 0040d1952b13d1cf5c7f428f9b061806023e2d08acd716e9aa72caa0c4bca99059ac4ddfbecebc4f3b993c576b834d4bcf0914586d0020719a1b1c428461a16a + languageName: node + linkType: hard + +"@opentelemetry/exporter-jaeger@npm:1.15.2": + version: 1.15.2 + resolution: "@opentelemetry/exporter-jaeger@npm:1.15.2" + dependencies: + "@opentelemetry/core": 1.15.2 + "@opentelemetry/sdk-trace-base": 1.15.2 + "@opentelemetry/semantic-conventions": 1.15.2 + jaeger-client: ^3.15.0 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 3c0a1dbcccf00e6aa036c97eb0d5b9f25f2a9e043b7bd28ac0ca2075e9ced5bcb327b538ed37a7048ab0817986403126ff8a8acfd0a608cf74df627fba2b8a2c + languageName: node + linkType: hard + +"@opentelemetry/exporter-trace-otlp-grpc@npm:0.41.2": + version: 0.41.2 + resolution: "@opentelemetry/exporter-trace-otlp-grpc@npm:0.41.2" + dependencies: + "@grpc/grpc-js": ^1.7.1 + "@opentelemetry/core": 1.15.2 + "@opentelemetry/otlp-grpc-exporter-base": 0.41.2 + "@opentelemetry/otlp-transformer": 0.41.2 + "@opentelemetry/resources": 1.15.2 + "@opentelemetry/sdk-trace-base": 1.15.2 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: f1ceb00fe92be4b61e51ad34ee230372fdf9e0df4c001bb25a92eec7ef4d820542c7be1cf8aa328ba980f9123ea2014f93bc01c7957f53a4b60f710d9380d6b4 + languageName: node + linkType: hard + +"@opentelemetry/exporter-trace-otlp-http@npm:0.41.2": + version: 0.41.2 + resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.41.2" + dependencies: + "@opentelemetry/core": 1.15.2 + "@opentelemetry/otlp-exporter-base": 0.41.2 + "@opentelemetry/otlp-transformer": 0.41.2 + "@opentelemetry/resources": 1.15.2 + "@opentelemetry/sdk-trace-base": 1.15.2 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 53e9f0f28386fbd5dff4238496d869cfc75771191f43aac2db37d412ce14f31bd723d354af43130370ca02b3c5d5a8893bddd4c9480cadcafc22728039561b65 + languageName: node + linkType: hard + +"@opentelemetry/exporter-trace-otlp-proto@npm:0.41.2": + version: 0.41.2 + resolution: "@opentelemetry/exporter-trace-otlp-proto@npm:0.41.2" + dependencies: + "@opentelemetry/core": 1.15.2 + "@opentelemetry/otlp-exporter-base": 0.41.2 + "@opentelemetry/otlp-proto-exporter-base": 0.41.2 + "@opentelemetry/otlp-transformer": 0.41.2 + "@opentelemetry/resources": 1.15.2 + "@opentelemetry/sdk-trace-base": 1.15.2 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 0cbed4ff3c74e3fb14f78dfea84da73e01d3e88f00208395d9e7247460b60c908e9f4d786acc389b91fe64d9f17de125fb299476fe0df14dda4a89a1c4e17d54 + languageName: node + linkType: hard + +"@opentelemetry/exporter-zipkin@npm:1.15.2": + version: 1.15.2 + resolution: "@opentelemetry/exporter-zipkin@npm:1.15.2" + dependencies: + "@opentelemetry/core": 1.15.2 + "@opentelemetry/resources": 1.15.2 + "@opentelemetry/sdk-trace-base": 1.15.2 + "@opentelemetry/semantic-conventions": 1.15.2 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: aeada7982ca78b6db91b37292b5b88351dccab625d37de87ce0983336cfd7f4ad4c0225d1eaf42cb3b8338f06ba296024130f4989997e0d61ab9079864bd47c1 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-graphql@npm:^0.35.0": + version: 0.35.1 + resolution: "@opentelemetry/instrumentation-graphql@npm:0.35.1" + dependencies: + "@opentelemetry/instrumentation": ^0.41.2 + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: d6490d8e5e7797bd07c57ddb3728e8473584924909c44c2355bef8d590835e1827888529dac5f0fc2f00539d58047420e5c6f15c770d32e23edb7511eaa78ea0 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-http@npm:^0.41.1": + version: 0.41.2 + resolution: "@opentelemetry/instrumentation-http@npm:0.41.2" + dependencies: + "@opentelemetry/core": 1.15.2 + "@opentelemetry/instrumentation": 0.41.2 + "@opentelemetry/semantic-conventions": 1.15.2 + semver: ^7.5.1 + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 756fdb276c50382e24464679bdc89133dd86e0c35082084665c1171c2baf7f25320afd5bd9def9fd6974508785e312d945f1fc1e2f77bd09ab12f86651975795 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-ioredis@npm:^0.35.0": + version: 0.35.1 + resolution: "@opentelemetry/instrumentation-ioredis@npm:0.35.1" + dependencies: + "@opentelemetry/instrumentation": ^0.41.2 + "@opentelemetry/redis-common": ^0.36.1 + "@opentelemetry/semantic-conventions": ^1.0.0 + "@types/ioredis4": "npm:@types/ioredis@^4.28.10" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 18399e5d3c691d0596ef29b5fee8fcafd486bc71a94c8dcbfb23786d84440ff74c0a83407acab3ad0f312391fccafee69ab4461f0ad30f4cfac10609ab35e51d + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-nestjs-core@npm:^0.33.0": + version: 0.33.1 + resolution: "@opentelemetry/instrumentation-nestjs-core@npm:0.33.1" + dependencies: + "@opentelemetry/instrumentation": ^0.41.2 + "@opentelemetry/semantic-conventions": ^1.0.0 + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 2aac41e67b3f617e03e250c884eacb7c88a7d2bf680d014ba633d28548c2b3334fcc1bbe4bbcf2d5023647a82e4de74f747a5ac071150c2278aadd738d01b61c + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-socket.io@npm:^0.34.0": + version: 0.34.1 + resolution: "@opentelemetry/instrumentation-socket.io@npm:0.34.1" + dependencies: + "@opentelemetry/instrumentation": ^0.41.2 + "@opentelemetry/semantic-conventions": ^1.0.0 + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 29a16df4a0dfa3acc6ad64cc301d30b26dbef1b2a66508a2b96836dde3a5c5cd330badc009058f284478ef96d4b0ca7d8ee26b00afe60ed23e45593127a1a359 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation@npm:0.41.2, @opentelemetry/instrumentation@npm:^0.41.1, @opentelemetry/instrumentation@npm:^0.41.2": + version: 0.41.2 + resolution: "@opentelemetry/instrumentation@npm:0.41.2" + dependencies: + "@types/shimmer": ^1.0.2 + import-in-the-middle: 1.4.2 + require-in-the-middle: ^7.1.1 + semver: ^7.5.1 + shimmer: ^1.2.1 + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 73df84c356064aaf2465f691001d6c165877d7204d2c66063b89dfa8b7206bc47442cd6dec88c40af86b104dbe72081c2e234376a63b3afaa529a9648ac016cd + languageName: node + linkType: hard + +"@opentelemetry/otlp-exporter-base@npm:0.41.2": + version: 0.41.2 + resolution: "@opentelemetry/otlp-exporter-base@npm:0.41.2" + dependencies: + "@opentelemetry/core": 1.15.2 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: e2f27327247de65316e64b625fa4e496e7978c8bf503d2720ec48e7b0201f9065e230b439a9186a3764cc1be2e0df1efcf9b0def93db0fc80de2db0220628763 + languageName: node + linkType: hard + +"@opentelemetry/otlp-grpc-exporter-base@npm:0.41.2": + version: 0.41.2 + resolution: "@opentelemetry/otlp-grpc-exporter-base@npm:0.41.2" + dependencies: + "@grpc/grpc-js": ^1.7.1 + "@opentelemetry/core": 1.15.2 + "@opentelemetry/otlp-exporter-base": 0.41.2 + protobufjs: ^7.2.3 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 339a748b7dd421e6098230147a8726893f46ff7bab580ce51411b9f14e62df506d6976d3fa75068105059e94eaea00ed06a55210422976578d64b9c7c18982f7 + languageName: node + linkType: hard + +"@opentelemetry/otlp-proto-exporter-base@npm:0.41.2": + version: 0.41.2 + resolution: "@opentelemetry/otlp-proto-exporter-base@npm:0.41.2" + dependencies: + "@opentelemetry/core": 1.15.2 + "@opentelemetry/otlp-exporter-base": 0.41.2 + protobufjs: ^7.2.3 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 2810e7b89385ff25fe432282f17f1a62db1d99fd8e2f174327fb19a09c36a448d87bb5c0ab0128224c20ac25f00b8de01c3f428de0cab0caf5737e4973492cd0 + languageName: node + linkType: hard + +"@opentelemetry/otlp-transformer@npm:0.41.2": + version: 0.41.2 + resolution: "@opentelemetry/otlp-transformer@npm:0.41.2" + dependencies: + "@opentelemetry/api-logs": 0.41.2 + "@opentelemetry/core": 1.15.2 + "@opentelemetry/resources": 1.15.2 + "@opentelemetry/sdk-logs": 0.41.2 + "@opentelemetry/sdk-metrics": 1.15.2 + "@opentelemetry/sdk-trace-base": 1.15.2 + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.5.0" + checksum: dc3332377012296597243b45faca0e83ad4b60b30b6b256d81056a211bf70c3a4ec27be15bf07a9e9f21338e862016792029c1320ce41ffe73db7a413dd38be5 + languageName: node + linkType: hard + +"@opentelemetry/propagator-b3@npm:1.15.2": + version: 1.15.2 + resolution: "@opentelemetry/propagator-b3@npm:1.15.2" + dependencies: + "@opentelemetry/core": 1.15.2 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 830b34f678b1b6c12f51135ada6555f2981d1d9c44f59fa21c4a550b0edb483b644791368ffc0b09b4ec7aed77e9ff6e437df91513c86c4dc35a25841f6248c1 + languageName: node + linkType: hard + +"@opentelemetry/propagator-jaeger@npm:1.15.2": + version: 1.15.2 + resolution: "@opentelemetry/propagator-jaeger@npm:1.15.2" + dependencies: + "@opentelemetry/core": 1.15.2 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 319e4c5e197d338d016ad3d5eddc7307c34d2217e81c182d17868c3b5a44442d59de5c0eb4b88c9690d253c42cf8151201d69169641349d250503d9f36d9fe7b + languageName: node + linkType: hard + +"@opentelemetry/redis-common@npm:^0.36.1": + version: 0.36.1 + resolution: "@opentelemetry/redis-common@npm:0.36.1" + checksum: 85a992408ed2057ee3f6932b68553bae6a8be6bffdf6e49244df50252a494a63bfa486ecd203ce8a69f67fa79c5a74f5ae84f425de54727c628bca679b5cd16b + languageName: node + linkType: hard + +"@opentelemetry/resources@npm:1.15.2": + version: 1.15.2 + resolution: "@opentelemetry/resources@npm:1.15.2" + dependencies: + "@opentelemetry/core": 1.15.2 + "@opentelemetry/semantic-conventions": 1.15.2 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 072d64bee2a073ac3f1218ba9d24bd0a20fc69988d206688c2f53e813c9d84ae325093c3c0e8909c2208cfa583fe5bad71d16af7d6b087a348d866c1d0857396 + languageName: node + linkType: hard + +"@opentelemetry/sdk-logs@npm:0.41.2": + version: 0.41.2 + resolution: "@opentelemetry/sdk-logs@npm:0.41.2" + dependencies: + "@opentelemetry/core": 1.15.2 + "@opentelemetry/resources": 1.15.2 + peerDependencies: + "@opentelemetry/api": ">=1.4.0 <1.5.0" + "@opentelemetry/api-logs": ">=0.39.1" + checksum: 055dd8dbb78442dc9742bce12491c5ccb48ffa8b9a5ed3046294a21bf40802cc4ddc753a6324b1f571a1af8d2936c93027c1d4e90bb7c8536390be12fc9cbc8a + languageName: node + linkType: hard + +"@opentelemetry/sdk-metrics@npm:1.15.2, @opentelemetry/sdk-metrics@npm:^1.15.1": + version: 1.15.2 + resolution: "@opentelemetry/sdk-metrics@npm:1.15.2" + dependencies: + "@opentelemetry/core": 1.15.2 + "@opentelemetry/resources": 1.15.2 + lodash.merge: ^4.6.2 + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.5.0" + checksum: 15eb40b977618ea24a7ce5bea264ab4c8a428d91c2ef0d824c9c98bea9fe3e136c92d03e5538ce86438e123f59977516112a657c4fc572e71ac544d483348b17 + languageName: node + linkType: hard + +"@opentelemetry/sdk-node@npm:^0.41.1": + version: 0.41.2 + resolution: "@opentelemetry/sdk-node@npm:0.41.2" + dependencies: + "@opentelemetry/api-logs": 0.41.2 + "@opentelemetry/core": 1.15.2 + "@opentelemetry/exporter-jaeger": 1.15.2 + "@opentelemetry/exporter-trace-otlp-grpc": 0.41.2 + "@opentelemetry/exporter-trace-otlp-http": 0.41.2 + "@opentelemetry/exporter-trace-otlp-proto": 0.41.2 + "@opentelemetry/exporter-zipkin": 1.15.2 + "@opentelemetry/instrumentation": 0.41.2 + "@opentelemetry/resources": 1.15.2 + "@opentelemetry/sdk-logs": 0.41.2 + "@opentelemetry/sdk-metrics": 1.15.2 + "@opentelemetry/sdk-trace-base": 1.15.2 + "@opentelemetry/sdk-trace-node": 1.15.2 + "@opentelemetry/semantic-conventions": 1.15.2 + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.5.0" + checksum: accc2b3826250782ece88045d3a1f698b70aa3968ddd2add29e89d558c9e72ce39c84f0d6d483ba3b1de71f6fd0348711a7d05d8b5384635442c6b3f850f1a92 + languageName: node + linkType: hard + +"@opentelemetry/sdk-trace-base@npm:1.15.2": + version: 1.15.2 + resolution: "@opentelemetry/sdk-trace-base@npm:1.15.2" + dependencies: + "@opentelemetry/core": 1.15.2 + "@opentelemetry/resources": 1.15.2 + "@opentelemetry/semantic-conventions": 1.15.2 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 3ca9d71919451f8e4a0644434981c7fa6dc29d002da03784578fbe47689625d106c00ab5b35539a8d348e40676ea57d44ba6845a9a604dd6be670911f83835bf + languageName: node + linkType: hard + +"@opentelemetry/sdk-trace-node@npm:1.15.2, @opentelemetry/sdk-trace-node@npm:^1.15.1": + version: 1.15.2 + resolution: "@opentelemetry/sdk-trace-node@npm:1.15.2" + dependencies: + "@opentelemetry/context-async-hooks": 1.15.2 + "@opentelemetry/core": 1.15.2 + "@opentelemetry/propagator-b3": 1.15.2 + "@opentelemetry/propagator-jaeger": 1.15.2 + "@opentelemetry/sdk-trace-base": 1.15.2 + semver: ^7.5.1 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 67ffa3c9c40a4571ee1aadeec3623bb9bbf981593b5d68b157250b9a4a6a544605b4c68107b5cffb8bb81775868b9d5fee23ccec643f79d773d800ac8d4c155b + languageName: node + linkType: hard + +"@opentelemetry/semantic-conventions@npm:1.15.2, @opentelemetry/semantic-conventions@npm:^1.0.0": + version: 1.15.2 + resolution: "@opentelemetry/semantic-conventions@npm:1.15.2" + checksum: 6de4f8ffa277af18351dff19b821f04438bd4f3917f84816f0bf1577a810424d11ba5f15dca9739a17812a996eeb251048fc7d61b0eef9dc39beb9d4304f57e2 + languageName: node + linkType: hard + "@panva/hkdf@npm:^1.0.2, @panva/hkdf@npm:^1.0.4": version: 1.1.1 resolution: "@panva/hkdf@npm:1.1.1" @@ -8148,7 +8694,7 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:^1.37.1": +"@playwright/test@npm:^1.37.0, @playwright/test@npm:^1.37.1": version: 1.37.1 resolution: "@playwright/test@npm:1.37.1" dependencies: @@ -8252,6 +8798,17 @@ __metadata: languageName: node linkType: hard +"@prisma/instrumentation@npm:^5.0.0": + version: 5.2.0 + resolution: "@prisma/instrumentation@npm:5.2.0" + dependencies: + "@opentelemetry/api": 1.4.1 + "@opentelemetry/instrumentation": 0.41.2 + "@opentelemetry/sdk-trace-base": 1.15.2 + checksum: 35010602a8b5fa096f029b6fd08ed682e1876f5aaa54b5294dad417555285b3351a6f78cf662e9c8b844225ea51cbc6c26094617a5dff059a7ab28012fc84c35 + languageName: node + linkType: hard + "@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": version: 1.1.2 resolution: "@protobufjs/aspromise@npm:1.1.2" @@ -9662,6 +10219,33 @@ __metadata: languageName: node linkType: hard +"@sinonjs/commons@npm:^2.0.0": + version: 2.0.0 + resolution: "@sinonjs/commons@npm:2.0.0" + dependencies: + type-detect: 4.0.8 + checksum: 5023ba17edf2b85ed58262313b8e9b59e23c6860681a9af0200f239fe939e2b79736d04a260e8270ddd57196851dde3ba754d7230be5c5234e777ae2ca8af137 + languageName: node + linkType: hard + +"@sinonjs/commons@npm:^3.0.0": + version: 3.0.0 + resolution: "@sinonjs/commons@npm:3.0.0" + dependencies: + type-detect: 4.0.8 + checksum: b4b5b73d4df4560fb8c0c7b38c7ad4aeabedd362f3373859d804c988c725889cde33550e4bcc7cd316a30f5152a2d1d43db71b6d0c38f5feef71fd8d016763f8 + languageName: node + linkType: hard + +"@sinonjs/fake-timers@npm:^10.0.2, @sinonjs/fake-timers@npm:^10.3.0": + version: 10.3.0 + resolution: "@sinonjs/fake-timers@npm:10.3.0" + dependencies: + "@sinonjs/commons": ^3.0.0 + checksum: 614d30cb4d5201550c940945d44c9e0b6d64a888ff2cd5b357f95ad6721070d6b8839cd10e15b76bf5e14af0bcc1d8f9ec00d49a46318f1f669a4bec1d7f3148 + languageName: node + linkType: hard + "@sinonjs/fake-timers@npm:^9.1.2": version: 9.1.2 resolution: "@sinonjs/fake-timers@npm:9.1.2" @@ -9671,6 +10255,24 @@ __metadata: languageName: node linkType: hard +"@sinonjs/samsam@npm:^8.0.0": + version: 8.0.0 + resolution: "@sinonjs/samsam@npm:8.0.0" + dependencies: + "@sinonjs/commons": ^2.0.0 + lodash.get: ^4.4.2 + type-detect: ^4.0.8 + checksum: 95e40d0bb9f7288e27c379bee1b03c3dc51e7e78b9d5ea6aef66a690da7e81efc4715145b561b449cefc5361a171791e3ce30fb1a46ab247d4c0766024c60a60 + languageName: node + linkType: hard + +"@sinonjs/text-encoding@npm:^0.7.1": + version: 0.7.2 + resolution: "@sinonjs/text-encoding@npm:0.7.2" + checksum: fe690002a32ba06906cf87e2e8fe84d1590294586f2a7fd180a65355b53660c155c3273d8011a5f2b77209b819aa7306678ae6e4aea0df014bd7ffd4bbbcf1ab + languageName: node + linkType: hard + "@smithy/abort-controller@npm:^2.0.5": version: 2.0.5 resolution: "@smithy/abort-controller@npm:2.0.5" @@ -10194,6 +10796,26 @@ __metadata: languageName: node linkType: hard +"@socket.io/component-emitter@npm:~3.1.0": + version: 3.1.0 + resolution: "@socket.io/component-emitter@npm:3.1.0" + checksum: db069d95425b419de1514dffe945cc439795f6a8ef5b9465715acf5b8b50798e2c91b8719cbf5434b3fe7de179d6cdcd503c277b7871cb3dd03febb69bdd50fa + languageName: node + linkType: hard + +"@socket.io/redis-adapter@npm:^8.2.1": + version: 8.2.1 + resolution: "@socket.io/redis-adapter@npm:8.2.1" + dependencies: + debug: ~4.3.1 + notepack.io: ~3.0.1 + uid2: 1.0.0 + peerDependencies: + socket.io-adapter: ^2.4.0 + checksum: f0fc24a537b075ed6340a4adbd12631e3b55e867b8aa9203ed1cd5a28a4c97dc0dc3e65c00bbbe13ee6dc3bae382b6d9ed9ac44d4eb156f23965ee0e0ac9e6fd + languageName: node + linkType: hard + "@spacingbat3/lss@npm:^1.0.0": version: 1.2.0 resolution: "@spacingbat3/lss@npm:1.2.0" @@ -11868,6 +12490,16 @@ __metadata: languageName: node linkType: hard +"@tomfreudenberg/next-auth-mock@npm:^0.5.6": + version: 0.5.6 + resolution: "@tomfreudenberg/next-auth-mock@npm:0.5.6" + peerDependencies: + next-auth: ^4.12.3 + react: ^18 + checksum: ed87ad78e9f74f6c4224769cfb8ec643d8645447761970f3e7b9b80143ba4e2ef3991a995dfbfb06a028f369e644762cdbdd9ef2d013be35d4a131fb617cd8bd + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -12079,6 +12711,15 @@ __metadata: languageName: node linkType: hard +"@types/cors@npm:^2.8.12": + version: 2.8.13 + resolution: "@types/cors@npm:2.8.13" + dependencies: + "@types/node": "*" + checksum: 7ef197ea19d2e5bf1313b8416baa6f3fd6dd887fd70191da1f804f557395357dafd8bc8bed0ac60686923406489262a7c8a525b55748f7b2b8afa686700de907 + languageName: node + linkType: hard + "@types/cross-spawn@npm:^6.0.2": version: 6.0.2 resolution: "@types/cross-spawn@npm:6.0.2" @@ -12281,6 +12922,15 @@ __metadata: languageName: node linkType: hard +"@types/ioredis4@npm:@types/ioredis@^4.28.10": + version: 4.28.10 + resolution: "@types/ioredis@npm:4.28.10" + dependencies: + "@types/node": "*" + checksum: 0f2788cf25f490d3b345db8c5f8b8ce3f6c92cc99abcf744c8f974f02b9b3875233b3d22098614c462a0d6c41c523bd655509418ea88eb6249db6652290ce7cf + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.4 resolution: "@types/istanbul-lib-coverage@npm:2.0.4" @@ -12491,6 +13141,13 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:>=10.0.0, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0": + version: 20.5.7 + resolution: "@types/node@npm:20.5.7" + checksum: fc284c8e16ddc04569730d58e87eae349eb1c3dd9020cb79a1862d9d9add6f04e7367a236f3252db8db2572f90278e250f4cd43d27d264972b54394eaba1ed76 + languageName: node + linkType: hard + "@types/node@npm:^16.0.0": version: 16.18.46 resolution: "@types/node@npm:16.18.46" @@ -12528,6 +13185,15 @@ __metadata: languageName: node linkType: hard +"@types/on-headers@npm:^1.0.0": + version: 1.0.0 + resolution: "@types/on-headers@npm:1.0.0" + dependencies: + "@types/node": "*" + checksum: 470c26ccfe2430118a21a331a13792a5c02201478b5b5cfea375e9a0097a38eedbc7d33cb5d4c622dffa3afb87fc8c370b2597ca932e6479773f5d50ed04a52c + languageName: node + linkType: hard + "@types/parse-json@npm:^4.0.0": version: 4.0.0 resolution: "@types/parse-json@npm:4.0.0" @@ -12558,6 +13224,13 @@ __metadata: languageName: node linkType: hard +"@types/pretty-time@npm:^1.1.2": + version: 1.1.2 + resolution: "@types/pretty-time@npm:1.1.2" + checksum: e161c981f88f831b0966c7709f722a20d606d34c4f0ce566efa8b5b780a2738af41a03fcc1e78e6f1665edf7ebf261bb9b5ca11088c2c91fa1235429d73305ab + languageName: node + linkType: hard + "@types/prop-types@npm:*, @types/prop-types@npm:^15.7.5": version: 15.7.5 resolution: "@types/prop-types@npm:15.7.5" @@ -12714,6 +13387,29 @@ __metadata: languageName: node linkType: hard +"@types/shimmer@npm:^1.0.2": + version: 1.0.2 + resolution: "@types/shimmer@npm:1.0.2" + checksum: 952b5555e914f632f3312a7b54e2b720b12ddce7373e1da58d25f25f32c07e5513afeb53bfce4256035a1205bbe81223fe8e47c160163de3e56fb93a3e0da42b + languageName: node + linkType: hard + +"@types/sinon@npm:^10.0.15": + version: 10.0.16 + resolution: "@types/sinon@npm:10.0.16" + dependencies: + "@types/sinonjs__fake-timers": "*" + checksum: 1216aac584500d6bf845ca76f57e82f8459cf9de4ed80a55e50aa4438360fc418789a42181e211c5d279e97f86a3a994e3c81e43971d540737caca0193242bbf + languageName: node + linkType: hard + +"@types/sinonjs__fake-timers@npm:*": + version: 8.1.2 + resolution: "@types/sinonjs__fake-timers@npm:8.1.2" + checksum: bbc73a5ab6c0ec974929392f3d6e1e8db4ebad97ec506d785301e1c3d8a4f98a35b1aa95b97035daef02886fd8efd7788a2fa3ced2ec7105988bfd8dce61eedd + languageName: node + linkType: hard + "@types/sockjs@npm:^0.3.33": version: 0.3.33 resolution: "@types/sockjs@npm:0.3.33" @@ -14071,6 +14767,13 @@ __metadata: languageName: node linkType: hard +"ansi-color@npm:^0.2.1": + version: 0.2.1 + resolution: "ansi-color@npm:0.2.1" + checksum: f3b809a91db1b2ec869a3bf5c0af13a4a8fa971d69a3404852b09d27e7789e1ca885ecd61d7c36f446d9c9f04980393ee099f9d02223d588a0dae19be033c4f3 + languageName: node + linkType: hard + "ansi-colors@npm:^4.1.1, ansi-colors@npm:^4.1.3": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" @@ -14373,6 +15076,13 @@ __metadata: languageName: node linkType: hard +"arrify@npm:^2.0.0": + version: 2.0.1 + resolution: "arrify@npm:2.0.1" + checksum: 067c4c1afd182806a82e4c1cb8acee16ab8b5284fbca1ce29408e6e91281c36bb5b612f6ddfbd40a0f7a7e0c75bf2696eb94c027f6e328d6e9c52465c98e4209 + languageName: node + linkType: hard + "as-table@npm:^1.0.36": version: 1.0.55 resolution: "as-table@npm:1.0.55" @@ -14876,13 +15586,20 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": +"base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 languageName: node linkType: hard +"base64id@npm:2.0.0, base64id@npm:~2.0.0": + version: 2.0.0 + resolution: "base64id@npm:2.0.0" + checksum: 581b1d37e6cf3738b7ccdd4d14fe2bfc5c238e696e2720ee6c44c183b838655842e22034e53ffd783f872a539915c51b0d4728a49c7cc678ac5a758e00d62168 + languageName: node + linkType: hard + "batch@npm:0.6.1": version: 0.6.1 resolution: "batch@npm:0.6.1" @@ -14931,6 +15648,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:^9.0.0": + version: 9.1.2 + resolution: "bignumber.js@npm:9.1.2" + checksum: 582c03af77ec9cb0ebd682a373ee6c66475db94a4325f92299621d544aa4bd45cb45fd60001610e94aef8ae98a0905fa538241d9638d4422d57abbeeac6fadaf + languageName: node + linkType: hard + "binary-extensions@npm:^2.0.0, binary-extensions@npm:^2.2.0": version: 2.2.0 resolution: "binary-extensions@npm:2.2.0" @@ -14954,6 +15678,13 @@ __metadata: languageName: node linkType: hard +"bintrees@npm:1.0.2": + version: 1.0.2 + resolution: "bintrees@npm:1.0.2" + checksum: 56a52b7d3634e30002b1eda740d2517a22fa8e9e2eb088e919f37c030a0ed86e364ab59e472fc770fc8751308054bb1c892979d150e11d9e11ac33bcc1b5d16e + languageName: node + linkType: hard + "bl@npm:^4.0.3, bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -15151,6 +15882,13 @@ __metadata: languageName: node linkType: hard +"buffer-equal-constant-time@npm:1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 80bb945f5d782a56f374b292770901065bad21420e34936ecbe949e57724b4a13874f735850dd1cc61f078773c4fb5493a41391e7bda40d1fa388d6bd80daaab + languageName: node + linkType: hard + "buffer-equal@npm:^1.0.0": version: 1.0.1 resolution: "buffer-equal@npm:1.0.1" @@ -15192,6 +15930,18 @@ __metadata: languageName: node linkType: hard +"bufrw@npm:^1.3.0": + version: 1.3.0 + resolution: "bufrw@npm:1.3.0" + dependencies: + ansi-color: ^0.2.1 + error: ^7.0.0 + hexer: ^1.5.0 + xtend: ^4.0.0 + checksum: e0cdfae2d1f4c0a2ffdc4e352ce3dbd547c4683c76072d48b98322945c318cbb0b6c2ccb5719d7de14abbe2076d68796f7d905b9b2c859fa29259fe66894b6c6 + languageName: node + linkType: hard + "builder-util-runtime@npm:9.2.1": version: 9.2.1 resolution: "builder-util-runtime@npm:9.2.1" @@ -15624,6 +16374,13 @@ __metadata: languageName: node linkType: hard +"check-password-strength@npm:^2.0.7": + version: 2.0.7 + resolution: "check-password-strength@npm:2.0.7" + checksum: 0d859f6e434a86efc66f4dbbeb28b8be7c0e929726b80f68d3148bd410984857b411e4f980ba25a629a6f581de9bff3e1ad2d280af45c28f619a7457c65af1ba + languageName: node + linkType: hard + "cheerio-select@npm:^2.1.0": version: 2.1.0 resolution: "cheerio-select@npm:2.1.0" @@ -15719,7 +16476,7 @@ __metadata: languageName: node linkType: hard -"cjs-module-lexer@npm:^1.0.0": +"cjs-module-lexer@npm:^1.0.0, cjs-module-lexer@npm:^1.2.2": version: 1.2.3 resolution: "cjs-module-lexer@npm:1.2.3" checksum: 5ea3cb867a9bb609b6d476cd86590d105f3cfd6514db38ff71f63992ab40939c2feb68967faa15a6d2b1f90daa6416b79ea2de486e9e2485a6f8b66a21b4fb0a @@ -15947,6 +16704,13 @@ __metadata: languageName: node linkType: hard +"cluster-key-slot@npm:^1.1.0": + version: 1.1.2 + resolution: "cluster-key-slot@npm:1.1.2" + checksum: be0ad2d262502adc998597e83f9ded1b80f827f0452127c5a37b22dfca36bab8edf393f7b25bb626006fb9fb2436106939ede6d2d6ecf4229b96a47f27edd681 + languageName: node + linkType: hard + "cmdk@npm:^0.2.0": version: 0.2.0 resolution: "cmdk@npm:0.2.0" @@ -16396,7 +17160,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:^0.4.1, cookie@npm:^0.4.2": +"cookie@npm:^0.4.1, cookie@npm:^0.4.2, cookie@npm:~0.4.1": version: 0.4.2 resolution: "cookie@npm:0.4.2" checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b @@ -16449,7 +17213,7 @@ __metadata: languageName: node linkType: hard -"cors@npm:2.8.5, cors@npm:^2.8.5": +"cors@npm:2.8.5, cors@npm:^2.8.5, cors@npm:~2.8.5": version: 2.8.5 resolution: "cors@npm:2.8.5" dependencies: @@ -16883,7 +17647,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -17106,6 +17870,13 @@ __metadata: languageName: node linkType: hard +"denque@npm:^2.1.0": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: 1d4ae1d05e59ac3a3481e7b478293f4b4c813819342273f3d5b826c7ffa9753c520919ba264f377e09108d24ec6cf0ec0ac729a5686cbb8f32d797126c5dae74 + languageName: node + linkType: hard + "depd@npm:2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -17450,6 +18221,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^5.1.0": + version: 5.1.0 + resolution: "diff@npm:5.1.0" + checksum: c7bf0df7c9bfbe1cf8a678fd1b2137c4fb11be117a67bc18a0e03ae75105e8533dbfb1cda6b46beb3586ef5aed22143ef9d70713977d5fb1f9114e21455fba90 + languageName: node + linkType: hard + "diffable-html@npm:^4.1.0": version: 4.1.0 resolution: "diffable-html@npm:4.1.0" @@ -17769,6 +18547,15 @@ __metadata: languageName: node linkType: hard +"ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: ^5.0.1 + checksum: 207f9ab1c2669b8e65540bce29506134613dd5f122cccf1e6a560f4d63f2732d427d938f8481df175505aad94583bcb32c688737bb39a6df0625f903d6d93c03 + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -18021,6 +18808,44 @@ __metadata: languageName: node linkType: hard +"engine.io-client@npm:~6.5.2": + version: 6.5.2 + resolution: "engine.io-client@npm:6.5.2" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.1 + engine.io-parser: ~5.2.1 + ws: ~8.11.0 + xmlhttprequest-ssl: ~2.0.0 + checksum: f93e09b842535a3f3e31b708cd30085b9e08a7a7ebf28f453e50e79e3fccf3f019474a46b41f7dc9164e3b8342c0b5d5a50a45299c1e2465d708c65140d05c92 + languageName: node + linkType: hard + +"engine.io-parser@npm:~5.2.1": + version: 5.2.1 + resolution: "engine.io-parser@npm:5.2.1" + checksum: 55b0e8e18500f50c1573675c53597c5552554ead08d3f30ff19fde6409e48f882a8e01f84e9772cd155c18a1d653d06f6bf57b4e1f8b834c63c9eaf3b657b88e + languageName: node + linkType: hard + +"engine.io@npm:~6.5.2": + version: 6.5.2 + resolution: "engine.io@npm:6.5.2" + dependencies: + "@types/cookie": ^0.4.1 + "@types/cors": ^2.8.12 + "@types/node": ">=10.0.0" + accepts: ~1.3.4 + base64id: 2.0.0 + cookie: ~0.4.1 + cors: ~2.8.5 + debug: ~4.3.1 + engine.io-parser: ~5.2.1 + ws: ~8.11.0 + checksum: 2fb1da39932d526cd5033c399978c65d367cce51e6b511a572bcf4a520b863652e26123d7efceee6cef8c96221585eb953a0e541ae25f6009e9ff5149b067ecd + languageName: node + linkType: hard + "enhanced-resolve@npm:^5.15.0, enhanced-resolve@npm:^5.8.3": version: 5.15.0 resolution: "enhanced-resolve@npm:5.15.0" @@ -18113,6 +18938,25 @@ __metadata: languageName: node linkType: hard +"error@npm:7.0.2": + version: 7.0.2 + resolution: "error@npm:7.0.2" + dependencies: + string-template: ~0.2.1 + xtend: ~4.0.0 + checksum: 407ff5faa73f5da3424a81d0160a1d3c6b5144e87cb1266334e7a4c2c7a69ae653e1b544032d7dbd8b210006858eea909ea0f46694b0484cd7555ba3086be0a8 + languageName: node + linkType: hard + +"error@npm:^7.0.0": + version: 7.2.1 + resolution: "error@npm:7.2.1" + dependencies: + string-template: ~0.2.1 + checksum: 9c790d20a386947acfeabb0d1c39173efe8e5a38cb732b5f06c11a25c23ce8ac4dafbb7aa240565e034580a49aba0703e743d0274c6228500ddf947a1b998568 + languageName: node + linkType: hard + "es-iterator-helpers@npm:@nolyfill/es-iterator-helpers@latest": version: 1.0.19 resolution: "@nolyfill/es-iterator-helpers@npm:1.0.19" @@ -19132,7 +19976,7 @@ __metadata: languageName: node linkType: hard -"extend@npm:^3.0.0": +"extend@npm:^3.0.0, extend@npm:^3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" checksum: a50a8309ca65ea5d426382ff09f33586527882cf532931cb08ca786ea3146c0553310bda688710ff61d7668eba9f96b923fe1420cdf56a2c3eaf30fcab87b515 @@ -19287,6 +20131,13 @@ __metadata: languageName: node linkType: hard +"fast-text-encoding@npm:^1.0.0": + version: 1.0.6 + resolution: "fast-text-encoding@npm:1.0.6" + checksum: 9d58f694314b3283e785bf61954902536da228607ad246905e30256f9ab8331f780ac987e7222c9f5eafd04168d07e12b8054c85cedb76a2c05af0e82387a903 + languageName: node + linkType: hard + "fast-url-parser@npm:1.1.3, fast-url-parser@npm:^1.1.3": version: 1.1.3 resolution: "fast-url-parser@npm:1.1.3" @@ -19454,6 +20305,17 @@ __metadata: languageName: node linkType: hard +"file-type@npm:^18.5.0": + version: 18.5.0 + resolution: "file-type@npm:18.5.0" + dependencies: + readable-web-to-node-stream: ^3.0.2 + strtok3: ^7.0.0 + token-types: ^5.0.1 + checksum: d2bc81d842b110970a0ca9d90356ce4e9738c1c05596ce8931f2af334477856d92bcecd0742dc6646e13a970c0125150ad4415898688d1901d80e972d90ab1ca + languageName: node + linkType: hard + "file-uri-to-path@npm:1.0.0": version: 1.0.0 resolution: "file-uri-to-path@npm:1.0.0" @@ -20049,6 +20911,51 @@ __metadata: languageName: node linkType: hard +"gaxios@npm:^4.0.0": + version: 4.3.3 + resolution: "gaxios@npm:4.3.3" + dependencies: + abort-controller: ^3.0.0 + extend: ^3.0.2 + https-proxy-agent: ^5.0.0 + is-stream: ^2.0.0 + node-fetch: ^2.6.7 + checksum: 0b72a00875404e2c3d7aca9f32535e931d7b0ebb850dc92fafc1685b99a109b04205c63e4637a2d0d9a261ac50adf83f7d33435f73e256dcca32564ef9358fee + languageName: node + linkType: hard + +"gaxios@npm:^5.0.0": + version: 5.1.3 + resolution: "gaxios@npm:5.1.3" + dependencies: + extend: ^3.0.2 + https-proxy-agent: ^5.0.0 + is-stream: ^2.0.0 + node-fetch: ^2.6.9 + checksum: 1cf72697715c64f6db1d6fa6e9243bb57ee14b0c758338a33790ecac2675d819a1fc0c51b2fab312d9bfe8201cc981c171b70ff60adcaaec881c5bc5610c42f1 + languageName: node + linkType: hard + +"gcp-metadata@npm:^4.2.0": + version: 4.3.1 + resolution: "gcp-metadata@npm:4.3.1" + dependencies: + gaxios: ^4.0.0 + json-bigint: ^1.0.0 + checksum: b0b1b85ea2efee1d640a1d4ead0937fdcceffd43ab4cacfdd66fd086fcfe5c3d09ad850ee14f43f2dc73244b2617b166adfa09a2a85e0652a8c56bed194f01fe + languageName: node + linkType: hard + +"gcp-metadata@npm:^5.0.1": + version: 5.3.0 + resolution: "gcp-metadata@npm:5.3.0" + dependencies: + gaxios: ^5.0.0 + json-bigint: ^1.0.0 + checksum: 891ea0b902a17f33d7bae753830d23962b63af94ed071092c30496e7d26f8128ba9af43c3d38474bea29cb32a884b4bcb5720ce8b9de4a7e1108475d3d7ae219 + languageName: node + linkType: hard + "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -20205,6 +21112,13 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^7.0.1": + version: 7.0.1 + resolution: "get-stream@npm:7.0.1" + checksum: 107083c25faf274136a246fa72faea65aa8cea0db54c2dc8c70d3cfe2dcf0d036356927d870dc83fccea8fa32f183ce3696a04eca9617f3e19119f87c5fc0807 + languageName: node + linkType: hard + "get-tsconfig@npm:^4.6.2": version: 4.7.0 resolution: "get-tsconfig@npm:4.7.0" @@ -20511,6 +21425,68 @@ __metadata: languageName: node linkType: hard +"google-auth-library@npm:^7.0.0, google-auth-library@npm:^7.0.2, google-auth-library@npm:^7.14.0": + version: 7.14.1 + resolution: "google-auth-library@npm:7.14.1" + dependencies: + arrify: ^2.0.0 + base64-js: ^1.3.0 + ecdsa-sig-formatter: ^1.0.11 + fast-text-encoding: ^1.0.0 + gaxios: ^4.0.0 + gcp-metadata: ^4.2.0 + gtoken: ^5.0.4 + jws: ^4.0.0 + lru-cache: ^6.0.0 + checksum: 78376eb2d424151dea7e3e162e20e06c11f0133451dd5ee3ea759dcb514d946acf2e0238ff08f002bdb3bf9d43c619793939feff9bc1d92025206ee836a641cf + languageName: node + linkType: hard + +"google-p12-pem@npm:^3.1.3": + version: 3.1.4 + resolution: "google-p12-pem@npm:3.1.4" + dependencies: + node-forge: ^1.3.1 + bin: + gp12-pem: build/src/bin/gp12-pem.js + checksum: 72ce13b9536c69f21584cb7477ed4c34674325639a5dac42e8d774b78d367cb196ae7d37a52bba868235205760df353ac678a5e1756a4f4ded82e16d29d6cbb1 + languageName: node + linkType: hard + +"google-proto-files@npm:^3.0.0": + version: 3.0.3 + resolution: "google-proto-files@npm:3.0.3" + dependencies: + protobufjs: ^7.0.0 + walkdir: ^0.4.0 + checksum: 6650768c72c76295c5a9357933f91d3fccafea2511c5e378dd9f43429b896f060a8f019dec2779d60bf11bdccd6f59c40d2195c3315681e4c41416ed02234ead + languageName: node + linkType: hard + +"googleapis-common@npm:^5.0.2": + version: 5.1.0 + resolution: "googleapis-common@npm:5.1.0" + dependencies: + extend: ^3.0.2 + gaxios: ^4.0.0 + google-auth-library: ^7.14.0 + qs: ^6.7.0 + url-template: ^2.0.8 + uuid: ^8.0.0 + checksum: 025daa078583f7bf6e92cf5d6e6ea804dce6ded84b1ead210803596b0b6d1fe9693069083302603173dc0c1f5e1125a501f2553952df60c001113a78ee055009 + languageName: node + linkType: hard + +"googleapis@npm:^97.0.0": + version: 97.0.0 + resolution: "googleapis@npm:97.0.0" + dependencies: + google-auth-library: ^7.0.2 + googleapis-common: ^5.0.2 + checksum: d4511471ccf9d84fcb2af52a636f61cc64be68ce0a688efa48e979aa5dc6e903c1006be97e2d682cac7b0baf7d9bf9df6b3360f49a225bd3b705357cb8c11e7a + languageName: node + linkType: hard + "got@npm:^11.7.0, got@npm:^11.8.5": version: 11.8.6 resolution: "got@npm:11.8.6" @@ -20641,6 +21617,17 @@ __metadata: languageName: node linkType: hard +"gtoken@npm:^5.0.4": + version: 5.3.2 + resolution: "gtoken@npm:5.3.2" + dependencies: + gaxios: ^4.0.0 + google-p12-pem: ^3.1.3 + jws: ^4.0.0 + checksum: 1fd640e98afcb3d5c77026fd4ff0671dce724acad11169e5b63701a853e1f5a03f4c76fe6eb95500db80f8444753ce212701d396186ef006088d08be4174f2d7 + languageName: node + linkType: hard + "gunzip-maybe@npm:^1.4.2": version: 1.4.2 resolution: "gunzip-maybe@npm:1.4.2" @@ -20803,6 +21790,20 @@ __metadata: languageName: node linkType: hard +"hexer@npm:^1.5.0": + version: 1.5.0 + resolution: "hexer@npm:1.5.0" + dependencies: + ansi-color: ^0.2.1 + minimist: ^1.1.0 + process: ^0.10.0 + xtend: ^4.0.0 + bin: + hexer: ./cli.js + checksum: 2e7a919da953ae7bc8ee3d88b01615fd640a71f65cfaa8e7f0775f44ebbd06fe9d3b901a582c155a518537282dd231f5ca2f8523a5e7b84defc0b07f16854c22 + languageName: node + linkType: hard + "hexoid@npm:^1.0.0": version: 1.0.0 resolution: "hexoid@npm:1.0.0" @@ -21297,6 +22298,18 @@ __metadata: languageName: node linkType: hard +"import-in-the-middle@npm:1.4.2": + version: 1.4.2 + resolution: "import-in-the-middle@npm:1.4.2" + dependencies: + acorn: ^8.8.2 + acorn-import-assertions: ^1.9.0 + cjs-module-lexer: ^1.2.2 + module-details-from-path: ^1.0.3 + checksum: 52971f821e9a3c94834cd5cf0ab5178321c07d4f4babd547b3cb24c4de21670d05b42ca1523890e7e90525c3bba6b7db7e54cf45421919b0b2712a34faa96ea5 + languageName: node + linkType: hard + "import-lazy@npm:~4.0.0": version: 4.0.0 resolution: "import-lazy@npm:4.0.0" @@ -21437,6 +22450,23 @@ __metadata: languageName: node linkType: hard +"ioredis@npm:^5.3.2": + version: 5.3.2 + resolution: "ioredis@npm:5.3.2" + dependencies: + "@ioredis/commands": ^1.1.1 + cluster-key-slot: ^1.1.0 + debug: ^4.3.4 + denque: ^2.1.0 + lodash.defaults: ^4.2.0 + lodash.isarguments: ^3.1.0 + redis-errors: ^1.2.0 + redis-parser: ^3.0.0 + standard-as-callback: ^2.1.0 + checksum: 9a23559133e862a768778301efb68ae8c2af3c33562174b54a4c2d6574b976e85c75a4c34857991af733e35c48faf4c356e7daa8fb0a3543d85ff1768c8754bc + languageName: node + linkType: hard + "ip@npm:^2.0.0": version: 2.0.0 resolution: "ip@npm:2.0.0" @@ -22070,6 +23100,19 @@ __metadata: languageName: node linkType: hard +"jaeger-client@npm:^3.15.0": + version: 3.19.0 + resolution: "jaeger-client@npm:3.19.0" + dependencies: + node-int64: ^0.4.0 + opentracing: ^0.14.4 + thriftrw: ^3.5.0 + uuid: ^8.3.2 + xorshift: ^1.1.1 + checksum: fcdc0523b70299c0db1c07e6c209fa170cef75aa3ad00e6241c906423ba07dc6112a60e1b96de59c2a8eb9d335bf0a0c2a23cc13595ef24aededca2fdff65837 + languageName: node + linkType: hard + "jake@npm:^10.8.5": version: 10.8.7 resolution: "jake@npm:10.8.7" @@ -22946,6 +23989,15 @@ __metadata: languageName: node linkType: hard +"json-bigint@npm:^1.0.0": + version: 1.0.0 + resolution: "json-bigint@npm:1.0.0" + dependencies: + bignumber.js: ^9.0.0 + checksum: c67bb93ccb3c291e60eb4b62931403e378906aab113ec1c2a8dd0f9a7f065ad6fd9713d627b732abefae2e244ac9ce1721c7a3142b2979532f12b258634ce6f6 + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -23130,6 +24182,34 @@ __metadata: languageName: node linkType: hard +"just-extend@npm:^4.0.2": + version: 4.2.1 + resolution: "just-extend@npm:4.2.1" + checksum: ff9fdede240fad313efeeeb68a660b942e5586d99c0058064c78884894a2690dc09bba44c994ad4e077e45d913fef01a9240c14a72c657b53687ac58de53b39c + languageName: node + linkType: hard + +"jwa@npm:^2.0.0": + version: 2.0.0 + resolution: "jwa@npm:2.0.0" + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: ^5.0.1 + checksum: 8f00b71ad5fe94cb55006d0d19202f8f56889109caada2f7eeb63ca81755769ce87f4f48101967f398462e3b8ae4faebfbd5a0269cb755dead5d63c77ba4d2f1 + languageName: node + linkType: hard + +"jws@npm:^4.0.0": + version: 4.0.0 + resolution: "jws@npm:4.0.0" + dependencies: + jwa: ^2.0.0 + safe-buffer: ^5.0.1 + checksum: d68d07aa6d1b8cb35c363a9bd2b48f15064d342a5d9dc18a250dbbce8dc06bd7e4792516c50baa16b8d14f61167c19e851fd7f66b59ecc68b7f6a013759765f7 + languageName: node + linkType: hard + "keyv@npm:^4.0.0": version: 4.5.3 resolution: "keyv@npm:4.5.3" @@ -23777,6 +24857,13 @@ __metadata: languageName: node linkType: hard +"lodash.defaults@npm:^4.2.0": + version: 4.2.0 + resolution: "lodash.defaults@npm:4.2.0" + checksum: 84923258235592c8886e29de5491946ff8c2ae5c82a7ac5cddd2e3cb697e6fbdfbbb6efcca015795c86eec2bb953a5a2ee4016e3735a3f02720428a40efbb8f1 + languageName: node + linkType: hard + "lodash.escaperegexp@npm:^4.1.2": version: 4.1.2 resolution: "lodash.escaperegexp@npm:4.1.2" @@ -23798,6 +24885,13 @@ __metadata: languageName: node linkType: hard +"lodash.isarguments@npm:^3.1.0": + version: 3.1.0 + resolution: "lodash.isarguments@npm:3.1.0" + checksum: ae1526f3eb5c61c77944b101b1f655f846ecbedcb9e6b073526eba6890dc0f13f09f72e11ffbf6540b602caee319af9ac363d6cdd6be41f4ee453436f04f13b5 + languageName: node + linkType: hard + "lodash.isequal@npm:^4.5.0": version: 4.5.0 resolution: "lodash.isequal@npm:4.5.0" @@ -23964,6 +25058,13 @@ __metadata: languageName: node linkType: hard +"long@npm:^2.4.0": + version: 2.4.0 + resolution: "long@npm:2.4.0" + checksum: e24fd5e14be90ba6ec3faa43b3b0f1c4ac88bfdc52471d90f63e173572f6db27c45873e847f8af58283ca3140eb42d7ba11708102f3cc0956793b03305c737e0 + languageName: node + linkType: hard + "long@npm:^4.0.0": version: 4.0.0 resolution: "long@npm:4.0.0" @@ -23971,6 +25072,13 @@ __metadata: languageName: node linkType: hard +"long@npm:^5.0.0": + version: 5.2.3 + resolution: "long@npm:5.2.3" + checksum: 885ede7c3de4facccbd2cacc6168bae3a02c3e836159ea4252c87b6e34d40af819824b2d4edce330bfb5c4d6e8ce3ec5864bdcf9473fa1f53a4f8225860e5897 + languageName: node + linkType: hard + "loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -24504,7 +25612,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.25, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.25, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -24682,7 +25790,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.7, minimist@npm:^1.2.8, minimist@npm:~1.2.5": +"minimist@npm:^1.1.0, minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.7, minimist@npm:^1.2.8, minimist@npm:~1.2.5": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 75a6d645fb122dad29c06a7597bddea977258957ed88d7a6df59b5cd3fe4a527e253e9bbf2e783e4b73657f9098b96a5fe96ab8a113655d4109108577ecf85b0 @@ -24889,6 +25997,13 @@ __metadata: languageName: node linkType: hard +"module-details-from-path@npm:^1.0.3": + version: 1.0.3 + resolution: "module-details-from-path@npm:1.0.3" + checksum: 378a8a26013889aa3086bfb0776b7860c5bb957336253e1ba5d779c2f239a218930b145ca76e52c1dd7c8079d52b2af64b8eec30822f81ffdb0dfa27d6fe6f33 + languageName: node + linkType: hard + "module-lookup-amd@npm:^7.0.1": version: 7.0.1 resolution: "module-lookup-amd@npm:7.0.1" @@ -25129,6 +26244,31 @@ __metadata: languageName: node linkType: hard +"next-auth@npm:^4.22.1": + version: 4.23.1 + resolution: "next-auth@npm:4.23.1" + dependencies: + "@babel/runtime": ^7.20.13 + "@panva/hkdf": ^1.0.2 + cookie: ^0.5.0 + jose: ^4.11.4 + oauth: ^0.9.15 + openid-client: ^5.4.0 + preact: ^10.6.3 + preact-render-to-string: ^5.1.19 + uuid: ^8.3.2 + peerDependencies: + next: ^12.2.5 || ^13 + nodemailer: ^6.6.5 + react: ^17.0.2 || ^18 + react-dom: ^17.0.2 || ^18 + peerDependenciesMeta: + nodemailer: + optional: true + checksum: 995114797c257ccf71a82d19fb6316fb7709b552aaaf66444591c505a4b8e00b0cae3f4db4316b63a8cc439076044cc391ab171c4f6ee2e086709c5b3bbfed24 + languageName: node + linkType: hard + "next-themes@npm:^0.2.1": version: 0.2.1 resolution: "next-themes@npm:0.2.1" @@ -25147,6 +26287,19 @@ __metadata: languageName: node linkType: hard +"nise@npm:^5.1.4": + version: 5.1.4 + resolution: "nise@npm:5.1.4" + dependencies: + "@sinonjs/commons": ^2.0.0 + "@sinonjs/fake-timers": ^10.0.2 + "@sinonjs/text-encoding": ^0.7.1 + just-extend: ^4.0.2 + path-to-regexp: ^1.7.0 + checksum: bc57c10eaec28a6a7ddfb2e1e9b21d5e1fe22710e514f8858ae477cf9c7e9c891475674d5241519193403db43d16c3675f4207bc094a7a27b7e4f56584a78c1b + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -25239,7 +26392,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2, node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.2, node-fetch@npm:^2.6.7": +"node-fetch@npm:^2, node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.2, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -25264,7 +26417,7 @@ __metadata: languageName: node linkType: hard -"node-forge@npm:^1": +"node-forge@npm:^1, node-forge@npm:^1.3.1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" checksum: 08fb072d3d670599c89a1704b3e9c649ff1b998256737f0e06fbd1a5bf41cae4457ccaee32d95052d80bbafd9ffe01284e078c8071f0267dc9744e51c5ed42a9 @@ -25461,6 +26614,13 @@ __metadata: languageName: node linkType: hard +"notepack.io@npm:~3.0.1": + version: 3.0.1 + resolution: "notepack.io@npm:3.0.1" + checksum: f2c722f6a26f6ae653f5e58fe0a05704aabd79d103b9f821e8e0404ef72fc0da0631f67d4fb7234fb669e4a2feb312691a0bbaebeb66cd54a14f2ab7f94372a6 + languageName: node + linkType: hard + "npm-run-path@npm:^2.0.0": version: 2.0.2 resolution: "npm-run-path@npm:2.0.2" @@ -25685,6 +26845,13 @@ __metadata: languageName: node linkType: hard +"object-hash@npm:3.0.0, object-hash@npm:^3.0.0": + version: 3.0.0 + resolution: "object-hash@npm:3.0.0" + checksum: 80b4904bb3857c52cc1bfd0b52c0352532ca12ed3b8a6ff06a90cd209dfda1b95cee059a7625eb9da29537027f68ac4619363491eedb2f5d3dddbba97494fd6c + languageName: node + linkType: hard + "object-hash@npm:^2.2.0": version: 2.2.0 resolution: "object-hash@npm:2.2.0" @@ -25692,13 +26859,6 @@ __metadata: languageName: node linkType: hard -"object-hash@npm:^3.0.0": - version: 3.0.0 - resolution: "object-hash@npm:3.0.0" - checksum: 80b4904bb3857c52cc1bfd0b52c0352532ca12ed3b8a6ff06a90cd209dfda1b95cee059a7625eb9da29537027f68ac4619363491eedb2f5d3dddbba97494fd6c - languageName: node - linkType: hard - "object-is@npm:@nolyfill/object-is@latest": version: 1.0.19 resolution: "@nolyfill/object-is@npm:1.0.19" @@ -25792,7 +26952,7 @@ __metadata: languageName: node linkType: hard -"on-headers@npm:~1.0.2": +"on-headers@npm:^1.0.2, on-headers@npm:~1.0.2": version: 1.0.2 resolution: "on-headers@npm:1.0.2" checksum: 2bf13467215d1e540a62a75021e8b318a6cfc5d4fc53af8e8f84ad98dbcea02d506c6d24180cd62e1d769c44721ba542f3154effc1f7579a8288c9f7873ed8e5 @@ -25878,6 +27038,13 @@ __metadata: languageName: node linkType: hard +"opentracing@npm:^0.14.4": + version: 0.14.7 + resolution: "opentracing@npm:0.14.7" + checksum: 5f7e44439062d056a2a72ac89eff463c9cf5659a2aea230ff7f5a226c5e960c195ce04ec2e2cc590140bbb9c5d2be11a5a50a23484cbe2d0e132af4309d4c904 + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.3 resolution: "optionator@npm:0.9.3" @@ -26368,6 +27535,15 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^1.7.0": + version: 1.8.0 + resolution: "path-to-regexp@npm:1.8.0" + dependencies: + isarray: 0.0.1 + checksum: 709f6f083c0552514ef4780cb2e7e4cf49b0cc89a97439f2b7cc69a608982b7690fb5d1720a7473a59806508fc2dae0be751ba49f495ecf89fd8fbc62abccbcd + languageName: node + linkType: hard + "path-to-regexp@npm:^6.2.0": version: 6.2.1 resolution: "path-to-regexp@npm:6.2.1" @@ -26433,6 +27609,13 @@ __metadata: languageName: node linkType: hard +"peek-readable@npm:^5.0.0": + version: 5.0.0 + resolution: "peek-readable@npm:5.0.0" + checksum: bef5ceb50586eb42e14efba274ac57ffe97f0ed272df9239ce029f688f495d9bf74b2886fa27847c706a9db33acda4b7d23bbd09a2d21eb4c2a54da915117414 + languageName: node + linkType: hard + "peek-stream@npm:^1.1.0": version: 1.1.3 resolution: "peek-stream@npm:1.1.3" @@ -27269,6 +28452,13 @@ __metadata: languageName: node linkType: hard +"pretty-time@npm:^1.1.0": + version: 1.1.0 + resolution: "pretty-time@npm:1.1.0" + checksum: a319e7009aadbc6cfedbd8b66861327d3a0c68bd3e8794bf5b86f62b40b01b9479c5a70c76bb368ad454acce52a1216daee460cc825766e2442c04f3a84a02c9 + languageName: node + linkType: hard + "printable-characters@npm:^1.0.42": version: 1.0.42 resolution: "printable-characters@npm:1.0.42" @@ -27326,6 +28516,13 @@ __metadata: languageName: node linkType: hard +"process@npm:^0.10.0": + version: 0.10.1 + resolution: "process@npm:0.10.1" + checksum: bdaaa28a8edf96d5daa0f5c1faf4adfedce512ebca829a82e846d991492780c34eb934decf4fa5b311c698881d07a8d4592b4d7ea53ec03d51580a2f364d3e30 + languageName: node + linkType: hard + "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -27340,6 +28537,15 @@ __metadata: languageName: node linkType: hard +"prom-client@npm:^14.2.0": + version: 14.2.0 + resolution: "prom-client@npm:14.2.0" + dependencies: + tdigest: ^0.1.1 + checksum: d4c04e57616c72643dd02862d0d4bde09cf8869a19d0aef5e7b785e6e27d02439b66cdc165e3492f62d579fa91579183820870cc757a09b99399d2d02f46b9f1 + languageName: node + linkType: hard + "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -27389,6 +28595,26 @@ __metadata: languageName: node linkType: hard +"protobufjs@npm:^7.0.0, protobufjs@npm:^7.2.3, protobufjs@npm:^7.2.4": + version: 7.2.5 + resolution: "protobufjs@npm:7.2.5" + dependencies: + "@protobufjs/aspromise": ^1.1.2 + "@protobufjs/base64": ^1.1.2 + "@protobufjs/codegen": ^2.0.4 + "@protobufjs/eventemitter": ^1.1.0 + "@protobufjs/fetch": ^1.1.0 + "@protobufjs/float": ^1.0.2 + "@protobufjs/inquire": ^1.1.0 + "@protobufjs/path": ^1.1.2 + "@protobufjs/pool": ^1.1.0 + "@protobufjs/utf8": ^1.1.0 + "@types/node": ">=13.7.0" + long: ^5.0.0 + checksum: 3770a072114061faebbb17cfd135bc4e187b66bc6f40cd8bac624368b0270871ec0cfb43a02b9fb4f029c8335808a840f1afba3c2e7ede7063b98ae6b98a703f + languageName: node + linkType: hard + "proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" @@ -27399,6 +28625,13 @@ __metadata: languageName: node linkType: hard +"proxy-compare@npm:2.5.1": + version: 2.5.1 + resolution: "proxy-compare@npm:2.5.1" + checksum: c7cc151ac255150bcb24becde6495b3e399416c31991af377ce082255b51f07eaeb5d861bf8bf482703e92f88b90a5892ad57d3153ea29450d03ef921683d9fa + languageName: node + linkType: hard + "proxy-from-env@npm:^1.0.0, proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" @@ -27519,7 +28752,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.10.0, qs@npm:^6.11.0": +"qs@npm:^6.10.0, qs@npm:^6.11.0, qs@npm:^6.7.0": version: 6.11.2 resolution: "qs@npm:6.11.2" dependencies: @@ -28217,7 +29450,7 @@ __metadata: languageName: node linkType: hard -"readable-web-to-node-stream@npm:^3.0.0": +"readable-web-to-node-stream@npm:^3.0.0, readable-web-to-node-stream@npm:^3.0.2": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2" dependencies: @@ -28290,6 +29523,22 @@ __metadata: languageName: node linkType: hard +"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": + version: 1.2.0 + resolution: "redis-errors@npm:1.2.0" + checksum: f28ac2692113f6f9c222670735aa58aeae413464fd58ccf3fce3f700cae7262606300840c802c64f2b53f19f65993da24dc918afc277e9e33ac1ff09edb394f4 + languageName: node + linkType: hard + +"redis-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "redis-parser@npm:3.0.0" + dependencies: + redis-errors: ^1.0.0 + checksum: 89290ae530332f2ae37577647fa18208d10308a1a6ba750b9d9a093e7398f5e5253f19855b64c98757f7129cccce958e4af2573fdc33bad41405f87f1943459a + languageName: node + linkType: hard + "redux@npm:^4.2.0": version: 4.2.1 resolution: "redux@npm:4.2.1" @@ -28512,6 +29761,17 @@ __metadata: languageName: node linkType: hard +"require-in-the-middle@npm:^7.1.1": + version: 7.2.0 + resolution: "require-in-the-middle@npm:7.2.0" + dependencies: + debug: ^4.1.1 + module-details-from-path: ^1.0.3 + resolve: ^1.22.1 + checksum: 5ed219d12aec4d0f098029827f9e929d8e0ca4f2fe01f23a9b02169e57c5157cced9e7acaef6a871d3f56646f2cb807b08f2f23d66912ee53eca16cb88eff743 + languageName: node + linkType: hard + "require-like@npm:>= 0.1.1": version: 0.1.2 resolution: "require-like@npm:0.1.2" @@ -28642,7 +29902,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.6, resolve@npm:^1.1.7, resolve@npm:^1.10.0, resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.21.0, resolve@npm:^1.22.2, resolve@npm:^1.22.3, resolve@npm:^1.22.4, resolve@npm:~1.22.1": +"resolve@npm:^1.1.6, resolve@npm:^1.1.7, resolve@npm:^1.10.0, resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.21.0, resolve@npm:^1.22.1, resolve@npm:^1.22.2, resolve@npm:^1.22.3, resolve@npm:^1.22.4, resolve@npm:~1.22.1": version: 1.22.4 resolution: "resolve@npm:1.22.4" dependencies: @@ -28678,7 +29938,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.12.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.21.0#~builtin, resolve@patch:resolve@^1.22.2#~builtin, resolve@patch:resolve@^1.22.3#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@~1.22.1#~builtin": +"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.12.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.21.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.22.2#~builtin, resolve@patch:resolve@^1.22.3#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@~1.22.1#~builtin": version: 1.22.4 resolution: "resolve@patch:resolve@npm%3A1.22.4#~builtin::version=1.22.4&hash=c3c19d" dependencies: @@ -29075,7 +30335,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.5.4, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.6, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:~7.5.4": +"semver@npm:7.5.4, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.6, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.1, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:~7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" dependencies: @@ -29345,6 +30605,13 @@ __metadata: languageName: node linkType: hard +"shimmer@npm:^1.2.1": + version: 1.2.1 + resolution: "shimmer@npm:1.2.1" + checksum: aa0d6252ad1c682a4fdfda69e541be987f7a265ac7b00b1208e5e48cc68dc55f293955346ea4c71a169b7324b82c70f8400b3d3d2d60b2a7519f0a3522423250 + languageName: node + linkType: hard + "side-channel@npm:@nolyfill/side-channel@latest": version: 1.0.19 resolution: "@nolyfill/side-channel@npm:1.0.19" @@ -29427,6 +30694,20 @@ __metadata: languageName: node linkType: hard +"sinon@npm:^15.2.0": + version: 15.2.0 + resolution: "sinon@npm:15.2.0" + dependencies: + "@sinonjs/commons": ^3.0.0 + "@sinonjs/fake-timers": ^10.3.0 + "@sinonjs/samsam": ^8.0.0 + diff: ^5.1.0 + nise: ^5.1.4 + supports-color: ^7.2.0 + checksum: 1641b9af8a73ba57c73c9b6fd955a2d062a5d78cce719887869eca45faf33b0fd20cabfeffdfd856bb35bfbd3d49debb2d954ff6ae5e9825a3da5ff4f604ab6c + languageName: node + linkType: hard + "sirv@npm:^2.0.3": version: 2.0.3 resolution: "sirv@npm:2.0.3" @@ -29515,6 +30796,52 @@ __metadata: languageName: node linkType: hard +"socket.io-adapter@npm:~2.5.2": + version: 2.5.2 + resolution: "socket.io-adapter@npm:2.5.2" + dependencies: + ws: ~8.11.0 + checksum: 481251c3547221e57eb5cb247d0b1a3cde4d152a4c1c9051cc887345a7770e59f3b47f1011cac4499e833f01fcfc301ed13c4ec6e72f7dbb48a476375a6344cd + languageName: node + linkType: hard + +"socket.io-client@npm:^4.7.1": + version: 4.7.2 + resolution: "socket.io-client@npm:4.7.2" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.2 + engine.io-client: ~6.5.2 + socket.io-parser: ~4.2.4 + checksum: 8f0ab6b623e014d889bae0cd847ef7826658e8f131bd9367ee5ae4404bb52a6d7b1755b8fbe8e68799b60e92149370a732b381f913b155e40094facb135cd088 + languageName: node + linkType: hard + +"socket.io-parser@npm:~4.2.4": + version: 4.2.4 + resolution: "socket.io-parser@npm:4.2.4" + dependencies: + "@socket.io/component-emitter": ~3.1.0 + debug: ~4.3.1 + checksum: 61540ef99af33e6a562b9effe0fad769bcb7ec6a301aba5a64b3a8bccb611a0abdbe25f469933ab80072582006a78ca136bf0ad8adff9c77c9953581285e2263 + languageName: node + linkType: hard + +"socket.io@npm:4.7.2, socket.io@npm:^4.7.1": + version: 4.7.2 + resolution: "socket.io@npm:4.7.2" + dependencies: + accepts: ~1.3.4 + base64id: ~2.0.0 + cors: ~2.8.5 + debug: ~4.3.2 + engine.io: ~6.5.2 + socket.io-adapter: ~2.5.2 + socket.io-parser: ~4.2.4 + checksum: 2dfac8983a75e100e889c3dafc83b21b75a9863d0d1ee79cdc60c4391d5d9dffcf3a86fc8deca7568032bc11c2572676335fd2e469c7982f40d19f1141d4b266 + languageName: node + linkType: hard + "sockjs@npm:^0.3.24": version: 0.3.24 resolution: "sockjs@npm:0.3.24" @@ -29823,6 +31150,13 @@ __metadata: languageName: node linkType: hard +"standard-as-callback@npm:^2.1.0": + version: 2.1.0 + resolution: "standard-as-callback@npm:2.1.0" + checksum: 88bec83ee220687c72d94fd86a98d5272c91d37ec64b66d830dbc0d79b62bfa6e47f53b71646011835fc9ce7fae62739545d13124262b53be4fbb3e2ebad551c + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -30010,6 +31344,13 @@ __metadata: languageName: node linkType: hard +"string-template@npm:~0.2.1": + version: 0.2.1 + resolution: "string-template@npm:0.2.1" + checksum: 042cdcf4d4832378f12fbf45b42f479990f330cc409e6dc184838801efbc8352ccf9428fe169f8f8cfff2b864879d4ba1ef8b5f41d63d1d71844c48005a1683f + languageName: node + linkType: hard + "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -30201,6 +31542,16 @@ __metadata: languageName: node linkType: hard +"strtok3@npm:^7.0.0": + version: 7.0.0 + resolution: "strtok3@npm:7.0.0" + dependencies: + "@tokenizer/token": ^0.3.0 + peek-readable: ^5.0.0 + checksum: 2ebe7ad8f2aea611dec6742cf6a42e82764892a362907f7ce493faf334501bf981ce21c828dcc300457e6d460dc9c34d644ededb3b01dcb9e37559203cf1748c + languageName: node + linkType: hard + "style-loader@npm:^3.3.3": version: 3.3.3 resolution: "style-loader@npm:3.3.3" @@ -30327,7 +31678,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0": +"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -30423,6 +31774,18 @@ __metadata: languageName: node linkType: hard +"swr@npm:^2.2.1": + version: 2.2.2 + resolution: "swr@npm:2.2.2" + dependencies: + client-only: ^0.0.1 + use-sync-external-store: ^1.2.0 + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + checksum: afb3d4824f7631bc3a045483f4e4beb5ca9bfa82b64690b3d1133cdf0666ad8e4a9a879657fac5b34dbf19ea9a1503cf92c9bde972f3d40cefe434c73bd9627a + languageName: node + linkType: hard + "symbol-observable@npm:^1.0.4": version: 1.2.0 resolution: "symbol-observable@npm:1.2.0" @@ -30560,6 +31923,15 @@ __metadata: languageName: node linkType: hard +"tdigest@npm:^0.1.1": + version: 0.1.2 + resolution: "tdigest@npm:0.1.2" + dependencies: + bintrees: 1.0.2 + checksum: 44de8246752b6f8c2924685f969fd3d94c36949f22b0907e99bef2b2220726dd8467f4730ea96b06040b9aa2587c0866049640039d1b956952dfa962bc2075a3 + languageName: node + linkType: hard + "telejson@npm:^7.0.3": version: 7.2.0 resolution: "telejson@npm:7.2.0" @@ -30720,6 +32092,19 @@ __metadata: languageName: node linkType: hard +"thriftrw@npm:^3.5.0": + version: 3.12.0 + resolution: "thriftrw@npm:3.12.0" + dependencies: + bufrw: ^1.3.0 + error: 7.0.2 + long: ^2.4.0 + bin: + thrift2json: ./thrift2json.js + checksum: fc03374131a713f5c5eca712a4092abf48282d985d23542f3ef7c92d222af52716e7037c446d5734708cc514d8fad42674f82dd6a9c59b8807beba0a1169b539 + languageName: node + linkType: hard + "through2@npm:^2.0.3": version: 2.0.5 resolution: "through2@npm:2.0.5" @@ -30895,6 +32280,16 @@ __metadata: languageName: node linkType: hard +"token-types@npm:^5.0.1": + version: 5.0.1 + resolution: "token-types@npm:5.0.1" + dependencies: + "@tokenizer/token": ^0.3.0 + ieee754: ^1.2.1 + checksum: 32780123bc6ce8b6a2231d860445c994a02a720abf38df5583ea957aa6626873cd1c4dd8af62314da4cf16ede00c379a765707a3b06f04b8808c38efdae1c785 + languageName: node + linkType: hard + "totalist@npm:^3.0.0": version: 3.0.1 resolution: "totalist@npm:3.0.1" @@ -31089,6 +32484,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2.6.2, tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1 || ^1.9.3, tslib@npm:^2.5.0, tslib@npm:^2.6.0, tslib@npm:^2.6.1": + version: 2.6.2 + resolution: "tslib@npm:2.6.2" + checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad + languageName: node + linkType: hard + "tslib@npm:^1.11.1, tslib@npm:^1.13.0, tslib@npm:^1.8.1, tslib@npm:^1.9.0": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -31096,13 +32498,6 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.4.1 || ^1.9.3, tslib@npm:^2.5.0, tslib@npm:^2.6.0, tslib@npm:^2.6.1": - version: 2.6.2 - resolution: "tslib@npm:2.6.2" - checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad - languageName: node - linkType: hard - "tslib@npm:~2.5.0": version: 2.5.3 resolution: "tslib@npm:2.5.3" @@ -31155,7 +32550,7 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:4.0.8, type-detect@npm:^4.0.0, type-detect@npm:^4.0.5": +"type-detect@npm:4.0.8, type-detect@npm:^4.0.0, type-detect@npm:^4.0.5, type-detect@npm:^4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" checksum: 62b5628bff67c0eb0b66afa371bd73e230399a8d2ad30d852716efcc4656a7516904570cd8631a49a3ce57c10225adf5d0cbdcb47f6b0255fe6557c453925a15 @@ -31379,6 +32774,13 @@ __metadata: languageName: node linkType: hard +"uid2@npm:1.0.0": + version: 1.0.0 + resolution: "uid2@npm:1.0.0" + checksum: 7efad0da3839ef2bebc6fae4bd29905702cd64233b3907e3300aa2d7ea1a00c1ae8c41a5e16ca34ac2db2d25c5607d5989673e1df51a2a076fefbeed51605ec3 + languageName: node + linkType: hard + "uid@npm:2.0.2": version: 2.0.2 resolution: "uid@npm:2.0.2" @@ -31623,6 +33025,13 @@ __metadata: languageName: node linkType: hard +"url-template@npm:^2.0.8": + version: 2.0.8 + resolution: "url-template@npm:2.0.8" + checksum: 4183fccd74e3591e4154134d4443dccecba9c455c15c7df774f1f1e3fa340fd9bffb903b5beec347196d15ce49c34edf6dec0634a95d170ad6e78c0467d6e13e + languageName: node + linkType: hard + "url@npm:0.11.0": version: 0.11.0 resolution: "url@npm:0.11.0" @@ -31725,7 +33134,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.2.0": +"use-sync-external-store@npm:1.2.0, use-sync-external-store@npm:^1.2.0": version: 1.2.0 resolution: "use-sync-external-store@npm:1.2.0" peerDependencies: @@ -31796,7 +33205,7 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^8.3.2": +"uuid@npm:^8.0.0, uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" bin: @@ -31847,6 +33256,24 @@ __metadata: languageName: node linkType: hard +"valtio@npm:^1.10.6": + version: 1.11.2 + resolution: "valtio@npm:1.11.2" + dependencies: + proxy-compare: 2.5.1 + use-sync-external-store: 1.2.0 + peerDependencies: + "@types/react": ">=16.8" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + react: + optional: true + checksum: cce2d9212aac9fc4bdeba2d381188cc831cfe8d2d03039024cfcd58ba1801f2a5b14d01c2bb21a2c9f12046d2ede64f1dd887175185f39bee553677a35592c30 + languageName: node + linkType: hard + "value-or-promise@npm:^1.0.11, value-or-promise@npm:^1.0.12": version: 1.0.12 resolution: "value-or-promise@npm:1.0.12" @@ -32253,7 +33680,7 @@ __metadata: languageName: node linkType: hard -"walkdir@npm:^0.4.1": +"walkdir@npm:^0.4.0, walkdir@npm:^0.4.1": version: 0.4.1 resolution: "walkdir@npm:0.4.1" checksum: 71045c21dc19aae3321f897b6e9e507cf8039202665c35a0b908eecccaf25636aab769b31cbd61ef8267237fe22fc316923a691ecc2d9d38840a15c59c0f2594 @@ -32865,6 +34292,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:~8.11.0": + version: 8.11.0 + resolution: "ws@npm:8.11.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 316b33aba32f317cd217df66dbfc5b281a2f09ff36815de222bc859e3424d83766d9eb2bd4d667de658b6ab7be151f258318fb1da812416b30be13103e5b5c67 + languageName: node + linkType: hard + "xml-name-validator@npm:^4.0.0": version: 4.0.0 resolution: "xml-name-validator@npm:4.0.0" @@ -32886,6 +34328,20 @@ __metadata: languageName: node linkType: hard +"xmlhttprequest-ssl@npm:~2.0.0": + version: 2.0.0 + resolution: "xmlhttprequest-ssl@npm:2.0.0" + checksum: 1e98df67f004fec15754392a131343ea92e6ab5ac4d77e842378c5c4e4fd5b6a9134b169d96842cc19422d77b1606b8df84a5685562b3b698cb68441636f827e + languageName: node + linkType: hard + +"xorshift@npm:^1.1.1": + version: 1.2.0 + resolution: "xorshift@npm:1.2.0" + checksum: bb5575707d20a806e71fa3e80bc3dc083a4bcf3c82965bd27b797a355cf87583273fe7676cfc9c7ffaa4c17d5a171903462045e2154e51fe1f07b90a4dc6b9d2 + languageName: node + linkType: hard + "xss@npm:^1.0.8": version: 1.0.14 resolution: "xss@npm:1.0.14" @@ -32898,7 +34354,7 @@ __metadata: languageName: node linkType: hard -"xtend@npm:^4.0.0, xtend@npm:~4.0.1": +"xtend@npm:^4.0.0, xtend@npm:~4.0.0, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a @@ -33086,7 +34542,7 @@ __metadata: languageName: node linkType: hard -"yjs@npm:^13.6.7": +"yjs@npm:^13.6.6, yjs@npm:^13.6.7": version: 13.6.7 resolution: "yjs@npm:13.6.7" dependencies: